Merge branch 'feature/credit-memo' into develop

This commit is contained in:
a.bouhuolia
2021-12-08 15:21:39 +02:00
122 changed files with 5662 additions and 75 deletions

View File

@@ -46,7 +46,7 @@ export const SaleInvoiceAction = {
Create: 'Create',
Edit: 'Edit',
Delete: 'Delete',
Writeoff: 'Writeoff',
Writeoff: 'bad-debt',
NotifyBySms: 'NotifyBySms',
};

View File

@@ -39,6 +39,8 @@ const CLASSES = {
PAGE_FORM_ITEM: 'page-form--item',
PAGE_FORM_MAKE_JOURNAL: 'page-form--make-journal-entries',
PAGE_FORM_EXPENSE: 'page-form--expense',
PAGE_FORM_CREDIT_NOTE:'page-form--credit-note',
PAGE_FORM_VENDOR_CREDIT_NOTE:'page-form--vendor-credit-note',
FORM_GROUP_LIST_SELECT: 'form-group--select-list',

View File

@@ -14,4 +14,6 @@ export const DRAWERS = {
QUICK_WRITE_VENDOR: 'quick-write-vendor',
QUICK_CREATE_CUSTOMER: 'quick-create-customer',
QUICK_CREATE_ITEM: 'quick-create-item',
CREDIT_NOTE_DETAIL_DRAWER: 'credit-note-detail-drawer',
VENDOR_CREDIT_DETAIL_DRAWER: 'vendor-credit-detail-drawer',
};

View File

@@ -11,4 +11,6 @@ export const RESOURCES_TYPES = {
EXPENSE: 'expense',
MANUAL_JOURNAL: 'manual_journal',
ACCOUNT: 'account',
CREDIT_NOTE: 'credit_note',
VENDOR_CREDIT:'vendor_credit'
};

View File

@@ -14,6 +14,8 @@ export const TABLES = {
EXPENSES: 'expenses',
CASHFLOW_ACCOUNTS: 'cashflow_accounts',
CASHFLOW_Transactions: 'cashflow_transactions',
CREDIT_NOTES: 'credit_notes',
VENDOR_CREDITS: 'vendor_credits',
};
export const TABLE_SIZE = {

View File

@@ -17,6 +17,8 @@ import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTrans
import QuickCreateCustomerDrawer from '../containers/Drawers/QuickCreateCustomerDrawer';
import QuickCreateItemDrawer from '../containers/Drawers/QuickCreateItemDrawer';
import QuickWriteVendorDrawer from '../containers/Drawers/QuickWriteVendorDrawer';
import CreditNoteDetailDrawer from '../containers/Drawers/CreditNoteDetailDrawer';
import VendorCreditDetailDrawer from '../containers/Drawers/VendorCreditDetailDrawer';
import { DRAWERS } from 'common/drawers';
@@ -47,6 +49,8 @@ export default function DrawersContainer() {
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} />
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
<CreditNoteDetailDrawer name={DRAWERS.CREDIT_NOTE_DETAIL_DRAWER} />
<VendorCreditDetailDrawer name={DRAWERS.VENDOR_CREDIT_DETAIL_DRAWER} />
</div>
);
}

View File

@@ -158,6 +158,10 @@ export default [
ability: SaleReceiptAction.View,
},
},
{
text: <T id={'sidebar_credit_note'} />,
href: '/credit-notes',
},
{
text: <T id={'payment_receives'} />,
href: '/payment-receives',
@@ -233,6 +237,10 @@ export default [
ability: SaleReceiptAction.Create,
},
},
{
text: <T id={'credit_note.label.new_credit_note'} />,
href: '/credit-notes/new',
},
{
text: <T id={'new_payment_receive'} />,
href: '/payment-receives/new',
@@ -254,6 +262,10 @@ export default [
ability: BillAction.View,
},
},
{
text: <T id={'sidebar_vendor_credits'} />,
href: '/vendor-credits',
},
{
text: <T id={'payment_mades'} />,
href: '/payment-mades',
@@ -298,6 +310,14 @@ export default [
ability: BillAction.Create,
},
},
{
text: <T id={'vendor_credits.label.new_vendor_credit'} />,
href: '/vendor-credits/new',
permission: {
subject: AbilitySubject.Bill,
ability: BillAction.Create,
},
},
{
text: <T id={'new_payment_made'} />,
href: '/payment-mades/new',
@@ -405,14 +425,14 @@ export default [
ability: ManualJournalAction.View,
},
},
{
text: <T id={'sidebar.transactions_locaking'} />,
href: '/transactions-locking',
permission: {
subject: AbilitySubject.ManualJournal,
ability: ManualJournalAction.TransactionLocking,
},
},
// {
// text: <T id={'sidebar.transactions_locaking'} />,
// href: '/transactions-locking',
// permission: {
// subject: AbilitySubject.ManualJournal,
// ability: ManualJournalAction.TransactionLocking,
// },
// },
{
text: <T id={'exchange_rate'} />,
href: '/exchange-rates',

View File

@@ -0,0 +1,80 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { useDeleteCreditNote } from 'hooks/query';
import { compose } from 'utils';
/**
* Credit note delete alert.
*/
function CreditNoteDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { creditNoteId },
// #withAlertActions
closeAlert,
// #withDrawerActions
closeDrawer,
}) {
const { isLoading, mutateAsync: deleteCreditNoteMutate } =
useDeleteCreditNote();
// handle cancel delete credit note alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
const handleConfirmCreditNoteDelete = () => {
deleteCreditNoteMutate(creditNoteId)
.then(() => {
AppToaster.show({
message: intl.get('credit_note.alert.success_message'),
intent: Intent.SUCCESS,
});
closeDrawer('credit-note-detail-drawer');
})
.catch(
({
response: {
data: { errors },
},
}) => {},
)
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmCreditNoteDelete}
loading={isLoading}
>
<p>
<FormattedHTMLMessage id={'credit_note.once_delete_this_credit_note'} />
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
withDrawerActions,
)(CreditNoteDeleteAlert);

View File

@@ -0,0 +1,82 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormattedMessage as T, FormattedHTMLMessage } from 'components';
import { Intent, Alert } from '@blueprintjs/core';
import { AppToaster } from 'components';
import withAlertStoreConnect from 'containers/Alert/withAlertStoreConnect';
import withAlertActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { useDeleteVendorCredit } from 'hooks/query';
import { compose } from 'utils';
/**
* Vendor Credit delete alert.
*/
function VendorCreditDeleteAlert({
name,
// #withAlertStoreConnect
isOpen,
payload: { vendorCreditId },
// #withAlertActions
closeAlert,
// #withDrawerActions
closeDrawer,
}) {
const { isLoading, mutateAsync: deleteVendorCreditMutate } =
useDeleteVendorCredit();
// handle cancel delete credit note alert.
const handleCancelDeleteAlert = () => {
closeAlert(name);
};
const handleConfirmCreditDelete = () => {
deleteVendorCreditMutate(vendorCreditId)
.then(() => {
AppToaster.show({
message: intl.get('vendor_credits.alert.success_message'),
intent: Intent.SUCCESS,
});
closeDrawer('vendor-credit-detail-drawer');
})
.catch(
({
response: {
data: { errors },
},
}) => {},
)
.finally(() => {
closeAlert(name);
});
};
return (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={<T id={'delete'} />}
icon="trash"
intent={Intent.DANGER}
isOpen={isOpen}
onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmCreditDelete}
loading={isLoading}
>
<p>
<FormattedHTMLMessage
id={'vendor_credits.note.once_delete_this_vendor_credit_note'}
/>
</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
withDrawerActions,
)(VendorCreditDeleteAlert);

View File

