chrone: sperate client and server to different repos.

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

View File

@@ -0,0 +1,129 @@
import React, { useEffect, useCallback } from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { DataTableEditable } from 'components';
import { useEditableItemsEntriesColumns } from './components';
import {
saveInvoke,
compose,
updateTableCell,
updateMinEntriesLines,
updateAutoAddNewLine,
updateRemoveLineByIndex,
} from 'utils';
import { updateItemsEntriesTotal, useFetchItemRow } from './utils';
import { updateTableRow } from '../../utils';
/**
* Items entries table.
*/
function ItemsEntriesTable({
// #ownProps
items,
entries,
initialEntries,
defaultEntry,
errors,
onUpdateData,
linesNumber,
currencyCode,
itemType, // sellable or purchasable
landedCost = false,
}) {
const [rows, setRows] = React.useState(initialEntries);
// Allows to observes `entries` to make table rows outside controlled.
useEffect(() => {
if (entries && entries !== rows) {
setRows(entries);
}
}, [entries, rows]);
// Editiable items entries columns.
const columns = useEditableItemsEntriesColumns({ landedCost });
// Handle the fetch item row details.
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
landedCost,
itemType,
notifyNewRow: (newRow, rowIndex) => {
// Update the rate, description and quantity data of the row.
const newRows = compose(
updateItemsEntriesTotal,
updateTableRow(rowIndex, newRow),
)(rows);
setRows(newRows);
onUpdateData(newRows);
},
});
// Handles the editor data update.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
if (columnId === 'item_id') {
setItemRow({ rowIndex, columnId, itemId: value });
}
const newRows = compose(
updateAutoAddNewLine(defaultEntry, ['item_id']),
updateItemsEntriesTotal,
updateTableCell(rowIndex, columnId, value),
)(rows);
setRows(newRows);
onUpdateData(newRows);
},
[rows, defaultEntry, onUpdateData, setItemRow],
);
// Handle table rows removing by index.
const handleRemoveRow = (rowIndex) => {
const newRows = compose(
// Ensure minimum lines count.
updateMinEntriesLines(4, defaultEntry),
// Remove the line by the given index.
updateRemoveLineByIndex(rowIndex),
)(rows);
setRows(newRows);
saveInvoke(onUpdateData, newRows);
};
return (
<DataTableEditable
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={rows}
sticky={true}
progressBarLoading={isItemFetching}
cellsLoading={isItemFetching}
cellsLoadingCoords={cellsLoading}
footer={true}
payload={{
items,
errors: errors || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,
autoFocus: ['item_id', 0],
currencyCode,
}}
/>
);
}
ItemsEntriesTable.defaultProps = {
defaultEntry: {
index: 0,
item_id: '',
description: '',
quantity: '',
rate: '',
discount: '',
},
initialEntries: [],
linesNumber: 4,
};
export default ItemsEntriesTable;

View File

@@ -0,0 +1,201 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Tooltip, Button, Intent, Position } from '@blueprintjs/core';
import { Hint, Icon, FormattedMessage as T } from 'components';
import { formattedAmount, safeSumBy } from 'utils';
import {
InputGroupCell,
MoneyFieldCell,
ItemsListCell,
PercentFieldCell,
NumericInputCell,
CheckBoxFieldCell,
} from 'components/DataTableCells';
/**
* Item header cell.
*/
export function ItemHeaderCell() {
return (
<>
<T id={'product_and_service'} />
<Hint
content={<T id={'item_entries.products_services.hint'} />}
tooltipProps={{ boundary: 'window', position: Position.RIGHT }}
/>
</>
);
}
/**
* Item column footer cell.
*/
export function ItemFooterCell() {
return (
<span>
<T id={'total'} />
</span>
);
}
/**
* Actions cell renderer component.
*/
export function ActionsCellRenderer({
row: { index },
column: { id },
cell: { value },
data,
payload: { removeRow },
}) {
const onRemoveRole = () => {
removeRow(index);
};
return (
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon={'times-circle'} iconSize={14} />}
iconSize={14}
className="m12"
intent={Intent.DANGER}
onClick={onRemoveRole}
/>
</Tooltip>
);
}
/**
* Quantity total footer cell.
*/
export function QuantityTotalFooterCell({ rows }) {
const quantity = safeSumBy(rows, 'original.quantity');
return <span>{quantity}</span>;
}
/**
* Total footer cell.
*/
export function TotalFooterCell({ payload: { currencyCode }, rows }) {
const total = safeSumBy(rows, 'original.amount');
return <span>{formattedAmount(total, currencyCode)}</span>;
}
/**
* Total accessor.
*/
export function TotalCell({ payload: { currencyCode }, value }) {
return <span>{formattedAmount(value, currencyCode, { noZero: true })}</span>;
}
// Index table cell.
export function IndexTableCell({ row: { index } }) {
return <span>{index + 1}</span>;
}
/**
* Landed cost header cell.
*/
const LandedCostHeaderCell = () => {
return (
<>
<T id={'Landed'} />
<Hint content={<T id={'item_entries.landed.hint'} />} />
</>
);
};
/**
* Retrieve editable items entries columns.
*/
export function useEditableItemsEntriesColumns({ landedCost }) {
return React.useMemo(
() => [
{
Header: '#',
accessor: 'index',
Cell: IndexTableCell,
width: 40,
disableResizing: true,
disableSortBy: true,
className: 'index',
},
{
Header: ItemHeaderCell,
id: 'item_id',
accessor: 'item_id',
Cell: ItemsListCell,
Footer: ItemFooterCell,
disableSortBy: true,
width: 130,
className: 'item',
},
{
Header: intl.get('description'),
accessor: 'description',
Cell: InputGroupCell,
disableSortBy: true,
className: 'description',
width: 120,
},
{
Header: intl.get('quantity'),
accessor: 'quantity',
Cell: NumericInputCell,
Footer: QuantityTotalFooterCell,
disableSortBy: true,
width: 70,
className: 'quantity',
},
{
Header: intl.get('rate'),
accessor: 'rate',
Cell: MoneyFieldCell,
disableSortBy: true,
width: 70,
className: 'rate',
},
{
Header: intl.get('discount'),
accessor: 'discount',
Cell: PercentFieldCell,
disableSortBy: true,
width: 60,
className: 'discount',
},
{
Header: intl.get('total'),
Footer: TotalFooterCell,
accessor: 'amount',
Cell: TotalCell,
disableSortBy: true,
width: 100,
className: 'total',
},
...(landedCost
? [
{
Header: LandedCostHeaderCell,
accessor: 'landed_cost',
Cell: CheckBoxFieldCell,
width: 100,
disabledAccessor: 'landed_cost_disabled',
disableSortBy: true,
disableResizing: true,
className: 'landed-cost',
},
]
: []),
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
className: 'actions',
disableSortBy: true,
disableResizing: true,
width: 45,
},
],
[],
);
}

