WIP item categories.

This commit is contained in:
elforjani3
2020-04-04 19:32:05 +02:00
parent cf5f56ae32
commit cff8945889
15 changed files with 697 additions and 111 deletions

View File

@@ -1,14 +1,14 @@
import React from 'react';
import AccountFormDialog from 'containers/Dashboard/Dialogs/AccountFormDialog';
import UserFormDialog from 'containers/Dashboard/Dialogs/UserFormDialog';
import ItemFromDialog from 'containers/Dashboard/Dialogs/ItemFromDialog';
import ItemFCategoryDialog from 'containers/Dashboard/Dialogs/ItemCategoryDialog';
export default function DialogsContainer() {
return (
<React.Fragment>
<ItemFCategoryDialog />
<AccountFormDialog />
<UserFormDialog />
<ItemFromDialog />
</React.Fragment>
);
}

View File

@@ -0,0 +1,85 @@
import React, { useEffect, useCallback, useState, 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
}) => {
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'
},
{
id: 'description',
Header: 'Description',
accessor: 'description'
},
{
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
// canResize: false
}
],
[]
);
const handelFetchData = useCallback(() => {
onFetchData && onFetchData();
}, []);
return (
<LoadingIndicator spinnerSize={30}>
<DataTable
columns={columns}
data={Object.values(categories)}
onFetchData={handelFetchData}
manualSortBy={true}
selectionColumn={true}
/>
</LoadingIndicator>
);
};
export default compose(DialogConnect, ItemsCategoryConnect)(ItemsCategoryList);

View File

@@ -17,6 +17,10 @@ export default [
iconSize: 20,
text: 'Items',
children: [
{
text: 'Category List',
href: '/dashboard/items/ItemCategoriesList'
},
{
text: 'Items List',
href: '/dashboard/items/list'
@@ -24,10 +28,6 @@ export default [
{
text: 'New Item',
href: '/dashboard/items/new'
},
{
text: 'Category List',
href: '/dashboard/items/category'
}
]
},

View File

@@ -0,0 +1,31 @@
import { connect } from 'react-redux';
import {
fetchItemCategories,
submitItemCategory,
deleteItemCategory,
editItemCategory
} from 'store/itemCategories/itemsCategory.actions';
import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
import { getCategoryId } from 'store/itemCategories/itemsCategory.reducer';
export const mapStateToProps = (state, props) => {
const dialogPayload = getDialogPayload(state, 'item-form');
return {
categories: state.itemCategories.categories,
name: 'item-form',
payload: { action: 'new', id: null },
editItemCategory:
dialogPayload && dialogPayload.action === 'edit'
? state.itemCategories.categories[dialogPayload.id]
: {},
getCategoryId: id => getCategoryId(state, id)
};
};
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(mapStateToProps, mapDispatchToProps);

View File

@@ -0,0 +1,205 @@
import React, { useState } from 'react';
import {
Button,
Classes,
FormGroup,
InputGroup,
Intent,
TextArea,
MenuItem
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { omit, pick } from 'lodash';
import * as Yup from 'yup';
import { useIntl } from 'react-intl';
import { useFormik } from 'formik';
import { compose } from 'utils';
import Dialog from 'components/Dialog';
import useAsync from 'hooks/async';
import AppToaster from 'components/AppToaster';
import DialogConnect from 'connectors/Dialog.connector';
import DialogReduxConnect from 'components/DialogReduxConnect';
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
function ItemCategoryDialog({
name,
payload,
isOpen,
openDialog,
closeDialog,
categories,
requestSubmitItemCategory,
requestFetchItemCategories,
requestEditItemCategory,
editItemCategory
}) {
const [state, setState] = useState({
selectedParentCategory: null
});
const intl = useIntl();
const ValidationSchema = Yup.object().shape({
name: Yup.string().required(intl.formatMessage({ id: 'required' })),
parent_category_id: Yup.string().nullable(),
description: Yup.string().trim()
});
const initialValues = {
name: '',
description: '',
parent_category_id: null
};
//Formik
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...(payload.action === 'edit' &&
pick(editItemCategory, Object.keys(initialValues)))
},
validationSchema: ValidationSchema,
onSubmit: values => {
if (payload.action === 'edit') {
requestEditItemCategory(payload.id, values).then(response => {
closeDialog(name);
AppToaster.show({
message: 'the_category_has_been_edited'
});
});
} else {
requestSubmitItemCategory(values)
.then(response => {
closeDialog(name);
AppToaster.show({
message: 'the_category_has_been_submit'
});
})
.catch(error => {
alert(error.message);
});
}
}
});
const filterItemCategory = (query, category, _index, exactMatch) => {
const normalizedTitle = category.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
const parentCategoryItem = (category, { handleClick, modifiers, query }) => {
return (
<MenuItem text={category.name} key={category.id} onClick={handleClick} />
);
};
const handleClose = () => {
closeDialog(name);
};
const fetchHook = useAsync(async () => {
await Promise.all([requestFetchItemCategories()]);
}, false);
const onDialogOpening = () => {
fetchHook.execute();
};
const onChangeParentCategory = parentCategory => {
setState({ ...state, selectedParentCategory: parentCategory.name });
formik.setFieldValue('parent_category_id', parentCategory.id);
};
const onDialogClosed = () => {
formik.resetForm();
closeDialog(name);
};
return (
<Dialog
name={name}
title={payload.action === 'edit' ? 'Edit Category' : ' New Category'}
className={{
'dialog--loading': state.isLoading,
'dialog--item-form': true
}}
isOpen={isOpen}
onClosed={onDialogClosed}
onOpening={onDialogOpening}
isLoading={fetchHook.pending}
>
<form onSubmit={formik.handleSubmit}>
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={'Category Name'}
className={'form-group--category-name'}
intent={formik.errors.name && Intent.DANGER}
helperText={formik.errors.name && formik.errors.name}
inline={true}
>
<InputGroup
medium={true}
intent={formik.errors.name && Intent.DANGER}
{...formik.getFieldProps('name')}
/>
</FormGroup>
<FormGroup
label={'Parent Category'}
className="{'form-group--parent-category'}"
inline={true}
helperText={
formik.errors.parent_category_id &&
formik.errors.parent_category_id
}
intent={formik.errors.parent_category_id && Intent.DANGER}
>
<Select
items={Object.values(categories)}
noResults={<MenuItem disabled={true} text='No results.' />}
itemRenderer={parentCategoryItem}
itemPredicate={filterItemCategory}
popoverProps={{ minimal: true }}
onItemSelect={onChangeParentCategory}
>
<Button
rightIcon='caret-down'
text={state.selectedParentCategory || 'Select Parent Category'}
/>
</Select>
</FormGroup>
<FormGroup
label={'Description'}
className={'form-group--description'}
intent={formik.errors.description && Intent.DANGER}
helperText={formik.errors.description && formik.errors.credential}
inline={true}
>
<TextArea
growVertically={true}
large={true}
{...formik.getFieldProps('description')}
/>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}>Close</Button>
<Button intent={Intent.PRIMARY} type='submit'>
{payload.action === 'edit' ? 'Edit' : 'Submit'}
</Button>
</div>
</div>
</form>
</Dialog>
);
}
export default compose(
ItemsCategoryConnect,
DialogConnect,
DialogReduxConnect
)(ItemCategoryDialog);

