chrone: sperate client and server to different repos.

This commit is contained in:
a.bouhuolia
2021-09-21 17:13:53 +02:00
parent e011b2a82b
commit 18df5530c7
10015 changed files with 17686 additions and 97524 deletions

View File

@@ -0,0 +1,29 @@
import React, { lazy } from 'react';
import withDrawers from 'containers/Drawer/withDrawers';
import { Drawer, DrawerSuspense } from 'components';
import { compose } from 'utils';
const ReceiptDrawerContent = lazy(() => import('./ReceiptDrawerContent'));
/**
* receipt drawer.
*/
const ReceiptDrawer = ({
name,
//#withDrawer
isOpen,
payload: { receiptId },
}) => {
return (
<div>
<Drawer isOpen={isOpen} name={name}>
<DrawerSuspense>
<ReceiptDrawerContent receiptId={receiptId} />
</DrawerSuspense>
</Drawer>
</div>
);
};
export default compose(withDrawers())(ReceiptDrawer);

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { ReceiptDrawerProvider } from './ReceiptDrawerProvider';
import ReceiptPaper from './ReceiptPaper';
/**
* Receipt drawer content.
*/
export default function ReceiptDrawerContent({
// #ownProp
receiptId,
}) {
return (
<ReceiptDrawerProvider receiptId={receiptId}>
<ReceiptPaper />
</ReceiptDrawerProvider>
);
}

View File

@@ -0,0 +1,33 @@
import React, { createContext, useContext } from 'react';
import { useReceipt } from 'hooks/query';
import { DrawerHeaderContent, DashboardInsider } from 'components';
const ReceiptDrawerContext = createContext();
function ReceiptDrawerProvider({ receiptId, ...props }) {
// Fetch sale receipt details.
const {
data: { entries, ...receipt },
isFetching: isReceiptLoading,
} = useReceipt(receiptId, {
enabled: !!receiptId,
});
// Provider payload.
const provider = {
receiptId,
receipt,
entries,
isReceiptLoading,
};
return (
<DashboardInsider loading={isReceiptLoading}>
<DrawerHeaderContent name={'receipt-drawer'} />
<ReceiptDrawerContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useReceiptDrawerContext = () => useContext(ReceiptDrawerContext);
export { ReceiptDrawerProvider, useReceiptDrawerContext };

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useReceiptDrawerContext } from './ReceiptDrawerProvider';
import PaperTemplate from 'containers/Drawers/PaperTemplate/PaperTemplate';
import intl from 'react-intl-universal';
export default function ReceiptPaper() {
const { receipt, entries } = useReceiptDrawerContext();
const propLabels = {
labels: {
name: intl.get('receipt_'),
billedTo: intl.get('billed_to'),
date: intl.get('receipt_date_'),
refNo: intl.get('receipt_no'),
billedFrom: intl.get('billed_from'),
amount: intl.get('receipt_amount'),
dueDate: intl.get('due_date_'),
},
};
const receipts = {
billedTo: receipt.customer.display_name,
date: receipt.receipt_date,
amount: '',
billedFrom: '',
dueDate: receipt.due_date,
referenceNo: receipt.receipt_number,
...receipt,
};
return (
<PaperTemplate
labels={propLabels.labels}
paperData={receipts}
entries={entries}
/>
);
}

View File

@@ -0,0 +1,196 @@
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { omit, 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 { useReceiptFormContext } from './ReceiptFormProvider';
import ReceiptFromHeader from './ReceiptFormHeader';
import ReceiptItemsEntriesEditor from './ReceiptItemsEntriesEditor';
import ReceiptFormFloatingActions from './ReceiptFormFloatingActions';
import ReceiptFormFooter from './ReceiptFormFooter';
import ReceiptFormDialogs from './ReceiptFormDialogs';
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 } 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), currency_code: base_currency }
: {
...defaultReceipt,
...(receiptAutoIncrement && {
receipt_number: nextReceiptNumber,
}),
deposit_account_id: parseInt(preferredDepositAccount),
entries: orderingLinesIndexes(defaultReceipt.entries),
currency_code: base_currency,
}),
}),
[receipt, preferredDepositAccount, nextReceiptNumber, receiptAutoIncrement],
);
// Transform response error to fields.
const handleErrors = (errors, { setErrors }) => {
if (errors.some((e) => e.type === ERROR.SALE_RECEIPT_NUMBER_NOT_UNIQUE)) {
setErrors({
receipt_number: intl.get('sale_receipt_number_not_unique'),
});
}
if (errors.some((e) => e.type === ERROR.SALE_RECEIPT_NO_IS_REQUIRED)) {
setErrors({
receipt_number: intl.get('receipt.field.error.receipt_number_required'),
});
}
};
// 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 = {
...omit(values, ['receipt_number_manually', 'receipt_number']),
...(values.receipt_number_manually && {
receipt_number: values.receipt_number,
currency_code: base_currency,
}),
closed: submitPayload.status,
entries: entries.map((entry) => ({ ...omit(entry, ['total']) })),
};
// 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>
<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);