View File

@@ -0,0 +1,133 @@
import React from 'react';
import * as R from 'ramda';
import { sumBy, isEmpty, last } from 'lodash';
import { useItem } from 'hooks/query';
import { toSafeNumber, saveInvoke } from 'utils';
/**
* Retrieve item entry total from the given rate, quantity and discount.
* @param {number} rate
* @param {number} quantity
* @param {number} discount
* @return {number}
*/
export const calcItemEntryTotal = (discount, quantity, rate) => {
const _quantity = toSafeNumber(quantity);
const _rate = toSafeNumber(rate);
const _discount = toSafeNumber(discount);
return _quantity * _rate - (_quantity * _rate * _discount) / 100;
};
/**
* Updates the items entries total.
*/
export function updateItemsEntriesTotal(rows) {
return rows.map((row) => ({
...row,
amount: calcItemEntryTotal(row.discount, row.quantity, row.rate),
}));
}
export const ITEM_TYPE = {
SELLABLE: 'SELLABLE',
PURCHASABLE: 'PURCHASABLE',
};
/**
* Retrieve total of the given items entries.
*/
export function getEntriesTotal(entries) {
return sumBy(entries, 'amount');
}
/**
* Ensure the given entries have enough empty line on the last.
* @param {Object} defaultEntry - Default entry.
* @param {Array} entries - Entries.
* @return {Array}
*/
export const ensureEntriesHaveEmptyLine = R.curry((defaultEntry, entries) => {
const lastEntry = last(entries);
if (isEmpty(lastEntry.account_id) || isEmpty(lastEntry.amount)) {
return [...entries, defaultEntry];
}
return entries;
});
export const isLandedCostDisabled = (item) =>
['service', 'non-inventory'].indexOf(item.type) !== -1;
/**
* Handle fetch item row details and retrieves the new table row.
*/
export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
const [itemRow, setItemRow] = React.useState(null);
const [cellsLoading, setCellsLoading] = React.useState(null);
// Fetches the item details.
const {
data: item,
isFetching: isItemFetching,
isSuccess: isItemSuccess,
} = useItem(itemRow && itemRow.itemId, {
enabled: !!(itemRow && itemRow.itemId),
});
// Once the item start loading give the table cells loading state.
React.useEffect(() => {
if (itemRow && isItemFetching) {
setCellsLoading([
[itemRow.rowIndex, 'rate'],
[itemRow.rowIndex, 'description'],
[itemRow.rowIndex, 'quantity'],
[itemRow.rowIndex, 'discount'],
]);
} else {
setCellsLoading(null);
}
}, [isItemFetching, setCellsLoading, itemRow]);
// Once the item selected and fetched set the initial details to the table.
React.useEffect(() => {
if (isItemSuccess && item && itemRow) {
const { rowIndex } = itemRow;
const price =
itemType === ITEM_TYPE.PURCHASABLE ? item.cost_price : item.sell_price;
const description =
itemType === ITEM_TYPE.PURCHASABLE
? item.cost_description
: item.sell_description;
// Detarmines whether the landed cost checkbox should be disabled.
const landedCostDisabled = isLandedCostDisabled(item);
// The new row.
const newRow = {
rate: price,
description,
quantity: 1,
...(landedCost
? {
landed_cost: false,
landed_cost_disabled: landedCostDisabled,
}
: {}),
};
setItemRow(null);
saveInvoke(notifyNewRow, newRow, rowIndex);
}
}, [item, itemRow, itemType, isItemSuccess, landedCost, notifyNewRow]);
return {
isItemFetching,
isItemSuccess,
item,
setItemRow,
itemRow,
cellsLoading,
};
}