WIP Version 0.0.1

This commit is contained in:
Ahmed Bouhuolia
2020-05-08 04:36:04 +02:00
parent bd7eb0eb76
commit 71cc561bb2
151 changed files with 1742 additions and 1081 deletions

View File

@@ -0,0 +1,53 @@
import React, { useEffect, useState, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import useAsync from 'hooks/async';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import ItemCategoriesDataTable from 'containers/Items/ItemCategoriesTable';
import ItemsCategoryActionsBar from 'containers/Items/ItemsCategoryActionsBar';
import withDashboardActions from 'containers/Dashboard/withDashboard';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import withItemCategories from 'containers/Items/withItemCategories';
import { compose } from 'utils';
const ItemCategoryList = ({
changePageTitle,
requestFetchItemCategories,
}) => {
const { id } = useParams();
const [selectedRows, setSelectedRows] = useState([]);
useEffect(() => {
id
? changePageTitle('Edit Category Details')
: changePageTitle('Category List');
}, []);
const fetchCategories = useAsync(() => {
return Promise.all([
requestFetchItemCategories(),
]);
});
const handleFilterChanged = useCallback(() => {
}, []);
return (
<DashboardInsider name={'item-category-list'}>
<ItemsCategoryActionsBar
onFilterChanged={handleFilterChanged}
selectedRows={selectedRows} />
<ItemCategoriesDataTable />
</DashboardInsider>
);
};
export default compose(
withDashboardActions,
withItemCategoriesActions,
withItemCategories,
)(ItemCategoryList);

View File

@@ -0,0 +1,108 @@
import React, { useCallback, useMemo } from 'react';
import Icon from 'components/Icon';
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
import DialogConnect from 'connectors/Dialog.connector';
import LoadingIndicator from 'components/LoadingIndicator';
import { compose } from 'utils';
import DataTable from 'components/DataTable';
import {
Button,
Popover,
Menu,
MenuItem,
Position,
} from '@blueprintjs/core';
const ItemsCategoryList = ({
categories,
onFetchData,
onDeleteCategory,
onEditCategory,
openDialog,
count,
onSelectedRowsChange,
}) => {
const handelEditCategory = (category) => () => {
openDialog('item-form', { action: 'edit', id: category.id });
onEditCategory(category.id);
};
const handleDeleteCategory = (category) => () => {
onDeleteCategory(category);
};
const actionMenuList = (category) => (
<Menu>
<MenuItem text='Edit Category' onClick={handelEditCategory(category)} />
<MenuItem
text='Delete Category'
onClick={handleDeleteCategory(category)}
/>
</Menu>
);
const columns = useMemo(() => [
{
id: 'name',
Header: 'Category Name',
accessor: 'name',
width: 150,
},
{
id: 'description',
Header: 'Description',
accessor: 'description',
className: 'description',
width: 150,
},
{
id: 'count',
Header: 'Count',
accessor: (r) => r.count || '',
className: 'count',
width: 50,
},
{
id: 'actions',
Header: '',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}
>
<Button icon={<Icon icon='ellipsis-h' />} />
</Popover>
),
className: 'actions',
width: 50,
disableResizing: false
},
], [actionMenuList]);
const handelFetchData = useCallback(() => {
onFetchData && onFetchData();
}, []);
const handleSelectedRowsChange = useCallback((selectedRows) => {
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
}, [onSelectedRowsChange]);
return (
<LoadingIndicator spinnerSize={30}>
<DataTable
columns={columns}
data={Object.values(categories)}
onFetchData={handelFetchData}
manualSortBy={true}
selectionColumn={true}
expandable={true}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</LoadingIndicator>
);
};
export default compose(
DialogConnect,
ItemsCategoryConnect,
)(ItemsCategoryList);

View File

