refactoring: expenses landing list.

refactoring: customers landing list.
refactoring: vendors landing list.
refactoring: manual journals landing list.
This commit is contained in:
a.bouhuolia
2021-02-10 18:35:19 +02:00
parent 6e10ed0721
commit c68b4ca9ba
170 changed files with 2835 additions and 4430 deletions

View File

@@ -0,0 +1,120 @@
import React from 'react';
import Icon from 'components/Icon';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import { useManualJournalsContext } from './ManualJournalsListProvider';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { If, DashboardActionViewsList } from 'components';
import withManualJournalsActions from './withManualJournalsActions';
import { compose } from 'utils';
/**
* Manual journal actions bar.
*/
function ManualJournalActionsBar({
// #withManualJournalsActions
setManualJournalsTableState,
}) {
// History context.
const history = useHistory();
// Manual journals context.
const { journalsViews } = useManualJournalsContext();
// Handle click a new manual journal.
const onClickNewManualJournal = () => {
history.push('/make-journal-entry');
};
// Handle delete button click.
const handleBulkDelete = () => {
};
// Handle tab change.
const handleTabChange = (viewId) => {
setManualJournalsTableState({
customViewid: viewId.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'manual-journals'}
views={journalsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'journal_entry'} />}
onClick={onClickNewManualJournal}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': false,
})}
text="Filter"
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withManualJournalsActions,
)(ManualJournalActionsBar);

View File

@@ -0,0 +1,15 @@
import React from 'react';
import JournalDeleteAlert from 'containers/Alerts/ManualJournals/JournalDeleteAlert';
import JournalPublishAlert from 'containers/Alerts/ManualJournals/JournalPublishAlert';
/**
* Manual journals alerts.
*/
export default function ManualJournalsAlerts() {
return (
<div>
<JournalDeleteAlert name={'journal-delete'} />
<JournalPublishAlert name={'journal-publish'} />
</div>
)
}

View File

@@ -0,0 +1,118 @@
import React from 'react';
import classNames from 'classnames';
import { DataTable, Choose } from 'components';
import { CLASSES } from 'common/classes';
import ManualJournalsEmptyStatus from './ManualJournalsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withManualJournalsActions from './withManualJournalsActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import { useManualJournalsContext } from './ManualJournalsListProvider';
import { useManualJournalsColumns } from './utils';
import { ActionsMenu } from './components';
import { compose } from 'utils';
/**
* Manual journals data-table.
*/
function ManualJournalsDataTable({
// #withManualJournalsActions
setManualJournalsTableState,
// #withAlertsActions
openAlert,
// #ownProps
onSelectedRowsChange,
}) {
// Manual journals context.
const {
manualJournals,
pagination,
isManualJournalsLoading,
isManualJournalsFetching,
isEmptyStatus
} = useManualJournalsContext();
// Manual journals columns.
const columns = useManualJournalsColumns();
// Handles the journal publish action.
const handlePublishJournal = ({ id }) => {
openAlert('journal-publish', { manualJournalId: id })
};
// Handle the journal edit action.
const handleEditJournal = ({ id }) => {
};
// Handle the journal delete action.
const handleDeleteJournal = ({ id }) => {
openAlert('journal-delete', { manualJournalId: id });
};
// Handle fetch data once the page index, size or sort by of the table change.
const handleFetchData = React.useCallback(
({ pageSize, pageIndex, sortBy }) => {
setManualJournalsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setManualJournalsTableState],
);
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<Choose>
<Choose.When condition={isEmptyStatus}>
<ManualJournalsEmptyStatus />
</Choose.When>
<Choose.Otherwise>
<DataTable
noInitialFetch={true}
columns={columns}
data={manualJournals}
manualSortBy={true}
selectionColumn={true}
expandable={true}
sticky={true}
loading={isManualJournalsLoading}
headerLoading={isManualJournalsLoading}
progressBarLoading={isManualJournalsFetching}
pagesCount={pagination.pagesCount}
pagination={true}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onFetchData={handleFetchData}
payload={{
onDelete: handleDeleteJournal,
onPublish: handlePublishJournal
}}
/>
</Choose.Otherwise>
</Choose>
</div>
);
}
export default compose(
withManualJournalsActions,
withAlertsActions
)(ManualJournalsDataTable);

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
export default function ManualJournalsEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={'Create your first journal entries on accounts chart.'}
description={
<p>
It is a long established fact that a reader will be distracted by the
readable content of a page when looking at its layout.
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/make-journal-entry');
}}
>
Make journal
</Button>
<Button intent={Intent.NONE} large={true}>
Learn more
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import { ManualJournalsListProvider } from './ManualJournalsListProvider';
import ManualJournalsAlerts from './ManualJournalsAlerts';
import ManualJournalsViewTabs from './ManualJournalsViewTabs';
import ManualJournalsDataTable from './ManualJournalsDataTable';
import ManualJournalsActionsBar from './ManualJournalActionsBar';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withManualJournals from './withManualJournals';
import { transformTableStateToQuery, compose } from 'utils';
import 'style/pages/ManualJournal/List.scss';
/**
* Manual journals table.
*/
function ManualJournalsTable({
// #withDashboardActions
changePageTitle,
// #withManualJournals
journalsTableState,
}) {
const { formatMessage } = useIntl();
// Handle update the page title.
useEffect(() => {
changePageTitle(formatMessage({ id: 'manual_journals' }));
}, [changePageTitle, formatMessage]);
return (
<ManualJournalsListProvider
query={transformTableStateToQuery(journalsTableState)}
>
<ManualJournalsActionsBar />
<DashboardPageContent>
<ManualJournalsViewTabs />
<ManualJournalsDataTable />
<ManualJournalsAlerts />
</DashboardPageContent>
</ManualJournalsListProvider>
);
}
export default compose(
withDashboardActions,
withManualJournals(({ manualJournalsTableState }) => ({
journalsTableState: manualJournalsTableState,
})),
)(ManualJournalsTable);

