fix: BIG-148 items entries ordered by index.

This commit is contained in:
a.bouhuolia
2021-10-31 13:24:12 +02:00
parent cbce9f6d50
commit 9211e963c6
16 changed files with 200 additions and 94 deletions

View File

@@ -50,7 +50,6 @@ function InventoryAdjustmentFloatingActions({
</Button> </Button>
<Button <Button
disabled={isSubmitting}
loading={isSubmitting && !submitPayload.publish} loading={isSubmitting && !submitPayload.publish}
style={{ minWidth: '75px' }} style={{ minWidth: '75px' }}
type="submit" type="submit"
@@ -61,7 +60,6 @@ function InventoryAdjustmentFloatingActions({
<Button <Button
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
disabled={isSubmitting}
loading={isSubmitting && submitPayload.publish} loading={isSubmitting && submitPayload.publish}
style={{ minWidth: '75px' }} style={{ minWidth: '75px' }}
type="submit" type="submit"

View File

@@ -14,19 +14,12 @@ const PaymentMadeDetailContext = React.createContext();
*/ */
function PaymentMadeDetailProvider({ paymentMadeId, ...props }) { function PaymentMadeDetailProvider({ paymentMadeId, ...props }) {
// Handle fetch specific payment made details. // Handle fetch specific payment made details.
const { data: paymentMade, isLoading: isPaymentMadeLoading } = const { data: paymentMade, isLoading: isPaymentMadeLoading } = usePaymentMade(
usePaymentMade(paymentMadeId, { paymentMadeId,
{
enabled: !!paymentMadeId, enabled: !!paymentMadeId,
}); },
);
// Handle fetch specific payment made details.
const {
data: { entries: paymentEntries },
isLoading: isPaymentLoading,
} = usePaymentMadeEditPage(paymentMadeId, {
enabled: !!paymentMadeId,
});
// Handle fetch transaction by reference. // Handle fetch transaction by reference.
const { const {
data: { transactions }, data: { transactions },
@@ -44,11 +37,9 @@ function PaymentMadeDetailProvider({ paymentMadeId, ...props }) {
transactions, transactions,
paymentMadeId, paymentMadeId,
paymentMade, paymentMade,
paymentEntries,
}; };
const loading = const loading = isTransactionLoading || isPaymentMadeLoading;
isTransactionLoading || isPaymentMadeLoading || isPaymentLoading;
return ( return (
<DrawerLoading loading={loading}> <DrawerLoading loading={loading}>

View File

@@ -12,18 +12,17 @@ import PaymentDrawerCls from './PaymentMadeDrawer.module.scss';
* Payment made read-only details table. * Payment made read-only details table.
*/ */
export default function PaymentMadeDetailTable() { export default function PaymentMadeDetailTable() {
// Retrieve payment made entries columns. // Retrieve payment made entries columns.
const columns = usePaymentMadeEntriesColumns(); const columns = usePaymentMadeEntriesColumns();
// Payment made details context. // Payment made details context.
const { paymentEntries } = usePaymentMadeDetailContext(); const { paymentMade } = usePaymentMadeDetailContext();
return ( return (
<div className={clsx(PaymentDrawerCls.detail_panel_table)}> <div className={clsx(PaymentDrawerCls.detail_panel_table)}>
<DataTable <DataTable
columns={columns} columns={columns}
data={paymentEntries} data={paymentMade.entries}
className={'table-constrant'} className={'table-constrant'}
/> />
</div> </div>

View File

@@ -8,13 +8,14 @@ import { useEditableItemsEntriesColumns } from './components';
import { import {
saveInvoke, saveInvoke,
compose, compose,
updateTableCell,
updateMinEntriesLines, updateMinEntriesLines,
updateAutoAddNewLine,
updateRemoveLineByIndex, updateRemoveLineByIndex,
} from 'utils'; } from 'utils';
import { updateItemsEntriesTotal, useFetchItemRow } from './utils'; import {
import { updateTableRow } from '../../utils'; useFetchItemRow,
composeRowsOnNewRow,
composeRowsOnEditCell,
} from './utils';
/** /**
* Items entries table. * Items entries table.
@@ -50,10 +51,7 @@ function ItemsEntriesTable({
itemType, itemType,
notifyNewRow: (newRow, rowIndex) => { notifyNewRow: (newRow, rowIndex) => {
// Update the rate, description and quantity data of the row. // Update the rate, description and quantity data of the row.
const newRows = compose( const newRows = composeRowsOnNewRow(rowIndex, newRow, rows);
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
setRows(newRows); setRows(newRows);
onUpdateData(newRows); onUpdateData(newRows);
@@ -66,11 +64,8 @@ function ItemsEntriesTable({
if (columnId === 'item_id') { if (columnId === 'item_id') {
setItemRow({ rowIndex, columnId, itemId: value }); setItemRow({ rowIndex, columnId, itemId: value });
} }
const newRows = compose( const composeEditCell = composeRowsOnEditCell(rowIndex, columnId);
updateAutoAddNewLine(defaultEntry, ['item_id']), const newRows = composeEditCell(value, defaultEntry, rows);
updateItemsEntriesTotal,
updateTableCell(rowIndex, columnId, value),
)(rows);
setRows(newRows); setRows(newRows);
onUpdateData(newRows); onUpdateData(newRows);

View File

@@ -3,7 +3,15 @@ import * as R from 'ramda';
import { sumBy, isEmpty, last } from 'lodash'; import { sumBy, isEmpty, last } from 'lodash';
import { useItem } from 'hooks/query'; import { useItem } from 'hooks/query';
import { toSafeNumber, saveInvoke } from 'utils'; import {
toSafeNumber,
saveInvoke,
compose,
updateTableCell,
updateAutoAddNewLine,
orderingLinesIndexes,
updateTableRow,
} from 'utils';
/** /**
* Retrieve item entry total from the given rate, quantity and discount. * Retrieve item entry total from the given rate, quantity and discount.
@@ -131,3 +139,28 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
cellsLoading, cellsLoading,
}; };
} }
/**
* Compose table rows when edit specific row index of table rows.
*/
export const composeRowsOnEditCell = R.curry(
(rowIndex, columnId, value, defaultEntry, rows) => {
return compose(
orderingLinesIndexes,
updateAutoAddNewLine(defaultEntry, ['item_id']),
updateItemsEntriesTotal,
updateTableCell(rowIndex, columnId, value),
)(rows);
},
);
/**
* Compose table rows when insert a new row to table rows.
*/
export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
return compose(
orderingLinesIndexes,
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
});

View File

@@ -35,6 +35,7 @@ export function useGeneralLedgerTableColumns() {
return row.date; return row.date;
}, },
className: 'date', className: 'date',
textOverview: true,
width: 120, width: 120,
}, },
{ {

View File

@@ -14,7 +14,10 @@ import ItemFormBody from './ItemFormBody';
import ItemFormFloatingActions from './ItemFormFloatingActions'; import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection'; import ItemFormInventorySection from './ItemFormInventorySection';
import { useItemFormInitialValues } from './utils'; import {
transformSubmitRequestErrors,
useItemFormInitialValues,
} from './utils';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema'; import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
@@ -40,27 +43,6 @@ export default function ItemForm() {
// Initial values in create and edit mode. // Initial values in create and edit mode.
const initialValues = useItemFormInitialValues(item); const initialValues = useItemFormInitialValues(item);
// Transform API errors.
const transformApiErrors = (error) => {
const {
response: {
data: { errors },
},
} = error;
const fields = {};
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) {
fields.name = intl.get('the_name_used_before');
}
if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) {
AppToaster.show({
message: intl.get('cannot_change_item_inventory_account'),
intent: Intent.DANGER,
});
}
return fields;
};
// Handles the form submit. // Handles the form submit.
const handleFormSubmit = ( const handleFormSubmit = (
values, values,
@@ -94,7 +76,7 @@ export default function ItemForm() {
const onError = (errors) => { const onError = (errors) => {
setSubmitting(false); setSubmitting(false);
if (errors) { if (errors) {
const _errors = transformApiErrors(errors); const _errors = transformSubmitRequestErrors(errors);
setErrors({ ..._errors }); setErrors({ ..._errors });
} }
}; };

View File

@@ -1,6 +1,7 @@
import React, { useEffect, createContext, useState } from 'react'; import React, { useEffect, createContext, useState } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { import {
useItem, useItem,
@@ -11,6 +12,7 @@ import {
useAccounts, useAccounts,
} from 'hooks/query'; } from 'hooks/query';
import { useDashboardPageTitle } from 'hooks/state'; import { useDashboardPageTitle } from 'hooks/state';
import { useWatchItemError } from './utils';
const ItemFormContext = createContext(); const ItemFormContext = createContext();
@@ -32,12 +34,14 @@ function ItemFormProvider({ itemId, ...props }) {
} = useItemsCategories(); } = useItemsCategories();
// Fetches the given item details. // Fetches the given item details.
const { isLoading: isItemLoading, data: item } = useItem( const itemQuery = useItem(itemId || duplicateId, {
itemId || duplicateId, enabled: !!itemId || !!duplicateId,
{ });
enabled: !!itemId || !!duplicateId,
}, const { isLoading: isItemLoading, data: item } = itemQuery;
);
// Watches and handles item not found response error.
useWatchItemError(itemQuery);
// Fetches item settings. // Fetches item settings.
const { const {

View File

@@ -1,7 +1,8 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { defaultTo } from 'lodash'; import { defaultTo, includes } from 'lodash';
import { useHistory } from 'react-router-dom';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import { import {
transformTableStateToQuery, transformTableStateToQuery,
@@ -10,6 +11,7 @@ import {
import { transformToForm } from 'utils'; import { transformToForm } from 'utils';
import { useSettingsSelector } from '../../hooks/state'; import { useSettingsSelector } from '../../hooks/state';
import { transformItemFormData } from './ItemForm.schema'; import { transformItemFormData } from './ItemForm.schema';
import { useWatch } from 'hooks/utils';
const defaultInitialValues = { const defaultInitialValues = {
active: 1, active: 1,
@@ -182,3 +184,59 @@ export function transformItemsTableState(tableState) {
inactive_mode: tableState.inactiveMode, inactive_mode: tableState.inactiveMode,
}; };
} }
/**
* Transform API errors.
*/
export const transformSubmitRequestErrors = (error) => {
const {
response: {
data: { errors },
},
} = error;
const fields = {};
if (errors.find((e) => e.type === 'ITEM.NAME.ALREADY.EXISTS')) {
fields.name = intl.get('the_name_used_before');
}
if (errors.find((e) => e.type === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED')) {
AppToaster.show({
message: intl.get('cannot_change_item_inventory_account'),
intent: Intent.DANGER,
});
}
if (
errors.find(
(e) => e.type === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
)
) {
AppToaster.show({
message: intl.get(
'item.error.type_cannot_change_with_item_has_transactions',
),
intent: Intent.DANGER,
});
}
return fields;
};
/**
* Watches and handles item response not found error.
* @param {*} itemQuery
*/
export function useWatchItemError(itemQuery) {
const { error, isError } = itemQuery;
// History context.
const history = useHistory();
useWatch(() => {
if (isError && includes([400, 404], error.response.status)) {
AppToaster.show({
message: 'The given item not found.',
intent: Intent.DANGER,
});
history.push('/items');
}
}, isError);
}

View File

@@ -23,7 +23,12 @@ import {
} from './PaymentMadeForm.schema'; } from './PaymentMadeForm.schema';
import { compose, orderingLinesIndexes } from 'utils'; import { compose, orderingLinesIndexes } from 'utils';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
import { defaultPaymentMade, transformToEditForm, ERRORS } from './utils'; import {
defaultPaymentMade,
transformToEditForm,
ERRORS,
transformFormToRequest,
} from './utils';
/** /**
* Payment made form component. * Payment made form component.
@@ -71,15 +76,8 @@ function PaymentMadeForm({
{ setSubmitting, resetForm, setFieldError }, { setSubmitting, resetForm, setFieldError },
) => { ) => {
setSubmitting(true); setSubmitting(true);
// Filters entries that have no `bill_id` or `payment_amount`.
const entries = values.entries
.filter((item) => item.bill_id && item.payment_amount)
.map((entry) => ({
...pick(entry, ['payment_amount', 'bill_id']),
}));
// Total payment amount of entries. // Total payment amount of entries.
const totalPaymentAmount = sumBy(entries, 'payment_amount'); const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
if (totalPaymentAmount <= 0) { if (totalPaymentAmount <= 0) {
AppToaster.show({ AppToaster.show({
@@ -88,7 +86,8 @@ function PaymentMadeForm({
}); });
return; return;
} }
const form = { ...values, entries }; // Transformes the form values to request body.
const form = transformFormToRequest(values);
// Triggers once the save request success. // Triggers once the save request success.
const onSaved = (response) => { const onSaved = (response) => {

View File

@@ -1,8 +1,10 @@
import moment from 'moment'; import moment from 'moment';
import { pick } from 'lodash';
import { import {
defaultFastFieldShouldUpdate, defaultFastFieldShouldUpdate,
safeSumBy, safeSumBy,
transformToForm, transformToForm,
orderingLinesIndexes,
} from 'utils'; } from 'utils';
export const ERRORS = { export const ERRORS = {
@@ -75,3 +77,17 @@ export const accountsFieldShouldUpdate = (newProps, oldProps) => {
defaultFastFieldShouldUpdate(newProps, oldProps) defaultFastFieldShouldUpdate(newProps, oldProps)
); );
}; };
/**
* Transformes the form values to request body.
*/
export const transformFormToRequest = (form) => {
// Filters entries that have no `bill_id` or `payment_amount`.
const entries = form.entries
.filter((item) => item.bill_id && item.payment_amount)
.map((entry) => ({
...pick(entry, ['payment_amount', 'bill_id']),
}));
return { ...form, entries: orderingLinesIndexes(entries) };
};

View File

@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Formik, Form } from 'formik'; import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { sumBy, omit, isEmpty } from 'lodash'; import { sumBy, isEmpty } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
@@ -144,7 +144,10 @@ function InvoiceForm({
} }
}; };
// Create invoice form schema.
const CreateInvoiceFormSchema = getCreateInvoiceFormSchema(); const CreateInvoiceFormSchema = getCreateInvoiceFormSchema();
// Edit invoice form schema.
const EditInvoiceFormSchema = getEditInvoiceFormSchema(); const EditInvoiceFormSchema = getEditInvoiceFormSchema();
return ( return (

View File

@@ -73,6 +73,7 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
customers, customers,
newInvoice, newInvoice,
estimateId, estimateId,
invoiceId,
submitPayload, submitPayload,
isInvoiceLoading, isInvoiceLoading,

View File

@@ -28,7 +28,7 @@ import { AppToaster } from 'components';
import { transactionNumber, compose } from 'utils'; import { transactionNumber, compose } from 'utils';
import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider'; import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider';
import { defaultPaymentReceive, transformToEditForm } from './utils'; import { defaultPaymentReceive, transformToEditForm, transformFormToRequest } from './utils';
/** /**
* Payment Receive form. * Payment Receive form.
@@ -91,15 +91,8 @@ function PaymentReceiveForm({
) => { ) => {
setSubmitting(true); setSubmitting(true);
// Filters entries that have no `invoice_id` and `payment_amount`.
const entries = values.entries
.filter((entry) => entry.invoice_id && entry.payment_amount)
.map((entry) => ({
...pick(entry, ['invoice_id', 'payment_amount']),
}));
// Calculates the total payment amount of entries. // Calculates the total payment amount of entries.
const totalPaymentAmount = sumBy(entries, 'payment_amount'); const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
if (totalPaymentAmount <= 0) { if (totalPaymentAmount <= 0) {
AppToaster.show({ AppToaster.show({
@@ -108,13 +101,8 @@ function PaymentReceiveForm({
}); });
return; return;
} }
const form = { // Transformes the form values to request body.
...omit(values, ['payment_receive_no_manually', 'payment_receive_no']), const form = transformFormToRequest(values);
...(values.payment_receive_no_manually && {
payment_receive_no: values.payment_receive_no,
}),
entries,
};
// Handle request response success. // Handle request response success.
const onSaved = (response) => { const onSaved = (response) => {

View File

@@ -1,15 +1,18 @@
import React from 'react'; import React from 'react';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import moment from 'moment'; import moment from 'moment';
import { omit, pick } from 'lodash';
import { import {
defaultFastFieldShouldUpdate, defaultFastFieldShouldUpdate,
transactionNumber, transactionNumber,
transformToForm, transformToForm,
safeSumBy, safeSumBy,
orderingLinesIndexes
} from 'utils'; } from 'utils';
// Default payment receive entry. // Default payment receive entry.
export const defaultPaymentReceiveEntry = { export const defaultPaymentReceiveEntry = {
index: '',
payment_amount: '', payment_amount: '',
invoice_id: '', invoice_id: '',
invoice_no: '', invoice_no: '',
@@ -32,8 +35,23 @@ export const defaultPaymentReceive = {
entries: [], entries: [],
}; };
export const defaultRequestPaymentEntry = {
index: '',
payment_amount: '',
invoice_id: '',
};
export const defaultRequestPayment = {
customer_id: '',
deposit_account_id: '',
payment_date: '',
payment_receive_no: '',
statement: '',
entries: []
};
/** /**
* * Transformes the edit payment receive to initial values of the form.
*/ */
export const transformToEditForm = (paymentReceive, paymentReceiveEntries) => ({ export const transformToEditForm = (paymentReceive, paymentReceiveEntries) => ({
...transformToForm(paymentReceive, defaultPaymentReceive), ...transformToForm(paymentReceive, defaultPaymentReceive),
@@ -124,3 +142,23 @@ export const accountsFieldShouldUpdate = (newProps, oldProps) => {
defaultFastFieldShouldUpdate(newProps, oldProps) defaultFastFieldShouldUpdate(newProps, oldProps)
); );
}; };
/**
* Tranformes form values to request.
*/
export const transformFormToRequest = (form) => {
// Filters entries that have no `invoice_id` and `payment_amount`.
const entries = form.entries
.filter((entry) => entry.invoice_id && entry.payment_amount)
.map((entry) => ({
...pick(entry, Object.keys(defaultRequestPaymentEntry)),
}));
return {
...omit(form, ['payment_receive_no_manually', 'payment_receive_no']),
...(form.payment_receive_no_manually && {
payment_receive_no: form.payment_receive_no,
}),
entries: orderingLinesIndexes(entries),
};
};

View File

@@ -852,7 +852,7 @@ function keyIdentity(key) {
return key; return key;
} }
export function flatten(target, opts) { export function flatten(opts, target) {
opts = opts || {}; opts = opts || {};
const delimiter = opts.delimiter || '.'; const delimiter = opts.delimiter || '.';