@@ -0,0 +1,459 @@
import React, { useState, useMemo, useCallback } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import {
FormGroup,
MenuItem,
Intent,
InputGroup,
HTMLSelect,
Button,
Classes,
Checkbox,
} from '@blueprintjs/core';
import { Row, Col } from 'react-grid-system';
import { Select } from '@blueprintjs/select';
import AppToaster from 'components/AppToaster';
import AccountsConnect from 'connectors/Accounts.connector';
import ItemsConnect from 'connectors/Items.connect';
import {compose} from 'utils';
import ErrorMessage from 'components/ErrorMessage';
import classNames from 'classnames';
import Icon from 'components/Icon';
import ItemCategoryConnect from 'connectors/ItemsCategory.connect';
import MoneyInputGroup from 'components/MoneyInputGroup';
import {useHistory} from 'react-router-dom';
import Dragzone from 'components/Dragzone';
import MediaConnect from 'connectors/Media.connect';
import useMedia from 'hooks/useMedia';
const ItemForm = ({
requestSubmitItem,
accounts,
categories,
requestSubmitMedia,
requestDeleteMedia,
}) => {
const [selectedAccounts, setSelectedAccounts] = useState({});
const history = useHistory();
const {
files,
setFiles,
saveMedia,
deletedFiles,
setDeletedFiles,
deleteMedia,
} = useMedia({
saveCallback: requestSubmitMedia,
deleteCallback: requestDeleteMedia,
})
const ItemTypeDisplay = useMemo(() => ([
{ value: null, label: 'Select Item Type' },
{ value: 'service', label: 'Service' },
{ value: 'inventory', label: 'Inventory' },
{ value: 'non-inventory', label: 'Non-Inventory' }
]), []);
const validationSchema = Yup.object().shape({
active: Yup.boolean(),
name: Yup.string().required(),
type: Yup.string().trim().required(),
sku: Yup.string().trim(),
cost_price: Yup.number(),
sell_price: Yup.number(),
cost_account_id: Yup.number().required(),
sell_account_id: Yup.number().required(),
inventory_account_id: Yup.number().when('type', {
is: (value) => value === 'inventory',
then: Yup.number().required(),
otherwise: Yup.number().nullable(),
}),
category_id: Yup.number().nullable(),
stock: Yup.string() || Yup.boolean()
});
const initialValues = useMemo(() => ({
active: true,
name: '',
type: '',
sku: '',
cost_price: 0,
sell_price: 0,
cost_account_id: null,
sell_account_id: null,
inventory_account_id: null,
category_id: null,
note: '',
}), []);
const {
getFieldProps,
setFieldValue,
values,
touched,
errors,
handleSubmit,
} = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting }) => {
const saveItem = (mediaIds) => {
const formValues = { ...values, media_ids: mediaIds };
return requestSubmitItem(formValues).then((response) => {
AppToaster.show({
message: 'The_Items_has_been_submit'
});
setSubmitting(false);
history.push('/dashboard/items');
})
.catch((error) => {
setSubmitting(false);
});
};
Promise.all([
saveMedia(),
deleteMedia(),
]).then(([savedMediaResponses]) => {
const mediaIds = savedMediaResponses.map(res => res.data.media.id);
return saveItem(mediaIds);
});
}
});
const accountItem = useCallback((item, { handleClick }) => (
<MenuItem key={item.id} text={item.name} label={item.code} onClick={handleClick} />
), []);
// Filter Account Items
const filterAccounts = (query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0;
}
};
const onItemAccountSelect = useCallback((filedName) => {
return (account) => {
setSelectedAccounts({
...selectedAccounts,
[filedName]: account
});
setFieldValue(filedName, account.id);
};
}, [setFieldValue, selectedAccounts]);
const categoryItem = useCallback((item, { handleClick }) => (
<MenuItem text={item.name} onClick={handleClick} />
), []);
const getSelectedAccountLabel = useCallback((fieldName, defaultLabel) => {
return typeof selectedAccounts[fieldName] !== 'undefined'
? selectedAccounts[fieldName].name : defaultLabel;
}, [selectedAccounts]);
const requiredSpan = useMemo(() => (<span class="required">*</span>), []);
const infoIcon = useMemo(() => (<Icon icon="info-circle" iconSize={12} />), []);
const handleMoneyInputChange = (fieldKey) => (e, value) => {
setFieldValue(fieldKey, value);
};
const initialAttachmentFiles = useMemo(() => {
return [];
}, []);
const handleDropFiles = useCallback((_files) => {
setFiles(_files.filter((file) => file.uploaded === false));
}, []);
const handleDeleteFile = useCallback((_deletedFiles) => {
_deletedFiles.forEach((deletedFile) => {
if (deletedFile.uploaded && deletedFile.metadata.id) {
setDeletedFiles([
...deletedFiles, deletedFile.metadata.id,
]);
}
});
}, [setDeletedFiles, deletedFiles]);
const handleCancelClickBtn = () => { history.goBack(); };
return (
<div class='item-form'>
<form onSubmit={handleSubmit}>
<div class="item-form__primary-section">
<Row>
<Col xs={7}>
<FormGroup
medium={true}
label={'Item Type'}
labelInfo={requiredSpan}
className={'form-group--item-type'}
intent={(errors.type && touched.type) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="type" />}
inline={true}
>
<HTMLSelect
fill={true}
options={ItemTypeDisplay}
{...getFieldProps('type')}
/>
</FormGroup>
<FormGroup
label={'Item Name'}
labelInfo={requiredSpan}
className={'form-group--item-name'}
intent={(errors.name && touched.name) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="name" />}
inline={true}
>
<InputGroup
medium={true}
intent={(errors.name && touched.name) && Intent.DANGER}
{...getFieldProps('name')}
/>
</FormGroup>
<FormGroup
label={'SKU'}
labelInfo={infoIcon}
className={'form-group--item-sku'}
intent={(errors.sku && touched.sku) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="sku" />}
inline={true}
>
<InputGroup
medium={true}
intent={(errors.sku && touched.sku) && Intent.DANGER}
{...getFieldProps('sku')}
/>
</FormGroup>
<FormGroup
label={'Category'}
labelInfo={infoIcon}
inline={true}
intent={(errors.category_id && touched.category_id) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="category" />}
className={classNames(
'form-group--select-list',
'form-group--category',
Classes.FILL,
)}
>
<Select
items={categories}
itemRenderer={categoryItem}
itemPredicate={filterAccounts}
popoverProps={{ minimal: true }}
onItemSelect={onItemAccountSelect('category_id')}
>
<Button
fill={true}
rightIcon='caret-down'
text={getSelectedAccountLabel('category_id', 'Select category')}
/>
</Select>
</FormGroup>
<FormGroup
label={' '}
inline={true}
className={'form-group--active'}
>
<Checkbox
inline={true}
label={'Active'}
defaultChecked={values.active}
{...getFieldProps('active')}
/>
</FormGroup>
</Col>
<Col xs={3}>
<Dragzone
initialFiles={initialAttachmentFiles}
onDrop={handleDropFiles}
onDeleteFile={handleDeleteFile}
hint={'Attachments: Maxiumum size: 20MB'}
className={'mt2'} />
</Col>
</Row>
</div>
<Row gutterWidth={16} className={'item-form__accounts-section'}>
<Col width={404}>
<h4>Purchase Information</h4>
<FormGroup
label={'Selling Price'}
className={'form-group--item-selling-price'}
intent={(errors.selling_price && touched.selling_price) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="selling_price" />}
inline={true}
>
<MoneyInputGroup
value={values.selling_price}
prefix={'$'}
onChange={handleMoneyInputChange('selling_price')}
inputGroupProps={{
medium: true,
intent: (errors.selling_price && touched.selling_price) && Intent.DANGER,
}} />
</FormGroup>
<FormGroup
label={'Account'}
labelInfo={infoIcon}
inline={true}
intent={(errors.sell_account_id && touched.sell_account_id) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="sell_account_id" />}
className={classNames(
'form-group--sell-account', 'form-group--select-list',
Classes.FILL)}
>
<Select
items={accounts}
itemRenderer={accountItem}
itemPredicate={filterAccounts}
popoverProps={{ minimal: true }}
onItemSelect={onItemAccountSelect('sell_account_id')}
>
<Button
fill={true}
rightIcon='caret-down'
text={getSelectedAccountLabel('sell_account_id', 'Select account')}
/>
</Select>
</FormGroup>
</Col>
<Col width={404}>
<h4>
Sales Information
</h4>
<FormGroup
label={'Cost Price'}
className={'form-group--item-cost-price'}
intent={(errors.cost_price && touched.cost_price) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="cost_price" />}
inline={true}
>
<MoneyInputGroup
value={values.cost_price}
prefix={'$'}
onChange={handleMoneyInputChange('cost_price')}
inputGroupProps={{
medium: true,
intent: (errors.cost_price && touched.cost_price) && Intent.DANGER,
}} />
</FormGroup>
<FormGroup
label={'Account'}
labelInfo={infoIcon}
inline={true}
intent={(errors.cost_account_id && touched.cost_account_id) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="cost_account_id" />}
className={classNames(
'form-group--cost-account',
'form-group--select-list',
Classes.FILL)}
>
<Select
items={accounts}
itemRenderer={accountItem}
itemPredicate={filterAccounts}
popoverProps={{ minimal: true }}
onItemSelect={onItemAccountSelect('cost_account_id')}
>
<Button
fill={true}
rightIcon='caret-down'
text={getSelectedAccountLabel('cost_account_id', 'Select account')}
/>
</Select>
</FormGroup>
</Col>
</Row>
<Row className={'item-form__accounts-section mt2'}>
<Col width={404}>
<h4>
Inventory Information
</h4>
<FormGroup
label={'Inventory Account'}
inline={true}
intent={(errors.inventory_account_id && touched.inventory_account_id) && Intent.DANGER}
helperText={<ErrorMessage {...{errors, touched}} name="inventory_account_id" />}
className={classNames(
'form-group--item-inventory_account',
'form-group--select-list',
Classes.FILL)}
>
<Select
items={accounts}
itemRenderer={accountItem}
itemPredicate={filterAccounts}
popoverProps={{ minimal: true }}
onItemSelect={onItemAccountSelect('inventory_account_id')}
>
<Button
fill={true}
rightIcon='caret-down'
text={getSelectedAccountLabel('inventory_account_id','Select account')}
/>
</Select>
</FormGroup>
<FormGroup
label={'Opening Stock'}
className={'form-group--item-stock'}
// intent={errors.cost_price && Intent.DANGER}
// helperText={formik.errors.stock && formik.errors.stock}
inline={true}
>
<InputGroup
medium={true}
intent={errors.stock && Intent.DANGER}
{...getFieldProps('stock')}
/>
</FormGroup>
</Col>
</Row>
<div class='form__floating-footer'>
<Button intent={Intent.PRIMARY} type='submit'>
Save
</Button>
<Button className={'ml1'}>Save as Draft</Button>
<Button className={'ml1'} onClick={handleCancelClickBtn}>Close</Button>
</div>
</form>
</div>
);
};
export default compose(
AccountsConnect,
ItemsConnect,
ItemCategoryConnect,
MediaConnect,
)(ItemForm);