View File

@@ -0,0 +1,57 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/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(),
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,25 @@
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}
/>
</>
);
}

View File

@@ -0,0 +1,190 @@
import React from 'react';
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 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() {
// 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}
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,56 @@
import React from 'react';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FastField } from 'formik';
import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import { Dragzone, Postbox, 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)}>
<Postbox title={<T id={'receipt_details'}/>} defaultOpen={false}>
<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={<T id={'attachments_maximum'} />}
/>
</Col>
</Row>
</Postbox>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import intl from 'react-intl-universal';
import { CLASSES } from 'common/classes';
import { PageFormBigNumber } from 'components';
import ReceiptFormHeaderFields from './ReceiptFormHeaderFields';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { getEntriesTotal } from 'containers/Entries/utils';
import { compose } from 'redux';
/**
* Receipt form header section.
*/
function ReceiptFormHeader({
// #ownProps
onReceiptNumberChanged,
// #withCurrentOrganization
organization: { base_currency },
}) {
const { values } = useFormikContext();
// Calculate the total due amount of bill entries.
const totalDueAmount = useMemo(
() => getEntriesTotal(values.entries),
[values.entries],
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<ReceiptFormHeaderFields
onReceiptNumberChanged={onReceiptNumberChanged}
/>
<PageFormBigNumber
label={intl.get('due_amount')}
amount={totalDueAmount}
currencyCode={base_currency}
/>
</div>
);
}
export default compose(withCurrentOrganization())(ReceiptFormHeader);

View File

@@ -0,0 +1,228 @@
import React, { useCallback } from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames';
import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
ContactSelecetList,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { ACCOUNT_TYPE } from 'common/accountTypes';
import {
momentFormatter,
compose,
tansformDateValue,
handleDateChange,
inputIntent,
} from 'utils';
import { useReceiptFormContext } from './ReceiptFormProvider';
import {
accountsFieldShouldUpdate,
customersFieldShouldUpdate,
useObserveReceiptNoSettings,
} from './utils';
/**
* Receipt form header fields.
*/
function ReceiptFormHeader({
//#withDialogActions
openDialog,
// #ownProps
onReceiptNumberChanged,
// #withSettings
receiptAutoIncrement,
receiptNextNumber,
receiptNumberPrefix,
}) {
const { accounts, customers } = 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'} />}
>
<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'}
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,
]}
/>
</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>
</div>
);
}
export default compose(
withDialogActions,
withSettings(({ receiptSettings }) => ({
receiptAutoIncrement: receiptSettings?.autoIncrement,
receiptNextNumber: receiptSettings?.nextNumber,
receiptNumberPrefix: receiptSettings?.numberPrefix,
})),
)(ReceiptFormHeader);

View File

@@ -0,0 +1,21 @@
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>
);
}

View File

