refactoring: sales tables.

refacoring: purchases tables.
This commit is contained in:
a.bouhuolia
2021-02-11 20:45:06 +02:00
parent 3901c336df
commit d48532a7e6
210 changed files with 2799 additions and 5392 deletions

View File

@@ -0,0 +1,262 @@
import React, { useMemo, useCallback, useEffect } from 'react';
import { Formik, Form } from 'formik';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { pick, sumBy, isEmpty } from 'lodash';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import { ERROR } from 'common/errors';
import {
EditReceiptFormSchema,
CreateReceiptFormSchema,
} from './ReceiptForm.schema';
import 'style/pages/SaleReceipt/PageForm.scss';
import { useReceiptFormContext } from './ReceiptFormProvider';
import ReceiptFromHeader from './ReceiptFormHeader';
import ReceiptFormBody from './ReceiptFormBody';
import ReceiptFormFloatingActions from './ReceiptFormFloatingActions';
import ReceiptFormFooter from './ReceiptFormFooter';
import ReceiptNumberWatcher from './ReceiptNumberWatcher';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSettings from 'containers/Settings/withSettings';
import { AppToaster } from 'components';
import {
compose,
repeatValue,
orderingLinesIndexes,
defaultToTransform,
transactionNumber,
} from 'utils';
const MIN_LINES_NUMBER = 4;
const defaultReceipt = {
index: 0,
item_id: '',
rate: '',
discount: 0,
quantity: '',
description: '',
};
const defaultInitialValues = {
customer_id: '',
deposit_account_id: '',
receipt_number: '',
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
reference_no: '',
receipt_message: '',
statement: '',
closed: '',
entries: [...repeatValue(defaultReceipt, MIN_LINES_NUMBER)],
};
/**
* Receipt form.
*/
function ReceiptForm({
// #withDashboard
changePageTitle,
changePageSubtitle,
// #withSettings
receiptNextNumber,
receiptNumberPrefix,
preferredDepositAccount,
}) {
const { formatMessage } = useIntl();
const history = useHistory();
// Receipt form context.
const {
receiptId,
receipt,
editReceiptMutate,
createReceiptMutate,
submitPayload
} = useReceiptFormContext();
const isNewMode = !receiptId;
// The next receipt number.
const receiptNumber = transactionNumber(
receiptNumberPrefix,
receiptNextNumber,
);
useEffect(() => {
const transactionNumber = !isNewMode
? receipt.receipt_number
: receiptNumber;
if (receipt && receipt.id) {
changePageTitle(formatMessage({ id: 'edit_receipt' }));
} else {
changePageTitle(formatMessage({ id: 'new_receipt' }));
}
changePageSubtitle(
defaultToTransform(transactionNumber, `No. ${transactionNumber}`, ''),
);
}, [
isNewMode,
changePageTitle,
changePageSubtitle,
receipt,
receiptNumber,
formatMessage,
]);
// Initial values in create and edit mode.
const initialValues = useMemo(
() => ({
...(!isEmpty(receipt)
? {
...pick(receipt, Object.keys(defaultInitialValues)),
entries: [
...receipt.entries.map((receipt) => ({
...pick(receipt, Object.keys(defaultReceipt)),
})),
...repeatValue(
defaultReceipt,
Math.max(MIN_LINES_NUMBER - receipt.entries.length, 0),
),
],
}
: {
...defaultInitialValues,
receipt_number: receiptNumber,
deposit_account_id: parseInt(preferredDepositAccount),
entries: orderingLinesIndexes(defaultInitialValues.entries),
}),
}),
[receipt, preferredDepositAccount, receiptNumber],
);
// Transform response error to fields.
const handleErrors = (errors, { setErrors }) => {
if (errors.some((e) => e.type === ERROR.SALE_RECEIPT_NUMBER_NOT_UNIQUE)) {
setErrors({
receipt_number: formatMessage({
id: 'sale_receipt_number_not_unique',
}),
});
}
};
// Handle the form submit.
const handleFormSubmit = (
values,
{ setErrors, setSubmitting, resetForm },
) => {
const entries = values.entries.filter(
(item) => item.item_id && item.quantity,
);
const totalQuantity = sumBy(entries, (entry) => parseInt(entry.quantity));
if (totalQuantity === 0) {
AppToaster.show({
message: formatMessage({
id: 'quantity_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
}
const form = {
...values,
closed: submitPayload.status,
entries: entries.map((entry) => ({
// Exclude all properties that out of request entries schema.
...pick(entry, Object.keys(defaultReceipt)),
})),
};
// Handle the request success.
const onSuccess = (response) => {
AppToaster.show({
message: formatMessage(
{
id: isNewMode
? 'the_receipt_has_been_created_successfully'
: 'the_receipt_has_been_edited_successfully',
},
{ number: values.receipt_number },
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.redirect) {
history.push('/receipts');
}
if (submitPayload.resetForm) {
resetForm();
}
};
// Handle the request error.
const onError = (errors) => {
handleErrors(errors, { setErrors });
setSubmitting(false);
};
if (receipt && receipt.id) {
editReceiptMutate(receipt.id, form).then(onSuccess).catch(onError);
} else {
createReceiptMutate(form).then(onSuccess).catch(onError);
}
};
const handleReceiptNumberChanged = useCallback(
(receiptNumber) => {
changePageSubtitle(
defaultToTransform(receiptNumber, `No. ${receiptNumber}`, ''),
);
},
[changePageSubtitle],
);
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_RECEIPT,
)}
>
<Formik
validationSchema={
isNewMode ? CreateReceiptFormSchema : EditReceiptFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<ReceiptFromHeader
onReceiptNumberChanged={handleReceiptNumberChanged}
/>
<ReceiptNumberWatcher receiptNumber={receiptNumber} />
<ReceiptFormBody defaultReceipt={defaultReceipt} />
<ReceiptFormFooter />
<ReceiptFormFloatingActions />
</Form>
</Formik>
</div>
);
}
export default compose(
withDashboardActions,
withSettings(({ receiptSettings }) => ({
receiptNextNumber: receiptSettings?.nextNumber,
receiptNumberPrefix: receiptSettings?.numberPrefix,
preferredDepositAccount: receiptSettings?.preferredDepositAccount,
})),
)(ReceiptForm);

View File

@@ -0,0 +1,57 @@
import * as Yup from 'yup';
import { formatMessage } from 'services/intl';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const Schema = Yup.object().shape({
customer_id: Yup.string()
.label(formatMessage({ id: 'customer_name_' }))
.required(),
receipt_date: Yup.date()
.required()
.label(formatMessage({ id: 'receipt_date_' })),
receipt_number: Yup.string()
.nullable()
.max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'receipt_no_' })),
deposit_account_id: Yup.number()
.required()
.label(formatMessage({ id: 'deposit_account_' })),
reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING),
receipt_message: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'receipt_message_' })),
statement: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.TEXT)
.label(formatMessage({ id: 'note' })),
closed: Yup.boolean(),
entries: Yup.array().of(
Yup.object().shape({
quantity: Yup.number()
.nullable()
.max(DATATYPES_LENGTH.INT_10)
.when(['rate'], {
is: (rate) => rate,
then: Yup.number().required(),
}),
rate: Yup.number().nullable().max(DATATYPES_LENGTH.INT_10),
item_id: Yup.number()
.nullable()
.when(['quantity', 'rate'], {
is: (quantity, rate) => !isBlank(quantity) && !isBlank(rate),
then: Yup.number().required(),
}),
discount: Yup.number().nullable().min(0).max(DATATYPES_LENGTH.INT_10),
description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
}),
),
});
const CreateReceiptFormSchema = Schema;
const EditReceiptFormSchema = Schema;
export { CreateReceiptFormSchema, EditReceiptFormSchema };