View File

@@ -0,0 +1,52 @@
import React, { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useQuery } from 'react-query';
import ItemForm from 'containers/Items/ItemForm';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withDashboard from 'containers/Dashboard/withDashboard';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
import { compose } from 'utils';
const ItemFormContainer = ({
// #withDashboard
changePageTitle,
// #withAccountsActions
requestFetchAccounts,
// #withItemCategoriesActions
requestFetchItemCategories,
}) => {
const { id } = useParams();
useEffect(() => {
id ?
changePageTitle('Edit Item Details') :
changePageTitle('New Item');
}, [id, changePageTitle]);
const fetchAccounts = useQuery('accounts-list',
(key) => requestFetchAccounts());
const fetchCategories = useQuery('item-categories-list',
(key) => requestFetchItemCategories());
return (
<DashboardInsider
loading={fetchAccounts.isFetching || fetchCategories.isFetching}
name={'item-form'}>
<ItemForm />
</DashboardInsider>
);
};
export default compose(
withDashboard,
withAccountsActions,
withItemCategoriesActions,
)(ItemFormContainer);

View File

@@ -0,0 +1,133 @@
import React, { useMemo, useCallback, useState } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
import classNames from 'classnames';
import {
MenuItem,
Popover,
NavbarGroup,
Menu,
NavbarDivider,
PopoverInteractionKind,
Position,
Button,
Classes,
Intent,
} from '@blueprintjs/core';
import { compose } from 'utils';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import Icon from 'components/Icon';
import FilterDropdown from 'components/FilterDropdown';
import DialogConnect from 'connectors/Dialog.connector';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withItems from 'containers/Items/withItems';
import { If } from 'components';
const ItemsActionsBar = ({
openDialog,
resourceName = 'items',
resourceFields,
itemsViews,
onFilterChanged,
selectedRows = [],
}) => {
const { path } = useRouteMatch();
const history = useHistory();
const [filterCount, setFilterCount] = useState(0);
const viewsMenuItems = itemsViews.map(view =>
(<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />));
const onClickNewItem = () => {
history.push('/dashboard/items/new');
};
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length);
onFilterChanged && onFilterChanged(filterConditions);
}
});
const onClickNewCategory = useCallback(() => {
openDialog('item-form', {});
}, [openDialog]);
return (
<DashboardActionsBar>
<NavbarGroup>
<Popover
content={<Menu>{viewsMenuItems}</Menu>}
minimal={true}
interactionKind={PopoverInteractionKind.HOVER}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon='table' />}
text='Table Views'
rightIcon={'caret-down'}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Item'
onClick={onClickNewItem}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Category'
onClick={onClickNewCategory}
/>
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={filterCount <= 0 ? 'Filter' : `${filterCount} filters applied`}
icon={<Icon icon='filter' />}
/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
intent={Intent.DANGER}
icon={<Icon icon='trash' />}
text='Delete'
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-import' />}
text='Import'
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-export' />}
text='Export'
/>
</NavbarGroup>
</DashboardActionsBar>
);
};
export default compose(
DialogConnect,
withItems,
withResourceDetail,
)(ItemsActionsBar);

