mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
re-structure to monorepo.
This commit is contained in:
125
packages/webapp/src/containers/Entries/ItemsEntriesTable.tsx
Normal file
125
packages/webapp/src/containers/Entries/ItemsEntriesTable.tsx
Normal 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;
|
||||
186
packages/webapp/src/containers/Entries/components.tsx
Normal file
186
packages/webapp/src/containers/Entries/components.tsx
Normal 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,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
}
|
||||
180
packages/webapp/src/containers/Entries/utils.tsx
Normal file
180
packages/webapp/src/containers/Entries/utils.tsx
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user