View File

@@ -1,5 +1,5 @@
import React, {useMemo} from 'react';
import {useRouteMatch, useHistory} from 'react-router-dom'
import React, { useMemo } from 'react';
import { useRouteMatch, useHistory } from 'react-router-dom';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { compose } from 'utils';
@@ -13,38 +13,50 @@ import {
Position,
Button,
Classes,
Intent,
Intent
} from '@blueprintjs/core';
import classNames from 'classnames';
import Icon from 'components/Icon';
import DashboardConnect from 'connectors/Dashboard.connector';
import ResourceConnect from 'connectors/Resource.connector'
import ResourceConnect from 'connectors/Resource.connector';
import FilterDropdown from 'components/FilterDropdown';
import ItemsConnect from 'connectors/Items.connect';
import DialogConnect from 'connectors/Dialog.connector';
const ItemsActionsBar = ({
openDialog,
getResourceFields,
getResourceViews,
views,
onFilterChange,
bulkSelected,
bulkSelected
}) => {
const {path} = useRouteMatch();
const { path } = useRouteMatch();
const history = useHistory();
const viewsMenuItems = views.map((view) => {
return (<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />);
const viewsMenuItems = views.map(view => {
return (
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
);
});
const onClickNewItem = () => { history.push('/dashboard/items/new'); };
const onClickNewItem = () => {
history.push('/dashboard/items/new');
};
const itemsFields = getResourceFields('items');
const filterDropdown = FilterDropdown({
fields: itemsFields,
onFilterChange,
onFilterChange
});
const hasBulkActionsSelected = useMemo(() =>
!!Object.keys(bulkSelected).length, [bulkSelected]);
const hasBulkActionsSelected = useMemo(
() => !!Object.keys(bulkSelected).length,
[bulkSelected]
);
const onClickNewCategory = () => {
openDialog('item-form', {});
};
return (
<DashboardActionsBar>
@@ -53,13 +65,14 @@ const ItemsActionsBar = ({
content={<Menu>{viewsMenuItems}</Menu>}
minimal={true}
interactionKind={PopoverInteractionKind.HOVER}
position={Position.BOTTOM_LEFT}>
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={ <Icon icon="table" /> }
text="Table Views"
rightIcon={'caret-down'} />
icon={<Icon icon='table' />}
text='Table Views'
rightIcon={'caret-down'}
/>
</Popover>
<NavbarDivider />
@@ -67,26 +80,36 @@ const ItemsActionsBar = ({
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}>
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text="Filter"
icon={ <Icon icon="filter" /> } />
text='Filter'
icon={<Icon icon='filter' />}
/>
</Popover>
<Button
className={Classes.MINIMAL}
icon={ <Icon icon="plus" /> }
text="New Item"
onClick={onClickNewItem} />
icon={<Icon icon='plus' />}
text='New Item'
onClick={onClickNewItem}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Category'
onClick={onClickNewCategory}
/>
{hasBulkActionsSelected && (
<Button
className={Classes.MINIMAL}
intent={Intent.DANGER}
icon={ <Icon icon="trash" />}
text="Delete" />)}
icon={<Icon icon='trash' />}
text='Delete'
/>
)}
<Button
className={Classes.MINIMAL}
@@ -104,7 +127,8 @@ const ItemsActionsBar = ({
};
export default compose(
DialogConnect,
DashboardConnect,
ResourceConnect,
ItemsConnect,
)(ItemsActionsBar);
ItemsConnect
)(ItemsActionsBar);

View File

@@ -0,0 +1,58 @@
import React, { useMemo } from 'react';
import {} from 'reselect';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { compose } from 'utils';
import { NavbarGroup, Button, Classes, Intent } from '@blueprintjs/core';
import Icon from 'components/Icon';
import DashboardConnect from 'connectors/Dashboard.connector';
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
import DialogConnect from 'connectors/Dialog.connector';
const ItemsCategoryActionsBar = ({ openDialog, onDeleteCategory }) => {
const onClickNewCategory = () => {
openDialog('item-form', {});
};
const handleDeleteCategory = category => {
onDeleteCategory(category);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='plus' />}
text='New Category'
onClick={onClickNewCategory}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon='trash' iconSize={15} />}
text='Delete Category'
intent={Intent.DANGER}
onClick={handleDeleteCategory}
/>
<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,
DashboardConnect,
ItemsCategoryConnect
)(ItemsCategoryActionsBar);

View File

@@ -0,0 +1,90 @@
import React, { useEffect, useState, useCallback } from 'react';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import useAsync from 'hooks/async';
import { useParams } from 'react-router-dom';
import DashboardConnect from 'connectors/Dashboard.connector';
import ItemsCategoryConnect from 'connectors/ItemsCategory.connect';
import { compose } from 'utils';
import ItemsCategoryList from 'components/Items/ItemsCategoryList';
import ItemsCategoryActionsBar from './ItemsCategoryActionsBar';
import { Alert, Intent } from '@blueprintjs/core';
import AppToaster from 'components/AppToaster';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
const ItemCategoriesList = ({
changePageTitle,
views,
requestFetchItemCategories,
requestEditItemCategory
}) => {
const { id } = useParams();
const [deleteCategory, setDeleteCategory] = useState(false);
useEffect(() => {
id
? changePageTitle('Edit Item Details')
: changePageTitle('Categories List');
}, []);
const fetchHook = useAsync(async () => {
await Promise.all([requestFetchItemCategories()]);
}, false);
const handelDeleteCategory = category => {
setDeleteCategory(category);
};
const handelEditCategory = category => {};
const handelCancelCategoryDelete = () => {
setDeleteCategory(false);
};
const handelConfirmCategoryDelete = useCallback(() => {
requestEditItemCategory(deleteCategory.id).then(() => {
setDeleteCategory(false);
AppToaster.show({
message: 'the_category_has_been_delete'
});
});
}, [deleteCategory]);
const handleFetchData = useCallback(() => {
fetchHook.execute();
}, []);
return (
<DashboardInsider loading={fetchHook.pending}>
<ItemsCategoryActionsBar
views={views}
onDeleteCategory={handelDeleteCategory}
/>
<DashboardPageContent>
<ItemsCategoryList
onDeleteCategory={handelDeleteCategory}
onFetchData={handleFetchData}
onEditCategory={handelEditCategory}
categories
/>
<Alert
cancelButtonText='Cancel'
confirmButtonText='Move to Trash'
icon='trash'
intent={Intent.DANGER}
isOpen={deleteCategory}
onCancel={handelCancelCategoryDelete}
onConfirm={handelConfirmCategoryDelete}
>
<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>
</DashboardPageContent>
</DashboardInsider>
);
};
export default compose(
DashboardConnect,
ItemsCategoryConnect
)(ItemCategoriesList);

View File

@@ -79,13 +79,14 @@ export default [
loader: () => import('containers/Dashboard/Items/ItemForm')
})
},
{
path: `${BASE_URL}/items/category`,
path: `${BASE_URL}/items/ItemCategoriesList`,
component: LazyLoader({
loader: () => import('containers/Dashboard/Items/ItemCategoryList')
loader: () => import('containers/Dashboard/Items/ItemsCategoryList')
})
},
,
// Financial Reports.
{
path: `${BASE_URL}/accounting/general-ledger`,

View File

@@ -1,42 +1,59 @@
import ApiService from 'services/ApiService';
import t from 'store/types';
export const submitCategory = ({ form }) => {
return dispatch =>
new Promise((resolve, reject) => {
ApiService.post('item_categories', form)
.then(response => {
dispatch({ type: t.ITEMS_CATEGORY_LIST_SET });
resolve(response);
})
.catch(error => {
const { response } = error;
const { data } = response;
const { errors } = data;
dispatch({ type: t.ITEMS_CATEGORY_LIST_SET });
if (errors) {
dispatch({ type: t.ITEMS_CATEGORY_LIST_SET, errors });
}
reject(error);
});
});
};
export const submitItemCategory = ({ form }) => {
return dispatch => {
return ApiService.post('item_categories', { ...form });
};
};
export const fetchCategory = () => {
export const fetchItemCategories = () => {
return (dispatch, getState) =>
new Promise((resolve, reject) => {
ApiService.get('item_categories')
.then(response => {
dispatch({
type: t.ITEMS_CATEGORY_DATA_TABLE,
data: response.data
type: t.ITEMS_CATEGORY_LIST_SET,
categories: response.data.categories
});
resolve(response);
})
.catch(error => {
reject(error);
});
});
};
export const editItemCategory = (id, form) => {
return dispatch =>
new Promise((resolve, reject) => {
ApiService.post(`item_categories/${id}`, form)
.then(response => {
dispatch({ type: t.CLEAR_CATEGORY_FORM_ERRORS });
resolve(response);
})
.catch(error => {
const { response } = error;
const { data } = response;
const { errors } = data;
dispatch({ type: t.CLEAR_CATEGORY_FORM_ERRORS });
if (errors) {
dispatch({ type: t.CATEGORY_FORM_ERRORS, errors });
}
reject(error);
});
});
};
export const deleteItemCategory = id => {
return dispatch =>
new Promise((resolve, reject) => {
ApiService.delete(`item_categories/${id}`)
.then(response => {
dispatch({
type: t.CATEGORY_DELETE,
id
});
resolve(response);
})

View File

@@ -0,0 +1,30 @@
import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
const initialState = {
categories: {}
};
export default createReducer(initialState, {
[t.ITEMS_CATEGORY_LIST_SET]: (state, action) => {
const _categories = {};
action.categories.forEach(category => {
_categories[category.id] = category;
});
state.categories = {
...state.categories,
..._categories
};
},
[t.CATEGORY_DELETE]: (state, action) => {
if (typeof state.categories[action.id] !== 'undefined') {
delete state.categories[action.id];
}
}
});
export const getCategoryId = (state, id) => {
return state.itemCategories.categories[id] || {};
};

View File

@@ -1,6 +1,6 @@
export default {
ITEMS_CATEGORY_LIST_SET: 'ITEMS_CATEGORY_LIST_SET',
ITEMS_CATEGORY_DATA_TABLE: 'ITEMS_CATEGORY_DATA_TABLE',
CATEGORY_DELETE: 'CATEGORY_DELETE',
CLEAR_CATEGORY_FORM_ERRORS: 'CLEAR_CATEGORY_FORM_ERRORS'
};

View File

@@ -2,8 +2,7 @@ import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
const initialState = {
categories: {},
categoriesById: {}
categories: {}
};
export default createReducer(initialState, {
@@ -18,10 +17,14 @@ export default createReducer(initialState, {
..._categories
};
},
[t.CATEGORY_SET]: (state, action) => {
state.categoriesById[action.category.id] = action.category;
[t.CATEGORY_DELETE]: (state, action) => {
if (typeof state.categories[action.id] !== 'undefined') {
delete state.categories[action.id];
}
}
});
export const getCategoryId = (state, id) => {
return state.categories.categoriesById[id];
return state.itemCategories.categories[id] || {};
};

View File

@@ -11,7 +11,7 @@ import expenses from './expenses/expenses.reducer';
import currencies from './currencies/currencies.reducer';
import resources from './resources/resources.reducer';
import financialStatements from './financialStatement/financialStatements.reducer';
import itemCategories from './itemCategories/itemsCateory.reducer';
import itemCategories from './itemCategories/itemsCategory.reducer';
export default combineReducers({
authentication,