View File

@@ -0,0 +1,108 @@
import React, { useCallback, useMemo } from 'react';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { compose } from 'utils';
import {
NavbarGroup,
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { If } from 'components';
import Icon from 'components/Icon';
import DialogConnect from 'connectors/Dialog.connector';
import FilterDropdown from 'components/FilterDropdown';
import withResourceDetail from 'containers/Resources/withResourceDetails';
import withDashboard from 'containers/Dashboard/withDashboard';
const ItemsCategoryActionsBar = ({
resourceName = 'item_category',
resourceFields,
openDialog,
onDeleteCategory,
onFilterChanged,
selectedRows,
}) => {
const onClickNewCategory = useCallback(() => {
openDialog('item-form', {});
}, [openDialog]);
const handleDeleteCategory = useCallback((category) => {
onDeleteCategory(selectedRows);
}, [selectedRows, onDeleteCategory]);
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [selectedRows]);
const filterDropdown = FilterDropdown({
fields: resourceFields,
onFilterChange: (filterConditions) => {
onFilterChanged && onFilterChanged(filterConditions);
},
});
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Category'
onClick={onClickNewCategory}
/>
<Popover
minimal={true}
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text='Filter'
icon={<Icon icon='filter' />}
/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='trash' iconSize={15} />}
text='Delete'
intent={Intent.DANGER}
onClick={handleDeleteCategory}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-import' />}
text='Import'
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='file-export' />}
text='Export'
/>
</NavbarGroup>
</DashboardActionsBar>
);
};
const mapStateToProps = (state, props) => ({
resourceName: 'items_categories',
});
const withItemsCategoriesActionsBar = connect(mapStateToProps);
export default compose(
withItemsCategoriesActionsBar,
DialogConnect,
withDashboard,
withResourceDetail
)(ItemsCategoryActionsBar);