View File

@@ -0,0 +1,20 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import EditableItemsEntriesTable from 'containers/Entries/EditableItemsEntriesTable';
import { useReceiptFormContext } from './ReceiptFormProvider';
export default function ExpenseFormBody({ defaultReceipt }) {
const { items } = useReceiptFormContext();
return (
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
<EditableItemsEntriesTable
items={items}
defaultEntry={defaultReceipt}
filterSellableItems={true}
/>
</div>
);
}

View File

@@ -0,0 +1,185 @@
import React from 'react';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { useFormikContext } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { If, Icon } from 'components';
import { useReceiptFormContext } from './ReceiptFormProvider';
/**
* Receipt floating actions bar.
*/
export default function ReceiptFormFloatingActions() {
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
// Receipt form context.
const { receipt, setSubmitPayload } = useReceiptFormContext();
// Handle submit & close button click.
const handleSubmitCloseBtnClick = (event) => {
setSubmitPayload({ redirect: true, status: true });
submitForm();
};
// Handle submit, close & new button click.
const handleSubmitCloseAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: true, resetForm: true });
submitForm();
};
// Handle submit, close & continue editing button click.
const handleSubmitCloseContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: true });
submitForm();
};
// Handle submit & draft button click.
const handleSubmitDraftBtnClick = (event) => {
setSubmitPayload({ redirect: true, status: false });
submitForm();
};
// Handle submit, draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: false, resetForm: true });
submitForm();
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: false });
submitForm();
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
};
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Close ----------- */}
<If condition={!receipt || !receipt?.is_closed}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitCloseBtnClick}
text={<T id={'save_close'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'close_and_new'} />}
onClick={handleSubmitCloseAndNewBtnClick}
/>
<MenuItem
text={<T id={'close_continue_editing'} />}
onClick={handleSubmitCloseContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
className={'ml1'}
onClick={handleSubmitDraftBtnClick}
text={<T id={'save_as_draft'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitDraftAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={receipt && receipt?.is_closed}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitCloseBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitCloseAndNewBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={receipt ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FastField } from 'formik';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { Dragzone, Row, Col } from 'components';
import { CLASSES } from 'common/classes';
import { inputIntent } from 'utils';
export default function ReceiptFormFooter({}) {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Row>
<Col md={8}>
{/* --------- Receipt message --------- */}
<FastField name={'receipt_message'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt_message'} />}
className={'form-group--receipt_message'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
{/* --------- Statement--------- */}
<FastField name={'statement'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'statement'} />}
className={'form-group--statement'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
</Col>
<Col md={4}>
<Dragzone
initialFiles={[]}
// onDrop={handleDropFiles}
// onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { sumBy } from 'lodash';
import { useFormikContext } from 'formik';
import { CLASSES } from 'common/classes';
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
import { PageFormBigNumber } from 'components';
import withSettings from 'containers/Settings/withSettings';
import { compose } from 'redux';
/**
* Receipt form header section.
*/
function ReceiptFormHeader({
// #ownProps
onReceiptNumberChanged,
// #withSettings
baseCurrency,
}) {
const { values } = useFormikContext();
// Calculate the total due amount of bill entries.
const totalDueAmount = useMemo(() => sumBy(values.entries, 'total'), [
values.entries,
]);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<ReceiptFormHeaderFields
onReceiptNumberChanged={onReceiptNumberChanged}
/>
<PageFormBigNumber
label={'Due Amount'}
amount={totalDueAmount}
currencyCode={baseCurrency}
/>
</div>
);
}
export default compose(
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(ReceiptFormHeader);

View File

@@ -0,0 +1,186 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
saveInvoke,
handleDateChange,
inputIntent,
} from 'utils';
import { useReceiptFormContext } from './ReceiptFormProvider';
/**
* Receipt form header fields.
*/
function ReceiptFormHeader({
//#withDialogActions
openDialog,
// #ownProps
onReceiptNumberChanged,
}) {
const { accounts, customers } = useReceiptFormContext();
const handleReceiptNumberChange = useCallback(() => {
openDialog('receipt-number-form', {});
}, [openDialog]);
const handleReceiptNumberChanged = (event) => {
saveInvoke(onReceiptNumberChanged, event.currentTarget.value);
};
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField name={'customer_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(CLASSES.FILL, 'form-group--customer')}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Deposit account ----------- */}
<FastField name={'deposit_account_id'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'deposit_account'} />}
className={classNames('form-group--deposit-account', CLASSES.FILL)}
inline={true}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'deposit_account_id'} />}
>
<AccountsSelectList
accounts={accounts}
onAccountSelected={(account) => {
form.setFieldValue('deposit_account_id', account.id);
}}
defaultSelectText={<T id={'select_deposit_account'} />}
selectedAccountId={value}
// filterByTypes={['current_asset']}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt date ----------- */}
<FastField name={'receipt_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt_date'} />}
inline={true}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('receipt_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Receipt number ----------- */}
<FastField name={'receipt_number'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'receipt'} />}
inline={true}
className={('form-group--receipt_number', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="receipt_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
{...field}
onBlur={handleReceiptNumberChanged}
/>
<InputPrependButton
buttonProps={{
onClick: handleReceiptNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated receipt number',
position: Position.BOTTOM_LEFT,
}}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference'} />}
inline={true}
className={classNames('form-group--reference', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="reference" />}
>
<InputGroup minimal={true} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}
export default compose(
withDialogActions,
)(ReceiptFormHeader);

View File

@@ -0,0 +1,57 @@
import React, { useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import ReceiptFrom from './ReceiptForm';
import { ReceiptFormProvider } from './ReceiptFormProvider';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Receipt form page.
*/
function ReceiptFormPage({
// #withDashboardActions
setSidebarShrink,
resetSidebarPreviousExpand,
setDashboardBackLink,
}) {
const { id } = useParams();
useEffect(() => {
// Shrink the sidebar by foce.
setSidebarShrink();
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Reset the sidebar to the previous status.
resetSidebarPreviousExpand();
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
}, [resetSidebarPreviousExpand, setSidebarShrink, setDashboardBackLink]);
// const handleFormSubmit = useCallback(
// (payload) => {
// payload.redirect && history.push('/receipts');
// },
// [history],
// );
// const handleCancel = useCallback(() => {
// history.goBack();
// }, [history]);
return (
<ReceiptFormProvider receiptId={id}>
<ReceiptFrom />
</ReceiptFormProvider>
);
}
export default compose(
withDashboardActions,
)(ReceiptFormPage);

View File

@@ -0,0 +1,85 @@
import React, { createContext, useState } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useReceipt,
useAccounts,
useSettings,
useCustomers,
useItems,
useCreateReceipt,
useEditReceipt
} from 'hooks/query';
const ReceiptFormContext = createContext();
/**
* Receipt form provider.
*/
function ReceiptFormProvider({ receiptId, ...props }) {
// Fetch sale receipt details.
const { data: receipt, isFetching: isReceiptLoading } = useReceipt(
receiptId,
{
enabled: !!receiptId,
},
);
// Fetch accounts list.
const { data: accounts, isFetching: isAccountsLoading } = useAccounts();
// Fetch customers list.
const {
data: { customers },
isFetching: isCustomersLoading,
} = useCustomers();
// Handle fetch Items data table or list
const {
data: { items },
isFetching: isItemsLoading,
} = useItems();
// Fetch receipt settings.
const { isFetching: isSettingLoading } = useSettings();
const { mutateAsync: createReceiptMutate } = useCreateReceipt();
const { mutateAsync: editReceiptMutate } = useEditReceipt();
const [submitPayload, setSubmitPayload] = useState({});
const provider = {
receiptId,
receipt,
accounts,
customers,
items,
submitPayload,
isReceiptLoading,
isAccountsLoading,
isCustomersLoading,
isItemsLoading,
isSettingLoading,
createReceiptMutate,
editReceiptMutate,
setSubmitPayload
};
return (
<DashboardInsider
loading={
isReceiptLoading ||
isAccountsLoading ||
isCustomersLoading ||
isItemsLoading ||
isSettingLoading
}
name={'receipt-form'}
>
<ReceiptFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useReceiptFormContext = () => React.useContext(ReceiptFormContext);
export { ReceiptFormProvider, useReceiptFormContext };

View File

@@ -0,0 +1,45 @@
import { useEffect } from 'react';
import { useFormikContext } from 'formik';
import withDashboardActions from "containers/Dashboard/withDashboardActions";
import withReceipts from './withReceipts';
import withReceiptActions from './withReceiptActions';
import { compose } from 'utils';
function ReceiptNumberWatcher({
receiptNumber,
// #withDashboardActions
changePageSubtitle,
// #withReceipts
receiptNumberChanged,
//#withReceiptActions
setReceiptNumberChanged,
}) {
const { setFieldValue } = useFormikContext();
useEffect(() => {
if (receiptNumberChanged) {
setFieldValue('receipt_number', receiptNumber);
changePageSubtitle(`No. ${receiptNumber}`);
setReceiptNumberChanged(false);
}
}, [
receiptNumber,
receiptNumberChanged,
setReceiptNumberChanged,
setFieldValue,
changePageSubtitle,
]);
return null
}
export default compose(
withReceipts(({ receiptNumberChanged }) => ({ receiptNumberChanged })),
withReceiptActions,
withDashboardActions
)(ReceiptNumberWatcher);

View File

@@ -0,0 +1,15 @@
import React from 'react';
import ReceiptDeleteAlert from 'containers/Alerts/Receipts/ReceiptDeleteAlert';
import ReceiptCloseAlert from 'containers/Alerts/Receipts/ReceiptCloseAlert';
/**
* Receipts alerts.
*/
export default function ReceiptsAlerts() {
return (
<div>
<ReceiptDeleteAlert name={'receipt-delete'} />
<ReceiptCloseAlert name={'receipt-close'} />
</div>
);
}

View File

@@ -0,0 +1,116 @@
import React, { useState } from 'react';
import Icon from 'components/Icon';
import {
Button,
Classes,
Popover,
NavbarDivider,
NavbarGroup,
PopoverInteractionKind,
Position,
Intent,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { If, DashboardActionViewsList } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withReceiptsActions from './withReceiptsActions';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { compose } from 'utils';
/**
* Receipts actions bar.
*/
function ReceiptActionsBar({
// #withReceiptsActions
setReceiptsTableState,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
const [filterCount, setFilterCount] = useState(0);
// Sale receipts list context.
const { receiptsViews } = useReceiptsListContext();
// Handle new receipt button click.
const onClickNewReceipt = () => {
history.push('/receipts/new');
};
// Handle the active tab change.
const handleTabChange = (viewId) => {
setReceiptsTableState({
csutomViewId: viewId.id || null,
});
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'receipts'}
views={receiptsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_receipt'} />}
onClick={onClickNewReceipt}
/>
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${formatMessage({ id: 'filters_applied' })}`
)
}
icon={<Icon icon={'filter-16'} iconSize={16} />}
/>
</Popover>
<If condition={false}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'trash-16'} iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'print-16'} iconSize={'16'} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'file-export-16'} iconSize={'16'} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(withReceiptsActions)(ReceiptActionsBar);

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { pick } from 'lodash';
import { DashboardViewsTabs } from 'components';
import withReceiptActions from './withReceiptsActions';
import withReceipts from './withReceipts';
import { compose } from 'utils';
import { useReceiptsListContext } from './ReceiptsListProvider';
/**
* Receipts views tabs.
*/
function ReceiptViewTabs({
// #withReceiptActions
setReceiptsTableState,
// #withReceipts
receiptTableState
}) {
// Receipts list context.
const { receiptsViews } = useReceiptsListContext();
const tabs = receiptsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
// Handles the active tab chaning.
const handleTabsChange = (customView) => {
setReceiptsTableState({
customViewId: customView.id || null,
});
};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={receiptTableState.customViewId}
tabs={tabs}
resourceName={'receipts'}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withReceiptActions,
withReceipts(({ receiptTableState }) => ({ receiptTableState })),
)(ReceiptViewTabs);

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
export default function ReceiptsEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={'Manage the organizations services and products.'}
description={
<p>
Here a list of your organization products and services, to be used when you create invoices or bills to your customers or vendors.
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => {
history.push('/receipts/new');
}}
>
New receipt
</Button>
<Button intent={Intent.NONE} large={true}>
Learn more
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { useIntl } from 'react-intl';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ReceiptActionsBar from './ReceiptActionsBar';
import ReceiptViewTabs from './ReceiptViewTabs';
import ReceiptsAlerts from '../ReceiptsAlerts';
import ReceiptsTable from './ReceiptsTable';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withReceipts from './withReceipts';
import { ReceiptsListProvider } from './ReceiptsListProvider';
import { transformTableStateToQuery, compose } from 'utils';
/**
* Receipts list page.
*/
function ReceiptsList({
// #withDashboardActions
changePageTitle,
// #withReceipts
receiptTableState,
}) {
const { formatMessage } = useIntl();
// Changes the dashboard page title once the page mount.
useEffect(() => {
changePageTitle(formatMessage({ id: 'receipts_list' }));
}, [changePageTitle, formatMessage]);
return (
<ReceiptsListProvider query={transformTableStateToQuery(receiptTableState)}>
<DashboardPageContent>
<ReceiptActionsBar />
<DashboardPageContent>
<ReceiptViewTabs />
<ReceiptsTable />
</DashboardPageContent>
<ReceiptsAlerts />
</DashboardPageContent>
</ReceiptsListProvider>
);
}
export default compose(
withDashboardActions,
withReceipts(({ receiptTableState }) => ({
receiptTableState,
})),
)(ReceiptsList);

View File

@@ -0,0 +1,59 @@
import React, { createContext } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useResourceViews, useResourceFields, useReceipts } from 'hooks/query';
import { isTableEmptyStatus } from 'utils';
const ReceiptsListContext = createContext();
// Receipts list provider.
function ReceiptsListProvider({ query, ...props }) {
// Fetch receipts resource views and fields.
const { data: receiptsViews, isFetching: isViewsLoading } = useResourceViews(
'sale_receipt',
);
// Fetches the sale receipts resource fields.
// const {
// data: receiptsFields,
// isFetching: isFieldsLoading,
// } = useResourceFields('sale_receipt');
const {
data: { receipts, pagination, filterMeta },
isLoading: isReceiptsLoading,
isFetching: isReceiptsFetching,
} = useReceipts(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isTableEmptyStatus({
data: receipts,
pagination,
filterMeta,
}) && !isReceiptsLoading;
const provider = {
receipts,
pagination,
// receiptsFields,
receiptsViews,
isViewsLoading,
// isFieldsLoading,
isReceiptsLoading,
isReceiptsFetching,
isEmptyStatus
};
return (
<DashboardInsider
loading={isViewsLoading}
name={'sales_receipts'}
>
<ReceiptsListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useReceiptsListContext = () => React.useContext(ReceiptsListContext);
export { ReceiptsListProvider, useReceiptsListContext };

View File

@@ -0,0 +1,126 @@
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { compose } from 'utils';
import { CLASSES } from 'common/classes';
import { DataTable } from 'components';
import ReceiptsEmptyStatus from './ReceiptsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withReceipts from './withReceipts';
import withReceiptsActions from './withReceiptsActions';
import withSettings from 'containers/Settings/withSettings';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { useReceiptsTableColumns, ActionsMenu } from './components';
/**
* Sale receipts datatable.
*/
function ReceiptsDataTable({
// #withReceiptsActions
setReceiptsTableState,
// #withSettings
baseCurrency,
// #withAlertsActions
openAlert
}) {
const history = useHistory();
// Receipts list context.
const {
receipts,
pagination,
isReceiptsFetching,
isReceiptsLoading,
isEmptyStatus
} = useReceiptsListContext();
// Receipts table columns.
const columns = useReceiptsTableColumns();
// Handle receipt edit action.
const handleEditReceipt = ({ id }) => {
history.push(`/receipts/${id}/edit`);
};
// Handles receipt delete action.
const handleDeleteReceipt = (receipt) => {
openAlert('receipt-delete', { receiptId: receipt.id });
};
// Handles receipt close action.
const handleCloseReceipt = (receipt) => {
openAlert('receipt-close', { receiptId: receipt.id });
}
// Handles the datable fetch data once the state changing.
const handleDataTableFetchData = useCallback(
({ sortBy, pageIndex, pageSize }) => {
setReceiptsTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setReceiptsTableState],
);
if (isEmptyStatus) {
return <ReceiptsEmptyStatus />
}
return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<DataTable
columns={columns}
data={receipts}
loading={isReceiptsLoading}
headerLoading={isReceiptsLoading}
progressBarLoading={isReceiptsFetching}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
manualPagination={true}
autoResetSortBy={false}
autoResetPage={false}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
payload={{
onEdit: handleEditReceipt,
onDelete: handleDeleteReceipt,
onClose: handleCloseReceipt,
baseCurrency
}}
/>
</div>
);
}
export default compose(
withAlertsActions,
withReceiptsActions,
withReceipts(({ receiptTableState }) => ({
receiptTableState,
})),
withSettings(({ organizationSettings }) => ({
baseCurrency: organizationSettings?.baseCurrency,
})),
)(ReceiptsDataTable);

View File

@@ -0,0 +1,155 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import {
Position,
Menu,
MenuItem,
MenuDivider,
Intent,
Popover,
Tag,
Button,
} from '@blueprintjs/core';
import { safeCallback } from 'utils';
import { Choose, Money, Icon, If } from 'components';
import moment from 'moment';
export function ActionsMenu({
payload: { onEdit, onDelete, onClose },
row: { original: receipt },
}) {
const { formatMessage } = useIntl();
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_receipt' })}
onClick={safeCallback(onEdit, receipt)}
/>
<If condition={!receipt.is_closed}>
<MenuItem
text={formatMessage({ id: 'mark_as_closed' })}
onClick={safeCallback(onClose, receipt)}
/>
</If>
<MenuItem
text={formatMessage({ id: 'delete_receipt' })}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, receipt)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Actions cell.
*/
export function ActionsCell(props) {
return (
<Popover
content={<ActionsMenu {...props} />}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
/**
* Status accessor.
*/
export function StatusAccessor(receipt) {
return (
<Choose>
<Choose.When condition={receipt.is_closed}>
<Tag minimal={true} intent={Intent.SUCCESS}>
<T id={'closed'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);
}
/**
* Retrieve receipts table columns.
*/
export function useReceiptsTableColumns() {
const { formatMessage } = useIntl();
return React.useMemo(
() => [
{
id: 'receipt_date',
Header: formatMessage({ id: 'receipt_date' }),
accessor: (r) => moment(r.receipt_date).format('YYYY MMM DD'),
width: 140,
className: 'receipt_date',
},
{
id: 'customer_id',
Header: formatMessage({ id: 'customer_name' }),
accessor: 'customer.display_name',
width: 140,
className: 'customer_id',
},
{
id: 'receipt_number',
Header: formatMessage({ id: 'receipt_number' }),
accessor: (row) =>
row.receipt_number ? `#${row.receipt_number}` : null,
width: 140,
className: 'receipt_number',
},
{
id: 'deposit_account_id',
Header: formatMessage({ id: 'deposit_account' }),
accessor: 'deposit_account.name',
width: 140,
className: 'deposit_account',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={'USD'} />,
width: 140,
className: 'amount',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: StatusAccessor,
width: 140,
className: 'amount',
},
{
id: 'reference_no',
Header: formatMessage({ id: 'reference_no' }),
accessor: 'reference_no',
width: 140,
className: 'reference_no',
},
{
id: 'actions',
Header: '',
Cell: ActionsCell,
className: 'actions',
width: 50,
disableResizing: true,
},
],
[formatMessage],
);
}

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import {
getReceiptsTableStateFactory,
} from 'store/receipts/receipts.selector';
export default (mapState) => {
const getReceiptsTableState = getReceiptsTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
receiptTableState: getReceiptsTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import { setReceiptsTableState } from 'store/receipts/receipts.actions';
const mapDispatchToProps = (dispatch) => ({
setReceiptsTableState: (queries) => dispatch(setReceiptsTableState(queries)),
});
export default connect(null, mapDispatchToProps);