@@ -0,0 +1,101 @@
import React, { createContext, useState } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useReceipt,
useAccounts,
useSettingsReceipts,
useCustomers,
useItems,
useCreateReceipt,
useEditReceipt,
} from 'hooks/query';
const ReceiptFormContext = createContext();
/**
* Receipt form provider.
*/
function ReceiptFormProvider({ receiptId, ...props }) {
// 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 });
// 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 receipt settings.
const { isLoading: isSettingLoading } = useSettingsReceipts();
const { mutateAsync: createReceiptMutate } = useCreateReceipt();
const { mutateAsync: editReceiptMutate } = useEditReceipt();
const [submitPayload, setSubmitPayload] = useState({});
const isNewMode = !receiptId;
const provider = {
receiptId,
receipt,
accounts,
customers,
items,
submitPayload,
isNewMode,
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,34 @@
import React from 'react';
import classNames from 'classnames';
import { FastField } from 'formik';
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
import { CLASSES } from 'common/classes';
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>
);
}

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { useFormikContext } from 'formik';
import moment from 'moment';
import * as R from 'ramda';
import {
defaultFastFieldShouldUpdate,
transactionNumber,
repeatValue,
transformToForm,
} from 'utils';
import {
updateItemsEntriesTotal,
ensureEntriesHaveEmptyLine,
} from 'containers/Entries/utils';
export const MIN_LINES_NUMBER = 4;
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: '',
entries: [...repeatValue(defaultReceiptEntry, MIN_LINES_NUMBER)],
};
/**
* 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)
);
};

View File

@@ -0,0 +1,103 @@
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 "../../../common/resourcesTypes";
import withDrawerActions from "../../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,
});

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,137 @@
import React, { useState } from 'react';
import Icon from 'components/Icon';
import {
Button,
Classes,
NavbarDivider,
NavbarGroup,
Intent,
Alignment,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
AdvancedFilterPopover,
DashboardFilterButton,
FormattedMessage as T,
} from 'components';
import { If, DashboardActionViewsList } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withReceiptsActions from './withReceiptsActions';
import withReceipts from './withReceipts';
import { useReceiptsListContext } from './ReceiptsListProvider';
import { useRefreshReceipts } from 'hooks/query/receipts';
import { compose } from 'utils';
/**
* Receipts actions bar.
*/
function ReceiptActionsBar({
// #withReceiptsActions
setReceiptsTableState,
// #withReceipts
receiptsFilterConditions,
}) {
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();
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
resourceName={'receipts'}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
views={receiptsViews}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'new_receipt'} />}
onClick={onClickNewReceipt}
/>
<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'} />}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withReceiptsActions,
withReceipts(({ receiptTableState }) => ({
receiptsFilterConditions: receiptTableState.filterRoles,
})),
)(ReceiptActionsBar);

View File

@@ -0,0 +1,52 @@
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);

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import { EmptyStatus } from 'components';
import { FormattedMessage as T } from 'components';
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={
<>
<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>
</>
}
/>
);
}

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from 'components';
import 'style/pages/SaleReceipt/List.scss';
import ReceiptActionsBar from './ReceiptActionsBar';
import ReceiptViewTabs from './ReceiptViewTabs';
import ReceiptsAlerts from '../ReceiptsAlerts';
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>
<ReceiptsAlerts />
</DashboardPageContent>
</ReceiptsListProvider>
);
}
export default compose(
withReceipts(({ receiptTableState, receiptsTableStateChanged }) => ({
receiptTableState,
receiptsTableStateChanged,
})),
withReceiptsActions,
)(ReceiptsList);

View File

@@ -0,0 +1,60 @@
import React, { createContext } from 'react';
import { isEmpty } from 'lodash';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
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} name={'sales_receipts'}>
<ReceiptsListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useReceiptsListContext = () => React.useContext(ReceiptsListContext);
export { ReceiptsListProvider, useReceiptsListContext };

View File

@@ -0,0 +1,148 @@
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import { compose } from 'utils';
import { DataTable, DashboardContentTable } from 'components';
import { TABLES } from 'common/tables';
import ReceiptsEmptyStatus from './ReceiptsEmptyStatus';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withReceipts from './withReceipts';
import withReceiptsActions from './withReceiptsActions';
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,
}) {
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}
payload={{
onEdit: handleEditReceipt,
onDelete: handleDeleteReceipt,
onClose: handleCloseReceipt,
onViewDetails: handleViewDetailReceipt,
onPrint: handlePrintInvoice,
}}
/>
</DashboardContentTable>
);
}
export default compose(
withAlertsActions,
withReceiptsActions,
withDrawerActions,
withDialogActions,
withReceipts(({ receiptTableState }) => ({
receiptTableState,
})),
)(ReceiptsDataTable);

View File

@@ -0,0 +1,167 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import {
Position,
Menu,
MenuItem,
MenuDivider,
Intent,
Popover,
Tag,
Button,
} from '@blueprintjs/core';
import clsx from 'classnames';
import { CLASSES } from '../../../../common/classes';
import { safeCallback } from 'utils';
import { FormatDateCell, Choose, Money, Icon, If } from 'components';
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)}
/>
<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>
<MenuItem
icon={<Icon icon={'print-16'} iconSize={16} />}
text={intl.get('print')}
onClick={safeCallback(onPrint, receipt)}
/>
<MenuItem
text={intl.get('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() {
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,
},
],
[],
);
}

View File

@@ -0,0 +1,19 @@
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);
};

View File

@@ -0,0 +1,12 @@
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);