View File

@@ -0,0 +1,134 @@
import React, {useState, useEffect, useCallback, useMemo} from 'react';
import {
Button,
Popover,
Menu,
MenuItem,
MenuDivider,
Position,
} from '@blueprintjs/core'
import {compose} from 'utils';
import DataTable from 'components/DataTable';
import Icon from 'components/Icon';
import Money from 'components/Money';
import withItems from 'containers/Items/withItems';
import LoadingIndicator from 'components/LoadingIndicator';
const ItemsDataTable = ({
loading,
itemsTableLoading,
itemsCurrentPage,
// props
onEditItem,
onDeleteItem,
onFetchData,
onSelectedRowsChange,
}) => {
const [initialMount, setInitialMount] = useState(false);
useEffect(() => {
if (!itemsTableLoading) {
setInitialMount(true);
}
}, [itemsTableLoading, setInitialMount]);
const handleEditItem = (item) => () => { onEditItem(item); };
const handleDeleteItem = (item) => () => { onDeleteItem(item); };
const actionMenuList = useCallback((item) =>
(<Menu>
<MenuItem text="View Details" />
<MenuDivider />
<MenuItem text="Edit Item" onClick={handleEditItem(item)} />
<MenuItem text="Delete Item" onClick={handleDeleteItem(item)} />
</Menu>), [handleEditItem, handleDeleteItem]);
const columns = useMemo(() => [
{
Header: 'Item Name',
accessor: 'name',
className: "actions",
},
{
Header: 'SKU',
accessor: 'sku',
className: "sku",
},
{
Header: 'Category',
accessor: 'category.name',
className: 'category',
},
{
Header: 'Sell Price',
accessor: row => (<Money amount={row.sell_price} currency={'USD'} />),
className: 'sell-price',
},
{
Header: 'Cost Price',
accessor: row => (<Money amount={row.cost_price} currency={'USD'} />),
className: 'cost-price',
},
// {
// Header: 'Cost Account',
// accessor: 'cost_account.name',
// className: "cost-account",
// },
// {
// Header: 'Sell Account',
// accessor: 'sell_account.name',
// className: "sell-account",
// },
// {
// Header: 'Inventory Account',
// accessor: 'inventory_account.name',
// className: "inventory-account",
// },
{
id: 'actions',
Cell: ({ cell }) => (
<Popover
content={actionMenuList(cell.row.original)}
position={Position.RIGHT_BOTTOM}>
<Button icon={<Icon icon="ellipsis-h" />} />
</Popover>
),
className: 'actions',
width: 50,
},
], [actionMenuList]);
const selectionColumn = useMemo(() => ({
minWidth: 42,
width: 42,
maxWidth: 42,
}), []);
const handleFetchData = useCallback((...args) => {
onFetchData && onFetchData(...args)
}, [onFetchData]);
const handleSelectedRowsChange = useCallback((selectedRows) => {
onSelectedRowsChange && onSelectedRowsChange(selectedRows.map(s => s.original));
}, [onSelectedRowsChange]);
return (
<LoadingIndicator loading={loading} mount={false}>
<DataTable
columns={columns}
data={itemsCurrentPage}
selectionColumn={selectionColumn}
onFetchData={handleFetchData}
loading={itemsTableLoading && !initialMount}
noInitialFetch={true}
onSelectedRowsChange={handleSelectedRowsChange} />
</LoadingIndicator>
);
};
export default compose(
withItems,
)(ItemsDataTable);

