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

@@ -0,0 +1,56 @@
import React, {useMemo} from 'react';
import {
Intent,
Button,
} from '@blueprintjs/core';
import { FormattedList } from 'react-intl';
export default function MakeJournalEntriesFooter({
formik: { isSubmitting },
onSubmitClick,
onCancelClick,
}) {
return (
<div>
<div class="form__floating-footer">
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
name={'save'}
onClick={() => {
onSubmitClick({ publish: true, redirect: true });
}}>
Save
</Button>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
className={'ml1'}
name={'save_and_new'}
onClick={() => {
onSubmitClick({ publish: true, redirect: false });
}}>
Save & New
</Button>
<Button
disabled={isSubmitting}
className={'button-secondary ml1'}
onClick={() => {
onSubmitClick({ publish: false, redirect: false });
}}>
Save as Draft
</Button>
<Button
className={'button-secondary ml1'}
onClick={() => {
onCancelClick && onCancelClick();
}}>
Cancel
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,262 @@
import React, {useMemo, useState, useEffect, useRef, useCallback} from 'react';
import * as Yup from 'yup';
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 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 useMedia from 'hooks/useMedia';
import {compose} from 'utils';
function MakeJournalEntriesForm({
requestSubmitMedia,
requestDeleteMedia,
requestMakeJournalEntries,
requestEditManualJournal,
changePageTitle,
changePageSubtitle,
manualJournalId,
manualJournal,
onFormSubmit,
onCancelForm,
}) {
const { setFiles, saveMedia, deletedFiles, setDeletedFiles, deleteMedia } = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
});
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const savedMediaIds = useRef([]);
const clearSavedMediaIds = () => { savedMediaIds.current = []; }
useEffect(() => {
if (manualJournal && manualJournal.id) {
changePageTitle('Edit Journal');
changePageSubtitle(`No. ${manualJournal.journal_number}`);
} else {
changePageTitle('New Journal');
}
}, [changePageTitle, changePageSubtitle, manualJournal]);
const validationSchema = Yup.object().shape({
journal_number: Yup.string().required(),
date: Yup.date().required(),
reference: Yup.string(),
description: Yup.string(),
entries: Yup.array().of(
Yup.object().shape({
credit: Yup.number().nullable(),
debit: Yup.number().nullable(),
account_id: Yup.number().nullable().when(['credit', 'debit'], {
is: (credit, debit) => credit || debit,
then: Yup.number().required(),
}),
note: Yup.string().nullable(),
}),
)
});
const saveInvokeSubmit = useCallback((payload) => {
onFormSubmit && onFormSubmit(payload)
}, [onFormSubmit]);
const [payload, setPayload] = useState({});
const defaultEntry = useMemo(() => ({
account_id: null,
credit: 0,
debit: 0,
note: '',
}), []);
const defaultInitialValues = useMemo(() => ({
journal_number: '',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
entries: [
defaultEntry,
defaultEntry,
defaultEntry,
defaultEntry,
],
}), [defaultEntry]);
const initialValues = useMemo(() => ({
...(manualJournal) ? {
...pick(manualJournal, Object.keys(defaultInitialValues)),
entries: manualJournal.entries.map((entry) => ({
...pick(entry, Object.keys(defaultEntry)),
})),
} : {
...defaultInitialValues,
}
}), [manualJournal, defaultInitialValues, defaultEntry]);
const initialAttachmentFiles = useMemo(() => {
return manualJournal && manualJournal.media
? manualJournal.media.map((attach) => ({
preview: attach.attachment_file,
uploaded: true,
metadata: { ...attach },
})) : [];
}, [manualJournal]);
const formik = useFormik({
enableReinitialize: true,
validationSchema,
initialValues: {
...initialValues,
},
onSubmit: async (values, { setErrors, setSubmitting }) => {
const entries = values.entries.filter((entry) => (
(entry.credit || entry.debit)
));
const getTotal = (type = 'credit') => {
return entries.reduce((total, item) => {
return item[type] ? item[type] + total : total;
}, 0);
}
const totalCredit = getTotal('credit');
const totalDebit = getTotal('debit');
// Validate the total credit should be eqials total debit.
if (totalCredit !== totalDebit) {
AppToaster.show({
message: 'credit_and_debit_not_equal',
});
setSubmitting(false);
return;
}
const form = { ...values, status: payload.publish, entries };
const saveJournal = (mediaIds) => new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mediaIds };
if (manualJournal && manualJournal.id) {
requestEditManualJournal(manualJournal.id, requestForm)
.then((response) => {
AppToaster.show({
message: 'manual_journal_has_been_edited',
intent: Intent.SUCCESS,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'update', ...payload });
clearSavedMediaIds([]);
resolve(response);
}).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,
});
setSubmitting(false);
saveInvokeSubmit({ action: 'new', ...payload });
clearSavedMediaIds();
resolve(response);
}).catch((errors) => {
if (errors.find(e => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')) {
setErrors({
journal_number: 'Journal number is already used.',
});
}
setSubmitting(false);
});
}
});
Promise.all([
saveMedia(),
deleteMedia(),
]).then(([savedMediaResponses]) => {
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
savedMediaIds.current = mediaIds;
return savedMediaResponses;
}).then(() => {
return saveJournal(savedMediaIds.current);
});
},
});
const handleSubmitClick = useCallback((payload) => {
setPayload(payload);
formik.handleSubmit();
}, [setPayload, formik]);
const handleCancelClick = useCallback((payload) => {
onCancelForm && onCancelForm(payload);
}, [onCancelForm]);
const handleDeleteFile = useCallback((_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([
...deletedFiles, deletedFile.metadata.id,
]);
}
});
}, [setDeletedFiles, deletedFiles]);
return (
<div class="make-journal-entries">
<form onSubmit={formik.handleSubmit}>
<MakeJournalEntriesHeader formik={formik} />
<MakeJournalEntriesTable
initialValues={initialValues}
formik={formik}
defaultRow={defaultEntry} />
<MakeJournalEntriesFooter
formik={formik}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick} />
</form>
<Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'} />
</div>
);
}
export default compose(
// ManualJournalsConnect,
// MakeJournalEntriesConnect,
withJournalsActions,
withManualJournalDetail,
withAccountsActions,
withDashboardActions,
MediaConnect,
)(MakeJournalEntriesForm);