View File

@@ -0,0 +1,48 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useJournals } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const ManualJournalsContext = createContext();
function ManualJournalsListProvider({ query, ...props }) {
// Fetches accounts resource views and fields.
const { data: journalsViews, isLoading: isViewsLoading } = useResourceViews(
'manual_journals',
);
// Fetches the manual journals transactions with pagination meta.
const {
data: { manualJournals, pagination, filterMeta },
isLoading: isManualJournalsLoading,
isFetching: isManualJournalsFetching
} = useJournals(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus = isTableEmptyStatus({
data: manualJournals, pagination, filterMeta,
}) && !isManualJournalsFetching;
// Global state.
const state = {
manualJournals,
pagination,
journalsViews,
isManualJournalsLoading,
isManualJournalsFetching,
isViewsLoading,
isEmptyStatus
};
return (
<DashboardInsider loading={isViewsLoading} name={'manual-journals'}>
<ManualJournalsContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useManualJournalsContext = () => React.useContext(ManualJournalsContext);
export { ManualJournalsListProvider, useManualJournalsContext };

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import { useManualJournalsContext } from './ManualJournalsListProvider';
import withManualJournalsActions from './withManualJournalsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withManualJournals from './withManualJournals';
import { compose } from 'utils';
/**
* Manual journal views tabs.
*/
function ManualJournalsViewTabs({
// #withManualJournalsActions
setManualJournalsTableState,
// #withManualJournals
journalsTableState
}) {
// Manual journals context.
const { journalsViews } = useManualJournalsContext();
const tabs = journalsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const handleClickNewView = () => {};
// Handles the tab change.
const handleTabChange = (viewId) => {
setManualJournalsTableState({
customViewId: viewId || null,
});
};
return (
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
resourceName={'manual-journals'}
customViewId={journalsTableState.customViewId}
tabs={tabs}
onChange={handleTabChange}
onNewViewTabClick={handleClickNewView}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withManualJournalsActions,
withDashboardActions,
withManualJournals(({ manualJournalsTableState }) => ({
journalsTableState: manualJournalsTableState,
})),
)(ManualJournalsViewTabs);

View File

@@ -0,0 +1,175 @@
import React from 'react';
import {
Intent,
Classes,
Tooltip,
Position,
Tag,
Button,
MenuItem,
Menu,
MenuDivider,
Popover,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import moment from 'moment';
import { Choose, Money, If, Icon } from 'components';
import { safeCallback } from 'utils';
import { formatMessage } from 'services/intl';
/**
* Amount accessor.
*/
export const AmountAccessor = (r) => (
<Tooltip
content={<AmountPopoverContent journalEntries={r.entries} />}
position={Position.RIGHT_TOP}
boundary={'viewport'}
>
<Money amount={r.amount} currency={'USD'} />
</Tooltip>
);
/**
* Amount popover content line.
*/
export const AmountPopoverContentLine = ({ journalEntry }) => {
const isCredit = !!journalEntry.credit;
const isDebit = !!journalEntry.debit;
const { account } = journalEntry;
return (
<Choose>
<Choose.When condition={isDebit}>
<div>
C. <Money amount={journalEntry.debit} currency={'USD'} /> USD -{' '}
{account.name} <If condition={account.code}>({account.code})</If>
</div>
</Choose.When>
<Choose.When condition={isCredit}>
<div>
D. <Money amount={journalEntry.credit} currency={'USD'} /> USD -{' '}
{account.name} <If condition={account.code}>({account.code})</If>
</div>
</Choose.When>
</Choose>
);
};
/**
* Amount popover content.
*/
export function AmountPopoverContent({ journalEntries }) {
const journalLinesProps = journalEntries.map((journalEntry) => ({
journalEntry,
accountId: journalEntry.account_id,
}));
return (
<div>
{journalLinesProps.map(({ journalEntry, accountId }) => (
<AmountPopoverContentLine
journalEntry={journalEntry}
accountId={accountId}
/>
))}
</div>
);
}
/**
* Publish column accessor.
*/
export const StatusAccessor = (row) => {
return (
<Choose>
<Choose.When condition={!!row.is_published}>
<Tag minimal={true}>
<T id={'published'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);
};
/**
* Date accessor.
*/
export const DateAccessor = (row) => {
return moment(row.date).format('YYYY MMM DD');
};
/**
* Note column accessor.
*/
export function NoteAccessor(row) {
return (
<If condition={row.description}>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.LEFT_TOP}
hoverOpenDelay={50}
>
<Icon icon={'file-alt'} iconSize={16} />
</Tooltip>
</If>
);
}
/**
* Table actions cell.
*/
export const ActionsCell = (props) => {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
};
/**
* Actions menu of the table.
*/
export const ActionsMenu = ({
payload: { onPublish, onEdit, onDelete },
row: { original },
}) => {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<If condition={!original.is_published}>
<MenuItem
icon={<Icon icon="arrow-to-top" />}
text={formatMessage({ id: 'publish_journal' })}
onClick={safeCallback(onPublish, original)}
/>
</If>
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_journal' })}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
text={formatMessage({ id: 'delete_journal' })}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
/>
</Menu>
);
};

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { formatMessage } from 'services/intl';
import moment from 'moment';
import {
NoteAccessor,
StatusAccessor,
DateAccessor,
AmountAccessor,
ActionsCell,
} from './components';
/**
* Retrieve the manual journals columns.
*/
export const useManualJournalsColumns = () => {
return React.useMemo(
() => [
{
id: 'date',
Header: formatMessage({ id: 'date' }),
accessor: DateAccessor,
width: 115,
className: 'date',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: AmountAccessor,
className: 'amount',
width: 115,
},
{
id: 'journal_number',
Header: formatMessage({ id: 'journal_no' }),
accessor: (row) => `#${row.journal_number}`,
className: 'journal_number',
width: 100,
},
{
id: 'journal_type',
Header: formatMessage({ id: 'journal_type' }),
accessor: 'journal_type',
width: 110,
className: 'journal_type',
},
{
id: 'publish',
Header: formatMessage({ id: 'publish' }),
accessor: (row) => StatusAccessor(row),
width: 95,
className: 'publish',
},
{
id: 'note',
Header: formatMessage({ id: 'note' }),
accessor: NoteAccessor,
disableSorting: true,
width: 85,
className: 'note',
},
{
id: 'created_at',
Header: formatMessage({ id: 'created_at' }),
accessor: (r) => moment(r.created_at).format('YYYY MMM DD'),
width: 125,
className: 'created_at',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[],
);
};

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import {
getManualJournalsTableStateFactory
} from 'store/manualJournals/manualJournals.selectors';
export default (mapState) => {
const getJournalsTableQuery = getManualJournalsTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
manualJournalsTableState: getJournalsTableQuery(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import {
setManualJournalsTableState,
} from 'store/manualJournals/manualJournals.actions';
const mapActionsToProps = (dispatch) => ({
setManualJournalsTableState: (queries) =>
dispatch(setManualJournalsTableState(queries)),
});
export default connect(null, mapActionsToProps);