WIP Version 0.0.1

This commit is contained in:
Ahmed Bouhuolia
2020-05-08 04:36:04 +02:00
parent bd7eb0eb76
commit 71cc561bb2
151 changed files with 1742 additions and 1081 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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);

View 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);

View File

@@ -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);

View File

@@ -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);

View 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);

View 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);

View File

@@ -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);

View 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);

View File

@@ -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);

View 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);

View File

@@ -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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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);

View 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,
);

View File

@@ -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,
);

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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({

View File

@@ -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';

View File

@@ -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,

View File

@@ -16,4 +16,5 @@ const mapActionsToProps = (dispatch) => ({
type: t.CHANGE_DASHBOARD_PAGE_TITLE, pageTitle
}),
});
export default connect(null, mapActionsToProps)(DashboardHomepage);

View 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);

View 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);

View 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);

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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