View File

@@ -0,0 +1,103 @@
import React, {useMemo, useCallback} from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
} from '@blueprintjs/core';
import {DateInput} from '@blueprintjs/datetime';
import {useIntl} from 'react-intl';
import {Row, Col} from 'react-grid-system';
import moment from 'moment';
import {momentFormatter} from 'utils';
import Icon from 'components/Icon';
import CurrenciesSelectList from 'components/CurrenciesSelectList';
import ErrorMessage from 'components/ErrorMessage';
export default function MakeJournalEntriesHeader({
formik: { errors, touched, setFieldValue, getFieldProps }
}) {
const intl = useIntl();
const handleDateChange = useCallback((date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date', formatted);
}, [setFieldValue]);
const infoIcon = useMemo(() =>
(<Icon icon="info-circle" iconSize={12} />), []);
return (
<div class="make-journal-entries__header">
<Row>
<Col sm={3}>
<FormGroup
label={'Journal number'}
labelInfo={infoIcon}
className={'form-group--journal-number'}
intent={(errors.journal_number && touched.journal_number) && Intent.DANGER}
helperText={<ErrorMessage name="journal_number" {...{errors, touched}} />}
fill={true}>
<InputGroup
intent={(errors.journal_number && touched.journal_number) && Intent.DANGER}
fill={true}
{...getFieldProps('journal_number')} />
</FormGroup>
</Col>
<Col sm={2}>
<FormGroup
label={intl.formatMessage({'id': 'date'})}
intent={(errors.date && touched.date) && Intent.DANGER}
helperText={<ErrorMessage name="date" {...{errors, touched}} />}
minimal={true}>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }} />
</FormGroup>
</Col>
<Col sm={4}>
<FormGroup
label={intl.formatMessage({'id': 'description'})}
className={'form-group--description'}
intent={(errors.name && touched.name) && Intent.DANGER}
helperText={<ErrorMessage name="description" {...{errors, touched}} />}
fill={true}>
<InputGroup
intent={(errors.name && touched.name) && Intent.DANGER}
fill={true}
{...getFieldProps('description')} />
</FormGroup>
</Col>
</Row>
<Row>
<Col sm={3}>
<FormGroup
label={'Reference'}
labelInfo={infoIcon}
className={'form-group--reference'}
intent={(errors.reference && touched.reference) && Intent.DANGER}
helperText={<ErrorMessage name="reference" {...{errors, touched}} />}
fill={true}>
<InputGroup
intent={(errors.reference && touched.reference) && Intent.DANGER}
fill={true}
{...getFieldProps('reference')} />
</FormGroup>
</Col>
<Col sm={4}>
<CurrenciesSelectList />
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import React, { useCallback } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { useQuery } from 'react-query';
import MakeJournalEntriesForm from './MakeJournalEntriesForm';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import {compose} from 'utils';
function MakeJournalEntriesPage({
requestFetchManualJournal,
requestFetchAccounts,
}) {
const history = useHistory();
const { id } = useParams();
const fetchAccounts = useQuery('accounts-list',
(key) => requestFetchAccounts());
const fetchJournal = useQuery(
id && ['manual-journal', id],
(key, journalId) => requestFetchManualJournal(journalId));
const handleFormSubmit = useCallback((payload) => {
payload.redirect &&
history.push('/dashboard/accounting/manual-journals');
}, [history]);
const handleCancel = useCallback(() => {
history.push('/dashboard/accounting/manual-journals');
}, [history]);
return (
<DashboardInsider
loading={fetchJournal.isFetching || fetchAccounts.isFetching}
name={'make-journal-page'}>
<MakeJournalEntriesForm
onFormSubmit={handleFormSubmit}
manualJournalId={id}
onCancelForm={handleCancel} />
</DashboardInsider>
);
}
export default compose(
// DashboardConnect,
withAccountsActions,
withManualJournalsActions,
)(MakeJournalEntriesPage);

