mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
WIP Version 0.0.1
This commit is contained in:
@@ -1,39 +1,41 @@
|
||||
import React, {useMemo, useState, useEffect, useRef, useCallback} from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
ProgressBar,
|
||||
Classes,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import {useFormik} from "formik";
|
||||
import moment from 'moment';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
|
||||
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
|
||||
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
|
||||
import MakeJournalEntriesTable from './MakeJournalEntriesTable';
|
||||
import {useFormik} from "formik";
|
||||
import MakeJournalEntriesConnect from 'connectors/MakeJournalEntries.connect';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import {compose, saveFilesInAsync} from 'utils';
|
||||
import moment from 'moment';
|
||||
|
||||
import withJournalsActions from 'containers/Accounting/withJournalsActions';
|
||||
import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import {pick} from 'lodash';
|
||||
import Dragzone from 'components/Dragzone';
|
||||
import MediaConnect from 'connectors/Media.connect';
|
||||
import classNames from 'classnames';
|
||||
import ManualJournalsConnect from 'connectors/ManualJournals.connect';
|
||||
|
||||
import useMedia from 'hooks/useMedia';
|
||||
import {compose} from 'utils';
|
||||
|
||||
|
||||
function MakeJournalEntriesForm({
|
||||
requestSubmitMedia,
|
||||
requestDeleteMedia,
|
||||
|
||||
requestMakeJournalEntries,
|
||||
requestEditManualJournal,
|
||||
|
||||
changePageTitle,
|
||||
changePageSubtitle,
|
||||
editJournal,
|
||||
onFormSubmit,
|
||||
onCancelForm,
|
||||
|
||||
requestDeleteMedia,
|
||||
manualJournalsItems
|
||||
manualJournalId,
|
||||
manualJournal,
|
||||
onFormSubmit,
|
||||
onCancelForm,
|
||||
}) {
|
||||
const { setFiles, saveMedia, deletedFiles, setDeletedFiles, deleteMedia } = useMedia({
|
||||
saveCallback: requestSubmitMedia,
|
||||
@@ -47,13 +49,13 @@ function MakeJournalEntriesForm({
|
||||
const clearSavedMediaIds = () => { savedMediaIds.current = []; }
|
||||
|
||||
useEffect(() => {
|
||||
if (editJournal && editJournal.id) {
|
||||
if (manualJournal && manualJournal.id) {
|
||||
changePageTitle('Edit Journal');
|
||||
changePageSubtitle(`No. ${editJournal.journal_number}`);
|
||||
changePageSubtitle(`No. ${manualJournal.journal_number}`);
|
||||
} else {
|
||||
changePageTitle('New Journal');
|
||||
}
|
||||
}, [changePageTitle, changePageSubtitle, editJournal]);
|
||||
}, [changePageTitle, changePageSubtitle, manualJournal]);
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
journal_number: Yup.string().required(),
|
||||
@@ -100,24 +102,24 @@ function MakeJournalEntriesForm({
|
||||
}), [defaultEntry]);
|
||||
|
||||
const initialValues = useMemo(() => ({
|
||||
...(editJournal) ? {
|
||||
...pick(editJournal, Object.keys(defaultInitialValues)),
|
||||
entries: editJournal.entries.map((entry) => ({
|
||||
...(manualJournal) ? {
|
||||
...pick(manualJournal, Object.keys(defaultInitialValues)),
|
||||
entries: manualJournal.entries.map((entry) => ({
|
||||
...pick(entry, Object.keys(defaultEntry)),
|
||||
})),
|
||||
} : {
|
||||
...defaultInitialValues,
|
||||
}
|
||||
}), [editJournal, defaultInitialValues, defaultEntry]);
|
||||
}), [manualJournal, defaultInitialValues, defaultEntry]);
|
||||
|
||||
const initialAttachmentFiles = useMemo(() => {
|
||||
return editJournal && editJournal.media
|
||||
? editJournal.media.map((attach) => ({
|
||||
return manualJournal && manualJournal.media
|
||||
? manualJournal.media.map((attach) => ({
|
||||
preview: attach.attachment_file,
|
||||
uploaded: true,
|
||||
metadata: { ...attach },
|
||||
})) : [];
|
||||
}, [editJournal]);
|
||||
}, [manualJournal]);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -125,7 +127,7 @@ function MakeJournalEntriesForm({
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
},
|
||||
onSubmit: async (values, actions) => {
|
||||
onSubmit: async (values, { setErrors, setSubmitting }) => {
|
||||
const entries = values.entries.filter((entry) => (
|
||||
(entry.credit || entry.debit)
|
||||
));
|
||||
@@ -142,7 +144,7 @@ function MakeJournalEntriesForm({
|
||||
AppToaster.show({
|
||||
message: 'credit_and_debit_not_equal',
|
||||
});
|
||||
actions.setSubmitting(false);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const form = { ...values, status: payload.publish, entries };
|
||||
@@ -150,33 +152,43 @@ function MakeJournalEntriesForm({
|
||||
const saveJournal = (mediaIds) => new Promise((resolve, reject) => {
|
||||
const requestForm = { ...form, media_ids: mediaIds };
|
||||
|
||||
if (editJournal && editJournal.id) {
|
||||
requestEditManualJournal(editJournal.id, requestForm)
|
||||
if (manualJournal && manualJournal.id) {
|
||||
requestEditManualJournal(manualJournal.id, requestForm)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: 'manual_journal_has_been_edited',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
actions.setSubmitting(false);
|
||||
setSubmitting(false);
|
||||
saveInvokeSubmit({ action: 'update', ...payload });
|
||||
clearSavedMediaIds([]);
|
||||
resolve(response);
|
||||
}).catch((error) => {
|
||||
actions.setSubmitting(false);
|
||||
reject(error);
|
||||
}).catch((errors) => {
|
||||
if (errors.find(e => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')) {
|
||||
setErrors({
|
||||
journal_number: 'Journal number is already used.',
|
||||
});
|
||||
}
|
||||
setSubmitting(false);
|
||||
});
|
||||
} else {
|
||||
requestMakeJournalEntries(requestForm)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: 'manual_journal_has_been_submit',
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
actions.setSubmitting(false);
|
||||
setSubmitting(false);
|
||||
saveInvokeSubmit({ action: 'new', ...payload });
|
||||
clearSavedMediaIds();
|
||||
resolve(response);
|
||||
}).catch((error) => {
|
||||
actions.setSubmitting(false);
|
||||
reject(error);
|
||||
}).catch((errors) => {
|
||||
if (errors.find(e => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')) {
|
||||
setErrors({
|
||||
journal_number: 'Journal number is already used.',
|
||||
});
|
||||
}
|
||||
setSubmitting(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -240,9 +252,11 @@ function MakeJournalEntriesForm({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
ManualJournalsConnect,
|
||||
MakeJournalEntriesConnect,
|
||||
AccountsConnect,
|
||||
DashboardConnect,
|
||||
// ManualJournalsConnect,
|
||||
// MakeJournalEntriesConnect,
|
||||
withJournalsActions,
|
||||
withManualJournalDetail,
|
||||
withAccountsActions,
|
||||
withDashboardActions,
|
||||
MediaConnect,
|
||||
)(MakeJournalEntriesForm);
|
||||
@@ -1,31 +1,28 @@
|
||||
import React, {useMemo, useCallback} from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
import { useQuery } from 'react-query';
|
||||
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
|
||||
|
||||
import {compose} from 'utils';
|
||||
import MakeJournalEntriesConnect from 'connectors/MakeJournalEntries.connect';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
|
||||
|
||||
function MakeJournalEntriesPage({
|
||||
fetchManualJournal,
|
||||
getManualJournal,
|
||||
requestFetchManualJournal,
|
||||
requestFetchAccounts,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { id } = useParams();
|
||||
|
||||
const fetchJournal = useAsync(() => {
|
||||
return Promise.all([
|
||||
requestFetchAccounts(),
|
||||
(id) && fetchManualJournal(id),
|
||||
]);
|
||||
});
|
||||
const fetchAccounts = useQuery('accounts-list',
|
||||
(key) => requestFetchAccounts());
|
||||
|
||||
const editJournal = useMemo(() =>
|
||||
getManualJournal(id) || null,
|
||||
[getManualJournal, id]);
|
||||
const fetchJournal = useQuery(
|
||||
id && ['manual-journal', id],
|
||||
(key, journalId) => requestFetchManualJournal(journalId));
|
||||
|
||||
const handleFormSubmit = useCallback((payload) => {
|
||||
payload.redirect &&
|
||||
@@ -37,17 +34,19 @@ function MakeJournalEntriesPage({
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={fetchJournal.pending} name={'make-journal-page'}>
|
||||
<DashboardInsider
|
||||
loading={fetchJournal.isFetching || fetchAccounts.isFetching}
|
||||
name={'make-journal-page'}>
|
||||
<MakeJournalEntriesForm
|
||||
onFormSubmit={handleFormSubmit}
|
||||
editJournal={editJournal}
|
||||
manualJournalId={id}
|
||||
onCancelForm={handleCancel} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
AccountsConnect,
|
||||
MakeJournalEntriesConnect,
|
||||
// DashboardConnect,
|
||||
withAccountsActions,
|
||||
withManualJournalsActions,
|
||||
)(MakeJournalEntriesPage);
|
||||
@@ -5,8 +5,7 @@ import {
|
||||
} from '@blueprintjs/core';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Icon from 'components/Icon';
|
||||
import AccountsConnect from 'connectors/Accounts.connector.js';
|
||||
import {compose, formattedAmount} from 'utils';
|
||||
import { compose, formattedAmount} from 'utils';
|
||||
import {
|
||||
AccountsListFieldCell,
|
||||
MoneyFieldCell,
|
||||
@@ -14,6 +13,9 @@ import {
|
||||
} from 'components/DataTableCells';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
|
||||
// Actions cell renderer.
|
||||
const ActionsCellRenderer = ({
|
||||
row: { index },
|
||||
@@ -70,6 +72,7 @@ const NoteCellRenderer = (chainedComponent) => (props) => {
|
||||
return chainedComponent(props);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Make journal entries table component.
|
||||
*/
|
||||
@@ -223,5 +226,5 @@ function MakeJournalEntriesTable({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AccountsConnect,
|
||||
withAccounts,
|
||||
)(MakeJournalEntriesTable);
|
||||
131
client/src/containers/Accounting/ManualJournalActionsBar.js
Normal file
131
client/src/containers/Accounting/ManualJournalActionsBar.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import {
|
||||
Button,
|
||||
NavbarGroup,
|
||||
Classes,
|
||||
NavbarDivider,
|
||||
MenuItem,
|
||||
Menu,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Intent
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import { compose } from 'utils';
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withManualJournals from 'containers/Accounting/withManualJournals';
|
||||
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
|
||||
|
||||
|
||||
function ManualJournalActionsBar({
|
||||
resourceName = 'manual_journal',
|
||||
resourceFields,
|
||||
|
||||
manualJournalsViews,
|
||||
addManualJournalsTableQueries,
|
||||
|
||||
onFilterChanged,
|
||||
selectedRows,
|
||||
onBulkDelete
|
||||
}) {
|
||||
const { path } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const viewsMenuItems = manualJournalsViews.map(view => {
|
||||
return (
|
||||
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
|
||||
);
|
||||
});
|
||||
|
||||
const onClickNewManualJournal = useCallback(() => {
|
||||
history.push('/dashboard/accounting/make-journal-entry');
|
||||
}, [history]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
onFilterChange: filterConditions => {
|
||||
addManualJournalsTableQueries({
|
||||
filter_roles: filterConditions || ''
|
||||
});
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
}
|
||||
});
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
// Handle delete button click.
|
||||
const handleBulkDelete = useCallback(() => {
|
||||
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
|
||||
}, [onBulkDelete, selectedRows]);
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<Popover
|
||||
content={<Menu>{viewsMenuItems}</Menu>}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--table-views')}
|
||||
icon={<Icon icon='table' />}
|
||||
text='Table Views'
|
||||
rightIcon={'caret-down'}
|
||||
/>
|
||||
</Popover>
|
||||
<NavbarDivider />
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='plus' />}
|
||||
text='New Journal'
|
||||
onClick={onClickNewManualJournal}
|
||||
/>
|
||||
<Popover
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--filter')}
|
||||
text='Filter'
|
||||
icon={<Icon icon='filter' />}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
{(hasSelectedRows) && (
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='trash' iconSize={15} />}
|
||||
text='Delete'
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleBulkDelete}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-import' />}
|
||||
text='Import'
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-export' />}
|
||||
text='Export'
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
withResourceDetail,
|
||||
withManualJournals,
|
||||
withManualJournalsActions,
|
||||
)(ManualJournalActionsBar);
|
||||
207
client/src/containers/Accounting/ManualJournalsDataTable.js
Normal file
207
client/src/containers/Accounting/ManualJournalsDataTable.js
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
||||
import {
|
||||
Intent,
|
||||
Button,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Icon from 'components/Icon';
|
||||
import { compose } from 'utils';
|
||||
import moment from 'moment';
|
||||
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Money from 'components/Money';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withViewDetails from 'containers/Views/withViewDetails';
|
||||
import withManualJournals from 'containers/Accounting/withManualJournals';
|
||||
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
|
||||
|
||||
|
||||
function ManualJournalsDataTable({
|
||||
loading,
|
||||
|
||||
manualJournals,
|
||||
manualJournalsLoading,
|
||||
|
||||
changeCurrentView,
|
||||
changePageSubtitle,
|
||||
|
||||
viewId,
|
||||
viewMeta,
|
||||
setTopbarEditView,
|
||||
|
||||
onFetchData,
|
||||
onEditJournal,
|
||||
onDeleteJournal,
|
||||
onPublishJournal,
|
||||
onSelectedRowsChange,
|
||||
}) {
|
||||
const { custom_view_id: customViewId } = useParams();
|
||||
const [initialMount, setInitialMount] = useState(false);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!manualJournalsLoading) {
|
||||
setInitialMount(true);
|
||||
}
|
||||
}, [manualJournalsLoading, setInitialMount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customViewId) {
|
||||
changeCurrentView(customViewId);
|
||||
setTopbarEditView(customViewId);
|
||||
}
|
||||
changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
|
||||
}, [
|
||||
customViewId,
|
||||
changeCurrentView,
|
||||
changePageSubtitle,
|
||||
setTopbarEditView,
|
||||
viewMeta,
|
||||
]);
|
||||
|
||||
const handlePublishJournal = useCallback((journal) => () => {
|
||||
onPublishJournal && onPublishJournal(journal);
|
||||
}, [onPublishJournal]);
|
||||
|
||||
const handleEditJournal = useCallback((journal) => () => {
|
||||
onEditJournal && onEditJournal(journal);
|
||||
}, [onEditJournal]);
|
||||
|
||||
const handleDeleteJournal = useCallback((journal) => () => {
|
||||
onDeleteJournal && onDeleteJournal(journal);
|
||||
}, [onDeleteJournal]);
|
||||
|
||||
const actionMenuList = (journal) => (
|
||||
<Menu>
|
||||
<MenuItem text='View Details' />
|
||||
<MenuDivider />
|
||||
{!journal.status && (
|
||||
<MenuItem
|
||||
text="Publish Journal"
|
||||
onClick={handlePublishJournal(journal)} />
|
||||
)}
|
||||
<MenuItem
|
||||
text='Edit Journal'
|
||||
onClick={handleEditJournal(journal)} />
|
||||
<MenuItem
|
||||
text='Delete Journal'
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteJournal(journal)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
id: 'date',
|
||||
Header: 'Date',
|
||||
accessor: r => moment().format('YYYY-MM-DD'),
|
||||
disableResizing: true,
|
||||
width: 150,
|
||||
className: 'date',
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
Header: 'Amount',
|
||||
accessor: r => (<Money amount={r.amount} currency={'USD'} />),
|
||||
disableResizing: true,
|
||||
className: 'amount',
|
||||
},
|
||||
{
|
||||
id: 'journal_number',
|
||||
Header: 'Journal No.',
|
||||
accessor: 'journal_number',
|
||||
disableResizing: true,
|
||||
className: 'journal_number',
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: 'Status',
|
||||
accessor: (r) => {
|
||||
return r.status ? 'Published' : 'Draft';
|
||||
},
|
||||
disableResizing: true,
|
||||
width: 100,
|
||||
className: 'status',
|
||||
},
|
||||
{
|
||||
id: 'note',
|
||||
Header: 'Note',
|
||||
accessor: r => (<Icon icon={'file-alt'} iconSize={16} />),
|
||||
disableResizing: true,
|
||||
disableSorting: true,
|
||||
width: 100,
|
||||
className: 'note',
|
||||
},
|
||||
{
|
||||
id: 'transaction_type',
|
||||
Header: 'Transaction type ',
|
||||
accessor: 'transaction_type',
|
||||
width: 100,
|
||||
className: 'transaction_type',
|
||||
},
|
||||
{
|
||||
id: 'created_at',
|
||||
Header: 'Created At',
|
||||
accessor: r => moment().format('YYYY-MM-DD'),
|
||||
disableResizing: true,
|
||||
width: 150,
|
||||
className: 'created_at',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Cell: ({ cell }) => (
|
||||
<Popover
|
||||
content={actionMenuList(cell.row.original)}
|
||||
position={Position.RIGHT_BOTTOM}
|
||||
>
|
||||
<Button icon={<Icon icon='ellipsis-h' />} />
|
||||
</Popover>
|
||||
),
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
disableResizing: true,
|
||||
},
|
||||
], []);
|
||||
|
||||
const handleDataTableFetchData = useCallback((...args) => {
|
||||
onFetchData && onFetchData(...args);
|
||||
}, [onFetchData]);
|
||||
|
||||
const handleSelectedRowsChange = useCallback((selectedRows) => {
|
||||
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
|
||||
}, [onSelectedRowsChange]);
|
||||
|
||||
return (
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={manualJournals}
|
||||
onFetchData={handleDataTableFetchData}
|
||||
manualSortBy={true}
|
||||
selectionColumn={true}
|
||||
noInitialFetch={true}
|
||||
loading={manualJournalsLoading && !initialMount}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
/>
|
||||
</LoadingIndicator>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
withDashboardActions,
|
||||
// withViewsActions,
|
||||
withManualJournalsActions,
|
||||
withManualJournals,
|
||||
withViewDetails,
|
||||
)(ManualJournalsDataTable);
|
||||
@@ -1,45 +1,49 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import ManualJournalsViewTabs from 'components/JournalEntry/ManualJournalsViewTabs';
|
||||
import ManualJournalsDataTable from 'components/JournalEntry/ManualJournalsDataTable';
|
||||
import ManualJournalsActionsBar from 'components/JournalEntry/ManualJournalActionsBar';
|
||||
import ManualJournalsConnect from 'connectors/ManualJournals.connect';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import CustomViewConnect from 'connectors/CustomView.connector';
|
||||
import ResourceConnect from 'connectors/Resource.connector';
|
||||
|
||||
import ManualJournalsViewTabs from 'containers/Accounting/ManualJournalsViewTabs';
|
||||
import ManualJournalsDataTable from 'containers/Accounting/ManualJournalsDataTable';
|
||||
import ManualJournalsActionsBar from 'containers/Accounting/ManualJournalActionsBar';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
|
||||
import withViewsActions from 'containers/Views/withViewsActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
/**
|
||||
* Manual journals table.
|
||||
*/
|
||||
function ManualJournalsTable({
|
||||
changePageTitle,
|
||||
|
||||
fetchResourceViews,
|
||||
fetchManualJournalsTable,
|
||||
requestFetchResourceViews,
|
||||
|
||||
requestFetchManualJournalsTable,
|
||||
requestDeleteManualJournal,
|
||||
requestPublishManualJournal,
|
||||
requestDeleteBulkManualJournals,
|
||||
|
||||
addManualJournalsTableQueries
|
||||
addManualJournalsTableQueries,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const [deleteManualJournal, setDeleteManualJournal] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [bulkDelete, setBulkDelete] = useState(false);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchResourceViews('manual_journals'),
|
||||
]);
|
||||
const fetchViews = useQuery('journals-resource-views', () => {
|
||||
return requestFetchResourceViews('manual_journals');
|
||||
});
|
||||
|
||||
const fetchManualJournalsHook = useAsync(async () => {
|
||||
return fetchManualJournalsTable();
|
||||
});
|
||||
const fetchManualJournals = useQuery('manual-journals-table', () =>
|
||||
requestFetchManualJournalsTable());
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Manual Journals');
|
||||
@@ -89,13 +93,13 @@ function ManualJournalsTable({
|
||||
|
||||
// Handle filter change to re-fetch data-table.
|
||||
const handleFilterChanged = useCallback(() => {
|
||||
fetchManualJournalsHook.execute();
|
||||
}, [fetchManualJournalsHook]);
|
||||
fetchManualJournals.refetch();
|
||||
}, [fetchManualJournals]);
|
||||
|
||||
// Handle view change to re-fetch data table.
|
||||
const handleViewChanged = useCallback(() => {
|
||||
fetchManualJournalsHook.execute();
|
||||
}, [fetchManualJournalsHook]);
|
||||
fetchManualJournals.refetch();
|
||||
}, [fetchManualJournals]);
|
||||
|
||||
// Handle fetch data of manual jouranls datatable.
|
||||
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
|
||||
@@ -105,9 +109,7 @@ function ManualJournalsTable({
|
||||
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||
} : {},
|
||||
});
|
||||
fetchManualJournalsHook.execute();
|
||||
}, [
|
||||
fetchManualJournalsHook,
|
||||
addManualJournalsTableQueries,
|
||||
]);
|
||||
|
||||
@@ -123,7 +125,9 @@ function ManualJournalsTable({
|
||||
}, [setSelectedRows]);
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={fetchHook.pending} name={'manual-journals'}>
|
||||
<DashboardInsider
|
||||
loading={fetchViews.isFetching || fetchManualJournals.isFetching}
|
||||
name={'manual-journals'}>
|
||||
<ManualJournalsActionsBar
|
||||
onBulkDelete={handleBulkDelete}
|
||||
selectedRows={selectedRows}
|
||||
@@ -184,8 +188,7 @@ function ManualJournalsTable({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
ManualJournalsConnect,
|
||||
CustomViewConnect,
|
||||
ResourceConnect,
|
||||
DashboardConnect
|
||||
withDashboardActions,
|
||||
withManualJournalsActions,
|
||||
withViewsActions,
|
||||
)(ManualJournalsTable);
|
||||
@@ -1,27 +1,26 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Alignment,
|
||||
Navbar,
|
||||
NavbarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Icon from 'components/Icon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import ManualJournalsConnect from 'connectors/ManualJournals.connect';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import {useUpdateEffect} from 'hooks';
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
|
||||
function ItemsViewsTabs({
|
||||
function ManualJournalsViewTabs({
|
||||
views,
|
||||
setTopbarEditView,
|
||||
customViewChanged,
|
||||
addItemsTableQueries,
|
||||
addManualJournalsTableQueries,
|
||||
onViewChanged,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
@@ -29,39 +28,40 @@ function ItemsViewsTabs({
|
||||
|
||||
const handleClickNewView = () => {
|
||||
setTopbarEditView(null);
|
||||
history.push('/dashboard/custom_views/items/new');
|
||||
history.push('/dashboard/custom_views/manual_journals/new');
|
||||
};
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
}
|
||||
};
|
||||
|
||||
useUpdateEffect(() => {
|
||||
customViewChanged && customViewChanged(customViewId);
|
||||
|
||||
addItemsTableQueries({
|
||||
addManualJournalsTableQueries({
|
||||
custom_view_id: customViewId || null,
|
||||
});
|
||||
onViewChanged && onViewChanged(customViewId);
|
||||
}, [customViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
addItemsTableQueries({
|
||||
addManualJournalsTableQueries({
|
||||
custom_view_id: customViewId,
|
||||
})
|
||||
}, [customViewId, addItemsTableQueries]);
|
||||
});
|
||||
}, [customViewId]);
|
||||
|
||||
const tabs = views.map(view => {
|
||||
const baseUrl = '/dashboard/items';
|
||||
const tabs = views.map((view) => {
|
||||
const baseUrl = '/dashboard/accounting/manual-journals';
|
||||
const link = (
|
||||
<Link
|
||||
to={`${baseUrl}/${view.id}/custom_view`}
|
||||
onClick={handleViewLinkClick}
|
||||
>{view.name}</Link>
|
||||
>
|
||||
{view.name}
|
||||
</Link>
|
||||
);
|
||||
return <Tab
|
||||
id={`custom_view_${view.id}`}
|
||||
title={link} />;
|
||||
return <Tab id={`custom_view_${view.id}`} title={link} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<Navbar className='navbar--dashboard-views'>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
@@ -73,8 +73,10 @@ function ItemsViewsTabs({
|
||||
>
|
||||
<Tab
|
||||
id='all'
|
||||
title={<Link to={`/dashboard/items`}>All</Link>} />
|
||||
|
||||
title={
|
||||
<Link to={`/dashboard/accounting/manual-journals`}>All</Link>
|
||||
}
|
||||
/>
|
||||
{tabs}
|
||||
<Button
|
||||
className='button--new-view'
|
||||
@@ -89,6 +91,6 @@ function ItemsViewsTabs({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
ItemsConnect,
|
||||
DashboardConnect,
|
||||
)(ItemsViewsTabs);
|
||||
ManualJournalsConnect,
|
||||
DashboardConnect
|
||||
)(ManualJournalsViewTabs);
|
||||
10
client/src/containers/Accounting/withJournalDetail.js
Normal file
10
client/src/containers/Accounting/withJournalDetail.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
getManualJournal,
|
||||
} from 'store/manualJournals/manualJournals.reducers';
|
||||
|
||||
export const mapStateToProps = (state, props) => ({
|
||||
manualJournal: getManualJournal(state, props.manualJournalId),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
14
client/src/containers/Accounting/withJournalsActions.js
Normal file
14
client/src/containers/Accounting/withJournalsActions.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
makeJournalEntries,
|
||||
fetchManualJournal,
|
||||
editManualJournal,
|
||||
} from 'store/manualJournals/manualJournals.actions';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
requestMakeJournalEntries: (form) => dispatch(makeJournalEntries({ form })),
|
||||
requestFetchManualJournal: (id) => dispatch(fetchManualJournal({ id })),
|
||||
requestEditManualJournal: (id, form) => dispatch(editManualJournal({ id, form }))
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
import { getManualJournal } from 'store/manualJournals/manualJournals.reducers';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
manualJournal: getManualJournal(state, props.manualJournalId),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
16
client/src/containers/Accounting/withManualJournals.js
Normal file
16
client/src/containers/Accounting/withManualJournals.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { getResourceViews } from 'store/customViews/customViews.selectors';
|
||||
import {
|
||||
getManualJournalsItems,
|
||||
} from 'store/manualJournals/manualJournals.selectors'
|
||||
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
manualJournals: getManualJournalsItems(state, state.manualJournals.currentViewId),
|
||||
manualJournalsViews: getResourceViews(state, 'manual_journals'),
|
||||
manualJournalsItems: state.manualJournals.items,
|
||||
manualJournalsTableQuery: state.manualJournals.tableQuery,
|
||||
manualJournalsLoading: state.manualJournals.loading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
import {
|
||||
deleteManualJournal,
|
||||
fetchManualJournalsTable,
|
||||
publishManualJournal,
|
||||
deleteBulkManualJournals,
|
||||
fetchManualJournal,
|
||||
} from 'store/manualJournals/manualJournals.actions';
|
||||
|
||||
const mapActionsToProps = (dispatch) => ({
|
||||
requestDeleteManualJournal: (id) => dispatch(deleteManualJournal({ id })),
|
||||
requestFetchManualJournalsTable: (query = {}) => dispatch(fetchManualJournalsTable({ query: { ...query } })),
|
||||
requestFetchManualJournal: (id) => dispatch(fetchManualJournal({ id })),
|
||||
requestPublishManualJournal: (id) => dispatch(publishManualJournal({ id })),
|
||||
requestDeleteBulkManualJournals: (ids) => dispatch(deleteBulkManualJournals({ ids })),
|
||||
|
||||
changeCurrentView: (id) => dispatch({
|
||||
type: t.MANUAL_JOURNALS_SET_CURRENT_VIEW,
|
||||
currentViewId: parseInt(id, 10),
|
||||
}),
|
||||
addManualJournalsTableQueries: (queries) => dispatch({
|
||||
type: t.MANUAL_JOURNALS_TABLE_QUERIES_ADD,
|
||||
queries,
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps);
|
||||
157
client/src/containers/Accounts/AccountsActionsBar.js
Normal file
157
client/src/containers/Accounts/AccountsActionsBar.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import {
|
||||
Button,
|
||||
NavbarGroup,
|
||||
Classes,
|
||||
NavbarDivider,
|
||||
MenuItem,
|
||||
Menu,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { If } from 'components';
|
||||
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
import {compose} from 'utils';
|
||||
|
||||
|
||||
function AccountsActionsBar({
|
||||
openDialog,
|
||||
accountsViews,
|
||||
|
||||
resourceFields,
|
||||
addAccountsTableQueries,
|
||||
|
||||
selectedRows = [],
|
||||
onFilterChanged,
|
||||
onBulkDelete,
|
||||
onBulkArchive,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const [filterCount, setFilterCount] = useState(0);
|
||||
|
||||
const onClickNewAccount = () => { openDialog('account-form', {}); };
|
||||
const onClickViewItem = (view) => {
|
||||
history.push(view
|
||||
? `/dashboard/accounts/${view.id}/custom_view` : '/dashboard/accounts');
|
||||
};
|
||||
|
||||
const viewsMenuItems = accountsViews.map((view) => {
|
||||
return (<MenuItem onClick={() => onClickViewItem(view)} text={view.name} />);
|
||||
});
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: resourceFields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
setFilterCount(filterConditions.length || 0);
|
||||
addAccountsTableQueries({
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
},
|
||||
});
|
||||
|
||||
const handleBulkArchive = useCallback(() => {
|
||||
onBulkArchive && onBulkArchive(selectedRows.map(r => r.id));
|
||||
}, [onBulkArchive, selectedRows]);
|
||||
|
||||
const handleBulkDelete = useCallback(() => {
|
||||
onBulkDelete && onBulkDelete(selectedRows.map(r => r.id));
|
||||
}, [onBulkDelete, selectedRows]);
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<Popover
|
||||
content={<Menu>{viewsMenuItems}</Menu>}
|
||||
minimal={true}
|
||||
interactionKind={PopoverInteractionKind.HOVER}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
>
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--table-views')}
|
||||
icon={<Icon icon='table' />}
|
||||
text='Table Views'
|
||||
rightIcon={'caret-down'}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
<NavbarDivider />
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='plus' />}
|
||||
text='New Account'
|
||||
onClick={onClickNewAccount}
|
||||
/>
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}>
|
||||
|
||||
<Button
|
||||
className={classNames(Classes.MINIMAL, 'button--filter')}
|
||||
text={filterCount <= 0 ? 'Filter' : `${filterCount} filters applied`}
|
||||
icon={ <Icon icon="filter" /> }/>
|
||||
</Popover>
|
||||
|
||||
<If condition={hasSelectedRows}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='archive' iconSize={15} />}
|
||||
text='Archive'
|
||||
onClick={handleBulkArchive}
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='trash' iconSize={15} />}
|
||||
text='Delete'
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleBulkDelete}
|
||||
/>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-import' />}
|
||||
text='Import'
|
||||
/>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-export' />}
|
||||
text='Export'
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: 'accounts',
|
||||
});
|
||||
|
||||
const withAccountsActionsBar = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withAccountsActionsBar,
|
||||
DialogConnect,
|
||||
withAccounts,
|
||||
withResourceDetail,
|
||||
withAccountsTableActions,
|
||||
)(AccountsActionsBar);
|
||||
@@ -3,29 +3,49 @@ import {
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import { useQuery } from 'react-query'
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import AccountsViewsTabs from 'components/Accounts/AccountsViewsTabs';
|
||||
import AccountsDataTable from 'components/Accounts/AccountsDataTable';
|
||||
import DashboardActionsBar from 'components/Accounts/AccountsActionsBar';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import CustomViewConnect from 'connectors/CustomView.connector';
|
||||
import ResourceConnect from 'connectors/Resource.connector';
|
||||
import AccountsViewsTabs from 'containers/Accounts/AccountsViewsTabs';
|
||||
import AccountsDataTable from 'containers/Accounts/AccountsDataTable';
|
||||
import DashboardActionsBar from 'containers/Accounts/AccountsActionsBar';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withResourceActions from 'containers/Resources/withResourcesActions';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
|
||||
import withViewsActions from 'containers/Views/withViewsActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
function AccountsChart({
|
||||
|
||||
// #withDashboard
|
||||
changePageTitle,
|
||||
|
||||
// #withAccountsActions
|
||||
requestDeleteAccount,
|
||||
requestInactiveAccount,
|
||||
fetchResourceViews,
|
||||
fetchResourceFields,
|
||||
|
||||
// #withViewsActions
|
||||
requestFetchResourceViews,
|
||||
|
||||
// #withResourceActions
|
||||
requestFetchResourceFields,
|
||||
|
||||
// #withAccountsTableActions
|
||||
requestFetchAccountsTable,
|
||||
addAccountsTableQueries,
|
||||
requestDeleteBulkAccounts,
|
||||
addAccountsTableQueries,
|
||||
|
||||
// #withAccounts
|
||||
accountsTableQuery,
|
||||
}) {
|
||||
const [deleteAccount, setDeleteAccount] = useState(false);
|
||||
const [inactiveAccount, setInactiveAccount] = useState(false);
|
||||
@@ -35,19 +55,16 @@ function AccountsChart({
|
||||
const [tableLoading, setTableLoading] = useState(false);
|
||||
|
||||
// Fetch accounts resource views and fields.
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchResourceViews('accounts'),
|
||||
fetchResourceFields('accounts'),
|
||||
const fetchHook = useQuery('resource-accounts', () => {
|
||||
return Promise.all([
|
||||
requestFetchResourceViews('accounts'),
|
||||
requestFetchResourceFields('accounts'),
|
||||
]);
|
||||
});
|
||||
|
||||
// Fetch accounts list according to the given custom view id.
|
||||
const fetchAccountsHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
requestFetchAccountsTable(),
|
||||
]);
|
||||
}, false);
|
||||
const fetchAccountsHook = useQuery(['accounts-table', accountsTableQuery],
|
||||
() => requestFetchAccountsTable());
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Chart of Accounts');
|
||||
@@ -136,18 +153,20 @@ function AccountsChart({
|
||||
|
||||
// Refetches accounts data table when current custom view changed.
|
||||
const handleFilterChanged = useCallback(() => {
|
||||
fetchAccountsHook.execute();
|
||||
fetchAccountsHook.refetch();
|
||||
}, [fetchAccountsHook]);
|
||||
|
||||
// Refetch accounts data table when current custom view changed.
|
||||
const handleViewChanged = useCallback(() => {
|
||||
const handleViewChanged = useCallback(async () => {
|
||||
setTableLoading(true);
|
||||
|
||||
fetchAccountsHook.execute().finally(() => {
|
||||
setTableLoading(false);
|
||||
});
|
||||
}, [fetchAccountsHook]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableLoading && !fetchAccountsHook.isFetching) {
|
||||
setTableLoading(false);
|
||||
}
|
||||
}, [tableLoading, fetchAccountsHook.isFetching]);
|
||||
|
||||
// Handle fetch data of accounts datatable.
|
||||
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
|
||||
addAccountsTableQueries({
|
||||
@@ -156,11 +175,13 @@ function AccountsChart({
|
||||
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||
} : {},
|
||||
});
|
||||
fetchAccountsHook.execute();
|
||||
fetchAccountsHook.refetch();
|
||||
}, [fetchAccountsHook, addAccountsTableQueries]);
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={fetchHook.pending} name={'accounts-chart'}>
|
||||
<DashboardInsider
|
||||
loading={fetchHook.isFetching}
|
||||
name={'accounts-chart'}>
|
||||
<DashboardActionsBar
|
||||
selectedRows={selectedRows}
|
||||
onFilterChanged={handleFilterChanged}
|
||||
@@ -236,8 +257,11 @@ function AccountsChart({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AccountsConnect,
|
||||
CustomViewConnect,
|
||||
ResourceConnect,
|
||||
DashboardConnect,
|
||||
withAccountsActions,
|
||||
withAccountsTableActions,
|
||||
withViewsActions,
|
||||
withResourceActions,
|
||||
withDashboardActions,
|
||||
|
||||
withAccounts,
|
||||
)(AccountsChart);
|
||||
191
client/src/containers/Accounts/AccountsDataTable.js
Normal file
191
client/src/containers/Accounts/AccountsDataTable.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Position,
|
||||
Classes,
|
||||
Tooltip,
|
||||
} from '@blueprintjs/core';
|
||||
import Icon from 'components/Icon';
|
||||
import { compose } from 'utils';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Money from 'components/Money';
|
||||
import { useUpdateEffect } from 'hooks';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
import {If} from 'components';
|
||||
|
||||
function AccountsDataTable({
|
||||
// # withAccounts
|
||||
accounts,
|
||||
accountsLoading,
|
||||
|
||||
// # withDialog.
|
||||
openDialog,
|
||||
|
||||
// own properties
|
||||
loading,
|
||||
onFetchData,
|
||||
onSelectedRowsChange,
|
||||
onDeleteAccount,
|
||||
onInactiveAccount,
|
||||
}) {
|
||||
const [initialMount, setInitialMount] = useState(false);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
if (!accountsLoading) {
|
||||
setInitialMount(true);
|
||||
}
|
||||
}, [accountsLoading, setInitialMount]);
|
||||
|
||||
const handleEditAccount = useCallback((account) => () => {
|
||||
openDialog('account-form', { action: 'edit', id: account.id });
|
||||
}, [openDialog]);
|
||||
|
||||
const handleNewParentAccount = useCallback((account) => {
|
||||
openDialog('account-form', { action: 'new_child', id: account.id });
|
||||
}, [openDialog]);
|
||||
|
||||
const actionMenuList = useCallback((account) => (
|
||||
<Menu>
|
||||
<MenuItem text='View Details' />
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text='Edit Account'
|
||||
onClick={handleEditAccount(account)} />
|
||||
<MenuItem
|
||||
text='New Account'
|
||||
onClick={() => handleNewParentAccount(account)} />
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text='Inactivate Account'
|
||||
onClick={() => onInactiveAccount(account)} />
|
||||
<MenuItem
|
||||
text='Delete Account'
|
||||
onClick={() => onDeleteAccount(account)} />
|
||||
</Menu>
|
||||
), [handleEditAccount, onDeleteAccount, onInactiveAccount]);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: 'Account Name',
|
||||
accessor: row => {
|
||||
return (row.description) ?
|
||||
(<Tooltip
|
||||
className={Classes.TOOLTIP_INDICATOR}
|
||||
content={row.description}
|
||||
position={Position.RIGHT_TOP}
|
||||
hoverOpenDelay={500}>
|
||||
{ row.name }
|
||||
</Tooltip>) : row.name;
|
||||
},
|
||||
className: 'account_name',
|
||||
width: 300,
|
||||
},
|
||||
{
|
||||
id: 'code',
|
||||
Header: 'Code',
|
||||
accessor: 'code',
|
||||
className: 'code',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
Header: 'Type',
|
||||
accessor: 'type.name',
|
||||
className: 'type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
id: 'normal',
|
||||
Header: 'Normal',
|
||||
Cell: ({ cell }) => {
|
||||
const account = cell.row.original;
|
||||
const type = account.type ? account.type.normal : '';
|
||||
const arrowDirection = type === 'credit' ? 'down' : 'up';
|
||||
|
||||
return (<Icon icon={`arrow-${arrowDirection}`} />);
|
||||
},
|
||||
className: 'normal',
|
||||
width: 75,
|
||||
},
|
||||
{
|
||||
id: 'balance',
|
||||
Header: 'Balance',
|
||||
Cell: ({ cell }) => {
|
||||
const account = cell.row.original;
|
||||
const {balance = null} = account;
|
||||
|
||||
return (balance) ?
|
||||
(<span>
|
||||
<Money amount={balance.amount} currency={balance.currency_code} />
|
||||
</span>) :
|
||||
(<span class="placeholder">--</span>);
|
||||
},
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Cell: ({ cell }) => (
|
||||
<Popover
|
||||
content={actionMenuList(cell.row.original)}
|
||||
position={Position.RIGHT_TOP}>
|
||||
<Button icon={<Icon icon='ellipsis-h' />} />
|
||||
</Popover>
|
||||
),
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
}
|
||||
], [actionMenuList]);
|
||||
|
||||
const selectionColumn = useMemo(() => ({
|
||||
minWidth: 50,
|
||||
width: 50,
|
||||
maxWidth: 50,
|
||||
}), [])
|
||||
|
||||
const handleDatatableFetchData = useCallback((...params) => {
|
||||
onFetchData && onFetchData(...params);
|
||||
}, []);
|
||||
|
||||
const handleSelectedRowsChange = useCallback((selectedRows) => {
|
||||
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
|
||||
}, [onSelectedRowsChange]);
|
||||
|
||||
return (
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<If condition={loading}>
|
||||
asdasdsadsa
|
||||
</If>
|
||||
<DataTable
|
||||
noInitialFetch={true}
|
||||
columns={columns}
|
||||
data={accounts}
|
||||
onFetchData={handleDatatableFetchData}
|
||||
manualSortBy={true}
|
||||
selectionColumn={selectionColumn}
|
||||
expandable={true}
|
||||
treeGraph={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
loading={accountsLoading && !initialMount}
|
||||
spinnerProps={{size: 30}} />
|
||||
</LoadingIndicator>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
withDashboardActions,
|
||||
withAccountsActions,
|
||||
withAccounts,
|
||||
)(AccountsDataTable);
|
||||
132
client/src/containers/Accounts/AccountsViewsTabs.js
Normal file
132
client/src/containers/Accounts/AccountsViewsTabs.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import React, {useEffect, useCallback} from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Alignment,
|
||||
Navbar,
|
||||
NavbarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams, withRouter } from 'react-router-dom';
|
||||
import Icon from 'components/Icon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
import {useUpdateEffect} from 'hooks';
|
||||
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
|
||||
import withViewDetail from 'containers/Views/withViewDetails';
|
||||
|
||||
function AccountsViewsTabs({
|
||||
// #withViewDetail
|
||||
viewId,
|
||||
viewItem,
|
||||
|
||||
// #withAccounts
|
||||
accountsViews,
|
||||
|
||||
// #withAccountsTableActions
|
||||
addAccountsTableQueries,
|
||||
changeAccountsCurrentView,
|
||||
|
||||
// #withDashboard
|
||||
setTopbarEditView,
|
||||
changePageSubtitle,
|
||||
|
||||
// props
|
||||
customViewChanged,
|
||||
onViewChanged,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { custom_view_id: customViewId = null } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
changeAccountsCurrentView(customViewId || -1);
|
||||
setTopbarEditView(customViewId);
|
||||
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
|
||||
|
||||
addAccountsTableQueries({
|
||||
custom_view_id: customViewId,
|
||||
});
|
||||
|
||||
return () => {
|
||||
setTopbarEditView(null);
|
||||
changePageSubtitle('');
|
||||
changeAccountsCurrentView(null)
|
||||
};
|
||||
}, [customViewId]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
onViewChanged && onViewChanged(customViewId);
|
||||
}, [customViewId]);
|
||||
|
||||
|
||||
// Handle click a new view tab.
|
||||
const handleClickNewView = () => {
|
||||
setTopbarEditView(null);
|
||||
history.push('/dashboard/custom_views/accounts/new');
|
||||
};
|
||||
|
||||
// Handle view tab link click.
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
};
|
||||
|
||||
const tabs = accountsViews.map((view) => {
|
||||
const baseUrl = '/dashboard/accounts';
|
||||
|
||||
const link = (
|
||||
<Link
|
||||
to={`${baseUrl}/${view.id}/custom_view`}
|
||||
onClick={handleViewLinkClick}
|
||||
>{ view.name }</Link>
|
||||
);
|
||||
return <Tab id={`custom_view_${view.id}`} title={link} />;
|
||||
});
|
||||
|
||||
return (
|
||||
<Navbar className='navbar--dashboard-views'>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<Tabs
|
||||
id='navbar'
|
||||
large={true}
|
||||
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
|
||||
className='tabs--dashboard-views'
|
||||
>
|
||||
<Tab
|
||||
id={'all'}
|
||||
title={<Link to={`/dashboard/accounts`}>All</Link>}
|
||||
onClick={handleViewLinkClick}
|
||||
/>
|
||||
{ tabs }
|
||||
<Button
|
||||
className='button--new-view'
|
||||
icon={<Icon icon='plus' />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tabs>
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
|
||||
// Mapping view id from matched route params.
|
||||
viewId: ownProps.match.params.custom_view_id,
|
||||
});
|
||||
|
||||
const withAccountsViewsTabs = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
withAccountsViewsTabs,
|
||||
withDashboard,
|
||||
withAccounts,
|
||||
withAccountsTableActions,
|
||||
withViewDetail
|
||||
)(AccountsViewsTabs);
|
||||
10
client/src/containers/Accounts/withAccountDetail.js
Normal file
10
client/src/containers/Accounts/withAccountDetail.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getItemById
|
||||
} from 'store/selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
account: getItemById(state.accounts.items, props.accountId),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
19
client/src/containers/Accounts/withAccounts.js
Normal file
19
client/src/containers/Accounts/withAccounts.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getAccountsItems,
|
||||
} from 'store/accounts/accounts.selectors';
|
||||
import {
|
||||
getResourceViews,
|
||||
} from 'store/customViews/customViews.selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountsViews: getResourceViews(state, 'accounts'),
|
||||
accounts: getAccountsItems(state, state.accounts.currentViewId),
|
||||
accountsTypes: state.accounts.accountsTypes,
|
||||
|
||||
accountsTableQuery: state.accounts.tableQuery,
|
||||
accountsLoading: state.accounts.loading,
|
||||
accountErrors: state.accounts.errors,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
22
client/src/containers/Accounts/withAccountsActions.js
Normal file
22
client/src/containers/Accounts/withAccountsActions.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchAccountTypes,
|
||||
fetchAccountsList,
|
||||
deleteAccount,
|
||||
inactiveAccount,
|
||||
submitAccount,
|
||||
fetchAccount,
|
||||
deleteBulkAccounts,
|
||||
} from 'store/accounts/accounts.actions';
|
||||
|
||||
const mapActionsToProps = (dispatch) => ({
|
||||
requestFetchAccounts: (query) => dispatch(fetchAccountsList({ query })),
|
||||
requestFetchAccountTypes: () => dispatch(fetchAccountTypes()),
|
||||
requestSubmitAccount: ({ form }) => dispatch(submitAccount({ form })),
|
||||
requestDeleteAccount: (id) => dispatch(deleteAccount({ id })),
|
||||
requestInactiveAccount: (id) => dispatch(inactiveAccount({ id })),
|
||||
requestFetchAccount: (id) => dispatch(fetchAccount({ id })),
|
||||
requestDeleteBulkAccounts: (ids) => dispatch(deleteBulkAccounts({ ids })),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps);
|
||||
24
client/src/containers/Accounts/withAccountsTableActions.js
Normal file
24
client/src/containers/Accounts/withAccountsTableActions.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
import {
|
||||
fetchAccountsTable,
|
||||
} from 'store/accounts/accounts.actions';
|
||||
|
||||
const mapActionsToProps = (dispatch) => ({
|
||||
requestFetchAccountsTable: (query = {}) => dispatch(fetchAccountsTable({ query: { ...query } })),
|
||||
changeAccountsCurrentView: (id) => dispatch({
|
||||
type: t.ACCOUNTS_SET_CURRENT_VIEW,
|
||||
currentViewId: parseInt(id, 10),
|
||||
}),
|
||||
setAccountsTableQuery: (key, value) => dispatch({
|
||||
type: 'ACCOUNTS_TABLE_QUERY_SET', key, value,
|
||||
}),
|
||||
addAccountsTableQueries: (queries) => dispatch({
|
||||
type: 'ACCOUNTS_TABLE_QUERIES_ADD', queries,
|
||||
}),
|
||||
setSelectedRowsAccounts: (ids) => dispatch({
|
||||
type: t.ACCOUNTS_SELECTED_ROWS_SET, payload: { ids },
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps);
|
||||
@@ -5,7 +5,7 @@ import { useIntl } from 'react-intl';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import { compose } from 'utils';
|
||||
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||
import withAuthenticationActions from './withAuthenticationActions';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
@@ -21,6 +21,7 @@ import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
|
||||
|
||||
function Invite({
|
||||
requestInviteAccept,
|
||||
requestInviteMetaByToken,
|
||||
@@ -224,4 +225,6 @@ function Invite({
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(AuthenticationConnect)(Invite);
|
||||
export default compose(
|
||||
withAuthenticationActions,
|
||||
)(Invite);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { connect } from 'react-redux';
|
||||
import { useIntl } from 'react-intl';
|
||||
import {
|
||||
Button,
|
||||
@@ -10,13 +9,12 @@ import {
|
||||
Intent,
|
||||
FormGroup,
|
||||
Checkbox,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import Toaster from 'components/AppToaster';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
import Icon from 'components/Icon';
|
||||
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||
import withAuthenticationActions from './withAuthenticationActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const ERRORS_TYPES = {
|
||||
@@ -162,5 +160,5 @@ function Login({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AuthenticationConnect
|
||||
withAuthenticationActions,
|
||||
)(Login);
|
||||
@@ -11,12 +11,13 @@ import {
|
||||
} from '@blueprintjs/core';
|
||||
import { Row, Col } from 'react-grid-system';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||
import withAuthenticationActions from './withAuthenticationActions';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
import { compose } from 'utils';
|
||||
import Icon from 'components/Icon';
|
||||
import { If } from 'components';
|
||||
|
||||
function Register({
|
||||
requestRegister,
|
||||
@@ -212,16 +213,16 @@ function Register({
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{ isSubmitting && (
|
||||
<If condition={isSubmitting}>
|
||||
<div class="authentication-page__loading-overlay">
|
||||
<Spinner size={50} />
|
||||
</div>
|
||||
)}
|
||||
</If>
|
||||
</div>
|
||||
</AuthInsider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AuthenticationConnect,
|
||||
withAuthenticationActions,
|
||||
)(Register);
|
||||
|
||||
@@ -13,9 +13,10 @@ import { Link, useParams, useHistory } from 'react-router-dom';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import { compose } from 'utils';
|
||||
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||
import withAuthenticationActions from './withAuthenticationActions';
|
||||
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
|
||||
|
||||
function ResetPassword({
|
||||
requestResetPassword,
|
||||
}) {
|
||||
@@ -131,5 +132,5 @@ function ResetPassword({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AuthenticationConnect,
|
||||
withAuthenticationActions,
|
||||
)(ResetPassword);
|
||||
|
||||
@@ -5,12 +5,17 @@ import { useIntl } from 'react-intl';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { Button, InputGroup, Intent, FormGroup } from '@blueprintjs/core';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import AuthenticationConnect from 'connectors/Authentication.connect';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
|
||||
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
|
||||
import withAuthenticationActions from './withAuthenticationActions';
|
||||
|
||||
|
||||
function SendResetPassword({
|
||||
requestSendResetPassword,
|
||||
}) {
|
||||
@@ -65,6 +70,7 @@ function SendResetPassword({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div class='reset-form'>
|
||||
@@ -110,5 +116,5 @@ function SendResetPassword({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AuthenticationConnect,
|
||||
withAuthenticationActions,
|
||||
)(SendResetPassword);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
login,
|
||||
resetPassword,
|
||||
sendResetPassword,
|
||||
inviteAccept,
|
||||
register,
|
||||
inviteMetaByToken,
|
||||
} from 'store/authentication/authentication.actions';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
requestLogin: (form) => dispatch(login({ form })),
|
||||
requestRegister: (form) => dispatch(register({ form })),
|
||||
requestSendResetPassword: (email) => dispatch(sendResetPassword({ email })),
|
||||
requestResetPassword: (form, token) => dispatch(resetPassword({ form, token })),
|
||||
requestInviteAccept: (form, token) => dispatch(inviteAccept({ form, token })),
|
||||
requestInviteMetaByToken: (token) => dispatch(inviteMetaByToken({ token })),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
@@ -1,24 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import CategoryList from 'components/Items/categoryList';
|
||||
import ItemFormDialog from 'connectors/ItemFormDialog.connect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const ItemCategoryList = ({ changePageTitle }) => {
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
id
|
||||
? changePageTitle('Edit Category Details')
|
||||
: changePageTitle('Category List');
|
||||
}, []);
|
||||
return (
|
||||
<DashboardInsider isLoading={null} name={'item-category-list'}>
|
||||
<CategoryList />
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(DashboardConnect, ItemFormDialog)(ItemCategoryList);
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ItemForm from 'components/Items/ItemForm';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import { compose } from 'utils';
|
||||
|
||||
const ItemFormContainer = ({
|
||||
changePageTitle,
|
||||
requestFetchAccounts,
|
||||
requestFetchItemCategories,
|
||||
}) => {
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
id ?
|
||||
changePageTitle('Edit Item Details') :
|
||||
changePageTitle('New Item');
|
||||
}, [id, changePageTitle]);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
requestFetchAccounts(),
|
||||
requestFetchItemCategories(),
|
||||
]);
|
||||
});
|
||||
return (
|
||||
<DashboardInsider loading={fetchHook.loading} name={'item-form'}>
|
||||
<ItemForm />
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
ItemsConnect,
|
||||
AccountsConnect,
|
||||
ItemCategoryConnect,
|
||||
)(ItemFormContainer);
|
||||
@@ -1,102 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import useAsync from 'hooks/async';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import { compose } from 'utils';
|
||||
import ItemsCategoryList from 'components/Items/ItemsCategoryList';
|
||||
import ItemsCategoryActionsBar from './ItemsCategoryActionsBar';
|
||||
import { Alert, Intent } from '@blueprintjs/core';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
|
||||
const ItemCategoriesList = ({
|
||||
changePageTitle,
|
||||
views,
|
||||
requestFetchItemCategories,
|
||||
requestEditItemCategory,
|
||||
requestDeleteItemCategory,
|
||||
}) => {
|
||||
const { id } = useParams();
|
||||
const [deleteCategory, setDeleteCategory] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
id
|
||||
? changePageTitle('Edit Item Details')
|
||||
: changePageTitle('Categories List');
|
||||
}, [id, changePageTitle]);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
requestFetchItemCategories(),
|
||||
]);
|
||||
}, false);
|
||||
|
||||
const handelDeleteCategory = useCallback((category) => {
|
||||
setDeleteCategory(category);
|
||||
}, [setDeleteCategory]);
|
||||
|
||||
const handelEditCategory = category => {};
|
||||
|
||||
const handelCancelCategoryDelete = useCallback(() => {
|
||||
setDeleteCategory(false);
|
||||
}, [setDeleteCategory]);
|
||||
|
||||
const handelConfirmCategoryDelete = useCallback(() => {
|
||||
requestDeleteItemCategory(deleteCategory.id).then(() => {
|
||||
setDeleteCategory(false);
|
||||
AppToaster.show({
|
||||
message: 'the_category_has_been_delete'
|
||||
});
|
||||
});
|
||||
}, [deleteCategory, requestDeleteItemCategory, setDeleteCategory]);
|
||||
|
||||
const handleFetchData = useCallback(() => {
|
||||
fetchHook.execute();
|
||||
}, []);
|
||||
|
||||
// Handle selected rows change.
|
||||
const handleSelectedRowsChange = useCallback((accounts) => {
|
||||
setSelectedRows(accounts);
|
||||
}, [setSelectedRows]);
|
||||
|
||||
return (
|
||||
<DashboardInsider loading={fetchHook.pending} name="items-categories">
|
||||
<ItemsCategoryActionsBar
|
||||
views={views}
|
||||
onDeleteCategory={handelDeleteCategory}
|
||||
selectedRows={selectedRows}
|
||||
/>
|
||||
<DashboardPageContent>
|
||||
<ItemsCategoryList
|
||||
onDeleteCategory={handelDeleteCategory}
|
||||
onFetchData={handleFetchData}
|
||||
onEditCategory={handelEditCategory}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
cancelButtonText='Cancel'
|
||||
confirmButtonText='Move to Trash'
|
||||
icon='trash'
|
||||
intent={Intent.DANGER}
|
||||
isOpen={deleteCategory}
|
||||
onCancel={handelCancelCategoryDelete}
|
||||
onConfirm={handelConfirmCategoryDelete}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to move <b>filename</b> to Trash? You will be
|
||||
able to restore it later, but it will become private to you.
|
||||
</p>
|
||||
</Alert>
|
||||
</DashboardPageContent>
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
ItemsCategoryConnect
|
||||
)(ItemCategoriesList);
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, {useEffect, useState, useCallback} from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import { Intent, Alert } from '@blueprintjs/core';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
import ViewForm from 'components/Views/ViewForm';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ResourceConnect from 'connectors/Resource.connector';
|
||||
import ViewConnect from 'connectors/View.connector';
|
||||
import {compose} from 'utils';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
|
||||
function ViewFormPage({
|
||||
changePageTitle,
|
||||
fetchResourceFields,
|
||||
fetchResourceColumns,
|
||||
fetchView,
|
||||
getResourceColumns,
|
||||
getResourceFields,
|
||||
submitView,
|
||||
getViewMeta,
|
||||
deleteView,
|
||||
}) {
|
||||
const { resource_slug: resourceSlug, view_id: viewId } = useParams();
|
||||
|
||||
const columns = getResourceColumns('accounts');
|
||||
const fields = getResourceFields('accounts');
|
||||
|
||||
const viewForm = (viewId) ? getViewMeta(viewId) : null;
|
||||
|
||||
const [stateDeleteView, setStateDeleteView] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewId) {
|
||||
changePageTitle('Edit Custom View');
|
||||
} else {
|
||||
changePageTitle('New Custom View');
|
||||
}
|
||||
}, [viewId, changePageTitle]);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchResourceColumns('accounts'),
|
||||
fetchResourceFields('accounts'),
|
||||
...(viewId) ? [
|
||||
fetchView(viewId),
|
||||
] : [],
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const handleDeleteView = useCallback((view) => {
|
||||
setStateDeleteView(view);
|
||||
}, []);
|
||||
|
||||
const handleCancelDeleteView = useCallback(() => {
|
||||
setStateDeleteView(null);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteView = useCallback(() => {
|
||||
deleteView(stateDeleteView.id).then((response) => {
|
||||
setStateDeleteView(null);
|
||||
AppToaster.show({
|
||||
message: 'the_custom_view_has_been_deleted',
|
||||
});
|
||||
})
|
||||
}, [deleteView, stateDeleteView]);
|
||||
|
||||
return (
|
||||
<DashboardInsider name={'view-form'} loading={fetchHook.loading} mount={false}>
|
||||
<DashboardPageContent>
|
||||
<ViewForm
|
||||
resourceName={resourceSlug}
|
||||
columns={columns}
|
||||
fields={fields}
|
||||
viewForm={viewForm}
|
||||
onDelete={handleDeleteView} />
|
||||
|
||||
<Alert
|
||||
cancelButtonText="Cancel"
|
||||
confirmButtonText="Move to Trash"
|
||||
icon="trash"
|
||||
intent={Intent.DANGER}
|
||||
isOpen={stateDeleteView}
|
||||
onCancel={handleCancelDeleteView}
|
||||
onConfirm={handleConfirmDeleteView}>
|
||||
<p>
|
||||
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
|
||||
but it will become private to you.
|
||||
</p>
|
||||
</Alert>
|
||||
</DashboardPageContent>
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
ResourceConnect,
|
||||
ViewConnect,
|
||||
)(ViewFormPage);
|
||||
27
client/src/containers/Dashboard/withDashboard.js
Normal file
27
client/src/containers/Dashboard/withDashboard.js
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import t from 'store/types';
|
||||
|
||||
const mapActionsToProps = (dispatch) => ({
|
||||
changePageTitle: (pageTitle) => dispatch({
|
||||
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle
|
||||
}),
|
||||
|
||||
changePageSubtitle: (pageSubtitle) => dispatch({
|
||||
type: t.ALTER_DASHBOARD_PAGE_SUBTITLE, pageSubtitle,
|
||||
}),
|
||||
|
||||
setTopbarEditView: (id) => dispatch({
|
||||
type: t.SET_TOPBAR_EDIT_VIEW, id,
|
||||
}),
|
||||
|
||||
setDashboardRequestLoading: () => dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||
}),
|
||||
|
||||
setDashboardRequestCompleted: () => dispatch({
|
||||
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps);
|
||||
30
client/src/containers/Dialogs/AccountFormDialog.container.js
Normal file
30
client/src/containers/Dialogs/AccountFormDialog.container.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import {connect} from 'react-redux';
|
||||
import { compose } from 'utils';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import DialogReduxConnect from 'components/DialogReduxConnect';
|
||||
import {getDialogPayload} from 'store/dashboard/dashboard.reducer';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withAccountDetail from 'containers/Accounts/withAccountDetail';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
const dialogPayload = getDialogPayload(state, 'account-form');
|
||||
|
||||
return {
|
||||
name: 'account-form',
|
||||
payload: {action: 'new', id: null, ...dialogPayload},
|
||||
|
||||
accountId: dialogPayload?.id || null,
|
||||
};
|
||||
};
|
||||
|
||||
const AccountFormDialogConnect = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
AccountFormDialogConnect,
|
||||
withAccountsActions,
|
||||
withAccountDetail,
|
||||
withAccounts,
|
||||
DialogReduxConnect,
|
||||
DialogConnect,
|
||||
);
|
||||
@@ -15,31 +15,40 @@ import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { omit } from 'lodash';
|
||||
import { compose } from 'utils';
|
||||
import useAsync from 'hooks/async';
|
||||
import { useQuery, queryCache } from 'react-query';
|
||||
|
||||
import Dialog from 'components/Dialog';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import DialogReduxConnect from 'components/DialogReduxConnect';
|
||||
import AccountFormDialogConnect from 'connectors/AccountFormDialog.connector';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
|
||||
import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'components/Icon';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import { fetchAccountTypes } from 'store/accounts/accounts.actions';
|
||||
|
||||
|
||||
function AccountFormDialog({
|
||||
name,
|
||||
payload,
|
||||
isOpen,
|
||||
|
||||
// #withAccounts
|
||||
accountsTypes,
|
||||
accounts,
|
||||
|
||||
// #withAccountDetail
|
||||
account,
|
||||
|
||||
// #withAccountsActions
|
||||
requestFetchAccounts,
|
||||
requestFetchAccountTypes,
|
||||
requestFetchAccount,
|
||||
closeDialog,
|
||||
requestSubmitAccount,
|
||||
requestEditAccount,
|
||||
getAccountById,
|
||||
|
||||
// #withDialog
|
||||
closeDialog,
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const accountFormValidationSchema = Yup.object().shape({
|
||||
@@ -63,10 +72,6 @@ function AccountFormDialog({
|
||||
accounts.find(a => a.id === payload.id) : null,
|
||||
);
|
||||
|
||||
const editAccount = useMemo(() =>
|
||||
payload.action === 'edit' ? getAccountById(payload.id) : null,
|
||||
[payload, getAccountById]);
|
||||
|
||||
const transformApiErrors = (errors) => {
|
||||
const fields = {};
|
||||
if (errors.find(e => e.type === 'NOT_UNIQUE_CODE')) {
|
||||
@@ -79,8 +84,7 @@ function AccountFormDialog({
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
...(payload.action === 'edit' && editAccount)
|
||||
? editAccount : initialValues,
|
||||
...(payload.action === 'edit' && account) ? account : initialValues,
|
||||
},
|
||||
validationSchema: accountFormValidationSchema,
|
||||
onSubmit: (values, { setSubmitting, setErrors }) => {
|
||||
@@ -97,12 +101,13 @@ function AccountFormDialog({
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
queryCache.refetchQueries('accounts-table', { force: true });
|
||||
}).catch((errors) => {
|
||||
setSubmitting(false);
|
||||
setErrors(transformApiErrors(errors));
|
||||
});
|
||||
} else {
|
||||
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then(response => {
|
||||
requestSubmitAccount({ form: { ...omit(values, exclude) } }).then((response) => {
|
||||
closeDialog(name);
|
||||
AppToaster.show({
|
||||
message: 'the_account_has_been_submit',
|
||||
@@ -110,6 +115,7 @@ function AccountFormDialog({
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
setSubmitting(false);
|
||||
queryCache.refetchQueries('accounts-table', { force: true });
|
||||
}).catch((errors) => {
|
||||
setSubmitting(false);
|
||||
setErrors(transformApiErrors(errors));
|
||||
@@ -121,13 +127,13 @@ function AccountFormDialog({
|
||||
|
||||
// Set default account type.
|
||||
useEffect(() => {
|
||||
if (editAccount && editAccount.account_type_id) {
|
||||
if (account && account.account_type_id) {
|
||||
const defaultType = accountsTypes.find((t) =>
|
||||
t.id === editAccount.account_type_id);
|
||||
t.id === account.account_type_id);
|
||||
|
||||
defaultType && setSelectedAccountType(defaultType);
|
||||
}
|
||||
}, [editAccount, accountsTypes]);
|
||||
}, [account, accountsTypes]);
|
||||
|
||||
// Filters accounts types items.
|
||||
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
|
||||
@@ -168,18 +174,32 @@ function AccountFormDialog({
|
||||
// Handles dialog close.
|
||||
const handleClose = useCallback(() => { closeDialog(name); }, [closeDialog, name]);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
requestFetchAccounts(),
|
||||
requestFetchAccountTypes(),
|
||||
// Fetch the target in case edit mode.
|
||||
...(payload.action === 'edit' ?
|
||||
[requestFetchAccount(payload.id)] : [])
|
||||
]);
|
||||
}, false);
|
||||
// Fetches accounts list.
|
||||
const fetchAccountsList = useQuery('accounts-list',
|
||||
() => requestFetchAccounts(), { manual: true });
|
||||
|
||||
// Fetches accounts types.
|
||||
const fetchAccountsTypes = useQuery('accounts-types-list', async () => {
|
||||
await requestFetchAccountTypes();
|
||||
}, { manual: true });
|
||||
|
||||
// Fetch the given account id on edit mode.
|
||||
const fetchAccount = useQuery(
|
||||
payload.action === 'edit' && ['account', payload.id],
|
||||
(key, id) => requestFetchAccount(id),
|
||||
{ manual: true });
|
||||
|
||||
const isFetching = (
|
||||
fetchAccountsList.isFetching ||
|
||||
fetchAccountTypes.isFetching ||
|
||||
fetchAccount.isFetching);
|
||||
|
||||
// Fetch requests on dialog opening.
|
||||
const onDialogOpening = useCallback(() => { fetchHook.execute(); }, [fetchHook]);
|
||||
const onDialogOpening = useCallback(() => {
|
||||
fetchAccountsList.refetch();
|
||||
fetchAccountsTypes.refetch();
|
||||
fetchAccount.refetch();
|
||||
}, []);
|
||||
|
||||
const onChangeAccountType = useCallback((accountType) => {
|
||||
setSelectedAccountType(accountType);
|
||||
@@ -211,7 +231,7 @@ function AccountFormDialog({
|
||||
name={name}
|
||||
title={payload.action === 'edit' ? 'Edit Account' : 'New Account'}
|
||||
className={{
|
||||
'dialog--loading': fetchHook.pending,
|
||||
'dialog--loading': isFetching,
|
||||
'dialog--account-form': true
|
||||
}}
|
||||
autoFocus={true}
|
||||
@@ -219,7 +239,7 @@ function AccountFormDialog({
|
||||
onClosed={onDialogClosed}
|
||||
onOpening={onDialogOpening}
|
||||
isOpen={isOpen}
|
||||
isLoading={fetchHook.pending}
|
||||
isLoading={isFetching}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
@@ -350,9 +370,6 @@ function AccountFormDialog({
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
AccountFormDialogConnect,
|
||||
AccountsConnect,
|
||||
DialogReduxConnect,
|
||||
DialogConnect
|
||||
)(AccountFormDialog);
|
||||
export default AccountFormDialogContainer(
|
||||
AccountFormDialog,
|
||||
);
|
||||
@@ -14,31 +14,52 @@ import * as Yup from 'yup';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useFormik } from 'formik';
|
||||
import { compose } from 'utils';
|
||||
import Dialog from 'components/Dialog';
|
||||
import useAsync from 'hooks/async';
|
||||
import { useQuery } from 'react-query';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
|
||||
import Dialog from 'components/Dialog';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import DialogReduxConnect from 'components/DialogReduxConnect';
|
||||
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
|
||||
import withItemCategoryDetail from 'containers/Items/withItemCategoryDetail';
|
||||
import withItemCategories from 'containers/Items/withItemCategories';
|
||||
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
|
||||
|
||||
import Icon from 'components/Icon';
|
||||
|
||||
|
||||
function ItemCategoryDialog({
|
||||
name,
|
||||
payload,
|
||||
isOpen,
|
||||
|
||||
// #withDialog
|
||||
openDialog,
|
||||
closeDialog,
|
||||
categories,
|
||||
|
||||
// #withItemCategoryDetail
|
||||
itemCategoryId,
|
||||
itemCategory,
|
||||
|
||||
// #withItemCategories
|
||||
categoriesList,
|
||||
|
||||
// #withItemCategoriesActions
|
||||
requestSubmitItemCategory,
|
||||
requestFetchItemCategories,
|
||||
requestEditItemCategory,
|
||||
editItemCategory
|
||||
}) {
|
||||
const [selectedParentCategory, setParentCategory] = useState(null);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const fetchList = useQuery(['items-categories-list'],
|
||||
() => requestFetchItemCategories());
|
||||
|
||||
const ValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required(intl.formatMessage({ id: 'required' })),
|
||||
parent_category_id: Yup.string().nullable(),
|
||||
@@ -51,12 +72,12 @@ function ItemCategoryDialog({
|
||||
parent_category_id: null
|
||||
}), []);
|
||||
|
||||
//Formik
|
||||
// Formik
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
...(payload.action === 'edit' &&
|
||||
pick(editItemCategory, Object.keys(initialValues)))
|
||||
pick(itemCategory, Object.keys(initialValues)))
|
||||
},
|
||||
validationSchema: ValidationSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
@@ -104,15 +125,13 @@ function ItemCategoryDialog({
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Handle the dialog closing.
|
||||
const handleClose = useCallback(() => { closeDialog(name); }, [name, closeDialog]);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
requestFetchItemCategories(),
|
||||
]);
|
||||
}, false);
|
||||
|
||||
const onDialogOpening = useCallback(() => { fetchHook.execute(); }, [fetchHook]);
|
||||
// Handle the dialog opening.
|
||||
const onDialogOpening = useCallback(() => {
|
||||
fetchList.refetch();
|
||||
}, [fetchList]);
|
||||
|
||||
const onChangeParentCategory = useCallback((parentCategory) => {
|
||||
setParentCategory(parentCategory);
|
||||
@@ -132,14 +151,14 @@ function ItemCategoryDialog({
|
||||
name={name}
|
||||
title={payload.action === 'edit' ? 'Edit Category' : ' New Category'}
|
||||
className={classNames({
|
||||
'dialog--loading': fetchHook.pending,
|
||||
'dialog--loading': fetchList.isFetching,
|
||||
},
|
||||
'dialog--category-form',
|
||||
)}
|
||||
isOpen={isOpen}
|
||||
onClosed={onDialogClosed}
|
||||
onOpening={onDialogOpening}
|
||||
isLoading={fetchHook.pending}
|
||||
isLoading={fetchList.isFetching}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
@@ -172,7 +191,7 @@ function ItemCategoryDialog({
|
||||
intent={(errors.parent_category_id && touched.parent_category_id) && Intent.DANGER}
|
||||
>
|
||||
<Select
|
||||
items={Object.values(categories)}
|
||||
items={categoriesList}
|
||||
noResults={<MenuItem disabled={true} text='No results.' />}
|
||||
itemRenderer={parentCategoryItem}
|
||||
itemPredicate={filterItemCategory}
|
||||
@@ -214,8 +233,25 @@ function ItemCategoryDialog({
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
ItemsCategoryConnect,
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const dialogPayload = getDialogPayload(state, 'item-category-form');
|
||||
|
||||
return {
|
||||
name: 'account-form',
|
||||
payload: {action: 'new', id: null, ...dialogPayload},
|
||||
|
||||
itemCategoryId: dialogPayload?.id || null,
|
||||
};
|
||||
};
|
||||
|
||||
const withItemCategoryDialog = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
DialogReduxConnect
|
||||
DialogReduxConnect,
|
||||
withItemCategoryDialog,
|
||||
withItemCategoryDetail,
|
||||
withItemCategories,
|
||||
withItemCategoriesActions
|
||||
)(ItemCategoryDialog);
|
||||
@@ -32,8 +32,7 @@ function ExpenseFormContainer({
|
||||
});
|
||||
return (
|
||||
<DashboardInsider isLoading={fetchHook.loading} name={'expense-form'}>
|
||||
<ExpenseForm {...{submitExpense, editExpense, accounts, currencies} } />
|
||||
|
||||
<ExpenseForm {...{submitExpense, editExpense, accounts, currencies} } />
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useMemo, useCallback} from 'react';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
|
||||
import {Row, Col} from 'react-grid-system';
|
||||
import {
|
||||
Button,
|
||||
@@ -12,7 +12,7 @@ import moment from 'moment';
|
||||
import Icon from 'components/Icon';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import FinancialStatementDateRange from 'containers/Dashboard/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
|
||||
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
|
||||
import RadiosAccountingBasis from '../RadiosAccountingBasis';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import GeneralLedgerTable from 'containers/Dashboard/FinancialStatements/GeneralLedger/GeneralLedgerTable';
|
||||
import GeneralLedgerTable from 'containers/FinancialStatements/GeneralLedger/GeneralLedgerTable';
|
||||
import useAsync from 'hooks/async';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import GeneralLedgerConnect from 'connectors/GeneralLedgerSheet.connect';
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useState, useMemo, useEffect, useCallback} from 'react';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Button,
|
||||
@@ -15,7 +15,7 @@ import AccountsConnect from 'connectors/Accounts.connector'
|
||||
import classNames from 'classnames';
|
||||
import AccountsMultiSelect from 'components/AccountsMultiSelect';
|
||||
import {useFormik} from 'formik';
|
||||
import FinancialStatementDateRange from 'containers/Dashboard/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
|
||||
import * as Yup from 'yup';
|
||||
import RadiosAccountingBasis from '../RadiosAccountingBasis';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {useState, useCallback, useEffect, useMemo} from 'react';
|
||||
import {compose} from 'utils';
|
||||
import JournalConnect from 'connectors/Journal.connect';
|
||||
import JournalHeader from 'containers/Dashboard/FinancialStatements/Journal/JournalHeader';
|
||||
import JournalHeader from 'containers/FinancialStatements/Journal/JournalHeader';
|
||||
import useAsync from 'hooks/async';
|
||||
import {useIntl} from 'react-intl';
|
||||
import moment from 'moment';
|
||||
@@ -8,8 +8,8 @@ import moment from 'moment';
|
||||
import {useFormik} from 'formik';
|
||||
import {useIntl} from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
import FinancialStatementDateRange from 'containers/Dashboard/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
|
||||
|
||||
|
||||
export default function JournalHeader({
|
||||
@@ -7,8 +7,8 @@ import moment from 'moment';
|
||||
import {useFormik} from 'formik';
|
||||
import {useIntl} from 'react-intl';
|
||||
import * as Yup from 'yup';
|
||||
import FinancialStatementDateRange from 'containers/Dashboard/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
|
||||
import SelectsListColumnsBy from '../SelectDisplayColumnsBy';
|
||||
import RadiosAccountingBasis from '../RadiosAccountingBasis';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useState, useCallback, useMemo} from 'react';
|
||||
import FinancialStatementHeader from 'containers/Dashboard/FinancialStatements/FinancialStatementHeader';
|
||||
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
|
||||
import {Row, Col} from 'react-grid-system';
|
||||
import {
|
||||
Button,
|
||||
@@ -17,7 +17,7 @@ import {useIntl} from 'react-intl';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Icon from 'components/Icon';
|
||||
import FinancialStatementDateRange from 'containers/Dashboard/FinancialStatements/FinancialStatementDateRange';
|
||||
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
|
||||
|
||||
export default function TrialBalanceSheetHeader({
|
||||
pageFilter,
|
||||
@@ -16,4 +16,5 @@ const mapActionsToProps = (dispatch) => ({
|
||||
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapActionsToProps)(DashboardHomepage);
|
||||
53
client/src/containers/Items/ItemCategoriesList.js
Normal file
53
client/src/containers/Items/ItemCategoriesList.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useAsync from 'hooks/async';
|
||||
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import ItemCategoriesDataTable from 'containers/Items/ItemCategoriesTable';
|
||||
import ItemsCategoryActionsBar from 'containers/Items/ItemsCategoryActionsBar';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
|
||||
import withItemCategories from 'containers/Items/withItemCategories';
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
const ItemCategoryList = ({
|
||||
changePageTitle,
|
||||
requestFetchItemCategories,
|
||||
}) => {
|
||||
const { id } = useParams();
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
id
|
||||
? changePageTitle('Edit Category Details')
|
||||
: changePageTitle('Category List');
|
||||
}, []);
|
||||
|
||||
const fetchCategories = useAsync(() => {
|
||||
return Promise.all([
|
||||
requestFetchItemCategories(),
|
||||
]);
|
||||
});
|
||||
|
||||
const handleFilterChanged = useCallback(() => {
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DashboardInsider name={'item-category-list'}>
|
||||
<ItemsCategoryActionsBar
|
||||
onFilterChanged={handleFilterChanged}
|
||||
selectedRows={selectedRows} />
|
||||
|
||||
<ItemCategoriesDataTable />
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
withItemCategoriesActions,
|
||||
withItemCategories,
|
||||
)(ItemCategoryList);
|
||||
108
client/src/containers/Items/ItemCategoriesTable.js
Normal file
108
client/src/containers/Items/ItemCategoriesTable.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
import { compose } from 'utils';
|
||||
import DataTable from 'components/DataTable';
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
const ItemsCategoryList = ({
|
||||
categories,
|
||||
onFetchData,
|
||||
onDeleteCategory,
|
||||
onEditCategory,
|
||||
openDialog,
|
||||
count,
|
||||
onSelectedRowsChange,
|
||||
}) => {
|
||||
const handelEditCategory = (category) => () => {
|
||||
openDialog('item-form', { action: 'edit', id: category.id });
|
||||
onEditCategory(category.id);
|
||||
};
|
||||
|
||||
const handleDeleteCategory = (category) => () => {
|
||||
onDeleteCategory(category);
|
||||
};
|
||||
|
||||
const actionMenuList = (category) => (
|
||||
<Menu>
|
||||
<MenuItem text='Edit Category' onClick={handelEditCategory(category)} />
|
||||
<MenuItem
|
||||
text='Delete Category'
|
||||
onClick={handleDeleteCategory(category)}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
id: 'name',
|
||||
Header: 'Category Name',
|
||||
accessor: 'name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
Header: 'Description',
|
||||
accessor: 'description',
|
||||
className: 'description',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
id: 'count',
|
||||
Header: 'Count',
|
||||
accessor: (r) => r.count || '',
|
||||
className: 'count',
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: '',
|
||||
Cell: ({ cell }) => (
|
||||
<Popover
|
||||
content={actionMenuList(cell.row.original)}
|
||||
position={Position.RIGHT_BOTTOM}
|
||||
>
|
||||
<Button icon={<Icon icon='ellipsis-h' />} />
|
||||
</Popover>
|
||||
),
|
||||
className: 'actions',
|
||||
width: 50,
|
||||
disableResizing: false
|
||||
},
|
||||
], [actionMenuList]);
|
||||
|
||||
const handelFetchData = useCallback(() => {
|
||||
onFetchData && onFetchData();
|
||||
}, []);
|
||||
|
||||
const handleSelectedRowsChange = useCallback((selectedRows) => {
|
||||
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
|
||||
}, [onSelectedRowsChange]);
|
||||
|
||||
return (
|
||||
<LoadingIndicator spinnerSize={30}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={Object.values(categories)}
|
||||
onFetchData={handelFetchData}
|
||||
manualSortBy={true}
|
||||
selectionColumn={true}
|
||||
expandable={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange}
|
||||
/>
|
||||
</LoadingIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
ItemsCategoryConnect,
|
||||
)(ItemsCategoryList);
|
||||
459
client/src/containers/Items/ItemForm.js
Normal file
459
client/src/containers/Items/ItemForm.js
Normal file
@@ -0,0 +1,459 @@
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import {
|
||||
FormGroup,
|
||||
MenuItem,
|
||||
Intent,
|
||||
InputGroup,
|
||||
HTMLSelect,
|
||||
Button,
|
||||
Classes,
|
||||
Checkbox,
|
||||
} from '@blueprintjs/core';
|
||||
import { Row, Col } from 'react-grid-system';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import AccountsConnect from 'connectors/Accounts.connector';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import {compose} from 'utils';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'components/Icon';
|
||||
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||
import {useHistory} from 'react-router-dom';
|
||||
import Dragzone from 'components/Dragzone';
|
||||
import MediaConnect from 'connectors/Media.connect';
|
||||
import useMedia from 'hooks/useMedia';
|
||||
|
||||
const ItemForm = ({
|
||||
requestSubmitItem,
|
||||
|
||||
accounts,
|
||||
categories,
|
||||
|
||||
requestSubmitMedia,
|
||||
requestDeleteMedia,
|
||||
}) => {
|
||||
const [selectedAccounts, setSelectedAccounts] = useState({});
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
files,
|
||||
setFiles,
|
||||
saveMedia,
|
||||
deletedFiles,
|
||||
setDeletedFiles,
|
||||
deleteMedia,
|
||||
} = useMedia({
|
||||
saveCallback: requestSubmitMedia,
|
||||
deleteCallback: requestDeleteMedia,
|
||||
})
|
||||
|
||||
const ItemTypeDisplay = useMemo(() => ([
|
||||
{ value: null, label: 'Select Item Type' },
|
||||
{ value: 'service', label: 'Service' },
|
||||
{ value: 'inventory', label: 'Inventory' },
|
||||
{ value: 'non-inventory', label: 'Non-Inventory' }
|
||||
]), []);
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
active: Yup.boolean(),
|
||||
name: Yup.string().required(),
|
||||
type: Yup.string().trim().required(),
|
||||
sku: Yup.string().trim(),
|
||||
cost_price: Yup.number(),
|
||||
sell_price: Yup.number(),
|
||||
cost_account_id: Yup.number().required(),
|
||||
sell_account_id: Yup.number().required(),
|
||||
inventory_account_id: Yup.number().when('type', {
|
||||
is: (value) => value === 'inventory',
|
||||
then: Yup.number().required(),
|
||||
otherwise: Yup.number().nullable(),
|
||||
}),
|
||||
category_id: Yup.number().nullable(),
|
||||
stock: Yup.string() || Yup.boolean()
|
||||
});
|
||||
|
||||
const initialValues = useMemo(() => ({
|
||||
active: true,
|
||||
name: '',
|
||||
type: '',
|
||||
sku: '',
|
||||
cost_price: 0,
|
||||
sell_price: 0,
|
||||
cost_account_id: null,
|
||||
sell_account_id: null,
|
||||
inventory_account_id: null,
|
||||
category_id: null,
|
||||
note: '',
|
||||
}), []);
|
||||
|
||||
const {
|
||||
getFieldProps,
|
||||
setFieldValue,
|
||||
values,
|
||||
touched,
|
||||
errors,
|
||||
handleSubmit,
|
||||
} = useFormik({
|
||||
enableReinitialize: true,
|
||||
validationSchema: validationSchema,
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
},
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
const saveItem = (mediaIds) => {
|
||||
const formValues = { ...values, media_ids: mediaIds };
|
||||
|
||||
return requestSubmitItem(formValues).then((response) => {
|
||||
AppToaster.show({
|
||||
message: 'The_Items_has_been_submit'
|
||||
});
|
||||
setSubmitting(false);
|
||||
history.push('/dashboard/items');
|
||||
})
|
||||
.catch((error) => {
|
||||
setSubmitting(false);
|
||||
});
|
||||
};
|
||||
|
||||
Promise.all([
|
||||
saveMedia(),
|
||||
deleteMedia(),
|
||||
]).then(([savedMediaResponses]) => {
|
||||
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
|
||||
return saveItem(mediaIds);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const accountItem = useCallback((item, { handleClick }) => (
|
||||
<MenuItem key={item.id} text={item.name} label={item.code} onClick={handleClick} />
|
||||
), []);
|
||||
|
||||
// Filter Account Items
|
||||
const filterAccounts = (query, account, _index, exactMatch) => {
|
||||
const normalizedTitle = account.name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
return normalizedTitle === normalizedQuery;
|
||||
} else {
|
||||
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
|
||||
}
|
||||
};
|
||||
|
||||
const onItemAccountSelect = useCallback((filedName) => {
|
||||
return (account) => {
|
||||
setSelectedAccounts({
|
||||
...selectedAccounts,
|
||||
[filedName]: account
|
||||
});
|
||||
setFieldValue(filedName, account.id);
|
||||
};
|
||||
}, [setFieldValue, selectedAccounts]);
|
||||
|
||||
const categoryItem = useCallback((item, { handleClick }) => (
|
||||
<MenuItem text={item.name} onClick={handleClick} />
|
||||
), []);
|
||||
|
||||
const getSelectedAccountLabel = useCallback((fieldName, defaultLabel) => {
|
||||
return typeof selectedAccounts[fieldName] !== 'undefined'
|
||||
? selectedAccounts[fieldName].name : defaultLabel;
|
||||
}, [selectedAccounts]);
|
||||
|
||||
const requiredSpan = useMemo(() => (<span class="required">*</span>), []);
|
||||
const infoIcon = useMemo(() => (<Icon icon="info-circle" iconSize={12} />), []);
|
||||
|
||||
const handleMoneyInputChange = (fieldKey) => (e, value) => {
|
||||
setFieldValue(fieldKey, value);
|
||||
};
|
||||
|
||||
const initialAttachmentFiles = useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const handleDropFiles = useCallback((_files) => {
|
||||
setFiles(_files.filter((file) => file.uploaded === false));
|
||||
}, []);
|
||||
|
||||
const handleDeleteFile = useCallback((_deletedFiles) => {
|
||||
_deletedFiles.forEach((deletedFile) => {
|
||||
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||
setDeletedFiles([
|
||||
...deletedFiles, deletedFile.metadata.id,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}, [setDeletedFiles, deletedFiles]);
|
||||
|
||||
const handleCancelClickBtn = () => { history.goBack(); };
|
||||
|
||||
return (
|
||||
<div class='item-form'>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div class="item-form__primary-section">
|
||||
<Row>
|
||||
<Col xs={7}>
|
||||
<FormGroup
|
||||
medium={true}
|
||||
label={'Item Type'}
|
||||
labelInfo={requiredSpan}
|
||||
className={'form-group--item-type'}
|
||||
intent={(errors.type && touched.type) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="type" />}
|
||||
inline={true}
|
||||
>
|
||||
<HTMLSelect
|
||||
fill={true}
|
||||
options={ItemTypeDisplay}
|
||||
{...getFieldProps('type')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Item Name'}
|
||||
labelInfo={requiredSpan}
|
||||
className={'form-group--item-name'}
|
||||
intent={(errors.name && touched.name) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="name" />}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={(errors.name && touched.name) && Intent.DANGER}
|
||||
{...getFieldProps('name')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'SKU'}
|
||||
labelInfo={infoIcon}
|
||||
className={'form-group--item-sku'}
|
||||
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="sku" />}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={(errors.sku && touched.sku) && Intent.DANGER}
|
||||
{...getFieldProps('sku')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Category'}
|
||||
labelInfo={infoIcon}
|
||||
inline={true}
|
||||
intent={(errors.category_id && touched.category_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="category" />}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--category',
|
||||
Classes.FILL,
|
||||
)}
|
||||
>
|
||||
<Select
|
||||
items={categories}
|
||||
itemRenderer={categoryItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('category_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('category_id', 'Select category')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={' '}
|
||||
inline={true}
|
||||
className={'form-group--active'}
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={'Active'}
|
||||
defaultChecked={values.active}
|
||||
{...getFieldProps('active')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col xs={3}>
|
||||
<Dragzone
|
||||
initialFiles={initialAttachmentFiles}
|
||||
onDrop={handleDropFiles}
|
||||
onDeleteFile={handleDeleteFile}
|
||||
hint={'Attachments: Maxiumum size: 20MB'}
|
||||
className={'mt2'} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Row gutterWidth={16} className={'item-form__accounts-section'}>
|
||||
<Col width={404}>
|
||||
<h4>Purchase Information</h4>
|
||||
|
||||
<FormGroup
|
||||
label={'Selling Price'}
|
||||
className={'form-group--item-selling-price'}
|
||||
intent={(errors.selling_price && touched.selling_price) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="selling_price" />}
|
||||
inline={true}
|
||||
>
|
||||
<MoneyInputGroup
|
||||
value={values.selling_price}
|
||||
prefix={'$'}
|
||||
onChange={handleMoneyInputChange('selling_price')}
|
||||
inputGroupProps={{
|
||||
medium: true,
|
||||
intent: (errors.selling_price && touched.selling_price) && Intent.DANGER,
|
||||
}} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Account'}
|
||||
labelInfo={infoIcon}
|
||||
inline={true}
|
||||
intent={(errors.sell_account_id && touched.sell_account_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="sell_account_id" />}
|
||||
className={classNames(
|
||||
'form-group--sell-account', 'form-group--select-list',
|
||||
Classes.FILL)}
|
||||
>
|
||||
<Select
|
||||
items={accounts}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('sell_account_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('sell_account_id', 'Select account')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
|
||||
<Col width={404}>
|
||||
<h4>
|
||||
Sales Information
|
||||
</h4>
|
||||
|
||||
<FormGroup
|
||||
label={'Cost Price'}
|
||||
className={'form-group--item-cost-price'}
|
||||
intent={(errors.cost_price && touched.cost_price) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="cost_price" />}
|
||||
inline={true}
|
||||
>
|
||||
<MoneyInputGroup
|
||||
value={values.cost_price}
|
||||
prefix={'$'}
|
||||
onChange={handleMoneyInputChange('cost_price')}
|
||||
inputGroupProps={{
|
||||
medium: true,
|
||||
intent: (errors.cost_price && touched.cost_price) && Intent.DANGER,
|
||||
}} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Account'}
|
||||
labelInfo={infoIcon}
|
||||
inline={true}
|
||||
intent={(errors.cost_account_id && touched.cost_account_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="cost_account_id" />}
|
||||
className={classNames(
|
||||
'form-group--cost-account',
|
||||
'form-group--select-list',
|
||||
Classes.FILL)}
|
||||
>
|
||||
<Select
|
||||
items={accounts}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('cost_account_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('cost_account_id', 'Select account')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className={'item-form__accounts-section mt2'}>
|
||||
<Col width={404}>
|
||||
<h4>
|
||||
Inventory Information
|
||||
</h4>
|
||||
|
||||
<FormGroup
|
||||
label={'Inventory Account'}
|
||||
inline={true}
|
||||
intent={(errors.inventory_account_id && touched.inventory_account_id) && Intent.DANGER}
|
||||
helperText={<ErrorMessage {...{errors, touched}} name="inventory_account_id" />}
|
||||
className={classNames(
|
||||
'form-group--item-inventory_account',
|
||||
'form-group--select-list',
|
||||
Classes.FILL)}
|
||||
>
|
||||
<Select
|
||||
items={accounts}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccounts}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onItemAccountSelect('inventory_account_id')}
|
||||
>
|
||||
<Button
|
||||
fill={true}
|
||||
rightIcon='caret-down'
|
||||
text={getSelectedAccountLabel('inventory_account_id','Select account')}
|
||||
/>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={'Opening Stock'}
|
||||
className={'form-group--item-stock'}
|
||||
// intent={errors.cost_price && Intent.DANGER}
|
||||
// helperText={formik.errors.stock && formik.errors.stock}
|
||||
inline={true}
|
||||
>
|
||||
<InputGroup
|
||||
medium={true}
|
||||
intent={errors.stock && Intent.DANGER}
|
||||
{...getFieldProps('stock')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<div class='form__floating-footer'>
|
||||
<Button intent={Intent.PRIMARY} type='submit'>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button className={'ml1'}>Save as Draft</Button>
|
||||
<Button className={'ml1'} onClick={handleCancelClickBtn}>Close</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
AccountsConnect,
|
||||
ItemsConnect,
|
||||
ItemCategoryConnect,
|
||||
MediaConnect,
|
||||
)(ItemForm);
|
||||
52
client/src/containers/Items/ItemFormPage.js
Normal file
52
client/src/containers/Items/ItemFormPage.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import ItemForm from 'containers/Items/ItemForm';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
const ItemFormContainer = ({
|
||||
// #withDashboard
|
||||
changePageTitle,
|
||||
|
||||
// #withAccountsActions
|
||||
requestFetchAccounts,
|
||||
|
||||
// #withItemCategoriesActions
|
||||
requestFetchItemCategories,
|
||||
}) => {
|
||||
const { id } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
id ?
|
||||
changePageTitle('Edit Item Details') :
|
||||
changePageTitle('New Item');
|
||||
}, [id, changePageTitle]);
|
||||
|
||||
const fetchAccounts = useQuery('accounts-list',
|
||||
(key) => requestFetchAccounts());
|
||||
|
||||
const fetchCategories = useQuery('item-categories-list',
|
||||
(key) => requestFetchItemCategories());
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={fetchAccounts.isFetching || fetchCategories.isFetching}
|
||||
name={'item-form'}>
|
||||
<ItemForm />
|
||||
</DashboardInsider>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
withDashboard,
|
||||
withAccountsActions,
|
||||
withItemCategoriesActions,
|
||||
)(ItemFormContainer);
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
|
||||
import { compose } from 'utils';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
MenuItem,
|
||||
Popover,
|
||||
@@ -15,43 +13,42 @@ import {
|
||||
Classes,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { compose } from 'utils';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
import Icon from 'components/Icon';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ResourceConnect from 'connectors/Resource.connector';
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import { If } from 'components';
|
||||
|
||||
const ItemsActionsBar = ({
|
||||
openDialog,
|
||||
getResourceFields,
|
||||
getResourceViews,
|
||||
views,
|
||||
|
||||
resourceName = 'items',
|
||||
resourceFields,
|
||||
|
||||
itemsViews,
|
||||
|
||||
onFilterChanged,
|
||||
addItemsTableQueries,
|
||||
selectedRows = [],
|
||||
}) => {
|
||||
const { path } = useRouteMatch();
|
||||
const history = useHistory();
|
||||
const [filterCount, setFilterCount] = useState(0);
|
||||
|
||||
const viewsMenuItems = views.map(view =>
|
||||
const viewsMenuItems = itemsViews.map(view =>
|
||||
(<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />));
|
||||
|
||||
const onClickNewItem = () => {
|
||||
history.push('/dashboard/items/new');
|
||||
};
|
||||
const itemsFields = getResourceFields('items');
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: itemsFields,
|
||||
fields: resourceFields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
setFilterCount(filterConditions.length);
|
||||
addItemsTableQueries({
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
}
|
||||
});
|
||||
@@ -105,14 +102,14 @@ const ItemsActionsBar = ({
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
{hasSelectedRows && (
|
||||
<If condition={hasSelectedRows}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
intent={Intent.DANGER}
|
||||
icon={<Icon icon='trash' />}
|
||||
text='Delete'
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
@@ -131,7 +128,6 @@ const ItemsActionsBar = ({
|
||||
|
||||
export default compose(
|
||||
DialogConnect,
|
||||
DashboardConnect,
|
||||
ResourceConnect,
|
||||
ItemsConnect
|
||||
withItems,
|
||||
withResourceDetail,
|
||||
)(ItemsActionsBar);
|
||||
@@ -11,19 +11,24 @@ import {
|
||||
PopoverInteractionKind,
|
||||
} from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import { If } from 'components';
|
||||
|
||||
import Icon from 'components/Icon';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
|
||||
import DialogConnect from 'connectors/Dialog.connector';
|
||||
import FilterDropdown from 'components/FilterDropdown';
|
||||
import ResourceConnect from 'connectors/Resource.connector';
|
||||
|
||||
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
|
||||
|
||||
const ItemsCategoryActionsBar = ({
|
||||
resourceName = 'item_category',
|
||||
resourceFields,
|
||||
|
||||
openDialog,
|
||||
onDeleteCategory,
|
||||
onFilterChanged,
|
||||
getResourceFields,
|
||||
selectedRows,
|
||||
}) => {
|
||||
const onClickNewCategory = useCallback(() => {
|
||||
@@ -34,11 +39,10 @@ const ItemsCategoryActionsBar = ({
|
||||
onDeleteCategory(selectedRows);
|
||||
}, [selectedRows, onDeleteCategory]);
|
||||
|
||||
const categoriesFields = getResourceFields('itemCategories');
|
||||
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
|
||||
|
||||
const filterDropdown = FilterDropdown({
|
||||
fields: categoriesFields,
|
||||
fields: resourceFields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
onFilterChanged && onFilterChanged(filterConditions);
|
||||
},
|
||||
@@ -53,6 +57,7 @@ const ItemsCategoryActionsBar = ({
|
||||
onClick={onClickNewCategory}
|
||||
/>
|
||||
<Popover
|
||||
minimal={true}
|
||||
content={filterDropdown}
|
||||
interactionKind={PopoverInteractionKind.CLICK}
|
||||
position={Position.BOTTOM_LEFT}
|
||||
@@ -64,7 +69,7 @@ const ItemsCategoryActionsBar = ({
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
{ hasSelectedRows && (
|
||||
<If condition={hasSelectedRows}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='trash' iconSize={15} />}
|
||||
@@ -72,7 +77,8 @@ const ItemsCategoryActionsBar = ({
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteCategory}
|
||||
/>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon='file-import' />}
|
||||
@@ -88,9 +94,15 @@ const ItemsCategoryActionsBar = ({
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
resourceName: 'items_categories',
|
||||
});
|
||||
|
||||
const withItemsCategoriesActionsBar = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withItemsCategoriesActionsBar,
|
||||
DialogConnect,
|
||||
DashboardConnect,
|
||||
ItemsCategoryConnect,
|
||||
ResourceConnect
|
||||
withDashboard,
|
||||
withResourceDetail
|
||||
)(ItemsCategoryActionsBar);
|
||||
@@ -7,17 +7,21 @@ import {
|
||||
MenuDivider,
|
||||
Position,
|
||||
} from '@blueprintjs/core'
|
||||
import CustomViewConnect from 'connectors/View.connector';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import {compose} from 'utils';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Icon from 'components/Icon';
|
||||
import Money from 'components/Money';
|
||||
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
|
||||
const ItemsDataTable = ({
|
||||
loading,
|
||||
|
||||
itemsTableLoading,
|
||||
currentPageItems,
|
||||
itemsCurrentPage,
|
||||
|
||||
// props
|
||||
onEditItem,
|
||||
onDeleteItem,
|
||||
onFetchData,
|
||||
@@ -112,18 +116,19 @@ const ItemsDataTable = ({
|
||||
}, [onSelectedRowsChange]);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={currentPageItems}
|
||||
selectionColumn={selectionColumn}
|
||||
onFetchData={handleFetchData}
|
||||
loading={itemsTableLoading && !initialMount}
|
||||
noInitialFetch={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange} />
|
||||
<LoadingIndicator loading={loading} mount={false}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={itemsCurrentPage}
|
||||
selectionColumn={selectionColumn}
|
||||
onFetchData={handleFetchData}
|
||||
loading={itemsTableLoading && !initialMount}
|
||||
noInitialFetch={true}
|
||||
onSelectedRowsChange={handleSelectedRowsChange} />
|
||||
</LoadingIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
export default compose(
|
||||
ItemsConnect,
|
||||
CustomViewConnect,
|
||||
withItems,
|
||||
)(ItemsDataTable);
|
||||
@@ -8,47 +8,58 @@ import {
|
||||
Alert,
|
||||
} from '@blueprintjs/core';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import useAsync from 'hooks/async';
|
||||
import ItemsActionsBar from 'containers/Dashboard/Items/ItemsActionsBar';
|
||||
import { useQuery } from 'react-query';
|
||||
import ItemsActionsBar from 'containers/Items/ItemsActionsBar';
|
||||
import { compose } from 'utils';
|
||||
import ItemsDataTable from './ItemsDataTable';
|
||||
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||
import ResourceConnect from 'connectors/Resource.connector';
|
||||
import DashboardConnect from 'connectors/Dashboard.connector';
|
||||
import ItemsConnect from 'connectors/Items.connect';
|
||||
import CustomViewsConnect from 'connectors/CustomView.connector'
|
||||
import ItemsViewsTabs from 'containers/Dashboard/Items/ItemsViewsTabs';
|
||||
|
||||
import ItemsViewsTabs from 'containers/Items/ItemsViewsTabs';
|
||||
import AppToaster from 'components/AppToaster';
|
||||
|
||||
import withItems from 'containers/Items/withItems';
|
||||
import withResourceActions from 'containers/Resources/withResourcesActions';
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboard';
|
||||
import withItemsActions from 'containers/Items/withItemsActions';
|
||||
import withViewsActions from 'containers/Views/withViewsActions';
|
||||
|
||||
|
||||
function ItemsList({
|
||||
// #withDashboard
|
||||
changePageTitle,
|
||||
fetchResourceViews,
|
||||
fetchResourceFields,
|
||||
views,
|
||||
|
||||
// #withResourceActions
|
||||
requestFetchResourceViews,
|
||||
requestFetchResourceFields,
|
||||
|
||||
// #withItems
|
||||
itemsViews,
|
||||
itemsCurrentPage,
|
||||
itemsTableQuery,
|
||||
|
||||
// #withItemsActions
|
||||
requestDeleteItem,
|
||||
requestFetchItems,
|
||||
addItemsTableQueries,
|
||||
changeItemsCurrentView
|
||||
}) {
|
||||
const [deleteItem, setDeleteItem] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
const [tableLoading, setTableLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
changePageTitle('Items List');
|
||||
}, [changePageTitle]);
|
||||
|
||||
const fetchHook = useAsync(async () => {
|
||||
await Promise.all([
|
||||
fetchResourceViews('items'),
|
||||
fetchResourceFields('items'),
|
||||
const fetchHook = useQuery('items-resource', () => {
|
||||
return Promise.all([
|
||||
requestFetchResourceViews('items'),
|
||||
requestFetchResourceFields('items'),
|
||||
]);
|
||||
});
|
||||
|
||||
const fetchItems = useAsync(async () => {
|
||||
await Promise.all([
|
||||
requestFetchItems({ }),
|
||||
])
|
||||
});
|
||||
const fetchItems = useQuery(['items-table', itemsTableQuery],
|
||||
() => requestFetchItems({}));
|
||||
|
||||
// Handle click delete item.
|
||||
const handleDeleteItem = useCallback((item) => {
|
||||
@@ -70,6 +81,7 @@ function ItemsList({
|
||||
});
|
||||
}, [requestDeleteItem, deleteItem]);
|
||||
|
||||
// Handle fetch data table.
|
||||
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
|
||||
addItemsTableQueries({
|
||||
...(sortBy.length > 0) ? {
|
||||
@@ -77,30 +89,40 @@ function ItemsList({
|
||||
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||
} : {},
|
||||
});
|
||||
fetchItems.execute();
|
||||
}, [fetchItems, addItemsTableQueries]);
|
||||
|
||||
// Handle filter change to re-fetch the items.
|
||||
const handleFilterChanged = useCallback(() => {
|
||||
fetchItems.execute();
|
||||
const handleFilterChanged = useCallback((filterConditions) => {
|
||||
addItemsTableQueries({
|
||||
filter_roles: filterConditions || '',
|
||||
});
|
||||
}, [fetchItems]);
|
||||
|
||||
// Handle custom view change to re-fetch the items.
|
||||
const handleCustomViewChanged = useCallback(() => {
|
||||
fetchItems.execute();
|
||||
const handleCustomViewChanged = useCallback((customViewId) => {
|
||||
setTableLoading(true);
|
||||
}, [fetchItems]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableLoading && !fetchItems.isFetching) {
|
||||
setTableLoading(false);
|
||||
}
|
||||
}, [tableLoading, fetchItems.isFetching]);
|
||||
|
||||
// Handle selected rows change.
|
||||
const handleSelectedRowsChange = useCallback((accounts) => {
|
||||
setSelectedRows(accounts);
|
||||
}, [setSelectedRows]);
|
||||
|
||||
return (
|
||||
<DashboardInsider isLoading={fetchHook.pending} name={'items-list'}>
|
||||
<DashboardInsider
|
||||
isLoading={fetchHook.isFetching}
|
||||
name={'items-list'}>
|
||||
|
||||
<ItemsActionsBar
|
||||
onFilterChanged={handleFilterChanged}
|
||||
selectedRows={selectedRows}
|
||||
views={views} />
|
||||
views={itemsViews} />
|
||||
|
||||
<DashboardPageContent>
|
||||
<Switch>
|
||||
@@ -111,9 +133,11 @@ function ItemsList({
|
||||
'/dashboard/items'
|
||||
]}>
|
||||
<ItemsViewsTabs
|
||||
itemsViews={itemsViews}
|
||||
onViewChanged={handleCustomViewChanged} />
|
||||
|
||||
<ItemsDataTable
|
||||
loading={tableLoading}
|
||||
onDeleteItem={handleDeleteItem}
|
||||
onEditItem={handleEditItem}
|
||||
onFetchData={handleFetchData}
|
||||
@@ -140,8 +164,9 @@ function ItemsList({
|
||||
}
|
||||
|
||||
export default compose(
|
||||
DashboardConnect,
|
||||
ResourceConnect,
|
||||
ItemsConnect,
|
||||
CustomViewsConnect,
|
||||
withItems,
|
||||
withResourceActions,
|
||||
withDashboardActions,
|
||||
withItemsActions,
|
||||
withViewsActions,
|
||||
)(ItemsList);
|
||||
124
client/src/containers/Items/ItemsViewsTabs.js
Normal file
124
client/src/containers/Items/ItemsViewsTabs.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Alignment,
|
||||
Navbar,
|
||||
NavbarGroup,
|
||||
Tabs,
|
||||
Tab,
|
||||
Button
|
||||
} from '@blueprintjs/core';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Icon from 'components/Icon';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
import {useUpdateEffect} from 'hooks';
|
||||
|
||||
import withItemsActions from 'containers/Items/withItemsActions';
|
||||
import withDashboard from 'containers/Dashboard/withDashboard';
|
||||
import withViewDetail from 'containers/Views/withViewDetails';
|
||||
|
||||
|
||||
function ItemsViewsTabs({
|
||||
// #withViewDetail
|
||||
viewId,
|
||||
viewItem,
|
||||
|
||||
itemsViews,
|
||||
|
||||
// #withItemsActions
|
||||
addItemsTableQueries,
|
||||
changeItemsCurrentView,
|
||||
|
||||
// #withDashboard
|
||||
setTopbarEditView,
|
||||
changePageSubtitle,
|
||||
|
||||
// #props
|
||||
onViewChanged,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { custom_view_id: customViewId } = useParams();
|
||||
|
||||
const handleClickNewView = () => {
|
||||
setTopbarEditView(null);
|
||||
history.push('/dashboard/custom_views/items/new');
|
||||
};
|
||||
|
||||
const handleViewLinkClick = () => {
|
||||
setTopbarEditView(customViewId);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
changeItemsCurrentView(customViewId || -1);
|
||||
setTopbarEditView(customViewId);
|
||||
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
|
||||
|
||||
addItemsTableQueries({
|
||||
custom_view_id: customViewId || null,
|
||||
});
|
||||
|
||||
return () => {
|
||||
setTopbarEditView(null);
|
||||
changeItemsCurrentView(-1);
|
||||
changePageSubtitle('');
|
||||
};
|
||||
}, [customViewId]);
|
||||
|
||||
useUpdateEffect(() => {
|
||||
onViewChanged && onViewChanged(customViewId);
|
||||
}, [customViewId]);
|
||||
|
||||
const tabs = itemsViews.map(view => {
|
||||
const baseUrl = '/dashboard/items';
|
||||
const link = (
|
||||
<Link to={`${baseUrl}/${view.id}/custom_view`} onClick={handleViewLinkClick}>
|
||||
{view.name}
|
||||
</Link>
|
||||
);
|
||||
return (<Tab id={`custom_view_${view.id}`} title={link} />);
|
||||
});
|
||||
|
||||
return (
|
||||
<Navbar className='navbar--dashboard-views'>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<Tabs
|
||||
id='navbar'
|
||||
large={true}
|
||||
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
|
||||
className='tabs--dashboard-views'
|
||||
>
|
||||
<Tab
|
||||
id='all'
|
||||
title={<Link to={`/dashboard/items`}>All</Link>}
|
||||
onClick={handleViewLinkClick} />
|
||||
|
||||
{tabs}
|
||||
|
||||
<Button
|
||||
className='button--new-view'
|
||||
icon={<Icon icon='plus' />}
|
||||
onClick={handleClickNewView}
|
||||
minimal={true}
|
||||
/>
|
||||
</Tabs>
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => ({
|
||||
// Mapping view id from matched route params.
|
||||
viewId: ownProps.match.params.custom_view_id,
|
||||
});
|
||||
|
||||
const withItemsViewsTabs = connect(mapStateToProps);
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
withItemsViewsTabs,
|
||||
withDashboard,
|
||||
withItemsActions,
|
||||
withViewDetail,
|
||||
)(ItemsViewsTabs);
|
||||
11
client/src/containers/Items/withItemCategories.js
Normal file
11
client/src/containers/Items/withItemCategories.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
categoriesList: Object.values(state.itemCategories.categories),
|
||||
categoriesTableLoading: state.itemCategories.loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
16
client/src/containers/Items/withItemCategoriesActions.js
Normal file
16
client/src/containers/Items/withItemCategoriesActions.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchItemCategories,
|
||||
submitItemCategory,
|
||||
deleteItemCategory,
|
||||
editItemCategory,
|
||||
} from 'store/itemCategories/itemsCategory.actions';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
requestSubmitItemCategory: (form) => dispatch(submitItemCategory({ form })),
|
||||
requestFetchItemCategories: () => dispatch(fetchItemCategories()),
|
||||
requestDeleteItemCategory: (id) => dispatch(deleteItemCategory(id)),
|
||||
requestEditItemCategory: (id, form) => dispatch(editItemCategory(id, form)),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
12
client/src/containers/Items/withItemCategoryDetail.js
Normal file
12
client/src/containers/Items/withItemCategoryDetail.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getCategoryId,
|
||||
} from 'store/itemCategories/itemsCategory.reducer';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
itemCategory: getCategoryId(state, props.itemCategoryId),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
26
client/src/containers/Items/withItems.js
Normal file
26
client/src/containers/Items/withItems.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
getResourceViews,
|
||||
getViewPages,
|
||||
} from 'store/customViews/customViews.selectors'
|
||||
import {
|
||||
getCurrentPageResults
|
||||
} from 'store/selectors';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
const viewPages = getViewPages(state.items.views, state.items.currentViewId);
|
||||
|
||||
return {
|
||||
itemsViews: getResourceViews(state, 'items'),
|
||||
itemsCurrentPage: getCurrentPageResults(
|
||||
state.items.items,
|
||||
viewPages,
|
||||
state.items.currentPage,
|
||||
),
|
||||
itemsBulkSelected: state.items.bulkActions,
|
||||
itemsTableLoading: state.items.loading,
|
||||
itemsTableQuery: state.items.tableQuery,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
32
client/src/containers/Items/withItemsActions.js
Normal file
32
client/src/containers/Items/withItemsActions.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
fetchItems,
|
||||
deleteItem,
|
||||
submitItem,
|
||||
} from 'store/items/items.actions';
|
||||
import t from 'store/types';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
requestFetchItems: (query) => dispatch(fetchItems({ query })),
|
||||
requestDeleteItem: (id) => dispatch(deleteItem({ id })),
|
||||
requestSubmitItem: (form) => dispatch(submitItem({ form })),
|
||||
addBulkActionItem: (id) => dispatch({
|
||||
type: t.ITEM_BULK_ACTION_ADD, itemId: id
|
||||
}),
|
||||
removeBulkActionItem: (id) => dispatch({
|
||||
type: t.ITEM_BULK_ACTION_REMOVE, itemId: id,
|
||||
}),
|
||||
setItemsTableQuery: (key, value) => dispatch({
|
||||
type: t.ITEMS_TABLE_QUERY_SET, key, value,
|
||||
}),
|
||||
addItemsTableQueries: (queries) => dispatch({
|
||||
type: t.ITEMS_TABLE_QUERIES_ADD, queries,
|
||||
}),
|
||||
|
||||
changeItemsCurrentView: (id) => dispatch({
|
||||
type: t.ITEMS_SET_CURRENT_VIEW,
|
||||
currentViewId: parseInt(id, 10),
|
||||
}),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
9
client/src/containers/Preferences/withUsers.js
Normal file
9
client/src/containers/Preferences/withUsers.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export const mapStateToProps = (state, props) => {
|
||||
return {
|
||||
usersList: state.users.list.results,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps);
|
||||
18
client/src/containers/Preferences/withUsersActions.js
Normal file
18
client/src/containers/Preferences/withUsersActions.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchUsers,
|
||||
fetchUser,
|
||||
deleteUser,
|
||||
inactiveUser,
|
||||
editUser,
|
||||
} from 'store/users/users.actions';
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
requestFetchUsers: () => dispatch(fetchUsers({})),
|
||||
requestFetchUser: (id) => dispatch(fetchUser({ id })),
|
||||
requestDeleteUser: (id) => dispatch(deleteUser({ id })),
|
||||
requestInactiveUser: (id) => dispatch(inactiveUser({ id })),
|
||||
requestEditUser: (id, form) => dispatch(editUser({ form, id })),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user