@@ -17,6 +17,8 @@ import AccountTransactionsAlerts from '../CashFlow/AccountTransactions/AccountTr
import UsersAlerts from '../Preferences/Users/UsersAlerts';
import CurrenciesAlerts from '../Preferences/Currencies/CurrenciesAlerts';
import RolesAlerts from '../Preferences/Users/Roles/RolesAlerts';
import CreditNotesAlerts from '../Sales/CreditNotes/CreditNotesAlerts';
import VendorCreditNotesAlerts from '../Purchases/CreditNotes/VendorCreditNotesAlerts';
export default [
...AccountsAlerts,
@@ -38,4 +40,6 @@ export default [
...UsersAlerts,
...CurrenciesAlerts,
...RolesAlerts,
...CreditNotesAlerts,
...VendorCreditNotesAlerts,
];

View File

@@ -90,8 +90,10 @@ export default function AllocateLandedCostFormFields() {
selectedItem={value}
selectedItemProp={'id'}
textProp={'name'}
labelProp={'id'}
defaultText={intl.get('Select transaction')}
labelProp={'formatted_unallocated_cost_amount'}
defaultText={intl.get(
'landed_cost.dialog.label_select_transaction',
)}
popoverProps={{ minimal: true }}
/>
</FormGroup>
@@ -129,7 +131,10 @@ export default function AllocateLandedCostFormFields() {
selectedItem={value}
selectedItemProp={'id'}
textProp={'name'}
defaultText={intl.get('Select transaction entry')}
labelProp={'formatted_unallocated_cost_amount'}
defaultText={intl.get(
'landed_cost.dialog.label_select_transaction_entry',
)}
popoverProps={{ minimal: true }}
/>
</FormGroup>

View File

@@ -0,0 +1,102 @@
import React from 'react';
import intl from 'react-intl-universal';
import { useSaveSettings } from 'hooks/query';
import { CreditNoteNumberDialogProvider } from './CreditNoteNumberDialogProvider';
import ReferenceNumberForm from 'containers/JournalNumber/ReferenceNumberForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withSettings from 'containers/Settings/withSettings';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils';
import {
transformFormToSettings,
transformSettingsToForm,
} from 'containers/JournalNumber/utils';
/**
* credit note number dialog content
*/
function CreditNoteNumberDialogContent({
// #ownProps
initialValues,
onConfirm,
// #withSettings
nextNumber,
numberPrefix,
autoIncrement,
// #withDialogActions
closeDialog,
}) {
const { mutateAsync: saveSettings } = useSaveSettings();
const [referenceFormValues, setReferenceFormValues] = React.useState(null);
// Handle the submit form.
const handleSubmitForm = (values, { setSubmitting }) => {
// Handle the form success.
const handleSuccess = () => {
setSubmitting(false);
closeDialog('credit-number-form');
onConfirm(values);
};
// Handle the form errors.
const handleErrors = () => {
setSubmitting(false);
};
if (values.incrementMode === 'manual-transaction') {
handleSuccess();
return;
}
// Transformes the form values to settings to save it.
const options = transformFormToSettings(values, 'credit_note');
// Save the settings.
saveSettings({ options }).then(handleSuccess).catch(handleErrors);
};
// Handle the dialog close.
const handleClose = () => {
closeDialog('credit-number-form');
};
// Handle form change.
const handleChange = (values) => {
setReferenceFormValues(values);
};
// Description.
const description =
referenceFormValues?.incrementMode === 'auto'
? intl.get('credit_note.auto_increment.auto')
: intl.get('credit_note.auto_increment.manually');
return (
<CreditNoteNumberDialogProvider>
<ReferenceNumberForm
initialValues={{
...transformSettingsToForm({
nextNumber,
numberPrefix,
autoIncrement,
}),
...initialValues,
}}
description={description}
onSubmit={handleSubmitForm}
onClose={handleClose}
onChange={handleChange}
/>
</CreditNoteNumberDialogProvider>
);
}
export default compose(
withDialogActions,
withSettingsActions,
withSettings(({ creditNoteSettings }) => ({
autoIncrement: creditNoteSettings?.autoIncrement,
nextNumber: creditNoteSettings?.nextNumber,
numberPrefix: creditNoteSettings?.numberPrefix,
})),
)(CreditNoteNumberDialogContent);

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { DialogContent } from 'components';
import { useSettingsCreditNotes } from 'hooks/query';
const CreditNoteNumberDialogContext = React.createContext();
/**
*Credit Note number dialog provider
*/
function CreditNoteNumberDialogProvider({ query, ...props }) {
const { isLoading: isSettingsLoading } = useSettingsCreditNotes();
// Provider payload.
const provider = {
isSettingsLoading,
};
return (
<DialogContent isLoading={isSettingsLoading}>
<CreditNoteNumberDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useCreditNoteNumberDialogContext = () =>
React.useContext(CreditNoteNumberDialogContext);
export { CreditNoteNumberDialogProvider, useCreditNoteNumberDialogContext };

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose, saveInvoke } from 'utils';
const CreditNoteNumberDialogContent = React.lazy(() =>
import('./CreditNoteNumberDialogContent'),
);
/**
* Credit note number dialog.
*/
function CreditNoteNumberDialog({
dialogName,
payload: { initialFormValues },
isOpen,
onConfirm,
}) {
const handleConfirm = (values) => {
saveInvoke(onConfirm, values);
};
return (
<Dialog
title={<T id={'credit_note_number_settings'} />}
name={dialogName}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
>
<DialogSuspense>
<CreditNoteNumberDialogContent
initialValues={{ ...initialFormValues }}
onConfirm={handleConfirm}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(CreditNoteNumberDialog);

View File

@@ -24,7 +24,7 @@ function InviteUserFormContent({
const handleClose = () => {
closeDialog(dialogName);
};
console.log(roles, 'XX');
return (
<Form>
<div className={CLASSES.DIALOG_BODY}>

View File

@@ -0,0 +1,102 @@
import React from 'react';
import intl from 'react-intl-universal';
import { useSaveSettings } from 'hooks/query';
import { VendorCreditNumberDilaogProvider } from './VendorCreditNumberDilaogProvider';
import ReferenceNumberForm from 'containers/JournalNumber/ReferenceNumberForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withSettings from 'containers/Settings/withSettings';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils';
import {
transformFormToSettings,
transformSettingsToForm,
} from 'containers/JournalNumber/utils';
/**
* Vendor credit number dialog
*/
function VendorCreditNumberDialogContent({
// #ownProps
initialValues,
onConfirm,
// #withSettings
nextNumber,
numberPrefix,
autoIncrement,
// #withDialogActions
closeDialog,
}) {
const { mutateAsync: saveSettings } = useSaveSettings();
const [referenceFormValues, setReferenceFormValues] = React.useState(null);
// Handle the submit form.
const handleSubmitForm = (values, { setSubmitting }) => {
// Handle the form success.
const handleSuccess = () => {
setSubmitting(false);
closeDialog('vendor-credit-form');
onConfirm(values);
};
// Handle the form errors.
const handleErrors = () => {
setSubmitting(false);
};
if (values.incrementMode === 'manual-transaction') {
handleSuccess();
return;
}
// Transformes the form values to settings to save it.
const options = transformFormToSettings(values, 'vendor_credit');
// Save the settings.
saveSettings({ options }).then(handleSuccess).catch(handleErrors);
};
// Handle the dialog close.
const handleClose = () => {
closeDialog('vendor-credit-form');
};
// Handle form change.
const handleChange = (values) => {
setReferenceFormValues(values);
};
// Description.
const description =
referenceFormValues?.incrementMode === 'auto'
? intl.get('vendor_credit.auto_increment.auto')
: intl.get('vendor_credit.auto_increment.manually');
return (
<VendorCreditNumberDilaogProvider>
<ReferenceNumberForm
initialValues={{
...transformSettingsToForm({
nextNumber,
numberPrefix,
autoIncrement,
}),
...initialValues,
}}
description={description}
onSubmit={handleSubmitForm}
onClose={handleClose}
onChange={handleChange}
/>
</VendorCreditNumberDilaogProvider>
);
}
export default compose(
withDialogActions,
withSettingsActions,
withSettings(({ vendorsCreditNoteSetting }) => ({
autoIncrement: vendorsCreditNoteSetting?.autoIncrement,
nextNumber: vendorsCreditNoteSetting?.nextNumber,
numberPrefix: vendorsCreditNoteSetting?.numberPrefix,
})),
)(VendorCreditNumberDialogContent);

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { DialogContent } from 'components';
import { useSettingsVendorCredits } from 'hooks/query';
const VendorCreditNumberDialogContext = React.createContext();
/**
* Vendor credit number dialog provider
*/
function VendorCreditNumberDilaogProvider({ query, ...props }) {
const { isLoading: isSettingsLoading } = useSettingsVendorCredits();
// Provider payload.
const provider = {
isSettingsLoading,
};
return (
<DialogContent isLoading={isSettingsLoading}>
<VendorCreditNumberDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useVendorCreditNumberDialogContext = () =>
React.useContext(VendorCreditNumberDialogContext);
export { VendorCreditNumberDilaogProvider, useVendorCreditNumberDialogContext };

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Dialog, DialogSuspense, FormattedMessage as T } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose, saveInvoke } from 'utils';
const VendorCreditNumberDialogContent = React.lazy(() =>
import('./VendorCreditNumberDialogContent'),
);
/**
* Vendor Credit number dialog.
*/
function VendorCreditNumberDialog({
dialogName,
payload: { initialFormValues },
isOpen,
onConfirm,
}) {
const handleConfirm = (values) => {
saveInvoke(onConfirm, values);
};
return (
<Dialog
title={<T id={'vendor_credit_number_settings'} />}
name={dialogName}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
>
<DialogSuspense>
<VendorCreditNumberDialogContent
initialValues={{ ...initialFormValues }}
onConfirm={handleConfirm}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(VendorCreditNumberDialog);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { DrawerBody } from 'components';
import 'style/components/Drawers/ViewDetail/ViewDetail.scss';
import { BillDrawerProvider } from './BillDrawerProvider';
import BillDrawerDetails from './BillDrawerDetails';

View File

@@ -5,6 +5,8 @@ import { Button, Classes, NavbarGroup } from '@blueprintjs/core';
import { useLocatedLandedCostColumns, ActionsMenu } from './components';
import { useBillDrawerContext } from './BillDrawerProvider';
import '../../../style/pages/AllocateLandedCost/List.scss';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';

View File

@@ -1,5 +1,6 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent, MenuItem, Menu } from '@blueprintjs/core';
import { safeCallback } from 'utils';
import { Icon } from 'components';
@@ -24,7 +25,7 @@ export function ActionsMenu({ row: { original }, payload: { onDelete } }) {
*/
export function FromTransactionCell({
row: { original },
payload: { onFromTranscationClick }
payload: { onFromTranscationClick },
}) {
// Handle the link click
const handleAnchorClick = () => {
@@ -38,6 +39,18 @@ export function FromTransactionCell({
);
}
/**
* Name accessor.
*/
export const NameAccessor = (row) => {
return (
<span className="name">
<LabelName>{row.name}</LabelName>
<LabelDescription>{row.description}</LabelDescription>
</span>
);
};
/**
* Retrieve bill located landed cost table columns.
*/
@@ -46,7 +59,7 @@ export function useLocatedLandedCostColumns() {
() => [
{
Header: intl.get('name'),
accessor: 'description',
accessor: NameAccessor,
width: 150,
className: 'name',
},
@@ -65,7 +78,7 @@ export function useLocatedLandedCostColumns() {
},
{
Header: intl.get('allocation_method'),
accessor: 'allocation_method_formatted',
accessor: 'allocation_method',
width: 100,
className: 'allocation-method',
},
@@ -73,3 +86,12 @@ export function useLocatedLandedCostColumns() {
[],
);
}
const LabelName = styled.div``;
const LabelDescription = styled.div`
font-size: 12px;
margin-top: 6px;
display: block;
opacity: 0.75;
`;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { DrawerMainTabs } from 'components';
import CreditNoteDetailPanel from './CreditNoteDetailPanel';
import clsx from 'classnames';
import CreditNoteDetailCls from '../../../style/components/Drawers/CreditNoteDetails.module.scss';
/**
* Credit Note view detail.
*/
export default function CreditNoteDetail() {
return (
<div className={clsx(CreditNoteDetailCls.root)}>
<DrawerMainTabs>
<Tab
title={intl.get('details')}
id={'details'}
panel={<CreditNoteDetailPanel />}
/>
</DrawerMainTabs>
</div>
);
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
Intent,
} from '@blueprintjs/core';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { Icon, FormattedMessage as T, MoreMenuItems, Can } from 'components';
import { compose } from 'utils';
/**
* Credit note detail actions bar.
*/
function CreditNoteDetailActionsBar({
// #withDialogActions
openDialog,
// #withAlertsActions
openAlert,
// #withDrawerActions
closeDrawer,
}) {
const { creditNoteId } = useCreditNoteDetailDrawerContext();
const history = useHistory();
// Handle edit credit note.
const handleEditCreditNote = () => {
history.push(`/credit-notes/${creditNoteId}/edit`);
closeDrawer('credit-note-detail-drawer');
};
// Handle delete credit note.
const handleDeleteCreditNote = () => {
openAlert('credit-note-delete', { creditNoteId });
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="pen-18" />}
text={<T id={'credit_note.action.edit_credit_note'} />}
onClick={handleEditCreditNote}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'trash-16'} iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleDeleteCreditNote}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withAlertsActions,
withDrawerActions,
)(CreditNoteDetailActionsBar);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { DrawerBody } from 'components';
import CreditNoteDetail from './CreditNoteDetail';
import { CreditNoteDetailDrawerProvider } from './CreditNoteDetailDrawerProvider';
/**
* Credit note detail drawer content.
*/
export default function CreditNoteDetailDrawerContent({
// #ownProp
creditNoteId,
}) {
return (
<CreditNoteDetailDrawerProvider creditNoteId={creditNoteId}>
<DrawerBody>
<CreditNoteDetail />
</DrawerBody>
</CreditNoteDetailDrawerProvider>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import clsx from 'classnames';
import { T, TotalLines, TotalLine, If } from 'components';
import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider';
import { FormatNumber } from '../../../components';
import CreditNoteDetailCls from '../../../style/components/Drawers/CreditNoteDetails.module.scss';
/**
* Credit note details panel footer.
*/
export default function CreditNoteDetailDrawerFooter() {
const { creditNote } = useCreditNoteDetailDrawerContext();
return (
<div className={clsx(CreditNoteDetailCls.detail_panel_footer)}>
<TotalLines className={clsx(CreditNoteDetailCls.total_lines)}>
<TotalLine
title={<T id={'credit_note.drawer.label_subtotal'} />}
value={<FormatNumber value={creditNote.formatted_amount} />}
className={CreditNoteDetailCls.total_line_subtotal}
/>
<TotalLine
title={<T id={'credit_note.drawer.label_total'} />}
value={creditNote.formatted_amount}
className={CreditNoteDetailCls.total_line_total}
/>
</TotalLines>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import intl from 'react-intl-universal';
import { useCreditNote } from 'hooks/query';
import { DrawerHeaderContent, DrawerLoading } from 'components';
const CreditNoteDetailDrawerContext = React.createContext();
/**
* Credit note detail drawer provider.
*/
function CreditNoteDetailDrawerProvider({ creditNoteId, ...props }) {
// Handle fetch vendor credit details.
const { data: creditNote, isLoading: isCreditNoteLoading } = useCreditNote(
creditNoteId,
{
enabled: !!creditNoteId,
},
);
const provider = {
creditNote,
creditNoteId,
};
return (
<DrawerLoading loading={isCreditNoteLoading}>
<DrawerHeaderContent
name="credit-note-detail-drawer"
title={intl.get('credit_note.drawer_credit_note_detail')}
/>
<CreditNoteDetailDrawerContext.Provider value={provider} {...props} />
</DrawerLoading>
);
}
const useCreditNoteDetailDrawerContext = () =>
React.useContext(CreditNoteDetailDrawerContext);
export { CreditNoteDetailDrawerProvider, useCreditNoteDetailDrawerContext };

View File

@@ -0,0 +1,52 @@
import React from 'react';
import intl from 'react-intl-universal';
import { defaultTo } from 'lodash';
import clsx from 'classnames';
import { FormatDate, T, DetailsMenu, DetailItem } from 'components';
import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider';
import CreditNoteDetailCls from '../../../style/components/Drawers/CreditNoteDetails.module.scss';
/**
* Credit note details drawer header.
*/
export default function CreditNoteDetailHeader() {
const { creditNote } = useCreditNoteDetailDrawerContext();
return (
<div className={clsx(CreditNoteDetailCls.detail_panel_header)}>
<DetailsMenu>
<DetailItem label={intl.get('amount')}>
<span class="big-number">{creditNote.formatted_amount}</span>
</DetailItem>
<DetailItem
label={intl.get('credit_note.drawer.label_credit_note_no')}
children={defaultTo(creditNote.credit_note_number, '-')}
/>
<DetailItem
label={intl.get('customer_name')}
children={creditNote.customer?.display_name}
/>
<DetailItem
label={intl.get('credit_note.drawer.label_credit_note_date')}
children={
<FormatDate value={creditNote.formatted_credit_note_date} />
}
/>
</DetailsMenu>
<DetailsMenu direction={'horizantal'} minLabelSize={'140px'}>
<DetailItem
label={intl.get('note')}
children={defaultTo(creditNote.note, '-')}
/>
<DetailItem
label={<T id={'credit_note.drawer.label_create_at'} />}
children={<FormatDate value={creditNote.created_at} />}
/>
</DetailsMenu>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import clsx from 'classnames';
import { Card } from 'components';
import CreditNoteDetailActionsBar from './CreditNoteDetailActionsBar';
import CreditNoteDetailHeader from './CreditNoteDetailHeader';
import CreditNoteDetailTable from './CreditNoteDetailTable';
import CreditNoteDetailDrawerFooter from './CreditNoteDetailDrawerFooter';
import CreditNoteDetailCls from '../../../style/components/Drawers/CreditNoteDetails.module.scss';
export default function CreditNoteDetailPanel() {
return (
<div className={clsx(CreditNoteDetailCls.detail_panel)}>
<CreditNoteDetailActionsBar />
<Card>
<CreditNoteDetailHeader />
<CreditNoteDetailTable />
<CreditNoteDetailDrawerFooter />
</Card>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import clsx from 'classnames';
import { DataTable } from 'components';
import { useCreditNoteDetailDrawerContext } from './CreditNoteDetailDrawerProvider';
import { useCreditNoteReadOnlyEntriesColumns } from './utils';
import CreditNoteDetailCls from '../../../style/components/Drawers/CreditNoteDetails.module.scss';
/**
* Credit note detail table.
*/
export default function CreditNoteDetailTable() {
const {
creditNote: { entries },
} = useCreditNoteDetailDrawerContext();
// Credit note entries table columns.
const columns = useCreditNoteReadOnlyEntriesColumns();
return (
<div className={clsx(CreditNoteDetailCls.detail_panel_table)}>
<DataTable
columns={columns}
data={entries}
className={'table-constrant'}
/>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const CreditNoteDetailDrawerContent = React.lazy(() =>
import('./CreditNoteDetailDrawerContent'),
);
/**
* Credit note detail drawer.
*/
function CreditNoteDetailDrawer({
name,
// #withDrawer
isOpen,
payload: { creditNoteId },
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'65%'}
>
<DrawerSuspense>
<CreditNoteDetailDrawerContent creditNoteId={creditNoteId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(CreditNoteDetailDrawer);

View File

@@ -0,0 +1,47 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormatNumberCell } from '../../../components';
export const useCreditNoteReadOnlyEntriesColumns = () =>
React.useMemo(
() => [
{
Header: intl.get('product_and_service'),
accessor: 'item.name',
width: 150,
className: 'name',
disableSortBy: true,
},
{
Header: intl.get('description'),
accessor: 'description',
className: 'description',
disableSortBy: true,
},
{
Header: intl.get('quantity'),
accessor: 'quantity',
Cell: FormatNumberCell,
width: 100,
align: 'right',
disableSortBy: true,
},
{
Header: intl.get('rate'),
accessor: 'rate',
Cell: FormatNumberCell,
width: 100,
align: 'right',
disableSortBy: true,
},
{
Header: intl.get('amount'),
accessor: 'amount',
Cell: FormatNumberCell,
width: 100,
align: 'right',
disableSortBy: true,
},
],
[],
);

View File

@@ -90,11 +90,13 @@ function EstimateDetailActionsBar({
/>
<NavbarDivider />
</Can>
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
<Can I={SaleEstimateAction.NotifyBySms} a={AbilitySubject.Estimate}>
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
</Can>
</NavbarGroup>
</DashboardActionsBar>
);

View File

@@ -120,9 +120,9 @@ function InvoiceDetailActionsBar({
intent={Intent.DANGER}
onClick={handleDeleteInvoice}
/>
<NavbarDivider />
</Can>
<Can I={SaleInvoiceAction.Writeoff} a={AbilitySubject.Invoice}>
<NavbarDivider />
<BadDebtMenuItem
payload={{
onBadDebt: handleBadDebtInvoice,

View File

@@ -8,7 +8,11 @@ import {
MenuItem,
Menu,
} from '@blueprintjs/core';
import { Icon, FormattedMessage as T, Choose } from 'components';
import { Icon, FormattedMessage as T, Choose, Can } from 'components';
import {
SaleInvoiceAction,
AbilitySubject,
} from '../../../common/abilityOption';
import { FormatNumberCell } from '../../../components';
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
@@ -88,10 +92,12 @@ export const BadDebtMenuItem = ({
/>
</Choose.When>
</Choose>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
<Can I={SaleInvoiceAction.NotifyBySms} a={AbilitySubject.Invoice}>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
</Can>
</Menu>
}
>

View File

@@ -80,11 +80,13 @@ function PaymentReceiveActionsBar({
/>
<NavbarDivider />
</Can>
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
<Can I={PaymentReceiveAction.NotifyBySms} a={AbilitySubject.PaymentReceive}>
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
</Can>
</NavbarGroup>
</DashboardActionsBar>
);

View File

@@ -85,11 +85,13 @@ function ReceiptDetailActionBar({
/>
<NavbarDivider />
</Can>
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
<Can I={SaleReceiptAction.NotifyBySms} a={AbilitySubject.Receipt}>
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
</Can>
</NavbarGroup>
</DashboardActionsBar>
);

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Tab } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { DrawerMainTabs } from 'components';
import VendorCreditDetailPanel from './VendorCreditDetailPanel';
import clsx from 'classnames';
import VendorCreditDetailCls from '../../../style/components/Drawers/VendorCreditDetail.module.scss';
/**
* Vendor credit view detail.
*/
export default function VendorCreditDetail() {
return (
<div className={clsx(VendorCreditDetailCls.root)}>
<DrawerMainTabs>
<Tab
title={intl.get('details')}
id={'details'}
panel={<VendorCreditDetailPanel />}
/>
</DrawerMainTabs>
</div>
);
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
Button,
NavbarGroup,
Classes,
NavbarDivider,
Intent,
} from '@blueprintjs/core';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { Icon, FormattedMessage as T, Can } from 'components';
import { compose } from 'utils';
/**
* Vendor credit detail actions bar.
*/
function VendorCreditDetailActionsBar({
// #withDialogActions
openDialog,
// #withAlertsActions
openAlert,
// #withDrawerActions
closeDrawer,
}) {
const { vendorCreditId } = useVendorCreditDetailDrawerContext();
const history = useHistory();
// Handle edit credit note.
const handleEditVendorCredit = () => {
history.push(`/vendor-credits/${vendorCreditId}/edit`);
closeDrawer('vendor-credit-detail-drawer');
};
// Handle delete credit note.
const handleDeleteVendorCredit = () => {
openAlert('vendor-credit-delete', { vendorCreditId });
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="pen-18" />}
text={<T id={'vendor_credits.label.edit_vendor_credit'} />}
onClick={handleEditVendorCredit}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'trash-16'} iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handleDeleteVendorCredit}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withDialogActions,
withAlertsActions,
withDrawerActions,
)(VendorCreditDetailActionsBar);

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { DrawerBody } from 'components';
import VendorCreditDetail from './VendorCreditDetail';
import { VendorCreditDetailDrawerProvider } from './VendorCreditDetailDrawerProvider';
/**
* Vendor credit detail drawer content.
*/
export default function VendorCreditDetailDrawerContent({ vendorCreditId }) {
return (
<VendorCreditDetailDrawerProvider vendorCreditId={vendorCreditId}>
<DrawerBody>
<VendorCreditDetail />
</DrawerBody>
</VendorCreditDetailDrawerProvider>
);
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import clsx from 'classnames';
import { T, TotalLines, TotalLine, If } from 'components';
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
import { FormatNumber } from '../../../components';
import VendorCreditDetailCls from '../../../style/components/Drawers/VendorCreditDetail.module.scss';
/**
* Vendor Credit detail panel footer.
*/
export default function VendorCreditDetailDrawerFooter() {
const { vendorCredit } = useVendorCreditDetailDrawerContext();
return (
<div className={clsx(VendorCreditDetailCls.detail_panel_footer)}>
<TotalLines className={clsx(VendorCreditDetailCls.total_lines)}>
<TotalLine
title={<T id={'vendor_credit.drawer.label_subtotal'} />}
value={<FormatNumber value={vendorCredit.formatted_amount} />}
className={VendorCreditDetailCls.total_line_subtotal}
/>
<TotalLine
title={<T id={'vendor_credit.drawer.label_total'} />}
value={vendorCredit.formatted_amount}
className={VendorCreditDetailCls.total_line_total}
/>
</TotalLines>
</div>
);
}

View File

@@ -0,0 +1,37 @@
import React from 'react';
import intl from 'react-intl-universal';
import { useVendorCredit } from 'hooks/query';
import { DrawerHeaderContent, DrawerLoading } from 'components';
const VendorCreditDetailDrawerContext = React.createContext();
/**
* Vendor credit drawer provider.
*/
function VendorCreditDetailDrawerProvider({ vendorCreditId, ...props }) {
// Handle fetch vendor credit details.
const { data: vendorCredit, isLoading: isVendorCreditLoading } =
useVendorCredit(vendorCreditId, {
enabled: !!vendorCreditId,
});
const provider = {
vendorCredit,
vendorCreditId,
};
return (
<DrawerLoading loading={isVendorCreditLoading}>
<DrawerHeaderContent
name="vendor-credit-detail-drawer"
title={intl.get('vendor_credit.drawer_vendor_credit_detail')}
/>
<VendorCreditDetailDrawerContext.Provider value={provider} {...props} />
</DrawerLoading>
);
}
const useVendorCreditDetailDrawerContext = () =>
React.useContext(VendorCreditDetailDrawerContext);
export { VendorCreditDetailDrawerProvider, useVendorCreditDetailDrawerContext };

View File

@@ -0,0 +1,51 @@
import React from 'react';
import intl from 'react-intl-universal';
import { defaultTo } from 'lodash';
import clsx from 'classnames';
import { FormatDate, T, DetailsMenu, DetailItem } from 'components';
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
import VendorCreditDetailCls from '../../../style/components/Drawers/VendorCreditDetail.module.scss';
/**
* Vendor credit detail drawer header.
*/
export default function VendorCreditDetailHeader() {
const { vendorCredit } = useVendorCreditDetailDrawerContext();
return (
<div className={clsx(VendorCreditDetailCls.detail_panel_header)}>
<DetailsMenu>
<DetailItem label={intl.get('amount')}>
<span class="big-number">{vendorCredit.formatted_amount}</span>
</DetailItem>
<DetailItem
label={intl.get('vendor_credit.drawer.label_vendor_credit_no')}
children={defaultTo(vendorCredit.vendor_credit_number, '-')}
/>
<DetailItem
label={intl.get('vendor_name')}
children={vendorCredit.vendor?.display_name}
/>
<DetailItem
label={intl.get('vendor_credit.drawer.label_vendor_credit_date')}
children={
<FormatDate value={vendorCredit.formatted_vendor_credit_date} />
}
/>
</DetailsMenu>
<DetailsMenu direction={'horizantal'} minLabelSize={'140px'}>
<DetailItem
label={intl.get('note')}
children={defaultTo(vendorCredit.note, '-')}
/>
<DetailItem
label={<T id={'vendor_credit.drawer.label_create_at'} />}
children={<FormatDate value={vendorCredit.created_at} />}
/>
</DetailsMenu>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import clsx from 'classnames';
import { Card } from 'components';
import VendorCreditDetailActionsBar from './VendorCreditDetailActionsBar';
import VendorCreditDetailHeader from './VendorCreditDetailHeader';
import VendorCreditDetailTable from './VendorCreditDetailTable';
import VendorCreditDetailDrawerFooter from './VendorCreditDetailDrawerFooter';
import VendorCreditDetailCls from '../../../style/components/Drawers/VendorCreditDetail.module.scss';
export default function VendorCreditDetailPanel() {
return (
<div className={clsx(VendorCreditDetailCls.detail_panel)}>
<VendorCreditDetailActionsBar />
<Card>
<VendorCreditDetailHeader />
<VendorCreditDetailTable />
<VendorCreditDetailDrawerFooter />
</Card>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import clsx from 'classnames';
import { DataTable } from 'components';
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
import { useVendorCreditReadonlyEntriesTableColumns } from './utils';
import VendorCreditDetailCls from '../../../style/components/Drawers/VendorCreditDetail.module.scss';
/**
* Vendor Credit detail table.
*/
export default function VendorCreditDetailTable() {
const {
vendorCredit: { entries },
} = useVendorCreditDetailDrawerContext();
// Vendor Credit entries table columns.
const columns = useVendorCreditReadonlyEntriesTableColumns();
return (
<div className={clsx(VendorCreditDetailCls.detail_panel_table)}>
<DataTable
columns={columns}
data={entries}
className={'table-constrant'}
/>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const VendorCreditDetailDrawerContent = React.lazy(() =>
import('./VendorCreditDetailDrawerContent'),
);
/**
* Vendor Credit detail drawer.
*/
function VendorCreditDetailDrawer({
name,
// #withDrawer
isOpen,
payload: { vendorCreditId },
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'65%'}
>
<DrawerSuspense>
<VendorCreditDetailDrawerContent vendorCreditId={vendorCreditId} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(VendorCreditDetailDrawer);

View File

@@ -0,0 +1,51 @@
import React from 'react';
import intl from 'react-intl-universal';
import { FormatNumberCell } from '../../../components';
/**
* Retrieve vendor credit readonly details entries table columns.
*/
export const useVendorCreditReadonlyEntriesTableColumns = () =>
React.useMemo(
() => [
{
Header: intl.get('product_and_service'),
accessor: 'item.name',
width: 150,
className: 'item',
disableSortBy: true,
},
{
Header: intl.get('description'),
accessor: 'description',
className: 'description',
disableSortBy: true,
},
{
Header: intl.get('quantity'),
accessor: 'quantity',
Cell: FormatNumberCell,
width: 100,
align: 'right',
disableSortBy: true,
},
{
Header: intl.get('rate'),
accessor: 'rate',
Cell: FormatNumberCell,
width: 100,
align: 'right',
disableSortBy: true,
},
{
Header: intl.get('amount'),
accessor: 'amount',
Cell: FormatNumberCell,
width: 100,
align: 'right',
disableSortBy: true,
},
],
[],
);

View File

@@ -21,6 +21,7 @@ import {
import RolesFormContent from './RolesFormContent';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { handleDeleteErrors } from '../utils';
import { compose, transformToForm } from 'utils';
@@ -82,8 +83,14 @@ function RolesForm({
history.push('/preferences/users');
};
const onError = (errors) => {
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
handleDeleteErrors(errors);
};
if (isNewMode) {
createRolePermissionMutate(form).then(onSuccess).catch(onError);

View File

@@ -10,4 +10,22 @@ export const handleDeleteErrors = (errors) => {
intent: Intent.DANGER,
});
}
if (errors.find((error) => error.type === 'INVALIDATE_PERMISSIONS')) {
AppToaster.show({
message: intl.get('roles.error.the_submit_role_has_invalid_permissions'),
intent: Intent.DANGER,
});
}
if (
errors.find(
(error) => error.type === 'CANNOT_DELETE_ROLE_ASSOCIATED_TO_USERS',
)
) {
AppToaster.show({
message: intl.get(
'roles.error.you_cannot_delete_role_that_associated_to_users',
),
intent: Intent.DANGER,
});
}
};

View File

@@ -0,0 +1,159 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { If, Icon, FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider';
/**
* Purchases Credit note floating actions.
*/
export default function VendorCreditNoteFloatingActions() {
const history = useHistory();
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
// Credit note form context.
const { setSubmitPayload, isNewMode } = useVendorCreditNoteFormContext();
// Handle submit, save and anothe new button click.
const handleSubmitAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: true, resetForm: true });
submitForm();
};
// Handle submit as save & continue editing button click.
const handleSubmitSaveContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: true });
submitForm();
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
setSubmitPayload({ redirect: true, status: false });
submitForm();
};
// handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: false, resetForm: true });
submitForm();
};
// Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: false });
submitForm();
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
history.goBack();
};
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
setSubmitPayload({ redirect: true });
submitForm();
};
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitSaveContinueEditingBtnClick}
/>
</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>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,169 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Formik, Form } from 'formik';
import { Button, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { sumBy, omit, isEmpty } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
CreateCreditNoteFormSchema,
EditCreditNoteFormSchema,
} from './VendorCreditNoteForm.schema';
import VendorCreditNoteFormHeader from './VendorCreditNoteFormHeader';
import VendorCreditNoteItemsEntriesEditor from './VendorCreditNoteItemsEntriesEditor';
import VendorCreditNoteFormFooter from './VendorCreditNoteFormFooter';
import VendorCreditNoteFloatingActions from './VendorCreditNoteFloatingActions';
import VendorCreditNoteFormDialogs from './VendorCreditNoteFormDialogs';
import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider';
import { AppToaster } from 'components';
import { compose, safeSumBy, transactionNumber } from 'utils';
import {
defaultVendorsCreditNote,
filterNonZeroEntries,
transformToEditForm,
transformFormValuesToRequest,
} from './utils';
import withSettings from 'containers/Settings/withSettings';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
/**
* Vendor Credit note form.
*/
function VendorCreditNoteForm({
// #withSettings
vendorcreditAutoIncrement,
vendorcreditNumberPrefix,
vendorcreditNextNumber,
// #withCurrentOrganization
organization: { base_currency },
}) {
const history = useHistory();
// Vendor Credit note form context.
const {
isNewMode,
submitPayload,
vendorCredit,
createVendorCreditMutate,
editVendorCreditMutate,
} = useVendorCreditNoteFormContext();
// Credit number.
const vendorCreditNumber = transactionNumber(
vendorcreditNumberPrefix,
vendorcreditNextNumber,
);
// Initial values.
const initialValues = React.useMemo(
() => ({
...(!isEmpty(vendorCredit)
? {
...transformToEditForm(vendorCredit),
}
: {
...defaultVendorsCreditNote,
...(vendorcreditAutoIncrement && {
vendor_credit_number: vendorCreditNumber,
}),
}),
}),
[vendorCredit, base_currency],
);
// Handles form submit.
const handleFormSubmit = (
values,
{ setSubmitting, setErrors, resetForm },
) => {
const entries = filterNonZeroEntries(values.entries);
const totalQuantity = safeSumBy(entries, '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),
};
// Handle the request success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'vendor_credits.success_message'
: 'vendor_credits.edit_success_message',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.redirect) {
history.push('/vendor-credits');
}
if (submitPayload.resetForm) {
resetForm();
}
};
// Handle the request error.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
if (isNewMode) {
createVendorCreditMutate(form).then(onSuccess).catch(onError);
} else {
editVendorCreditMutate([vendorCredit.id, form])
.then(onSuccess)
.catch(onError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_VENDOR_CREDIT_NOTE,
)}
>
<Formik
validationSchema={
isNewMode ? CreateCreditNoteFormSchema : EditCreditNoteFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<VendorCreditNoteFormHeader />
<VendorCreditNoteItemsEntriesEditor />
<VendorCreditNoteFormFooter />
<VendorCreditNoteFloatingActions />
<VendorCreditNoteFormDialogs />
</Form>
</Formik>
</div>
);
}
export default compose(
withSettings(({ vendorsCreditNoteSetting }) => ({
vendorcreditAutoIncrement: vendorsCreditNoteSetting?.autoIncrement,
vendorcreditNextNumber: vendorsCreditNoteSetting?.nextNumber,
vendorcreditNumberPrefix: vendorsCreditNoteSetting?.numberPrefix,
})),
withCurrentOrganization(),
)(VendorCreditNoteForm);

View File

@@ -0,0 +1,41 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const getSchema = Yup.object().shape({
vendor_id: Yup.number().required().label(intl.get('vendor_name_')),
vendor_credit_date: Yup.date().required().label(intl.get('bill_date_')),
vendor_credit_number: Yup.string()
.max(DATATYPES_LENGTH.STRING)
.label(intl.get('bill_number_')),
reference_no: Yup.string().nullable().min(1).max(DATATYPES_LENGTH.STRING),
note: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.TEXT)
.label(intl.get('note')),
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),
}),
),
});
export const CreateCreditNoteFormSchema = getSchema;
export const EditCreditNoteFormSchema = getSchema;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import VendorCreditNumberDialog from '../../../Dialogs/VendorCreditNumberDialog';
import { useFormikContext } from 'formik';
/**
* Vendor credit form dialog.
*/
export default function VendorCreditNoteFormDialogs() {
// Update the form once the vendor credit number form submit confirm.
const handleVendorCreditNumberFormConfirm = ({
incrementNumber,
manually,
}) => {
setFieldValue('vendor_credit_number', incrementNumber || '');
setFieldValue('vendor_credit_no_manually', manually);
};
const { setFieldValue } = useFormikContext();
return (
<React.Fragment>
<VendorCreditNumberDialog
dialogName={'vendor-credit-form'}
onConfirm={handleVendorCreditNumberFormConfirm}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { FastField } from 'formik';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import { Row, Col, Postbox } from 'components';
import { inputIntent } from 'utils';
import classNames from 'classnames';
/**
* Vendor Credit note form footer.
*/
export default function VendorCreditNoteFormFooter() {
return (
<div class={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Postbox
title={<T id={'credit_note.label_credit_note_details'} />}
defaultOpen={false}
>
<Row>
<Col md={8}>
<FastField name={'note'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'note'} />}
className={'form-group--note'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
</Col>
</Row>
</Postbox>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { useFormikContext } from 'formik';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import VendorCreditNoteFormHeaderFields from './VendorCreditNoteFormHeaderFields';
import { getEntriesTotal } from 'containers/Entries/utils';
import { PageFormBigNumber } from 'components';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose } from 'utils';
/**
* Vendor Credit note header.
*/
function VendorCreditNoteFormHeader({
// #withCurrentOrganization
organization: { base_currency },
}) {
const { values } = useFormikContext();
// Calculate the total amount.
const totalAmount = React.useMemo(
() => getEntriesTotal(values.entries),
[values.entries],
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<VendorCreditNoteFormHeaderFields />
<PageFormBigNumber
label={intl.get('due_amount')}
amount={totalAmount}
currencyCode={base_currency}
/>
</div>
);
}
export default compose(withCurrentOrganization())(
VendorCreditNoteFormHeader,
);

View File

@@ -0,0 +1,200 @@
import React from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, Field, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import {
ContactSelecetList,
FieldRequiredHint,
InputPrependButton,
Icon,
FormattedMessage as T,
} from 'components';
import {
vendorsFieldShouldUpdate,
useObserveVendorCreditNoSettings,
} from './utils';
import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider';
import {
momentFormatter,
compose,
tansformDateValue,
inputIntent,
handleDateChange,
} from 'utils';
import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions';
/**
* Vendor Credit note form header fields.
*/
function VendorCreditNoteFormHeaderFields({
// #withDialogActions
openDialog,
// #withSettings
vendorcreditAutoIncrement,
vendorcreditNumberPrefix,
vendorcreditNextNumber,
}) {
// Vendor Credit form context.
const { vendors } = useVendorCreditNoteFormContext();
// Handle vendor credit number changing.
const handleVendorCreditNumberChange = () => {
openDialog('vendor-credit-form');
};
// Handle vendor credit no. field blur.
const handleVendorCreditNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && vendorcreditAutoIncrement) {
openDialog('vendor-credit-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs vendor credit number settings with form.
useObserveVendorCreditNoSettings(
vendorcreditNumberPrefix,
vendorcreditNextNumber,
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Vendor name ----------- */}
<FastField
name={'vendor_id'}
customers={vendors}
shouldUpdate={vendorsFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'vendor_name'} />}
inline={true}
className={classNames(
'form-group--vendor-name',
'form-group--select-list',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'vendor_id'} />}
>
<ContactSelecetList
contactsList={vendors}
selectedContactId={value}
defaultSelectText={<T id={'select_vender_account'} />}
onContactSelected={(vendor) => {
form.setFieldValue('vendor_id', vendor.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ------- Vendor Credit date ------- */}
<FastField name={'vendor_credit_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'credit_note.label_credit_note_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames(
'form-group--vendor_credit_date',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="vendor_credit_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('vendor_credit_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Vendor Credit No # ----------- */}
<FastField name={'vendor_credit_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'credit_note.label_credit_note'} />}
inline={true}
className={('form-group--vendor_credit_number', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="vendor_credit_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleVendorCreditNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleVendorCreditNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T
id={'setting_your_auto_generated_vendor_credit_number'}
/>
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</FastField>
{/* ----------- Reference ----------- */}
<FastField name={'reference_no'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference_no'} />}
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(({ vendorsCreditNoteSetting }) => ({
vendorcreditAutoIncrement: vendorsCreditNoteSetting?.autoIncrement,
vendorcreditNextNumber: vendorsCreditNoteSetting?.nextNumber,
vendorcreditNumberPrefix: vendorsCreditNoteSetting?.numberPrefix,
})),
)(VendorCreditNoteFormHeaderFields);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import '../../../../style/pages/VendorsCreditNote/PageForm.scss';
import VendorCreditNoteForm from './VendorCreditNoteForm';
import { VendorCreditNoteFormProvider } from './VendorCreditNoteFormProvider';
/**
* Vendor Credit note form pages.
*/
export default function VendorCreditNoteFormPage() {
const { id } = useParams();
const idAsInteger = parseInt(id, 10);
return (
<VendorCreditNoteFormProvider vendorCreditId={idAsInteger}>
<VendorCreditNoteForm />
</VendorCreditNoteFormProvider>
);
}

View File

@@ -0,0 +1,80 @@
import React from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useCreateVendorCredit,
useEditVendorCredit,
useVendorCredit,
useItems,
useVendors,
useSettingsVendorCredits,
} from 'hooks/query';
const VendorCreditNoteFormContext = React.createContext();
/**
* Vendor Credit note data provider.
*/
function VendorCreditNoteFormProvider({ vendorCreditId, ...props }) {
// Handle fetching the items table based on the given query.
const {
data: { items },
isLoading: isItemsLoading,
} = useItems({
page_size: 10000,
});
// Handle fetching settings.
useSettingsVendorCredits();
// Handle fetch vendors data table or list
const {
data: { vendors },
isLoading: isVendorsLoading,
} = useVendors({ page_size: 10000 });
// Handle fetch vendor credit details.
const { data: vendorCredit, isLoading: isVendorCreditLoading } =
useVendorCredit(vendorCreditId, {
enabled: !!vendorCreditId,
});
// Form submit payload.
const [submitPayload, setSubmitPayload] = React.useState();
// Create and edit vendor credit mutations.
const { mutateAsync: createVendorCreditMutate } = useCreateVendorCredit();
const { mutateAsync: editVendorCreditMutate } = useEditVendorCredit();
// Determines whether the form in new mode.
const isNewMode = !vendorCreditId;
// Provider payload.
const provider = {
items,
vendors,
vendorCredit,
submitPayload,
isNewMode,
isVendorCreditLoading,
createVendorCreditMutate,
editVendorCreditMutate,
setSubmitPayload,
};
return (
<DashboardInsider
loading={isItemsLoading || isVendorsLoading || isVendorCreditLoading}
name={'vendor-credits-form'}
>
<VendorCreditNoteFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useVendorCreditNoteFormContext = () =>
React.useContext(VendorCreditNoteFormContext);
export { VendorCreditNoteFormProvider, useVendorCreditNoteFormContext };

View File

@@ -0,0 +1,38 @@
import React from 'react';
import classNames from 'classnames';
import { FastField } from 'formik';
import { CLASSES } from 'common/classes';
import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider';
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
import { entriesFieldShouldUpdate } from './utils';
import { ITEM_TYPE } from 'containers/Entries/utils';
export default function VendorCreditNoteItemsEntriesEditor() {
const { items } = useVendorCreditNoteFormContext();
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,129 @@
import React from 'react';
import * as R from 'ramda';
import moment from 'moment';
import {
defaultFastFieldShouldUpdate,
transformToForm,
repeatValue,
transactionNumber,
orderingLinesIndexes,
} from 'utils';
import {
updateItemsEntriesTotal,
ensureEntriesHaveEmptyLine,
} from 'containers/Entries/utils';
import { useFormikContext } from 'formik';
export const MIN_LINES_NUMBER = 4;
// Default Vendors Credit Note entry.
export const defaultCreditNoteEntry = {
index: 0,
item_id: '',
rate: '',
discount: '',
quantity: '',
description: '',
amount: '',
};
// Default Vendors Credit Note.
export const defaultVendorsCreditNote = {
vendor_id: '',
vendor_credit_number: '',
vendor_credit_no_manually: false,
vendor_credit_date: moment(new Date()).format('YYYY-MM-DD'),
// reference_no: '',
note: '',
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
};
/**
* Transformes the credit note to initial values of edit form.
*/
export const transformToEditForm = (creditNote) => {
const initialEntries = [
...creditNote.entries.map((entry) => ({
...transformToForm(entry, defaultCreditNoteEntry),
})),
...repeatValue(
defaultCreditNoteEntry,
Math.max(MIN_LINES_NUMBER - creditNote.entries.length, 0),
),
];
const entries = R.compose(
ensureEntriesHaveEmptyLine(defaultCreditNoteEntry),
updateItemsEntriesTotal,
)(initialEntries);
return {
...transformToForm(creditNote, defaultVendorsCreditNote),
entries,
};
};
/**
* Transformes credit note entries to submit request.
*/
export const transformEntriesToSubmit = (entries) => {
const transformCreditNoteEntry = R.compose(
R.omit(['amount']),
R.curry(transformToForm)(R.__, defaultCreditNoteEntry),
);
return R.compose(
orderingLinesIndexes,
R.map(transformCreditNoteEntry),
)(entries);
};
/**
* Filters the givne non-zero entries.
*/
export const filterNonZeroEntries = (entries) => {
return entries.filter((item) => item.item_id && item.quantity);
};
/**
* Transformes form values to request body.
*/
export const transformFormValuesToRequest = (values) => {
const entries = filterNonZeroEntries(values.entries);
return {
...values,
entries: transformEntriesToSubmit(entries),
};
};
/**
* Detarmines vendors fast field should update
*/
export const vendorsFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.vendors !== oldProps.vendors ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Detarmines entries fast field should update.
*/
export const entriesFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.items !== oldProps.items ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Syncs invoice no. settings with form.
*/
export const useObserveVendorCreditNoSettings = (prefix, nextNumber) => {
const { setFieldValue } = useFormikContext();
React.useEffect(() => {
const creditNo = transactionNumber(prefix, nextNumber);
setFieldValue('vendor_credit_number', creditNo);
}, [setFieldValue, prefix, nextNumber]);
};

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
Button,
Classes,
NavbarDivider,
NavbarGroup,
Intent,
Alignment,
} from '@blueprintjs/core';
import {
Icon,
FormattedMessage as T,
DashboardActionViewsList,
AdvancedFilterPopover,
DashboardFilterButton,
DashboardRowsHeightButton,
} from 'components';
import DashboardActionsBar from '../../../../components/Dashboard/DashboardActionsBar';
import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider';
import withVendorsCreditNotes from './withVendorsCreditNotes';
import withVendorsCreditNotesActions from './withVendorsCreditNotesActions';
import withSettings from '../../../Settings/withSettings';
import withSettingsActions from '../../../Settings/withSettingsActions';
import withVendorActions from './withVendorActions';
import { compose } from 'utils';
/**
* Vendors Credit note table actions bar.
*/
function VendorsCreditNoteActionsBar({
setVendorCreditsTableState,
// #withVendorsCreditNotes
vendorCreditFilterRoles,
// #withVendorsCreditNotesActions
setVendorsCreditNoteTableState,
// #withSettings
creditNoteTableSize,
// #withSettingsActions
addSetting,
}) {
const history = useHistory();
// vendor credit list context.
const { VendorCreditsViews, fields, refresh } =
useVendorsCreditNoteListContext();
// Handle click a new Vendor.
const handleClickNewVendorCredit = () => {
history.push('/vendor-credits/new');
};
// Handle view tab change.
const handleTabChange = (view) => {
setVendorCreditsTableState({ viewSlug: view ? view.slug : null });
};
// Handle click a refresh credit note.
const handleRefreshBtnClick = () => {
refresh();
};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('vendorCredit', 'tableSize', size);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
allMenuItem={true}
resourceName={'vendor_credit'}
views={VendorCreditsViews}
allMenuItem={true}
allMenuItemText={<T id={'all'} />}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'vendor_credits.action.new_vendor_credit'} />}
onClick={handleClickNewVendorCredit}
/>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: vendorCreditFilterRoles,
defaultFieldKey: 'created_at',
fields: fields,
onFilterChange: (filterConditions) => {
setVendorsCreditNoteTableState({ filterRoles: filterConditions });
},
}}
>
<DashboardFilterButton
conditionsCount={vendorCreditFilterRoles.length}
/>
</AdvancedFilterPopover>
<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={creditNoteTableSize}
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(
withVendorsCreditNotesActions,
withVendorActions,
withSettingsActions,
withVendorsCreditNotes(({ vendorsCreditNoteTableState }) => ({
vendorCreditFilterRoles: vendorsCreditNoteTableState.filterRoles,
})),
withSettings(({ vendorsCreditNoteSetting }) => ({
creditNoteTableSize: vendorsCreditNoteSetting?.tableSize,
})),
)(VendorsCreditNoteActionsBar);

View File

@@ -0,0 +1,135 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import VendorsCreditNoteEmptyStatus from './VendorsCreditNoteEmptyStatus';
import { DataTable, DashboardContentTable } from 'components';
import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withVendorsCreditNotesActions from './withVendorsCreditNotesActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import withSettings from '../../../Settings/withSettings';
import { useVendorsCreditNoteTableColumns, ActionsMenu } from './components';
import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider';
import { compose } from 'utils';
/**
* Vendors Credit note data table.
*/
function VendorsCreditNoteDataTable({
// #withVendorsCreditNotesActions
setVendorsCreditNoteTableState,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withSettings
creditNoteTableSize,
}) {
const history = useHistory();
// Vendor credits context.
const {
vendorCredits,
pagination,
isEmptyStatus,
isVendorCreditsFetching,
isVendorCreditsLoading,
} = useVendorsCreditNoteListContext();
// Credit note table columns.
const columns = useVendorsCreditNoteTableColumns();
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.VENDOR_CREDITS);
// Handles fetch data once the table state change.
const handleDataTableFetchData = React.useCallback(
({ pageSize, pageIndex, sortBy }) => {
setVendorsCreditNoteTableState({
pageSize,
pageIndex,
sortBy,
});
},
[setVendorsCreditNoteTableState],
);
// Display create note empty status instead of the table.
if (isEmptyStatus) {
return <VendorsCreditNoteEmptyStatus />;
}
const handleViewDetailVendorCredit = ({ id }) => {
openDrawer('vendor-credit-detail-drawer', { vendorCreditId: id });
};
// Handle delete credit note.
const handleDeleteVendorCreditNote = ({ id }) => {
openAlert('vendor-credit-delete', { vendorCreditId: id });
};
// Handle edit credit note.
const hanldeEditVendorCreditNote = (vendorCredit) => {
history.push(`/vendor-credits/${vendorCredit.id}/edit`);
};
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer('vendor-credit-detail-drawer', {
vendorCreditId: cell.row.original.id,
});
};
return (
<DashboardContentTable>
<DataTable
columns={columns}
data={vendorCredits}
loading={isVendorCreditsLoading}
headerLoading={isVendorCreditsLoading}
progressBarLoading={isVendorCreditsFetching}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onCellClick={handleCellClick}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={creditNoteTableSize}
payload={{
onViewDetails: handleViewDetailVendorCredit,
onDelete: handleDeleteVendorCreditNote,
onEdit: hanldeEditVendorCreditNote,
}}
/>
</DashboardContentTable>
);
}
export default compose(
withDashboardActions,
withVendorsCreditNotesActions,
withAlertsActions,
withDrawerActions,
withSettings(({ vendorsCreditNoteSetting }) => ({
creditNoteTableSize: vendorsCreditNoteSetting?.tableSize,
})),
)(VendorsCreditNoteDataTable);

View File

@@ -0,0 +1,34 @@
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 VendorsCreditNoteEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={<T id={'the_organization_doesn_t_receive_money_yet'} />}
description={
<p>
<T id={'credit_note.empty_status_description'} />
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => history.push('/vendor-credits/new')}
>
<T id={'vendor_credits.action.new_vendor_credit'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,77 @@
import React from 'react';
import { isEmpty } from 'lodash';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useResourceViews,
useResourceMeta,
useVendorCredits,
useRefreshVendorCredits,
} from 'hooks/query';
import { getFieldsFromResourceMeta } from 'utils';
const VendorsCreditNoteListContext = React.createContext();
/**
* Vendors Credit note data provider.
*/
function VendorsCreditNoteListProvider({ query, tableStateChanged, ...props }) {
// Vendor Credits refresh action.
const { refresh } = useRefreshVendorCredits();
// Fetch accounts resource views and fields.
const { data: VendorCreditsViews, isLoading: isViewsLoading } =
useResourceViews('vendor_credits');
// Fetch the accounts resource fields.
const {
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('vendor_credits');
// Fetch vendor credits list.
const {
data: { vendorCredits, pagination, filterMeta },
isLoading: isVendorCreditsLoading,
isFetching: isVendorCreditsFetching,
} = useVendorCredits(query, { keepPreviousData: true });
// Detarmines the datatable empty status.
const isEmptyStatus =
isEmpty(vendorCredits) && !isVendorCreditsLoading && !tableStateChanged;
// Provider payload.
const provider = {
vendorCredits,
pagination,
VendorCreditsViews,
refresh,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isResourceLoading,
isResourceFetching,
isVendorCreditsFetching,
isVendorCreditsLoading,
isViewsLoading,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isResourceLoading}
name={'vendor-credits'}
>
<VendorsCreditNoteListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useVendorsCreditNoteListContext = () =>
React.useContext(VendorsCreditNoteListContext);
export { VendorsCreditNoteListProvider, useVendorsCreditNoteListContext };

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { DashboardViewsTabs } from 'components';
import withVendorsCreditNotes from './withVendorsCreditNotes';
import withVendorsCreditNotesActions from './withVendorsCreditNotesActions';
import { compose, transfromViewsToTabs } from 'utils';
import { useVendorsCreditNoteListContext } from './VendorsCreditNoteListProvider';
/**
* Vendors Credit note views tabs.
*/
function VendorsCreditNoteViewTabs({
// #withVendorsCreditNotes
vendorCreditCurrentView,
// #withVendorsCreditNotesActions
setVendorsCreditNoteTableState,
}) {
// vendor credit list context.
const { VendorCreditsViews } = useVendorsCreditNoteListContext();
// Handle tab change.
const handleTabsChange = (viewSlug) => {
setVendorsCreditNoteTableState({ viewSlug: viewSlug || null });
};
const tabs = transfromViewsToTabs(VendorCreditsViews);
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
currentViewSlug={vendorCreditCurrentView}
resourceName={'vendor_credits'}
tabs={tabs}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withVendorsCreditNotesActions,
withVendorsCreditNotes(({ vendorsCreditNoteTableState }) => ({
vendorCreditCurrentView: vendorsCreditNoteTableState.viewSlug,
})),
)(VendorsCreditNoteViewTabs);

View File

@@ -0,0 +1,54 @@
import React from 'react';
import '../../../../style/pages/VendorsCreditNote/List.scss';
import { DashboardPageContent } from 'components';
import VendorsCreditNoteActionsBar from './VendorsCreditNoteActionsBar';
import VendorsCreditNoteViewTabs from './VendorsCreditNoteViewTabs';
import VendorsCreditNoteDataTable from './VendorsCreditNoteDataTable';
import withVendorsCreditNotes from './withVendorsCreditNotes';
import withVendorsCreditNotesActions from './withVendorsCreditNotesActions';
import { VendorsCreditNoteListProvider } from './VendorsCreditNoteListProvider';
import { transformTableStateToQuery, compose } from 'utils';
function VendorsCreditNotesList({
// #withVendorsCreditNotes
vendorsCreditNoteTableState,
vendorsCreditNoteTableStateChanged,
// #withVendorsCreditNotesActions
resetVendorsCreditNoteTableState,
}) {
// Resets the credit note table state once the page unmount.
React.useEffect(
() => () => {
resetVendorsCreditNoteTableState();
},
[resetVendorsCreditNoteTableState],
);
return (
<VendorsCreditNoteListProvider
query={transformTableStateToQuery(vendorsCreditNoteTableState)}
tableStateChanged={vendorsCreditNoteTableStateChanged}
>
<VendorsCreditNoteActionsBar />
<DashboardPageContent>
<VendorsCreditNoteViewTabs />
<VendorsCreditNoteDataTable />
</DashboardPageContent>
</VendorsCreditNoteListProvider>
);
}
export default compose(
withVendorsCreditNotesActions,
withVendorsCreditNotes(
({ vendorsCreditNoteTableState, vendorsCreditNoteTableStateChanged }) => ({
vendorsCreditNoteTableState,
vendorsCreditNoteTableStateChanged,
}),
),
)(VendorsCreditNotesList);

View File

@@ -0,0 +1,118 @@
import React from 'react';
import {
Intent,
Tag,
Menu,
MenuItem,
MenuDivider,
ProgressBar,
} from '@blueprintjs/core';
import intl from 'react-intl-universal';
import clsx from 'classnames';
import { CLASSES } from '../../../../common/classes';
import {
FormatDateCell,
FormattedMessage as T,
AppToaster,
Choose,
If,
Icon,
} from 'components';
import { formattedAmount, safeCallback, calculateStatus } from 'utils';
/**
* Actions menu.
*/
export function ActionsMenu({
payload: { onEdit, onDelete, onViewDetails },
row: { original },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('vendor_credits.action.edit_vendor_credit')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
text={intl.get('vendor_credits.action.delete_vendor_credit')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Retrieve vendors credit note table columns.
*/
export function useVendorsCreditNoteTableColumns() {
return React.useMemo(
() => [
{
id: 'vendor_credit_date',
Header: intl.get('date'),
accessor: 'formatted_vendor_credit_date',
Cell: FormatDateCell,
width: 110,
className: 'vendor_credit_date',
clickable: true,
textOverview: true,
},
{
id: 'vendor',
Header: intl.get('vendor_name'),
accessor: 'vendor.display_name',
width: 180,
className: 'vendor_id',
clickable: true,
textOverview: true,
},
{
id: 'vendor_credit_number',
Header: intl.get('vendor_credits.column.vendor_credit_no'),
accessor: 'vendor_credit_number',
width: 100,
className: 'vendor_credit_number',
clickable: true,
textOverview: true,
},
{
id: 'amount',
Header: intl.get('amount'),
accessor: 'formatted_amount',
width: 120,
align: 'right',
clickable: true,
textOverview: true,
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'status',
Header: intl.get('status'),
// accessor:
width: 120, // 160
className: 'status',
clickable: true,
},
{
id: 'reference_no',
Header: intl.get('reference_no'),
accessor: 'reference_no',
width: 90,
className: 'reference_no',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import {
setVendorCreditTableState,
resetVendorCreditTableState,
} from '../../../../store/VendorCredit/vendorCredit.actions';
const mapDipatchToProps = (dispatch) => ({
setVendorCreditsTableState: (queries) =>
dispatch(setVendorCreditTableState(queries)),
resetVendorCreditsTableState: () => dispatch(resetVendorCreditTableState()),
});
export default connect(null, mapDipatchToProps);

View File

@@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import {
getVendorCreditTableStateFactory,
isVendorCreditTableStateChangedFactory,
} from '../../../../store/VendorCredit/vendorCredit.selector';
export default (mapState) => {
const getVendorsCreditNoteTableState = getVendorCreditTableStateFactory();
const isVendorsCreditNoteTableChanged =
isVendorCreditTableStateChangedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
vendorsCreditNoteTableState: getVendorsCreditNoteTableState(state, props),
vendorsCreditNoteTableStateChanged: isVendorsCreditNoteTableChanged(
state,
props,
),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import {
setVendorCreditTableState,
resetVendorCreditTableState,
} from '../../../../store/VendorCredit/vendorCredit.actions';
const mapDispatchToProps = (dispatch) => ({
setVendorsCreditNoteTableState: (queries) =>
dispatch(setVendorCreditTableState(queries)),
resetVendorsCreditNoteTableState: () =>
dispatch(resetVendorCreditTableState()),
});
export default connect(null, mapDispatchToProps);

View File

@@ -0,0 +1,15 @@
import React from 'react';
const VendorCreditDeleteAlert = React.lazy(() =>
import('../../Alerts/VendorCeditNotes/VendorCreditDeleteAlert'),
);
/**
* Vendor Credit notes alerts.
*/
export default [
{
name: 'vendor-credit-delete',
component: VendorCreditDeleteAlert,
},
];

View File

@@ -0,0 +1,159 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { If, Icon, FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
/**
* Credit note floating actions.
*/
export default function CreditNoteFloatingActions() {
const history = useHistory();
// Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext();
// Credit note form context.
const { setSubmitPayload, isNewMode } = useCreditNoteFormContext();
// Handle submit, save and anothe new button click.
const handleSubmitAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: true, resetForm: true });
submitForm();
};
// Handle submit as save & continue editing button click.
const handleSubmitSaveContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: true });
submitForm();
};
// Handle submit as draft button click.
const handleSubmitDraftBtnClick = (event) => {
setSubmitPayload({ redirect: true, status: false });
submitForm();
};
// handle submit as draft & new button click.
const handleSubmitDraftAndNewBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: false, resetForm: true });
submitForm();
};
// Handle submit as draft & continue editing button click.
const handleSubmitDraftContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, status: false });
submitForm();
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
history.goBack();
};
// Handle submit button click.
const handleSubmitBtnClick = (event) => {
setSubmitPayload({ redirect: true });
submitForm();
};
const handleClearBtnClick = (event) => {
resetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY}
onClick={handleSubmitBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitSaveContinueEditingBtnClick}
/>
</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>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={isNewMode ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -0,0 +1,170 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { sumBy, omit, isEmpty } from 'lodash';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
CreateCreditNoteFormSchema,
EditCreditNoteFormSchema,
} from './CreditNoteForm.schema';
import CreditNoteFormHeader from './CreditNoteFormHeader';
import CreditNoteItemsEntriesEditorField from './CreditNoteItemsEntriesEditorField';
import CreditNoteFormFooter from './CreditNoteFormFooter';
import CreditNoteFloatingActions from './CreditNoteFloatingActions';
import CreditNoteFormDialogs from './CreditNoteFormDialogs';
import { AppToaster } from 'components';
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
import {
filterNonZeroEntries,
transformToEditForm,
transformFormValuesToRequest,
defaultCreditNote,
} from './utils';
import {
compose,
orderingLinesIndexes,
transactionNumber,
safeSumBy,
} from 'utils';
import withSettings from 'containers/Settings/withSettings';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
/**
* Credit note form.
*/
function CreditNoteForm({
// #withSettings
creditAutoIncrement,
creditNumberPrefix,
creditNextNumber,
// #withCurrentOrganization
organization: { base_currency },
}) {
const history = useHistory();
// Credit note form context.
const {
isNewMode,
submitPayload,
creditNote,
createCreditNoteMutate,
editCreditNoteMutate,
} = useCreditNoteFormContext();
// Credit number.
const creditNumber = transactionNumber(creditNumberPrefix, creditNextNumber);
// Initial values.
const initialValues = React.useMemo(
() => ({
...(!isEmpty(creditNote)
? { ...transformToEditForm(creditNote), currency_code: base_currency }
: {
...defaultCreditNote,
...(creditAutoIncrement && {
credit_note_number: creditNumber,
}),
entries: orderingLinesIndexes(defaultCreditNote.entries),
}),
}),
[],
);
// Handles form submit.
const handleFormSubmit = (
values,
{ setSubmitting, setErrors, resetForm },
) => {
const entries = filterNonZeroEntries(values.entries);
const totalQuantity = safeSumBy(entries, '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),
};
// Handle the request success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'credit_note.success_message'
: 'credit_note.edit_success_message',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
if (submitPayload.redirect) {
history.push('/credit-notes');
}
if (submitPayload.resetForm) {
resetForm();
}
};
// Handle the request error.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
if (isNewMode) {
createCreditNoteMutate(form).then(onSuccess).catch(onError);
} else {
editCreditNoteMutate([creditNote.id, form])
.then(onSuccess)
.catch(onError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_STRIP_STYLE,
CLASSES.PAGE_FORM_CREDIT_NOTE,
)}
>
<Formik
validationSchema={
isNewMode ? CreateCreditNoteFormSchema : EditCreditNoteFormSchema
}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<CreditNoteFormHeader />
<CreditNoteItemsEntriesEditorField />
<CreditNoteFormFooter />
<CreditNoteFloatingActions />
<CreditNoteFormDialogs />
</Form>
</Formik>
</div>
);
}
export default compose(
withSettings(({ creditNoteSettings }) => ({
creditAutoIncrement: creditNoteSettings?.autoIncrement,
creditNextNumber: creditNoteSettings?.nextNumber,
creditNumberPrefix: creditNoteSettings?.numberPrefix,
})),
withCurrentOrganization(),
)(CreditNoteForm);

View File

@@ -0,0 +1,47 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
import { isBlank } from 'utils';
const getSchema = () =>
Yup.object().shape({
customer_id: Yup.string().label(intl.get('customer_name_')).required(),
credit_note_date: Yup.date().required().label(intl.get('invoice_date_')),
credit_note_number: Yup.string()
.max(DATATYPES_LENGTH.STRING)
.label(intl.get('invoice_no_')),
reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING),
note: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.TEXT)
.label(intl.get('note')),
terms_conditions: Yup.string()
.trim()
.min(1)
.max(DATATYPES_LENGTH.TEXT)
.label(intl.get('note')),
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(100),
description: Yup.string().nullable().max(DATATYPES_LENGTH.TEXT),
}),
),
});
export const CreateCreditNoteFormSchema = getSchema;
export const EditCreditNoteFormSchema = getSchema;

View File

@@ -0,0 +1,24 @@
import React from 'react';
import CreditNoteNumberDialog from '../../../Dialogs/CreditNoteNumberDialog';
import { useFormikContext } from 'formik';
/**
* Credit note form dialogs.
*/
export default function CreditNoteFormDialogs() {
// Update the form once the credit number form submit confirm.
const handleCreditNumberFormConfirm = ({ incrementNumber, manually }) => {
setFieldValue('credit_note_number', incrementNumber || '');
setFieldValue('credit_note_no_manually', manually);
};
const { setFieldValue } = useFormikContext();
return (
<React.Fragment>
<CreditNoteNumberDialog
dialogName={'credit-number-form'}
onConfirm={handleCreditNumberFormConfirm}
/>
</React.Fragment>
);
}

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { FastField } from 'formik';
import { FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import { Row, Col, Postbox } from 'components';
import { inputIntent } from 'utils';
import classNames from 'classnames';
/**
* Credit note form footer.
*/
export default function CreditNoteFormFooter() {
return (
<div className={classNames(CLASSES.PAGE_FORM_FOOTER)}>
<Postbox
title={<T id={'credit_note.label_credit_note_details'} />}
defaultOpen={false}
>
<Row>
<Col md={8}>
{/* --------- Customer notes --------- */}
<FastField name={'note'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'credit_note.label_customer_note'} />}
className={'form-group--note'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
{/* --------- Terms and conditions --------- */}
<FastField name={'terms_conditions'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'terms_conditions'} />}
className={'form-group--terms_conditions'}
intent={inputIntent({ error, touched })}
>
<TextArea growVertically={true} {...field} />
</FormGroup>
)}
</FastField>
</Col>
</Row>
</Postbox>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { useFormikContext } from 'formik';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import CreditNoteFormHeaderFields from './CreditNoteFormHeaderFields';
import { getEntriesTotal } from 'containers/Entries/utils';
import { PageFormBigNumber } from 'components';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose } from 'utils';
/**
* Credit note header.
*/
function CreditNoteFormHeader({
// #withCurrentOrganization
organization: { base_currency },
}) {
const { values } = useFormikContext();
// Calculate the total amount.
const totalAmount = React.useMemo(
() => getEntriesTotal(values.entries),
[values.entries],
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
<CreditNoteFormHeaderFields />
<PageFormBigNumber
label={intl.get('due_amount')}
amount={totalAmount}
currencyCode={base_currency}
/>
</div>
);
}
export default compose(withCurrentOrganization())(CreditNoteFormHeader);

View File

@@ -0,0 +1,193 @@
import React from 'react';
import {
FormGroup,
InputGroup,
Position,
ControlGroup,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { FastField, Field, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import {
ContactSelecetList,
FieldRequiredHint,
InputPrependButton,
Icon,
FormattedMessage as T,
} from 'components';
import {
customerNameFieldShouldUpdate,
useObserveCreditNoSettings,
} from './utils';
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions';
import {
momentFormatter,
compose,
tansformDateValue,
inputIntent,
handleDateChange,
} from 'utils';
/**
* Credit note form header fields.
*/
function CreditNoteFormHeaderFields({
// #withDialogActions
openDialog,
// #withSettings
creditAutoIncrement,
creditNumberPrefix,
creditNextNumber,
}) {
// Credit note form context.
const { customers } = useCreditNoteFormContext();
// Handle credit number changing.
const handleCreditNumberChange = () => {
openDialog('credit-number-form');
};
// Handle credit no. field blur.
const handleCreditNoBlur = (form, field) => (event) => {
const newValue = event.target.value;
if (field.value !== newValue && creditAutoIncrement) {
openDialog('credit-number-form', {
initialFormValues: {
manualTransactionNo: newValue,
incrementMode: 'manual-transaction',
},
});
}
};
// Syncs credit number settings with form.
useObserveCreditNoSettings(creditNumberPrefix, creditNextNumber);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_FIELDS)}>
{/* ----------- Customer name ----------- */}
<FastField
name={'customer_id'}
customers={customers}
shouldUpdate={customerNameFieldShouldUpdate}
>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'customer_name'} />}
inline={true}
className={classNames(
'form-group--customer-name',
'form-group--select-list',
CLASSES.FILL,
)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_id'} />}
>
<ContactSelecetList
contactsList={customers}
selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Credit note date ----------- */}
<FastField name={'credit_note_date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'credit_note.label_credit_note_date'} />}
inline={true}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--credit_note_date', CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="credit_note_date" />}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('credit_note_date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM_LEFT, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Credit note # ----------- */}
<Field name={'credit_note_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'credit_note.label_credit_note'} />}
labelInfo={<FieldRequiredHint />}
inline={true}
className={classNames(
'form-group--credit_note_number',
CLASSES.FILL,
)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="credit_note_number" />}
>
<ControlGroup fill={true}>
<InputGroup
minimal={true}
value={field.value}
asyncControl={true}
onBlur={handleCreditNoBlur(form, field)}
/>
<InputPrependButton
buttonProps={{
onClick: handleCreditNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: (
<T id={'setting_your_auto_generated_credit_note_number'} />
),
position: Position.BOTTOM_LEFT,
}}
/>
</ControlGroup>
</FormGroup>
)}
</Field>
{/* ----------- Reference ----------- */}
<FastField name={'reference_no'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'reference_no'} />}
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(({ creditNoteSettings }) => ({
creditAutoIncrement: creditNoteSettings?.autoIncrement,
creditNextNumber: creditNoteSettings?.nextNumber,
creditNumberPrefix: creditNoteSettings?.numberPrefix,
})),
)(CreditNoteFormHeaderFields);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { useParams } from 'react-router-dom';
import '../../../../style/pages/CreditNote/PageForm.scss';
import CreditNoteForm from './CreditNoteForm';
import { CreditNoteFormProvider } from './CreditNoteFormProvider';
/**
* Credit note form page.
*/
export default function CreditNoteFormPage() {
const { id } = useParams();
const idAsInteger = parseInt(id, 10);
return (
<CreditNoteFormProvider creditNoteId={idAsInteger}>
<CreditNoteForm />
</CreditNoteFormProvider>
);
}

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { isEmpty, pick } from 'lodash';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { transformToEditForm } from './utils';
import {
useCreditNote,
useCreateCreditNote,
useEditCreditNote,
useItems,
useCustomers,
useSettingsCreditNotes,
} from 'hooks/query';
const CreditNoteFormContext = React.createContext();
/**
* Credit note data provider.
*/
function CreditNoteFormProvider({ creditNoteId, ...props }) {
// Handle fetch customers data table or list
const {
data: { customers },
isLoading: isCustomersLoading,
} = useCustomers({ page_size: 10000 });
// Handle fetching the items table based on the given query.
const {
data: { items },
isLoading: isItemsLoading,
} = useItems({
page_size: 10000,
});
// Handle fetch vendor credit details.
const { data: creditNote, isLoading: isCreditNoteLoading } = useCreditNote(
creditNoteId,
{
enabled: !!creditNoteId,
},
);
// Handle fetching settings.
useSettingsCreditNotes();
// Create and edit credit note mutations.
const { mutateAsync: createCreditNoteMutate } = useCreateCreditNote();
const { mutateAsync: editCreditNoteMutate } = useEditCreditNote();
// Form submit payload.
const [submitPayload, setSubmitPayload] = React.useState();
// Determines whether the form in new mode.
const isNewMode = !creditNoteId;
// Provider payload.
const provider = {
items,
customers,
creditNote,
submitPayload,
isNewMode,
isItemsLoading,
isCustomersLoading,
createCreditNoteMutate,
editCreditNoteMutate,
setSubmitPayload,
};
return (
<DashboardInsider
loading={isItemsLoading || isCustomersLoading || isCreditNoteLoading}
name={'credit-notes-form'}
>
<CreditNoteFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useCreditNoteFormContext = () => React.useContext(CreditNoteFormContext);
export { CreditNoteFormProvider, useCreditNoteFormContext };

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { FastField } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ItemsEntriesTable from 'containers/Entries/ItemsEntriesTable';
import { useCreditNoteFormContext } from './CreditNoteFormProvider';
import { entriesFieldShouldUpdate } from './utils';
/**
* Credit note items entries editor field.
*/
export default function CreditNoteItemsEntriesEditorField() {
const { items } = useCreditNoteFormContext();
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,131 @@
import React from 'react';
import * as R from 'ramda';
import moment from 'moment';
import {
defaultFastFieldShouldUpdate,
transformToForm,
repeatValue,
transactionNumber,
orderingLinesIndexes,
} from 'utils';
import { useFormikContext } from 'formik';
import {
updateItemsEntriesTotal,
ensureEntriesHaveEmptyLine,
} from 'containers/Entries/utils';
export const MIN_LINES_NUMBER = 4;
// Default entry object.
export const defaultCreditNoteEntry = {
index: 0,
item_id: '',
rate: '',
discount: '',
quantity: '',
description: '',
amount: '',
};
// Default credit note object.
export const defaultCreditNote = {
customer_id: '',
credit_note_date: moment(new Date()).format('YYYY-MM-DD'),
credit_note_number: '',
credit_note_no_manually: false,
// reference_no: '',
note: '',
terms_conditions: '',
entries: [...repeatValue(defaultCreditNoteEntry, MIN_LINES_NUMBER)],
};
/**
* Transform credit note to initial values in edit mode.
*/
export function transformToEditForm(creditNote) {
const initialEntries = [
...creditNote.entries.map((creditNote) => ({
...transformToForm(creditNote, defaultCreditNoteEntry),
})),
...repeatValue(
defaultCreditNoteEntry,
Math.max(MIN_LINES_NUMBER - creditNote.entries.length, 0),
),
];
const entries = R.compose(
ensureEntriesHaveEmptyLine(defaultCreditNoteEntry),
updateItemsEntriesTotal,
)(initialEntries);
return {
...transformToForm(creditNote, defaultCreditNote),
entries,
};
}
/**
* Transformes credit note entries to submit request.
*/
export const transformEntriesToSubmit = (entries) => {
const transformCreditNoteEntry = R.compose(
R.omit(['amount']),
R.curry(transformToForm)(R.__, defaultCreditNoteEntry),
);
return R.compose(
orderingLinesIndexes,
R.map(transformCreditNoteEntry),
)(entries);
};
/**
* Filters the givne non-zero entries.
*/
export const filterNonZeroEntries = (entries) => {
return entries.filter((item) => item.item_id && item.quantity);
};
/**
* Transformes form values to request body.
*/
export const transformFormValuesToRequest = (values) => {
const entries = filterNonZeroEntries(values.entries);
return {
...values,
entries: transformEntriesToSubmit(entries),
};
};
/**
* Determines customer name field when should update.
*/
export const customerNameFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.customers !== oldProps.customers ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Determines invoice entries field when should update.
*/
export const entriesFieldShouldUpdate = (newProps, oldProps) => {
return (
newProps.items !== oldProps.items ||
defaultFastFieldShouldUpdate(newProps, oldProps)
);
};
/**
* Syncs invoice no. settings with form.
*/
export const useObserveCreditNoSettings = (prefix, nextNumber) => {
const { setFieldValue } = useFormikContext();
React.useEffect(() => {
const creditNo = transactionNumber(prefix, nextNumber);
setFieldValue('credit_note_number', creditNo);
}, [setFieldValue, prefix, nextNumber]);
};

View File

@@ -0,0 +1,15 @@
import React from 'react';
const CreditNoteDeleteAlert = React.lazy(() =>
import('../../Alerts/CreditNotes/CreditNoteDeleteAlert'),
);
/**
* Credit notes alerts.
*/
export default [
{
name: 'credit-note-delete',
component: CreditNoteDeleteAlert,
},
];

View File

@@ -0,0 +1,144 @@
import React from 'react';
import {
Button,
Classes,
NavbarDivider,
NavbarGroup,
Intent,
Alignment,
} from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import {
Icon,
FormattedMessage as T,
DashboardActionViewsList,
AdvancedFilterPopover,
DashboardFilterButton,
DashboardRowsHeightButton,
} from 'components';
import DashboardActionsBar from '../../../../components/Dashboard/DashboardActionsBar';
import { useCreditNoteListContext } from './CreditNotesListProvider';
import withCreditNotes from './withCreditNotes';
import withCreditNotesActions from './withCreditNotesActions';
import withSettings from '../../../Settings/withSettings';
import withSettingsActions from '../../../Settings/withSettingsActions';
import { compose } from 'utils';
/**
* Credit note table actions bar.
*/
function CreditNotesActionsBar({
// #withCreditNotes
creditNoteFilterRoles,
// #withCreditNotesActions
setCreditNotesTableState,
// #withSettings
creditNoteTableSize,
// #withSettingsActions
addSetting,
}) {
const history = useHistory();
// credit note list context.
const { CreditNotesView, fields, refresh } = useCreditNoteListContext();
// Handle view tab change.
const handleTabChange = (view) => {
setCreditNotesTableState({ viewSlug: view ? view.slug : null });
};
// Handle click a new Credit.
const handleClickNewCreateNote = () => {
history.push('/credit-notes/new');
};
// Handle click a refresh credit note.
const handleRefreshBtnClick = () => {
refresh();
};
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('creditNote', 'tableSize', size);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<DashboardActionViewsList
allMenuItem={true}
resourceName={'credit_notes'}
views={CreditNotesView}
onChange={handleTabChange}
/>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus'} />}
text={<T id={'credit_note.label.new_credit_note'} />}
onClick={handleClickNewCreateNote}
/>
<AdvancedFilterPopover
advancedFilterProps={{
conditions: creditNoteFilterRoles,
defaultFieldKey: 'created_at',
fields: fields,
onFilterChange: (filterConditions) => {
setCreditNotesTableState({ filterRoles: filterConditions });
},
}}
>
<DashboardFilterButton
conditionsCount={creditNoteFilterRoles.length}
/>
</AdvancedFilterPopover>
<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={creditNoteTableSize}
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(
withCreditNotesActions,
withSettingsActions,
withCreditNotes(({ creditNoteTableState }) => ({
creditNoteFilterRoles: creditNoteTableState.filterRoles,
})),
withSettings(({ creditNoteSettings }) => ({
creditNoteTableSize: creditNoteSettings?.tableSize,
})),
)(CreditNotesActionsBar);

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import CreditNoteEmptyStatus from './CreditNotesEmptyStatus';
import { DataTable, DashboardContentTable } from 'components';
import { TABLES } from 'common/tables';
import { useMemorizedColumnsWidths } from 'hooks';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import TableSkeletonHeader from 'components/Datatable/TableHeaderSkeleton';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCreditNotesActions from './withCreditNotesActions';
import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import withSettings from '../../../Settings/withSettings';
import { useCreditNoteTableColumns, ActionsMenu } from './components';
import { useCreditNoteListContext } from './CreditNotesListProvider';
import { compose } from 'utils';
/**
* Credit note data table.
*/
function CreditNotesDataTable({
// #withCreditNotesActions
setCreditNotesTableState,
// #withAlertsActions
openAlert,
// #withDrawerActions
openDrawer,
// #withSettings
creditNoteTableSize,
}) {
const history = useHistory();
// Credit note list context.
const {
creditNotes,
pagination,
isEmptyStatus,
isCreditNotesFetching,
isCreditNotesLoading,
} = useCreditNoteListContext();
// Credit note table columns.
const columns = useCreditNoteTableColumns();
// Local storage memorizing columns widths.
const [initialColumnsWidths, , handleColumnResizing] =
useMemorizedColumnsWidths(TABLES.CREDIT_NOTES);
// Handles fetch data once the table state change.
const handleDataTableFetchData = React.useCallback(
({ pageSize, pageIndex, sortBy }) => {
setCreditNotesTableState({
pageSize,
pageIndex,
sortBy,
});
},
[setCreditNotesTableState],
);
// Display create note empty status instead of the table.
if (isEmptyStatus) {
return <CreditNoteEmptyStatus />;
}
const handleViewDetailCreditNote = ({ id }) => {
openDrawer('credit-note-detail-drawer', { creditNoteId: id });
};
// Handle delete credit note.
const handleDeleteCreditNote = ({ id }) => {
openAlert('credit-note-delete', { creditNoteId: id });
};
// Handle edit credit note.
const hanldeEditCreditNote = (creditNote) => {
history.push(`/credit-notes/${creditNote.id}/edit`);
};
// Handle cell click.
const handleCellClick = (cell, event) => {
openDrawer('credit-note-detail-drawer', {
creditNoteId: cell.row.original.id,
});
};
return (
<DashboardContentTable>
<DataTable
columns={columns}
data={creditNotes}
loading={isCreditNotesLoading}
headerLoading={isCreditNotesLoading}
progressBarLoading={isCreditNotesFetching}
onFetchData={handleDataTableFetchData}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
pagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionsMenu}
onCellClick={handleCellClick}
initialColumnsWidths={initialColumnsWidths}
onColumnResizing={handleColumnResizing}
size={creditNoteTableSize}
payload={{
onViewDetails: handleViewDetailCreditNote,
onDelete: handleDeleteCreditNote,
onEdit: hanldeEditCreditNote,
}}
/>
</DashboardContentTable>
);
}
export default compose(
withDashboardActions,
withCreditNotesActions,
withDrawerActions,
withAlertsActions,
withSettings(({ creditNoteSettings }) => ({
creditNoteTableSize: creditNoteSettings?.tableSize,
})),
)(CreditNotesDataTable);

View File

@@ -0,0 +1,34 @@
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 CreditNotesEmptyStatus() {
const history = useHistory();
return (
<EmptyStatus
title={<T id={'the_organization_doesn_t_receive_money_yet'} />}
description={
<p>
<T id={'credit_note.empty_status_description'} />
</p>
}
action={
<>
<Button
intent={Intent.PRIMARY}
large={true}
onClick={() => history.push('/credit-notes/new')}
>
<T id={'credit_note.label.new_credit_note'} />
</Button>
<Button intent={Intent.NONE} large={true}>
<T id={'learn_more'} />
</Button>
</>
}
/>
);
}

View File

@@ -0,0 +1,53 @@
import React from 'react';
import '../../../../style/pages/CreditNote/List.scss';
import { DashboardPageContent } from 'components';
import CreditNotesActionsBar from './CreditNotesActionsBar';
import CreditNotesViewTabs from './CreditNotesViewTabs';
import CreditNotesDataTable from './CreditNotesDataTable';
import withCreditNotes from './withCreditNotes';
import withCreditNotesActions from './withCreditNotesActions';
import { CreditNotesListProvider } from './CreditNotesListProvider';
import { transformTableStateToQuery, compose } from 'utils';
function CreditNotesList({
// #withCreditNotes
creditNoteTableState,
creditNoteTableStateChanged,
// #withCreditNotesActions
resetCreditNotesTableState,
}) {
// Resets the credit note table state once the page unmount.
React.useEffect(
() => () => {
resetCreditNotesTableState();
},
[resetCreditNotesTableState],
);
return (
<CreditNotesListProvider
query={transformTableStateToQuery(creditNoteTableState)}
tableStateChanged={creditNoteTableStateChanged}
>
<CreditNotesActionsBar />
<DashboardPageContent>
<CreditNotesViewTabs />
<CreditNotesDataTable />
</DashboardPageContent>
</CreditNotesListProvider>
);
}
export default compose(
withCreditNotesActions,
withCreditNotes(({ creditNoteTableState, creditNoteTableStateChanged }) => ({
creditNoteTableState,
creditNoteTableStateChanged,
})),
)(CreditNotesList);

View File

@@ -0,0 +1,75 @@
import React from 'react';
import { isEmpty } from 'lodash';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useResourceViews,
useResourceMeta,
useCreditNotes,
useRefreshCreditNotes,
} from 'hooks/query';
import { getFieldsFromResourceMeta } from 'utils';
const CreditNoteListContext = React.createContext();
/**
* Credit note data provider.
*/
function CreditNotesListProvider({ query, tableStateChanged, ...props }) {
// Credit notes refresh action.
const { refresh } = useRefreshCreditNotes();
// Fetch create notes resource views and fields.
const { data: CreditNotesView, isLoading: isViewsLoading } =
useResourceViews('credit_notes');
// Fetch the accounts resource fields.
const {
data: resourceMeta,
isLoading: isResourceLoading,
isFetching: isResourceFetching,
} = useResourceMeta('credit_notes');
// Fetch credit note list.
const {
data: { creditNotes, pagination, filterMeta },
isFetching: isCreditNotesFetching,
isLoading: isCreditNotesLoading,
} = useCreditNotes(query, { keepPreviousData: true });
// Detarmines the datatable empty status.S
const isEmptyStatus = isEmpty(creditNotes) && !isCreditNotesLoading && !tableStateChanged;
// Provider payload.
const provider = {
creditNotes,
pagination,
CreditNotesView,
refresh,
resourceMeta,
fields: getFieldsFromResourceMeta(resourceMeta.fields),
isResourceLoading,
isResourceFetching,
isCreditNotesFetching,
isCreditNotesLoading,
isViewsLoading,
isEmptyStatus,
};
return (
<DashboardInsider
loading={isViewsLoading || isResourceLoading}
name={'credit-notes-list'}
>
<CreditNoteListContext.Provider value={provider} {...props} />
</DashboardInsider>
);
}
const useCreditNoteListContext = () => React.useContext(CreditNoteListContext);
export { CreditNotesListProvider, useCreditNoteListContext };

View File

@@ -0,0 +1,51 @@
import React from 'react';
import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
import { DashboardViewsTabs } from 'components';
import withCreditNotes from './withCreditNotes';
import withCreditNotesActions from './withCreditNotesActions';
import { compose, transfromViewsToTabs } from 'utils';
import { useCreditNoteListContext } from './CreditNotesListProvider';
/**
* Credit Note views tabs.
*/
function CreditNotesViewTabs({
// #withCreditNotes
creditNoteCurrentView,
// #withCreditNotesActions
setCreditNotesTableState,
}) {
// Credit note list context.
const { CreditNotesView } = useCreditNoteListContext();
const tabs = transfromViewsToTabs(CreditNotesView);
// Handle tab change.
const handleTabsChange = (viewSlug) => {
setCreditNotesTableState({ viewSlug });
};
return (
<Navbar className={'navbar--dashboard-views'}>
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
currentViewSlug={creditNoteCurrentView}
resourceName={'credit_notes'}
tabs={tabs}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
export default compose(
withCreditNotesActions,
withCreditNotes(({ creditNoteTableState }) => ({
creditNoteCurrentView: creditNoteTableState.viewSlug,
})),
)(CreditNotesViewTabs);

View File

@@ -0,0 +1,115 @@
import React from 'react';
import {
Intent,
Tag,
Menu,
MenuItem,
MenuDivider,
ProgressBar,
} from '@blueprintjs/core';
import intl from 'react-intl-universal';
import clsx from 'classnames';
import { CLASSES } from '../../../../common/classes';
import {
FormatDateCell,
FormattedMessage as T,
AppToaster,
Choose,
If,
Icon,
} from 'components';
import { formattedAmount, safeCallback, calculateStatus } from 'utils';
export function ActionsMenu({
payload: { onEdit, onDelete, onViewDetails },
row: { original },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="reader-18" />}
text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)}
/>
<MenuDivider />
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('credit_note.action.edit_credit_note')}
onClick={safeCallback(onEdit, original)}
/>
<MenuItem
text={intl.get('credit_note.action.delete_credit_note')}
intent={Intent.DANGER}
onClick={safeCallback(onDelete, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Retrieve credit note table columns.
*/
export function useCreditNoteTableColumns() {
return React.useMemo(
() => [
{
id: 'credit_note_date',
Header: intl.get('credit_note.column.credit_date'),
accessor: 'formatted_credit_note_date',
Cell: FormatDateCell,
width: 110,
className: 'credit_note_date',
clickable: true,
textOverview: true,
},
{
id: 'customer',
Header: intl.get('customer_name'),
accessor: 'customer.display_name',
width: 180,
className: 'customer_id',
clickable: true,
textOverview: true,
},
{
id: 'credit_note_number',
Header: intl.get('credit_note.column.credit_note_no'),
accessor: 'credit_note_number',
width: 100,
className: 'credit_note_number',
clickable: true,
textOverview: true,
},
{
id: 'amount',
Header: intl.get('amount'),
accessor: 'formatted_amount',
width: 120,
align: 'right',
clickable: true,
textOverview: true,
className: clsx(CLASSES.FONT_BOLD),
},
{
id: 'status',
Header: intl.get('status'),
// accessor:
width: 120, // 160
className: 'status',
clickable: true,
},
{
id: 'reference_no',
Header: intl.get('reference_no'),
accessor: 'reference_no',
width: 90,
className: 'reference_no',
clickable: true,
textOverview: true,
},
],
[],
);
}

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
getCreditNotesTableStateFactory,
isCreditNotesTableStateChangedFactory,
} from '../../../../store/CreditNote/creditNote.selector';
export default (mapState) => {
const getCreditNoteTableState = getCreditNotesTableStateFactory();
const isCreditNoteTableChanged = isCreditNotesTableStateChangedFactory();
const mapStateToProps = (state, props) => {
const mapped = {
creditNoteTableState: getCreditNoteTableState(state, props),
creditNoteTableStateChanged: isCreditNoteTableChanged(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import {
setCreditNoteTableState,
resetCreditNoteTableState,
} from '../../../../store/CreditNote/creditNote.actions';
const mapDipatchToProps = (dispatch) => ({
setCreditNotesTableState: (queries) =>
dispatch(setCreditNoteTableState(queries)),
resetCreditNotesTableState: () => dispatch(resetCreditNoteTableState()),
});
export default connect(null, mapDipatchToProps);

View File

@@ -19,6 +19,8 @@ export default (mapState) => {
cashflowSettings: state.settings.data.cashflowAccounts,
cashflowTransactionsSettings: state.settings.data.cashflowTransactions,
cashflowSetting: state.settings.data.cashflow,
creditNoteSettings: state.settings.data.creditNote,
vendorsCreditNoteSetting: state.settings.data.vendorCredit,
};
return mapState ? mapState(mapped, state, props) : mapped;
};

View File

@@ -20,6 +20,10 @@ const commonInvalidateQueries = (queryClient) => {
queryClient.invalidateQueries(t.ACCOUNTS);
queryClient.invalidateQueries(t.ACCOUNT);
// Invalidate landed cost.
queryClient.invalidateQueries(t.LANDED_COST);
queryClient.invalidateQueries(t.LANDED_COST_TRANSACTION);
// Invalidate financial reports.
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
};

View File

@@ -0,0 +1,145 @@
import { useQueryClient, useMutation } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import { transformPagination } from 'utils';
import useApiRequest from '../useRequest';
import t from './types';
const commonInvalidateQueries = (queryClient) => {
// Invalidate credit note.
queryClient.invalidateQueries(t.CREDIT_NOTES);
queryClient.invalidateQueries(t.CREDIT_NOTE);
// Invalidate items.
queryClient.invalidateQueries(t.ITEMS);
queryClient.invalidateQueries(t.ITEM);
// Invalidate customers.
queryClient.invalidateQueries([t.CUSTOMER]);
queryClient.invalidateQueries(t.CUSTOMERS);
// Invalidate accounts.
queryClient.invalidateQueries(t.ACCOUNTS);
queryClient.invalidateQueries(t.ACCOUNT);
// Invalidate settings.
queryClient.invalidateQueries([t.SETTING, t.SETTING_CREDIT_NOTES]);
// Invalidate financial reports.
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
};
/**
* Create a new credit note.
*/
export function useCreateCreditNote(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(values) => apiRequest.post('sales/credit_notes', values),
{
onSuccess: (res, values) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Edit the given credit note.
*/
export function useEditCreditNote(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`sales/credit_notes/${id}`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
// Invalidate credit note query.
queryClient.invalidateQueries([t.CREDIT_NOTE, id]);
},
...props,
},
);
}
/**
* Delete the given credit note.
*/
export function useDeleteCreditNote(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation((id) => apiRequest.delete(`sales/credit_notes/${id}`), {
onSuccess: (res, id) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
// Invalidate vendor credit query.
queryClient.invalidateQueries([t.CREDIT_NOTE, id]);
},
...props,
});
}
const transformCreditNotes = (res) => ({
creditNotes: res.data.credit_notes,
pagination: transformPagination(res.data.pagination),
filterMeta: res.data.filter_meta,
});
/**
* Retrieve credit notes list with pagination meta.
*/
export function useCreditNotes(query, props) {
return useRequestQuery(
[t.CREDIT_NOTES, query],
{ method: 'get', url: 'sales/credit_notes', params: query },
{
select: transformCreditNotes,
defaultData: {
creditNotes: [],
pagination: {
page: 1,
pageSize: 20,
total: 0,
},
filterMeta: {},
},
...props,
},
);
}
/**
* Retrieve credit note detail of the given id.
* @param {number} id
*
*/
export function useCreditNote(id, props, requestProps) {
return useRequestQuery(
[t.CREDIT_NOTE, id],
{ method: 'get', url: `sales/credit_notes/${id}`, ...requestProps },
{
select: (res) => res.data.credit_note,
defaultData: {},
...props,
},
);
}
export function useRefreshCreditNotes() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries(t.CREDIT_NOTES);
},
};
}

View File

@@ -28,5 +28,7 @@ export * from './UniversalSearch/UniversalSearch';
export * from './GenericResource';
export * from './jobs';
export * from './misc';
export * from './cashflowAccounts'
export * from './roles'
export * from './cashflowAccounts';
export * from './roles';
export * from './creditNote';
export * from './vendorCredit';

View File

@@ -102,6 +102,7 @@ const transformInvoices = (res) => ({
* Retrieve sale invoices list with pagination meta.
*/
export function useInvoices(query, props) {
return useRequestQuery(
[t.SALE_INVOICES, query],
{ method: 'get', url: 'sales/invoices', params: query },

View File

@@ -123,6 +123,27 @@ export function useSettingCashFlow(props) {
);
}
/**
* Retrieve credit notes settings.
*/
export function useSettingsCreditNotes(props) {
return useSettingsQuery(
[t.SETTING, t.SETTING_CREDIT_NOTES],
{ group: 'credit_note' },
props,
);
}
/**
* Retrieve vendor credit settings.
*/
export function useSettingsVendorCredits(props) {
return useSettingsQuery(
[t.SETTING, t.SETTING_VENDOR_CREDITS],
{ group: 'vendor_credit' },
props,
);
}
/**
* Retrieve SMS Notifications settings.
*/

View File

@@ -108,6 +108,16 @@ const ROLES = {
ROLES_PERMISSIONS_SCHEMA: 'ROLES_PERMISSIONS_SCHEMA',
};
const CREDIT_NOTES = {
CREDIT_NOTE: 'CREDIT_NOTE',
CREDIT_NOTES: 'CREDIT_NOTES',
};
const VENDOR_CREDIT_NOTES = {
VENDOR_CREDITS: 'VENDOR_CREDITS',
VENDOR_CREDIT: 'VENDOR_CREDIT',
};
const SETTING = {
SETTING: 'SETTING',
SETTING_INVOICES: 'SETTING_INVOICES',
@@ -120,6 +130,8 @@ const SETTING = {
SETTING_SMS_NOTIFICATION: 'SETTING_SMS_NOTIFICATION',
SETTING_SMS_NOTIFICATIONS: 'SETTING_SMS_NOTIFICATIONS',
SETTING_EDIT_SMS_NOTIFICATION: 'SETTING_EDIT_SMS_NOTIFICATION',
SETTING_CREDIT_NOTES: 'SETTING_CREDIT_NOTES',
SETTING_VENDOR_CREDITS: 'SETTING_VENDOR_CREDITS',
};
const ORGANIZATIONS = {
@@ -184,4 +196,6 @@ export default {
...CONTACTS,
...CASH_FLOW_ACCOUNTS,
...ROLES,
...CREDIT_NOTES,
...VENDOR_CREDIT_NOTES,
};

View File

@@ -0,0 +1,152 @@
import { useQueryClient, useMutation } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import { transformPagination } from 'utils';
import useApiRequest from '../useRequest';
import t from './types';
const commonInvalidateQueries = (queryClient) => {
// Invalidate vendor credit.
queryClient.invalidateQueries(t.VENDOR_CREDITS);
queryClient.invalidateQueries(t.VENDOR_CREDIT);
// Invalidate items.
queryClient.invalidateQueries(t.ITEMS);
queryClient.invalidateQueries(t.ITEM);
// Invalidate vendors.
queryClient.invalidateQueries([t.VENDORS]);
queryClient.invalidateQueries(t.VENDOR);
// Invalidate accounts.
queryClient.invalidateQueries(t.ACCOUNTS);
queryClient.invalidateQueries(t.ACCOUNT);
// Invalidate settings.
queryClient.invalidateQueries([t.SETTING, t.SETTING_VENDOR_CREDITS]);
// Invalidate financial reports.
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
};
/**
* Create a new vendor credit.
*/
export function useCreateVendorCredit(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(values) => apiRequest.post('purchases/vendor-credit', values),
{
onSuccess: (res, values) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
/**
* Edit the given vendor credit.
*/
export function useEditVendorCredit(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
([id, values]) => apiRequest.post(`purchases/vendor-credit/${id}`, values),
{
onSuccess: (res, [id, values]) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
// Invalidate vendor credit query.
queryClient.invalidateQueries([t.VENDOR_CREDIT, id]);
},
...props,
},
);
}
/**
* Delete the given vendor credit.
*/
export function useDeleteVendorCredit(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(id) => apiRequest.delete(`purchases/vendor-credit/${id}`),
{
onSuccess: (res, id) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
// Invalidate vendor credit query.
queryClient.invalidateQueries([t.VENDOR_CREDIT_NOTE, id]);
},
...props,
},
);
}
const transformVendorCreditsResponse = (response) => ({
vendorCredits: response.data.vendor_credits,
pagination: transformPagination(response.data.pagination),
filterMeta: response.data.filter_meta,
});
/**
* Retrieve vendor credit notes list with pagination meta.
*/
export function useVendorCredits(query, props) {
return useRequestQuery(
[t.VENDOR_CREDITS, query],
{
method: 'get',
url: 'purchases/vendor-credit',
params: query,
},
{
select: transformVendorCreditsResponse,
defaultData: {
vendorCredits: [],
pagination: {
page: 1,
page_size: 12,
total: 0,
},
filterMeta: {},
},
...props,
},
);
}
/**
* Retrieve vendor credit detail of the given id.
* @param {number} id
*
*/
export function useVendorCredit(id, props, requestProps) {
return useRequestQuery(
[t.VENDOR_CREDIT, id],
{ method: 'get', url: `purchases/vendor-credit/${id}`, ...requestProps },
{
select: (res) => res.data.data,
defaultData: {},
...props,
},
);
}
export function useRefreshVendorCredits() {
const queryClient = useQueryClient();
return {
refresh: () => {
queryClient.invalidateQueries(t.VENDOR_CREDITS);
},
};
}

View File

@@ -1496,5 +1496,6 @@
"roles.permission_schema.once_delete_this_role_you_will_able_to_restore_it":"بمجرد حذف دور المستخدم ، لن تتمكن من استعادتها لاحقًا. هل أنت متأكد أنك تريد حذف هذا الدور؟",
"users.column.role_name":"دور المستخدم",
"roles.error.you_cannot_edit_predefined_roles":"لا يمكنك تعديل أدوار المستخدم المحددة مسبقا.",
"roles.error.you_cannot_delete_predefined_roles":"لا يمكنك حذف أدوار المستخدم المحددة مسبقا"
"roles.error.you_cannot_delete_predefined_roles":"لا يمكنك حذف أدوار المستخدم المحددة مسبقا",
"roles.error.you_cannot_delete_role_that_associated_to_users":"لا يمكن حذف دور المستخدم لأنه لديه مستخدمين مرتبطة به."
}

Some files were not shown because too many files have changed in this diff Show More