View File

@@ -0,0 +1,172 @@
import React, { useEffect, useCallback, useState } from 'react';
import {
Route,
Switch,
} from 'react-router-dom';
import {
Intent,
Alert,
} from '@blueprintjs/core';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { useQuery } from 'react-query';
import ItemsActionsBar from 'containers/Items/ItemsActionsBar';
import { compose } from 'utils';
import ItemsDataTable from './ItemsDataTable';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import ItemsViewsTabs from 'containers/Items/ItemsViewsTabs';
import AppToaster from 'components/AppToaster';
import withItems from 'containers/Items/withItems';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withDashboardActions from 'containers/Dashboard/withDashboard';
import withItemsActions from 'containers/Items/withItemsActions';
import withViewsActions from 'containers/Views/withViewsActions';
function ItemsList({
// #withDashboard
changePageTitle,
// #withResourceActions
requestFetchResourceViews,
requestFetchResourceFields,
// #withItems
itemsViews,
itemsCurrentPage,
itemsTableQuery,
// #withItemsActions
requestDeleteItem,
requestFetchItems,
addItemsTableQueries,
changeItemsCurrentView
}) {
const [deleteItem, setDeleteItem] = useState(false);
const [selectedRows, setSelectedRows] = useState([]);
const [tableLoading, setTableLoading] = useState(false);
useEffect(() => {
changePageTitle('Items List');
}, [changePageTitle]);
const fetchHook = useQuery('items-resource', () => {
return Promise.all([
requestFetchResourceViews('items'),
requestFetchResourceFields('items'),
]);
});
const fetchItems = useQuery(['items-table', itemsTableQuery],
() => requestFetchItems({}));
// Handle click delete item.
const handleDeleteItem = useCallback((item) => {
setDeleteItem(item);
}, [setDeleteItem]);
const handleEditItem = () => {};
// Handle cancel delete the item.
const handleCancelDeleteItem = useCallback(() => {
setDeleteItem(false);
}, [setDeleteItem]);
// handle confirm delete item.
const handleConfirmDeleteItem = useCallback(() => {
requestDeleteItem(deleteItem.id).then(() => {
AppToaster.show({ message: 'the_item_has_been_deleted' });
setDeleteItem(false);
});
}, [requestDeleteItem, deleteItem]);
// Handle fetch data table.
const handleFetchData = useCallback(({ pageIndex, pageSize, sortBy }) => {
addItemsTableQueries({
...(sortBy.length > 0) ? {
column_sort_order: sortBy[0].id,
sort_order: sortBy[0].desc ? 'desc' : 'asc',
} : {},
});
}, [fetchItems, addItemsTableQueries]);
// Handle filter change to re-fetch the items.
const handleFilterChanged = useCallback((filterConditions) => {
addItemsTableQueries({
filter_roles: filterConditions || '',
});
}, [fetchItems]);
// Handle custom view change to re-fetch the items.
const handleCustomViewChanged = useCallback((customViewId) => {
setTableLoading(true);
}, [fetchItems]);
useEffect(() => {
if (tableLoading && !fetchItems.isFetching) {
setTableLoading(false);
}
}, [tableLoading, fetchItems.isFetching]);
// Handle selected rows change.
const handleSelectedRowsChange = useCallback((accounts) => {
setSelectedRows(accounts);
}, [setSelectedRows]);
return (
<DashboardInsider
isLoading={fetchHook.isFetching}
name={'items-list'}>
<ItemsActionsBar
onFilterChanged={handleFilterChanged}
selectedRows={selectedRows}
views={itemsViews} />
<DashboardPageContent>
<Switch>
<Route
exact={true}
path={[
'/dashboard/items/:custom_view_id/custom_view',
'/dashboard/items'
]}>
<ItemsViewsTabs
itemsViews={itemsViews}
onViewChanged={handleCustomViewChanged} />
<ItemsDataTable
loading={tableLoading}
onDeleteItem={handleDeleteItem}
onEditItem={handleEditItem}
onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange} />
<Alert
cancelButtonText="Cancel"
confirmButtonText="Move to Trash"
icon="trash"
intent={Intent.DANGER}
isOpen={deleteItem}
onCancel={handleCancelDeleteItem}
onConfirm={handleConfirmDeleteItem}>
<p>
Are you sure you want to move <b>filename</b> to Trash? You will be able to restore it later,
but it will become private to you.
</p>
</Alert>
</Route>
</Switch>
</DashboardPageContent>
</DashboardInsider>
)
}
export default compose(
withItems,
withResourceActions,
withDashboardActions,
withItemsActions,
withViewsActions,
)(ItemsList);

