mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
re-structure to monorepo.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// @ts-nocheck
|
||||
import * as Yup from 'yup';
|
||||
import intl from 'react-intl-universal';
|
||||
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
|
||||
import { isBlank } from '@/utils';
|
||||
|
||||
const Schema = Yup.object().shape({
|
||||
customer_id: Yup.string().label(intl.get('customer_name_')).required(),
|
||||
receipt_date: Yup.date().required().label(intl.get('receipt_date_')),
|
||||
receipt_number: Yup.string()
|
||||
.nullable()
|
||||
.max(DATATYPES_LENGTH.STRING)
|
||||
.label(intl.get('receipt_no_')),
|
||||
deposit_account_id: Yup.number()
|
||||
.required()
|
||||
.label(intl.get('deposit_account_')),
|
||||
reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING),
|
||||
receipt_message: Yup.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(DATATYPES_LENGTH.STRING)
|
||||
.label(intl.get('receipt_message_')),
|
||||
statement: Yup.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(DATATYPES_LENGTH.TEXT)
|
||||
.label(intl.get('note')),
|
||||
closed: Yup.boolean(),
|
||||
branch_id: Yup.string(),
|
||||
warehouse_id: Yup.string(),
|
||||
exchange_rate: Yup.number(),
|
||||
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 };
|
||||
@@ -0,0 +1,184 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { Formik, Form } from 'formik';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { sumBy, isEmpty } from 'lodash';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import {
|
||||
EditReceiptFormSchema,
|
||||
CreateReceiptFormSchema,
|
||||
} from './ReceiptForm.schema';
|
||||
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
|
||||
import ReceiptFromHeader from './ReceiptFormHeader';
|
||||
import ReceiptItemsEntriesEditor from './ReceiptItemsEntriesEditor';
|
||||
import ReceiptFormFloatingActions from './ReceiptFormFloatingActions';
|
||||
import ReceiptFormFooter from './ReceiptFormFooter';
|
||||
import ReceiptFormDialogs from './ReceiptFormDialogs';
|
||||
import ReceiptFormTopBar from './ReceiptFormTopbar';
|
||||
|
||||
import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
|
||||
|
||||
import { AppToaster } from '@/components';
|
||||
import { compose, orderingLinesIndexes, transactionNumber } from '@/utils';
|
||||
import {
|
||||
transformToEditForm,
|
||||
defaultReceipt,
|
||||
handleErrors,
|
||||
transformFormValuesToRequest,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* Receipt form.
|
||||
*/
|
||||
function ReceiptForm({
|
||||
// #withSettings
|
||||
receiptNextNumber,
|
||||
receiptNumberPrefix,
|
||||
receiptAutoIncrement,
|
||||
preferredDepositAccount,
|
||||
|
||||
// #withCurrentOrganization
|
||||
organization: { base_currency },
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
// Receipt form context.
|
||||
const {
|
||||
receipt,
|
||||
editReceiptMutate,
|
||||
createReceiptMutate,
|
||||
submitPayload,
|
||||
isNewMode,
|
||||
} = useReceiptFormContext();
|
||||
|
||||
// The next receipt number.
|
||||
const nextReceiptNumber = transactionNumber(
|
||||
receiptNumberPrefix,
|
||||
receiptNextNumber,
|
||||
);
|
||||
// Initial values in create and edit mode.
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
...(!isEmpty(receipt)
|
||||
? { ...transformToEditForm(receipt) }
|
||||
: {
|
||||
...defaultReceipt,
|
||||
...(receiptAutoIncrement && {
|
||||
receipt_number: nextReceiptNumber,
|
||||
}),
|
||||
deposit_account_id: parseInt(preferredDepositAccount),
|
||||
entries: orderingLinesIndexes(defaultReceipt.entries),
|
||||
currency_code: base_currency,
|
||||
}),
|
||||
}),
|
||||
[receipt, preferredDepositAccount, nextReceiptNumber, receiptAutoIncrement],
|
||||
);
|
||||
|
||||
// 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: intl.get('quantity_cannot_be_zero_or_empty'),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const form = {
|
||||
...transformFormValuesToRequest(values),
|
||||
closed: submitPayload.status,
|
||||
};
|
||||
// Handle the request success.
|
||||
const onSuccess = (response) => {
|
||||
AppToaster.show({
|
||||
message: intl.get(
|
||||
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 = ({
|
||||
response: {
|
||||
data: { errors },
|
||||
},
|
||||
}) => {
|
||||
if (errors) {
|
||||
handleErrors(errors, { setErrors });
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
if (!isNewMode) {
|
||||
editReceiptMutate([receipt.id, form]).then(onSuccess).catch(onError);
|
||||
} else {
|
||||
createReceiptMutate(form).then(onSuccess).catch(onError);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
<ReceiptFormTopBar />
|
||||
<ReceiptFromHeader />
|
||||
<ReceiptItemsEntriesEditor />
|
||||
<ReceiptFormFooter />
|
||||
<ReceiptFormFloatingActions />
|
||||
|
||||
<ReceiptFormDialogs />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDashboardActions,
|
||||
withSettings(({ receiptSettings }) => ({
|
||||
receiptNextNumber: receiptSettings?.nextNumber,
|
||||
receiptNumberPrefix: receiptSettings?.numberPrefix,
|
||||
receiptAutoIncrement: receiptSettings?.autoIncrement,
|
||||
preferredDepositAccount: receiptSettings?.preferredDepositAccount,
|
||||
})),
|
||||
withCurrentOrganization(),
|
||||
)(ReceiptForm);
|
||||
@@ -0,0 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { BaseCurrency, BaseCurrencyRoot } from '@/components';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
|
||||
/**
|
||||
* Receipt form currency tag.
|
||||
* @returns
|
||||
*/
|
||||
export default function ReceiptFormCurrencyTag() {
|
||||
const { isForeignCustomer, selectCustomer } = useReceiptFormContext();
|
||||
|
||||
if (!isForeignCustomer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseCurrencyRoot>
|
||||
<BaseCurrency currency={selectCustomer?.currency_code} />
|
||||
</BaseCurrencyRoot>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useFormikContext } from 'formik';
|
||||
import ReceiptNumberDialog from '@/containers/Dialogs/ReceiptNumberDialog';
|
||||
|
||||
/**
|
||||
* Receipt form dialogs.
|
||||
*/
|
||||
export default function ReceiptFormDialogs() {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
// Update the form once the receipt number form submit confirm.
|
||||
const handleReceiptNumberFormConfirm = ({ incrementNumber, manually }) => {
|
||||
setFieldValue('receipt_number', incrementNumber || '');
|
||||
setFieldValue('receipt_number_manually', manually);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReceiptNumberDialog
|
||||
dialogName={'receipt-number-form'}
|
||||
onConfirm={handleReceiptNumberFormConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Intent,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { If, Icon } from '@/components';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
|
||||
/**
|
||||
* Receipt floating actions bar.
|
||||
*/
|
||||
export default function ReceiptFormFloatingActions() {
|
||||
// History context.
|
||||
const history = useHistory();
|
||||
|
||||
// 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) => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
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}
|
||||
loading={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}
|
||||
style={{ minWidth: '85px' }}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { Paper, Row, Col } from '@/components';
|
||||
import { ReceiptFormFooterLeft } from './ReceiptFormFooterLeft';
|
||||
import { ReceiptFormFooterRight } from './ReceiptFormFooterRight';
|
||||
|
||||
export default function ReceiptFormFooter({}) {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
|
||||
<ReceiptFooterPaper>
|
||||
<Row>
|
||||
<Col md={8}>
|
||||
<ReceiptFormFooterLeft />
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
<ReceiptFormFooterRight />
|
||||
</Col>
|
||||
</Row>
|
||||
</ReceiptFooterPaper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ReceiptFooterPaper = styled(Paper)`
|
||||
padding: 20px;
|
||||
`;
|
||||
@@ -0,0 +1,62 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import styled from 'styled-components';
|
||||
import { FFormGroup, FEditableText, FormattedMessage as T } from '@/components';
|
||||
|
||||
export function ReceiptFormFooterLeft() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{/* --------- Receipt message --------- */}
|
||||
<ReceiptMsgFormGroup
|
||||
name={'receipt_message'}
|
||||
label={<T id={'receipt_form.label.receipt_message'} />}
|
||||
hintText={'Will be displayed on the Receipt'}
|
||||
>
|
||||
<FEditableText
|
||||
name={'receipt_message'}
|
||||
placeholder={intl.get('receipt_form.receipt_message.placeholder')}
|
||||
/>
|
||||
</ReceiptMsgFormGroup>
|
||||
|
||||
{/* --------- Terms and conditions --------- */}
|
||||
<TermsConditsFormGroup
|
||||
label={<T id={'receipt_form.label.terms_conditions'} />}
|
||||
name={'terms_conditions'}
|
||||
>
|
||||
<FEditableText
|
||||
name={'terms_conditions'}
|
||||
placeholder={intl.get(
|
||||
'receipt_form.terms_and_conditions.placeholder',
|
||||
)}
|
||||
/>
|
||||
</TermsConditsFormGroup>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const ReceiptMsgFormGroup = styled(FFormGroup)`
|
||||
&.bp3-form-group {
|
||||
margin-bottom: 40px;
|
||||
|
||||
.bp3-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.bp3-form-content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const TermsConditsFormGroup = styled(FFormGroup)`
|
||||
&.bp3-form-group {
|
||||
.bp3-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.bp3-form-content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
T,
|
||||
TotalLines,
|
||||
TotalLine,
|
||||
TotalLineBorderStyle,
|
||||
TotalLineTextStyle,
|
||||
} from '@/components';
|
||||
import { useReceiptTotals } from './utils';
|
||||
|
||||
export function ReceiptFormFooterRight() {
|
||||
const {
|
||||
formattedSubtotal,
|
||||
formattedTotal,
|
||||
formattedDueTotal,
|
||||
formattedPaymentTotal,
|
||||
} = useReceiptTotals();
|
||||
|
||||
return (
|
||||
<ReceiptTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||
<TotalLine
|
||||
title={<T id={'receipt_form.label.subtotal'} />}
|
||||
value={formattedSubtotal}
|
||||
borderStyle={TotalLineBorderStyle.None}
|
||||
/>
|
||||
<TotalLine
|
||||
title={<T id={'receipt_form.label.total'} />}
|
||||
value={formattedTotal}
|
||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||
textStyle={TotalLineTextStyle.Bold}
|
||||
/>
|
||||
<TotalLine
|
||||
title={<T id={'receipt_form.label.payment_amount'} />}
|
||||
value={formattedPaymentTotal}
|
||||
borderStyle={TotalLineBorderStyle.None}
|
||||
/>
|
||||
<TotalLine
|
||||
title={<T id={'receipt_form.label.due_amount'} />}
|
||||
value={formattedDueTotal}
|
||||
textStyle={TotalLineTextStyle.Bold}
|
||||
/>
|
||||
</ReceiptTotalLines>
|
||||
);
|
||||
}
|
||||
|
||||
const ReceiptTotalLines = styled(TotalLines)`
|
||||
width: 100%;
|
||||
color: #555555;
|
||||
`;
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-nocheck
|
||||
import React, { useMemo } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import classNames from 'classnames';
|
||||
import { useFormikContext } from 'formik';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { PageFormBigNumber } from '@/components';
|
||||
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
|
||||
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
|
||||
/**
|
||||
* Receipt form header section.
|
||||
*/
|
||||
function ReceiptFormHeader({
|
||||
// #ownProps
|
||||
onReceiptNumberChanged,
|
||||
}) {
|
||||
const {
|
||||
values: { currency_code, entries },
|
||||
} = useFormikContext();
|
||||
|
||||
// Calculate the total due amount of bill entries.
|
||||
const totalDueAmount = useMemo(() => getEntriesTotal(entries), [entries]);
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
|
||||
<ReceiptFormHeaderFields
|
||||
onReceiptNumberChanged={onReceiptNumberChanged}
|
||||
/>
|
||||
<PageFormBigNumber
|
||||
label={intl.get('due_amount')}
|
||||
amount={totalDueAmount}
|
||||
currencyCode={currency_code}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReceiptFormHeader;
|
||||
@@ -0,0 +1,274 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Position,
|
||||
Classes,
|
||||
ControlGroup,
|
||||
} from '@blueprintjs/core';
|
||||
import { DateInput } from '@blueprintjs/datetime';
|
||||
import { FastField, ErrorMessage } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
|
||||
import {
|
||||
FFormGroup,
|
||||
AccountsSelectList,
|
||||
CustomerSelectField,
|
||||
FieldRequiredHint,
|
||||
Icon,
|
||||
InputPrependButton,
|
||||
CustomerDrawerLink,
|
||||
FormattedMessage as T,
|
||||
FeatureCan,
|
||||
} from '@/components';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import { ACCOUNT_TYPE } from '@/constants/accountTypes';
|
||||
import { ProjectsSelect } from '@/containers/Projects/components';
|
||||
import {
|
||||
momentFormatter,
|
||||
compose,
|
||||
tansformDateValue,
|
||||
handleDateChange,
|
||||
inputIntent,
|
||||
} from '@/utils';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
import {
|
||||
accountsFieldShouldUpdate,
|
||||
customersFieldShouldUpdate,
|
||||
useObserveReceiptNoSettings,
|
||||
} from './utils';
|
||||
import {
|
||||
ReceiptExchangeRateInputField,
|
||||
ReceiptProjectSelectButton,
|
||||
} from './components';
|
||||
import { Features } from '@/constants';
|
||||
|
||||
/**
|
||||
* Receipt form header fields.
|
||||
*/
|
||||
function ReceiptFormHeader({
|
||||
//#withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #withSettings
|
||||
receiptAutoIncrement,
|
||||
receiptNextNumber,
|
||||
receiptNumberPrefix,
|
||||
}) {
|
||||
const { accounts, customers, projects } = useReceiptFormContext();
|
||||
|
||||
const handleReceiptNumberChange = useCallback(() => {
|
||||
openDialog('receipt-number-form', {});
|
||||
}, [openDialog]);
|
||||
|
||||
const handleReceiptNoBlur = (form, field) => (event) => {
|
||||
const newValue = event.target.value;
|
||||
|
||||
if (field.value !== newValue && receiptAutoIncrement) {
|
||||
openDialog('receipt-number-form', {
|
||||
initialFormValues: {
|
||||
manualTransactionNo: newValue,
|
||||
incrementMode: 'manual-transaction',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Synsc receipt number settings with the form.
|
||||
useObserveReceiptNoSettings(receiptNumberPrefix, receiptNextNumber);
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
|
||||
{/* ----------- Customer name ----------- */}
|
||||
<FastField
|
||||
name={'customer_id'}
|
||||
customers={customers}
|
||||
shouldUpdate={customersFieldShouldUpdate}
|
||||
>
|
||||
{({ 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'} />}
|
||||
>
|
||||
<CustomerSelectField
|
||||
contacts={customers}
|
||||
selectedContactId={value}
|
||||
defaultSelectText={<T id={'select_customer_account'} />}
|
||||
onContactSelected={(customer) => {
|
||||
form.setFieldValue('customer_id', customer.id);
|
||||
form.setFieldValue('currency_code', customer?.currency_code);
|
||||
}}
|
||||
popoverFill={true}
|
||||
allowCreate={true}
|
||||
/>
|
||||
{value && (
|
||||
<CustomerButtonLink customerId={value}>
|
||||
<T id={'view_customer_details'} />
|
||||
</CustomerButtonLink>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
{/* ----------- Exchange rate ----------- */}
|
||||
<ReceiptExchangeRateInputField
|
||||
name={'exchange_rate'}
|
||||
formGroupProps={{ label: ' ', inline: true }}
|
||||
/>
|
||||
|
||||
{/* ----------- Deposit account ----------- */}
|
||||
<FastField
|
||||
name={'deposit_account_id'}
|
||||
accounts={accounts}
|
||||
shouldUpdate={accountsFieldShouldUpdate}
|
||||
>
|
||||
{({ 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}
|
||||
popoverFill={true}
|
||||
filterByTypes={[
|
||||
ACCOUNT_TYPE.CASH,
|
||||
ACCOUNT_TYPE.BANK,
|
||||
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
|
||||
]}
|
||||
allowCreate={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'}>
|
||||
{({ form, 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}
|
||||
value={field.value}
|
||||
asyncControl={true}
|
||||
onBlur={handleReceiptNoBlur(form, field)}
|
||||
/>
|
||||
<InputPrependButton
|
||||
buttonProps={{
|
||||
onClick: handleReceiptNumberChange,
|
||||
icon: <Icon icon={'settings-18'} />,
|
||||
}}
|
||||
tooltip={true}
|
||||
tooltipProps={{
|
||||
content: (
|
||||
<T
|
||||
id={'setting_your_auto_generated_payment_receive_number'}
|
||||
/>
|
||||
),
|
||||
position: Position.BOTTOM_LEFT,
|
||||
}}
|
||||
inputProps={{
|
||||
leftIcon: <Icon icon={'date-range'} />,
|
||||
}}
|
||||
/>
|
||||
</ControlGroup>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
{/* ----------- Reference ----------- */}
|
||||
<FastField name={'reference_no'}>
|
||||
{({ 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_no" />}
|
||||
>
|
||||
<InputGroup minimal={true} {...field} />
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
{/*------------ Project name -----------*/}
|
||||
<FeatureCan feature={Features.Projects}>
|
||||
<FFormGroup
|
||||
name={'project_id'}
|
||||
label={<T id={'receipt.project_name.label'} />}
|
||||
inline={true}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
>
|
||||
<ProjectsSelect
|
||||
name={'project_id'}
|
||||
projects={projects}
|
||||
input={ReceiptProjectSelectButton}
|
||||
popoverFill={true}
|
||||
/>
|
||||
</FFormGroup>
|
||||
</FeatureCan>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
withSettings(({ receiptSettings }) => ({
|
||||
receiptAutoIncrement: receiptSettings?.autoIncrement,
|
||||
receiptNextNumber: receiptSettings?.nextNumber,
|
||||
receiptNumberPrefix: receiptSettings?.numberPrefix,
|
||||
})),
|
||||
)(ReceiptFormHeader);
|
||||
|
||||
const CustomerButtonLink = styled(CustomerDrawerLink)`
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
`;
|
||||
@@ -0,0 +1,22 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import '@/style/pages/SaleReceipt/PageForm.scss';
|
||||
|
||||
import ReceiptFrom from './ReceiptForm';
|
||||
import { ReceiptFormProvider } from './ReceiptFormProvider';
|
||||
|
||||
/**
|
||||
* Receipt form page.
|
||||
*/
|
||||
export default function ReceiptFormPage() {
|
||||
const { id } = useParams();
|
||||
const idInt = parseInt(id, 10);
|
||||
|
||||
return (
|
||||
<ReceiptFormProvider receiptId={idInt}>
|
||||
<ReceiptFrom />
|
||||
</ReceiptFormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { Features } from '@/constants';
|
||||
import { useFeatureCan } from '@/hooks/state';
|
||||
import { DashboardInsider } from '@/components/Dashboard';
|
||||
import {
|
||||
useReceipt,
|
||||
useAccounts,
|
||||
useSettingsReceipts,
|
||||
useCustomers,
|
||||
useWarehouses,
|
||||
useBranches,
|
||||
useItems,
|
||||
useCreateReceipt,
|
||||
useEditReceipt,
|
||||
} from '@/hooks/query';
|
||||
import { useProjects } from '@/containers/Projects/hooks';
|
||||
|
||||
const ReceiptFormContext = createContext();
|
||||
|
||||
/**
|
||||
* Receipt form provider.
|
||||
*/
|
||||
function ReceiptFormProvider({ receiptId, ...props }) {
|
||||
// Features guard.
|
||||
const { featureCan } = useFeatureCan();
|
||||
const isWarehouseFeatureCan = featureCan(Features.Warehouses);
|
||||
const isBranchFeatureCan = featureCan(Features.Branches);
|
||||
const isProjectsFeatureCan = featureCan(Features.Projects);
|
||||
|
||||
// Fetch sale receipt details.
|
||||
const { data: receipt, isLoading: isReceiptLoading } = useReceipt(receiptId, {
|
||||
enabled: !!receiptId,
|
||||
});
|
||||
// Fetch accounts list.
|
||||
const { data: accounts, isLoading: isAccountsLoading } = useAccounts();
|
||||
|
||||
// Fetch customers list.
|
||||
const {
|
||||
data: { customers },
|
||||
isLoading: isCustomersLoading,
|
||||
} = useCustomers({ page_size: 10000 });
|
||||
|
||||
// Fetch warehouses list.
|
||||
const {
|
||||
data: warehouses,
|
||||
isLoading: isWarehouesLoading,
|
||||
isSuccess: isWarehousesSuccess,
|
||||
} = useWarehouses({}, { enabled: isWarehouseFeatureCan });
|
||||
|
||||
// Fetches the branches list.
|
||||
const {
|
||||
data: branches,
|
||||
isLoading: isBranchesLoading,
|
||||
isSuccess: isBranchesSuccess,
|
||||
} = useBranches({}, { enabled: isBranchFeatureCan });
|
||||
|
||||
// Filter all sellable items only.
|
||||
const stringifiedFilterRoles = React.useMemo(
|
||||
() =>
|
||||
JSON.stringify([
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'sellable',
|
||||
value: true,
|
||||
condition: '&&',
|
||||
comparator: 'equals',
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
fieldKey: 'active',
|
||||
value: true,
|
||||
condition: '&&',
|
||||
comparator: 'equals',
|
||||
},
|
||||
]),
|
||||
[],
|
||||
);
|
||||
|
||||
// Handle fetch Items data table or list
|
||||
const {
|
||||
data: { items },
|
||||
isLoading: isItemsLoading,
|
||||
} = useItems({
|
||||
page_size: 10000,
|
||||
stringified_filter_roles: stringifiedFilterRoles,
|
||||
});
|
||||
|
||||
// Fetch project list.
|
||||
const {
|
||||
data: { projects },
|
||||
isLoading: isProjectsLoading,
|
||||
} = useProjects({}, { enabled: !!isProjectsFeatureCan });
|
||||
|
||||
// Fetch receipt settings.
|
||||
const { isLoading: isSettingLoading } = useSettingsReceipts();
|
||||
|
||||
const { mutateAsync: createReceiptMutate } = useCreateReceipt();
|
||||
const { mutateAsync: editReceiptMutate } = useEditReceipt();
|
||||
|
||||
const [submitPayload, setSubmitPayload] = useState({});
|
||||
|
||||
const isNewMode = !receiptId;
|
||||
|
||||
const isFeatureLoading = isWarehouesLoading || isBranchesLoading;
|
||||
|
||||
const provider = {
|
||||
receiptId,
|
||||
receipt,
|
||||
accounts,
|
||||
customers,
|
||||
items,
|
||||
branches,
|
||||
warehouses,
|
||||
projects,
|
||||
submitPayload,
|
||||
|
||||
isNewMode,
|
||||
isReceiptLoading,
|
||||
isAccountsLoading,
|
||||
isCustomersLoading,
|
||||
isItemsLoading,
|
||||
isWarehouesLoading,
|
||||
isBranchesLoading,
|
||||
isFeatureLoading,
|
||||
isSettingLoading,
|
||||
isBranchesSuccess,
|
||||
isWarehousesSuccess,
|
||||
|
||||
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 };
|
||||
@@ -0,0 +1,125 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Alignment,
|
||||
NavbarGroup,
|
||||
NavbarDivider,
|
||||
Button,
|
||||
Classes,
|
||||
} from '@blueprintjs/core';
|
||||
import {
|
||||
useSetPrimaryWarehouseToForm,
|
||||
useSetPrimaryBranchToForm,
|
||||
} from './utils';
|
||||
|
||||
import { Features } from '@/constants';
|
||||
import { useFeatureCan } from '@/hooks/state';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
import {
|
||||
Icon,
|
||||
BranchSelect,
|
||||
FeatureCan,
|
||||
WarehouseSelect,
|
||||
FormTopbar,
|
||||
DetailsBarSkeletonBase,
|
||||
} from '@/components';
|
||||
|
||||
/**
|
||||
* Receipt form topbar .
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function ReceiptFormTopBar() {
|
||||
// Features guard.
|
||||
const { featureCan } = useFeatureCan();
|
||||
|
||||
// Sets the primary warehouse to form.
|
||||
useSetPrimaryWarehouseToForm();
|
||||
|
||||
// Sets the primary branch to form.
|
||||
useSetPrimaryBranchToForm();
|
||||
|
||||
// Can't display the navigation bar if warehouses or branches feature is not enabled.
|
||||
if (!featureCan(Features.Warehouses) && !featureCan(Features.Branches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormTopbar>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<FeatureCan feature={Features.Branches}>
|
||||
<ReceiptFormSelectBranch />
|
||||
</FeatureCan>
|
||||
{featureCan(Features.Warehouses) && featureCan(Features.Branches) && (
|
||||
<NavbarDivider />
|
||||
)}
|
||||
<FeatureCan feature={Features.Warehouses}>
|
||||
<ReceiptFormSelectWarehouse />
|
||||
</FeatureCan>
|
||||
</NavbarGroup>
|
||||
</FormTopbar>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt select branch.
|
||||
* @returns
|
||||
*/
|
||||
function ReceiptFormSelectBranch() {
|
||||
// Receipt form context.
|
||||
const { branches, isBranchesLoading } = useReceiptFormContext();
|
||||
|
||||
return isBranchesLoading ? (
|
||||
<DetailsBarSkeletonBase className={Classes.SKELETON} />
|
||||
) : (
|
||||
<BranchSelect
|
||||
name={'branch_id'}
|
||||
branches={branches}
|
||||
input={ReceiptBranchSelectButton}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt select warehouse.
|
||||
* @returns
|
||||
*/
|
||||
function ReceiptFormSelectWarehouse() {
|
||||
// Receipt form context.
|
||||
const { warehouses, isWarehouesLoading } = useReceiptFormContext();
|
||||
|
||||
return isWarehouesLoading ? (
|
||||
<DetailsBarSkeletonBase className={Classes.SKELETON} />
|
||||
) : (
|
||||
<WarehouseSelect
|
||||
name={'warehouse_id'}
|
||||
warehouses={warehouses}
|
||||
input={ReceiptWarehouseSelectButton}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ReceiptBranchSelectButton({ label }) {
|
||||
return (
|
||||
<Button
|
||||
text={intl.get('receipt.branch_button.label', { label })}
|
||||
minimal={true}
|
||||
small={true}
|
||||
icon={<Icon icon={'branch-16'} iconSize={16} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ReceiptWarehouseSelectButton({ label }) {
|
||||
return (
|
||||
<Button
|
||||
text={intl.get('receipt.warehouse_button.label', { label })}
|
||||
minimal={true}
|
||||
small={true}
|
||||
icon={<Icon icon={'warehouse-16'} iconSize={16} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FastField } from 'formik';
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import ItemsEntriesTable from '@/containers/Entries/ItemsEntriesTable';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
import { entriesFieldShouldUpdate } from './utils';
|
||||
|
||||
export default function ReceiptItemsEntriesEditor({ defaultReceipt }) {
|
||||
const { items } = useReceiptFormContext();
|
||||
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM_BODY)}>
|
||||
<FastField name={'entries'} items={items} shouldUpdate={entriesFieldShouldUpdate}>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<ItemsEntriesTable
|
||||
entries={value}
|
||||
onUpdateData={(entries) => {
|
||||
setFieldValue('entries', entries);
|
||||
}}
|
||||
items={items}
|
||||
errors={error}
|
||||
linesNumber={4}
|
||||
currencyCode={values.currency_code}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Button } from '@blueprintjs/core';
|
||||
import { useFormikContext } from 'formik';
|
||||
import { ExchangeRateInputGroup } from '@/components';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { useReceiptIsForeignCustomer } from './utils';
|
||||
|
||||
/**
|
||||
* Receipt exchange rate input field.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function ReceiptExchangeRateInputField({ ...props }) {
|
||||
const currentOrganization = useCurrentOrganization();
|
||||
const { values } = useFormikContext();
|
||||
|
||||
const isForeignCustomer = useReceiptIsForeignCustomer();
|
||||
|
||||
// Can't continue if the customer is not foreign.
|
||||
if (!isForeignCustomer) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ExchangeRateInputGroup
|
||||
fromCurrency={values.currency_code}
|
||||
toCurrency={currentOrganization.base_currency}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt project select.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export function ReceiptProjectSelectButton({ label }) {
|
||||
return <Button text={label ?? intl.get('select_project')} />;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { omit, first } from 'lodash';
|
||||
import { useFormikContext } from 'formik';
|
||||
import {
|
||||
defaultFastFieldShouldUpdate,
|
||||
transactionNumber,
|
||||
repeatValue,
|
||||
transformToForm,
|
||||
formattedAmount,
|
||||
} from '@/utils';
|
||||
import { useReceiptFormContext } from './ReceiptFormProvider';
|
||||
import {
|
||||
updateItemsEntriesTotal,
|
||||
ensureEntriesHaveEmptyLine,
|
||||
} from '@/containers/Entries/utils';
|
||||
import { useCurrentOrganization } from '@/hooks/state';
|
||||
import { getEntriesTotal } from '@/containers/Entries/utils';
|
||||
|
||||
export const MIN_LINES_NUMBER = 1;
|
||||
|
||||
export const defaultReceiptEntry = {
|
||||
index: 0,
|
||||
item_id: '',
|
||||
rate: '',
|
||||
discount: '',
|
||||
quantity: '',
|
||||
description: '',
|
||||
amount: '',
|
||||
};
|
||||
|
||||
export const defaultReceipt = {
|
||||
customer_id: '',
|
||||
deposit_account_id: '',
|
||||
receipt_number: '',
|
||||
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
receipt_message: '',
|
||||
statement: '',
|
||||
closed: '',
|
||||
branch_id: '',
|
||||
warehouse_id: '',
|
||||
exchange_rate: 1,
|
||||
currency_code: '',
|
||||
entries: [...repeatValue(defaultReceiptEntry, MIN_LINES_NUMBER)],
|
||||
};
|
||||
|
||||
const ERRORS = {
|
||||
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
|
||||
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform to form in edit mode.
|
||||
*/
|
||||
export const transformToEditForm = (receipt) => {
|
||||
const initialEntries = [
|
||||
...receipt.entries.map((entry) => ({
|
||||
...transformToForm(entry, defaultReceiptEntry),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultReceiptEntry,
|
||||
Math.max(MIN_LINES_NUMBER - receipt.entries.length, 0),
|
||||
),
|
||||
];
|
||||
const entries = R.compose(
|
||||
ensureEntriesHaveEmptyLine(defaultReceiptEntry),
|
||||
updateItemsEntriesTotal,
|
||||
)(initialEntries);
|
||||
|
||||
return {
|
||||
...transformToForm(receipt, defaultReceipt),
|
||||
entries,
|
||||
};
|
||||
};
|
||||
|
||||
export const useObserveReceiptNoSettings = (prefix, nextNumber) => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
const receiptNo = transactionNumber(prefix, nextNumber);
|
||||
setFieldValue('receipt_number', receiptNo);
|
||||
}, [setFieldValue, prefix, nextNumber]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines entries fast field should update.
|
||||
*/
|
||||
export const entriesFieldShouldUpdate = (newProps, oldProps) => {
|
||||
return (
|
||||
newProps.items !== oldProps.items ||
|
||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines accounts fast field should update.
|
||||
*/
|
||||
export const accountsFieldShouldUpdate = (newProps, oldProps) => {
|
||||
return (
|
||||
newProps.accounts !== oldProps.accounts ||
|
||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines customers fast field should update.
|
||||
*/
|
||||
export const customersFieldShouldUpdate = (newProps, oldProps) => {
|
||||
return (
|
||||
newProps.customers !== oldProps.customers ||
|
||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform response error to fields.
|
||||
*/
|
||||
export const handleErrors = (errors, { setErrors }) => {
|
||||
if (errors.some((e) => e.type === ERRORS.SALE_RECEIPT_NUMBER_NOT_UNIQUE)) {
|
||||
setErrors({
|
||||
receipt_number: intl.get('sale_receipt_number_not_unique'),
|
||||
});
|
||||
}
|
||||
if (errors.some((e) => e.type === ERRORS.SALE_RECEIPT_NO_IS_REQUIRED)) {
|
||||
setErrors({
|
||||
receipt_number: intl.get('receipt.field.error.receipt_number_required'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the form values to request body.
|
||||
* @param {*} values
|
||||
* @returns
|
||||
*/
|
||||
export const transformFormValuesToRequest = (values) => {
|
||||
const entries = values.entries.filter(
|
||||
(item) => item.item_id && item.quantity,
|
||||
);
|
||||
|
||||
return {
|
||||
...omit(values, ['receipt_number_manually', 'receipt_number']),
|
||||
...(values.receipt_number_manually && {
|
||||
receipt_number: values.receipt_number,
|
||||
}),
|
||||
entries: entries.map((entry) => ({ ...omit(entry, ['amount']) })),
|
||||
closed: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const useSetPrimaryWarehouseToForm = () => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { warehouses, isWarehousesSuccess } = useReceiptFormContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isWarehousesSuccess) {
|
||||
const primaryWarehouse =
|
||||
warehouses.find((b) => b.primary) || first(warehouses);
|
||||
|
||||
if (primaryWarehouse) {
|
||||
setFieldValue('warehouse_id', primaryWarehouse.id);
|
||||
}
|
||||
}
|
||||
}, [isWarehousesSuccess, setFieldValue, warehouses]);
|
||||
};
|
||||
|
||||
export const useSetPrimaryBranchToForm = () => {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const { branches, isBranchesSuccess } = useReceiptFormContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isBranchesSuccess) {
|
||||
const primaryBranch = branches.find((b) => b.primary) || first(branches);
|
||||
|
||||
if (primaryBranch) {
|
||||
setFieldValue('branch_id', primaryBranch.id);
|
||||
}
|
||||
}
|
||||
}, [isBranchesSuccess, setFieldValue, branches]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retreives the Receipt totals.
|
||||
*/
|
||||
export const useReceiptTotals = () => {
|
||||
const {
|
||||
values: { entries, currency_code: currencyCode },
|
||||
} = useFormikContext();
|
||||
|
||||
// Retrieves the invoice entries total.
|
||||
const total = React.useMemo(() => getEntriesTotal(entries), [entries]);
|
||||
|
||||
// Retrieves the formatted total money.
|
||||
const formattedTotal = React.useMemo(
|
||||
() => formattedAmount(total, currencyCode),
|
||||
[total, currencyCode],
|
||||
);
|
||||
// Retrieves the formatted subtotal.
|
||||
const formattedSubtotal = React.useMemo(
|
||||
() => formattedAmount(total, currencyCode, { money: false }),
|
||||
[total, currencyCode],
|
||||
);
|
||||
// Retrieves the payment total.
|
||||
const paymentTotal = React.useMemo(() => 0, []);
|
||||
|
||||
// Retireves the formatted payment total.
|
||||
const formattedPaymentTotal = React.useMemo(
|
||||
() => formattedAmount(paymentTotal, currencyCode),
|
||||
[paymentTotal, currencyCode],
|
||||
);
|
||||
// Retrieves the formatted due total.
|
||||
const dueTotal = React.useMemo(
|
||||
() => total - paymentTotal,
|
||||
[total, paymentTotal],
|
||||
);
|
||||
// Retrieves the formatted due total.
|
||||
const formattedDueTotal = React.useMemo(
|
||||
() => formattedAmount(dueTotal, currencyCode),
|
||||
[dueTotal, currencyCode],
|
||||
);
|
||||
|
||||
return {
|
||||
total,
|
||||
paymentTotal,
|
||||
dueTotal,
|
||||
formattedTotal,
|
||||
formattedSubtotal,
|
||||
formattedPaymentTotal,
|
||||
formattedDueTotal,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether the receipt has foreign customer.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const useReceiptIsForeignCustomer = () => {
|
||||
const { values } = useFormikContext();
|
||||
const currentOrganization = useCurrentOrganization();
|
||||
|
||||
const isForeignCustomer = React.useMemo(
|
||||
() => values.currency_code !== currentOrganization.base_currency,
|
||||
[values.currency_code, currentOrganization.base_currency],
|
||||
);
|
||||
return isForeignCustomer;
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
|
||||
import { Icon, Choose, T } from '@/components';
|
||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||
import { AbilitySubject, SaleReceiptAction } from '@/constants/abilityOption';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
|
||||
/**
|
||||
* Receipt universal search item select action.
|
||||
*/
|
||||
function ReceiptUniversalSearchSelectComponent({
|
||||
// #ownProps
|
||||
resourceType,
|
||||
resourceId,
|
||||
onAction,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
}) {
|
||||
if (resourceType === RESOURCES_TYPES.RECEIPT) {
|
||||
openDrawer('receipt-detail-drawer', { receiptId: resourceId });
|
||||
onAction && onAction();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const ReceiptUniversalSearchSelect = withDrawerActions(
|
||||
ReceiptUniversalSearchSelectComponent,
|
||||
);
|
||||
|
||||
/**
|
||||
* Status accessor.
|
||||
*/
|
||||
function ReceiptStatus({ receipt }) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={receipt.is_closed}>
|
||||
<span class="closed">
|
||||
<T id={'closed'} />
|
||||
</span>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<span class="draft">
|
||||
<T id={'draft'} />
|
||||
</span>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receipt universal search item.
|
||||
*/
|
||||
export function ReceiptUniversalSearchItem(
|
||||
item,
|
||||
{ handleClick, modifiers, query },
|
||||
) {
|
||||
return (
|
||||
<MenuItem
|
||||
active={modifiers.active}
|
||||
text={
|
||||
<div>
|
||||
<div>{item.text}</div>
|
||||
<span class="bp3-text-muted">
|
||||
{item.reference.receipt_number}{' '}
|
||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||
{item.reference.formatted_receipt_date}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
label={
|
||||
<>
|
||||
<div class="amount">${item.reference.amount}</div>
|
||||
<ReceiptStatus receipt={item.reference} />
|
||||
</>
|
||||
}
|
||||
onClick={handleClick}
|
||||
className={'universal-search__item--receipt'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes receipt resource item to search item.
|
||||
*/
|
||||
const transformReceiptsToSearch = (receipt) => ({
|
||||
id: receipt.id,
|
||||
text: receipt.customer.display_name,
|
||||
label: receipt.formatted_amount,
|
||||
reference: receipt,
|
||||
});
|
||||
|
||||
/**
|
||||
* Receipt universal search bind configuration.
|
||||
*/
|
||||
export const universalSearchReceiptBind = () => ({
|
||||
resourceType: RESOURCES_TYPES.RECEIPT,
|
||||
optionItemLabel: intl.get('receipts'),
|
||||
selectItemAction: ReceiptUniversalSearchSelect,
|
||||
itemRenderer: ReceiptUniversalSearchItem,
|
||||
itemSelect: transformReceiptsToSearch,
|
||||
permission: {
|
||||
ability: SaleReceiptAction.View,
|
||||
subject: AbilitySubject.Receipt,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
const ReceiptDeleteAlert = React.lazy(
|
||||
() => import('@/containers/Alerts/Receipts/ReceiptDeleteAlert'),
|
||||
);
|
||||
const ReceiptCloseAlert = React.lazy(
|
||||
() => import('@/containers/Alerts/Receipts/ReceiptCloseAlert'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Receipts alerts.
|
||||
*/
|
||||
export default [
|
||||
{ name: 'receipt-delete', component: ReceiptDeleteAlert },
|
||||
{ name: 'receipt-close', component: ReceiptCloseAlert },
|
||||
];
|
||||
@@ -0,0 +1,171 @@
|
||||
// @ts-nocheck
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
NavbarDivider,
|
||||
NavbarGroup,
|
||||
Intent,
|
||||
Alignment,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
Icon,
|
||||
AdvancedFilterPopover,
|
||||
DashboardFilterButton,
|
||||
FormattedMessage as T,
|
||||
DashboardRowsHeightButton,
|
||||
} from '@/components';
|
||||
|
||||
import {
|
||||
Can,
|
||||
If,
|
||||
DashboardActionsBar,
|
||||
DashboardActionViewsList,
|
||||
} from '@/components';
|
||||
|
||||
import withReceipts from './withReceipts';
|
||||
import withReceiptsActions from './withReceiptsActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
import withSettingsActions from '@/containers/Settings/withSettingsActions';
|
||||
|
||||
import { useReceiptsListContext } from './ReceiptsListProvider';
|
||||
import { useRefreshReceipts } from '@/hooks/query/receipts';
|
||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Receipts actions bar.
|
||||
*/
|
||||
function ReceiptActionsBar({
|
||||
// #withReceiptsActions
|
||||
setReceiptsTableState,
|
||||
|
||||
// #withReceipts
|
||||
receiptsFilterConditions,
|
||||
|
||||
// #withSettings
|
||||
receiptsTableSize,
|
||||
|
||||
// #withSettingsActions
|
||||
addSetting,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
// Sale receipts list context.
|
||||
const { receiptsViews, fields } = useReceiptsListContext();
|
||||
|
||||
// Handle new receipt button click.
|
||||
const onClickNewReceipt = () => {
|
||||
history.push('/receipts/new');
|
||||
};
|
||||
|
||||
// Sale receipt refresh action.
|
||||
const { refresh } = useRefreshReceipts();
|
||||
|
||||
const handleTabChange = (view) => {
|
||||
setReceiptsTableState({
|
||||
viewSlug: view ? view.slug : null,
|
||||
});
|
||||
};
|
||||
|
||||
// Handle click a refresh sale estimates
|
||||
const handleRefreshBtnClick = () => {
|
||||
refresh();
|
||||
};
|
||||
|
||||
// Handle table row size change.
|
||||
const handleTableRowSizeChange = (size) => {
|
||||
addSetting('salesReceipts', 'tableSize', size);
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
<DashboardActionViewsList
|
||||
resourceName={'receipts'}
|
||||
allMenuItem={true}
|
||||
allMenuItemText={<T id={'all'} />}
|
||||
views={receiptsViews}
|
||||
onChange={handleTabChange}
|
||||
/>
|
||||
|
||||
<NavbarDivider />
|
||||
<Can I={SaleReceiptAction.Create} a={AbilitySubject.Receipt}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon={'plus'} />}
|
||||
text={<T id={'new_receipt'} />}
|
||||
onClick={onClickNewReceipt}
|
||||
/>
|
||||
</Can>
|
||||
<AdvancedFilterPopover
|
||||
advancedFilterProps={{
|
||||
conditions: receiptsFilterConditions,
|
||||
defaultFieldKey: 'reference_no',
|
||||
fields: fields,
|
||||
onFilterChange: (filterConditions) => {
|
||||
setReceiptsTableState({ filterRoles: filterConditions });
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DashboardFilterButton
|
||||
conditionsCount={receiptsFilterConditions.length}
|
||||
/>
|
||||
</AdvancedFilterPopover>
|
||||
|
||||
<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'} />}
|
||||
/>
|
||||
|
||||
<NavbarDivider />
|
||||
<DashboardRowsHeightButton
|
||||
initialValue={receiptsTableSize}
|
||||
onChange={handleTableRowSizeChange}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
</NavbarGroup>
|
||||
<NavbarGroup align={Alignment.RIGHT}>
|
||||
<Button
|
||||
className={Classes.MINIMAL}
|
||||
icon={<Icon icon="refresh-16" iconSize={14} />}
|
||||
onClick={handleRefreshBtnClick}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withReceiptsActions,
|
||||
withSettingsActions,
|
||||
withReceipts(({ receiptTableState }) => ({
|
||||
receiptsFilterConditions: receiptTableState.filterRoles,
|
||||
})),
|
||||
withSettings(({ receiptSettings }) => ({
|
||||
receiptsTableSize: receiptSettings?.tableSize,
|
||||
})),
|
||||
)(ReceiptActionsBar);
|
||||
@@ -0,0 +1,53 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
|
||||
|
||||
import { DashboardViewsTabs } from '@/components';
|
||||
import withReceiptActions from './withReceiptsActions';
|
||||
import withReceipts from './withReceipts';
|
||||
|
||||
import { compose, transfromViewsToTabs } from '@/utils';
|
||||
import { useReceiptsListContext } from './ReceiptsListProvider';
|
||||
|
||||
/**
|
||||
* Receipts views tabs.
|
||||
*/
|
||||
function ReceiptViewTabs({
|
||||
// #withReceiptActions
|
||||
setReceiptsTableState,
|
||||
|
||||
// #withReceipts
|
||||
receiptsCurrentView,
|
||||
}) {
|
||||
// Receipts list context.
|
||||
const { receiptsViews } = useReceiptsListContext();
|
||||
|
||||
const tabs = transfromViewsToTabs(receiptsViews);
|
||||
|
||||
// Handles the active tab chaning.
|
||||
const handleTabsChange = (viewSlug) => {
|
||||
setReceiptsTableState({
|
||||
viewSlug: viewSlug || null,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Navbar className={'navbar--dashboard-views'}>
|
||||
<NavbarGroup align={Alignment.LEFT}>
|
||||
<DashboardViewsTabs
|
||||
currentViewSlug={receiptsCurrentView}
|
||||
tabs={tabs}
|
||||
resourceName={'receipts'}
|
||||
onChange={handleTabsChange}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withReceiptActions,
|
||||
withReceipts(({ receiptTableState }) => ({
|
||||
receiptsCurrentView: receiptTableState.viewSlug,
|
||||
})),
|
||||
)(ReceiptViewTabs);
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { EmptyStatus } from '@/components';
|
||||
import { Can, FormattedMessage as T } from '@/components';
|
||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
export default function ReceiptsEmptyStatus() {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<EmptyStatus
|
||||
title={<T id={'manage_the_organization_s_services_and_products'} />}
|
||||
description={
|
||||
<p>
|
||||
<T id={'receipt_empty_status_description'} />
|
||||
</p>
|
||||
}
|
||||
action={
|
||||
<>
|
||||
<Can I={SaleReceiptAction.Create} a={AbilitySubject.Receipt}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
large={true}
|
||||
onClick={() => {
|
||||
history.push('/receipts/new');
|
||||
}}
|
||||
>
|
||||
<T id={'new_receipt'} />
|
||||
</Button>
|
||||
|
||||
<Button intent={Intent.NONE} large={true}>
|
||||
<T id={'learn_more'} />
|
||||
</Button>
|
||||
</Can>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { DashboardPageContent } from '@/components';
|
||||
|
||||
import '@/style/pages/SaleReceipt/List.scss';
|
||||
|
||||
import ReceiptActionsBar from './ReceiptActionsBar';
|
||||
import ReceiptViewTabs from './ReceiptViewTabs';
|
||||
import ReceiptsTable from './ReceiptsTable';
|
||||
|
||||
import withReceipts from './withReceipts';
|
||||
import withReceiptsActions from './withReceiptsActions';
|
||||
|
||||
import { ReceiptsListProvider } from './ReceiptsListProvider';
|
||||
import { transformTableStateToQuery, compose } from '@/utils';
|
||||
|
||||
/**
|
||||
* Receipts list page.
|
||||
*/
|
||||
function ReceiptsList({
|
||||
// #withReceipts
|
||||
receiptTableState,
|
||||
receiptsTableStateChanged,
|
||||
|
||||
// #withReceiptsActions
|
||||
resetReceiptsTableState,
|
||||
}) {
|
||||
// Resets the receipts table state once the page unmount.
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
resetReceiptsTableState();
|
||||
},
|
||||
[resetReceiptsTableState],
|
||||
);
|
||||
|
||||
return (
|
||||
<ReceiptsListProvider
|
||||
query={transformTableStateToQuery(receiptTableState)}
|
||||
tableStateChanged={receiptsTableStateChanged}
|
||||
>
|
||||
<DashboardPageContent>
|
||||
<ReceiptActionsBar />
|
||||
|
||||
<DashboardPageContent>
|
||||
<ReceiptViewTabs />
|
||||
<ReceiptsTable />
|
||||
</DashboardPageContent>
|
||||
</DashboardPageContent>
|
||||
</ReceiptsListProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withReceipts(({ receiptTableState, receiptsTableStateChanged }) => ({
|
||||
receiptTableState,
|
||||
receiptsTableStateChanged,
|
||||
})),
|
||||
withReceiptsActions,
|
||||
)(ReceiptsList);
|
||||
@@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import React, { createContext } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { DashboardInsider } from '@/components/Dashboard';
|
||||
|
||||
import { useResourceMeta, useResourceViews, useReceipts } from '@/hooks/query';
|
||||
import { getFieldsFromResourceMeta } from '@/utils';
|
||||
|
||||
const ReceiptsListContext = createContext();
|
||||
|
||||
// Receipts list provider.
|
||||
function ReceiptsListProvider({ query, tableStateChanged, ...props }) {
|
||||
// Fetch receipts resource views and fields.
|
||||
const { data: receiptsViews, isLoading: isViewsLoading } =
|
||||
useResourceViews('sale_receipt');
|
||||
|
||||
// Fetches the sale receipts resource fields.
|
||||
const {
|
||||
data: resourceMeta,
|
||||
isFetching: isResourceFetching,
|
||||
isLoading: isResourceLoading,
|
||||
} = useResourceMeta('sale_receipt');
|
||||
|
||||
const {
|
||||
data: { receipts, pagination, filterMeta },
|
||||
isLoading: isReceiptsLoading,
|
||||
isFetching: isReceiptsFetching,
|
||||
} = useReceipts(query, { keepPreviousData: true });
|
||||
|
||||
// Detarmines the datatable empty status.
|
||||
const isEmptyStatus =
|
||||
isEmpty(receipts) && !tableStateChanged && !isReceiptsLoading;
|
||||
|
||||
const provider = {
|
||||
receipts,
|
||||
pagination,
|
||||
|
||||
receiptsViews,
|
||||
isViewsLoading,
|
||||
|
||||
resourceMeta,
|
||||
fields: getFieldsFromResourceMeta(resourceMeta.fields),
|
||||
isResourceFetching,
|
||||
isResourceLoading,
|
||||
|
||||
isReceiptsLoading,
|
||||
isReceiptsFetching,
|
||||
isEmptyStatus,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={isViewsLoading || isResourceLoading}
|
||||
name={'sales_receipts'}
|
||||
>
|
||||
<ReceiptsListContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
const useReceiptsListContext = () => React.useContext(ReceiptsListContext);
|
||||
|
||||
export { ReceiptsListProvider, useReceiptsListContext };
|
||||
@@ -0,0 +1,160 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { compose } from '@/utils';
|
||||
import {
|
||||
DataTable,
|
||||
DashboardContentTable,
|
||||
TableSkeletonRows,
|
||||
TableSkeletonHeader,
|
||||
} from '@/components';
|
||||
import { TABLES } from '@/constants/tables';
|
||||
|
||||
import ReceiptsEmptyStatus from './ReceiptsEmptyStatus';
|
||||
|
||||
import withReceipts from './withReceipts';
|
||||
import withReceiptsActions from './withReceiptsActions';
|
||||
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
||||
import withSettings from '@/containers/Settings/withSettings';
|
||||
|
||||
import { useReceiptsListContext } from './ReceiptsListProvider';
|
||||
import { useReceiptsTableColumns, ActionsMenu } from './components';
|
||||
import { useMemorizedColumnsWidths } from '@/hooks';
|
||||
|
||||
/**
|
||||
* Sale receipts datatable.
|
||||
*/
|
||||
function ReceiptsDataTable({
|
||||
// #withReceiptsActions
|
||||
setReceiptsTableState,
|
||||
|
||||
// #withReceipts
|
||||
receiptTableState,
|
||||
|
||||
// #withAlertsActions
|
||||
openAlert,
|
||||
|
||||
// #withDrawerActions
|
||||
openDrawer,
|
||||
|
||||
// #withDialogAction
|
||||
openDialog,
|
||||
|
||||
// #withSettings
|
||||
receiptsTableSize,
|
||||
}) {
|
||||
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 });
|
||||
};
|
||||
|
||||
// Handle view detail receipt.
|
||||
const handleViewDetailReceipt = ({ id }) => {
|
||||
openDrawer('receipt-detail-drawer', { receiptId: id });
|
||||
};
|
||||
|
||||
// Handle print receipt.
|
||||
const handlePrintInvoice = ({ id }) => {
|
||||
openDialog('receipt-pdf-preview', { receiptId: id });
|
||||
};
|
||||
|
||||
// Local storage memorizing columns widths.
|
||||
const [initialColumnsWidths, , handleColumnResizing] =
|
||||
useMemorizedColumnsWidths(TABLES.RECEIPTS);
|
||||
|
||||
// Handles the datable fetch data once the state changing.
|
||||
const handleDataTableFetchData = useCallback(
|
||||
({ sortBy, pageIndex, pageSize }) => {
|
||||
setReceiptsTableState({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
sortBy,
|
||||
});
|
||||
},
|
||||
[setReceiptsTableState],
|
||||
);
|
||||
|
||||
if (isEmptyStatus) {
|
||||
return <ReceiptsEmptyStatus />;
|
||||
}
|
||||
// Handle cell click.
|
||||
const handleCellClick = (cell, event) => {
|
||||
openDrawer('receipt-detail-drawer', { receiptId: cell.row.original.id });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardContentTable>
|
||||
<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}
|
||||
onCellClick={handleCellClick}
|
||||
initialColumnsWidths={initialColumnsWidths}
|
||||
onColumnResizing={handleColumnResizing}
|
||||
size={receiptsTableSize}
|
||||
payload={{
|
||||
onEdit: handleEditReceipt,
|
||||
onDelete: handleDeleteReceipt,
|
||||
onClose: handleCloseReceipt,
|
||||
onViewDetails: handleViewDetailReceipt,
|
||||
onPrint: handlePrintInvoice,
|
||||
}}
|
||||
/>
|
||||
</DashboardContentTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAlertsActions,
|
||||
withReceiptsActions,
|
||||
withDrawerActions,
|
||||
withDialogActions,
|
||||
withReceipts(({ receiptTableState }) => ({
|
||||
receiptTableState,
|
||||
})),
|
||||
withSettings(({ receiptSettings }) => ({
|
||||
receiptsTableSize: receiptSettings?.tableSize,
|
||||
})),
|
||||
)(ReceiptsDataTable);
|
||||
@@ -0,0 +1,181 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import clsx from 'classnames';
|
||||
import { FormattedMessage as T } from '@/components';
|
||||
import {
|
||||
Position,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuDivider,
|
||||
Intent,
|
||||
Popover,
|
||||
Tag,
|
||||
Button,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
import { CLASSES } from '@/constants/classes';
|
||||
import { safeCallback } from '@/utils';
|
||||
import { FormatDateCell, Choose, Money, Icon, If, Can } from '@/components';
|
||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||
|
||||
/**
|
||||
* Receipts table row actions menu.
|
||||
* @returns {React.JSX}
|
||||
*/
|
||||
export function ActionsMenu({
|
||||
payload: { onEdit, onDelete, onClose, onDrawer, onViewDetails, onPrint },
|
||||
row: { original: receipt },
|
||||
}) {
|
||||
return (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={<Icon icon="reader-18" />}
|
||||
text={intl.get('view_details')}
|
||||
onClick={safeCallback(onViewDetails, receipt)}
|
||||
/>
|
||||
<Can I={SaleReceiptAction.Edit} a={AbilitySubject.Receipt}>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
icon={<Icon icon="pen-18" />}
|
||||
text={intl.get('edit_receipt')}
|
||||
onClick={safeCallback(onEdit, receipt)}
|
||||
/>
|
||||
|
||||
<If condition={!receipt.is_closed}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'check'} iconSize={18} />}
|
||||
text={intl.get('mark_as_closed')}
|
||||
onClick={safeCallback(onClose, receipt)}
|
||||
/>
|
||||
</If>
|
||||
</Can>
|
||||
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'print-16'} iconSize={16} />}
|
||||
text={intl.get('print')}
|
||||
onClick={safeCallback(onPrint, receipt)}
|
||||
/>
|
||||
</Can>
|
||||
<Can I={SaleReceiptAction.Delete} a={AbilitySubject.Receipt}>
|
||||
<MenuDivider />
|
||||
<MenuItem
|
||||
text={intl.get('delete_receipt')}
|
||||
intent={Intent.DANGER}
|
||||
onClick={safeCallback(onDelete, receipt)}
|
||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
||||
/>
|
||||
</Can>
|
||||
</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} round={true}>
|
||||
<T id={'closed'} />
|
||||
</Tag>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
<Tag minimal={true} intent={Intent.WARNING} round={true}>
|
||||
<T id={'draft'} />
|
||||
</Tag>
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve receipts table columns.
|
||||
*/
|
||||
export function useReceiptsTableColumns() {
|
||||
return React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'receipt_date',
|
||||
Header: intl.get('receipt_date'),
|
||||
accessor: 'receipt_date',
|
||||
Cell: FormatDateCell,
|
||||
width: 140,
|
||||
className: 'receipt_date',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'customer',
|
||||
Header: intl.get('customer_name'),
|
||||
accessor: 'customer.display_name',
|
||||
width: 140,
|
||||
className: 'customer_id',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'receipt_number',
|
||||
Header: intl.get('receipt_number'),
|
||||
accessor: 'receipt_number',
|
||||
width: 140,
|
||||
className: 'receipt_number',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'deposit_account',
|
||||
Header: intl.get('deposit_account'),
|
||||
accessor: 'deposit_account.name',
|
||||
width: 140,
|
||||
className: 'deposit_account',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
Header: intl.get('amount'),
|
||||
accessor: (r) => <Money amount={r.amount} currency={r.currency_code} />,
|
||||
width: 140,
|
||||
align: 'right',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
className: clsx(CLASSES.FONT_BOLD),
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
Header: intl.get('status'),
|
||||
accessor: StatusAccessor,
|
||||
width: 140,
|
||||
className: 'status',
|
||||
clickable: true,
|
||||
},
|
||||
{
|
||||
id: 'reference_no',
|
||||
Header: intl.get('reference_no'),
|
||||
accessor: 'reference_no',
|
||||
width: 140,
|
||||
className: 'reference_no',
|
||||
clickable: true,
|
||||
textOverview: true,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getReceiptsTableStateFactory,
|
||||
receiptsTableStateChangedFactory,
|
||||
} from '@/store/receipts/receipts.selector';
|
||||
|
||||
export default (mapState) => {
|
||||
const getReceiptsTableState = getReceiptsTableStateFactory();
|
||||
const receiptsTableStateChanged = receiptsTableStateChangedFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
receiptTableState: getReceiptsTableState(state, props),
|
||||
receiptsTableStateChanged: receiptsTableStateChanged(state, props),
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
// @ts-nocheck
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
setReceiptsTableState,
|
||||
resetReceiptsTableState,
|
||||
} from '@/store/receipts/receipts.actions';
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setReceiptsTableState: (queries) => dispatch(setReceiptsTableState(queries)),
|
||||
resetReceiptsTableState: () => dispatch(resetReceiptsTableState()),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
Reference in New Issue
Block a user