re-structure to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 01:02:31 +02:00
parent 8242ec64ba
commit 7a0a13f9d5
10400 changed files with 46966 additions and 17223 deletions

View File

@@ -0,0 +1,125 @@
// @ts-nocheck
import React, { useEffect, useCallback } from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { DataTableEditable } from '@/components';
import { useEditableItemsEntriesColumns } from './components';
import {
saveInvoke,
compose,
updateMinEntriesLines,
updateRemoveLineByIndex,
} from '@/utils';
import {
useFetchItemRow,
composeRowsOnNewRow,
composeRowsOnEditCell,
} from './utils';
/**
* Items entries table.
*/
function ItemsEntriesTable({
// #ownProps
items,
entries,
initialEntries,
defaultEntry,
errors,
onUpdateData,
currencyCode,
itemType, // sellable or purchasable
landedCost = false,
minLinesNumber
}) {
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 = composeRowsOnNewRow(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 composeEditCell = composeRowsOnEditCell(rowIndex, columnId);
const newRows = composeEditCell(value, defaultEntry, 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(minLinesNumber, 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}
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: 1,
minLinesNumber: 1,
};
export default ItemsEntriesTable;

View File

@@ -0,0 +1,186 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { MenuItem, Menu, Button, Position } from '@blueprintjs/core';
import { Popover2 } from '@blueprintjs/popover2';
import { Align, CellType, Features } from '@/constants';
import { Hint, Icon, FormattedMessage as T } from '@/components';
import { formattedAmount } from '@/utils';
import {
InputGroupCell,
MoneyFieldCell,
ItemsListCell,
PercentFieldCell,
NumericInputCell,
CheckBoxFieldCell,
ProjectBillableEntriesCell,
} from '@/components/DataTableCells';
import { useFeatureCan } from '@/hooks/state';
/**
* 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 }}
/>
</>
);
}
/**
* Actions cell renderer component.
*/
export function ActionsCellRenderer({
row: { index },
payload: { removeRow },
}) {
const onRemoveRole = () => {
removeRow(index);
};
const exampleMenu = (
<Menu>
<MenuItem
onClick={onRemoveRole}
text={<T id={'item_entries.remove_row'} />}
/>
</Menu>
);
return (
<Popover2 content={exampleMenu} placement="left-start">
<Button
icon={<Icon icon={'more-13'} iconSize={13} />}
iconSize={14}
className="m12"
minimal={true}
/>
</Popover2>
);
}
ActionsCellRenderer.cellType = CellType.Button;
/**
* Total accessor.
*/
export function TotalCell({ payload: { currencyCode }, value }) {
return <span>{formattedAmount(value, currencyCode, { noZero: true })}</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 }) {
const { featureCan } = useFeatureCan();
const isProjectsFeatureEnabled = featureCan(Features.Projects);
return React.useMemo(
() => [
{
Header: ItemHeaderCell,
id: 'item_id',
accessor: 'item_id',
Cell: ItemsListCell,
disableSortBy: true,
width: 130,
className: 'item',
fieldProps: { allowCreate: true },
},
{
Header: intl.get('description'),
accessor: 'description',
Cell: InputGroupCell,
disableSortBy: true,
className: 'description',
width: 120,
},
{
Header: intl.get('quantity'),
accessor: 'quantity',
Cell: NumericInputCell,
disableSortBy: true,
width: 70,
align: Align.Right,
},
{
Header: intl.get('rate'),
accessor: 'rate',
Cell: MoneyFieldCell,
disableSortBy: true,
width: 70,
align: Align.Right,
},
{
Header: intl.get('discount'),
accessor: 'discount',
Cell: PercentFieldCell,
disableSortBy: true,
width: 60,
align: Align.Right,
},
{
Header: intl.get('total'),
accessor: 'amount',
Cell: TotalCell,
disableSortBy: true,
width: 100,
align: Align.Right,
},
...(landedCost
? [
{
Header: LandedCostHeaderCell,
accessor: 'landed_cost',
Cell: CheckBoxFieldCell,
width: 100,
disabledAccessor: 'landed_cost_disabled',
disableSortBy: true,
disableResizing: true,
align: Align.Center,
},
]
: []),
...(isProjectsFeatureEnabled
? [
{
Header: '',
accessor: 'invoicing',
Cell: ProjectBillableEntriesCell,
disableSortBy: true,
disableResizing: true,
width: 45,
align: Align.Center,
},
]
: []),
{
Header: '',
accessor: 'action',
Cell: ActionsCellRenderer,
disableSortBy: true,
disableResizing: true,
width: 45,
align: Align.Center,
},
],
[],
);
}

View File

@@ -0,0 +1,180 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { sumBy, isEmpty, last } from 'lodash';
import { useItem } from '@/hooks/query';
import {
toSafeNumber,
saveInvoke,
compose,
updateTableCell,
updateAutoAddNewLine,
orderingLinesIndexes,
updateTableRow,
} 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;
});
/**
* Disable landed cost checkbox once the item type is not service or non-inventorty.
* @returns {boolean}
*/
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.purchase_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,
};
}
/**
* 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);
});
/**
*
* @param {*} entries
* @returns
*/
export const composeControlledEntries = (entries) => {
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
};