View File

@@ -0,0 +1,124 @@
import React, {useEffect} from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button
} from '@blueprintjs/core';
import { useParams } from 'react-router-dom';
import Icon from 'components/Icon';
import { Link, withRouter } from 'react-router-dom';
import { compose } from 'utils';
import {useUpdateEffect} from 'hooks';
import withItemsActions from 'containers/Items/withItemsActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import withViewDetail from 'containers/Views/withViewDetails';
function ItemsViewsTabs({
// #withViewDetail
viewId,
viewItem,
itemsViews,
// #withItemsActions
addItemsTableQueries,
changeItemsCurrentView,
// #withDashboard
setTopbarEditView,
changePageSubtitle,
// #props
onViewChanged,
}) {
const history = useHistory();
const { custom_view_id: customViewId } = useParams();
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/dashboard/custom_views/items/new');
};
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
}
useEffect(() => {
changeItemsCurrentView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
addItemsTableQueries({
custom_view_id: customViewId || null,
});
return () => {
setTopbarEditView(null);
changeItemsCurrentView(-1);
changePageSubtitle('');
};
}, [customViewId]);
useUpdateEffect(() => {
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
const tabs = itemsViews.map(view => {
const baseUrl = '/dashboard/items';
const link = (
<Link to={`${baseUrl}/${view.id}/custom_view`} onClick={handleViewLinkClick}>
{view.name}
</Link>
);
return (<Tab id={`custom_view_${view.id}`} title={link} />);
});
return (
<Navbar className='navbar--dashboard-views'>
<NavbarGroup align={Alignment.LEFT}>
<Tabs
id='navbar'
large={true}
selectedTabId={customViewId ? `custom_view_${customViewId}` : 'all'}
className='tabs--dashboard-views'
>
<Tab
id='all'
title={<Link to={`/dashboard/items`}>All</Link>}
onClick={handleViewLinkClick} />
{tabs}
<Button
className='button--new-view'
icon={<Icon icon='plus' />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
</NavbarGroup>
</Navbar>
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
});
const withItemsViewsTabs = connect(mapStateToProps);
export default compose(
withRouter,
withItemsViewsTabs,
withDashboard,
withItemsActions,
withViewDetail,
)(ItemsViewsTabs);

View File

@@ -0,0 +1,11 @@
import { connect } from 'react-redux';
export const mapStateToProps = (state, props) => {
return {
categoriesList: Object.values(state.itemCategories.categories),
categoriesTableLoading: state.itemCategories.loading,
};
};
export default connect(mapStateToProps);

View File

@@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import {
fetchItemCategories,
submitItemCategory,
deleteItemCategory,
editItemCategory,
} from 'store/itemCategories/itemsCategory.actions';
export const mapDispatchToProps = (dispatch) => ({
requestSubmitItemCategory: (form) => dispatch(submitItemCategory({ form })),
requestFetchItemCategories: () => dispatch(fetchItemCategories()),
requestDeleteItemCategory: (id) => dispatch(deleteItemCategory(id)),
requestEditItemCategory: (id, form) => dispatch(editItemCategory(id, form)),
});
export default connect(null, mapDispatchToProps);

View File

@@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import {
getCategoryId,
} from 'store/itemCategories/itemsCategory.reducer';
export const mapStateToProps = (state, props) => {
return {
itemCategory: getCategoryId(state, props.itemCategoryId),
};
};
export default connect(mapStateToProps);

View File

@@ -0,0 +1,26 @@
import {connect} from 'react-redux';
import {
getResourceViews,
getViewPages,
} from 'store/customViews/customViews.selectors'
import {
getCurrentPageResults
} from 'store/selectors';
export const mapStateToProps = (state, props) => {
const viewPages = getViewPages(state.items.views, state.items.currentViewId);
return {
itemsViews: getResourceViews(state, 'items'),
itemsCurrentPage: getCurrentPageResults(
state.items.items,
viewPages,
state.items.currentPage,
),
itemsBulkSelected: state.items.bulkActions,
itemsTableLoading: state.items.loading,
itemsTableQuery: state.items.tableQuery,
};
};
export default connect(mapStateToProps);

View File

@@ -0,0 +1,32 @@
import {connect} from 'react-redux';
import {
fetchItems,
deleteItem,
submitItem,
} from 'store/items/items.actions';
import t from 'store/types';
export const mapDispatchToProps = (dispatch) => ({
requestFetchItems: (query) => dispatch(fetchItems({ query })),
requestDeleteItem: (id) => dispatch(deleteItem({ id })),
requestSubmitItem: (form) => dispatch(submitItem({ form })),
addBulkActionItem: (id) => dispatch({
type: t.ITEM_BULK_ACTION_ADD, itemId: id
}),
removeBulkActionItem: (id) => dispatch({
type: t.ITEM_BULK_ACTION_REMOVE, itemId: id,
}),
setItemsTableQuery: (key, value) => dispatch({
type: t.ITEMS_TABLE_QUERY_SET, key, value,
}),
addItemsTableQueries: (queries) => dispatch({
type: t.ITEMS_TABLE_QUERIES_ADD, queries,
}),
changeItemsCurrentView: (id) => dispatch({
type: t.ITEMS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
}),
});
export default connect(null, mapDispatchToProps);