diff --git a/client/src/components/EstimateListField.js b/client/src/components/EstimateListField.js
index ea37b794f..a7b94a662 100644
--- a/client/src/components/EstimateListField.js
+++ b/client/src/components/EstimateListField.js
@@ -1,39 +1,71 @@
import React, { useCallback, useMemo, useEffect, useState } from 'react';
-import { MenuItem, Button } from '@blueprintjs/core';
+import { MenuItem } from '@blueprintjs/core';
import ListSelect from 'components/ListSelect';
import { FormattedMessage as T } from 'react-intl';
function EstimateListField({
products,
+ initialProductId,
selectedProductId,
- onProductSelected,
defautlSelectText = ,
+ onProductSelected,
}) {
+ const initialProduct = useMemo(
+ () => products.find((a) => a.id === initialProductId),
+ [initialProductId],
+ );
+
+ const [selectedProduct, setSelectedProduct] = useState(
+ initialProduct || null,
+ );
+
+ useEffect(() => {
+ if (typeof selectedProductId !== 'undefined') {
+ const product = selectedProductId
+ ? products.find((a) => a.id === selectedProductId)
+ : null;
+ setSelectedProduct(product);
+ }
+ }, [selectedProductId, products, setSelectedProduct]);
+
const onProductSelect = useCallback(
(product) => {
+ setSelectedProduct({ ...product });
onProductSelected && onProductSelected(product);
},
[onProductSelected],
);
const productRenderer = useCallback(
- (item, { handleClick, modifiers, query }) => (
-
+ (item, { handleClick }) => (
+
),
[],
);
+ const filterProduct = useCallback((query, product, _index, exactMatch) => {
+ const normalizedTitle = product.name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return normalizedTitle.indexOf(normalizedQuery) >= 0;
+ }
+ }, []);
+
return (
}
itemRenderer={productRenderer}
+ itemPredicate={filterProduct}
popoverProps={{ minimal: true }}
onItemSelect={onProductSelect}
selectedItem={`${selectedProductId}`}
selectedItemProp={'id'}
labelProp={'name'}
- defaultText={defautlSelectText}
+ defaultText={selectedProduct ? selectedProduct.name : defautlSelectText}
/>
);
}
diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js
index 9e72634cf..8945fa9dd 100644
--- a/client/src/config/sidebarMenu.js
+++ b/client/src/config/sidebarMenu.js
@@ -42,21 +42,35 @@ export default [
text: ,
href: '/estimates/new',
},
+ // {
+ // text: ,
+ // href: '/estimates',
+ // },
{
text: ,
href: '/invoices/new',
},
+ // {
+ // text: ,
+ // href: '/invoices',
+ // },
{
text: ,
href: '/payment-receive/new',
},
{
divider: true,
+ text: ,
+ href: '/invoices',
},
{
text: ,
href: '/receipts/new',
},
+ // {
+ // text: ,
+ // href: '/receipts',
+ // },
],
},
{
@@ -64,8 +78,12 @@ export default [
children: [
{
text: ,
- href: '/bill/new',
+ href: '/bills/new',
},
+ // {
+ // text: ,
+ // href: '/bills',
+ // },
{
text: ,
},
diff --git a/client/src/containers/Purchases/Bill/BillActionsBar.js b/client/src/containers/Purchases/Bill/BillActionsBar.js
new file mode 100644
index 000000000..dad1c4f2b
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillActionsBar.js
@@ -0,0 +1,148 @@
+import React, { useCallback, useState, useMemo } 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, useIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+import FilterDropdown from 'components/FilterDropdown';
+import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
+
+import { If, DashboardActionViewsList } from 'components';
+
+import withResourceDetail from 'containers/Resources/withResourceDetails';
+import withBillActions from './withBillActions';
+import withBills from './withBills';
+
+import { compose } from 'utils';
+
+function BillActionsBar({
+ // #withResourceDetail
+ resourceFields,
+
+ //#withBills
+ billsViews,
+
+ //#withBillActions
+ addBillsTableQueries,
+
+ // #own Porps
+ onFilterChanged,
+ selectedRows = [],
+}) {
+ const history = useHistory();
+ const { path } = useRouteMatch();
+ const [filterCount, setFilterCount] = useState(0);
+ const { formatMessage } = useIntl();
+
+ const handleClickNewBill = useCallback(() => {
+ history.push('/bills/new');
+ }, [history]);
+
+ // const FilterDropdown = FilterDropdown({
+ // initialCondition: {
+ // fieldKey: '',
+ // compatator: '',
+ // value: '',
+ // },
+ // fields: resourceFields,
+ // onFilterChange: (filterConditions) => {
+ // addBillsTableQueries({
+ // filter_roles: filterConditions || '',
+ // });
+ // onFilterChanged && onFilterChanged(filterConditions);
+ // },
+ // });
+
+ const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
+ selectedRows,
+ ]);
+
+ return (
+
+
+
+
+ }
+ text={}
+ onClick={handleClickNewBill}
+ />
+
+
+ ) : (
+ `${filterCount} ${formatMessage({ id: 'filters_applied' })}`
+ )
+ }
+ icon={}
+ />
+
+
+ }
+ text={}
+ intent={Intent.DANGER}
+ // onClick={handleBulkDelete}
+ />
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+const mapStateToProps = (state, props) => ({
+ resourceName: 'bills',
+});
+const withBillActionsBar = connect(mapStateToProps);
+
+export default compose(
+ withBillActionsBar,
+ withResourceDetail(({ resourceFields }) => ({
+ resourceFields,
+ })),
+
+ // withBills(({billsViews})=>({
+ // billsViews
+ // })),
+
+ withBillActions,
+)(BillActionsBar);
diff --git a/client/src/containers/Purchases/Bill/BillForm.js b/client/src/containers/Purchases/Bill/BillForm.js
new file mode 100644
index 000000000..f958d5d32
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillForm.js
@@ -0,0 +1,334 @@
+import React, {
+ useMemo,
+ useState,
+ useCallback,
+ useEffect,
+ useRef,
+} 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, omit } from 'lodash';
+
+import BillFormHeader from './BillFormHeader';
+import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
+import BillFormFooter from './BillFormFooter';
+
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withMediaActions from 'containers/Media/withMediaActions';
+import withBillActions from './withBillActions';
+import withBillDetail from './withBillDetail';
+
+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 BillForm({
+ //#WithMedia
+ requestSubmitMedia,
+ requestDeleteMedia,
+
+ //#withBillActions
+ requestSubmitBill,
+ requestEditBill,
+
+ //#withDashboard
+ changePageTitle,
+ changePageSubtitle,
+
+ //#withBillDetail
+ bill,
+
+ //#Own Props
+ billId,
+ onFormSubmit,
+ onCancelForm,
+}) {
+ const { formatMessage } = useIntl();
+ const [payload, setPayload] = 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 (bill && bill.id) {
+ changePageTitle(formatMessage({ id: 'edit_bill' }));
+ } else {
+ changePageTitle(formatMessage({ id: 'new_bill' }));
+ }
+ }, [changePageTitle, bill, formatMessage]);
+
+ const validationSchema = Yup.object().shape({
+ vendor_id: Yup.number()
+ .required()
+ .label(formatMessage({ id: 'vendor_name_' })),
+ bill_date: Yup.date()
+ .required()
+ .label(formatMessage({ id: 'bill_date_' })),
+ due_date: Yup.date()
+ .required()
+ .label(formatMessage({ id: 'due_date_' })),
+ bill_number: Yup.number()
+ .required()
+ .label(formatMessage({ id: 'bill_number_' })),
+ reference_no: Yup.string().min(1).max(255),
+ status: Yup.string().required().nullable(),
+ note: 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 saveBillSubmit = useCallback(
+ (payload) => {
+ onFormSubmit && onFormSubmit(payload);
+ },
+ [onFormSubmit],
+ );
+
+ const defaultBill = useMemo(() => ({
+ index: 0,
+ item_id: null,
+ rate: null,
+ discount: 0,
+ quantity: null,
+ description: '',
+ }));
+
+ const defaultInitialValues = useMemo(
+ () => ({
+ vendor_id: '',
+ bill_number: '',
+ bill_date: moment(new Date()).format('YYYY-MM-DD'),
+ due_date: moment(new Date()).format('YYYY-MM-DD'),
+ status: 'Bill',
+ reference_no: '',
+ note: '',
+ entries: [...repeatValue(defaultBill, MIN_LINES_NUMBER)],
+ }),
+ [defaultBill],
+ );
+
+ const orderingIndex = (_bill) => {
+ return _bill.map((item, index) => ({
+ ...item,
+ index: index + 1,
+ }));
+ };
+
+ const initialValues = useMemo(
+ () => ({
+ ...(bill
+ ? {
+ ...pick(bill, Object.keys(defaultInitialValues)),
+ entries: [
+ ...bill.entries.map((bill) => ({
+ ...pick(bill, Object.keys(defaultBill)),
+ })),
+ ...repeatValue(
+ defaultBill,
+ Math.max(MIN_LINES_NUMBER - bill.entries.length, 0),
+ ),
+ ],
+ }
+ : {
+ ...defaultInitialValues,
+ entries: orderingIndex(defaultInitialValues.entries),
+ }),
+ }),
+ [bill, defaultInitialValues, defaultBill],
+ );
+
+ // const initialValues = useMemo(
+ // () => ({
+ // ...defaultInitialValues,
+ // entries: orderingIndex(defaultInitialValues.entries),
+ // }),
+ // [defaultInitialValues],
+ // );
+
+ const initialAttachmentFiles = useMemo(() => {
+ return bill && bill.media
+ ? bill.media.map((attach) => ({
+ preview: attach.attachment_file,
+ uploaded: true,
+ metadata: { ...attach },
+ }))
+ : [];
+ }, [bill]);
+
+ const formik = useFormik({
+ enableReinitialize: true,
+ validationSchema,
+ initialValues: {
+ ...initialValues,
+ },
+ onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => {
+ setSubmitting(true);
+
+ const entries = values.entries.filter(
+ (item) => item.item_id && item.quantity,
+ );
+ const form = {
+ ...values,
+ entries,
+ };
+
+ const requestForm = { ...form };
+ if (bill && bill.id) {
+ requestEditBill(bill.id, requestForm)
+ .then((response) => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_bill_has_been_successfully_edited',
+ }),
+ intent: Intent.SUCCESS,
+ });
+ setSubmitting(false);
+ saveBillSubmit({ action: 'update', ...payload });
+ resetForm();
+ })
+ .catch((error) => {
+ setSubmitting(false);
+ });
+ } else {
+ requestSubmitBill(requestForm)
+ .then((response) => {
+ AppToaster.show({
+ message: formatMessage(
+ { id: 'the_bill_has_been_successfully_created' },
+ { number: values.bill_number },
+ ),
+ intent: Intent.SUCCESS,
+ });
+ setSubmitting(false);
+ resetForm();
+ saveBillSubmit({ action: 'new', ...payload });
+ })
+ .catch((errors) => {
+ setSubmitting(false);
+ });
+ }
+ },
+ });
+
+ const handleSubmitClick = useCallback(
+ (payload) => {
+ setPayload(payload);
+ formik.submitForm();
+ },
+ [setPayload, 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 onClickCleanAllLines = () => {
+ formik.setFieldValue(
+ 'entries',
+ orderingIndex([...repeatValue(defaultBill, MIN_LINES_NUMBER)]),
+ );
+ };
+
+ const onClickAddNewRow = () => {
+ formik.setFieldValue(
+ 'entries',
+ orderingIndex([...formik.values.entries, defaultBill]),
+ );
+ };
+
+ console.log(formik.errors, 'Errors');
+ console.log(formik.values, 'values');
+
+ return (
+
+
+
+
+ );
+}
+
+export default compose(
+ withBillActions,
+ withDashboardActions,
+ withMediaActions,
+ withBillDetail(),
+)(BillForm);
diff --git a/client/src/containers/Purchases/Bill/BillFormFooter.js b/client/src/containers/Purchases/Bill/BillFormFooter.js
new file mode 100644
index 000000000..e08a5eaec
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillFormFooter.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Intent, Button } from '@blueprintjs/core';
+import { FormattedMessage as T } from 'react-intl';
+
+export default function BillFormFooter({
+ formik: { isSubmitting },
+ onSubmitClick,
+ onCancelClick,
+ bill,
+}) {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/containers/Purchases/Bill/BillFormHeader.js b/client/src/containers/Purchases/Bill/BillFormHeader.js
new file mode 100644
index 000000000..ec609ddf2
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillFormHeader.js
@@ -0,0 +1,190 @@
+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 {
+ AccountsSelectList,
+ ListSelect,
+ ErrorMessage,
+ FieldRequiredHint,
+ Hint,
+} from 'components';
+
+// import withCustomers from 'containers/Customers/withCustomers';
+import withVendors from 'containers/Vendors/withVendors';
+import withAccounts from 'containers/Accounts/withAccounts';
+
+function BillFormHeader({
+ formik: { errors, touched, setFieldValue, getFieldProps, values },
+
+ //#withVendors
+ vendorsCurrentPage,
+ vendorItems,
+ //#withAccouts
+ accountsList,
+}) {
+ const handleDateChange = useCallback(
+ (date_filed) => (date) => {
+ const formatted = moment(date).format('YYYY-MM-DD');
+ setFieldValue(date_filed, formatted);
+ },
+ [setFieldValue],
+ );
+
+ const onChangeSelected = useCallback(
+ (filedName) => {
+ return (item) => {
+ setFieldValue(filedName, item.id);
+ };
+ },
+ [setFieldValue],
+ );
+
+ const vendorNameRenderer = useCallback(
+ (accept, { handleClick }) => (
+
+ ),
+ [],
+ );
+
+ // Filter vendor name
+ const filterVendorAccount = (query, vendor, _index, exactMatch) => {
+ const normalizedTitle = vendor.display_name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return (
+ `${vendor.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
+ 0
+ );
+ }
+ };
+
+ console.log(vendorsCurrentPage, 'vendorsCurrentPage');
+ console.log(vendorItems, 'vendorItems');
+ return (
+
+
+ {/* vendor account name */}
+ }
+ inline={true}
+ className={classNames('form-group--select-list', Classes.FILL)}
+ labelInfo={}
+ intent={errors.vendor_id && touched.vendor_id && Intent.DANGER}
+ helperText={
+
+ }
+ >
+ }
+ itemRenderer={vendorNameRenderer}
+ itemPredicate={filterVendorAccount}
+ popoverProps={{ minimal: true }}
+ onItemSelect={onChangeSelected('vendor_id')}
+ selectedItem={values.vendor_id}
+ selectedItemProp={'id'}
+ defaultText={}
+ labelProp={'display_name'}
+ />
+
+
+
+ }
+ inline={true}
+ labelInfo={}
+ className={classNames('form-group--select-list', Classes.FILL)}
+ intent={errors.bill_date && touched.bill_date && Intent.DANGER}
+ helperText={
+
+ }
+ >
+
+
+
+
+ }
+ inline={true}
+ className={classNames('form-group--select-list', Classes.FILL)}
+ intent={errors.due_date && touched.due_date && Intent.DANGER}
+ helperText={
+
+ }
+ >
+
+
+
+
+ {/* bill number */}
+ }
+ inline={true}
+ className={('form-group--estimate', Classes.FILL)}
+ labelInfo={}
+ intent={errors.bill_number && touched.bill_number && Intent.DANGER}
+ helperText={
+
+ }
+ >
+
+
+
+
}
+ inline={true}
+ className={classNames('form-group--reference', Classes.FILL)}
+ intent={errors.reference_no && touched.reference_no && Intent.DANGER}
+ helperText={
}
+ >
+
+
+
+ );
+}
+
+export default compose(
+ withVendors(({ vendorsCurrentPage, vendorItems }) => ({
+ vendorsCurrentPage,
+ vendorItems,
+ })),
+ withAccounts(({ accountsList }) => ({
+ accountsList,
+ })),
+)(BillFormHeader);
diff --git a/client/src/containers/Purchases/Bill/BillList.js b/client/src/containers/Purchases/Bill/BillList.js
new file mode 100644
index 000000000..158c9a61b
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillList.js
@@ -0,0 +1,178 @@
+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 BillsDataTable from './BillsDataTable';
+import BillActionsBar from './BillActionsBar';
+import BillViewTabs from './BillViewTabs';
+
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withResourceActions from 'containers/Resources/withResourcesActions';
+
+import withBills from './withBills';
+import withBillActions from './withBillActions';
+import withViewsActions from 'containers/Views/withViewsActions';
+
+import { compose } from 'utils';
+
+function BillList({
+ // #withDashboardActions
+ changePageTitle,
+
+ // #withViewsActions
+ requestFetchResourceViews,
+ requestFetchResourceFields,
+
+ //#withBills
+ billsTableQuery,
+
+ //#withBillActions
+ requestFetchBillsTable,
+ requestDeleteBill,
+ addBillsTableQueries,
+}) {
+ const history = useHistory();
+ const { formatMessage } = useIntl();
+ const [deleteBill, setDeleteBill] = useState(false);
+ const [selectedRows, setSelectedRows] = useState([]);
+
+ useEffect(() => {
+ changePageTitle(formatMessage({ id: 'bill_list' }));
+ }, [changePageTitle, formatMessage]);
+
+ const fetchResourceViews = useQuery(
+ ['resource-views', 'bills'],
+ (key, resourceName) => requestFetchResourceViews(resourceName),
+ );
+
+ const fetchResourceFields = useQuery(
+ ['resource-fields', 'bills'],
+ (key, resourceName) => requestFetchResourceFields(resourceName),
+ );
+
+ const fetchBills = useQuery(['bills-table', billsTableQuery], () =>
+ requestFetchBillsTable(),
+ );
+
+ //handle dalete Bill
+ const handleDeleteBill = useCallback(
+ (bill) => {
+ setDeleteBill(bill);
+ },
+ [setDeleteBill],
+ );
+
+ // handle cancel Bill
+ const handleCancelBillDelete = useCallback(() => {
+ setDeleteBill(false);
+ }, [setDeleteBill]);
+
+ // handleConfirm delete invoice
+ const handleConfirmBillDelete = useCallback(() => {
+ requestDeleteBill(deleteBill.id).then(() => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_bill_has_been_successfully_deleted',
+ }),
+ intent: Intent.SUCCESS,
+ });
+ setDeleteBill(false);
+ });
+ }, [deleteBill, requestDeleteBill, formatMessage]);
+
+ const handleEditBill = useCallback((bill) => {
+ history.push(`/bills/${bill.id}/edit`);
+ });
+
+ const handleFetchData = useCallback(
+ ({ pageIndex, pageSize, sortBy }) => {
+ const page = pageIndex + 1;
+
+ addBillsTableQueries({
+ ...(sortBy.length > 0
+ ? {
+ column_sort_by: sortBy[0].id,
+ sort_order: sortBy[0].desc ? 'desc' : 'asc',
+ }
+ : {}),
+ page_size: pageSize,
+ page,
+ });
+ },
+ [addBillsTableQueries],
+ );
+
+ // Handle selected rows change.
+ const handleSelectedRowsChange = useCallback(
+ (_invoices) => {
+ setSelectedRows(_invoices);
+ },
+ [setSelectedRows],
+ );
+
+ // Handle filter change to re-fetch data-table.
+ const handleFilterChanged = useCallback(
+ (filterConditions) => {
+ addBillsTableQueries({
+ filter_roles: filterConditions || '',
+ });
+ },
+ [fetchBills],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ confirmButtonText={}
+ icon={'trash'}
+ intent={Intent.DANGER}
+ isOpen={deleteBill}
+ onCancel={handleCancelBillDelete}
+ onConfirm={handleConfirmBillDelete}
+ >
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withResourceActions,
+ withBillActions,
+ withDashboardActions,
+ withViewsActions,
+ withBills(({ billsTableQuery }) => ({
+ billsTableQuery,
+ })),
+)(BillList);
diff --git a/client/src/containers/Purchases/Bill/BillViewTabs.js b/client/src/containers/Purchases/Bill/BillViewTabs.js
new file mode 100644
index 000000000..aae359dbc
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillViewTabs.js
@@ -0,0 +1,110 @@
+import React, { useEffect, useRef } from 'react';
+import { useHistory } from 'react-router';
+import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
+import { useParams, withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import { pick, debounce } from 'lodash';
+
+import { DashboardViewsTabs } from 'components';
+import { useUpdateEffect } from 'hooks';
+
+import withBills from './withBills';
+import withBillActions from './withBillActions';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withViewDetails from 'containers/Views/withViewDetails';
+
+import { compose } from 'utils';
+
+function BillViewTabs({
+ //#withBills
+ billsViews,
+
+ // #withViewDetails
+ viewItem,
+
+ //#withBillActions
+ changeBillView,
+ addBillsTableQueries,
+
+ // #withDashboardActions
+ setTopbarEditView,
+ changePageSubtitle,
+
+ // #ownProps
+ customViewChanged,
+ onViewChanged,
+}) {
+ const history = useHistory();
+ const { custom_view_id: customViewId = null } = useParams();
+
+ useEffect(() => {
+ changeBillView(customViewId || -1);
+ setTopbarEditView(customViewId);
+ changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
+
+ addBillsTableQueries({
+ custom_view_id: customViewId,
+ });
+ return () => {
+ setTopbarEditView(null);
+ changePageSubtitle('');
+ changeBillView(null);
+ };
+ }, [customViewId, addBillsTableQueries, changeBillView]);
+
+ useUpdateEffect(() => {
+ onViewChanged && onViewChanged(customViewId);
+ }, [customViewId]);
+
+ const debounceChangeHistory = useRef(
+ debounce((toUrl) => {
+ history.push(toUrl);
+ }, 250),
+ );
+ // Handle click a new view tab.
+ const handleClickNewView = () => {
+ setTopbarEditView(null);
+ history.push('/custom_views/invoices/new');
+ };
+ const handleTabsChange = (viewId) => {
+ const toPath = viewId ? `${viewId}/custom_view` : '';
+ debounceChangeHistory.current(`/bills/${toPath}`);
+ setTopbarEditView(viewId);
+ };
+ const tabs = billsViews.map((view) => ({
+ ...pick(view, ['name', 'id']),
+ }));
+
+ console.log(billsViews, 'billsViews');
+
+ return (
+
+
+
+
+
+ );
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ viewId: ownProps.match.params.custom_view_id,
+});
+
+const withBillsViewTabs = connect(mapStateToProps);
+
+export default compose(
+ withRouter,
+ withBillsViewTabs,
+ withBillActions,
+ withDashboardActions,
+ withViewDetails(),
+ withBills(({ billsViews }) => ({
+ billsViews,
+ })),
+)(BillViewTabs);
diff --git a/client/src/containers/Purchases/Bill/Bills.js b/client/src/containers/Purchases/Bill/Bills.js
new file mode 100644
index 000000000..c336a3e74
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/Bills.js
@@ -0,0 +1,85 @@
+import React, { useCallback } from 'react';
+import { useParams, useHistory } from 'react-router-dom';
+import { useQuery } from 'react-query';
+
+import BillForm from './BillForm';
+import DashboardInsider from 'components/Dashboard/DashboardInsider';
+
+import withVendorActions from 'containers/Vendors/withVendorActions';
+import withAccountsActions from 'containers/Accounts/withAccountsActions';
+import withItemsActions from 'containers/Items/withItemsActions';
+import withBillActions from './withBillActions';
+
+import { compose } from 'utils';
+
+function Bills({
+ //#withwithAccountsActions
+ requestFetchAccounts,
+
+ //#withVendorActions
+ requestFetchVendorsTable,
+
+ //#withItemsActions
+ requestFetchItems,
+
+ //# withBilleActions
+ requestFetchBill,
+}) {
+ const history = useHistory();
+ const { id } = useParams();
+
+ // Handle fetch accounts
+ const fetchAccounts = useQuery('accounts-list', (key) =>
+ requestFetchAccounts(),
+ );
+
+ // Handle fetch customers data table
+ const fetchVendors = useQuery('vendors-list', () =>
+ requestFetchVendorsTable({}),
+ );
+
+ // Handle fetch Items data table or list
+ const fetchItems = useQuery('items-list', () => requestFetchItems({}));
+
+ const handleFormSubmit = useCallback(
+ (payload) => {
+ payload.redirect && history.push('/bills');
+ },
+ [history],
+ );
+
+ const handleCancel = useCallback(() => {
+ history.goBack();
+ }, [history]);
+
+ const fetchBill = useQuery(
+ ['bill', id],
+ (key, _id) => requestFetchBill(_id),
+ { enabled: !!id },
+ );
+
+ return (
+
+
+
+ );
+}
+
+export default compose(
+ withBillActions,
+ withVendorActions,
+ withItemsActions,
+ withAccountsActions,
+)(Bills);
diff --git a/client/src/containers/Purchases/Bill/BillsDataTable.js b/client/src/containers/Purchases/Bill/BillsDataTable.js
new file mode 100644
index 000000000..0f7c351a1
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/BillsDataTable.js
@@ -0,0 +1,243 @@
+import React, { useEffect, useCallback, useState, useMemo } from 'react';
+import {
+ Intent,
+ Button,
+ Classes,
+ Popover,
+ Menu,
+ MenuItem,
+ MenuDivider,
+ Position,
+} 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 DataTable from 'components/DataTable';
+
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withViewDetails from 'containers/Views/withViewDetails';
+
+import withBills from './withBills';
+import withBillActions from './withBillActions';
+import withCurrentView from 'containers/Views/withCurrentView';
+
+function BillsDataTable({
+ //#withBills
+ billsCurrentPage,
+ billsLoading,
+ billsPageination,
+
+ // #withDashboardActions
+ changeCurrentView,
+ changePageSubtitle,
+ setTopbarEditView,
+
+ // #withView
+ viewMeta,
+
+ //#OwnProps
+ loading,
+ onFetchData,
+ onEditBill,
+ onDeleteBill,
+ onSelectedRowsChange,
+}) {
+ const [initialMount, setInitialMount] = useState(false);
+ const { custom_view_id: customViewId } = useParams();
+ const { formatMessage } = useIntl();
+
+ useEffect(() => {
+ setInitialMount(false);
+ }, [customViewId]);
+
+ useUpdateEffect(() => {
+ if (!billsLoading) {
+ setInitialMount(true);
+ }
+ }, [billsLoading, setInitialMount]);
+
+ useEffect(() => {
+ if (customViewId) {
+ changeCurrentView(customViewId);
+ setTopbarEditView(customViewId);
+ }
+ changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
+ }, [
+ customViewId,
+ changeCurrentView,
+ changePageSubtitle,
+ setTopbarEditView,
+ viewMeta,
+ ]);
+
+ const handleEditBill = useCallback(
+ (_bill) => () => {
+ onEditBill && onEditBill(_bill);
+ },
+ [onEditBill],
+ );
+
+ const handleDeleteBill = useCallback(
+ (_bill) => () => {
+ onDeleteBill && onDeleteBill(_bill);
+ },
+ [onDeleteBill],
+ );
+
+ const actionMenuList = useCallback(
+ (bill) => (
+
+ ),
+ [handleDeleteBill, handleEditBill, formatMessage],
+ );
+
+ const onRowContextMenu = useCallback(
+ (cell) => {
+ return actionMenuList(cell.row.original);
+ },
+ [actionMenuList],
+ );
+
+ const columns = useMemo(
+ () => [
+ {
+ id: 'bill_date',
+ Header: formatMessage({ id: 'bill_date' }),
+ accessor: (r) => moment(r.bill_date).format('YYYY MMM DD'),
+ width: 140,
+ className: 'bill_date',
+ },
+ {
+ id: 'vendor_id',
+ Header: formatMessage({ id: 'vendor_name' }),
+ accessor: (row) => row.vendor_id,
+ width: 140,
+ className: 'vendor_id',
+ },
+ {
+ id: 'bill_number',
+ Header: formatMessage({ id: 'bill_number' }),
+ accessor: (row) => `#${row.bill_number}`,
+ width: 140,
+ className: 'bill_number',
+ },
+
+ {
+ id: 'due_date',
+ Header: formatMessage({ id: 'due_date' }),
+ accessor: (r) => moment(r.due_date).format('YYYY MMM DD'),
+ width: 140,
+ className: 'due_date',
+ },
+ {
+ id: 'amount',
+ Header: formatMessage({ id: 'amount' }),
+ accessor: 'amount',
+ width: 140,
+ className: 'amount',
+ },
+ {
+ id: 'reference_no',
+ Header: formatMessage({ id: 'reference_no' }),
+ accessor: 'reference_no',
+ width: 140,
+ className: 'reference_no',
+ },
+ {
+ id: 'status',
+ Header: formatMessage({ id: 'status' }),
+ accessor: 'status',
+ width: 140,
+ className: 'status',
+ },
+ {
+ id: 'actions',
+ Header: '',
+ Cell: ({ cell }) => (
+
+ } />
+
+ ),
+ className: 'actions',
+ width: 50,
+ disableResizing: true,
+ },
+ ],
+ [actionMenuList, formatMessage],
+ );
+
+ const handleDataTableFetchData = useCallback(
+ (...args) => {
+ onFetchData && onFetchData(...args);
+ },
+ [onFetchData],
+ );
+ const handleSelectedRowsChange = useCallback(
+ (selectedRows) => {
+ onSelectedRowsChange &&
+ onSelectedRowsChange(selectedRows.map((s) => s.original));
+ },
+ [onSelectedRowsChange],
+ );
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withRouter,
+ withCurrentView,
+ withDialogActions,
+ withDashboardActions,
+ withBillActions,
+ withBills(({ billsCurrentPage, billsLoading, billsPageination }) => ({
+ billsCurrentPage,
+ billsLoading,
+ billsPageination,
+ })),
+ withViewDetails(),
+)(BillsDataTable);
diff --git a/client/src/containers/Purchases/Bill/withBillActions.js b/client/src/containers/Purchases/Bill/withBillActions.js
new file mode 100644
index 000000000..83bd057d7
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/withBillActions.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux';
+import {
+ submitBill,
+ deleteBill,
+ editBill,
+ fetchBillsTable,
+ fetchBill,
+} from 'store/Bills/bills.actions';
+import t from 'store/types';
+
+const mapDispatchToProps = (dispatch) => ({
+ requestSubmitBill: (form) => dispatch(submitBill({ form })),
+ requestFetchBill: (id) => dispatch(fetchBill({ id })),
+ requestEditBill: (id, form) => dispatch(editBill( id, form )),
+ requestDeleteBill: (id) => dispatch(deleteBill({ id })),
+ requestFetchBillsTable: (query = {}) =>
+ dispatch(fetchBillsTable({ query: { ...query } })),
+
+ changeBillView: (id) =>
+ dispatch({
+ type: t.BILLS_SET_CURRENT_VIEW,
+ currentViewId: parseInt(id, 10),
+ }),
+
+ addBillsTableQueries: (queries) =>
+ dispatch({
+ type: t.BILLS_TABLE_QUERIES_ADD,
+ queries,
+ }),
+});
+
+export default connect(null, mapDispatchToProps);
diff --git a/client/src/containers/Purchases/Bill/withBillDetail.js b/client/src/containers/Purchases/Bill/withBillDetail.js
new file mode 100644
index 000000000..3170ee5d5
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/withBillDetail.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux';
+import { getBillByIdFactory } from 'store/Bills/bills.selectors';
+
+export default () => {
+ const getBillById = getBillByIdFactory();
+
+ const mapStateToProps = (state, props) => ({
+ bill: getBillById(state, props),
+ });
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/Purchases/Bill/withBills.js b/client/src/containers/Purchases/Bill/withBills.js
new file mode 100644
index 000000000..3cd292828
--- /dev/null
+++ b/client/src/containers/Purchases/Bill/withBills.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { getResourceViews } from 'store/customViews/customViews.selectors';
+import {
+ getBillCurrentPageFactory,
+ getBillPaginationMetaFactory,
+ getBillTableQuery,
+} from 'store/Bills/bills.selectors';
+
+export default (mapState) => {
+ const getBillsItems = getBillCurrentPageFactory();
+ const getBillsPaginationMeta = getBillPaginationMetaFactory();
+ const mapStateToProps = (state, props) => {
+ const query = getBillTableQuery(state, props);
+ const mapped = {
+ billsCurrentPage: getBillsItems(state, props, query),
+ billsViews: getResourceViews(state, props, 'bills'),
+ billsItems: state.bills.items,
+ billsTableQuery: query,
+ billsPageination: getBillsPaginationMeta(state, props, query),
+ billsLoading: state.bills.loading,
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/Sales/Estimate/EntriesItemsTable.js b/client/src/containers/Sales/Estimate/EntriesItemsTable.js
index eaa0a370e..ab7e2f266 100644
--- a/client/src/containers/Sales/Estimate/EntriesItemsTable.js
+++ b/client/src/containers/Sales/Estimate/EntriesItemsTable.js
@@ -4,7 +4,7 @@ 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 { compose, formattedAmount } from 'utils';
import {
InputGroupCell,
MoneyFieldCell,
@@ -176,7 +176,7 @@ function EstimateTable({
setFieldValue(
'entries',
newRow.map((row) => ({
- ...omit(row),
+ ...omit(row, ['total']),
})),
);
},
diff --git a/client/src/containers/Sales/Estimate/EstimateActionsBar.js b/client/src/containers/Sales/Estimate/EstimateActionsBar.js
index dca9ab169..0a8d42d43 100644
--- a/client/src/containers/Sales/Estimate/EstimateActionsBar.js
+++ b/client/src/containers/Sales/Estimate/EstimateActionsBar.js
@@ -1,4 +1,4 @@
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, useState } from 'react';
import Icon from 'components/Icon';
import {
Button,
@@ -14,9 +14,9 @@ import {
} from '@blueprintjs/core';
import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom';
-import { FormattedMessage as T } from 'react-intl';
+import { FormattedMessage as T, useIntl } from 'react-intl';
-import { If } from 'components';
+import { If, DashboardActionViewsList } from 'components';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -41,37 +41,60 @@ function EstimateActionsBar({
// #own Porps
onFilterChanged,
- selectedRows,
+ selectedRows = [],
}) {
const { path } = useRouteMatch();
const history = useHistory();
+ const [filterCount, setFilterCount] = useState(0);
+ const { formatMessage } = useIntl();
const onClickNewEstimate = useCallback(() => {
- // history.push('/estimates/new');
+ history.push('/estimates/new');
}, [history]);
- const filterDropdown = FilterDropdown({
- initialCondition: {
- fieldKey: '',
- compatator: '',
- value: '',
- },
- fields: resourceFields,
- onFilterChange: (filterConditions) => {
- addEstimatesTableQueries({
- filter_roles: filterConditions || '',
- });
- onFilterChanged && onFilterChange(filterConditions);
- },
- });
+ // const filterDropdown = FilterDropdown({
+ // fields: resourceFields,
+ // initialCondition: {
+ // fieldKey: 'estimate_number',
+ // compatator: 'contains',
+ // value: '',
+ // },
+ // onFilterChange: (filterConditions) => {
+ // setFilterCount(filterConditions.length || 0);
+ // addEstimatesTableQueries({
+ // filter_roles: filterConditions || '',
+ // });
+ // onFilterChanged && onFilterChanged(filterConditions);
+ // },
+ // });
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows,
]);
+ const viewsMenuItems = estimateViews.map((view) => {
+ return (
+
+ );
+ });
+
return (
+ {viewsMenuItems}}
+ minimal={true}
+ interactionKind={PopoverInteractionKind.HOVER}
+ position={Position.BOTTOM_LEFT}
+ >
+ }
+ text={}
+ rightIcon={'caret-down'}
+ />
+
+
+ ) : (
+ `${filterCount} ${formatMessage({ id: 'filters_applied' })}`
+ )
+ }
icon={}
/>
@@ -122,7 +151,7 @@ function EstimateActionsBar({
}
const mapStateToProps = (state, props) => ({
- resourceName: 'estimates',
+ resourceName: 'sales_estimates',
});
const withEstimateActionsBar = connect(mapStateToProps);
@@ -130,11 +159,11 @@ const withEstimateActionsBar = connect(mapStateToProps);
export default compose(
withEstimateActionsBar,
withDialogActions,
+ withEstimates(({ estimateViews }) => ({
+ estimateViews,
+ })),
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
- // withEstimate(({ estimateViews }) => ({
- // estimateViews,
- // })),
withEstimateActions,
)(EstimateActionsBar);
diff --git a/client/src/containers/Sales/Estimate/EstimateForm.js b/client/src/containers/Sales/Estimate/EstimateForm.js
index aceb930f0..c62fb864b 100644
--- a/client/src/containers/Sales/Estimate/EstimateForm.js
+++ b/client/src/containers/Sales/Estimate/EstimateForm.js
@@ -11,20 +11,22 @@ 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 { pick, omit } from 'lodash';
+import { queryCache } from 'react-query';
import EstimateFormHeader from './EstimateFormHeader';
import EstimatesItemsTable from './EntriesItemsTable';
import EstimateFormFooter from './EstimateFormFooter';
import withEstimateActions from './withEstimateActions';
+import withEstimateDetail from './withEstimateDetail';
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;
@@ -51,7 +53,7 @@ const EstimateForm = ({
onCancelForm,
}) => {
const { formatMessage } = useIntl();
- const [payload, setPaload] = useState({});
+ const [payload, setPayload] = useState({});
const {
setFiles,
@@ -93,6 +95,7 @@ const EstimateForm = ({
.label(formatMessage({ id: 'expiration_date_' })),
estimate_number: Yup.number()
.required()
+ .nullable()
.label(formatMessage({ id: 'estimate_number_' })),
reference: Yup.string().min(1).max(255),
note: Yup.string()
@@ -134,7 +137,7 @@ const EstimateForm = ({
),
});
- const saveInvokeSubmit = useCallback(
+ const saveEstimateSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
@@ -146,7 +149,7 @@ const EstimateForm = ({
index: 0,
item_id: null,
rate: null,
- discount: null,
+ discount: 0,
quantity: null,
description: '',
}),
@@ -155,10 +158,10 @@ const EstimateForm = ({
const defaultInitialValues = useMemo(
() => ({
- customer_id: null,
+ customer_id: '',
estimate_date: moment(new Date()).format('YYYY-MM-DD'),
expiration_date: moment(new Date()).format('YYYY-MM-DD'),
- estimate_number: null,
+ estimate_number: '',
reference: '',
note: '',
terms_conditions: '',
@@ -173,15 +176,38 @@ const EstimateForm = ({
index: index + 1,
}));
};
-
+ // debugger;
const initialValues = useMemo(
() => ({
- ...defaultInitialValues,
- entries: orderingProductsIndex(defaultInitialValues.entries),
+ ...(estimate
+ ? {
+ ...pick(estimate, Object.keys(defaultInitialValues)),
+ entries: [
+ ...estimate.entries.map((estimate) => ({
+ ...pick(estimate, Object.keys(defaultEstimate)),
+ })),
+ ...repeatValue(
+ defaultEstimate,
+ Math.max(MIN_LINES_NUMBER - estimate.entries.length, 0),
+ ),
+ ],
+ }
+ : {
+ ...defaultInitialValues,
+ entries: orderingProductsIndex(defaultInitialValues.entries),
+ }),
}),
- [defaultEstimate, defaultInitialValues, estimate],
+ [estimate, defaultInitialValues, defaultEstimate],
);
+ // const initialValues = useMemo(
+ // () => ({
+ // ...defaultInitialValues,
+ // entries: orderingProductsIndex(defaultInitialValues.entries),
+ // }),
+ // [defaultEstimate, defaultInitialValues, estimate],
+ // );
+
const initialAttachmentFiles = useMemo(() => {
return estimate && estimate.media
? estimate.media.map((attach) => ({
@@ -199,56 +225,58 @@ const EstimateForm = ({
...initialValues,
},
onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => {
- const entries = values.entries.map((item) => omit(item, ['total']));
-
+ const entries = values.entries.filter(
+ (item) => item.item_id && item.quantity,
+ );
const form = {
...values,
entries,
};
- const saveEstimate = (mediaIds) =>
- new Promise((resolve, reject) => {
- const requestForm = { ...form, media_ids: mediaIds };
+ const requestForm = { ...form };
- 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);
+ if (estimate && estimate.id) {
+ requestEditEstimate(estimate.id, requestForm).then((response) => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_estimate_has_been_successfully_edited',
+ }),
+ intent: Intent.SUCCESS,
+ });
+ setSubmitting(false);
+ saveEstimateSubmit({ action: 'update', ...payload });
+ resetForm();
+ });
+ } else {
+ requestSubmitEstimate(requestForm)
+ .then((response) => {
+ AppToaster.show({
+ message: formatMessage(
+ {
+ id: 'the_estimate_has_been_successfully_created',
+ },
+ {
+ number: values.estimate_number,
+ },
+ ),
+ intent: Intent.SUCCESS,
});
- });
- 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);
- });
+ setSubmitting(false);
+ resetForm();
+ saveEstimateSubmit({ action: 'new', ...payload });
+ })
+ .catch((errors) => {
+ setSubmitting(false);
+ });
+ }
},
});
const handleSubmitClick = useCallback(
(payload) => {
- setPaload(payload);
+ setPayload(payload);
formik.submitForm();
},
- [setPaload, formik],
+ [setPayload, formik],
);
const handleCancelClick = useCallback(
@@ -285,6 +313,9 @@ const EstimateForm = ({
);
};
+
+
+
return (
+
);
};
@@ -331,4 +364,5 @@ export default compose(
withEstimateActions,
withDashboardActions,
withMediaActions,
+ withEstimateDetail(),
)(EstimateForm);
diff --git a/client/src/containers/Sales/Estimate/EstimateFormFooter.js b/client/src/containers/Sales/Estimate/EstimateFormFooter.js
index d1881d750..cd93c0dd6 100644
--- a/client/src/containers/Sales/Estimate/EstimateFormFooter.js
+++ b/client/src/containers/Sales/Estimate/EstimateFormFooter.js
@@ -1,16 +1,26 @@
import React from 'react';
import { Intent, Button } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
+import { queryCache } from 'react-query';
export default function EstimateFormFooter({
- formik: { isSubmitting },
+ formik: { isSubmitting, resetForm },
onSubmitClick,
onCancelClick,
+ onClearClick,
+ estimate,
}) {
return (
-
);
}
@@ -318,4 +347,5 @@ export default compose(
withInvoiceActions,
withDashboardActions,
withMediaActions,
+ withInvoiceDetail(),
)(InvoiceForm);
diff --git a/client/src/containers/Sales/Invoice/InvoiceFormFooter.js b/client/src/containers/Sales/Invoice/InvoiceFormFooter.js
index d1881d750..466dcc665 100644
--- a/client/src/containers/Sales/Invoice/InvoiceFormFooter.js
+++ b/client/src/containers/Sales/Invoice/InvoiceFormFooter.js
@@ -6,11 +6,19 @@ export default function EstimateFormFooter({
formik: { isSubmitting },
onSubmitClick,
onCancelClick,
+ invoice,
}) {
return (
-
-
+ {
+ onSubmitClick({ redirect: true });
+ }}
+ >
+ {invoice && invoice.id ? : }
{
+ onSubmitClick({ redirect: false });
+ }}
>
diff --git a/client/src/containers/Sales/Invoice/InvoiceList.js b/client/src/containers/Sales/Invoice/InvoiceList.js
index f80af54b2..12ba1b74d 100644
--- a/client/src/containers/Sales/Invoice/InvoiceList.js
+++ b/client/src/containers/Sales/Invoice/InvoiceList.js
@@ -8,23 +8,34 @@ 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 InvoicesDataTable from './InvoicesDataTable';
+import InvoiceActionsBar from './InvoiceActionsBar';
+import InvoiceViewTabs from './InvoiceViewTabs';
-// import InvoiceActionsBar from './InvoiceActionsBar';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withResourceActions from 'containers/Resources/withResourcesActions';
+import withInvoices from './withInvoices';
+import withInvoiceActions from './withInvoiceActions';
+import withViewsActions from 'containers/Views/withViewsActions';
import { compose } from 'utils';
-import InvoiceActionsBar from './InvoiceActionsBar';
function InvoiceList({
// #withDashboardActions
changePageTitle,
// #withViewsActions
+ requestFetchResourceViews,
+ requestFetchResourceFields,
//#withInvoice
+ invoicesTableQuery,
+ invoicesViews,
//#withInvoiceActions
+ requestFetchInvoiceTable,
+ requestDeleteInvoice,
+ addInvoiceTableQueries,
}) {
const history = useHistory();
const { formatMessage } = useIntl();
@@ -35,6 +46,19 @@ function InvoiceList({
changePageTitle(formatMessage({ id: 'invoice_list' }));
}, [changePageTitle, formatMessage]);
+ const fetchResourceViews = useQuery(
+ ['resource-views', 'sales_invoices'],
+ (key, resourceName) => requestFetchResourceViews(resourceName),
+ );
+
+ const fetchResourceFields = useQuery(
+ ['resource-fields', 'sales_invoices'],
+ (key, resourceName) => requestFetchResourceFields(resourceName),
+ );
+
+ const fetchInvoices = useQuery(['invoices-table', invoicesTableQuery], () =>
+ requestFetchInvoiceTable(),
+ );
//handle dalete Invoice
const handleDeleteInvoice = useCallback(
(invoice) => {
@@ -59,15 +83,16 @@ function InvoiceList({
});
setDeleteInvoice(false);
});
- }, [setDeleteInvoice, requestDeleteInvoice]);
+ }, [deleteInvoice, requestDeleteInvoice, formatMessage]);
const handleEditInvoice = useCallback((invoice) => {
history.push(`/invoices/${invoice.id}/edit`);
});
- const fetchInvoice = useQuery(['invoice-table'], () =>
- requsetFetchInvoiceTable(),
- );
+ // Calculates the selected rows count.
+ const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
+ selectedRows,
+ ]);
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
@@ -86,24 +111,67 @@ function InvoiceList({
},
[addInvoiceTableQueries],
);
- const handleSelectedRowsChange = useCallback((_invoice) => {
- selectedRows(_invoice);
- });
+ // Handle filter change to re-fetch data-table.
+ const handleFilterChanged = useCallback(() => {}, [fetchInvoices]);
+
+ // Handle selected rows change.
+ const handleSelectedRowsChange = useCallback(
+ (_invoices) => {
+ setSelectedRows(_invoices);
+ },
+ [setSelectedRows],
+ );
return (
-
-
+
-
+
+
+
+
+ }
+ confirmButtonText={}
+ icon={'trash'}
+ intent={Intent.DANGER}
+ isOpen={deleteInvoice}
+ onCancel={handleCancelInvoiceDelete}
+ onConfirm={handleConfirmInvoiceDelete}
+ >
+
+
+
+
);
}
-export default InvoiceList;
+export default compose(
+ withResourceActions,
+ withInvoiceActions,
+ withDashboardActions,
+ withViewsActions,
+ withInvoices(({ invoicesTableQuery }) => ({
+ invoicesTableQuery,
+ })),
+)(InvoiceList);
diff --git a/client/src/containers/Sales/Invoice/InvoiceViewTabs.js b/client/src/containers/Sales/Invoice/InvoiceViewTabs.js
new file mode 100644
index 000000000..fcb9ace55
--- /dev/null
+++ b/client/src/containers/Sales/Invoice/InvoiceViewTabs.js
@@ -0,0 +1,113 @@
+import React, { useEffect, useRef } from 'react';
+import { useHistory } from 'react-router';
+import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
+import { useParams, withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import { pick, debounce } from 'lodash';
+
+import { DashboardViewsTabs } from 'components';
+import { useUpdateEffect } from 'hooks';
+
+import withInvoices from './withInvoices';
+import withInvoiceActions from './withInvoiceActions';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withViewDetails from 'containers/Views/withViewDetails';
+
+import { compose } from 'utils';
+
+function InvoiceViewTabs({
+ //#withInvoices
+ invoicesViews,
+
+ // #withViewDetails
+ viewItem,
+
+ //#withInvoiceActions
+ changeInvoiceView,
+ addInvoiceTableQueries,
+
+ // #withDashboardActions
+ setTopbarEditView,
+ changePageSubtitle,
+
+ // #ownProps
+ customViewChanged,
+ onViewChanged,
+}) {
+ const history = useHistory();
+ const { custom_view_id: customViewId = null } = useParams();
+
+ useEffect(() => {
+ changeInvoiceView(customViewId || -1);
+ setTopbarEditView(customViewId);
+ changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
+
+ addInvoiceTableQueries({
+ custom_view_id: customViewId,
+ });
+ return () => {
+ setTopbarEditView(null);
+ changePageSubtitle('');
+ changeInvoiceView(null);
+ };
+ }, [customViewId, addInvoiceTableQueries, changeInvoiceView]);
+
+ useUpdateEffect(() => {
+ onViewChanged && onViewChanged(customViewId);
+ }, [customViewId]);
+
+
+
+ const debounceChangeHistory = useRef(
+ debounce((toUrl) => {
+ history.push(toUrl);
+ }, 250),
+ );
+
+ const handleTabsChange = (viewId) => {
+ const toPath = viewId ? `${viewId}/custom_view` : '';
+ debounceChangeHistory.current(`/invoices/${toPath}`);
+ setTopbarEditView(viewId);
+ };
+ const tabs = invoicesViews.map((view) => ({
+ ...pick(view, ['name', 'id']),
+ }));
+
+ // Handle click a new view tab.
+ const handleClickNewView = () => {
+ setTopbarEditView(null);
+ history.push('/custom_views/invoices/new');
+ };
+ console.log(invoicesViews, 'invoicesViews');
+
+ return (
+
+
+
+
+
+ );
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ viewId: ownProps.match.params.custom_view_id,
+});
+
+const withInvoicesViewTabs = connect(mapStateToProps);
+
+export default compose(
+ withRouter,
+ withInvoicesViewTabs,
+ withInvoiceActions,
+ withDashboardActions,
+ withViewDetails(),
+ withInvoices(({ invoicesViews }) => ({
+ invoicesViews,
+ })),
+)(InvoiceViewTabs);
diff --git a/client/src/containers/Sales/Invoice/Invoices.js b/client/src/containers/Sales/Invoice/Invoices.js
index e5804c8f4..d41a83fe5 100644
--- a/client/src/containers/Sales/Invoice/Invoices.js
+++ b/client/src/containers/Sales/Invoice/Invoices.js
@@ -11,31 +11,50 @@ import withInvoiceActions from './withInvoiceActions';
import { compose } from 'utils';
-function Invoices({ requestFetchCustomers, requestFetchItems }) {
+function Invoices({
+ requestFetchCustomers,
+ requestFetchItems,
+ requsetFetchInvoice,
+}) {
const history = useHistory();
const { id } = useParams();
// Handle fetch Items data table or list
const fetchItems = useQuery('items-table', () => requestFetchItems({}));
- const handleFormSubmit = useCallback((payload) => {}, [history]);
-
+ const handleFormSubmit = useCallback(
+ (payload) => {
+ payload.redirect && history.push('/invoices');
+ },
+ [history],
+ );
// Handle fetch customers data table or list
const fetchCustomers = useQuery('customers-table', () =>
requestFetchCustomers({}),
);
+ const fetchInvoice = useQuery(
+ ['invoice', id],
+ (key, _id) => requsetFetchInvoice(_id),
+ { enabled: !!id },
+ );
+
const handleCancel = useCallback(() => {
history.goBack();
}, [history]);
return (
diff --git a/client/src/containers/Sales/Invoice/InvoicesDataTable.js b/client/src/containers/Sales/Invoice/InvoicesDataTable.js
index 6a937410d..83cc6625d 100644
--- a/client/src/containers/Sales/Invoice/InvoicesDataTable.js
+++ b/client/src/containers/Sales/Invoice/InvoicesDataTable.js
@@ -4,35 +4,36 @@ import {
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 { DataTable, Money, Icon } from 'components';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
-import witInvoice from './withInvoice';
+import withInvoices from './withInvoices';
import withInvoiceActions from './withInvoiceActions';
+import withCurrentView from 'containers/Views/withCurrentView';
function InvoicesDataTable({
//#withInvoices
+ invoicesCurrentPage,
+ invoicesLoading,
+ invoicesPageination,
+ invoicesItems,
// #withDashboardActions
changeCurrentView,
@@ -45,8 +46,8 @@ function InvoicesDataTable({
//#OwnProps
loading,
onFetchData,
- onEditEstimate,
- onDeleteEstimate,
+ onEditInvoice,
+ onDeleteInvoice,
onSelectedRowsChange,
}) {
const [initialMount, setInitialMount] = useState(false);
@@ -55,79 +56,120 @@ function InvoicesDataTable({
useEffect(() => {
setInitialMount(false);
- }, []);
+ }, [customViewId]);
- useEffect(() => {
- if (customViewId) {
- changeCurrentView(customViewId);
- setTopbarEditView(customViewId);
+ useUpdateEffect(() => {
+ if (!invoicesLoading) {
+ setInitialMount(true);
}
- changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
- }, [
- customViewId,
- changeCurrentView,
- changePageSubtitle,
- setTopbarEditView,
- viewMeta,
- ]);
+ }, [invoicesLoading, setInitialMount]);
+
+ // useEffect(() => {
+ // if (customViewId) {
+ // changeCurrentView(customViewId);
+ // setTopbarEditView(customViewId);
+ // }
+ // changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
+ // }, [
+ // customViewId,
+ // changeCurrentView,
+ // changePageSubtitle,
+ // setTopbarEditView,
+ // viewMeta,
+ // ]);
const handleEditInvoice = useCallback(
- (_invoice) => {
+ (_invoice) => () => {
onEditInvoice && onEditInvoice(_invoice);
},
[onEditInvoice],
);
- const handleDeleteInvoice = useCallback(() => {
- onDeleteInvoice && onDeleteInvoice();
- }, [onDeleteInvoice]);
+ const handleDeleteInvoice = useCallback(
+ (_invoice) => () => {
+ onDeleteInvoice && onDeleteInvoice(_invoice);
+ },
+ [onDeleteInvoice],
+ );
- const actionsMenuList = useCallback(
- (invoice) => {
+ const actionMenuList = useCallback(
+ (invoice) => (
;
- },
+ }
+ />
+
+ ),
[handleDeleteInvoice, handleEditInvoice, formatMessage],
);
+ const onRowContextMenu = useCallback(
+ (cell) => {
+ return actionMenuList(cell.row.original);
+ },
+ [actionMenuList],
+ );
+
const columns = useMemo(
() => [
{
- id: '',
- Header: formatMessage({ id: '' }),
- accessor: '',
- className: '',
+ id: 'invoice_date',
+ Header: formatMessage({ id: 'invoice_date' }),
+ accessor: (r) => moment(r.invoice_date).format('YYYY MMM DD'),
+ width: 140,
+ className: 'invoice_date',
},
{
- id: '',
- Header: formatMessage({ id: '' }),
- accessor: '',
- className: '',
+ id: 'customer_id',
+ Header: formatMessage({ id: 'customer_name' }),
+ accessor: (row) => row.customer_id,
+ width: 140,
+ className: 'customer_id',
},
{
- id: '',
- Header: formatMessage({ id: '' }),
- accessor: '',
- className: '',
+ id: 'invoice_no',
+ Header: formatMessage({ id: 'invoice_no__' }),
+ accessor: (row) => `#${row.invoice_no}`,
+ width: 140,
+ className: 'invoice_no',
+ },
+
+ {
+ id: 'due_date',
+ Header: formatMessage({ id: 'due_date' }),
+ accessor: (r) => moment(r.due_date).format('YYYY MMM DD'),
+ width: 140,
+ className: 'due_date',
},
{
- id: '',
- Header: formatMessage({ id: '' }),
- accessor: '',
- className: '',
+ id: 'balance',
+ Header: formatMessage({ id: 'balance' }),
+ accessor: (r) => ,
+ width: 140,
+ className: 'balance',
},
{
- id: '',
- Header: formatMessage({ id: '' }),
- accessor: '',
- className: '',
+ id: 'reference_no',
+ Header: formatMessage({ id: 'reference_no' }),
+ accessor: 'reference_no',
+ width: 140,
+ className: 'reference_no',
+ },
+ {
+ id: 'status',
+ Header: formatMessage({ id: 'status' }),
+ accessor: 'status',
+ width: 140,
+ className: 'status',
},
{
id: 'actions',
@@ -149,12 +191,11 @@ function InvoicesDataTable({
);
const handleDataTableFetchData = useCallback(
- (...arguments) => {
- onFetchData && onFetchData(...arguments);
+ (...args) => {
+ onFetchData && onFetchData(...args);
},
[onFetchData],
);
-
const handleSelectedRowsChange = useCallback(
(selectedRows) => {
onSelectedRowsChange &&
@@ -163,14 +204,33 @@ function InvoicesDataTable({
[onSelectedRowsChange],
);
+ const selectionColumn = useMemo(
+ () => ({
+ minWidth: 40,
+ width: 40,
+ maxWidth: 40,
+ }),
+ [],
+ );
+
return (
-
+
@@ -179,11 +239,16 @@ function InvoicesDataTable({
export default compose(
withRouter,
+ withCurrentView,
withDialogActions,
withDashboardActions,
withInvoiceActions,
- // withInvoices(({})=>({
-
- // }))
+ withInvoices(
+ ({ invoicesCurrentPage, invoicesLoading, invoicesPageination }) => ({
+ invoicesCurrentPage,
+ invoicesLoading,
+ invoicesPageination,
+ }),
+ ),
withViewDetails(),
)(InvoicesDataTable);
diff --git a/client/src/containers/Sales/Invoice/withInvoiceActions.js b/client/src/containers/Sales/Invoice/withInvoiceActions.js
index f00d5cf8f..0efec2c14 100644
--- a/client/src/containers/Sales/Invoice/withInvoiceActions.js
+++ b/client/src/containers/Sales/Invoice/withInvoiceActions.js
@@ -11,14 +11,14 @@ 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 })),
+ 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,
+ type: t.INVOICES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
addInvoiceTableQueries: (_queries) =>
diff --git a/client/src/containers/Sales/Invoice/withInvoiceDetail.js b/client/src/containers/Sales/Invoice/withInvoiceDetail.js
index c7f8623e9..421cfd9ce 100644
--- a/client/src/containers/Sales/Invoice/withInvoiceDetail.js
+++ b/client/src/containers/Sales/Invoice/withInvoiceDetail.js
@@ -1,9 +1,9 @@
import { connect } from 'react-redux';
-import { getInvoiceById } from 'store/Invoice/invoices.selector';
+import { getInvoiecsByIdFactory } from 'store/Invoice/invoices.selector';
export default () => {
- const getInvoiceById = getInvoiceById();
-
+ const getInvoiceById = getInvoiecsByIdFactory();
+
const mapStateToProps = (state, props) => ({
invoice: getInvoiceById(state, props),
});
diff --git a/client/src/containers/Sales/Invoice/withInvoices.js b/client/src/containers/Sales/Invoice/withInvoices.js
new file mode 100644
index 000000000..e9b399d70
--- /dev/null
+++ b/client/src/containers/Sales/Invoice/withInvoices.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { getResourceViews } from 'store/customViews/customViews.selectors';
+import {
+ getInvoiceCurrentPageFactory,
+ getInvoicePaginationMetaFactory,
+ getInvoiceTableQuery,
+} from 'store/Invoice/invoices.selector';
+
+export default (mapState) => {
+ const getInvoicesItems = getInvoiceCurrentPageFactory();
+ const getInvoicesPaginationMeta = getInvoicePaginationMetaFactory();
+ const mapStateToProps = (state, props) => {
+ const query = getInvoiceTableQuery(state, props);
+ const mapped = {
+ invoicesCurrentPage: getInvoicesItems(state, props, query),
+ invoicesViews: getResourceViews(state, props, 'sales_invoices'),
+ invoicesItems: state.sales_invoices.items,
+ invoicesTableQuery: query,
+ invoicesPageination: getInvoicesPaginationMeta(state, props, query),
+ invoicesLoading: state.sales_invoices.loading,
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/Sales/Receipt/ReceiptActionsBar.js b/client/src/containers/Sales/Receipt/ReceiptActionsBar.js
index e69de29bb..107ae0988 100644
--- a/client/src/containers/Sales/Receipt/ReceiptActionsBar.js
+++ b/client/src/containers/Sales/Receipt/ReceiptActionsBar.js
@@ -0,0 +1,149 @@
+import React, { useCallback, useState, useMemo } 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, useIntl } from 'react-intl';
+
+import { connect } from 'react-redux';
+import { If, DashboardActionViewsList } 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 withReceiptActions from './withReceipActions';
+import withReceipts from './withReceipts';
+
+import { compose } from 'utils';
+
+function ReceiptActionsBar({
+ // #withResourceDetail
+ resourceFields,
+
+ //#withReceipts
+ receiptview,
+ //#withReceiptActions
+ addReceiptsTableQueries,
+
+ //#OWn Props
+ onFilterChanged,
+ selectedRows = [],
+}) {
+ const { path } = useRouteMatch();
+ const history = useHistory();
+ const [filterCount, setFilterCount] = useState(0);
+ const { formatMessage } = useIntl();
+
+ const onClickNewReceipt = useCallback(() => {
+ history.push('/receipts/new');
+ }, [history]);
+
+ // const filterDropdown = FilterDropdown({
+ // initialCondition: {
+ // fieldKey: '',
+ // compatator: '',
+ // value: '',
+ // },
+ // fields: resourceFields,
+ // onFilterChange: (filterConditions) => {
+ // addReceiptsTableQueries({
+ // filter_roles: filterConditions || '',
+ // });
+ // onFilterChanged && onFilterChange(filterConditions);
+ // },
+ // });
+
+ const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
+ selectedRows,
+ ]);
+
+ return (
+
+
+
+
+
+ }
+ text={}
+ onClick={onClickNewReceipt}
+ />
+
+
+ ) : (
+ `${filterCount} ${formatMessage({ id: 'filters_applied' })}`
+ )
+ }
+ icon={}
+ />
+
+
+ }
+ text={}
+ intent={Intent.DANGER}
+ />
+
+ }
+ text={}
+ />
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+const mapStateToProps = (state, props) => ({
+ resourceName: 'sales_receipts',
+});
+
+const withReceiptActionsBar = connect(mapStateToProps);
+
+export default compose(
+ withReceiptActionsBar,
+ withResourceDetail(({ resourceFields }) => ({
+ resourceFields,
+ })),
+ withReceipts(({ receiptview }) => ({
+ receiptview,
+ })),
+ withReceiptActions,
+)(ReceiptActionsBar);
diff --git a/client/src/containers/Sales/Receipt/ReceiptForm.js b/client/src/containers/Sales/Receipt/ReceiptForm.js
index 57c3ee7ac..8ac82ab52 100644
--- a/client/src/containers/Sales/Receipt/ReceiptForm.js
+++ b/client/src/containers/Sales/Receipt/ReceiptForm.js
@@ -5,11 +5,11 @@ import React, {
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';
@@ -17,9 +17,10 @@ import ReceiptFromHeader from './ReceiptFormHeader';
import EstimatesItemsTable from 'containers/Sales/Estimate/EntriesItemsTable';
import ReceiptFormFooter from './ReceiptFormFooter';
+import withReceipActions from './withReceipActions';
+import withReceiptDetail from './withReceiptDetail';
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';
@@ -36,21 +37,22 @@ function ReceiptForm({
//#withReceiptActions
requestSubmitReceipt,
+ requestEditReceipt,
+
+ //#withReceiptDetail
+ receipt,
//#withDashboard
changePageTitle,
changePageSubtitle,
- //#withReceiptDetail
- receipt,
-
//#own Props
receiptId,
onFormSubmit,
onCancelForm,
}) {
const { formatMessage } = useIntl();
- const [payload, setPaload] = useState({});
+ const [payload, setPayload] = useState({});
const {
setFiles,
@@ -87,9 +89,9 @@ function ReceiptForm({
receipt_date: Yup.date()
.required()
.label(formatMessage({ id: 'receipt_date_' })),
- receipt_no: Yup.number()
- .required()
- .label(formatMessage({ id: 'receipt_no_' })),
+ // receipt_no: Yup.number()
+ // .required()
+ // .label(formatMessage({ id: 'receipt_no_' })),
deposit_account_id: Yup.number()
.required()
.label(formatMessage({ id: 'deposit_account_' })),
@@ -99,7 +101,7 @@ function ReceiptForm({
.min(1)
.max(1024)
.label(formatMessage({ id: 'receipt_message_' })),
- send_to_email: Yup.string().email(),
+ email_send_to: Yup.string().email().nullable(),
statement: Yup.string()
.trim()
.min(1)
@@ -121,7 +123,7 @@ function ReceiptForm({
}),
),
});
- const saveReceiptSubmit = useCallback(
+ const saveInvokeSubmit = useCallback(
(payload) => {
onFormSubmit && onFormSubmit(payload);
},
@@ -133,7 +135,7 @@ function ReceiptForm({
index: 0,
item_id: null,
rate: null,
- discount: null,
+ discount: 0,
quantity: null,
description: '',
}),
@@ -145,7 +147,7 @@ function ReceiptForm({
customer_id: '',
deposit_account_id: '',
receipt_date: moment(new Date()).format('YYYY-MM-DD'),
- send_to_email: '',
+ email_send_to: '',
reference_no: '',
receipt_message: '',
statement: '',
@@ -163,10 +165,25 @@ function ReceiptForm({
const initialValues = useMemo(
() => ({
- ...defaultInitialValues,
- entries: orderingIndex(defaultInitialValues.entries),
+ ...(receipt
+ ? {
+ ...pick(receipt, Object.keys(defaultInitialValues)),
+ entries: [
+ ...receipt.entries.map((receipt) => ({
+ ...pick(receipt, Object.keys(defaultReceipt)),
+ })),
+ ...repeatValue(
+ defaultReceipt,
+ Math.max(MIN_LINES_NUMBER - receipt.entries.length, 0),
+ ),
+ ],
+ }
+ : {
+ ...defaultInitialValues,
+ entries: orderingIndex(defaultInitialValues.entries),
+ }),
}),
- [defaultReceipt, defaultInitialValues, receipt],
+ [receipt, defaultInitialValues, defaultReceipt],
);
const initialAttachmentFiles = useMemo(() => {
@@ -186,49 +203,45 @@ function ReceiptForm({
...initialValues,
},
onSubmit: async (values, { setErrors, setSubmitting, resetForm }) => {
- const entries = values.entries.map(
- ({ item_id, quantity, rate, description }) => ({
- item_id,
- quantity,
- rate,
- description,
- }),
+ const entries = values.entries.filter(
+ (item) => item.item_id && item.quantity,
);
const form = {
...values,
entries,
};
- const saveReceipt = (mediaIds) =>
- new Promise((resolve, reject) => {
- const requestForm = { ...form, media_ids: mediaIds };
+ const requestForm = { ...form };
- 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);
+ if (receipt && receipt.id) {
+ requestEditReceipt(receipt.id && requestForm).then(() => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_receipt_has_been_successfully_edited',
+ }),
+ intent: Intent.SUCCESS,
+ });
+ setSubmitting(false);
+ saveInvokeSubmit({ action: 'update', ...payload });
+ resetForm();
+ });
+ } else {
+ requestSubmitReceipt(requestForm)
+ .then((response) => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_receipt_has_been_successfully_created',
+ }),
+ intent: Intent.SUCCESS,
});
- });
- 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);
- });
+ setSubmitting(false);
+ saveInvokeSubmit({ action: 'new', ...payload });
+ resetForm();
+ })
+ .catch((errors) => {
+ setSubmitting(false);
+ });
+ }
},
});
@@ -245,10 +258,10 @@ function ReceiptForm({
const handleSubmitClick = useCallback(
(payload) => {
- setPaload(payload);
+ setPayload(payload);
formik.submitForm();
},
- [setPaload, formik],
+ [setPayload, formik],
);
const handleCancelClick = useCallback(
@@ -308,12 +321,13 @@ function ReceiptForm({
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
/>
-
+
);
}
@@ -322,4 +336,5 @@ export default compose(
withReceipActions,
withDashboardActions,
withMediaActions,
+ withReceiptDetail(),
)(ReceiptForm);
diff --git a/client/src/containers/Sales/Receipt/ReceiptFormFooter.js b/client/src/containers/Sales/Receipt/ReceiptFormFooter.js
index e25ec3228..26c09555e 100644
--- a/client/src/containers/Sales/Receipt/ReceiptFormFooter.js
+++ b/client/src/containers/Sales/Receipt/ReceiptFormFooter.js
@@ -6,11 +6,19 @@ export default function ReceiptFormFooter({
formik: { isSubmitting },
onSubmitClick,
onCancelClick,
+ receipt,
}) {
return (
-
-
+ {
+ onSubmitClick({ redirect: true });
+ }}
+ >
+ {receipt && receipt.id ? : }
{
+ onSubmitClick({ redirect: false });
+ }}
>
diff --git a/client/src/containers/Sales/Receipt/ReceiptFormHeader.js b/client/src/containers/Sales/Receipt/ReceiptFormHeader.js
index 443d6f8f5..871ef41a6 100644
--- a/client/src/containers/Sales/Receipt/ReceiptFormHeader.js
+++ b/client/src/containers/Sales/Receipt/ReceiptFormHeader.js
@@ -157,7 +157,7 @@ function ReceiptFormHeader({
{/* receipt_no */}
- }
inline={true}
className={('form-group--receipt_no', Classes.FILL)}
@@ -170,7 +170,7 @@ function ReceiptFormHeader({
minimal={true}
{...getFieldProps('receipt_no')}
/>
-
+ */}
}
@@ -189,12 +189,12 @@ function ReceiptFormHeader({
label={}
inline={true}
className={classNames('form-group--send_to_email', Classes.FILL)}
- intent={errors.send_to_email && touched.send_to_email && Intent.DANGER}
+ intent={errors.email_send_to && touched.email_send_to && Intent.DANGER}
helperText={}
>
+ requestFetchReceiptsTable(),
+ );
+
+ const fetchResourceViews = useQuery(
+ ['resource-views', 'sales_receipts'],
+ (key, resourceName) => requestFetchResourceViews(resourceName),
+ );
+
+ const fetchResourceFields = useQuery(
+ ['resource-fields', 'sales_receipts'],
+ (key, resourceName) => requestFetchResourceFields(resourceName),
+ );
+
+ useEffect(() => {
+ changePageTitle(formatMessage({ id: 'receipt_list' }));
+ }, [changePageTitle, formatMessage]);
+
+ // handle delete receipt click
+ const handleDeleteReceipt = useCallback(
+ (_receipt) => {
+ setDeleteReceipt(_receipt);
+ },
+ [setDeleteReceipt],
+ );
+
+ // handle cancel receipt
+ const handleCancelReceiptDelete = useCallback(() => {
+ setDeleteReceipt(false);
+ }, [setDeleteReceipt]);
+
+ // handle confirm delete receipt
+ const handleConfirmReceiptDelete = useCallback(() => {
+ requestDeleteReceipt(deleteReceipt.id).then(() => {
+ AppToaster.show({
+ message: formatMessage({
+ id: 'the_receipt_has_been_successfully_deleted',
+ }),
+ intent: Intent.SUCCESS,
+ });
+ setDeleteReceipt(false);
+ });
+ }, [deleteReceipt, requestDeleteReceipt, formatMessage]);
+
+ // Handle filter change to re-fetch data-table.
+ // const handleFilterChanged = useCallback(
+ // (filterConditions) => {
+ // addReceiptsTableQueries({
+ // filter_roles: filterConditions || '',
+ // });
+ // },
+ // [fetchReceipt],
+ // );
+
+ // Handle filter change to re-fetch data-table.
+ const handleFilterChanged = useCallback(() => {}, [fetchReceipts]);
+
+
+
+ // Calculates the selected rows
+ const selectedRowsCount = useMemo(() => Object.values(selectedRows).length, [
+ selectedRows,
+ ]);
+
+ const handleEditReceipt = useCallback(
+ (receipt) => {
+ history.push(`/receipts/${receipt.id}/edit`);
+ },
+ [history],
+ );
+ const handleFetchData = useCallback(
+ ({ pageIndex, pageSize, sortBy }) => {
+ const page = pageIndex + 1;
+
+ addReceiptsTableQueries({
+ ...(sortBy.length > 0
+ ? {
+ column_sort_by: sortBy[0].id,
+ sort_order: sortBy[0].desc ? 'desc' : 'asc',
+ }
+ : {}),
+ page_size: pageSize,
+ page,
+ });
+ },
+ [addReceiptsTableQueries],
+ );
+
+ const handleSelectedRowsChange = useCallback(
+ (estimate) => {
+ setSelectedRows(estimate);
+ },
+ [setSelectedRows],
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ confirmButtonText={}
+ icon={'trash'}
+ intent={Intent.DANGER}
+ isOpen={deleteReceipt}
+ onCancel={handleCancelReceiptDelete}
+ onConfirm={handleConfirmReceiptDelete}
+ >
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withResourceActions,
+ withReceipActions,
+ withDashboardActions,
+ withViewsActions,
+ withReceipts(({ receiptTableQuery }) => ({
+ receiptTableQuery,
+ })),
+)(ReceiptList);
diff --git a/client/src/containers/Sales/Receipt/ReceiptViewTabs.js b/client/src/containers/Sales/Receipt/ReceiptViewTabs.js
new file mode 100644
index 000000000..40b8ab424
--- /dev/null
+++ b/client/src/containers/Sales/Receipt/ReceiptViewTabs.js
@@ -0,0 +1,109 @@
+import React, { useEffect, useRef } from 'react';
+import { useHistory } from 'react-router';
+import { Alignment, Navbar, NavbarGroup } from '@blueprintjs/core';
+import { useParams, withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import { pick, debounce } from 'lodash';
+
+import { DashboardViewsTabs } from 'components';
+import { useUpdateEffect } from 'hooks';
+
+import withReceipts from './withReceipts';
+import withReceiptActions from './withReceipActions';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withViewDetails from 'containers/Views/withViewDetails';
+
+import { compose } from 'utils';
+
+function ReceiptViewTabs({
+ //#withReceipts
+ receiptview,
+ // #withViewDetails
+ viewItem,
+
+ //#withReceiptActions
+ changeReceiptView,
+ addReceiptsTableQueries,
+
+ // #withDashboardActions
+ setTopbarEditView,
+ changePageSubtitle,
+
+ //# own Props
+ customViewChanged,
+ onViewChanged,
+}) {
+ const history = useHistory();
+ const { custom_view_id: customViewId = null } = useParams();
+
+ useEffect(() => {
+ changeReceiptView(customViewId || -1);
+ setTopbarEditView(customViewId);
+ changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
+
+ addReceiptsTableQueries({
+ custom_view_id: customViewId,
+ });
+ return () => {
+ setTopbarEditView(null);
+ changePageSubtitle('');
+ changeReceiptView(null);
+ };
+ }, [customViewId, addReceiptsTableQueries, changeReceiptView]);
+
+ useUpdateEffect(() => {
+ onViewChanged && onViewChanged(customViewId);
+ }, [customViewId]);
+
+ const debounceChangeHistory = useRef(
+ debounce((toUrl) => {
+ history.push(toUrl);
+ }, 250),
+ );
+
+ const handleTabsChange = (viewId) => {
+ const toPath = viewId ? `${viewId}/custom_view` : '';
+ debounceChangeHistory.current(`/receipts/${toPath}`);
+ setTopbarEditView(viewId);
+ };
+ const tabs = receiptview.map((view) => ({
+ ...pick(view, ['name', 'id']),
+ }));
+
+ // Handle click a new view tab.
+ const handleClickNewView = () => {
+ setTopbarEditView(null);
+ history.push('/custom_views/receipts/new');
+ };
+
+ console.log(receiptview, 'receiptview');
+
+ return (
+
+
+
+
+
+ );
+}
+
+const mapStateToProps = (state, ownProps) => ({
+ viewId: ownProps.match.params.custom_view_id,
+});
+
+const withReceiptsViewTabs = connect(mapStateToProps);
+
+export default compose(
+ withRouter,
+ withReceiptsViewTabs,
+ withReceiptActions,
+ withDashboardActions,
+ withViewDetails(),
+ withReceipts(({ receiptview }) => ({ receiptview })),
+)(ReceiptViewTabs);
diff --git a/client/src/containers/Sales/Receipt/Receipts.js b/client/src/containers/Sales/Receipt/Receipts.js
index 473d3cb8a..65dc6f1fc 100644
--- a/client/src/containers/Sales/Receipt/Receipts.js
+++ b/client/src/containers/Sales/Receipt/Receipts.js
@@ -8,6 +8,7 @@ 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 withReceipActions from './withReceipActions';
import { compose } from 'utils';
@@ -20,10 +21,18 @@ function Receipts({
//#withItemsActions
requestFetchItems,
+
+ //#withReceiptsActions
+ requsetFetchInvoice,
}) {
const history = useHistory();
const { id } = useParams();
+ const fetchReceipt = useQuery(
+ ['receipt', id],
+ (key, _id) => requsetFetchInvoice(_id),
+ { enabled: !!id },
+ );
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
@@ -35,7 +44,12 @@ function Receipts({
// Handle fetch Items data table or list
const fetchItems = useQuery('items-table', () => requestFetchItems({}));
- const handleFormSubmit = useCallback((payload) => {}, [history]);
+ const handleFormSubmit = useCallback(
+ (payload) => {
+ payload.redirect && history.push('/receipts');
+ },
+ [history],
+ );
const handleCancel = useCallback(() => {
history.goBack();
@@ -46,12 +60,14 @@ function Receipts({
loading={
fetchCustomers.isFetching ||
fetchItems.isFetching ||
- fetchAccounts.isFetching
+ fetchAccounts.isFetching||
+ fetchReceipt.isFetching
}
+ name={'receipt-form'}
>
@@ -59,6 +75,7 @@ function Receipts({
}
export default compose(
+ withReceipActions,
withCustomersActions,
withItemsActions,
withAccountsActions,
diff --git a/client/src/containers/Sales/Receipt/ReceiptsDataTable.js b/client/src/containers/Sales/Receipt/ReceiptsDataTable.js
new file mode 100644
index 000000000..4c64e8c88
--- /dev/null
+++ b/client/src/containers/Sales/Receipt/ReceiptsDataTable.js
@@ -0,0 +1,247 @@
+import React, { useEffect, useCallback, useState, useMemo } from 'react';
+import {
+ Intent,
+ Button,
+ Popover,
+ Menu,
+ MenuItem,
+ MenuDivider,
+ Position,
+} 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 { compose } from 'utils';
+import { useUpdateEffect } from 'hooks';
+
+import LoadingIndicator from 'components/LoadingIndicator';
+import { DataTable, Money, Icon } from 'components';
+
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+import withViewDetails from 'containers/Views/withViewDetails';
+
+import withReceipts from './withReceipts';
+import withReceipActions from './withReceipActions';
+import withCurrentView from 'containers/Views/withCurrentView';
+
+function ReceiptsDataTable({
+ //#withReceipts
+ receiptsCurrentPage,
+ receiptsLoading,
+ receiptsPagination,
+ receiptItems,
+ // #withDashboardActions
+ changeCurrentView,
+ changePageSubtitle,
+ setTopbarEditView,
+
+ // #withView
+ viewMeta,
+
+ // #Own Props
+
+ loading,
+ onFetchData,
+ onEditReceipt,
+ onDeleteReceipt,
+ onSelectedRowsChange,
+}) {
+ const [initialMount, setInitialMount] = useState(false);
+ const { custom_view_id: customViewId } = useParams();
+ const { formatMessage } = useIntl();
+
+ useUpdateEffect(() => {
+ if (!receiptsLoading) {
+ setInitialMount(true);
+ }
+ }, [receiptsLoading, setInitialMount]);
+
+ useEffect(() => {
+ setInitialMount(false);
+ }, []);
+
+ useEffect(() => {
+ if (customViewId) {
+ changeCurrentView(customViewId);
+ setTopbarEditView(customViewId);
+ }
+ changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
+ }, [
+ customViewId,
+ changeCurrentView,
+ changePageSubtitle,
+ setTopbarEditView,
+ viewMeta,
+ ]);
+
+ const handleEditReceipt = useCallback(
+ (receipt) => () => {
+ onEditReceipt && onEditReceipt(receipt);
+ },
+ [onEditReceipt],
+ );
+
+ const handleDeleteReceipt = useCallback(
+ (receipt) => () => {
+ onDeleteReceipt && onDeleteReceipt(receipt);
+ },
+ [onDeleteReceipt],
+ );
+
+ const actionMenuList = useCallback(
+ (estimate) => (
+
+ ),
+ [handleDeleteReceipt, handleEditReceipt, formatMessage],
+ );
+
+ const onRowContextMenu = useCallback(
+ (cell) => {
+ return actionMenuList(cell.row.original);
+ },
+ [actionMenuList],
+ );
+
+ const columns = useMemo(
+ () => [
+ {
+ id: 'receipt_date',
+ Header: formatMessage({ id: 'receipt_date' }),
+ accessor: (r) => moment(r.receipt_date).format('YYYY MMM DD'),
+ width: 140,
+ className: 'receipt_date',
+ },
+ {
+ id: 'customer_id',
+ Header: formatMessage({ id: 'customer_name' }),
+ accessor: (row) => row.customer_id,
+ width: 140,
+ className: 'customer_id',
+ },
+ {
+ id: 'deposit_account_id',
+ Header: formatMessage({ id: 'deposit_account' }),
+ accessor: 'deposit_account.name',
+ width: 140,
+ className: 'deposit_account',
+ },
+ {
+ id: 'email_send_to',
+ Header: formatMessage({ id: 'email' }),
+ accessor: 'email_send_to',
+ width: 140,
+ className: 'email_send_to',
+ },
+ {
+ id: 'amount',
+ Header: formatMessage({ id: 'amount' }),
+ accessor: (r) => ,
+
+ width: 140,
+ className: 'amount',
+ },
+ {
+ id: 'reference_no',
+ Header: formatMessage({ id: 'reference_no' }),
+ accessor: 'reference_no',
+ width: 140,
+ className: 'reference_no',
+ },
+ {
+ id: 'actions',
+ Header: '',
+ Cell: ({ cell }) => (
+
+ } />
+
+ ),
+ className: 'actions',
+ width: 50,
+ disableResizing: true,
+ },
+ ],
+ [actionMenuList, formatMessage],
+ );
+
+ const handleDataTableFetchData = useCallback(
+ (...args) => {
+ onFetchData && onFetchData(...args);
+ },
+ [onFetchData],
+ );
+
+ const handleSelectedRowsChange = useCallback(
+ (selectedRows) => {
+ onSelectedRowsChange &&
+ onSelectedRowsChange(selectedRows.map((s) => s.original));
+ },
+ [onSelectedRowsChange],
+ );
+
+ console.log(receiptsCurrentPage, 'receiptCurrnetPage');
+ console.log(receiptItems, 'receiptItems');
+
+ return (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withRouter,
+ withCurrentView,
+ withDialogActions,
+ withDashboardActions,
+ withReceipActions,
+ withReceipts(
+ ({
+ receiptsCurrentPage,
+ receiptsLoading,
+ receiptsPagination,
+ receiptItems,
+ }) => ({
+ receiptsCurrentPage,
+ receiptsLoading,
+ receiptsPagination,
+ receiptItems,
+ }),
+ ),
+ withViewDetails(),
+)(ReceiptsDataTable);
diff --git a/client/src/containers/Sales/Receipt/withReceipActions.js b/client/src/containers/Sales/Receipt/withReceipActions.js
index 1aebb3884..3d20263fd 100644
--- a/client/src/containers/Sales/Receipt/withReceipActions.js
+++ b/client/src/containers/Sales/Receipt/withReceipActions.js
@@ -11,7 +11,7 @@ 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 })),
+ requestEditReceipt: (id, form) => dispatch(editReceipt( id, form )),
requestDeleteReceipt: (id) => dispatch(deleteReceipt({ id })),
requestFetchReceiptsTable: (query = {}) =>
dispatch(fetchReceiptsTable({ query: { ...query } })),
@@ -19,7 +19,7 @@ const mapDispatchToProps = (dispatch) => ({
changeReceiptView: (id) =>
dispatch({
- type: t.RECEIPT_SET_CURRENT_VIEW,
+ type: t.RECEIPTS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
diff --git a/client/src/containers/Sales/Receipt/withReceiptDetail.js b/client/src/containers/Sales/Receipt/withReceiptDetail.js
new file mode 100644
index 000000000..a2f19dca9
--- /dev/null
+++ b/client/src/containers/Sales/Receipt/withReceiptDetail.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux';
+import { getReceiptByIdFactory } from 'store/receipt/receipt.selector';
+
+export default () => {
+ const getReceiptById = getReceiptByIdFactory();
+
+ const mapStateToProps = (state, props) => ({
+ receipt: getReceiptById(state, props),
+ });
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/Sales/Receipt/withReceipts.js b/client/src/containers/Sales/Receipt/withReceipts.js
new file mode 100644
index 000000000..190defb40
--- /dev/null
+++ b/client/src/containers/Sales/Receipt/withReceipts.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import { getResourceViews } from 'store/customViews/customViews.selectors';
+import {
+ getReceiptCurrentPageFactory,
+ getReceiptsTableQuery,
+ getReceiptsPaginationMetaFactory,
+} from 'store/receipt/receipt.selector';
+
+export default (mapState) => {
+ const getReceiptsItems = getReceiptCurrentPageFactory();
+ const getReceiptPaginationMeta = getReceiptsPaginationMetaFactory();
+
+ const mapStateToProps = (state, props) => {
+ const query = getReceiptsTableQuery(state, props);
+ const mapped = {
+ receiptsCurrentPage: getReceiptsItems(state, props, query),
+ receiptview:getResourceViews(state, props, 'sales_receipts'),
+ receiptItems: state.sales_receipts.items,
+ receiptTableQuery: query,
+ receiptsPagination: getReceiptPaginationMeta(state, props, query),
+ receiptsLoading: state.sales_receipts.loading,
+ };
+
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+};
diff --git a/client/src/containers/Vendors/withVendorActions.js b/client/src/containers/Vendors/withVendorActions.js
new file mode 100644
index 000000000..c387cd400
--- /dev/null
+++ b/client/src/containers/Vendors/withVendorActions.js
@@ -0,0 +1,32 @@
+import { connect } from 'react-redux';
+import {
+ submitVendor,
+ editVendor,
+ deleteVendor,
+ fetchVendorsTable,
+} from 'store/vendors/vendors.actions';
+import t from 'store/types';
+
+
+const mapDipatchToProps = (dispatch) => ({
+ requestSubmitVendor: (form) => dispatch(submitVendor({ form })),
+ requestEditVendor: (id, form) => dispatch(editVendor(id, form)),
+ requestFetchVendorsTable: (query = {}) =>
+ dispatch(fetchVendorsTable({ query: { ...query } })),
+ requestDeleteEstimate: (id) => dispatch(deleteVendor({ id })),
+
+ changeVendorView: (id) =>
+ dispatch({
+ type: t.VENDORS_SET_CURRENT_VIEW,
+ currentViewId: parseInt(id, 10),
+ }),
+
+ addVendorsTableQueries: (queries) =>
+ dispatch({
+ type: t.VENDORS_TABLE_QUERIES_ADD,
+ queries,
+ }),
+});
+
+export default connect(null, mapDipatchToProps);
+
diff --git a/client/src/containers/Vendors/withVendors.js b/client/src/containers/Vendors/withVendors.js
new file mode 100644
index 000000000..88e483be0
--- /dev/null
+++ b/client/src/containers/Vendors/withVendors.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import { getResourceViews } from 'store/customViews/customViews.selectors';
+
+import {
+ getVendorCurrentPageFactory,
+ getVendorsTableQuery,
+ getVendorsPaginationMetaFactory,
+} from 'store/vendors/vendors.selectors';
+
+export default (mapState) => {
+ const getVendorsItems = getVendorCurrentPageFactory();
+ const getVendorsPaginationMeta = getVendorsPaginationMetaFactory();
+ const mapStateToProps = (state, props) => {
+ const query = getVendorsTableQuery(state, props);
+
+ const mapped = {
+ vendorsCurrentPage: getVendorsItems(state, props, query),
+ vendorViews: getResourceViews(state, props, 'vendors'),
+ vendorItems: state.vendors.items,
+ vendorTableQuery: query,
+ vendorsPageination: getVendorsPaginationMeta(state, props, query),
+ vendorsLoading: state.vendors.loading,
+ };
+ return mapState ? mapState(mapped, state, props) : mapped;
+ };
+ return connect(mapStateToProps);
+};
diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js
index a6d645bd6..ebe426542 100644
--- a/client/src/lang/en/index.js
+++ b/client/src/lang/en/index.js
@@ -458,6 +458,7 @@ export default {
display_name_: 'Display name',
new_customer: 'New Customer',
customer_type: 'Customer Type',
+ customer_account: 'Customer Account',
business: 'Business',
individual: 'Individual',
display_name: 'Display Name',
@@ -584,6 +585,7 @@ export default {
quantity: 'Quantity',
rate: 'Rate',
estimate_list: 'Estimate List',
+ estimate_number: 'Estimate Number',
product_and_service: 'Product/Service',
the_estimate_has_been_successfully_edited:
@@ -592,15 +594,21 @@ export default {
'The estimate #{number} has been successfully created.',
the_estimate_has_been_successfully_deleted:
'The estimate has been successfully deleted.',
+
+ once_delete_this_estimate_you_will_able_to_restore_it: `Once you delete this estimate, you won\'t be able to restore it later. Are you sure you want to delete this estimate?`,
+
cannot_be_zero_or_empty: 'cannot be zero or empty.',
invocies: 'Invoices',
+ invoices_list: 'Invoices List',
invoice_date: 'Invoice Date',
due_date: 'Due Date',
invoice_date_: 'Invoice date',
invoice_no: 'Invoice #',
+ invoice_no__: 'Invoice No',
invoice_no_: 'Invoice number',
due_date_: 'Due date',
invoice_message: 'Invoice Message',
+ reference_no: 'Reference No',
edit_invoice: 'Edit Invoice',
delete_invoice: 'Delete Invoice',
@@ -613,6 +621,10 @@ export default {
'The invoice #{number} has been successfully created.',
the_invocie_has_been_successfully_deleted:
'The invoice has been successfully deleted.',
+
+ once_delete_this_invoice_you_will_able_to_restore_it: `Once you delete this invoice, you won\'t be able to restore it later. Are you sure you want to delete this invoice?`,
+
+ receipt_list: 'Receipt List',
receipts: 'Receipts',
receipt: 'Receipt #',
receipt_date_: 'Receipt date',
@@ -621,12 +633,16 @@ export default {
receipt_message_: 'Receipt message',
receipt_no_: 'receipt number',
edit_receipt: 'Edit Receipt',
+ delete_receipt: 'Delete 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',
+
+ once_delete_this_receipt_you_will_able_to_restore_it: `Once you delete this receipt, you won\'t be able to restore it later. Are you sure you want to delete this receipt?`,
+
the_receipt_has_been_successfully_created:
'The recepit has been successfully created.',
the_receipt_has_been_successfully_edited:
@@ -634,6 +650,7 @@ export default {
the_receipt_has_been_successfully_deleted:
'The receipt has been successfully deleted.',
+ bill_list: 'Bill List',
bills: 'Bills',
accept: 'Accept',
vendor_name: 'Vendor Name',
@@ -647,10 +664,19 @@ export default {
bill_date_: 'Bill date',
bill_number_: 'Bill number',
vendor_name_: 'Vendor name',
+ delete_bill: 'Delete Bill',
+
+ the_bill_has_been_successfully_edited:
+ 'The bill #{number} has been successfully edited.',
the_bill_has_been_successfully_created:
'The bill has been successfully created.',
+ the_bill_has_been_successfully_deleted:
+ 'The bill has been successfully deleted.',
+
+ once_delete_this_bill_you_will_able_to_restore_it: `Once you delete this bill, you won\'t be able to restore it later. Are you sure you want to delete this bill?`,
+
edit_payment_receive: 'Edit Payment Receive',
new_payment_receive: 'New Payment Receive',
payment_receives: 'Payment Receives',
@@ -659,7 +685,7 @@ export default {
the_payment_receive_has_been_successfully_created:
'The payment receive has been successfully created.',
- select_invoice:'Select Invoice',
+ select_invoice: 'Select Invoice',
payment_mades: 'Payment Mades',
};
diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js
index c1f10a9bd..90eacfa50 100644
--- a/client/src/routes/dashboard.js
+++ b/client/src/routes/dashboard.js
@@ -220,13 +220,13 @@ export default [
}),
breadcrumb: 'New Estimates',
},
- // {
- // path: `/estimates`,
- // component: LazyLoader({
- // loader: () => import('containers/Sales/EstimatesList'),
- // }),
- // breadcrumb: 'Estimates',
- // },
+ {
+ path: `/estimates`,
+ component: LazyLoader({
+ loader: () => import('containers/Sales/Estimate/EstimateList'),
+ }),
+ breadcrumb: 'Estimates List',
+ },
//Invoices
@@ -245,13 +245,13 @@ export default [
}),
breadcrumb: 'New Invoice',
},
- // {
- // path:`/invoices`,
- // component:LazyLoader({
- // loader:()=>import('containers/Sales/Invoice/Invoices')
- // }),
- // breadcrumb: 'New Invoice',
- // },
+ {
+ path: `/invoices`,
+ component: LazyLoader({
+ loader: () => import('containers/Sales/Invoice/InvoiceList'),
+ }),
+ breadcrumb: 'Invoices List',
+ },
//Receipts
{
@@ -268,16 +268,15 @@ export default [
}),
breadcrumb: 'New Receipt',
},
- // {
- // path: `/receipts`,
- // component: LazyLoader({
- // loader: () => import('containers/Sales/Receipt/Receipts'),
- // }),
- // breadcrumb: 'New Receipt',
- // }
+ {
+ path: `/receipts`,
+ component: LazyLoader({
+ loader: () => import('containers/Sales/Receipt/ReceiptList'),
+ }),
+ breadcrumb: 'Receipt List',
+ },
-
- // Payment Receives
+ // Payment Receives
{
path: `/payment-receive/:id/edit`,
@@ -285,7 +284,6 @@ export default [
loader: () => import('containers/Sales/PaymentReceive/PaymentReceives'),
}),
breadcrumb: 'Edit',
-
},
{
path: `/payment-receive/new`,
@@ -293,24 +291,35 @@ export default [
loader: () => import('containers/Sales/PaymentReceive/PaymentReceives'),
}),
breadcrumb: 'New Payment Receive',
-
},
-
-
//Bills
{
- path: `/bill/:id/edit`,
+ path: `/bills/:id/edit`,
component: LazyLoader({
- loader: () => import('containers/Purchases/Bills'),
+ loader: () => import('containers/Purchases/Bill/Bills'),
}),
breadcrumb: 'Edit',
},
{
- path: `/bill/new`,
+ path: `/bills/new`,
component: LazyLoader({
- loader: () => import('containers/Purchases/Bills'),
+ loader: () => import('containers/Purchases/Bill/Bills'),
}),
breadcrumb: 'New Bill',
},
+ {
+ path: `/bills`,
+ component: LazyLoader({
+ loader: () => import('containers/Purchases/Bill/BillList'),
+ }),
+ breadcrumb: 'Bill List',
+ },
+ {
+ path: `/receipts`,
+ component: LazyLoader({
+ loader: () => import('containers/Sales/Receipt/ReceiptList'),
+ }),
+ breadcrumb: 'Receipt List',
+ },
];
diff --git a/client/src/store/Bills/bills.actions.js b/client/src/store/Bills/bills.actions.js
index 970bca31c..59a4be00e 100644
--- a/client/src/store/Bills/bills.actions.js
+++ b/client/src/store/Bills/bills.actions.js
@@ -4,7 +4,7 @@ import t from 'store/types';
export const fetchBillsTable = ({ query = {} }) => {
return (dispatch, getState) =>
new Promise((resolve, rejcet) => {
- const pageQuery = getState().bill.tableQuery;
+ const pageQuery = getState().bills.tableQuery;
dispatch({
type: t.BILLS_TABLE_LOADING,
@@ -12,7 +12,7 @@ export const fetchBillsTable = ({ query = {} }) => {
loading: true,
},
});
- ApiService.get('bills', {
+ ApiService.get('purchases/bills', {
params: { ...pageQuery, ...query },
})
.then((response) => {
@@ -54,9 +54,9 @@ export const fetchBillsTable = ({ query = {} }) => {
export const deleteBill = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.delete(`bills/${id}`)
+ ApiService.delete(`purchases/bills/${id}`)
.then((response) => {
- dispatch({ type: t.BILL_DELETE });
+ dispatch({ type: t.BILL_DELETE, payload: { id } });
resovle(response);
})
.catch((error) => {
@@ -71,7 +71,7 @@ export const submitBill = ({ form }) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
- ApiService.post('bills', form)
+ ApiService.post('purchases/bills', form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
@@ -92,7 +92,7 @@ export const submitBill = ({ form }) => {
export const fetchBill = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.get(`bills/${id}`)
+ ApiService.get(`purchases/bills/${id}`)
.then((response) => {
dispatch({
type: t.BILL_SET,
@@ -117,7 +117,7 @@ export const editBill = (id, form) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
- ApiService.post(`bills/${id}`, form)
+ ApiService.post(`purchases/bills/${id}`, form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
diff --git a/client/src/store/Bills/bills.reducer.js b/client/src/store/Bills/bills.reducer.js
index e69de29bb..cb99f1ff3 100644
--- a/client/src/store/Bills/bills.reducer.js
+++ b/client/src/store/Bills/bills.reducer.js
@@ -0,0 +1,102 @@
+import { createReducer } from '@reduxjs/toolkit';
+import { createTableQueryReducers } from 'store/queryReducers';
+
+import t from 'store/types';
+
+const initialState = {
+ items: {},
+ views: {},
+ loading: false,
+ currentViewId: -1,
+ tableQuery: {
+ page_size: 5,
+ page: 1,
+ },
+};
+
+const defaultBill = {
+ entries: [],
+};
+
+const reducer = createReducer(initialState, {
+ [t.BILL_SET]: (state, action) => {
+ const { id, bill } = action.payload;
+ const _bill = state.items[id] || {};
+
+ state.items[id] = { ...defaultBill, ..._bill, ...bill };
+ },
+ [t.BILLS_TABLE_LOADING]: (state, action) => {
+ const { loading } = action.payload;
+ state.loading = loading;
+ },
+
+ [t.BILLS_SET_CURRENT_VIEW]: (state, action) => {
+ state.currentViewId = action.currentViewId;
+ },
+
+ [t.BILLS_ITEMS_SET]: (state, action) => {
+ const { bills } = action.payload;
+ const _bills = {};
+ bills.forEach((bill) => {
+ _bills[bill.id] = {
+ ...defaultBill,
+ ...bill,
+ };
+ });
+ state.items = {
+ ...state.items,
+ ..._bills,
+ };
+ },
+
+ [t.BILL_DELETE]: (state, action) => {
+ const { id } = action.payload;
+
+ if (typeof state.items[id] !== 'undefined') {
+ delete state.items[id];
+ }
+ },
+
+ [t.BILLS_PAGE_SET]: (state, action) => {
+ const { customViewId, bills, pagination } = action.payload;
+
+ const viewId = customViewId || -1;
+ const view = state.views[viewId] || {};
+
+ state.views[viewId] = {
+ ...view,
+ pages: {
+ ...(state.views?.[viewId]?.pages || {}),
+ [pagination.page]: {
+ ids: bills.map((i) => i.id),
+ },
+ },
+ };
+ },
+
+ [t.BILLS_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('bills', reducer);
+
diff --git a/client/src/store/Bills/bills.selectors.js b/client/src/store/Bills/bills.selectors.js
index 4b6b8f1d2..95a851235 100644
--- a/client/src/store/Bills/bills.selectors.js
+++ b/client/src/store/Bills/bills.selectors.js
@@ -1,6 +1,52 @@
import { createSelector } from '@reduxjs/toolkit';
+import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
-const billByIdSelector = (state, props) => state.bills.items[props.billId];
+const billTableQuery = (state) => {
+ return state.bills.tableQuery;
+};
-export const getBillById = () =>
- createSelector(billByIdSelector, (_bill) => _bill);
+export const getBillTableQuery = createSelector(
+ paginationLocationQuery,
+ billTableQuery,
+ (locationQuery, tableQuery) => {
+ return {
+ ...locationQuery,
+ ...tableQuery,
+ };
+ },
+);
+
+const billPageSelector = (state, props, query) => {
+ const viewId = state.bills.currentViewId;
+ return state.bills.views?.[viewId]?.pages?.[query.page];
+};
+
+const billItemsSelector = (state) => {
+ return state.bills.items;
+};
+
+export const getBillCurrentPageFactory = () =>
+ createSelector(billPageSelector, billItemsSelector, (billPage, billItems) => {
+ return typeof billPage === 'object'
+ ? pickItemsFromIds(billItems, billPage.ids) || []
+ : [];
+ });
+
+const billByIdSelector = (state, props) => {
+ return state.bills.items[props.billId];
+};
+
+export const getBillByIdFactory = () =>
+ createSelector(billByIdSelector, (bill) => {
+ return bill;
+ });
+
+const billPaginationSelector = (state, props) => {
+ const viewId = state.bills.currentViewId;
+ return state.bills.views?.[viewId];
+};
+
+export const getBillPaginationMetaFactory = () =>
+ createSelector(billPaginationSelector, (billPage) => {
+ return billPage?.paginationMeta || {};
+ });
diff --git a/client/src/store/Estimate/estimates.actions.js b/client/src/store/Estimate/estimates.actions.js
index c67d8ab80..526121679 100644
--- a/client/src/store/Estimate/estimates.actions.js
+++ b/client/src/store/Estimate/estimates.actions.js
@@ -8,6 +8,7 @@ export const submitEstimate = ({ form }) => {
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
ApiService.post('sales/estimates', form)
+
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
@@ -32,7 +33,7 @@ export const editEstimate = (id, form) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
- ApiService.post(`estimates/${id}`, form)
+ ApiService.post(`sales/estimates/${id}`, form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
@@ -54,9 +55,12 @@ export const editEstimate = (id, form) => {
export const deleteEstimate = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.delete(`estimates/${id}`)
+ ApiService.delete(`sales/estimates/${id}`)
.then((response) => {
- dispatch({ type: t.ESTIMATE_DELETE });
+ dispatch({
+ type: t.ESTIMATE_DELETE,
+ payload: { id },
+ });
resovle(response);
})
.catch((error) => {
@@ -68,8 +72,9 @@ export const deleteEstimate = ({ id }) => {
export const fetchEstimate = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.get(`estimate/${id}`)
+ ApiService.get(`sales/estimates/${id}`)
.then((response) => {
+
dispatch({
type: t.ESTIMATE_SET,
payload: {
@@ -90,36 +95,36 @@ export const fetchEstimate = ({ id }) => {
export const fetchEstimatesTable = ({ query = {} }) => {
return (dispatch, getState) =>
new Promise((resolve, rejcet) => {
- const pageQuery = getState().estimates.tableQuery;
-
+ const pageQuery = getState().sales_estimates.tableQuery;
dispatch({
type: t.ESTIMATES_TABLE_LOADING,
payload: {
loading: true,
},
});
- ApiService.get('estimates', {
+ ApiService.get('sales/estimates', {
params: { ...pageQuery, ...query },
})
.then((response) => {
+ // debugger;
dispatch({
type: t.ESTIMATES_PAGE_SET,
payload: {
- estimates: response.data.estimates.results,
- pagination: response.data.estimates.pagination,
+ sales_estimates: response.data.sales_estimates.results,
+ pagination: response.data.sales_estimates.pagination,
customViewId: response.data.customViewId || -1,
},
});
dispatch({
type: t.ESTIMATES_ITEMS_SET,
payload: {
- estimates: response.data.estimates.results,
+ sales_estimates: response.data.sales_estimates.results,
},
});
dispatch({
type: t.ESTIMATES_PAGINATION_SET,
payload: {
- pagination: response.data.estimates.pagination,
+ pagination: response.data.sales_estimates.pagination,
customViewId: response.data.customViewId || -1,
},
});
diff --git a/client/src/store/Estimate/estimates.reducer.js b/client/src/store/Estimate/estimates.reducer.js
index e52a9ce3b..d21c4461a 100644
--- a/client/src/store/Estimate/estimates.reducer.js
+++ b/client/src/store/Estimate/estimates.reducer.js
@@ -8,27 +8,28 @@ const initialState = {
views: {},
loading: false,
tableQuery: {
- page_size: 12,
+ page_size: 5,
page: 1,
},
currentViewId: -1,
};
const defaultEstimate = {
- products: [],
+ entries: [],
};
const reducer = createReducer(initialState, {
[t.ESTIMATE_SET]: (state, action) => {
- const { id, estiamate } = action.payload;
+ const { id, estimate } = action.payload;
const _estimate = state.items[id] || {};
- state.items[id] = { ...defaultEstimate, ..._estimate, ...estiamate };
+ state.items[id] = { ...defaultEstimate, ..._estimate, ...estimate };
},
+
[t.ESTIMATES_ITEMS_SET]: (state, action) => {
- const { estiamates } = action.payload;
+ const { sales_estimates } = action.payload;
const _estimates = {};
- estiamates.forEach((estimate) => {
+ sales_estimates.forEach((estimate) => {
_estimates[estimate.id] = {
...defaultEstimate,
...estimate,
@@ -58,7 +59,7 @@ const reducer = createReducer(initialState, {
},
[t.ESTIMATES_PAGE_SET]: (state, action) => {
- const { customViewId, estimates, pagination } = action.payload;
+ const { customViewId, sales_estimates, pagination } = action.payload;
const viewId = customViewId || -1;
const view = state.views[viewId] || {};
@@ -68,7 +69,7 @@ const reducer = createReducer(initialState, {
pages: {
...(state.views?.[viewId]?.pages || {}),
[pagination.page]: {
- ids: estimates.map((i) => i.id),
+ ids: sales_estimates.map((i) => i.id),
},
},
};
@@ -98,8 +99,8 @@ const reducer = createReducer(initialState, {
},
});
-export default createTableQueryReducers('estimates', reducer);
+export default createTableQueryReducers('sales_estimates', reducer);
export const getEstimateById = (state, id) => {
- return state.estiamates.items[id];
+ return state.sales_estimates.items[id];
};
diff --git a/client/src/store/Estimate/estimates.selectors.js b/client/src/store/Estimate/estimates.selectors.js
index d75d0e0fb..a565b7801 100644
--- a/client/src/store/Estimate/estimates.selectors.js
+++ b/client/src/store/Estimate/estimates.selectors.js
@@ -1,44 +1,54 @@
import { createSelector } from '@reduxjs/toolkit';
import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
-const estimateTableQuery = (state) => state.estimates.tableQuery;
+const estimateTableQuery = (state) => {
+ return state.sales_estimates.tableQuery;
+};
export const getEstimatesTableQuery = createSelector(
paginationLocationQuery,
estimateTableQuery,
- (location, query) => ({
- ...location,
- ...query,
- }),
+ (locationQuery, tableQuery) => {
+ return {
+ ...locationQuery,
+ ...tableQuery,
+ };
+ },
);
-const estimatesSelector = (state, props, query) => {
- const viewId = state.estimates.currentViewId;
- return state.estimates.views?.[viewId]?.pages?.[query.page];
+const estimatesPageSelector = (state, props, query) => {
+ const viewId = state.sales_estimates.currentViewId;
+ return state.sales_estimates.views?.[viewId]?.pages?.[query.page];
};
-const EstimateItemsSelector = (state) => state.estimates.items;
+const estimateItemsSelector = (state) => state.sales_estimates.items;
-export const getEstimateCurrentPage = () =>
- createSelector(estimatesSelector, EstimateItemsSelector, (page, items) => {
- return typeof page === 'object'
- ? pickItemsFromIds(items, page.ids) || []
- : [];
- });
+export const getEstimateCurrentPageFactory = () =>
+ createSelector(
+ estimatesPageSelector,
+ estimateItemsSelector,
+ (estimatePage, estimateItems) => {
+ return typeof estimatePage === 'object'
+ ? pickItemsFromIds(estimateItems, estimatePage.ids) || []
+ : [];
+ },
+ );
-const estimateByIdSelector = (state, props) => state.estimates.items;
+const estimateByIdSelector = (state, props) => {
+ return state.sales_estimates.items[props.estimateId];
+};
export const getEstimateByIdFactory = () =>
createSelector(estimateByIdSelector, (estimate) => {
return estimate;
});
-const paginationSelector = (state, props) => {
- const viewId = state.estimates.currentViewId;
- return state.estimates.views?.[viewId];
+const estimatesPaginationSelector = (state, props) => {
+ const viewId = state.sales_estimates.currentViewId;
+ return state.sales_estimates.views?.[viewId];
};
export const getEstimatesPaginationMetaFactory = () =>
- createSelector(paginationSelector, (estimatePage) => {
+ createSelector(estimatesPaginationSelector, (estimatePage) => {
return estimatePage?.paginationMeta || {};
});
diff --git a/client/src/store/Invoice/invoices.actions.js b/client/src/store/Invoice/invoices.actions.js
index d6673925d..ba1899b45 100644
--- a/client/src/store/Invoice/invoices.actions.js
+++ b/client/src/store/Invoice/invoices.actions.js
@@ -29,9 +29,12 @@ export const submitInvoice = ({ form }) => {
export const deleteInvoice = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.delete(`invoice/${id}`)
+ ApiService.delete(`sales/invoices/${id}`)
.then((response) => {
- dispatch({ type: t.INVOICE_DELETE });
+ dispatch({
+ type: t.INVOICE_DELETE,
+ payload: { id },
+ });
resovle(response);
})
.catch((error) => {
@@ -46,7 +49,7 @@ export const editInvoice = (id, form) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
- ApiService.post(`invoice/${id}`, form)
+ ApiService.post(`sales/invoices/${id}`, form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
@@ -64,39 +67,39 @@ export const editInvoice = (id, form) => {
});
});
};
-export const fetchInvoicesTable = ({ query = {} }) => {
- return (dispatch, getState) =>
- new Promise((resolve, rejcet) => {
- const pageQuery = getState().invoices.tableQuery;
+export const fetchInvoicesTable = ({ query } = {}) => {
+ return (dispatch, getState) =>
+ new Promise((resolve, reject) => {
+ const pageQuery = getState().sales_invoices.tableQuery;
dispatch({
type: t.INVOICES_TABLE_LOADING,
payload: {
loading: true,
},
});
- ApiService.get('invoices', {
+ ApiService.get('sales/invoices', {
params: { ...pageQuery, ...query },
})
.then((response) => {
dispatch({
type: t.INVOICES_PAGE_SET,
payload: {
- invoices: response.data.invoices.results,
- pagination: response.data.invoices.pagination,
+ sales_invoices: response.data.sales_invoices.results,
+ pagination: response.data.sales_invoices.pagination,
customViewId: response.data.customViewId || -1,
},
});
dispatch({
type: t.INVOICES_ITEMS_SET,
payload: {
- invoices: response.data.invoices.results,
+ sales_invoices: response.data.sales_invoices.results,
},
});
dispatch({
type: t.INVOICES_PAGINATION_SET,
payload: {
- pagination: response.data.invoices.pagination,
+ pagination: response.data.sales_invoices.pagination,
customViewId: response.data.customViewId || -1,
},
});
@@ -109,7 +112,7 @@ export const fetchInvoicesTable = ({ query = {} }) => {
resolve(response);
})
.catch((error) => {
- rejcet(error);
+ reject(error);
});
});
};
@@ -117,13 +120,13 @@ export const fetchInvoicesTable = ({ query = {} }) => {
export const fetchInvoice = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.get(`invoices/${id}`)
+ ApiService.get(`sales/invoices/${id}`)
.then((response) => {
dispatch({
type: t.INVOICE_SET,
payload: {
id,
- invoice: response.data.invoice,
+ sale_invoice: response.data.sale_invoice,
},
});
resovle(response);
diff --git a/client/src/store/Invoice/invoices.reducer.js b/client/src/store/Invoice/invoices.reducer.js
index d73488705..e9bbeff35 100644
--- a/client/src/store/Invoice/invoices.reducer.js
+++ b/client/src/store/Invoice/invoices.reducer.js
@@ -7,26 +7,99 @@ const initialState = {
items: {},
views: {},
loading: false,
+ currentViewId: -1,
tableQuery: {
- page_size: 12,
+ page_size: 5,
page: 1,
},
- currentViewId: -1,
};
const defaultInvoice = {
- entires: [],
+ entries: [],
};
-
-
-
const reducer = createReducer(initialState, {
-[t.INVOICE_SET]:(state,actio)=>{
+ [t.INVOICE_SET]: (state, action) => {
+ const { id, sale_invoice } = action.payload;
+ const _invoice = state.items[id] || {};
- const {id,INVOICE_SET} = action.payload;
+ state.items[id] = { ...defaultInvoice, ..._invoice, ...sale_invoice };
+ },
-}
+ [t.INVOICES_ITEMS_SET]: (state, action) => {
+ const { sales_invoices } = action.payload;
+ const _invoices = {};
+ sales_invoices.forEach((invoice) => {
+ _invoices[invoice.id] = {
+ ...defaultInvoice,
+ ...invoice,
+ };
+ });
+ state.items = {
+ ...state.items,
+ ..._invoices,
+ };
+ },
+ [t.INVOICES_TABLE_LOADING]: (state, action) => {
+ const { loading } = action.payload;
+ state.loading = loading;
+ },
+ [t.INVOICES_SET_CURRENT_VIEW]: (state, action) => {
+ state.currentViewId = action.currentViewId;
+ },
+
+ [t.INVOICE_DELETE]: (state, action) => {
+ const { id } = action.payload;
+
+ if (typeof state.items[id] !== 'undefined') {
+ delete state.items[id];
+ }
+ },
+
+ [t.INVOICES_PAGE_SET]: (state, action) => {
+ const { customViewId, sales_invoices, pagination } = action.payload;
+
+ const viewId = customViewId || -1;
+ const view = state.views[viewId] || {};
+
+ state.views[viewId] = {
+ ...view,
+ pages: {
+ ...(state.views?.[viewId]?.pages || {}),
+ [pagination.page]: {
+ ids: sales_invoices.map((i) => i.id),
+ },
+ },
+ };
+ },
+
+ [t.INVOICES_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('sales_invoices', reducer);
+
+export const getInvoiceById = (state, id) => {
+ return state.sales_invoices.items[id];
+};
diff --git a/client/src/store/Invoice/invoices.selector.js b/client/src/store/Invoice/invoices.selector.js
index 9422f83b5..9f488b05d 100644
--- a/client/src/store/Invoice/invoices.selector.js
+++ b/client/src/store/Invoice/invoices.selector.js
@@ -1,9 +1,54 @@
import { createSelector } from '@reduxjs/toolkit';
+import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
-const invoiceByIdSelector = (state, props) =>
- state.invoices.items[props.invoiceId];
+const invoiceTableQuery = (state) => state.sales_invoices.tableQuery;
-export const getInvoiceById = () =>
- createSelector(invoiceByIdSelector, (_invoice) => {
- return _invoice;
+export const getInvoiceTableQuery = createSelector(
+ paginationLocationQuery,
+ invoiceTableQuery,
+ (locationQuery, tableQuery) => {
+ return {
+ ...locationQuery,
+ ...tableQuery,
+ };
+ },
+);
+
+const invoicesPageSelector = (state, props, query) => {
+ const viewId = state.sales_invoices.currentViewId;
+ return state.sales_invoices.views?.[viewId]?.pages?.[query.page];
+};
+
+const invoicesItemsSelector = (state) => {
+ return state.sales_invoices.items;
+};
+
+export const getInvoiceCurrentPageFactory = () =>
+ createSelector(
+ invoicesPageSelector,
+ invoicesItemsSelector,
+ (invoicePage, invoicesItems) => {
+ return typeof invoicePage === 'object'
+ ? pickItemsFromIds(invoicesItems, invoicePage.ids) || []
+ : [];
+ },
+ );
+
+const invoicesByIdSelector = (state, props) => {
+ return state.sales_invoices.items[props.invoiceId];
+};
+
+export const getInvoiecsByIdFactory = () =>
+ createSelector(invoicesByIdSelector, (invoice) => {
+ return invoice;
+ });
+
+const invoicesPaginationSelector = (state, props) => {
+ const viewId = state.sales_invoices.currentViewId;
+ return state.sales_invoices.views?.[viewId];
+};
+
+export const getInvoicePaginationMetaFactory = () =>
+ createSelector(invoicesPaginationSelector, (invoicePage) => {
+ return invoicePage?.paginationMeta || {};
});
diff --git a/client/src/store/receipt/receipt.actions.js b/client/src/store/receipt/receipt.actions.js
index e6425c442..2865446ac 100644
--- a/client/src/store/receipt/receipt.actions.js
+++ b/client/src/store/receipt/receipt.actions.js
@@ -28,9 +28,12 @@ export const submitReceipt = ({ form }) => {
export const deleteReceipt = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.delete(`receipts/${id}`)
+ ApiService.delete(`sales/receipts/${id}`)
.then((response) => {
- dispatch({ type: t.RECEIPT_DELETE });
+ dispatch({
+ type: t.RECEIPT_DELETE,
+ payload: { id },
+ });
resovle(response);
})
.catch((error) => {
@@ -45,7 +48,7 @@ export const editReceipt = (id, form) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_LOADING,
});
- ApiService.delete(`receipt/${id}`, form)
+ ApiService.post(`sales/receipts/${id}`, form)
.then((response) => {
dispatch({
type: t.SET_DASHBOARD_REQUEST_COMPLETED,
@@ -66,7 +69,7 @@ export const editReceipt = (id, form) => {
export const fetchReceipt = ({ id }) => {
return (dispatch) =>
new Promise((resovle, reject) => {
- ApiService.get(`receipt/${id}`)
+ ApiService.get(`sales/receipts/${id}`)
.then((response) => {
dispatch({
type: t.RECEIPT_SET,
@@ -88,36 +91,35 @@ export const fetchReceipt = ({ id }) => {
export const fetchReceiptsTable = ({ query = {} }) => {
return (dispatch, getState) =>
new Promise((resolve, rejcet) => {
- const pageQuery = getState().receipt.tableQuery;
-
+ const pageQuery = getState().sales_receipts.tableQuery;
dispatch({
type: t.RECEIPTS_TABLE_LOADING,
payload: {
loading: true,
},
});
- ApiService.get('receipts', {
+ ApiService.get('sales/receipts', {
params: { ...pageQuery, ...query },
})
.then((response) => {
dispatch({
type: t.RECEIPTS_PAGE_SET,
payload: {
- receipts: response.data.receipts.results,
- pagination: response.data.receipts.pagination,
+ sales_receipts: response.data.sales_receipts.results,
+ pagination: response.data.sales_receipts.pagination,
customViewId: response.data.customViewId || -1,
},
});
dispatch({
type: t.RECEIPTS_ITEMS_SET,
payload: {
- receipts: response.data.receipts.results,
+ sales_receipts: response.data.sales_receipts.results,
},
});
dispatch({
type: t.RECEIPTS_PAGINATION_SET,
payload: {
- pagination: response.data.receipts.pagination,
+ pagination: response.data.sales_receipts.pagination,
customViewId: response.data.customViewId || -1,
},
});
diff --git a/client/src/store/receipt/receipt.reducer.js b/client/src/store/receipt/receipt.reducer.js
index e69de29bb..346859bdc 100644
--- a/client/src/store/receipt/receipt.reducer.js
+++ b/client/src/store/receipt/receipt.reducer.js
@@ -0,0 +1,103 @@
+import { createReducer } from '@reduxjs/toolkit';
+import { createTableQueryReducers } from 'store/queryReducers';
+import t from 'store/types';
+
+const initialState = {
+ items: {},
+ views: {},
+ loading: false,
+ tableQuery: {
+ page_size: 5,
+ page: 1,
+ },
+ currentViewId: -1,
+};
+
+const defaultReceipt = {
+ entries: [],
+};
+
+const reducer = createReducer(initialState, {
+ [t.RECEIPT_SET]: (state, action) => {
+ const { id, receipt } = action.payload;
+ const _receipt = state.items[id] || {};
+ state.items[id] = { ...defaultReceipt, ..._receipt, ...receipt };
+ },
+
+ [t.RECEIPTS_ITEMS_SET]: (state, action) => {
+ const { sales_receipts } = action.payload;
+ const _receipts = {};
+ sales_receipts.forEach((receipt) => {
+ _receipts[receipt.id] = {
+ ...defaultReceipt,
+ ...receipt,
+ };
+ });
+ state.items = {
+ ...state.items,
+ ..._receipts,
+ };
+ },
+
+ [t.RECEIPTS_TABLE_LOADING]: (state, action) => {
+ const { loading } = action.payload;
+ state.loading = loading;
+ },
+
+ [t.RECEIPT_DELETE]: (state, action) => {
+ const { id } = action.payload;
+ if (typeof state.items[id] !== 'undefined') {
+ delete state.items[id];
+ }
+ },
+
+ [t.RECEIPTS_SET_CURRENT_VIEW]: (state, action) => {
+ state.currentViewId = action.currentViewId;
+ },
+
+ [t.RECEIPTS_PAGE_SET]: (state, action) => {
+ const { customViewId, sales_receipts, pagination } = action.payload;
+
+ const viewId = customViewId || -1;
+ const view = state.views[viewId] || {};
+
+ state.views[viewId] = {
+ ...view,
+ pages: {
+ ...(state.views?.[viewId]?.pages || {}),
+ [pagination.page]: {
+ ids: sales_receipts.map((i) => i.id),
+ },
+ },
+ };
+ },
+
+ [t.RECEIPTS_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('sales_receipts', reducer);
+
+export const getReceiptById = (state, id) => {
+ return state.receipts.items[id];
+};
diff --git a/client/src/store/receipt/receipt.selector.js b/client/src/store/receipt/receipt.selector.js
index e69de29bb..884647508 100644
--- a/client/src/store/receipt/receipt.selector.js
+++ b/client/src/store/receipt/receipt.selector.js
@@ -0,0 +1,56 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
+
+const receiptsPageSelector = (state, props, query) => {
+ const viewId = state.sales_receipts.currentViewId;
+ return state.sales_receipts.views?.[viewId]?.pages?.[query.page];
+};
+
+const receiptItemsSelector = (state) => {
+ return state.sales_receipts.items;
+};
+
+export const getReceiptCurrentPageFactory = () =>
+ createSelector(
+ receiptsPageSelector,
+ receiptItemsSelector,
+ (receiptPage, receiptItems) => {
+ return typeof receiptPage === 'object'
+ ? pickItemsFromIds(receiptItems, receiptPage.ids) || []
+ : [];
+ },
+ );
+
+const receiptTableQuery = (state) => {
+ return state.sales_receipts.tableQuery;
+};
+
+export const getReceiptsTableQuery = createSelector(
+ paginationLocationQuery,
+ receiptTableQuery,
+ (locationQuery, tableQuery) => {
+ return {
+ ...locationQuery,
+ ...tableQuery,
+ };
+ },
+);
+
+const receiptByIdSelector = (state, props) => {
+ return state.sales_receipts.items[props.receiptId];
+};
+
+export const getReceiptByIdFactory = () =>
+ createSelector(receiptByIdSelector, (receipt) => {
+ return receipt;
+ });
+
+const receiptsPaginationSelector = (state, props) => {
+ const viewId = state.sales_receipts.currentViewId;
+ return state.sales_receipts.views?.[viewId];
+};
+
+export const getReceiptsPaginationMetaFactory = () =>
+ createSelector(receiptsPaginationSelector, (receiptPage) => {
+ return receiptPage?.paginationMeta || {};
+ });
diff --git a/client/src/store/reducers.js b/client/src/store/reducers.js
index 6a2fde88c..7e9508af9 100644
--- a/client/src/store/reducers.js
+++ b/client/src/store/reducers.js
@@ -18,7 +18,11 @@ import globalSearch from './search/search.reducer';
import exchangeRates from './ExchangeRate/exchange.reducer';
import globalErrors from './globalErrors/globalErrors.reducer';
import customers from './customers/customers.reducer';
-import estimates from './Estimate/estimates.reducer';
+import sales_estimates from './Estimate/estimates.reducer';
+import sales_invoices from './Invoice/invoices.reducer';
+import sales_receipts from './receipt/receipt.reducer';
+import bills from './Bills/bills.reducer';
+import vendors from './vendors/vendors.reducer';
export default combineReducers({
authentication,
@@ -39,5 +43,9 @@ export default combineReducers({
exchangeRates,
globalErrors,
customers,
- estimates
+ sales_estimates,
+ sales_invoices,
+ sales_receipts,
+ bills,
+ vendors,
});
diff --git a/client/src/store/types.js b/client/src/store/types.js
index da7e60037..182dd8562 100644
--- a/client/src/store/types.js
+++ b/client/src/store/types.js
@@ -22,6 +22,7 @@ import invoices from './Invoice/invoices.types';
import receipts from './receipt/receipt.type';
import bills from './Bills/bills.type';
import paymentReceives from './PaymentReceive/paymentReceive.type';
+import vendors from './vendors/vendors.types';
export default {
...authentication,
@@ -48,4 +49,5 @@ export default {
...receipts,
...bills,
...paymentReceives,
+ ...vendors,
};
diff --git a/client/src/store/vendors/vendors.actions.js b/client/src/store/vendors/vendors.actions.js
new file mode 100644
index 000000000..7b091e573
--- /dev/null
+++ b/client/src/store/vendors/vendors.actions.js
@@ -0,0 +1,118 @@
+import ApiService from 'services/ApiService';
+import t from 'store/types';
+
+export const fetchVendorsTable = ({ query }) => {
+ return (dispatch, getState) =>
+ new Promise((resolve, reject) => {
+ const pageQuery = getState().vendors.tableQuery;
+ dispatch({
+ type: t.VENDORS_TABLE_LOADING,
+ payload: { loading: true },
+ });
+ dispatch({
+ type: t.SET_DASHBOARD_REQUEST_LOADING,
+ });
+ ApiService.get(`vendors`, { params: { ...pageQuery, ...query } })
+ .then((response) => {
+ dispatch({
+ type: t.VENDORS_PAGE_SET,
+ payload: {
+ vendors: response.data.vendors.results,
+ pagination: response.data.vendors.pagination,
+ customViewId: response.data.customViewId,
+ },
+ });
+
+ dispatch({
+ type: t.VENDORS_ITEMS_SET,
+ payload: {
+ vendors: response.data.vendors.results,
+ },
+ });
+
+ dispatch({
+ type: t.VENDORS_PAGINATION_SET,
+ payload: {
+ pagination: response.data.vendors.pagination,
+ customViewId: response.data.customViewId || -1,
+ },
+ });
+
+ dispatch({
+ type: t.VENDORS_TABLE_LOADING,
+ payload: { loading: false },
+ });
+ dispatch({
+ type: t.SET_DASHBOARD_REQUEST_COMPLETED,
+ });
+ resolve(response);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+};
+
+export const editVendor = ({ form, id }) => {
+ return (dispatch) =>
+ new Promise((resolve, reject) => {
+ dispatch({
+ type: t.SET_DASHBOARD_REQUEST_LOADING,
+ });
+
+ ApiService.post(`vendors/${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 deleteVendor = ({ id }) => {
+ return (dispatch) =>
+ new Promise((resolve, reject) => {
+ ApiService.delete(`vendors/${id}`)
+ .then((response) => {
+ dispatch({ type: t.VENDOR_DELETE, payload: { id } });
+ resolve(response);
+ })
+ .catch((error) => {
+ reject(error.response.data.errors || []);
+ });
+ });
+};
+
+export const submitVendor = ({ form }) => {
+ return (dispatch) =>
+ new Promise((resolve, reject) => {
+ dispatch({
+ type: t.SET_DASHBOARD_REQUEST_LOADING,
+ });
+
+ ApiService.post('vendors', 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);
+ });
+ });
+};
diff --git a/client/src/store/vendors/vendors.reducer.js b/client/src/store/vendors/vendors.reducer.js
new file mode 100644
index 000000000..8a3737910
--- /dev/null
+++ b/client/src/store/vendors/vendors.reducer.js
@@ -0,0 +1,96 @@
+import { createReducer } from '@reduxjs/toolkit';
+import { createTableQueryReducers } from 'store/queryReducers';
+
+import t from 'store/types';
+
+const initialState = {
+ items: {},
+ views: {},
+ loading: false,
+ tableQuery: {
+ page_size: 5,
+ page: 1,
+ },
+ currentViewId: -1,
+};
+
+const reducer = createReducer(initialState, {
+ [t.VENDORS_TABLE_LOADING]: (state, action) => {
+ const { loading } = action.payload;
+ state.loading = loading;
+ },
+
+ [t.VENDORS_ITEMS_SET]: (state, action) => {
+ const { vendors } = action.payload;
+ const _vendors = {};
+ vendors.forEach((vendor) => {
+ _vendors[vendor.id] = {
+ ...vendor,
+ };
+ });
+ state.items = {
+ ...state.items,
+ ..._vendors,
+ };
+ },
+
+ [t.VENDORS_PAGE_SET]: (state, action) => {
+ const { customViewId, vendors, pagination } = action.payload;
+
+ const viewId = customViewId || -1;
+ const view = state.views[viewId] || {};
+
+ state.views[viewId] = {
+ ...view,
+ pages: {
+ ...(state.views?.[viewId]?.pages || {}),
+ [pagination.page]: {
+ ids: vendors.map((i) => i.id),
+ },
+ },
+ };
+ },
+
+ [t.VENDOR_DELETE]: (state, action) => {
+ const { id } = action.payload;
+
+ if (typeof state.items[id] !== 'undefined') {
+ delete state.items[id];
+ }
+ },
+
+ [t.VENDORS_SET_CURRENT_VIEW]: (state, action) => {
+ state.currentViewId = action.currentViewId;
+ },
+
+ [t.VENDORS_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,
+ },
+ };
+ },
+
+ // [t.VENDOR_SET]: (state, action) => {
+ // const { id, vendor } = action.payload;
+ // const _venders = state.items[id] || {};
+ // state.items[id] = { ..._venders, ...vendor };
+ // },
+});
+
+export default createTableQueryReducers('vendors', reducer);
diff --git a/client/src/store/vendors/vendors.selectors.js b/client/src/store/vendors/vendors.selectors.js
new file mode 100644
index 000000000..708338074
--- /dev/null
+++ b/client/src/store/vendors/vendors.selectors.js
@@ -0,0 +1,54 @@
+import { createSelector } from '@reduxjs/toolkit';
+import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
+
+const vendorsTableQuery = (state) => {
+ return state.vendors.tableQuery;
+};
+
+export const getVendorsTableQuery = createSelector(
+ paginationLocationQuery,
+ vendorsTableQuery,
+ (locationQuery, tableQuery) => {
+ return {
+ ...locationQuery,
+ ...tableQuery,
+ };
+ },
+);
+
+const vendorsPageSelector = (state, props, query) => {
+ const viewId = state.vendors.currentViewId;
+ return state.vendors.views?.[viewId]?.pages?.[query.page];
+};
+
+const vendorsItemsSelector = (state) => state.vendors.items;
+
+export const getVendorCurrentPageFactory = () =>
+ createSelector(
+ vendorsPageSelector,
+ vendorsItemsSelector,
+ (vendorPage, vendorItems) => {
+ return typeof vendorPage === 'object'
+ ? pickItemsFromIds(vendorItems, vendorPage.ids) || []
+ : [];
+ },
+ );
+
+const vendorsPaginationSelector = (state, props) => {
+ const viewId = state.vendors.currentViewId;
+ return state.vendors.views?.[viewId];
+};
+
+export const getVendorsPaginationMetaFactory = () =>
+ createSelector(vendorsPaginationSelector, (vendorPage) => {
+ return vendorPage?.paginationMeta || {};
+ });
+
+const vendorByIdSelector = (state, props) => {
+ return state.vendors.items[props.vendorId];
+};
+
+export const getEstimateByIdFactory = () =>
+ createSelector(vendorByIdSelector, (vendor) => {
+ return vendor;
+ });
diff --git a/client/src/store/vendors/vendors.types.js b/client/src/store/vendors/vendors.types.js
new file mode 100644
index 000000000..0ddced9ba
--- /dev/null
+++ b/client/src/store/vendors/vendors.types.js
@@ -0,0 +1,11 @@
+export default {
+ VENDORS_ITEMS_SET: 'VENDORS_ITEMS_SET',
+ VENDOR_SET: 'VENDOR_SET',
+ VENDORS_PAGE_SET: 'VENDORS_PAGE_SET',
+ VENDORS_TABLE_LOADING: 'VENDORS_TABLE_LOADING',
+ VENDORS_TABLE_QUERIES_ADD: 'VENDORS_TABLE_QUERIES_ADD',
+ VENDOR_DELETE: 'VENDOR_DELETE',
+ VENDORS_BULK_DELETE: 'VENDORS_BULK_DELETE',
+ VENDORS_PAGINATION_SET: 'VENDORS_PAGINATION_SET',
+ VENDORS_SET_CURRENT_VIEW: 'VENDORS_SET_CURRENT_VIEW',
+};