mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
WIP / Features / Sate
This commit is contained in:
11
client/src/components/DataTableCells/DivFieldCell.js
Normal file
11
client/src/components/DataTableCells/DivFieldCell.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function DivFieldCell({ cell: { value: initialValue } }) {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
return <div>${value}</div>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import EstimateListField from 'components/EstimateListField';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
function EstimatesListFieldCell({
|
||||||
|
column: { id },
|
||||||
|
row: { index },
|
||||||
|
cell: { value: initialValue },
|
||||||
|
payload: { products, updateData, errors },
|
||||||
|
}) {
|
||||||
|
const handleProductSelected = useCallback(
|
||||||
|
(item) => {
|
||||||
|
updateData(index, id, item.id);
|
||||||
|
},
|
||||||
|
[updateData, index, id],
|
||||||
|
);
|
||||||
|
|
||||||
|
const error = errors?.[index]?.[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
intent={error ? Intent.DANGER : null}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<EstimateListField
|
||||||
|
products={products}
|
||||||
|
onProductSelected={handleProductSelected}
|
||||||
|
selectedProductId={initialValue}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EstimatesListFieldCell;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Classes, InputGroup, FormGroup } from '@blueprintjs/core';
|
import { Classes, InputGroup, FormGroup, Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
const InputEditableCell = ({
|
const InputEditableCell = ({
|
||||||
row: { index },
|
row: { index },
|
||||||
@@ -20,8 +20,17 @@ const InputEditableCell = ({
|
|||||||
setValue(initialValue);
|
setValue(initialValue);
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
|
const error = payload.errors?.[index]?.[id];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup
|
||||||
|
intent={error ? Intent.DANGER : null}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--select-list',
|
||||||
|
'form-group--account',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
43
client/src/components/DataTableCells/PercentFieldCell.js
Normal file
43
client/src/components/DataTableCells/PercentFieldCell.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
|
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||||
|
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||||
|
|
||||||
|
const PercentFieldCell = ({
|
||||||
|
cell: { value: initialValue },
|
||||||
|
row: { index },
|
||||||
|
column: { id },
|
||||||
|
payload: { errors, updateData },
|
||||||
|
}) => {
|
||||||
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
const onBlur = (e) => {
|
||||||
|
updateData(index, id, parseInt(e.target.value, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = useCallback((e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(initialValue);
|
||||||
|
}, [initialValue]);
|
||||||
|
|
||||||
|
const error = errors?.[index]?.[id];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup intent={error ? Intent.DANGER : null}>
|
||||||
|
<MoneyInputGroup
|
||||||
|
value={value}
|
||||||
|
suffix={'%'}
|
||||||
|
onChange={onChange}
|
||||||
|
inputGroupProps={{
|
||||||
|
fill: true,
|
||||||
|
onBlur,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PercentFieldCell;
|
||||||
@@ -2,10 +2,15 @@ import AccountsListFieldCell from './AccountsListFieldCell';
|
|||||||
import MoneyFieldCell from './MoneyFieldCell';
|
import MoneyFieldCell from './MoneyFieldCell';
|
||||||
import InputGroupCell from './InputGroupCell';
|
import InputGroupCell from './InputGroupCell';
|
||||||
import ContactsListFieldCell from './ContactsListFieldCell';
|
import ContactsListFieldCell from './ContactsListFieldCell';
|
||||||
|
import EstimatesListFieldCell from './EstimatesListFieldCell';
|
||||||
|
import PercentFieldCell from './PercentFieldCell';
|
||||||
|
import DivFieldCell from './DivFieldCell';
|
||||||
export {
|
export {
|
||||||
AccountsListFieldCell,
|
AccountsListFieldCell,
|
||||||
MoneyFieldCell,
|
MoneyFieldCell,
|
||||||
InputGroupCell,
|
InputGroupCell,
|
||||||
ContactsListFieldCell,
|
ContactsListFieldCell,
|
||||||
}
|
EstimatesListFieldCell,
|
||||||
|
PercentFieldCell,
|
||||||
|
DivFieldCell,
|
||||||
|
};
|
||||||
|
|||||||
41
client/src/components/EstimateListField.js
Normal file
41
client/src/components/EstimateListField.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { useCallback, useMemo, useEffect, useState } from 'react';
|
||||||
|
import { MenuItem, Button } from '@blueprintjs/core';
|
||||||
|
import ListSelect from 'components/ListSelect';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
|
function EstimateListField({
|
||||||
|
products,
|
||||||
|
selectedProductId,
|
||||||
|
onProductSelected,
|
||||||
|
defautlSelectText = <T id={'select_product'} />,
|
||||||
|
}) {
|
||||||
|
const onProductSelect = useCallback(
|
||||||
|
(product) => {
|
||||||
|
onProductSelected && onProductSelected(product);
|
||||||
|
},
|
||||||
|
[onProductSelected],
|
||||||
|
);
|
||||||
|
|
||||||
|
const productRenderer = useCallback(
|
||||||
|
(item, { handleClick, modifiers, query }) => (
|
||||||
|
<MenuItem text={item.name} key={item.id} onClick={handleClick} />
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListSelect
|
||||||
|
items={products}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
itemRenderer={productRenderer}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
onItemSelect={onProductSelect}
|
||||||
|
selectedItem={`${selectedProductId}`}
|
||||||
|
selectedItemProp={'id'}
|
||||||
|
labelProp={'name'}
|
||||||
|
defaultText={defautlSelectText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EstimateListField;
|
||||||
@@ -37,7 +37,20 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: <T id={'sales'} />,
|
text: <T id={'sales'} />,
|
||||||
children: [],
|
children: [
|
||||||
|
{
|
||||||
|
text: <T id={'estimates'} />,
|
||||||
|
href: '/estimates/new',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: <T id={'invocies'} />,
|
||||||
|
href: '/invoices/new',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: <T id={'receipts'} />,
|
||||||
|
href: '/receipts/new',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: <T id={'purchases'} />,
|
text: <T id={'purchases'} />,
|
||||||
@@ -99,11 +112,11 @@ export default [
|
|||||||
text: <T id={'expenses'} />,
|
text: <T id={'expenses'} />,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
text: <T id={'expenses'}/>,
|
text: <T id={'expenses'} />,
|
||||||
href: '/expenses-list',
|
href: '/expenses-list',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: <T id={'new_expense'}/>,
|
text: <T id={'new_expense'} />,
|
||||||
href: '/expenses/new',
|
href: '/expenses/new',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -140,12 +153,12 @@ export default [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Receivable Aging Summary',
|
text: 'Receivable Aging Summary',
|
||||||
href: '/financial-reports/receivable-aging-summary'
|
href: '/financial-reports/receivable-aging-summary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Payable Aging Summary',
|
text: 'Payable Aging Summary',
|
||||||
href: '/financial-reports/payable-aging-summary'
|
href: '/financial-reports/payable-aging-summary',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
259
client/src/containers/Sales/Estimate/EntriesItemsTable.js
Normal file
259
client/src/containers/Sales/Estimate/EntriesItemsTable.js
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
|
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import DataTable from 'components/DataTable';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
|
||||||
|
import { compose, formattedAmount, transformUpdatedRows } from 'utils';
|
||||||
|
import {
|
||||||
|
InputGroupCell,
|
||||||
|
MoneyFieldCell,
|
||||||
|
EstimatesListFieldCell,
|
||||||
|
PercentFieldCell,
|
||||||
|
DivFieldCell,
|
||||||
|
} from 'components/DataTableCells';
|
||||||
|
|
||||||
|
import withItems from 'containers/Items/withItems';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
|
const ActionsCellRenderer = ({
|
||||||
|
row: { index },
|
||||||
|
column: { id },
|
||||||
|
cell: { value },
|
||||||
|
data,
|
||||||
|
payload,
|
||||||
|
}) => {
|
||||||
|
if (data.length <= index + 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const onRemoveRole = () => {
|
||||||
|
payload.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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TotalEstimateCellRederer = (content, type) => (props) => {
|
||||||
|
if (props.data.length === props.row.index + 1) {
|
||||||
|
const total = props.data.reduce((total, entry) => {
|
||||||
|
const amount = parseInt(entry[type], 10);
|
||||||
|
const computed = amount ? total + amount : total;
|
||||||
|
|
||||||
|
return computed;
|
||||||
|
}, 0);
|
||||||
|
return <span>{formattedAmount(total, 'USD')}</span>;
|
||||||
|
}
|
||||||
|
return content(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateDiscount = (discount, quantity, rate) =>
|
||||||
|
quantity * rate - (quantity * rate * discount) / 100;
|
||||||
|
|
||||||
|
const CellRenderer = (content, type) => (props) => {
|
||||||
|
if (props.data.length === props.row.index + 1) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return content(props);
|
||||||
|
};
|
||||||
|
|
||||||
|
function EstimateTable({
|
||||||
|
//#withitems
|
||||||
|
itemsCurrentPage,
|
||||||
|
|
||||||
|
//#ownProps
|
||||||
|
onClickRemoveRow,
|
||||||
|
onClickAddNewRow,
|
||||||
|
onClickClearAllLines,
|
||||||
|
entries,
|
||||||
|
formik: { errors, setFieldValue, values },
|
||||||
|
}) {
|
||||||
|
const [rows, setRows] = useState([]);
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRows([...entries.map((e) => ({ ...e }))]);
|
||||||
|
}, [entries]);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
Header: '#',
|
||||||
|
accessor: 'index',
|
||||||
|
Cell: ({ row: { index } }) => <span>{index + 1}</span>,
|
||||||
|
width: 40,
|
||||||
|
disableResizing: true,
|
||||||
|
disableSortBy: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: formatMessage({ id: 'product_and_service' }),
|
||||||
|
id: 'item_id',
|
||||||
|
accessor: 'item_id',
|
||||||
|
Cell: EstimatesListFieldCell,
|
||||||
|
disableSortBy: true,
|
||||||
|
disableResizing: true,
|
||||||
|
width: 250,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: formatMessage({ id: 'description' }),
|
||||||
|
accessor: 'description',
|
||||||
|
Cell: InputGroupCell,
|
||||||
|
disableSortBy: true,
|
||||||
|
className: 'description',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Header: formatMessage({ id: 'quantity' }),
|
||||||
|
accessor: 'quantity',
|
||||||
|
Cell: CellRenderer(InputGroupCell, 'quantity'),
|
||||||
|
disableSortBy: true,
|
||||||
|
width: 100,
|
||||||
|
className: 'quantity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: formatMessage({ id: 'rate' }),
|
||||||
|
accessor: 'rate',
|
||||||
|
Cell: TotalEstimateCellRederer(MoneyFieldCell, 'rate'),
|
||||||
|
disableSortBy: true,
|
||||||
|
width: 150,
|
||||||
|
className: 'rate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: formatMessage({ id: 'discount' }),
|
||||||
|
accessor: 'discount',
|
||||||
|
Cell: CellRenderer(PercentFieldCell, InputGroupCell),
|
||||||
|
disableSortBy: true,
|
||||||
|
disableResizing: true,
|
||||||
|
width: 100,
|
||||||
|
className: 'discount',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: formatMessage({ id: 'total' }),
|
||||||
|
accessor: (row) =>
|
||||||
|
calculateDiscount(row.discount, row.quantity, row.rate),
|
||||||
|
Cell: TotalEstimateCellRederer(DivFieldCell, 'total'),
|
||||||
|
disableSortBy: true,
|
||||||
|
width: 150,
|
||||||
|
className: 'total',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: '',
|
||||||
|
accessor: 'action',
|
||||||
|
Cell: ActionsCellRenderer,
|
||||||
|
className: 'actions',
|
||||||
|
disableSortBy: true,
|
||||||
|
disableResizing: true,
|
||||||
|
width: 45,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[formatMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateData = useCallback(
|
||||||
|
(rowIndex, columnId, value) => {
|
||||||
|
const newRow = rows.map((row, index) => {
|
||||||
|
if (index === rowIndex) {
|
||||||
|
const newRow = { ...rows[rowIndex], [columnId]: value };
|
||||||
|
return {
|
||||||
|
...newRow,
|
||||||
|
total: calculateDiscount(
|
||||||
|
newRow.discount,
|
||||||
|
newRow.quantity,
|
||||||
|
newRow.rate,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
setFieldValue(
|
||||||
|
'entries',
|
||||||
|
newRow.map((row) => ({
|
||||||
|
...omit(row),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[rows, setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveRow = useCallback(
|
||||||
|
(rowIndex) => {
|
||||||
|
if (rows.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeIndex = parseInt(rowIndex, 10);
|
||||||
|
const newRows = rows.filter((row, index) => index !== removeIndex);
|
||||||
|
setFieldValue(
|
||||||
|
'entries',
|
||||||
|
newRows.map((row, index) => ({
|
||||||
|
...omit(row),
|
||||||
|
index: index + 1,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
onClickRemoveRow && onClickRemoveRow(removeIndex);
|
||||||
|
},
|
||||||
|
[rows, setFieldValue, onClickRemoveRow],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickNewRow = () => {
|
||||||
|
onClickAddNewRow && onClickAddNewRow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickClearAllLines = () => {
|
||||||
|
onClickClearAllLines && onClickClearAllLines();
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowClassNames = useCallback(
|
||||||
|
(row) => ({
|
||||||
|
'row--total': rows.length === row.index + 1,
|
||||||
|
}),
|
||||||
|
[rows],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'estimate-form__table'}>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={rows}
|
||||||
|
rowClassNames={rowClassNames}
|
||||||
|
payload={{
|
||||||
|
products: itemsCurrentPage,
|
||||||
|
errors: errors.entries || [],
|
||||||
|
updateData: handleUpdateData,
|
||||||
|
removeRow: handleRemoveRow,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className={'mt1'}>
|
||||||
|
<Button
|
||||||
|
small={true}
|
||||||
|
className={'button--secondary button--new-line'}
|
||||||
|
onClick={onClickNewRow}
|
||||||
|
>
|
||||||
|
<T id={'new_lines'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
small={true}
|
||||||
|
className={'button--secondary button--clear-lines ml1'}
|
||||||
|
onClick={handleClickClearAllLines}
|
||||||
|
>
|
||||||
|
<T id={'clear_all_lines'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withItems(({ itemsCurrentPage }) => ({
|
||||||
|
itemsCurrentPage,
|
||||||
|
})),
|
||||||
|
)(EstimateTable);
|
||||||
140
client/src/containers/Sales/Estimate/EstimateActionsBar.js
Normal file
140
client/src/containers/Sales/Estimate/EstimateActionsBar.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import React, { useMemo, useCallback } from 'react';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Popover,
|
||||||
|
NavbarDivider,
|
||||||
|
NavbarGroup,
|
||||||
|
PopoverInteractionKind,
|
||||||
|
Position,
|
||||||
|
Intent,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
|
import { If } from 'components';
|
||||||
|
import FilterDropdown from 'components/FilterDropdown';
|
||||||
|
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||||
|
|
||||||
|
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||||
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
import withEstimateActions from './withEstimateActions';
|
||||||
|
|
||||||
|
import withEstimates from './withEstimates';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
function EstimateActionsBar({
|
||||||
|
// #withResourceDetail
|
||||||
|
resourceFields,
|
||||||
|
|
||||||
|
//#withEstimates
|
||||||
|
estimateViews,
|
||||||
|
|
||||||
|
// #withEstimateActions
|
||||||
|
addEstimatesTableQueries,
|
||||||
|
|
||||||
|
// #own Porps
|
||||||
|
onFilterChanged,
|
||||||
|
selectedRows,
|
||||||
|
}) {
|
||||||
|
const { path } = useRouteMatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const onClickNewEstimate = useCallback(() => {
|
||||||
|
// history.push('/estimates/new');
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
const filterDropdown = FilterDropdown({
|
||||||
|
initialCondition: {
|
||||||
|
fieldKey: '',
|
||||||
|
compatator: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
fields: resourceFields,
|
||||||
|
onFilterChange: (filterConditions) => {
|
||||||
|
addEstimatesTableQueries({
|
||||||
|
filter_roles: filterConditions || '',
|
||||||
|
});
|
||||||
|
onFilterChanged && onFilterChange(filterConditions);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
|
||||||
|
selectedRows,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardActionsBar>
|
||||||
|
<NavbarGroup>
|
||||||
|
<NavbarDivider />
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon={'plus'} />}
|
||||||
|
text={<T id={'new_estimate'} />}
|
||||||
|
onClick={onClickNewEstimate}
|
||||||
|
/>
|
||||||
|
<Popover
|
||||||
|
minimal={true}
|
||||||
|
content={filterDropdown}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
position={Position.BOTTOM_LEFT}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={classNames(Classes.MINIMAL, 'button--filter')}
|
||||||
|
text={'Filter'}
|
||||||
|
icon={<Icon icon={'filter-16'} iconSize={16} />}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<If condition={hasSelectedRows}>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon={'trash-16'} iconSize={16} />}
|
||||||
|
text={<T id={'delete'} />}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
// onClick={handleBulkDelete}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<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'} />}
|
||||||
|
/>
|
||||||
|
</NavbarGroup>
|
||||||
|
</DashboardActionsBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
resourceName: 'estimates',
|
||||||
|
});
|
||||||
|
|
||||||
|
const withEstimateActionsBar = connect(mapStateToProps);
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withEstimateActionsBar,
|
||||||
|
withDialogActions,
|
||||||
|
withResourceDetail(({ resourceFields }) => ({
|
||||||
|
resourceFields,
|
||||||
|
})),
|
||||||
|
// withEstimate(({ estimateViews }) => ({
|
||||||
|
// estimateViews,
|
||||||
|
// })),
|
||||||
|
withEstimateActions,
|
||||||
|
)(EstimateActionsBar);
|
||||||
334
client/src/containers/Sales/Estimate/EstimateForm.js
Normal file
334
client/src/containers/Sales/Estimate/EstimateForm.js
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Intent, FormGroup, TextArea, Button } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import { pick, omitBy, omit } from 'lodash';
|
||||||
|
|
||||||
|
import EstimateFormHeader from './EstimateFormHeader';
|
||||||
|
import EstimatesItemsTable from './EntriesItemsTable';
|
||||||
|
import EstimateFormFooter from './EstimateFormFooter';
|
||||||
|
|
||||||
|
import withEstimateActions from './withEstimateActions';
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
import withMediaActions from 'containers/Media/withMediaActions';
|
||||||
|
|
||||||
|
import AppToaster from 'components/AppToaster';
|
||||||
|
import Dragzone from 'components/Dragzone';
|
||||||
|
|
||||||
|
import useMedia from 'hooks/useMedia';
|
||||||
|
import { compose, repeatValue } from 'utils';
|
||||||
|
|
||||||
|
const MIN_LINES_NUMBER = 4;
|
||||||
|
|
||||||
|
const EstimateForm = ({
|
||||||
|
//#WithMedia
|
||||||
|
requestSubmitMedia,
|
||||||
|
requestDeleteMedia,
|
||||||
|
|
||||||
|
//#WithEstimateActions
|
||||||
|
requestSubmitEstimate,
|
||||||
|
requestEditEstimate,
|
||||||
|
|
||||||
|
//#withDashboard
|
||||||
|
changePageTitle,
|
||||||
|
changePageSubtitle,
|
||||||
|
|
||||||
|
//#withEstimateDetail
|
||||||
|
estimate,
|
||||||
|
|
||||||
|
//#own Props
|
||||||
|
estimateId,
|
||||||
|
onFormSubmit,
|
||||||
|
onCancelForm,
|
||||||
|
}) => {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [payload, setPaload] = useState({});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setFiles,
|
||||||
|
saveMedia,
|
||||||
|
deletedFiles,
|
||||||
|
setDeletedFiles,
|
||||||
|
deleteMedia,
|
||||||
|
} = useMedia({
|
||||||
|
saveCallback: requestSubmitMedia,
|
||||||
|
deleteCallback: requestDeleteMedia,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDropFiles = useCallback((_files) => {
|
||||||
|
setFiles(_files.filter((file) => file.uploaded === false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const savedMediaIds = useRef([]);
|
||||||
|
const clearSavedMediaIds = () => {
|
||||||
|
savedMediaIds.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (estimate && estimate.id) {
|
||||||
|
changePageTitle(formatMessage({ id: 'edit_estimate' }));
|
||||||
|
} else {
|
||||||
|
changePageTitle(formatMessage({ id: 'new_estimate' }));
|
||||||
|
}
|
||||||
|
}, [changePageTitle, estimate, formatMessage]);
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
customer_id: Yup.number()
|
||||||
|
.label(formatMessage({ id: 'customer_name_' }))
|
||||||
|
.required(),
|
||||||
|
estimate_date: Yup.date()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'estimate_date_' })),
|
||||||
|
expiration_date: Yup.date()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'expiration_date_' })),
|
||||||
|
estimate_number: Yup.number()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'estimate_number_' })),
|
||||||
|
reference: Yup.string().min(1).max(255),
|
||||||
|
note: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(1024)
|
||||||
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
|
||||||
|
terms_conditions: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(1024)
|
||||||
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
|
||||||
|
entries: Yup.array().of(
|
||||||
|
Yup.object().shape({
|
||||||
|
quantity: Yup.number().nullable(),
|
||||||
|
//Cyclic dependency
|
||||||
|
rate: Yup.number().nullable(),
|
||||||
|
// .when(['item_id'], {
|
||||||
|
// is: (item_id) => item_id,
|
||||||
|
// then: Yup.number().required(),
|
||||||
|
// }),
|
||||||
|
|
||||||
|
// rate: Yup.number().test((value) => {
|
||||||
|
// const { item_id } = this.parent;
|
||||||
|
// if (!item_id) return value != null;
|
||||||
|
// return false;
|
||||||
|
// }),
|
||||||
|
item_id: Yup.number()
|
||||||
|
.nullable()
|
||||||
|
.when(['quantity', 'rate'], {
|
||||||
|
is: (quantity, rate) => quantity || rate,
|
||||||
|
then: Yup.number().required(),
|
||||||
|
}),
|
||||||
|
discount: Yup.number().nullable(),
|
||||||
|
description: Yup.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveInvokeSubmit = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
onFormSubmit && onFormSubmit(payload);
|
||||||
|
},
|
||||||
|
[onFormSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultEstimate = useMemo(
|
||||||
|
() => ({
|
||||||
|
index: 0,
|
||||||
|
item_id: null,
|
||||||
|
rate: null,
|
||||||
|
discount: null,
|
||||||
|
quantity: null,
|
||||||
|
description: '',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultInitialValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
customer_id: null,
|
||||||
|
estimate_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
|
expiration_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
|
estimate_number: null,
|
||||||
|
reference: '',
|
||||||
|
note: '',
|
||||||
|
terms_conditions: '',
|
||||||
|
entries: [...repeatValue(defaultEstimate, MIN_LINES_NUMBER)],
|
||||||
|
}),
|
||||||
|
[defaultEstimate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderingProductsIndex = (_entries) => {
|
||||||
|
return _entries.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
index: index + 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
...defaultInitialValues,
|
||||||
|
entries: orderingProductsIndex(defaultInitialValues.entries),
|
||||||
|
}),
|
||||||
|
[defaultEstimate, defaultInitialValues, estimate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialAttachmentFiles = useMemo(() => {
|
||||||
|
return estimate && estimate.media
|
||||||
|
? estimate.media.map((attach) => ({
|
||||||
|
preview: attach.attachment_file,
|
||||||
|
uploaded: true,
|
||||||
|
metadata: { ...attach },
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
}, [estimate]);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
enableReinitialize: true,
|
||||||
|
validationSchema,
|
||||||
|
initialValues: {
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => {
|
||||||
|
const entries = values.entries.map((item) => omit(item, ['total']));
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
...values,
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
const saveEstimate = (mediaIds) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const requestForm = { ...form, media_ids: mediaIds };
|
||||||
|
|
||||||
|
requestSubmitEstimate(requestForm)
|
||||||
|
.then((response) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: formatMessage(
|
||||||
|
{
|
||||||
|
id: 'the_estimate_has_been_successfully_created',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: values.estimate_number,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
resetForm();
|
||||||
|
saveInvokeSubmit({ action: 'new', ...payload });
|
||||||
|
clearSavedMediaIds();
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Promise.all([saveMedia(), deleteMedia()])
|
||||||
|
.then(([savedMediaResponses]) => {
|
||||||
|
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
|
||||||
|
savedMediaIds.current = mediaIds;
|
||||||
|
return savedMediaResponses;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return saveEstimate(saveEstimate.current);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmitClick = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
setPaload(payload);
|
||||||
|
formik.submitForm();
|
||||||
|
},
|
||||||
|
[setPaload, formik],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancelClick = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
onCancelForm && onCancelForm(payload);
|
||||||
|
},
|
||||||
|
[onCancelForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteFile = useCallback(
|
||||||
|
(_deletedFiles) => {
|
||||||
|
_deletedFiles.forEach((deletedFile) => {
|
||||||
|
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||||
|
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDeletedFiles, deletedFiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickAddNewRow = () => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'entries',
|
||||||
|
orderingProductsIndex([...formik.values.entries, defaultEstimate]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllLines = () => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'entries',
|
||||||
|
orderingProductsIndex([
|
||||||
|
...repeatValue(defaultEstimate, MIN_LINES_NUMBER),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<EstimateFormHeader formik={formik} />
|
||||||
|
<EstimatesItemsTable
|
||||||
|
entries={formik.values.entries}
|
||||||
|
onClickAddNewRow={handleClickAddNewRow}
|
||||||
|
onClickClearAllLines={handleClearAllLines}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'customer_note'} />}
|
||||||
|
className={'form-group--customer_note'}
|
||||||
|
>
|
||||||
|
<TextArea growVertically={true} {...formik.getFieldProps('note')} />
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'terms_conditions'} />}
|
||||||
|
className={'form-group--terms_conditions'}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('terms_conditions')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EstimateFormFooter
|
||||||
|
formik={formik}
|
||||||
|
onSubmitClick={handleSubmitClick}
|
||||||
|
onCancelClick={handleCancelClick}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withEstimateActions,
|
||||||
|
withDashboardActions,
|
||||||
|
withMediaActions,
|
||||||
|
)(EstimateForm);
|
||||||
41
client/src/containers/Sales/Estimate/EstimateFormFooter.js
Normal file
41
client/src/containers/Sales/Estimate/EstimateFormFooter.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Intent, Button } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
|
export default function EstimateFormFooter({
|
||||||
|
formik: { isSubmitting },
|
||||||
|
onSubmitClick,
|
||||||
|
onCancelClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={'estimate-form__floating-footer'}>
|
||||||
|
<Button disabled={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||||
|
<T id={'save_send'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
className={'ml1'}
|
||||||
|
name={'save'}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<T id={'save'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button className={'ml1'} disabled={isSubmitting}>
|
||||||
|
<T id={'clear'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={'ml1'}
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
onCancelClick && onCancelClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<T id={'close'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
192
client/src/containers/Sales/Estimate/EstimateFormHeader.js
Normal file
192
client/src/containers/Sales/Estimate/EstimateFormHeader.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
Intent,
|
||||||
|
Position,
|
||||||
|
MenuItem,
|
||||||
|
Classes,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { momentFormatter, compose, tansformDateValue } from 'utils';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { ListSelect, ErrorMessage, FieldRequiredHint, Hint } from 'components';
|
||||||
|
|
||||||
|
import withCustomers from 'containers/Customers/withCustomers';
|
||||||
|
|
||||||
|
function EstimateFormHeader({
|
||||||
|
formik: { errors, touched, setFieldValue, getFieldProps, values },
|
||||||
|
|
||||||
|
//#withCustomers
|
||||||
|
customers,
|
||||||
|
}) {
|
||||||
|
const handleDateChange = useCallback(
|
||||||
|
(date_filed) => (date) => {
|
||||||
|
const formatted = moment(date).format('YYYY-MM-DD');
|
||||||
|
setFieldValue(date_filed, formatted);
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const CustomerRenderer = useCallback(
|
||||||
|
(cutomer, { handleClick }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={cutomer.id}
|
||||||
|
text={cutomer.display_name}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter Customer
|
||||||
|
const filterCustomer = (query, customer, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = customer.display_name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
`${customer.display_name} ${normalizedTitle}`.indexOf(
|
||||||
|
normalizedQuery,
|
||||||
|
) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle change customer
|
||||||
|
const onChangeCustomer = useCallback(
|
||||||
|
(filedName) => {
|
||||||
|
return (customer) => {
|
||||||
|
setFieldValue(filedName, customer.id);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'estimate-form'}>
|
||||||
|
<div className={'estimate-form__primary-section'}>
|
||||||
|
{/* customer name */}
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'customer_name'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name={'customer_id'} {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListSelect
|
||||||
|
items={customers}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
itemRenderer={CustomerRenderer}
|
||||||
|
itemPredicate={filterCustomer}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
onItemSelect={onChangeCustomer('customer_id')}
|
||||||
|
selectedItem={values.customer_id}
|
||||||
|
selectedItemProp={'id'}
|
||||||
|
defaultText={<T id={'select_customer_account'} />}
|
||||||
|
labelProp={'display_name'}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{/* estimate_date */}
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
|
||||||
|
// md={9} push={{ md: 3 }}
|
||||||
|
>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'estimate_date'} />}
|
||||||
|
inline={true}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
intent={
|
||||||
|
errors.estimate_date && touched.estimate_date && Intent.DANGER
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="estimate_date" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
value={tansformDateValue(values.estimate_date)}
|
||||||
|
onChange={handleDateChange('estimate_date')}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
|
||||||
|
// md={3} pull={{ md: 9 }}
|
||||||
|
>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'expiration_date'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
intent={
|
||||||
|
errors.expiration_date &&
|
||||||
|
touched.expiration_date &&
|
||||||
|
Intent.DANGER
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="expiration_date" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
value={tansformDateValue(values.expiration_date)}
|
||||||
|
onChange={handleDateChange('expiration_date')}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
{/* Estimate */}
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'estimate'} />}
|
||||||
|
inline={true}
|
||||||
|
className={('form-group--estimate', Classes.FILL)}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={
|
||||||
|
errors.estimate_number && touched.estimate_number && Intent.DANGER
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="estimate_number" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={
|
||||||
|
errors.estimate_number && touched.estimate_number && Intent.DANGER
|
||||||
|
}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('estimate_number')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'reference'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--reference', Classes.FILL)}
|
||||||
|
intent={errors.reference && touched.reference && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={errors.reference && touched.reference && Intent.DANGER}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('reference')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withCustomers(({ customers }) => ({
|
||||||
|
customers,
|
||||||
|
})),
|
||||||
|
)(EstimateFormHeader);
|
||||||
120
client/src/containers/Sales/Estimate/EstimateList.js
Normal file
120
client/src/containers/Sales/Estimate/EstimateList.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||||
|
import { useQuery, queryCache } from 'react-query';
|
||||||
|
import { Alert, Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import AppToaster from 'components/AppToaster';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||||
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
|
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
import withEstimateActions from './withEstimateActions';
|
||||||
|
|
||||||
|
import EstimateActionsBar from './EstimateActionsBar';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
function EstimateList({
|
||||||
|
// #withDashboardActions
|
||||||
|
changePageTitle,
|
||||||
|
|
||||||
|
// #withViewsActions
|
||||||
|
|
||||||
|
// #withEstimate
|
||||||
|
|
||||||
|
//#withEistimateActions
|
||||||
|
requestFetchEstimatesTable,
|
||||||
|
requestDeleteEstimate,
|
||||||
|
addEstimatesTableQueries,
|
||||||
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [deleteEstimate, setDeleteEstimate] = useState(false);
|
||||||
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
|
|
||||||
|
const fetchEstimate = useQuery(['estimate-table'], () =>
|
||||||
|
requestFetchEstimatesTable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
changePageTitle(formatMessage({ id: 'estimate_list' }));
|
||||||
|
}, [changePageTitle, formatMessage]);
|
||||||
|
|
||||||
|
// handle delete estimate click
|
||||||
|
|
||||||
|
const handleDeleteEstimate = useCallback(
|
||||||
|
(estimate) => {
|
||||||
|
setDeleteEstimate(estimate);
|
||||||
|
},
|
||||||
|
[setDeleteEstimate],
|
||||||
|
);
|
||||||
|
|
||||||
|
// handle cancel estimate
|
||||||
|
const handleCancelEstimateDelete = useCallback(() => {
|
||||||
|
setDeleteEstimate(false);
|
||||||
|
}, [setDeleteEstimate]);
|
||||||
|
|
||||||
|
// handle confirm delete estimate
|
||||||
|
const handleConfirmEstimateDelete = useCallback(() => {
|
||||||
|
requestDeleteEstimate(deleteEstimate.id).then((response) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: formatMessage({
|
||||||
|
id: 'the_estimate_has_been_successfully_deleted',
|
||||||
|
}),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setDeleteEstimate(false);
|
||||||
|
});
|
||||||
|
}, [deleteEstimate, requestDeleteEstimate, formatMessage]);
|
||||||
|
|
||||||
|
// Calculates the selected rows
|
||||||
|
const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
|
||||||
|
selectedRows,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleEidtEstimate = useCallback(
|
||||||
|
(estimate) => {
|
||||||
|
history.push(`/estimates/${estimate.id}/edit`);
|
||||||
|
},
|
||||||
|
[history],
|
||||||
|
);
|
||||||
|
const handleFetchData = useCallback(
|
||||||
|
({ pageIndex, pageSize, sortBy }) => {
|
||||||
|
const page = pageIndex + 1;
|
||||||
|
|
||||||
|
addEstimatesTableQueries({
|
||||||
|
...(sortBy.length > 0
|
||||||
|
? {
|
||||||
|
column_sort_by: sortBy[0].id,
|
||||||
|
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
page_size: pageSize,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addEstimatesTableQueries],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedRowsChange = useCallback((estimate) => {
|
||||||
|
selectedRows(estimate);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'estimates'}>
|
||||||
|
<EstimateActionsBar
|
||||||
|
// onBulkDelete={}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
// onFilterChanged={}
|
||||||
|
/>
|
||||||
|
<DashboardPageContent>
|
||||||
|
<Switch>
|
||||||
|
<Route></Route>
|
||||||
|
</Switch>
|
||||||
|
</DashboardPageContent>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EstimateList;
|
||||||
49
client/src/containers/Sales/Estimate/Estimates.js
Normal file
49
client/src/containers/Sales/Estimate/Estimates.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import EstimateForm from './EstimateForm';
|
||||||
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
|
|
||||||
|
import withCustomersActions from 'containers/Customers/withCustomersActions';
|
||||||
|
import withItemsActions from 'containers/Items/withItemsActions';
|
||||||
|
import withEstimateActions from './withEstimateActions';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
function Estimates({ requestFetchCustomers, requestFetchItems }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
// Handle fetch customers data table or list
|
||||||
|
const fetchCustomers = useQuery('customers-table', () =>
|
||||||
|
requestFetchCustomers({}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle fetch Items data table or list
|
||||||
|
const fetchItems = useQuery('items-table', () => requestFetchItems({}));
|
||||||
|
|
||||||
|
const handleFormSubmit = useCallback((payload) => {}, [history]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
history.goBack();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider
|
||||||
|
loading={fetchCustomers.isFetching || fetchItems.isFetching}
|
||||||
|
>
|
||||||
|
<EstimateForm
|
||||||
|
onFormSubmit={handleFormSubmit}
|
||||||
|
// estimateId={id}
|
||||||
|
onCancelForm={handleCancel}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withEstimateActions,
|
||||||
|
withCustomersActions,
|
||||||
|
withItemsActions,
|
||||||
|
)(Estimates);
|
||||||
196
client/src/containers/Sales/Estimate/EstimatesDataTable.js
Normal file
196
client/src/containers/Sales/Estimate/EstimatesDataTable.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Intent,
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
Popover,
|
||||||
|
Tooltip,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
|
Position,
|
||||||
|
Tag,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import { useUpdateEffect } from 'hooks';
|
||||||
|
|
||||||
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
|
import { If } from 'components';
|
||||||
|
import DataTable from 'components/DataTable';
|
||||||
|
|
||||||
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
import withViewDetails from 'containers/Views/withViewDetails';
|
||||||
|
import withEstimates from './withEstimates';
|
||||||
|
import withEstimateActions from './withEstimateActions';
|
||||||
|
|
||||||
|
function EstimatesDataTable({
|
||||||
|
//#withEitimates
|
||||||
|
|
||||||
|
// #withDashboardActions
|
||||||
|
changeCurrentView,
|
||||||
|
changePageSubtitle,
|
||||||
|
setTopbarEditView,
|
||||||
|
|
||||||
|
// #withView
|
||||||
|
viewMeta,
|
||||||
|
|
||||||
|
//#OwnProps
|
||||||
|
loading,
|
||||||
|
onFetchData,
|
||||||
|
onEditEstimate,
|
||||||
|
onDeleteEstimate,
|
||||||
|
onSelectedRowsChange,
|
||||||
|
}) {
|
||||||
|
const [initialMount, setInitialMount] = useState(false);
|
||||||
|
const { custom_view_id: customViewId } = useParams();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInitialMount(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// useUpdateEffect(() => {
|
||||||
|
// if (!estimateLoading) {
|
||||||
|
// setInitialMount(true);
|
||||||
|
// }
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customViewId) {
|
||||||
|
changeCurrentView(customViewId);
|
||||||
|
setTopbarEditView(customViewId);
|
||||||
|
}
|
||||||
|
changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
|
||||||
|
}, [
|
||||||
|
customViewId,
|
||||||
|
changeCurrentView,
|
||||||
|
changePageSubtitle,
|
||||||
|
setTopbarEditView,
|
||||||
|
viewMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleEditEstimate = useCallback(
|
||||||
|
(estimate) => {
|
||||||
|
onEditEstimate && onEditEstimate(estimate);
|
||||||
|
},
|
||||||
|
[onEditEstimate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteEstimate = useCallback(() => {
|
||||||
|
onDeleteEstimate && onDeleteEstimate();
|
||||||
|
}, [onDeleteEstimate]);
|
||||||
|
|
||||||
|
const actionMenuList = useCallback(
|
||||||
|
() => (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem text={formatMessage({ id: 'view_details' })} />
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem
|
||||||
|
text={formatMessage({ id: 'edit_estimate' })}
|
||||||
|
// onClick={handleEditEstimate(estimate)}
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
text={formatMessage({ id: 'delete_estimate' })}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
// onClick={handleDeleteEstimate(estimate)}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
),
|
||||||
|
[handleDeleteEstimate, handleEditEstimate, formatMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
Header: '',
|
||||||
|
Cell: ({ cell }) => (
|
||||||
|
<Popover
|
||||||
|
content={actionMenuList(cell.row.original)}
|
||||||
|
position={Position.RIGHT_BOTTOM}
|
||||||
|
>
|
||||||
|
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
|
||||||
|
</Popover>
|
||||||
|
),
|
||||||
|
className: 'actions',
|
||||||
|
width: 50,
|
||||||
|
disableResizing: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[actionMenuList, formatMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDataTableFetchData = useCallback(
|
||||||
|
(...arguments) => {
|
||||||
|
onFetchData && onFetchData(...arguments);
|
||||||
|
},
|
||||||
|
[onFetchData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedRowsChange = useCallback(
|
||||||
|
(selectedRows) => {
|
||||||
|
onSelectedRowsChange &&
|
||||||
|
onSelectedRowsChange(selectedRows.map((s) => s.original));
|
||||||
|
},
|
||||||
|
[onSelectedRowsChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoadingIndicator loading={loading} mount={false}>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={[]}
|
||||||
|
onFetchData={handleDataTableFetchData}
|
||||||
|
manualSortBy={true}
|
||||||
|
/>
|
||||||
|
</LoadingIndicator>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withRouter,
|
||||||
|
withDialogActions,
|
||||||
|
withDashboardActions,
|
||||||
|
withEstimateActions,
|
||||||
|
// withEstimates(({}) => ({})),
|
||||||
|
withViewDetails(),
|
||||||
|
)(EstimatesDataTable);
|
||||||
32
client/src/containers/Sales/Estimate/withEstimateActions.js
Normal file
32
client/src/containers/Sales/Estimate/withEstimateActions.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
submitEstimate,
|
||||||
|
editEstimate,
|
||||||
|
deleteEstimate,
|
||||||
|
fetchEstimate,
|
||||||
|
fetchEstimatesTable,
|
||||||
|
} from 'store/Estimate/estimates.actions';
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
const mapDipatchToProps = (dispatch) => ({
|
||||||
|
requestSubmitEstimate: (form) => dispatch(submitEstimate({ form })),
|
||||||
|
requsetFetchEstimate: (id) => dispatch(fetchEstimate({ id })),
|
||||||
|
requestEditEstimate: (id, form) => dispatch(editEstimate({ id, form })),
|
||||||
|
requestFetchEstimatesTable: (query = {}) =>
|
||||||
|
dispatch(fetchEstimatesTable({ query: { ...query } })),
|
||||||
|
requestDeleteEstimate: (id) => dispatch(deleteEstimate({ id })),
|
||||||
|
|
||||||
|
changeEstimateView: (id) =>
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_SET_CURRENT_VIEW,
|
||||||
|
currentViewId: parseInt(id, 10),
|
||||||
|
}),
|
||||||
|
|
||||||
|
addEstimatesTableQueries: (queries) =>
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_TABLE_QUERIES_ADD,
|
||||||
|
queries,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDipatchToProps);
|
||||||
11
client/src/containers/Sales/Estimate/withEstimateDetail.js
Normal file
11
client/src/containers/Sales/Estimate/withEstimateDetail.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { getEstimateByIdFactory } from 'store/Estimate/estimates.selectors';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const getEstimateById = getEstimateByIdFactory();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
estimate: getEstimateById(state, props),
|
||||||
|
});
|
||||||
|
return connect(mapStateToProps);
|
||||||
|
};
|
||||||
24
client/src/containers/Sales/Estimate/withEstimates.js
Normal file
24
client/src/containers/Sales/Estimate/withEstimates.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { getResourceViews } from 'store/customViews/customViews.selectors';
|
||||||
|
import {
|
||||||
|
getEstimateCurrentPage,
|
||||||
|
getEstimatesTableQuery,
|
||||||
|
getEstimatesPaginationMetaFactory,
|
||||||
|
} from 'store/Estimate/estimates.selectors';
|
||||||
|
|
||||||
|
function withEstimates(mapSate) {
|
||||||
|
const mapStateToProps = (state, props) => {
|
||||||
|
const query = getEstimatesTableQuery(state, props);
|
||||||
|
const mapped = {
|
||||||
|
estimateViews: getResourceViews(state, props, 'estimates'),
|
||||||
|
estimateItems: state.estiamte.items,
|
||||||
|
estimateTableQuery: query,
|
||||||
|
estimatesLoading: state.estiamte.loading,
|
||||||
|
};
|
||||||
|
return mapSate ? mapSate(mapped, state, props) : mapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
return connect(mapStateToProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withEstimates;
|
||||||
133
client/src/containers/Sales/Invoice/InvoiceActionsBar.js
Normal file
133
client/src/containers/Sales/Invoice/InvoiceActionsBar.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import React, { useMemo, useCallback } from 'react';
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Popover,
|
||||||
|
NavbarDivider,
|
||||||
|
NavbarGroup,
|
||||||
|
PopoverInteractionKind,
|
||||||
|
Position,
|
||||||
|
Intent,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useRouteMatch, useHistory } from 'react-router-dom';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
|
import { If } from 'components';
|
||||||
|
import FilterDropdown from 'components/FilterDropdown';
|
||||||
|
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||||
|
|
||||||
|
import withResourceDetail from 'containers/Resources/withResourceDetails';
|
||||||
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
import withInvoiceActions from './withInvoices';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
function InvoiceActionsBar({
|
||||||
|
// #withResourceDetail
|
||||||
|
resourceFields,
|
||||||
|
|
||||||
|
//#withInvoice
|
||||||
|
InvoiceViews,
|
||||||
|
|
||||||
|
// #withInvoiceActions
|
||||||
|
addInvoiceTableQueries,
|
||||||
|
|
||||||
|
// #own Porps
|
||||||
|
onFilterChanged,
|
||||||
|
selectedRows,
|
||||||
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const FilterDropdown = FilterDropdown({
|
||||||
|
initialCondition: {
|
||||||
|
fieldKey: '',
|
||||||
|
compatator: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
fields: resourceFields,
|
||||||
|
onFilterChange: (filterConditions) => {
|
||||||
|
addInvoiceTableQueries({
|
||||||
|
filter_roles: filterConditions || '',
|
||||||
|
});
|
||||||
|
onFilterChanged && onFilterChanged(filterConditions);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
|
||||||
|
selectedRows,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardActionsBar>
|
||||||
|
<NavbarGroup>
|
||||||
|
<NavbarDivider />
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon={'plus'} />}
|
||||||
|
text={<T id={'new_invoice'} />}
|
||||||
|
onClick={onClickNewInvoice}
|
||||||
|
/>
|
||||||
|
<Popover
|
||||||
|
minimal={true}
|
||||||
|
content={FilterDropdown}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
position={Position.BOTTOM_LEFT}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={classNames(Classes.MINIMAL)}
|
||||||
|
text={'Filter'}
|
||||||
|
icon={<Icon icon={'filter-16'} iconSize={16} />}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
<If condition={hasSelectedRows}>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon={'trash-16'} iconSize={16} />}
|
||||||
|
text={<T id={'delete'} />}
|
||||||
|
intent={Intent.DANGER}
|
||||||
|
// onClick={handleBulkDelete}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<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'} />}
|
||||||
|
/>
|
||||||
|
</NavbarGroup>
|
||||||
|
</DashboardActionsBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
resourceName: 'invoice',
|
||||||
|
});
|
||||||
|
|
||||||
|
const withInvoiceActionsBar = connect(mapStateToProps);
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withInvoiceActionsBar,
|
||||||
|
withDialogActions,
|
||||||
|
withResourceDetail(({ resourceFields }) => ({
|
||||||
|
resourceFields,
|
||||||
|
})),
|
||||||
|
// withInvoices(({ invoiceViews }) => ({
|
||||||
|
// invoiceViews,
|
||||||
|
// })),
|
||||||
|
withInvoiceActions,
|
||||||
|
)(InvoiceActionsBar);
|
||||||
321
client/src/containers/Sales/Invoice/InvoiceForm.js
Normal file
321
client/src/containers/Sales/Invoice/InvoiceForm.js
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Intent, FormGroup, TextArea, Button } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import { pick, omit } from 'lodash';
|
||||||
|
|
||||||
|
import InvoiceFormHeader from './InvoiceFormHeader';
|
||||||
|
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
||||||
|
import InvoiceFormFooter from './InvoiceFormFooter';
|
||||||
|
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
import withMediaActions from 'containers/Media/withMediaActions';
|
||||||
|
import withInvoiceActions from './withInvoiceActions';
|
||||||
|
|
||||||
|
import { AppToaster } from 'components';
|
||||||
|
import Dragzone from 'components/Dragzone';
|
||||||
|
import useMedia from 'hooks/useMedia';
|
||||||
|
|
||||||
|
import { compose, repeatValue } from 'utils';
|
||||||
|
|
||||||
|
const MIN_LINES_NUMBER = 4;
|
||||||
|
|
||||||
|
function InvoiceForm({
|
||||||
|
//#WithMedia
|
||||||
|
requestSubmitMedia,
|
||||||
|
requestDeleteMedia,
|
||||||
|
|
||||||
|
//#WithInvoiceActions
|
||||||
|
requestSubmitInvoice,
|
||||||
|
|
||||||
|
//#withDashboard
|
||||||
|
changePageTitle,
|
||||||
|
changePageSubtitle,
|
||||||
|
|
||||||
|
//#withInvoiceDetail
|
||||||
|
invoice,
|
||||||
|
|
||||||
|
//#own Props
|
||||||
|
InvoiceId,
|
||||||
|
onFormSubmit,
|
||||||
|
onCancelForm,
|
||||||
|
}) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [payload, setPaload] = useState({});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setFiles,
|
||||||
|
saveMedia,
|
||||||
|
deletedFiles,
|
||||||
|
setDeletedFiles,
|
||||||
|
deleteMedia,
|
||||||
|
} = useMedia({
|
||||||
|
saveCallback: requestSubmitMedia,
|
||||||
|
deleteCallback: requestDeleteMedia,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDropFiles = useCallback((_files) => {
|
||||||
|
setFiles(_files.filter((file) => file.uploaded === false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const savedMediaIds = useRef([]);
|
||||||
|
const clearSavedMediaIds = () => {
|
||||||
|
savedMediaIds.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (invoice && invoice.id) {
|
||||||
|
changePageTitle(formatMessage({ id: 'edit_invoice' }));
|
||||||
|
} else {
|
||||||
|
changePageTitle(formatMessage({ id: 'new_invoice' }));
|
||||||
|
}
|
||||||
|
}, [changePageTitle, invoice, formatMessage]);
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
customer_id: Yup.string()
|
||||||
|
.label(formatMessage({ id: 'customer_name_' }))
|
||||||
|
.required(),
|
||||||
|
invoice_date: Yup.date()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'invoice_date_' })),
|
||||||
|
due_date: Yup.date()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'due_date_' })),
|
||||||
|
invoice_no: Yup.number()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'invoice_no_' })),
|
||||||
|
reference_no: Yup.string().min(1).max(255),
|
||||||
|
status: Yup.string().required(),
|
||||||
|
invoice_message: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(1024)
|
||||||
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
terms_conditions: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(1024)
|
||||||
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
|
||||||
|
entries: Yup.array().of(
|
||||||
|
Yup.object().shape({
|
||||||
|
quantity: Yup.number().nullable(),
|
||||||
|
rate: Yup.number().nullable(),
|
||||||
|
item_id: Yup.number()
|
||||||
|
.nullable()
|
||||||
|
.when(['quantity', 'rate'], {
|
||||||
|
is: (quantity, rate) => quantity || rate,
|
||||||
|
then: Yup.number().required(),
|
||||||
|
}),
|
||||||
|
total: Yup.number().nullable(),
|
||||||
|
discount: Yup.number().nullable(),
|
||||||
|
description: Yup.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveInvokeSubmit = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
onFormSubmit && onFormSubmit(payload);
|
||||||
|
},
|
||||||
|
[onFormSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultInvoice = useMemo(
|
||||||
|
() => ({
|
||||||
|
index: 0,
|
||||||
|
item_id: null,
|
||||||
|
rate: null,
|
||||||
|
discount: null,
|
||||||
|
quantity: null,
|
||||||
|
description: '',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const defaultInitialValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
customer_id: '',
|
||||||
|
invoice_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
|
due_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
|
status: 'status',
|
||||||
|
invoice_no: '',
|
||||||
|
reference_no: '',
|
||||||
|
invoice_message: '',
|
||||||
|
terms_conditions: '',
|
||||||
|
entries: [...repeatValue(defaultInvoice, MIN_LINES_NUMBER)],
|
||||||
|
}),
|
||||||
|
[defaultInvoice],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderingIndex = (_invoice) => {
|
||||||
|
return _invoice.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
index: index + 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
...defaultInitialValues,
|
||||||
|
entries: orderingIndex(defaultInitialValues.entries),
|
||||||
|
}),
|
||||||
|
[defaultInvoice, defaultInitialValues, invoice],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialAttachmentFiles = useMemo(() => {
|
||||||
|
return invoice && invoice.media
|
||||||
|
? invoice.media.map((attach) => ({
|
||||||
|
preview: attach.attachment_file,
|
||||||
|
uploaded: true,
|
||||||
|
metadata: { ...attach },
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
}, [invoice]);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
enableReinitialize: true,
|
||||||
|
validationSchema,
|
||||||
|
initialValues: {
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
const entries = values.entries.map((item) => omit(item, ['total']));
|
||||||
|
|
||||||
|
const form = {
|
||||||
|
...values,
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
const saveInvoice = (mediaIds) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const requestForm = { ...form, media_ids: mediaIds };
|
||||||
|
|
||||||
|
requestSubmitInvoice(requestForm)
|
||||||
|
.then((response) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: formatMessage(
|
||||||
|
{ id: 'the_invocie_has_been_successfully_created' },
|
||||||
|
{ number: values.invoice_no },
|
||||||
|
),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
resetForm();
|
||||||
|
saveInvokeSubmit({ action: 'new', ...payload });
|
||||||
|
clearSavedMediaIds();
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all([saveMedia(), deleteMedia()])
|
||||||
|
.then(([savedMediaResponses]) => {
|
||||||
|
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
|
||||||
|
savedMediaIds.current = mediaIds;
|
||||||
|
return savedMediaResponses;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return saveInvoice(savedMediaIds.current);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const handleSubmitClick = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
setPaload(payload);
|
||||||
|
formik.submitForm();
|
||||||
|
},
|
||||||
|
[setPaload, formik],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancelClick = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
onCancelForm && onCancelForm(payload);
|
||||||
|
},
|
||||||
|
[onCancelForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(formik.errors, 'Errors');
|
||||||
|
const handleDeleteFile = useCallback(
|
||||||
|
(_deletedFiles) => {
|
||||||
|
_deletedFiles.forEach((deletedFile) => {
|
||||||
|
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||||
|
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDeletedFiles, deletedFiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickAddNewRow = () => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'entries',
|
||||||
|
orderingIndex([...formik.values.entries, defaultInvoice]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllLines = () => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'entries',
|
||||||
|
orderingIndex([...repeatValue(defaultInvoice, MIN_LINES_NUMBER)]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'invoice-form'}>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<InvoiceFormHeader formik={formik} />
|
||||||
|
<EstimatesItemsTable
|
||||||
|
entries={formik.values.entries}
|
||||||
|
onClickAddNewRow={handleClickAddNewRow}
|
||||||
|
onClickClearAllLines={handleClearAllLines}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'invoice_message'} />}
|
||||||
|
className={'form-group--customer_note'}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('invoice_message')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'terms_conditions'} />}
|
||||||
|
className={'form-group--terms_conditions'}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('terms_conditions')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InvoiceFormFooter
|
||||||
|
formik={formik}
|
||||||
|
onSubmitClick={handleSubmitClick}
|
||||||
|
onCancelClick={handleCancelClick}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withInvoiceActions,
|
||||||
|
withDashboardActions,
|
||||||
|
withMediaActions,
|
||||||
|
)(InvoiceForm);
|
||||||
41
client/src/containers/Sales/Invoice/InvoiceFormFooter.js
Normal file
41
client/src/containers/Sales/Invoice/InvoiceFormFooter.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Intent, Button } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
|
export default function EstimateFormFooter({
|
||||||
|
formik: { isSubmitting },
|
||||||
|
onSubmitClick,
|
||||||
|
onCancelClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={'estimate-form__floating-footer'}>
|
||||||
|
<Button disabled={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||||
|
<T id={'save_send'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
className={'ml1'}
|
||||||
|
name={'save'}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<T id={'save'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button className={'ml1'} disabled={isSubmitting}>
|
||||||
|
<T id={'clear'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={'ml1'}
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
onCancelClick && onCancelClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<T id={'close'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
client/src/containers/Sales/Invoice/InvoiceFormHeader.js
Normal file
177
client/src/containers/Sales/Invoice/InvoiceFormHeader.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
Intent,
|
||||||
|
Position,
|
||||||
|
MenuItem,
|
||||||
|
Classes,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
import { Row, Col } from 'react-grid-system';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { momentFormatter, compose, tansformDateValue } from 'utils';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { ListSelect, ErrorMessage, FieldRequiredHint, Hint } from 'components';
|
||||||
|
|
||||||
|
import withCustomers from 'containers/Customers/withCustomers';
|
||||||
|
|
||||||
|
function InvoiceFormHeader({
|
||||||
|
formik: { errors, touched, setFieldValue, getFieldProps, values },
|
||||||
|
|
||||||
|
//#withCustomers
|
||||||
|
customers,
|
||||||
|
}) {
|
||||||
|
const handleDateChange = useCallback(
|
||||||
|
(date_filed) => (date) => {
|
||||||
|
const formatted = moment(date).format('YYYY-MM-DD');
|
||||||
|
setFieldValue(date_filed, formatted);
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const CustomerRenderer = useCallback(
|
||||||
|
(cutomer, { handleClick }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={cutomer.id}
|
||||||
|
text={cutomer.display_name}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter Customer
|
||||||
|
const filterCustomer = (query, customer, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = customer.display_name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
`${customer.display_name} ${normalizedTitle}`.indexOf(
|
||||||
|
normalizedQuery,
|
||||||
|
) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle change customer
|
||||||
|
const onChangeCustomer = useCallback(
|
||||||
|
(filedName) => {
|
||||||
|
return (customer) => {
|
||||||
|
setFieldValue(filedName, customer.id);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'invoice-form'}>
|
||||||
|
<div className={'invoice__primary-section'}>
|
||||||
|
{/* customer name */}
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'customer_name'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name={'customer_id'} {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListSelect
|
||||||
|
items={customers}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
itemRenderer={CustomerRenderer}
|
||||||
|
itemPredicate={filterCustomer}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
onItemSelect={onChangeCustomer('customer_id')}
|
||||||
|
selectedItem={values.customer_id}
|
||||||
|
selectedItemProp={'id'}
|
||||||
|
defaultText={<T id={'select_customer_account'} />}
|
||||||
|
labelProp={'display_name'}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'invoice_date'} />}
|
||||||
|
inline={true}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
intent={
|
||||||
|
errors.invoice_date && touched.invoice_date && Intent.DANGER
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="invoice_date" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
value={tansformDateValue(values.invoice_date)}
|
||||||
|
onChange={handleDateChange('invoice_date')}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'due_date'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
intent={errors.due_date && touched.due_date && Intent.DANGER}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="due_date" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
value={tansformDateValue(values.due_date)}
|
||||||
|
onChange={handleDateChange('due_date')}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{/* invoice */}
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'invoice_no'} />}
|
||||||
|
inline={true}
|
||||||
|
className={('form-group--estimate', Classes.FILL)}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={errors.invoice_no && touched.invoice_no && Intent.DANGER}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="invoice_no" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={errors.invoice_no && touched.invoice_no && Intent.DANGER}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('invoice_no')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'reference'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--reference', Classes.FILL)}
|
||||||
|
intent={errors.reference_no && touched.reference_no && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={errors.reference_no && touched.reference_no && Intent.DANGER}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('reference_no')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withCustomers(({ customers }) => ({
|
||||||
|
customers,
|
||||||
|
})),
|
||||||
|
)(InvoiceFormHeader);
|
||||||
109
client/src/containers/Sales/Invoice/InvoiceList.js
Normal file
109
client/src/containers/Sales/Invoice/InvoiceList.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import React, { useEffect, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||||
|
import { useQuery, queryCache } from 'react-query';
|
||||||
|
import { Alert, Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import AppToaster from 'components/AppToaster';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
|
||||||
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
|
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
// import withInvoiceActions from './withInvoiceActions'
|
||||||
|
|
||||||
|
// import InvoiceActionsBar from './InvoiceActionsBar';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import InvoiceActionsBar from './InvoiceActionsBar';
|
||||||
|
|
||||||
|
function InvoiceList({
|
||||||
|
// #withDashboardActions
|
||||||
|
changePageTitle,
|
||||||
|
|
||||||
|
// #withViewsActions
|
||||||
|
|
||||||
|
//#withInvoice
|
||||||
|
|
||||||
|
//#withInvoiceActions
|
||||||
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [deleteInvoice, setDeleteInvoice] = useState(false);
|
||||||
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
changePageTitle(formatMessage({ id: 'invoice_list' }));
|
||||||
|
}, [changePageTitle, formatMessage]);
|
||||||
|
|
||||||
|
//handle dalete Invoice
|
||||||
|
const handleDeleteInvoice = useCallback(
|
||||||
|
(invoice) => {
|
||||||
|
setDeleteInvoice(invoice);
|
||||||
|
},
|
||||||
|
[setDeleteInvoice],
|
||||||
|
);
|
||||||
|
|
||||||
|
// handle cancel Invoice
|
||||||
|
const handleCancelInvoiceDelete = useCallback(() => {
|
||||||
|
setDeleteInvoice(false);
|
||||||
|
}, [setDeleteInvoice]);
|
||||||
|
|
||||||
|
// handleConfirm delete invoice
|
||||||
|
const handleConfirmInvoiceDelete = useCallback(() => {
|
||||||
|
requestDeleteInvoice(deleteInvoice.id).then(() => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: formatMessage({
|
||||||
|
id: 'the_invocie_has_been_successfully_deleted',
|
||||||
|
}),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setDeleteInvoice(false);
|
||||||
|
});
|
||||||
|
}, [setDeleteInvoice, requestDeleteInvoice]);
|
||||||
|
|
||||||
|
const handleEditInvoice = useCallback((invoice) => {
|
||||||
|
history.push(`/invoices/${invoice.id}/edit`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchInvoice = useQuery(['invoice-table'], () =>
|
||||||
|
requsetFetchInvoiceTable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFetchData = useCallback(
|
||||||
|
({ pageIndex, pageSize, sortBy }) => {
|
||||||
|
const page = pageIndex + 1;
|
||||||
|
|
||||||
|
addInvoiceTableQueries({
|
||||||
|
...(sortBy.length > 0
|
||||||
|
? {
|
||||||
|
column_sort_by: sortBy[0].id,
|
||||||
|
sort_order: sortBy[0].desc ? 'desc' : 'asc',
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
page_size: pageSize,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[addInvoiceTableQueries],
|
||||||
|
);
|
||||||
|
const handleSelectedRowsChange = useCallback((_invoice) => {
|
||||||
|
selectedRows(_invoice);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'invoices'}>
|
||||||
|
<InvoiceActionsBar
|
||||||
|
// onBulkDelete={}
|
||||||
|
selectedRows={selectedRows}
|
||||||
|
// onFilterChanged={}
|
||||||
|
/>
|
||||||
|
<DashboardPageContent>
|
||||||
|
<Switch>
|
||||||
|
<Route></Route>
|
||||||
|
</Switch>
|
||||||
|
</DashboardPageContent>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InvoiceList;
|
||||||
49
client/src/containers/Sales/Invoice/Invoices.js
Normal file
49
client/src/containers/Sales/Invoice/Invoices.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import InvoiceForm from './InvoiceForm';
|
||||||
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
|
|
||||||
|
import withCustomersActions from 'containers/Customers/withCustomersActions';
|
||||||
|
import withItemsActions from 'containers/Items/withItemsActions';
|
||||||
|
import withInvoiceActions from './withInvoiceActions';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
function Invoices({ requestFetchCustomers, requestFetchItems }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
// Handle fetch Items data table or list
|
||||||
|
const fetchItems = useQuery('items-table', () => requestFetchItems({}));
|
||||||
|
|
||||||
|
const handleFormSubmit = useCallback((payload) => {}, [history]);
|
||||||
|
|
||||||
|
// Handle fetch customers data table or list
|
||||||
|
const fetchCustomers = useQuery('customers-table', () =>
|
||||||
|
requestFetchCustomers({}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
history.goBack();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider
|
||||||
|
loading={fetchCustomers.isFetching || fetchItems.isFetching}
|
||||||
|
>
|
||||||
|
<InvoiceForm
|
||||||
|
onFormSubmit={handleFormSubmit}
|
||||||
|
// InvoiceId={id}
|
||||||
|
onCancelForm={handleCancel}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withInvoiceActions,
|
||||||
|
withCustomersActions,
|
||||||
|
withItemsActions,
|
||||||
|
)(Invoices);
|
||||||
189
client/src/containers/Sales/Invoice/InvoicesDataTable.js
Normal file
189
client/src/containers/Sales/Invoice/InvoicesDataTable.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import React, { useEffect, useCallback, useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Intent,
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
Popover,
|
||||||
|
Tooltip,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
MenuDivider,
|
||||||
|
Position,
|
||||||
|
Tag,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import Icon from 'components/Icon';
|
||||||
|
import { compose } from 'utils';
|
||||||
|
import { useUpdateEffect } from 'hooks';
|
||||||
|
|
||||||
|
import LoadingIndicator from 'components/LoadingIndicator';
|
||||||
|
import { If } from 'components';
|
||||||
|
import DataTable from 'components/DataTable';
|
||||||
|
|
||||||
|
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
import withViewDetails from 'containers/Views/withViewDetails';
|
||||||
|
|
||||||
|
import witInvoice from './withInvoice';
|
||||||
|
import withInvoiceActions from './withInvoiceActions';
|
||||||
|
|
||||||
|
function InvoicesDataTable({
|
||||||
|
//#withInvoices
|
||||||
|
|
||||||
|
// #withDashboardActions
|
||||||
|
changeCurrentView,
|
||||||
|
changePageSubtitle,
|
||||||
|
setTopbarEditView,
|
||||||
|
|
||||||
|
// #withView
|
||||||
|
viewMeta,
|
||||||
|
|
||||||
|
//#OwnProps
|
||||||
|
loading,
|
||||||
|
onFetchData,
|
||||||
|
onEditEstimate,
|
||||||
|
onDeleteEstimate,
|
||||||
|
onSelectedRowsChange,
|
||||||
|
}) {
|
||||||
|
const [initialMount, setInitialMount] = useState(false);
|
||||||
|
const { custom_view_id: customViewId } = useParams();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInitialMount(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customViewId) {
|
||||||
|
changeCurrentView(customViewId);
|
||||||
|
setTopbarEditView(customViewId);
|
||||||
|
}
|
||||||
|
changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
|
||||||
|
}, [
|
||||||
|
customViewId,
|
||||||
|
changeCurrentView,
|
||||||
|
changePageSubtitle,
|
||||||
|
setTopbarEditView,
|
||||||
|
viewMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleEditInvoice = useCallback(
|
||||||
|
(_invoice) => {
|
||||||
|
onEditInvoice && onEditInvoice(_invoice);
|
||||||
|
},
|
||||||
|
[onEditInvoice],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteInvoice = useCallback(() => {
|
||||||
|
onDeleteInvoice && onDeleteInvoice();
|
||||||
|
}, [onDeleteInvoice]);
|
||||||
|
|
||||||
|
const actionsMenuList = useCallback(
|
||||||
|
(invoice) => {
|
||||||
|
<Menu>
|
||||||
|
<MenuItem text={formatMessage({ id: 'view_details' })} />
|
||||||
|
<MenuDivider />
|
||||||
|
|
||||||
|
<MenuItem
|
||||||
|
text={formatMessage({ id: 'edit_invoice' })}
|
||||||
|
// onClick={handleEditInvoice(invoice)}
|
||||||
|
/>
|
||||||
|
</Menu>;
|
||||||
|
},
|
||||||
|
[handleDeleteInvoice, handleEditInvoice, formatMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '',
|
||||||
|
Header: formatMessage({ id: '' }),
|
||||||
|
accessor: '',
|
||||||
|
className: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actions',
|
||||||
|
Header: '',
|
||||||
|
Cell: ({ cell }) => (
|
||||||
|
<Popover
|
||||||
|
content={actionMenuList(cell.row.original)}
|
||||||
|
position={Position.RIGHT_BOTTOM}
|
||||||
|
>
|
||||||
|
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
|
||||||
|
</Popover>
|
||||||
|
),
|
||||||
|
className: 'actions',
|
||||||
|
width: 50,
|
||||||
|
disableResizing: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[actionMenuList, formatMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDataTableFetchData = useCallback(
|
||||||
|
(...arguments) => {
|
||||||
|
onFetchData && onFetchData(...arguments);
|
||||||
|
},
|
||||||
|
[onFetchData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedRowsChange = useCallback(
|
||||||
|
(selectedRows) => {
|
||||||
|
onSelectedRowsChange &&
|
||||||
|
onSelectedRowsChange(selectedRows.map((s) => s.original));
|
||||||
|
},
|
||||||
|
[onSelectedRowsChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoadingIndicator>
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={[]}
|
||||||
|
onFetchData={handleDataTableFetchData}
|
||||||
|
manualSortBy={true}
|
||||||
|
/>
|
||||||
|
</LoadingIndicator>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withRouter,
|
||||||
|
withDialogActions,
|
||||||
|
withDashboardActions,
|
||||||
|
withInvoiceActions,
|
||||||
|
// withInvoices(({})=>({
|
||||||
|
|
||||||
|
// }))
|
||||||
|
withViewDetails(),
|
||||||
|
)(InvoicesDataTable);
|
||||||
31
client/src/containers/Sales/Invoice/withInvoiceActions.js
Normal file
31
client/src/containers/Sales/Invoice/withInvoiceActions.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
submitInvoice,
|
||||||
|
editInvoice,
|
||||||
|
deleteInvoice,
|
||||||
|
fetchInvoice,
|
||||||
|
fetchInvoicesTable,
|
||||||
|
} from 'store/Invoice/invoices.actions';
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
const mapDipatchToProps = (dispatch) => ({
|
||||||
|
requestSubmitInvoice: (form) => dispatch(submitInvoice({ form })),
|
||||||
|
requsetFetchInvoice: (id) => dispatch(fetchInvoice({ id })),
|
||||||
|
requestEditInvoice: (id, form) => dispatch(editInvoice({ id, form })),
|
||||||
|
requestFetchInvoiceTable: (query = {}) =>
|
||||||
|
dispatch(fetchInvoicesTable({ query: { ...query } })),
|
||||||
|
requestDeleteInvoice: (id) => dispatch(deleteInvoice({ id })),
|
||||||
|
|
||||||
|
changeInvoiceView: (id) =>
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_SET_CURREMT_VIEW,
|
||||||
|
currentViewId: parseInt(id, 10),
|
||||||
|
}),
|
||||||
|
addInvoiceTableQueries: (_queries) =>
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_TABLE_QUERIES_ADD,
|
||||||
|
_queries,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDipatchToProps);
|
||||||
11
client/src/containers/Sales/Invoice/withInvoiceDetail.js
Normal file
11
client/src/containers/Sales/Invoice/withInvoiceDetail.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { getInvoiceById } from 'store/Invoice/invoices.selector';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const getInvoiceById = getInvoiceById();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
invoice: getInvoiceById(state, props),
|
||||||
|
});
|
||||||
|
return connect(mapStateToProps);
|
||||||
|
};
|
||||||
325
client/src/containers/Sales/Receipt/ReceiptForm.js
Normal file
325
client/src/containers/Sales/Receipt/ReceiptForm.js
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Intent, FormGroup, TextArea } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
|
import ReceiptFromHeader from './ReceiptFormHeader';
|
||||||
|
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
|
||||||
|
import ReceiptFormFooter from './ReceiptFormFooter';
|
||||||
|
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
import withMediaActions from 'containers/Media/withMediaActions';
|
||||||
|
import withReceipActions from './withReceipActions';
|
||||||
|
|
||||||
|
import { AppToaster } from 'components';
|
||||||
|
import Dragzone from 'components/Dragzone';
|
||||||
|
import useMedia from 'hooks/useMedia';
|
||||||
|
|
||||||
|
import { compose, repeatValue } from 'utils';
|
||||||
|
|
||||||
|
const MIN_LINES_NUMBER = 4;
|
||||||
|
|
||||||
|
function ReceiptForm({
|
||||||
|
//#withMedia
|
||||||
|
requestSubmitMedia,
|
||||||
|
requestDeleteMedia,
|
||||||
|
|
||||||
|
//#withReceiptActions
|
||||||
|
requestSubmitReceipt,
|
||||||
|
|
||||||
|
//#withDashboard
|
||||||
|
changePageTitle,
|
||||||
|
changePageSubtitle,
|
||||||
|
|
||||||
|
//#withReceiptDetail
|
||||||
|
receipt,
|
||||||
|
|
||||||
|
//#own Props
|
||||||
|
receiptId,
|
||||||
|
onFormSubmit,
|
||||||
|
onCancelForm,
|
||||||
|
}) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [payload, setPaload] = useState({});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setFiles,
|
||||||
|
saveMedia,
|
||||||
|
deletedFiles,
|
||||||
|
setDeletedFiles,
|
||||||
|
deleteMedia,
|
||||||
|
} = useMedia({
|
||||||
|
saveCallback: requestSubmitMedia,
|
||||||
|
deleteCallback: requestDeleteMedia,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDropFiles = useCallback((_files) => {
|
||||||
|
setFiles(_files.filter((file) => file.uploaded === false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const savedMediaIds = useRef([]);
|
||||||
|
const clearSavedMediaIds = () => {
|
||||||
|
savedMediaIds.current = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (receipt && receipt.id) {
|
||||||
|
changePageTitle(formatMessage({ id: 'edit_receipt' }));
|
||||||
|
} else {
|
||||||
|
changePageTitle(formatMessage({ id: 'new_receipt' }));
|
||||||
|
}
|
||||||
|
}, [changePageTitle, receipt, formatMessage]);
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
customer_id: Yup.string()
|
||||||
|
.label(formatMessage({ id: 'customer_name_' }))
|
||||||
|
.required(),
|
||||||
|
receipt_date: Yup.date()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'receipt_date_' })),
|
||||||
|
receipt_no: Yup.number()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'receipt_no_' })),
|
||||||
|
deposit_account_id: Yup.number()
|
||||||
|
.required()
|
||||||
|
.label(formatMessage({ id: 'deposit_account_' })),
|
||||||
|
reference_no: Yup.string().min(1).max(255),
|
||||||
|
receipt_message: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(1024)
|
||||||
|
.label(formatMessage({ id: 'receipt_message_' })),
|
||||||
|
send_to_email: Yup.string().email(),
|
||||||
|
statement: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(1024)
|
||||||
|
.label(formatMessage({ id: 'note' })),
|
||||||
|
|
||||||
|
entries: Yup.array().of(
|
||||||
|
Yup.object().shape({
|
||||||
|
quantity: Yup.number().nullable(),
|
||||||
|
rate: Yup.number().nullable(),
|
||||||
|
item_id: Yup.number()
|
||||||
|
.nullable()
|
||||||
|
.when(['quantity', 'rate'], {
|
||||||
|
is: (quantity, rate) => quantity || rate,
|
||||||
|
then: Yup.number().required(),
|
||||||
|
}),
|
||||||
|
discount: Yup.number().nullable(),
|
||||||
|
description: Yup.string().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const saveReceiptSubmit = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
onFormSubmit && onFormSubmit(payload);
|
||||||
|
},
|
||||||
|
[onFormSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultReceipt = useMemo(
|
||||||
|
() => ({
|
||||||
|
index: 0,
|
||||||
|
item_id: null,
|
||||||
|
rate: null,
|
||||||
|
discount: null,
|
||||||
|
quantity: null,
|
||||||
|
description: '',
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultInitialValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
customer_id: '',
|
||||||
|
deposit_account_id: '',
|
||||||
|
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
|
||||||
|
send_to_email: '',
|
||||||
|
reference_no: '',
|
||||||
|
receipt_message: '',
|
||||||
|
statement: '',
|
||||||
|
entries: [...repeatValue(defaultReceipt, MIN_LINES_NUMBER)],
|
||||||
|
}),
|
||||||
|
[defaultReceipt],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderingIndex = (_receipt) => {
|
||||||
|
return _receipt.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
index: index + 1,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialValues = useMemo(
|
||||||
|
() => ({
|
||||||
|
...defaultInitialValues,
|
||||||
|
entries: orderingIndex(defaultInitialValues.entries),
|
||||||
|
}),
|
||||||
|
[defaultReceipt, defaultInitialValues, receipt],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialAttachmentFiles = useMemo(() => {
|
||||||
|
return receipt && receipt.media
|
||||||
|
? receipt.media.map((attach) => ({
|
||||||
|
preview: attach.attachment_file,
|
||||||
|
uploaded: true,
|
||||||
|
metadata: { ...attach },
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
}, [receipt]);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
enableReinitialize: true,
|
||||||
|
validationSchema,
|
||||||
|
initialValues: {
|
||||||
|
...initialValues,
|
||||||
|
},
|
||||||
|
onSubmit: async (values, { setErrors, setSubmitting, resetForm }) => {
|
||||||
|
const entries = values.entries.map(
|
||||||
|
({ item_id, quantity, rate, description }) => ({
|
||||||
|
item_id,
|
||||||
|
quantity,
|
||||||
|
rate,
|
||||||
|
description,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const form = {
|
||||||
|
...values,
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveReceipt = (mediaIds) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const requestForm = { ...form, media_ids: mediaIds };
|
||||||
|
|
||||||
|
requestSubmitReceipt(requestForm)
|
||||||
|
.then((resposne) => {
|
||||||
|
AppToaster.show({
|
||||||
|
message: formatMessage({
|
||||||
|
id: 'the_receipt_has_been_successfully_created',
|
||||||
|
}),
|
||||||
|
intent: Intent.SUCCESS,
|
||||||
|
});
|
||||||
|
setSubmitting(false);
|
||||||
|
resetForm();
|
||||||
|
saveReceiptSubmit({ action: 'new', ...payload });
|
||||||
|
clearSavedMediaIds();
|
||||||
|
})
|
||||||
|
.catch((errors) => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Promise.all([saveMedia(), deleteMedia()])
|
||||||
|
.then(([savedMediaResponses]) => {
|
||||||
|
const mediaIds = savedMediaResponses.map((res) => res.data.media.id);
|
||||||
|
savedMediaIds.current = mediaIds;
|
||||||
|
return savedMediaResponses;
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return saveReceipt(saveReceipt.current);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteFile = useCallback(
|
||||||
|
(_deletedFiles) => {
|
||||||
|
_deletedFiles.forEach((deletedFile) => {
|
||||||
|
if (deletedFile.uploaded && deletedFile.metadata.id) {
|
||||||
|
setDeletedFiles([...deletedFiles, deletedFile.metadata.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDeletedFiles, deletedFiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmitClick = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
setPaload(payload);
|
||||||
|
formik.submitForm();
|
||||||
|
},
|
||||||
|
[setPaload, formik],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCancelClick = useCallback(
|
||||||
|
(payload) => {
|
||||||
|
onCancelForm && onCancelForm(payload);
|
||||||
|
},
|
||||||
|
[onCancelForm],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClickAddNewRow = () => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'entries',
|
||||||
|
orderingIndex([...formik.values.entries, defaultReceipt]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllLines = () => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
'entries',
|
||||||
|
orderingIndex([...repeatValue(defaultReceipt, MIN_LINES_NUMBER)]),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'receipt-form'}>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<ReceiptFromHeader formik={formik} />
|
||||||
|
|
||||||
|
<EstimatesItemsTable
|
||||||
|
entries={formik.values.entries}
|
||||||
|
onClickAddNewRow={handleClickAddNewRow}
|
||||||
|
onClickClearAllLines={handleClearAllLines}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'receipt_message'} />}
|
||||||
|
className={'form-group--'}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('receipt_message')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'statement'} />}
|
||||||
|
className={'form-group--statement'}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
growVertically={true}
|
||||||
|
{...formik.getFieldProps('statement')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<Dragzone
|
||||||
|
initialFiles={initialAttachmentFiles}
|
||||||
|
onDrop={handleDropFiles}
|
||||||
|
onDeleteFile={handleDeleteFile}
|
||||||
|
hint={'Attachments: Maxiumum size: 20MB'}
|
||||||
|
/>
|
||||||
|
<ReceiptFormFooter
|
||||||
|
formik={formik}
|
||||||
|
onSubmit={handleSubmitClick}
|
||||||
|
onCancelForm={handleCancelClick}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withReceipActions,
|
||||||
|
withDashboardActions,
|
||||||
|
withMediaActions,
|
||||||
|
)(ReceiptForm);
|
||||||
41
client/src/containers/Sales/Receipt/ReceiptFormFooter.js
Normal file
41
client/src/containers/Sales/Receipt/ReceiptFormFooter.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Intent, Button } from '@blueprintjs/core';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
|
export default function ReceiptFormFooter({
|
||||||
|
formik: { isSubmitting },
|
||||||
|
onSubmitClick,
|
||||||
|
onCancelClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={'estimate-form__floating-footer'}>
|
||||||
|
<Button disabled={isSubmitting} intent={Intent.PRIMARY} type="submit">
|
||||||
|
<T id={'save_send'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
className={'ml1'}
|
||||||
|
name={'save'}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<T id={'save'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button className={'ml1'} disabled={isSubmitting}>
|
||||||
|
<T id={'clear'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={'ml1'}
|
||||||
|
type="submit"
|
||||||
|
onClick={() => {
|
||||||
|
onCancelClick && onCancelClick();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<T id={'close'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
214
client/src/containers/Sales/Receipt/ReceiptFormHeader.js
Normal file
214
client/src/containers/Sales/Receipt/ReceiptFormHeader.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
import React, { useMemo, useCallback, useState } from 'react';
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
InputGroup,
|
||||||
|
Intent,
|
||||||
|
Position,
|
||||||
|
MenuItem,
|
||||||
|
Classes,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
|
||||||
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { momentFormatter, compose, tansformDateValue } from 'utils';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
AccountsSelectList,
|
||||||
|
ListSelect,
|
||||||
|
ErrorMessage,
|
||||||
|
FieldRequiredHint,
|
||||||
|
Hint,
|
||||||
|
} from 'components';
|
||||||
|
|
||||||
|
import withCustomers from 'containers/Customers/withCustomers';
|
||||||
|
import withAccounts from 'containers/Accounts/withAccounts';
|
||||||
|
|
||||||
|
function ReceiptFormHeader({
|
||||||
|
formik: { errors, touched, setFieldValue, getFieldProps, values },
|
||||||
|
|
||||||
|
//#withCustomers
|
||||||
|
customers,
|
||||||
|
//#withAccouts
|
||||||
|
accountsList,
|
||||||
|
}) {
|
||||||
|
const handleDateChange = useCallback(
|
||||||
|
(date) => {
|
||||||
|
const formatted = moment(date).format('YYYY-MM-DD');
|
||||||
|
setFieldValue('receipt_date', formatted);
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const CustomerRenderer = useCallback(
|
||||||
|
(cutomer, { handleClick }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={cutomer.id}
|
||||||
|
text={cutomer.display_name}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter Customer
|
||||||
|
const filterCustomer = (query, customer, _index, exactMatch) => {
|
||||||
|
const normalizedTitle = customer.display_name.toLowerCase();
|
||||||
|
const normalizedQuery = query.toLowerCase();
|
||||||
|
if (exactMatch) {
|
||||||
|
return normalizedTitle === normalizedQuery;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
`${customer.display_name} ${normalizedTitle}`.indexOf(
|
||||||
|
normalizedQuery,
|
||||||
|
) >= 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle change
|
||||||
|
const onChangeSelect = useCallback(
|
||||||
|
(filedName) => {
|
||||||
|
return (item) => {
|
||||||
|
setFieldValue(filedName, item.id);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[setFieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter deposit accounts.
|
||||||
|
const depositAccounts = useMemo(
|
||||||
|
() => accountsList.filter((a) => a?.type?.key === 'current_asset'),
|
||||||
|
[accountsList],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{/* customer name */}
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'customer_name'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={errors.customer_id && touched.customer_id && Intent.DANGER}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name={'customer_id'} {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListSelect
|
||||||
|
items={customers}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
itemRenderer={CustomerRenderer}
|
||||||
|
itemPredicate={filterCustomer}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
onItemSelect={onChangeSelect('customer_id')}
|
||||||
|
selectedItem={values.customer_id}
|
||||||
|
selectedItemProp={'id'}
|
||||||
|
defaultText={<T id={'select_customer_account'} />}
|
||||||
|
labelProp={'display_name'}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'deposit_account'} />}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--deposit_account_id',
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
inline={true}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={
|
||||||
|
errors.deposit_account_id &&
|
||||||
|
touched.deposit_account_id &&
|
||||||
|
Intent.DANGER
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage
|
||||||
|
name={'deposit_account_id'}
|
||||||
|
{...{ errors, touched }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AccountsSelectList
|
||||||
|
accounts={depositAccounts}
|
||||||
|
onAccountSelected={onChangeSelect('deposit_account_id')}
|
||||||
|
defaultSelectText={<T id={'select_deposit_account'} />}
|
||||||
|
selectedAccountId={values.deposit_account_id}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'receipt_date'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
intent={errors.receipt_date && touched.receipt_date && Intent.DANGER}
|
||||||
|
helperText={
|
||||||
|
<ErrorMessage name="receipt_date" {...{ errors, touched }} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DateInput
|
||||||
|
{...momentFormatter('YYYY/MM/DD')}
|
||||||
|
value={tansformDateValue(values.receipt_date)}
|
||||||
|
onChange={handleDateChange}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
{/* receipt_no */}
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'receipt'} />}
|
||||||
|
inline={true}
|
||||||
|
className={('form-group--receipt_no', Classes.FILL)}
|
||||||
|
labelInfo={<FieldRequiredHint />}
|
||||||
|
intent={errors.receipt_no && touched.receipt_no && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name="receipt_no" {...{ errors, touched }} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={errors.receipt_no && touched.receipt_no && Intent.DANGER}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('receipt_no')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'reference'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--reference', Classes.FILL)}
|
||||||
|
intent={errors.reference_no && touched.reference_no && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={errors.reference_no && touched.reference_no && Intent.DANGER}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('reference_no')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'send_to_email'} />}
|
||||||
|
inline={true}
|
||||||
|
className={classNames('form-group--send_to_email', Classes.FILL)}
|
||||||
|
intent={errors.send_to_email && touched.send_to_email && Intent.DANGER}
|
||||||
|
helperText={<ErrorMessage name="reference" {...{ errors, touched }} />}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
intent={
|
||||||
|
errors.send_to_email && touched.send_to_email && Intent.DANGER
|
||||||
|
}
|
||||||
|
minimal={true}
|
||||||
|
{...getFieldProps('send_to_email')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withCustomers(({ customers }) => ({
|
||||||
|
customers,
|
||||||
|
})),
|
||||||
|
withAccounts(({ accountsList }) => ({
|
||||||
|
accountsList,
|
||||||
|
})),
|
||||||
|
)(ReceiptFormHeader);
|
||||||
65
client/src/containers/Sales/Receipt/Receipts.js
Normal file
65
client/src/containers/Sales/Receipt/Receipts.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
|
import ReceiptFrom from './ReceiptForm';
|
||||||
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
|
|
||||||
|
import withCustomersActions from 'containers/Customers/withCustomersActions';
|
||||||
|
import withAccountsActions from 'containers/Accounts/withAccountsActions';
|
||||||
|
import withItemsActions from 'containers/Items/withItemsActions';
|
||||||
|
|
||||||
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
function Receipts({
|
||||||
|
//#withwithAccountsActions
|
||||||
|
requestFetchAccounts,
|
||||||
|
|
||||||
|
//#withCustomersActions
|
||||||
|
requestFetchCustomers,
|
||||||
|
|
||||||
|
//#withItemsActions
|
||||||
|
requestFetchItems,
|
||||||
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const fetchAccounts = useQuery('accounts-list', (key) =>
|
||||||
|
requestFetchAccounts(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchCustomers = useQuery('customers-table', () =>
|
||||||
|
requestFetchCustomers({}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle fetch Items data table or list
|
||||||
|
const fetchItems = useQuery('items-table', () => requestFetchItems({}));
|
||||||
|
|
||||||
|
const handleFormSubmit = useCallback((payload) => {}, [history]);
|
||||||
|
|
||||||
|
const handleCancel = useCallback(() => {
|
||||||
|
history.goBack();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardInsider
|
||||||
|
loading={
|
||||||
|
fetchCustomers.isFetching ||
|
||||||
|
fetchItems.isFetching ||
|
||||||
|
fetchAccounts.isFetching
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ReceiptFrom
|
||||||
|
onFormSubmit={handleFormSubmit}
|
||||||
|
// ReceiptId={id}
|
||||||
|
onCancelForm={handleCancel}
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withCustomersActions,
|
||||||
|
withItemsActions,
|
||||||
|
withAccountsActions,
|
||||||
|
)(Receipts);
|
||||||
33
client/src/containers/Sales/Receipt/withReceipActions.js
Normal file
33
client/src/containers/Sales/Receipt/withReceipActions.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
submitReceipt,
|
||||||
|
deleteReceipt,
|
||||||
|
fetchReceipt,
|
||||||
|
fetchReceiptsTable,
|
||||||
|
editReceipt,
|
||||||
|
} from 'store/receipt/receipt.actions';
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
requestSubmitReceipt: (form) => dispatch(submitReceipt({ form })),
|
||||||
|
requestFetchReceipt: (id) => dispatch(fetchReceipt({ id })),
|
||||||
|
requestEditTeceipt: (id, form) => dispatch(editReceipt({ id, form })),
|
||||||
|
requestDeleteReceipt: (id) => dispatch(deleteReceipt({ id })),
|
||||||
|
requestFetchReceiptsTable: (query = {}) =>
|
||||||
|
dispatch(fetchReceiptsTable({ query: { ...query } })),
|
||||||
|
// requestDeleteBulkReceipt: (ids) => dispatch(deleteBulkReceipt({ ids })),
|
||||||
|
|
||||||
|
changeReceiptView: (id) =>
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPT_SET_CURRENT_VIEW,
|
||||||
|
currentViewId: parseInt(id, 10),
|
||||||
|
}),
|
||||||
|
|
||||||
|
addReceiptsTableQueries: (queries) =>
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPTS_TABLE_QUERIES_ADD,
|
||||||
|
queries,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps);
|
||||||
@@ -528,14 +528,20 @@ export default {
|
|||||||
logic_expression: 'logic expression',
|
logic_expression: 'logic expression',
|
||||||
assign_to_customer: 'Assign to Customer',
|
assign_to_customer: 'Assign to Customer',
|
||||||
inactive: 'Inactive',
|
inactive: 'Inactive',
|
||||||
should_select_customers_with_entries_have_receivable_account: 'Should select customers with entries that have receivable account.',
|
should_select_customers_with_entries_have_receivable_account:
|
||||||
should_select_vendors_with_entries_have_payable_account: 'Should select vendors with entries that have payable account.',
|
'Should select customers with entries that have receivable account.',
|
||||||
vendors_should_selected_with_payable_account_only: 'Vendors contacts should selected with payable account only.',
|
should_select_vendors_with_entries_have_payable_account:
|
||||||
customers_should_selected_with_receivable_account_only: 'Customers contacts should selected with receivable account only.',
|
'Should select vendors with entries that have payable account.',
|
||||||
|
vendors_should_selected_with_payable_account_only:
|
||||||
|
'Vendors contacts should selected with payable account only.',
|
||||||
|
customers_should_selected_with_receivable_account_only:
|
||||||
|
'Customers contacts should selected with receivable account only.',
|
||||||
amount_cannot_be_zero_or_empty: 'Amount cannot be zero or empty.',
|
amount_cannot_be_zero_or_empty: 'Amount cannot be zero or empty.',
|
||||||
should_total_of_credit_and_debit_be_equal: 'Should total of credit and debit be equal.',
|
should_total_of_credit_and_debit_be_equal:
|
||||||
|
'Should total of credit and debit be equal.',
|
||||||
no_accounts: 'No Accounts',
|
no_accounts: 'No Accounts',
|
||||||
the_accounts_have_been_successfully_inactivated: 'The accounts have been successfully inactivated.',
|
the_accounts_have_been_successfully_inactivated:
|
||||||
|
'The accounts have been successfully inactivated.',
|
||||||
account_code_is_not_unique: 'Account code is not unqiue.',
|
account_code_is_not_unique: 'Account code is not unqiue.',
|
||||||
are_sure_to_publish_this_expense:
|
are_sure_to_publish_this_expense:
|
||||||
'Are you sure you want to publish this expense?',
|
'Are you sure you want to publish this expense?',
|
||||||
@@ -546,10 +552,85 @@ export default {
|
|||||||
|
|
||||||
accounts_without_zero_balance: 'Accounts without zero-balance',
|
accounts_without_zero_balance: 'Accounts without zero-balance',
|
||||||
accounts_with_transactions: 'Accounts with transactions',
|
accounts_with_transactions: 'Accounts with transactions',
|
||||||
include_accounts_once_has_transactions_on_given_date_period: 'Include accounts that onces have transactions on the given date period only.',
|
include_accounts_once_has_transactions_on_given_date_period:
|
||||||
include_accounts_and_exclude_zero_balance: 'Include accounts and exclude that ones have zero-balance.',
|
'Include accounts that onces have transactions on the given date period only.',
|
||||||
all_accounts_including_with_zero_balance: 'All accounts, including that ones have zero-balance.',
|
include_accounts_and_exclude_zero_balance:
|
||||||
|
'Include accounts and exclude that ones have zero-balance.',
|
||||||
|
all_accounts_including_with_zero_balance:
|
||||||
|
'All accounts, including that ones have zero-balance.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
you_could_not_delete_account_has_child_accounts: 'You could not delete account has child accounts.',
|
you_could_not_delete_account_has_child_accounts:
|
||||||
journal_entry: 'Journal Entry'
|
'You could not delete account has child accounts.',
|
||||||
|
journal_entry: 'Journal Entry',
|
||||||
|
|
||||||
|
estimate: 'Estimate #',
|
||||||
|
estimate_date: 'Estimate Date',
|
||||||
|
expiration_date: 'Expiration Date',
|
||||||
|
customer_note: 'Customer Note',
|
||||||
|
select_customer_account: 'Select Customer Account',
|
||||||
|
select_product: 'Select Product',
|
||||||
|
reference: 'Reference #',
|
||||||
|
clear: 'Clear',
|
||||||
|
save_send: 'Save & Send',
|
||||||
|
estimates: 'Estimates',
|
||||||
|
edit_estimate: 'Edit Estimate',
|
||||||
|
delete_estimate: 'Delete Estimate',
|
||||||
|
new_estimate: 'New Estimate',
|
||||||
|
customer_name_: 'Customer name',
|
||||||
|
estimate_date_: 'Estismate date',
|
||||||
|
expiration_date_: 'Expiration date',
|
||||||
|
estimate_number_: 'Estimate number',
|
||||||
|
discount: 'Discount %',
|
||||||
|
quantity: 'Quantity',
|
||||||
|
rate: 'Rate',
|
||||||
|
estimate_list: 'Estimate List',
|
||||||
|
|
||||||
|
product_and_service: 'Product/Service',
|
||||||
|
the_estimate_has_been_successfully_edited:
|
||||||
|
'The estimate #{number} has been successfully edited.',
|
||||||
|
the_estimate_has_been_successfully_created:
|
||||||
|
'The estimate #{number} has been successfully created.',
|
||||||
|
the_estimate_has_been_successfully_deleted:
|
||||||
|
'The estimate has been successfully deleted.',
|
||||||
|
cannot_be_zero_or_empty: 'cannot be zero or empty.',
|
||||||
|
invocies: 'Invoices',
|
||||||
|
invoice_date: 'Invoice Date',
|
||||||
|
due_date: 'Due Date',
|
||||||
|
invoice_date_: 'Invoice date',
|
||||||
|
invoice_no: 'Invoice #',
|
||||||
|
invoice_no_: 'Invoice number',
|
||||||
|
due_date_: 'Due date',
|
||||||
|
invoice_message: 'Invoice Message',
|
||||||
|
|
||||||
|
edit_invoice: 'Edit Invoice',
|
||||||
|
delete_invoice: 'Delete Invoice',
|
||||||
|
new_invoice: 'New Invoice',
|
||||||
|
invoice_list: 'Invoice List',
|
||||||
|
|
||||||
|
the_invoice_has_been_successfully_edited:
|
||||||
|
'The invoice #{number} has been successfully edited.',
|
||||||
|
the_invocie_has_been_successfully_created:
|
||||||
|
'The invoice #{number} has been successfully created.',
|
||||||
|
the_invocie_has_been_successfully_deleted:
|
||||||
|
'The invoice has been successfully deleted.',
|
||||||
|
receipts: 'Receipts',
|
||||||
|
receipt: 'Receipt #',
|
||||||
|
receipt_date_: 'Receipt date',
|
||||||
|
receipt_date: 'Receipt Date',
|
||||||
|
deposit_account_: 'Deposit account',
|
||||||
|
receipt_message_: 'Receipt message',
|
||||||
|
receipt_no_: 'receipt number',
|
||||||
|
edit_receipt: 'Edit Receipt',
|
||||||
|
new_receipt: 'New Receipt',
|
||||||
|
receipt_message: 'Receipt Message',
|
||||||
|
statement: 'Statement',
|
||||||
|
deposit_account: 'Deposit Account',
|
||||||
|
send_to_email: 'Send to email',
|
||||||
|
select_deposit_account: 'Select Deposit Account',
|
||||||
|
the_receipt_has_been_successfully_created:
|
||||||
|
'The recepit has been successfully created.',
|
||||||
|
the_receipt_has_been_successfully_edited:
|
||||||
|
'The receipt has been successfully edited.',
|
||||||
|
the_receipt_has_been_successfully_deleted:
|
||||||
|
'The receipt has been successfully deleted.',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -204,4 +204,75 @@ export default [
|
|||||||
}),
|
}),
|
||||||
breadcrumb: 'Customers',
|
breadcrumb: 'Customers',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//Estimates
|
||||||
|
{
|
||||||
|
path: `/estimates/:id/edit`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('containers/Sales/Estimate/Estimates'),
|
||||||
|
}),
|
||||||
|
breadcrumb: 'Edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `/estimates/new`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('containers/Sales/Estimate/Estimates'),
|
||||||
|
}),
|
||||||
|
breadcrumb: 'New Estimates',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: `/estimates`,
|
||||||
|
// component: LazyLoader({
|
||||||
|
// loader: () => import('containers/Sales/EstimatesList'),
|
||||||
|
// }),
|
||||||
|
// breadcrumb: 'Estimates',
|
||||||
|
// },
|
||||||
|
|
||||||
|
//Invoices
|
||||||
|
|
||||||
|
{
|
||||||
|
path: `/invoices/:id/edit`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('containers/Sales/Invoice/Invoices'),
|
||||||
|
}),
|
||||||
|
breadcrumb: 'Edit',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: `/invoices/new`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('containers/Sales/Invoice/Invoices'),
|
||||||
|
}),
|
||||||
|
breadcrumb: 'New Invoice',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path:`/invoices`,
|
||||||
|
// component:LazyLoader({
|
||||||
|
// loader:()=>import('containers/Sales/Invoice/Invoices')
|
||||||
|
// }),
|
||||||
|
// breadcrumb: 'New Invoice',
|
||||||
|
// },
|
||||||
|
|
||||||
|
//Receipts
|
||||||
|
{
|
||||||
|
path: `/receipts/:id/edit`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('containers/Sales/Receipt/Receipts'),
|
||||||
|
}),
|
||||||
|
breadcrumb: 'Edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `/receipts/new`,
|
||||||
|
component: LazyLoader({
|
||||||
|
loader: () => import('containers/Sales/Receipt/Receipts'),
|
||||||
|
}),
|
||||||
|
breadcrumb: 'New Receipt',
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: `/receipts`,
|
||||||
|
// component: LazyLoader({
|
||||||
|
// loader: () => import('containers/Sales/Receipt/Receipts'),
|
||||||
|
// }),
|
||||||
|
// breadcrumb: 'New Receipt',
|
||||||
|
// }
|
||||||
];
|
];
|
||||||
|
|||||||
138
client/src/store/Estimate/estimates.actions.js
Normal file
138
client/src/store/Estimate/estimates.actions.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import ApiService from 'services/ApiService';
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
export const submitEstimate = ({ form }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
|
});
|
||||||
|
ApiService.post('sales/estimates', form)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editEstimate = (id, form) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
|
});
|
||||||
|
ApiService.post(`estimates/${id}`, form)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteEstimate = ({ id }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resovle, reject) => {
|
||||||
|
ApiService.delete(`estimates/${id}`)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({ type: t.ESTIMATE_DELETE });
|
||||||
|
resovle(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error.response.data.errors || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchEstimate = ({ id }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resovle, reject) => {
|
||||||
|
ApiService.get(`estimate/${id}`)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATE_SET,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
estimate: response.data.estimate,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resovle(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchEstimatesTable = ({ query = {} }) => {
|
||||||
|
return (dispatch, getState) =>
|
||||||
|
new Promise((resolve, rejcet) => {
|
||||||
|
const pageQuery = getState().estimates.tableQuery;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_TABLE_LOADING,
|
||||||
|
payload: {
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ApiService.get('estimates', {
|
||||||
|
params: { ...pageQuery, ...query },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_PAGE_SET,
|
||||||
|
payload: {
|
||||||
|
estimates: response.data.estimates.results,
|
||||||
|
pagination: response.data.estimates.pagination,
|
||||||
|
customViewId: response.data.customViewId || -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_ITEMS_SET,
|
||||||
|
payload: {
|
||||||
|
estimates: response.data.estimates.results,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_PAGINATION_SET,
|
||||||
|
payload: {
|
||||||
|
pagination: response.data.estimates.pagination,
|
||||||
|
customViewId: response.data.customViewId || -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.ESTIMATES_TABLE_LOADING,
|
||||||
|
payload: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
rejcet(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
105
client/src/store/Estimate/estimates.reducer.js
Normal file
105
client/src/store/Estimate/estimates.reducer.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { createTableQueryReducers } from 'store/queryReducers';
|
||||||
|
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
items: {},
|
||||||
|
views: {},
|
||||||
|
loading: false,
|
||||||
|
tableQuery: {
|
||||||
|
page_size: 12,
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
currentViewId: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultEstimate = {
|
||||||
|
products: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = createReducer(initialState, {
|
||||||
|
[t.ESTIMATE_SET]: (state, action) => {
|
||||||
|
const { id, estiamate } = action.payload;
|
||||||
|
const _estimate = state.items[id] || {};
|
||||||
|
|
||||||
|
state.items[id] = { ...defaultEstimate, ..._estimate, ...estiamate };
|
||||||
|
},
|
||||||
|
[t.ESTIMATES_ITEMS_SET]: (state, action) => {
|
||||||
|
const { estiamates } = action.payload;
|
||||||
|
const _estimates = {};
|
||||||
|
estiamates.forEach((estimate) => {
|
||||||
|
_estimates[estimate.id] = {
|
||||||
|
...defaultEstimate,
|
||||||
|
...estimate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
state.items = {
|
||||||
|
...state.items,
|
||||||
|
..._estimates,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[t.ESTIMATES_TABLE_LOADING]: (state, action) => {
|
||||||
|
const { loading } = action.payload;
|
||||||
|
state.loading = loading;
|
||||||
|
},
|
||||||
|
|
||||||
|
[t.ESTIMATES_SET_CURRENT_VIEW]: (state, action) => {
|
||||||
|
state.currentViewId = action.currentViewId;
|
||||||
|
},
|
||||||
|
|
||||||
|
[t.ESTIMATE_DELETE]: (state, action) => {
|
||||||
|
const { id } = action.payload;
|
||||||
|
|
||||||
|
if (typeof state.items[id] !== 'undefined') {
|
||||||
|
delete state.items[id];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
[t.ESTIMATES_PAGE_SET]: (state, action) => {
|
||||||
|
const { customViewId, estimates, pagination } = action.payload;
|
||||||
|
|
||||||
|
const viewId = customViewId || -1;
|
||||||
|
const view = state.views[viewId] || {};
|
||||||
|
|
||||||
|
state.views[viewId] = {
|
||||||
|
...view,
|
||||||
|
pages: {
|
||||||
|
...(state.views?.[viewId]?.pages || {}),
|
||||||
|
[pagination.page]: {
|
||||||
|
ids: estimates.map((i) => i.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[t.ESTIMATES_PAGINATION_SET]: (state, action) => {
|
||||||
|
const { pagination, customViewId } = action.payload;
|
||||||
|
|
||||||
|
const mapped = {
|
||||||
|
pageSize: parseInt(pagination.pageSize, 10),
|
||||||
|
page: parseInt(pagination.page, 10),
|
||||||
|
total: parseInt(pagination.total, 10),
|
||||||
|
};
|
||||||
|
const paginationMeta = {
|
||||||
|
...mapped,
|
||||||
|
pagesCount: Math.ceil(mapped.total / mapped.pageSize),
|
||||||
|
pageIndex: Math.max(mapped.page - 1, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.views = {
|
||||||
|
...state.views,
|
||||||
|
[customViewId]: {
|
||||||
|
...(state.views?.[customViewId] || {}),
|
||||||
|
paginationMeta,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createTableQueryReducers('estimates', reducer);
|
||||||
|
|
||||||
|
export const getEstimateById = (state, id) => {
|
||||||
|
return state.estiamates.items[id];
|
||||||
|
};
|
||||||
44
client/src/store/Estimate/estimates.selectors.js
Normal file
44
client/src/store/Estimate/estimates.selectors.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
|
||||||
|
|
||||||
|
const estimateTableQuery = (state) => state.estimates.tableQuery;
|
||||||
|
|
||||||
|
export const getEstimatesTableQuery = createSelector(
|
||||||
|
paginationLocationQuery,
|
||||||
|
estimateTableQuery,
|
||||||
|
(location, query) => ({
|
||||||
|
...location,
|
||||||
|
...query,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const estimatesSelector = (state, props, query) => {
|
||||||
|
const viewId = state.estimates.currentViewId;
|
||||||
|
return state.estimates.views?.[viewId]?.pages?.[query.page];
|
||||||
|
};
|
||||||
|
|
||||||
|
const EstimateItemsSelector = (state) => state.estimates.items;
|
||||||
|
|
||||||
|
export const getEstimateCurrentPage = () =>
|
||||||
|
createSelector(estimatesSelector, EstimateItemsSelector, (page, items) => {
|
||||||
|
return typeof page === 'object'
|
||||||
|
? pickItemsFromIds(items, page.ids) || []
|
||||||
|
: [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const estimateByIdSelector = (state, props) => state.estimates.items;
|
||||||
|
|
||||||
|
export const getEstimateByIdFactory = () =>
|
||||||
|
createSelector(estimateByIdSelector, (estimate) => {
|
||||||
|
return estimate;
|
||||||
|
});
|
||||||
|
|
||||||
|
const paginationSelector = (state, props) => {
|
||||||
|
const viewId = state.estimates.currentViewId;
|
||||||
|
return state.estimates.views?.[viewId];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEstimatesPaginationMetaFactory = () =>
|
||||||
|
createSelector(paginationSelector, (estimatePage) => {
|
||||||
|
return estimatePage?.paginationMeta || {};
|
||||||
|
});
|
||||||
13
client/src/store/Estimate/estimates.types.js
Normal file
13
client/src/store/Estimate/estimates.types.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export default {
|
||||||
|
ESTIMATES_LIST_SET: 'ESTIMATES_LIST_SET',
|
||||||
|
ESTIMATE_SET: 'ESTIMATE_SET',
|
||||||
|
ESTIMATE_DELETE: 'ESTIMATE_DELETE',
|
||||||
|
ESTIMATES_BULK_DELETE: 'ESTIMATES_BULK_DELETE',
|
||||||
|
ESTIMATES_SET_CURRENT_VIEW: 'ESTIMATES_SET_CURRENT_VIEW',
|
||||||
|
ESTIMATES_TABLE_QUERIES_ADD: 'ESTIMATES_TABLE_QUERIES_ADD',
|
||||||
|
ESTIMATES_TABLE_LOADING: 'ESTIMATES_TABLE_LOADING',
|
||||||
|
ESTIMATES_PAGINATION_SET: 'ESTIMATES_PAGINATION_SET',
|
||||||
|
ESTIMATES_PAGE_SET: 'ESTIMATES_PAGE_SET',
|
||||||
|
ESTIMATES_ITEMS_SET:'ESTIMATES_ITEMS_SET',
|
||||||
|
|
||||||
|
};
|
||||||
137
client/src/store/Invoice/invoices.actions.js
Normal file
137
client/src/store/Invoice/invoices.actions.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import ApiService from 'services/ApiService';
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
export const submitInvoice = ({ form }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
|
});
|
||||||
|
ApiService.post('sales/invoices', form)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteInvoice = ({ id }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resovle, reject) => {
|
||||||
|
ApiService.delete(`invoice/${id}`)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({ type: t.INVOICE_DELETE });
|
||||||
|
resovle(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error.response.data.errors || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editInvoice = (id, form) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
|
});
|
||||||
|
ApiService.post(`invoice/${id}`, form)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const fetchInvoicesTable = ({ query = {} }) => {
|
||||||
|
return (dispatch, getState) =>
|
||||||
|
new Promise((resolve, rejcet) => {
|
||||||
|
const pageQuery = getState().invoices.tableQuery;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_TABLE_LOADING,
|
||||||
|
payload: {
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ApiService.get('invoices', {
|
||||||
|
params: { ...pageQuery, ...query },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_PAGE_SET,
|
||||||
|
payload: {
|
||||||
|
invoices: response.data.invoices.results,
|
||||||
|
pagination: response.data.invoices.pagination,
|
||||||
|
customViewId: response.data.customViewId || -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_ITEMS_SET,
|
||||||
|
payload: {
|
||||||
|
invoices: response.data.invoices.results,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_PAGINATION_SET,
|
||||||
|
payload: {
|
||||||
|
pagination: response.data.invoices.pagination,
|
||||||
|
customViewId: response.data.customViewId || -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICES_TABLE_LOADING,
|
||||||
|
payload: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
rejcet(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchInvoice = ({ id }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resovle, reject) => {
|
||||||
|
ApiService.get(`invoices/${id}`)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.INVOICE_SET,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
invoice: response.data.invoice,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resovle(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
32
client/src/store/Invoice/invoices.reducer.js
Normal file
32
client/src/store/Invoice/invoices.reducer.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
import { createTableQueryReducers } from 'store/queryReducers';
|
||||||
|
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
items: {},
|
||||||
|
views: {},
|
||||||
|
loading: false,
|
||||||
|
tableQuery: {
|
||||||
|
page_size: 12,
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
currentViewId: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultInvoice = {
|
||||||
|
entires: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const reducer = createReducer(initialState, {
|
||||||
|
[t.INVOICE_SET]:(state,actio)=>{
|
||||||
|
|
||||||
|
const {id,INVOICE_SET} = action.payload;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
9
client/src/store/Invoice/invoices.selector.js
Normal file
9
client/src/store/Invoice/invoices.selector.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
const invoiceByIdSelector = (state, props) =>
|
||||||
|
state.invoices.items[props.invoiceId];
|
||||||
|
|
||||||
|
export const getInvoiceById = () =>
|
||||||
|
createSelector(invoiceByIdSelector, (_invoice) => {
|
||||||
|
return _invoice;
|
||||||
|
});
|
||||||
12
client/src/store/Invoice/invoices.types.js
Normal file
12
client/src/store/Invoice/invoices.types.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
INVOICE_DELETE: 'INVOICE_DELETE',
|
||||||
|
INVOICES_BULK_DELETE: 'INVOICES_BULK_DELETE',
|
||||||
|
INVOICES_LIST_SET: 'INVOICES_LIST_SET',
|
||||||
|
INVOICE_SET: 'INVOICE_SET',
|
||||||
|
INVOICES_SET_CURRENT_VIEW: 'INVOICES_SET_CURRENT_VIEW',
|
||||||
|
INVOICES_TABLE_QUERIES_ADD: 'INVOICES_TABLE_QUERIES_ADD',
|
||||||
|
INVOICES_TABLE_LOADING: 'INVOICES_TABLE_LOADING',
|
||||||
|
INVOICES_PAGINATION_SET: 'INVOICES_PAGINATION_SET',
|
||||||
|
INVOICES_PAGE_SET: 'INVOICES_PAGE_SET',
|
||||||
|
INVOICES_ITEMS_SET: 'INVOICES_ITEMS_SET',
|
||||||
|
};
|
||||||
136
client/src/store/receipt/receipt.actions.js
Normal file
136
client/src/store/receipt/receipt.actions.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import ApiService from 'services/ApiService';
|
||||||
|
import t from 'store/types';
|
||||||
|
|
||||||
|
export const submitReceipt = ({ form }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
|
});
|
||||||
|
ApiService.post('sales/receipts', form)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteReceipt = ({ id }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resovle, reject) => {
|
||||||
|
ApiService.delete(`receipts/${id}`)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({ type: t.RECEIPT_DELETE });
|
||||||
|
resovle(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
reject(error.response.data.errors || []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editReceipt = (id, form) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resolve, rejcet) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_LOADING,
|
||||||
|
});
|
||||||
|
ApiService.delete(`receipt/${id}`, form)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
dispatch({
|
||||||
|
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
|
||||||
|
});
|
||||||
|
rejcet(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchReceipt = ({ id }) => {
|
||||||
|
return (dispatch) =>
|
||||||
|
new Promise((resovle, reject) => {
|
||||||
|
ApiService.get(`receipt/${id}`)
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPT_SET,
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
receipt: response.data.receipt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resovle(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const { response } = error;
|
||||||
|
const { data } = response;
|
||||||
|
reject(data?.errors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchReceiptsTable = ({ query = {} }) => {
|
||||||
|
return (dispatch, getState) =>
|
||||||
|
new Promise((resolve, rejcet) => {
|
||||||
|
const pageQuery = getState().receipt.tableQuery;
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPTS_TABLE_LOADING,
|
||||||
|
payload: {
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ApiService.get('receipts', {
|
||||||
|
params: { ...pageQuery, ...query },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPTS_PAGE_SET,
|
||||||
|
payload: {
|
||||||
|
receipts: response.data.receipts.results,
|
||||||
|
pagination: response.data.receipts.pagination,
|
||||||
|
customViewId: response.data.customViewId || -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPTS_ITEMS_SET,
|
||||||
|
payload: {
|
||||||
|
receipts: response.data.receipts.results,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPTS_PAGINATION_SET,
|
||||||
|
payload: {
|
||||||
|
pagination: response.data.receipts.pagination,
|
||||||
|
customViewId: response.data.customViewId || -1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dispatch({
|
||||||
|
type: t.RECEIPTS_TABLE_LOADING,
|
||||||
|
payload: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resolve(response);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
rejcet(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
0
client/src/store/receipt/receipt.reducer.js
Normal file
0
client/src/store/receipt/receipt.reducer.js
Normal file
0
client/src/store/receipt/receipt.selector.js
Normal file
0
client/src/store/receipt/receipt.selector.js
Normal file
12
client/src/store/receipt/receipt.type.js
Normal file
12
client/src/store/receipt/receipt.type.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
RECEIPT_DELETE: 'RECEIPT_DELETE',
|
||||||
|
RECEIPTS_BULK_DELETE: 'RECEIPTS_BULK_DELETE',
|
||||||
|
RECEIPTS_LIST_SET: 'RECEIPTS_LIST_SET',
|
||||||
|
RECEIPT_SET: 'RECEIPT_SET',
|
||||||
|
RECEIPTS_SET_CURRENT_VIEW: 'RECEIPTS_SET_CURRENT_VIEW',
|
||||||
|
RECEIPTS_TABLE_QUERIES_ADD: 'RECEIPTS_TABLE_QUERIES_ADD',
|
||||||
|
RECEIPTS_TABLE_LOADING: 'RECEIPTS_TABLE_LOADING',
|
||||||
|
RECEIPTS_PAGINATION_SET: 'RECEIPTS_PAGINATION_SET',
|
||||||
|
RECEIPTS_PAGE_SET: 'RECEIPTS_PAGE_SET',
|
||||||
|
RECEIPTS_ITEMS_SET: 'RECEIPTS_ITEMS_SET',
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ import globalSearch from './search/search.reducer';
|
|||||||
import exchangeRates from './ExchangeRate/exchange.reducer';
|
import exchangeRates from './ExchangeRate/exchange.reducer';
|
||||||
import globalErrors from './globalErrors/globalErrors.reducer';
|
import globalErrors from './globalErrors/globalErrors.reducer';
|
||||||
import customers from './customers/customers.reducer';
|
import customers from './customers/customers.reducer';
|
||||||
|
import estimates from './Estimate/estimates.reducer';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
authentication,
|
authentication,
|
||||||
@@ -38,4 +39,5 @@ export default combineReducers({
|
|||||||
exchangeRates,
|
exchangeRates,
|
||||||
globalErrors,
|
globalErrors,
|
||||||
customers,
|
customers,
|
||||||
|
estimates
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import search from './search/search.type';
|
|||||||
import register from './registers/register.type';
|
import register from './registers/register.type';
|
||||||
import exchangeRate from './ExchangeRate/exchange.type';
|
import exchangeRate from './ExchangeRate/exchange.type';
|
||||||
import customer from './customers/customers.type';
|
import customer from './customers/customers.type';
|
||||||
|
import estimates from './Estimate/estimates.types';
|
||||||
|
import invoices from './Invoice/invoices.types';
|
||||||
|
import receipts from './receipt/receipt.type';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...authentication,
|
...authentication,
|
||||||
@@ -38,4 +41,7 @@ export default {
|
|||||||
...register,
|
...register,
|
||||||
...exchangeRate,
|
...exchangeRate,
|
||||||
...customer,
|
...customer,
|
||||||
|
...estimates,
|
||||||
|
...invoices,
|
||||||
|
...receipts
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
|||||||
@import 'pages/invite-user.scss';
|
@import 'pages/invite-user.scss';
|
||||||
@import 'pages/exchange-rate.scss';
|
@import 'pages/exchange-rate.scss';
|
||||||
@import 'pages/customer.scss';
|
@import 'pages/customer.scss';
|
||||||
|
@import 'pages/estimate.scss';
|
||||||
|
|
||||||
// Views
|
// Views
|
||||||
@import 'views/filter-dropdown';
|
@import 'views/filter-dropdown';
|
||||||
@import 'views/sidebar';
|
@import 'views/sidebar';
|
||||||
|
|
||||||
.App {
|
.App {
|
||||||
@@ -100,45 +101,42 @@ body.authentication {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-list--fill-popover{
|
.select-list--fill-popover {
|
||||||
|
|
||||||
.bp3-transition-container,
|
.bp3-transition-container,
|
||||||
.bp3-popover{
|
.bp3-popover {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-list--fill-button{
|
.select-list--fill-button {
|
||||||
|
|
||||||
.bp3-popover-wrapper,
|
.bp3-popover-wrapper,
|
||||||
.bp3-popover-target{
|
.bp3-popover-target {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.bp3-button{
|
.bp3-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: start;
|
justify-content: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bp3-datepicker-caption .bp3-html-select::after {
|
||||||
.bp3-datepicker-caption .bp3-html-select::after{
|
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint{
|
.hint {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
|
|
||||||
.bp3-icon{
|
.bp3-icon {
|
||||||
color: #A1B2C5;
|
color: #a1b2c5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-popover-target:hover .bp3-icon{
|
.bp3-popover-target:hover .bp3-icon {
|
||||||
color: #90a1b5;
|
color: #90a1b5;
|
||||||
}
|
}
|
||||||
.bp3-icon{
|
.bp3-icon {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,20 +153,19 @@ body.authentication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-form-group .bp3-label{
|
.bp3-form-group .bp3-label {
|
||||||
|
.hint {
|
||||||
.hint{
|
.bp3-popover-wrapper {
|
||||||
.bp3-popover-wrapper{
|
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.bp3-inline) .hint .bp3-popover-target{
|
&:not(.bp3-inline) .hint .bp3-popover-target {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-popover.bp3-tooltip{
|
.bp3-popover.bp3-tooltip {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|||||||
379
client/src/style/pages/estimate.scss
Normal file
379
client/src/style/pages/estimate.scss
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
.estimate-form {
|
||||||
|
padding-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.bp3-form-group {
|
||||||
|
width: 100%;
|
||||||
|
margin: 25px 20px 15px;
|
||||||
|
}
|
||||||
|
.bp3-label {
|
||||||
|
margin: 0 20px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #444;
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
.bp3-form-content {
|
||||||
|
width: 35%;
|
||||||
|
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
|
||||||
|
width: 120%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__table {
|
||||||
|
padding: 15px 15px 0;
|
||||||
|
|
||||||
|
.bp3-form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
border: 1px dotted rgb(195, 195, 195);
|
||||||
|
border-bottom: transparent;
|
||||||
|
border-left: transparent;
|
||||||
|
|
||||||
|
.th,
|
||||||
|
.td {
|
||||||
|
border-left: 1px dotted rgb(195, 195, 195);
|
||||||
|
|
||||||
|
&.index {
|
||||||
|
> span,
|
||||||
|
> div {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thead {
|
||||||
|
.tr .th {
|
||||||
|
padding: 10px 10px;
|
||||||
|
background-color: #f2f5fa;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tbody {
|
||||||
|
.tr .td {
|
||||||
|
padding: 7px;
|
||||||
|
border-bottom: 1px dotted rgb(195, 195, 195);
|
||||||
|
min-height: 46px;
|
||||||
|
|
||||||
|
&.index {
|
||||||
|
background-color: #f2f5fa;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tr {
|
||||||
|
.bp3-form-group .bp3-input,
|
||||||
|
.form-group--select-list .bp3-button {
|
||||||
|
border-radius: 3px;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||||
|
.form-group--select-list:not(.bp3-intent-danger) .bp3-button {
|
||||||
|
border-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
.td {
|
||||||
|
border-bottom: transparent;
|
||||||
|
|
||||||
|
.bp3-button,
|
||||||
|
.bp3-input-group {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.td.actions {
|
||||||
|
.bp3-button {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #e68f8e;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #c23030;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.row--total {
|
||||||
|
.td.amount {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.th {
|
||||||
|
color: #444;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 1px dotted #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.td {
|
||||||
|
border-bottom: 1px dotted #999;
|
||||||
|
|
||||||
|
&.description {
|
||||||
|
.bp3-form-group {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions.td {
|
||||||
|
.bp3-button {
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__floating-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
padding: 18px 18px;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
|
|
||||||
|
.has-mini-sidebar & {
|
||||||
|
left: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bp3-button {
|
||||||
|
&.button--clear-lines {
|
||||||
|
background-color: #fcefef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button--clear-lines,
|
||||||
|
.button--new-line {
|
||||||
|
padding-left: 14px;
|
||||||
|
padding-right: 14px;
|
||||||
|
}
|
||||||
|
.dropzone-container {
|
||||||
|
margin-top: 0;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
.dropzone {
|
||||||
|
width: 300px;
|
||||||
|
height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group--description {
|
||||||
|
.bp3-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
.bp3-form-content {
|
||||||
|
// width: 280px;
|
||||||
|
textarea {
|
||||||
|
width: 450px;
|
||||||
|
min-height: 75px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// .estimate-form {
|
||||||
|
// padding-bottom: 30px;
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
|
||||||
|
// .bp3-form-group {
|
||||||
|
// margin: 25px 20px 15px;
|
||||||
|
// width: 100%;
|
||||||
|
// .bp3-label {
|
||||||
|
// font-weight: 500;
|
||||||
|
// font-size: 13px;
|
||||||
|
// color: #444;
|
||||||
|
// width: 130px;
|
||||||
|
// }
|
||||||
|
// .bp3-form-content {
|
||||||
|
// // width: 400px;
|
||||||
|
// width: 45%;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // .expense-form-footer {
|
||||||
|
// // display: flex;
|
||||||
|
// // padding: 30px 25px 0;
|
||||||
|
// // justify-content: space-between;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// &__primary-section {
|
||||||
|
// background: #fbfbfb;
|
||||||
|
// }
|
||||||
|
// &__table {
|
||||||
|
// padding: 15px 15px 0;
|
||||||
|
|
||||||
|
// .bp3-form-group {
|
||||||
|
// margin-bottom: 0;
|
||||||
|
// }
|
||||||
|
// .table {
|
||||||
|
// border: 1px dotted rgb(195, 195, 195);
|
||||||
|
// border-bottom: transparent;
|
||||||
|
// border-left: transparent;
|
||||||
|
|
||||||
|
// .th,
|
||||||
|
// .td {
|
||||||
|
// border-left: 1px dotted rgb(195, 195, 195);
|
||||||
|
|
||||||
|
// &.index {
|
||||||
|
// > span,
|
||||||
|
// > div {
|
||||||
|
// text-align: center;
|
||||||
|
// width: 100%;
|
||||||
|
// font-weight: 500;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .thead {
|
||||||
|
// .tr .th {
|
||||||
|
// padding: 10px 10px;
|
||||||
|
// background-color: #f2f5fa;
|
||||||
|
// font-size: 14px;
|
||||||
|
// font-weight: 500;
|
||||||
|
// color: #333;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .tbody {
|
||||||
|
// .tr .td {
|
||||||
|
// padding: 7px;
|
||||||
|
// border-bottom: 1px dotted rgb(195, 195, 195);
|
||||||
|
// min-height: 46px;
|
||||||
|
|
||||||
|
// &.index {
|
||||||
|
// background-color: #f2f5fa;
|
||||||
|
// text-align: center;
|
||||||
|
|
||||||
|
// > span {
|
||||||
|
// margin-top: auto;
|
||||||
|
// margin-bottom: auto;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .tr {
|
||||||
|
// .bp3-form-group .bp3-input,
|
||||||
|
// .form-group--select-list .bp3-button {
|
||||||
|
// border-radius: 3px;
|
||||||
|
// padding-left: 8px;
|
||||||
|
// padding-right: 8px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||||
|
// .form-group--select-list:not(.bp3-intent-danger) .bp3-button {
|
||||||
|
// border-color: #e5e5e5;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &:last-of-type {
|
||||||
|
// .td {
|
||||||
|
// border-bottom: transparent;
|
||||||
|
|
||||||
|
// .bp3-button,
|
||||||
|
// .bp3-input-group {
|
||||||
|
// display: none;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .td.actions {
|
||||||
|
// .bp3-button {
|
||||||
|
// background-color: transparent;
|
||||||
|
// color: #e68f8e;
|
||||||
|
|
||||||
|
// &:hover {
|
||||||
|
// color: #c23030;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.row--total {
|
||||||
|
// .td.amount {
|
||||||
|
// font-weight: bold;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .th {
|
||||||
|
// color: #444;
|
||||||
|
// font-weight: 600;
|
||||||
|
// border-bottom: 1px dotted #666;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .td {
|
||||||
|
// border-bottom: 1px dotted #999;
|
||||||
|
|
||||||
|
// &.description {
|
||||||
|
// .bp3-form-group {
|
||||||
|
// width: 100%;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .actions.td {
|
||||||
|
// .bp3-button {
|
||||||
|
// background: transparent;
|
||||||
|
// margin: 0;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// &__floating-footer {
|
||||||
|
// position: fixed;
|
||||||
|
// bottom: 0;
|
||||||
|
// width: 100%;
|
||||||
|
// background: #fff;
|
||||||
|
// padding: 18px 18px;
|
||||||
|
// border-top: 1px solid #ececec;
|
||||||
|
|
||||||
|
// .has-mini-sidebar & {
|
||||||
|
// left: 50px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .bp3-button {
|
||||||
|
// &.button--clear-lines {
|
||||||
|
// background-color: #fcefef;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .button--clear-lines,
|
||||||
|
// .button--new-line {
|
||||||
|
// padding-left: 14px;
|
||||||
|
// padding-right: 14px;
|
||||||
|
// }
|
||||||
|
// .dropzone-container {
|
||||||
|
// margin-top: 0;
|
||||||
|
// align-self: flex-end;
|
||||||
|
// }
|
||||||
|
// .dropzone {
|
||||||
|
// width: 300px;
|
||||||
|
// height: 75px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .form-group--description {
|
||||||
|
// .bp3-label {
|
||||||
|
// font-weight: 500;
|
||||||
|
// font-size: 13px;
|
||||||
|
// color: #444;
|
||||||
|
// }
|
||||||
|
// .bp3-form-content {
|
||||||
|
// // width: 280px;
|
||||||
|
// textarea {
|
||||||
|
// width: 450px;
|
||||||
|
// min-height: 75px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
Reference in New Issue
Block a user