View File

@@ -0,0 +1,230 @@
import React, {useState, useMemo, useEffect, useCallback} from 'react';
import {
Button,
Intent,
} from '@blueprintjs/core';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import { compose, formattedAmount} from 'utils';
import {
AccountsListFieldCell,
MoneyFieldCell,
InputGroupCell,
} from 'components/DataTableCells';
import { omit } from 'lodash';
import withAccounts from 'containers/Accounts/withAccounts';
// Actions cell renderer.
const ActionsCellRenderer = ({
row: { index },
column: { id },
cell: { value: initialValue },
data,
payload,
}) => {
if (data.length <= (index + 2)) {
return '';
}
const onClickRemoveRole = () => {
payload.removeRow(index);
};
return (
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole} />
);
};
// Total text cell renderer.
const TotalAccountCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === (props.row.index + 2)) {
return (<span>{ 'Total USD' }</span>);
}
return chainedComponent(props);
};
// Total credit/debit cell renderer.
const TotalCreditDebitCellRenderer = (chainedComponent, type) => (props) => {
if (props.data.length === (props.row.index + 2)) {
const total = props.data.reduce((total, entry) => {
const amount = parseInt(entry[type], 10);
const computed = amount ? total + amount : total;
return computed;
}, 0);
return (<span>{ formattedAmount(total, 'USD') }</span>);
}
return chainedComponent(props);
};
const NoteCellRenderer = (chainedComponent) => (props) => {
if (props.data.length === (props.row.index + 2)) {
return '';
}
return chainedComponent(props);
};
/**
* Make journal entries table component.
*/
function MakeJournalEntriesTable({
formik: { errors, values, setFieldValue },
accounts,
onClickRemoveRow,
onClickAddNewRow,
defaultRow,
initialValues,
}) {
const [rows, setRow] = useState([]);
useEffect(() => {
setRow([
...initialValues.entries.map((e) => ({ ...e, rowType: 'editor'})),
defaultRow,
defaultRow,
])
}, [initialValues, defaultRow])
// Handles update datatable data.
const handleUpdateData = useCallback((rowIndex, columnId, value) => {
const newRows = rows.map((row, index) => {
if (index === rowIndex) {
return { ...rows[rowIndex], [columnId]: value };
}
return { ...row };
});
setRow(newRows);
setFieldValue('entries', newRows.map(row => ({
...omit(row, ['rowType']),
})));
}, [rows, setFieldValue]);
// Handles click remove datatable row.
const handleRemoveRow = useCallback((rowIndex) => {
const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex);
setRow([ ...newRows ]);
setFieldValue('entries', newRows
.filter(row => row.rowType === 'editor')
.map(row => ({ ...omit(row, ['rowType']) })
));
onClickRemoveRow && onClickRemoveRow(removeIndex);
}, [rows, setFieldValue, onClickRemoveRow]);
// Memorized data table columns.
const columns = useMemo(() => [
{
Header: '#',
accessor: 'index',
Cell: ({ row: {index} }) => (
<span>{ index + 1 }</span>
),
className: "index",
width: 40,
disableResizing: true,
disableSortBy: true,
},
{
Header: 'Account',
id: 'account_id',
accessor: 'account_id',
Cell: TotalAccountCellRenderer(AccountsListFieldCell),
className: "account",
disableSortBy: true,
disableResizing: true,
width: 250,
},
{
Header: 'Credit (USD)',
accessor: 'credit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'credit'),
className: "credit",
disableSortBy: true,
disableResizing: true,
width: 150,
},
{
Header: 'Debit (USD)',
accessor: 'debit',
Cell: TotalCreditDebitCellRenderer(MoneyFieldCell, 'debit'),
className: "debit",
disableSortBy: true,
disableResizing: true,
width: 150,
},
{
Header: 'Note',
accessor: 'note',
Cell: NoteCellRenderer(InputGroupCell),
disableSortBy: true,
className: "note",
},
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: "actions",
disableSortBy: true,
disableResizing: true,
width: 45,
}
], []);
// Handles click new line.
const onClickNewRow = useCallback(() => {
setRow([
...rows,
{ ...defaultRow, rowType: 'editor' },
]);
onClickAddNewRow && onClickAddNewRow();
}, [defaultRow, rows, onClickAddNewRow]);
const rowClassNames = useCallback((row) => ({
'row--total': rows.length === (row.index + 2),
}), [rows]);
return (
<div class="make-journal-entries__table">
<DataTable
columns={columns}
data={rows}
rowClassNames={rowClassNames}
payload={{
accounts,
errors: errors.entries || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,
}}/>
<div class="mt1">
<Button
small={true}
className={'button--secondary button--new-line'}
onClick={onClickNewRow}>
New lines
</Button>
<Button
small={true}
className={'button--secondary button--clear-lines ml1'}
onClick={onClickNewRow}>
Clear all lines
</Button>
</div>
</div>
);
}
export default compose(
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

@@ -0,0 +1,194 @@
import React, { useEffect, useState, useCallback } from 'react';
import { Route, Switch, useHistory } from 'react-router-dom';
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 '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,
requestFetchResourceViews,
requestFetchManualJournalsTable,
requestDeleteManualJournal,
requestPublishManualJournal,
requestDeleteBulkManualJournals,
addManualJournalsTableQueries,
}) {
const history = useHistory();
const [deleteManualJournal, setDeleteManualJournal] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [bulkDelete, setBulkDelete] = useState(false);
const fetchViews = useQuery('journals-resource-views', () => {
return requestFetchResourceViews('manual_journals');
});
const fetchManualJournals = useQuery('manual-journals-table', () =>
requestFetchManualJournalsTable());
useEffect(() => {
changePageTitle('Manual Journals');
}, [changePageTitle]);
// Handle delete manual journal click.
const handleDeleteJournal = useCallback((journal) => {
setDeleteManualJournal(journal);
}, [setDeleteManualJournal]);
// Handle cancel delete manual journal.
const handleCancelManualJournalDelete = useCallback(() => {
setDeleteManualJournal(false);
}, [setDeleteManualJournal]);
// Handle confirm delete manual journal.
const handleConfirmManualJournalDelete = useCallback(() => {
requestDeleteManualJournal(deleteManualJournal.id).then(() => {
setDeleteManualJournal(false);
AppToaster.show({ message: 'the_manual_Journal_has_been_deleted' });
});
}, [deleteManualJournal, requestDeleteManualJournal]);
const handleBulkDelete = useCallback((accountsIds) => {
setBulkDelete(accountsIds);
}, [setBulkDelete]);
const handleConfirmBulkDelete = useCallback(() => {
requestDeleteBulkManualJournals(bulkDelete).then(() => {
setBulkDelete(false);
AppToaster.show({ message: 'the_accounts_have_been_deleted' });
}).catch((error) => {
setBulkDelete(false);
});
}, [
requestDeleteBulkManualJournals,
bulkDelete,
]);
const handleCancelBulkDelete = useCallback(() => {
setBulkDelete(false);
}, []);
const handleEditJournal = useCallback((journal) => {
history.push(`/dashboard/accounting/manual-journals/${journal.id}/edit`);
}, [history]);
// Handle filter change to re-fetch data-table.
const handleFilterChanged = useCallback(() => {
fetchManualJournals.refetch();
}, [fetchManualJournals]);
// Handle view change to re-fetch data table.
const handleViewChanged = useCallback(() => {
fetchManualJournals.refetch();
}, [fetchManualJournals]);
// Handle fetch data of manual jouranls datatable.
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
addManualJournalsTableQueries({
...(sortBy.length > 0) ? {
column_sort_by: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
} : {},
});
}, [
addManualJournalsTableQueries,
]);
const handlePublishJournal = useCallback((journal) => {
requestPublishManualJournal(journal.id).then(() => {
AppToaster.show({ message: 'the_manual_journal_id_has_been_published' });
})
}, [requestPublishManualJournal]);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback((accounts) => {
setSelectedRows(accounts);
}, [setSelectedRows]);
return (
<DashboardInsider
loading={fetchViews.isFetching || fetchManualJournals.isFetching}
name={'manual-journals'}>
<ManualJournalsActionsBar
onBulkDelete={handleBulkDelete}
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged} />
<DashboardPageContent>
<Switch>
<Route
exact={true}
path={[
'/dashboard/accounting/manual-journals/:custom_view_id/custom_view',
'/dashboard/accounting/manual-journals',
]}>
<ManualJournalsViewTabs
onViewChanged={handleViewChanged} />
</Route>
</Switch>
<ManualJournalsDataTable
onDeleteJournal={handleDeleteJournal}
onFetchData={handleFetchData}
onEditJournal={handleEditJournal}
onPublishJournal={handlePublishJournal}
onSelectedRowsChange={handleSelectedRowsChange} />
<Alert
cancelButtonText='Cancel'
confirmButtonText='Move to Trash'
icon='trash'
intent={Intent.DANGER}
isOpen={deleteManualJournal}
onCancel={handleCancelManualJournalDelete}
onConfirm={handleConfirmManualJournalDelete}
>
<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>
<Alert
cancelButtonText='Cancel'
confirmButtonText='Move to Trash'
icon='trash'
intent={Intent.DANGER}
isOpen={bulkDelete}
onCancel={handleCancelBulkDelete}
onConfirm={handleConfirmBulkDelete}
>
<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(
withDashboardActions,
withManualJournalsActions,
withViewsActions,
)(ManualJournalsTable);

View File

@@ -0,0 +1,96 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
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 ManualJournalsConnect from 'connectors/ManualJournals.connect';
import DashboardConnect from 'connectors/Dashboard.connector';
import { useUpdateEffect } from 'hooks';
function ManualJournalsViewTabs({
views,
setTopbarEditView,
customViewChanged,
addManualJournalsTableQueries,
onViewChanged,
}) {
const history = useHistory();
const { custom_view_id: customViewId } = useParams();
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/dashboard/custom_views/manual_journals/new');
};
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
};
useUpdateEffect(() => {
customViewChanged && customViewChanged(customViewId);
addManualJournalsTableQueries({
custom_view_id: customViewId || null,
});
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
useEffect(() => {
addManualJournalsTableQueries({
custom_view_id: customViewId,
});
}, [customViewId]);
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>
);
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={`custom_view_${customViewId}`}
className='tabs--dashboard-views'
>
<Tab
id='all'
title={
<Link to={`/dashboard/accounting/manual-journals`}>All</Link>
}
/>
{tabs}
<Button
className='button--new-view'
icon={<Icon icon='plus' />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
</NavbarGroup>
</Navbar>
);
}
export default compose(
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);