- feat: Update react-query package to V 2.1.1.

- feat: Favicon setup.
- feat: Fix accounts inactivate/activate 1 account.
- feat: Seed accounts, expenses and manual journals resource fields.
- feat: Validate make journal receivable/payable without contact.
- feat: Validate make journal contact without receivable or payable.
- feat: More components abstractions.
- feat: Use reselect.js to memorize components properties.
- fix: Journal type of manual journal.
- fix: Sidebar style optimization.
- fix: Data-table check-box style optimization.
- fix: Data-table spinner style dimensions.
- fix: Submit journal with contact_id and contact_type.
This commit is contained in:
Ahmed Bouhuolia
2020-07-01 12:51:12 +02:00
parent 111aa83908
commit 4718f63c94
94 changed files with 1706 additions and 1001 deletions

View File

@@ -27,11 +27,29 @@ export default function AccountsSelectList({
onAccountSelected && onAccountSelected(account);
}, [setSelectedAccount, onAccountSelected]);
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(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
);
}
},
[],
);
return (
<Select
items={accounts}
noResults={<MenuItem disabled={true} text='No results.' />}
itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater}
popoverProps={{ minimal: true }}
filterable={true}
onItemSelect={onAccountSelect}>

View File

@@ -17,9 +17,9 @@ function App({ locale }) {
const history = createBrowserHistory();
const queryConfig = {
refetchAllOnWindowFocus: false,
cacheTime: 10000,
staleTime: 10000,
queries: {
refetchOnWindowFocus: true,
}
};
return (
<IntlProvider locale={locale} messages={messages}>

View File

@@ -32,7 +32,7 @@ export default function ContactsListField({
const onContactSelect = useCallback((contact) => {
setSelectedContact(contact.id);
onContactSelected && onContactSelected(contact.id);
onContactSelected && onContactSelected(contact);
}, [setSelectedContact, onContactSelected]);
return (

View File

@@ -9,11 +9,15 @@ import {
import {
Select
} from '@blueprintjs/select';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
export default function CurrenciesSelectList(props) {
const {formGroupProps, selectProps, onItemSelect} = props;
export default function CurrenciesSelectList({
formGroupProps,
selectProps,
onItemSelect,
className,
}) {
const currencies = [{
name: 'USD US dollars', key: 'USD',
name: 'CAD Canadian dollars', key: 'CAD',
@@ -41,7 +45,13 @@ export default function CurrenciesSelectList(props) {
return (
<FormGroup
label={<T id={'currency'}/>}
className={'form-group--select-list form-group--currency'}
className={
classNames(
'form-group--select-list',
'form-group--currency',
className,
)
}
{...formGroupProps}
>
<Select
@@ -54,7 +64,6 @@ export default function CurrenciesSelectList(props) {
{...selectProps}
>
<Button
rightIcon='caret-down'
text={'USD US dollars'}
/>
</Select>

View File

@@ -14,17 +14,15 @@ import { FormattedMessage as T } from 'react-intl';
import DashboardTopbarUser from 'components/Dashboard/TopbarUser';
import DashboardBreadcrumbs from 'components/Dashboard/DashboardBreadcrumbs';
import Icon from 'components/Icon';
import { Icon, If } from 'components';
import withSearch from 'containers/GeneralSearch/withSearch'
import withSearch from 'containers/GeneralSearch/withSearch';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils';
function DashboardTopbar({
// #withDashboard
pageTitle,
pageSubtitle,
@@ -42,69 +40,75 @@ function DashboardTopbar({
history.push(`/custom_views/${editViewId}/edit`);
};
const maybleRenderPageSubtitle = pageSubtitle && <h3>{pageSubtitle}</h3>;
const maybeRenderEditViewBtn = pageSubtitle && editViewId && (
<Button
className={Classes.MINIMAL + ' button--view-edit'}
icon={<Icon icon='pen' iconSize={13} />}
onClick={handlerClickEditView}
/>
);
const handleSidebarToggleBtn = () => {
toggleSidebarExpend();
};
return (
<div class='dashboard__topbar'>
<div class='dashboard__topbar-left'>
<div class='dashboard__topbar-sidebar-toggle'>
<Tooltip content={<T id={'close_sidebar'} />} position={Position.RIGHT}>
<div class="dashboard__topbar">
<div class="dashboard__topbar-left">
<div class="dashboard__topbar-sidebar-toggle">
<Tooltip
content={<T id={'close_sidebar'} />}
position={Position.RIGHT}
>
<Button minimal={true} onClick={handleSidebarToggleBtn}>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 20 20'
role='img'
focusable='false'
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title><T id={'menu'}/></title>
<title>
<T id={'menu'} />
</title>
<path
stroke='currentColor'
stroke-linecap='round'
stroke-miterlimit='5'
stroke-width='2'
d='M4 7h15M4 12h15M4 17h15'
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
</Tooltip>
</div>
<div class='dashboard__title'>
<div class="dashboard__title">
<h1>{pageTitle}</h1>
{maybleRenderPageSubtitle}
{maybeRenderEditViewBtn}
<If condition={pageSubtitle}>
<h3>{ pageSubtitle }</h3>
</If>
<If condition={pageSubtitle && editViewId}>
<Button
className={Classes.MINIMAL + ' button--view-edit'}
icon={<Icon icon="pen" iconSize={13} />}
onClick={handlerClickEditView}
/>
</If>
</div>
<div class='dashboard__breadcrumbs'>
<div class="dashboard__breadcrumbs">
<DashboardBreadcrumbs />
</div>
</div>
<div class='dashboard__topbar-right'>
<Navbar class='dashboard__topbar-navbar'>
<div class="dashboard__topbar-right">
<Navbar class="dashboard__topbar-navbar">
<NavbarGroup>
<Button
onClick={() => openGlobalSearch(true)}
className={Classes.MINIMAL}
icon={<Icon icon={'search-24'} iconSize={20} />}
text={<T id={'quick_find'}/>}
text={<T id={'quick_find'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'plus-24'} iconSize={20} />}
text={<T id={'quick_new'}/>}
text={<T id={'quick_new'} />}
/>
<Button
className={Classes.MINIMAL}
@@ -113,13 +117,13 @@ function DashboardTopbar({
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'help-24'} iconSize={20} />}
text={<T id={'help'} />} />
text={<T id={'help'} />}
/>
<NavbarDivider />
</NavbarGroup>
</Navbar>
<div class='dashboard__topbar-user'>
<div class="dashboard__topbar-user">
<DashboardTopbarUser />
</div>
</div>
@@ -129,8 +133,10 @@ function DashboardTopbar({
export default compose(
withSearch,
withDashboard(({ pageTitle, pageSubtitle, editViewId }) => ({
pageTitle, pageSubtitle, editViewId
withDashboard(({ pageTitle, pageSubtitle, editViewId }) => ({
pageTitle,
pageSubtitle,
editViewId,
})),
withDashboardActions,
)(DashboardTopbar);

View File

@@ -5,6 +5,7 @@ import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
import { If, Icon } from 'components';
export default function DashboardViewsTabs({
initialViewId = 0,
tabs,
allTab = true,
newViewTab = true,
@@ -12,7 +13,7 @@ export default function DashboardViewsTabs({
onChange,
onTabClick,
}) {
const [currentView, setCurrentView] = useState(0);
const [currentView, setCurrentView] = useState(initialViewId || 0);
const handleClickNewView = () => {
onNewViewTabClick && onNewViewTabClick();

View File

@@ -45,7 +45,7 @@ export default function DataTable({
expandable = false,
expandToggleColumn = 2,
noInitialFetch = false,
spinnerProps = { size: 40 },
spinnerProps = { size: 30 },
pagination = false,
pagesCount: controlledPageCount,
@@ -282,6 +282,7 @@ export default function DataTable({
className={classnames('bigcapital-datatable', className, {
'has-sticky': sticky,
'is-expandable': expandable,
'is-loading': loading,
'has-virtualized-rows': virtualizedRows,
})}
>
@@ -357,19 +358,19 @@ export default function DataTable({
<div class="td">{noResults}</div>
</div>
</If>
<If condition={loading}>
<div class="loading">
<Spinner size={spinnerProps.size} />
</div>
</If>
</div>
<If condition={loading}>
<div class="loading">
<Spinner size={spinnerProps.size} />
</div>
</If>
</div>
</ScrollSyncPane>
</div>
</ScrollSync>
<If condition={pagination && pageCount}>
<If condition={pagination && pageCount && !loading}>
<Pagination
initialPage={pageIndex + 1}
total={pageSize * pageCount}

View File

@@ -9,8 +9,11 @@ export default function ContactsListCellRenderer({
cell: { value: initialValue },
payload: { contacts, updateData, errors }
}) {
const handleContactSelected = useCallback((contactId) => {
updateData(index, id, contactId)
const handleContactSelected = useCallback((contact) => {
updateData(index, {
contact_id: contact.id,
contact_type: contact.contact_type,
});
}, [updateData, index, id]);
const initialContact = useMemo(() => {

View File

@@ -1,20 +1,21 @@
import React from 'react';
import { connect } from 'react-redux';
import { isDialogOpen, getDialogPayload } from 'store/dashboard/dashboard.selectors';
import {
isDialogOpenFactory,
getDialogPayloadFactory,
} from 'store/dashboard/dashboard.selectors';
export default (Dialog) => {
function DialogReduxConnect(props) {
return (<Dialog {...props} />);
};
export default (mapState, dialogName) => {
const isDialogOpen = isDialogOpenFactory(dialogName);
const getDialogPayload = getDialogPayloadFactory(dialogName);
const mapStateToProps = (state, props) => {
return {
const mapped = {
dialogName,
isOpen: isDialogOpen(state, props),
payload: getDialogPayload(state, props),
};
return mapState ? mapState(mapped) : mapped;
};
return connect(
mapStateToProps,
)(DialogReduxConnect);
}
return connect(mapStateToProps);
};

View File

@@ -8,13 +8,13 @@ import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog';
export default function DialogsContainer() {
return (
<>
<div>
<ExchangeRateDialog />
<InviteUserDialog />
{/* <InviteUserDialog /> */}
<CurrencyDialog />
<ItemCategoryDialog />
<AccountFormDialog />
<UserFormDialog />
</>
{/* <UserFormDialog /> */}
</div>
);
}

View File

@@ -53,7 +53,7 @@ function DynamicFilterValueField({
const [localValue, setLocalValue] = useState();
const fetchResourceData = useQuery(
resourceName && ['resource-data', resourceName],
['resource-data', resourceName && resourceName],
(k, resName) => requestResourceData(resName),
{ manual: true },
);

View File

@@ -1,10 +1,13 @@
import React from 'react';
import { Tooltip, Position } from '@blueprintjs/core';
import Icon from './Icon';
export default function FieldHint({ hint }) {
export default function FieldHint({ content, position }) {
return (
<span class="hint">
<Icon icon="info-circle" iconSize={12} />
<Tooltip content={content} position={position}>
<Icon icon="info-circle" iconSize={12} />
</Tooltip>
</span>
);
}

View File

@@ -1,5 +1,4 @@
import { FieldRequiredHint } from "components"
import React from 'react';
export default function FieldRequiredHint() {

View File

@@ -15,6 +15,9 @@ export default function ListSelect ({
selectedItem,
selectedItemProp = 'id',
// itemTextProp,
// itemLabelProp,
...selectProps
}) {
const [currentItem, setCurrentItem] = useState(null);
@@ -29,8 +32,13 @@ export default function ListSelect ({
const noResults = isLoading ?
('loading') : <MenuItem disabled={true} text={noResultsText} />;
const itemRenderer = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item[labelProp]} key={item[selectedItemProp]} />);
};
return (
<Select
itemRenderer={itemRenderer}
{...selectProps}
noResults={noResults}
>

View File

@@ -13,6 +13,11 @@ import FieldHint from './FieldHint';
import MenuItemLabel from './MenuItemLabel';
import Pagination from './Pagination';
import DashboardViewsTabs from './Dashboard/DashboardViewsTabs';
import CurrenciesSelectList from './CurrenciesSelectList';
import FieldRequiredHint from './FieldRequiredHint';
import Dialog from './Dialog';
import AppToaster from './AppToaster';
import DataTable from './DataTable';
const Hint = FieldHint;
@@ -33,5 +38,9 @@ export {
MenuItemLabel,
Pagination,
DashboardViewsTabs,
// For,
CurrenciesSelectList,
FieldRequiredHint,
Dialog,
AppToaster,
DataTable,
};

View File

@@ -82,7 +82,7 @@ export default [
href: '/manual-journals',
},
{
text: <T id={'make_journal'} />,
text: <T id={'make_journal_entry'} />,
href: '/make-journal-entry',
},
{
@@ -140,7 +140,7 @@ export default [
text: <T id={'expenses'} />,
children: [
{
text: <T id={'expenses_list'}/>,
text: <T id={'expenses'}/>,
href: '/expenses-list',
},
{

View File

@@ -82,6 +82,9 @@ function MakeJournalEntriesForm({
journal_number: Yup.string()
.required()
.label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string()
.required()
.label(formatMessage({ id: 'journal_type' })),
date: Yup.date()
.required()
.label(formatMessage({ id: 'date' })),
@@ -112,6 +115,12 @@ function MakeJournalEntriesForm({
const [payload, setPayload] = useState({});
const reorderingEntriesIndex = (entries) =>
entries.map((entry, index) => ({
...entry,
index: index + 1,
}));
const defaultEntry = useMemo(
() => ({
account_id: null,
@@ -126,6 +135,7 @@ function MakeJournalEntriesForm({
const defaultInitialValues = useMemo(
() => ({
journal_number: '',
journal_type: 'Journal',
date: moment(new Date()).format('YYYY-MM-DD'),
description: '',
reference: '',
@@ -145,6 +155,7 @@ function MakeJournalEntriesForm({
}
: {
...defaultInitialValues,
entries: reorderingEntriesIndex(defaultInitialValues.entries),
}),
}),
[manualJournal, defaultInitialValues, defaultEntry],
@@ -160,6 +171,45 @@ function MakeJournalEntriesForm({
: [];
}, [manualJournal]);
const transformErrors = (errors, { setErrors }) => {
const hasError = (errorType) => errors.some((e) => e.type === errorType);
if (hasError('CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT')) {
AppToaster.show({
message:
'customers_should_assign_with_receivable_account_only',
intent: Intent.DANGER,
});
}
if (hasError('VENDORS.NOT.WITH.PAYABLE.ACCOUNT')) {
AppToaster.show({
message: 'vendors_should_assign_with_payable_account_only',
intent: Intent.DANGER,
});
}
if (hasError('RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS')) {
AppToaster.show({
message:
'entries_with_receivable_account_no_assigned_with_customers',
intent: Intent.DANGER,
});
}
if (hasError('PAYABLE.ENTRIES.HAS.NO.VENDORS')) {
AppToaster.show({
message:
'entries_with_payable_account_no_assigned_with_vendors',
intent: Intent.DANGER,
});
}
if (hasError('JOURNAL.NUMBER.ALREADY.EXISTS')) {
setErrors({
journal_number: formatMessage({
id: 'journal_number_is_already_used',
}),
});
}
}
const formik = useFormik({
enableReinitialize: true,
validationSchema,
@@ -181,7 +231,19 @@ function MakeJournalEntriesForm({
// Validate the total credit should be eqials total debit.
if (totalCredit !== totalDebit) {
AppToaster.show({
message: formatMessage({ id: 'credit_and_debit_not_equal' }),
message: formatMessage({
id: 'should_total_of_credit_and_debit_be_equal',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
} else if (totalCredit === 0 || totalDebit === 0) {
AppToaster.show({
message: formatMessage({
id: 'should_total_of_credit_and_debit_be_bigger_then_zero',
}),
intent: Intent.DANGER,
});
setSubmitting(false);
return;
@@ -209,15 +271,7 @@ function MakeJournalEntriesForm({
resolve(response);
})
.catch((errors) => {
if (
errors.find((e) => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')
) {
setErrors({
journal_number: formatMessage({
id: 'journal_number_is_already_used',
}),
});
}
transformErrors(errors, { setErrors });
setSubmitting(false);
});
} else {
@@ -237,15 +291,7 @@ function MakeJournalEntriesForm({
resolve(response);
})
.catch((errors) => {
if (
errors.find((e) => e.type === 'JOURNAL.NUMBER.ALREADY.EXISTS')
) {
setErrors({
journal_number: formatMessage({
id: 'journal_number_is_already_used',
}),
});
}
transformErrors(errors, { setErrors });
setSubmitting(false);
});
}

View File

@@ -1,78 +1,102 @@
import React, {useMemo, useCallback} from 'react';
import React, { useMemo, useCallback } from 'react';
import {
InputGroup,
FormGroup,
Intent,
Position,
Classes,
} from '@blueprintjs/core';
import {DateInput} from '@blueprintjs/datetime';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from 'react-intl';
import {Row, Col} from 'react-grid-system';
import { Row, Col } from 'react-grid-system';
import moment from 'moment';
import {momentFormatter} from 'utils';
import Icon from 'components/Icon';
import CurrenciesSelectList from 'components/CurrenciesSelectList';
import ErrorMessage from 'components/ErrorMessage';
import classNames from 'classnames';
import { momentFormatter } from 'utils';
import {
CurrenciesSelectList,
ErrorMessage,
Hint,
FieldHint,
FieldRequiredHint,
} from 'components';
export default function MakeJournalEntriesHeader({
formik: { errors, touched, setFieldValue, getFieldProps }
formik: { errors, touched, values, setFieldValue, getFieldProps },
}) {
const handleDateChange = useCallback((date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date', formatted);
}, [setFieldValue]);
const infoIcon = useMemo(() =>
(<Icon icon="info-circle" iconSize={12} />), []);
const handleDateChange = useCallback(
(date) => {
const formatted = moment(date).format('YYYY-MM-DD');
setFieldValue('date', formatted);
},
[setFieldValue],
);
return (
<div class="make-journal-entries__header">
<Row>
<Col width={260}>
<FormGroup
label={<T id={'journal_number'}/>}
labelInfo={infoIcon}
label={<T id={'journal_number'} />}
labelInfo={
<>
<FieldRequiredHint />
<FieldHint />
</>
}
className={'form-group--journal-number'}
intent={(errors.journal_number && touched.journal_number) && Intent.DANGER}
helperText={<ErrorMessage name="journal_number" {...{errors, touched}} />}
fill={true}>
intent={
errors.journal_number && touched.journal_number && Intent.DANGER
}
helperText={
<ErrorMessage name="journal_number" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={(errors.journal_number && touched.journal_number) && Intent.DANGER}
intent={
errors.journal_number && touched.journal_number && Intent.DANGER
}
fill={true}
{...getFieldProps('journal_number')} />
{...getFieldProps('journal_number')}
/>
</FormGroup>
</Col>
<Col width={220}>
<FormGroup
label={<T id={'date'}/>}
intent={(errors.date && touched.date) && Intent.DANGER}
helperText={<ErrorMessage name="date" {...{errors, touched}} />}
minimal={true}>
label={<T id={'date'} />}
labelInfo={<FieldRequiredHint />}
intent={errors.date && touched.date && Intent.DANGER}
helperText={<ErrorMessage name="date" {...{ errors, touched }} />}
minimal={true}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
defaultValue={new Date()}
onChange={handleDateChange}
popoverProps={{ position: Position.BOTTOM }} />
popoverProps={{
position: Position.BOTTOM,
minimal: true,
}}
/>
</FormGroup>
</Col>
<Col width={400}>
<FormGroup
label={<T id={'description'}/>}
label={<T id={'description'} />}
className={'form-group--description'}
intent={(errors.name && touched.name) && Intent.DANGER}
helperText={<ErrorMessage name="description" {...{errors, touched}} />}
fill={true}>
intent={errors.name && touched.name && Intent.DANGER}
helperText={
<ErrorMessage name="description" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={(errors.name && touched.name) && Intent.DANGER}
intent={errors.name && touched.name && Intent.DANGER}
fill={true}
{...getFieldProps('description')} />
{...getFieldProps('description')}
/>
</FormGroup>
</Col>
</Row>
@@ -80,24 +104,51 @@ export default function MakeJournalEntriesHeader({
<Row>
<Col width={260}>
<FormGroup
label={<T id={'reference'}/>}
labelInfo={infoIcon}
label={<T id={'reference'} />}
labelInfo={
<Hint
content={<T id={'journal_reference_hint'} />}
position={Position.RIGHT}
/>
}
className={'form-group--reference'}
intent={(errors.reference && touched.reference) && Intent.DANGER}
helperText={<ErrorMessage name="reference" {...{errors, touched}} />}
fill={true}>
intent={errors.reference && touched.reference && Intent.DANGER}
helperText={
<ErrorMessage name="reference" {...{ errors, touched }} />
}
fill={true}
>
<InputGroup
intent={(errors.reference && touched.reference) && Intent.DANGER}
intent={errors.reference && touched.reference && Intent.DANGER}
fill={true}
{...getFieldProps('reference')} />
{...getFieldProps('reference')}
/>
</FormGroup>
</Col>
<Col width={220}>
<CurrenciesSelectList />
<FormGroup
label={<T id={'journal_type'} />}
className={classNames(
'form-group--account-type',
'form-group--select-list',
Classes.FILL,
)}
>
<InputGroup
intent={
errors.journal_type && touched.journal_type && Intent.DANGER
}
fill={true}
{...getFieldProps('journal_type')}
/>
</FormGroup>
</Col>
<Col width={230}>
<CurrenciesSelectList className={Classes.FILL} />
</Col>
</Row>
</div>
);
}
}

View File

@@ -9,8 +9,7 @@ import withCustomersActions from 'containers/Customers/withCustomersActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
import {compose} from 'utils';
import { compose } from 'utils';
function MakeJournalEntriesPage({
// #withCustomersActions
@@ -25,20 +24,26 @@ function MakeJournalEntriesPage({
const history = useHistory();
const { id } = useParams();
const fetchAccounts = useQuery('accounts-list',
(key) => requestFetchAccounts());
const fetchAccounts = useQuery('accounts-list', (key) =>
requestFetchAccounts(),
);
const fetchCustomers = useQuery('customers-list',
(key) => requestFetchCustomers());
const fetchCustomers = useQuery('customers-list', (key) =>
requestFetchCustomers(),
);
const fetchJournal = useQuery(
id && ['manual-journal', id],
(key, journalId) => requestFetchManualJournal(journalId));
['manual-journal', id],
(key, journalId) => requestFetchManualJournal(journalId),
{ enabled: id && id },
);
const handleFormSubmit = useCallback((payload) => {
payload.redirect &&
history.push('/manual-journals');
}, [history]);
const handleFormSubmit = useCallback(
(payload) => {
payload.redirect && history.push('/manual-journals');
},
[history],
);
const handleCancel = useCallback(() => {
history.push('/manual-journals');
@@ -51,11 +56,13 @@ function MakeJournalEntriesPage({
fetchAccounts.isFetching ||
fetchCustomers.isFetching
}
name={'make-journal-page'}>
name={'make-journal-page'}
>
<MakeJournalEntriesForm
onFormSubmit={handleFormSubmit}
manualJournalId={id}
onCancelForm={handleCancel} />
onCancelForm={handleCancel}
/>
</DashboardInsider>
);
}
@@ -64,4 +71,4 @@ export default compose(
withAccountsActions,
withCustomersActions,
withManualJournalsActions,
)(MakeJournalEntriesPage);
)(MakeJournalEntriesPage);

View File

@@ -1,5 +1,5 @@
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { Button, Tooltip, Position, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit } from 'lodash';
@@ -31,14 +31,16 @@ const ActionsCellRenderer = ({
payload.removeRow(index);
};
return (
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
<Tooltip content={<T id={'remove_the_line'} />} position={Position.LEFT}>
<Button
icon={<Icon icon="times-circle" iconSize={14} />}
iconSize={14}
className="ml2"
minimal={true}
intent={Intent.DANGER}
onClick={onClickRemoveRole}
/>
</Tooltip>
);
};
@@ -101,10 +103,18 @@ function MakeJournalEntriesTable({
// Handles update datatable data.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
(rowIndex, columnIdOrBulk, value) => {
const columnId = typeof columnIdOrBulk !== 'object'
? columnIdOrBulk : null;
const updateTable = typeof columnIdOrBulk === 'object'
? columnIdOrBulk : null;
const newData = updateTable ? updateTable : { [columnId]: value };
const newRows = rows.map((row, index) => {
if (index === rowIndex) {
return { ...rows[rowIndex], [columnId]: value };
return { ...rows[rowIndex], ...newData };
}
return { ...row };
});
@@ -179,14 +189,22 @@ function MakeJournalEntriesTable({
width: 150,
},
{
Header: (<><T id={'contact'} /><Hint /></>),
Header: (
<>
<T id={'contact'} />
<Hint
content={<T id={'contact_column_hint'} />}
position={Position.LEFT_BOTTOM}
/>
</>
),
id: 'contact_id',
accessor: 'contact_id',
Cell: NoteCellRenderer(ContactsListFieldCell),
className: 'contact',
disableResizing: true,
disableSortBy: true,
width: 180,
width: 200,
},
{
Header: formatMessage({ id: 'note' }),
@@ -235,7 +253,8 @@ function MakeJournalEntriesTable({
removeRow: handleRemoveRow,
contacts: [
...customers.map((customer) => ({
...customer, contact_type: 'customer',
...customer,
contact_type: 'customer',
})),
],
}}

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from 'react';
import React, { useMemo, useState, useCallback } from 'react';
import Icon from 'components/Icon';
import {
Button,
@@ -45,6 +45,7 @@ function ManualJournalActionsBar({
onBulkDelete,
}) {
const { path } = useRouteMatch();
const [filterCount, setFilterCount] = useState(0);
const history = useHistory();
const viewsMenuItems = manualJournalsViews.map((view) => {
@@ -65,6 +66,7 @@ function ManualJournalActionsBar({
value: '',
},
onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0);
addManualJournalsTableQueries({
filter_roles: filterConditions || '',
});
@@ -112,7 +114,9 @@ function ManualJournalActionsBar({
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
className={classNames(Classes.MINIMAL, 'button--filter', {
'has-active-filters': filterCount > 0,
})}
text="Filter"
icon={<Icon icon="filter-16" iconSize={16} />}
/>
@@ -128,6 +132,11 @@ function ManualJournalActionsBar({
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}

View File

@@ -15,68 +15,79 @@ import { withRouter, useParams } from 'react-router-dom';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import Icon from 'components/Icon';
import { DataTable, If, Money, Choose, Icon } from 'components';
import { compose } from 'utils';
import { useUpdateEffect } from 'hooks';
import LoadingIndicator from 'components/LoadingIndicator';
import { If, Money } from 'components';
import DataTable from 'components/DataTable';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
import withManualJournals from 'containers/Accounting/withManualJournals';
import withManualJournalsActions from 'containers/Accounting/withManualJournalsActions';
function ManualJournalsDataTable({
loading,
/**
* Status column accessor.
*/
function StatusAccessor(row) {
return (
<Choose>
<Choose.When condition={row.status}>
<Tag minimal={true}>
<T id={'published'} />
</Tag>
</Choose.When>
<Choose.Otherwise>
<Tag minimal={true} intent={Intent.WARNING}>
<T id={'draft'} />
</Tag>
</Choose.Otherwise>
</Choose>
);
}
/**
* Note column accessor.
*/
function NoteAccessor(row) {
return (
<If condition={row.description}>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.LEFT_TOP}
hoverOpenDelay={50}
>
<Icon icon={'file-alt'} iconSize={16} />
</Tooltip>
</If>
);
}
function ManualJournalsDataTable({
// #withManualJournals
manualJournalsCurrentPage,
manualJournalsLoading,
manualJournalsPagination,
manualJournalsTableQuery,
// #withDashboardActions
changeCurrentView,
changePageSubtitle,
setTopbarEditView,
// #withViewDetails
viewId,
viewMeta,
onFetchData,
onEditJournal,
onDeleteJournal,
onPublishJournal,
onSelectedRowsChange,
}) {
const [isMounted, setIsMounted] = useState(false);
const { custom_view_id: customViewId } = useParams();
const [initialMount, setInitialMount] = useState(false);
const { formatMessage } = useIntl();
useUpdateEffect(() => {
if (!manualJournalsLoading) {
setInitialMount(true);
}
}, [manualJournalsLoading, setInitialMount]);
useEffect(() => {
setIsMounted(false);
}, [customViewId]);
useEffect(() => {
if (customViewId) {
changeCurrentView(customViewId);
setTopbarEditView(customViewId);
if (!manualJournalsLoading) {
setIsMounted(true);
}
changePageSubtitle(customViewId && viewMeta ? viewMeta.name : '');
}, [
customViewId,
changeCurrentView,
changePageSubtitle,
setTopbarEditView,
viewMeta,
]);
}, [manualJournalsLoading, setIsMounted]);
const handlePublishJournal = useCallback(
(journal) => () => {
@@ -106,7 +117,7 @@ function ManualJournalsDataTable({
<MenuDivider />
{!journal.status && (
<MenuItem
text={formatMessage({ id: 'publish_journal' })}
text={formatMessage({ id: 'publish_journal' })}
onClick={handlePublishJournal(journal)}
/>
)}
@@ -121,84 +132,69 @@ function ManualJournalsDataTable({
/>
</Menu>
),
[handleEditJournal, handleDeleteJournal, handlePublishJournal, formatMessage],
[
handleEditJournal,
handleDeleteJournal,
handlePublishJournal,
formatMessage,
],
);
const onRowContextMenu = useCallback((cell) => {
return actionMenuList(cell.row.original);
}, [actionMenuList]);
const onRowContextMenu = useCallback(
(cell) => actionMenuList(cell.row.original),
[actionMenuList],
);
const columns = useMemo(
() => [
{
id: 'date',
Header: formatMessage({ id: 'date' }),
accessor: (r) => moment().format('YYYY-MM-DD'),
disableResizing: true,
width: 150,
accessor: (r) => moment(r.date).format('YYYY MMM DD'),
width: 115,
className: 'date',
},
{
id: 'amount',
Header: formatMessage({ id: 'amount' }),
accessor: (r) => <Money amount={r.amount} currency={'USD'} />,
disableResizing: true,
className: 'amount',
width: 115,
},
{
id: 'journal_number',
Header: formatMessage({ id: 'journal_no' }),
accessor: 'journal_number',
disableResizing: true,
accessor: (row) => `#${row.journal_number}`,
className: 'journal_number',
width: 100,
},
{
id: 'journal_type',
Header: formatMessage({ id: 'journal_type' }),
accessor: 'journal_type',
width: 110,
className: 'journal_type',
},
{
id: 'status',
Header: formatMessage({ id: 'status' }),
accessor: (r) => {
return r.status ? (
<Tag minimal={true}><T id={'published'} /></Tag>
) : (
<Tag minimal={true} intent={Intent.WARNING}><T id={'draft'} /></Tag>
);
},
disableResizing: true,
width: 100,
accessor: StatusAccessor,
width: 95,
className: 'status',
},
{
id: 'note',
Header: formatMessage({ id: 'note' }),
accessor: (row) => (
<If condition={row.description}>
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.TOP}
hoverOpenDelay={250}
>
<Icon icon={'file-alt'} iconSize={16} />
</Tooltip>
</If>
),
disableResizing: true,
accessor: NoteAccessor,
disableSorting: true,
width: 100,
width: 85,
className: 'note',
},
{
id: 'transaction_type',
Header: formatMessage({ id: 'transaction_type' }),
accessor: 'transaction_type',
width: 100,
className: 'transaction_type',
},
{
id: 'created_at',
Header: formatMessage({ id: 'created_at' }),
accessor: (r) => moment().format('YYYY-MM-DD'),
disableResizing: true,
width: 150,
accessor: (r) => moment(r.created_at).format('YYYY MMM DD'),
width: 125,
className: 'created_at',
},
{
@@ -236,33 +232,28 @@ function ManualJournalsDataTable({
);
return (
<LoadingIndicator loading={loading} mount={false}>
<DataTable
columns={columns}
data={manualJournalsCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
loading={manualJournalsLoading && !manualJournalsCurrentPage.length > 0}
onSelectedRowsChange={handleSelectedRowsChange}
pagination={true}
rowContextMenu={onRowContextMenu}
pagesCount={manualJournalsPagination.pagesCount}
initialPageSize={manualJournalsTableQuery.page_size}
initialPageIndex={manualJournalsTableQuery.page - 1}
/>
</LoadingIndicator>
<DataTable
columns={columns}
data={manualJournalsCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
loading={manualJournalsLoading && !isMounted}
onSelectedRowsChange={handleSelectedRowsChange}
pagination={true}
rowContextMenu={onRowContextMenu}
pagesCount={manualJournalsPagination.pagesCount}
initialPageSize={manualJournalsTableQuery.page_size}
initialPageIndex={manualJournalsTableQuery.page - 1}
/>
);
}
export default compose(
withRouter,
withDialogActions,
withDashboardActions,
withManualJournalsActions,
withManualJournals(
({
@@ -277,5 +268,4 @@ export default compose(
manualJournalsTableQuery,
}),
),
withViewDetails,
)(ManualJournalsDataTable);

View File

@@ -43,8 +43,6 @@ function ManualJournalsTable({
requestPublishManualJournal,
requestDeleteBulkManualJournals,
addManualJournalsTableQueries,
addQuery,
}) {
const history = useHistory();
@@ -54,18 +52,19 @@ function ManualJournalsTable({
const { formatMessage } = useIntl();
const fetchViews = useQuery('manual-journals-resource-views', () => {
return requestFetchResourceViews('manual_journals');
});
const fetchResourceViews = useQuery(
['resource-views', 'manual-journals'],
() => requestFetchResourceViews('manual_journals'),
);
const fetchResourceFields = useQuery(
'manual-journals-resource-fields',
['resource-fields', 'manual-journals'],
() => requestFetchResourceFields('manual_journals'),
);
const fetchManualJournals = useQuery(
['manual-journals-table', manualJournalsTableQuery],
(key, q) => requestFetchManualJournalsTable(q),
(key, q) => requestFetchManualJournalsTable(),
);
useEffect(() => {
@@ -198,7 +197,7 @@ function ManualJournalsTable({
return (
<DashboardInsider
loading={fetchViews.isFetching || fetchResourceFields.isFetching}
loading={fetchResourceViews.isFetching || fetchResourceFields.isFetching}
name={'manual-journals'}
>
<ManualJournalsActionsBar

View File

@@ -1,115 +1,98 @@
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { useHistory } from 'react-router';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button,
} from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { FormattedMessage as T } from 'react-intl';
import { pick, debounce } from 'lodash';
import { useUpdateEffect } from 'hooks';
import Icon from 'components/Icon';
import { DashboardViewsTabs, Icon } from 'components';
import withManualJournals from './withManualJournals';
import withManualJournalsActions from './withManualJournalsActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetail from 'containers/Views/withViewDetails';
import { compose } from 'utils';
function ManualJournalsViewTabs({
// #withViewDetail
viewId,
viewItem,
// #withManualJournals
manualJournalsViews,
// #withManualJournalsActions
addManualJournalsTableQueries,
changeManualJournalCurrentView,
// #withDashboardActions
setTopbarEditView,
changePageSubtitle,
// #ownProps
customViewChanged,
onViewChanged,
}) {
const history = useHistory();
const { custom_view_id: customViewId } = useParams();
useEffect(() => {
changeManualJournalCurrentView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
addManualJournalsTableQueries({
custom_view_id: customViewId,
});
}, [customViewId, addManualJournalsTableQueries]);
useUpdateEffect(() => {
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
const tabs = manualJournalsViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
const debounceChangeHistory = useRef(
debounce((toUrl) => {
history.push(toUrl);
}, 250),
);
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/custom_views/manual_journals/new');
};
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
const handleTabChange = (viewId) => {
const toPath = viewId ? `${viewId}/custom_view` : '';
debounceChangeHistory.current(`/manual-journals/${toPath}`);
setTopbarEditView(viewId);
};
useUpdateEffect(() => {
customViewChanged && customViewChanged(customViewId);
addManualJournalsTableQueries({
custom_view_id: customViewId || null,
});
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
useEffect(() => {
addManualJournalsTableQueries({
custom_view_id: customViewId,
});
}, [customViewId,addManualJournalsTableQueries]);
const tabs = manualJournalsViews.map((view) => {
const baseUrl = '/manual-journals';
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'>
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<Tabs
id='navbar'
large={true}
selectedTabId={`custom_view_${customViewId}`}
className='tabs--dashboard-views'
>
<Tab
id='all'
title={
<Link to={`/dashboard/accounting/manual-journals`}>
<T id={'all'} />
</Link>
}
/>
{ tabs }
<Button
className='button--new-view'
icon={<Icon icon='plus' />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
<DashboardViewsTabs
initialViewId={customViewId}
baseUrl={'/manual-journals'}
tabs={tabs}
onChange={handleTabChange}
onNewViewTabClick={handleClickNewView}
/>
</NavbarGroup>
</Navbar>
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
viewId: parseInt(ownProps.match.params.custom_view_id, 10),
});
const withManualJournalsViewTabs = connect(mapStateToProps);
@@ -121,5 +104,6 @@ export default compose(
manualJournalsViews,
})),
withManualJournalsActions,
withDashboardActions
withDashboardActions,
withViewDetail(),
)(ManualJournalsViewTabs);

View File

@@ -1,37 +1,23 @@
import { connect } from 'react-redux';
import { pick, mapValues } from 'lodash';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import { getManualJournalsItems } from 'store/manualJournals/manualJournals.selectors';
import {
getManualJournalsItems,
getManualJournalsPagination,
getManualJournalsTableQuery,
} from 'store/manualJournals/manualJournals.selectors';
const queryParamsKeys = ['page_size', 'page'];
export default (mapState) => {
const mapStateToProps = (state, props) => {
const queryParams = props.location
? new URLSearchParams(props.location.search)
: null;
const manualJournalsTableQuery = {
...state.manualJournals.tableQuery,
...(queryParams
? mapValues(
pick(Object.fromEntries(queryParams), queryParamsKeys),
(v) => parseInt(v, 10),
)
: {}),
};
const query = getManualJournalsTableQuery(state, props);
const mapped = {
manualJournalsCurrentPage: getManualJournalsItems(
state,
state.manualJournals.currentViewId,
manualJournalsTableQuery.page,
),
manualJournalsTableQuery,
manualJournalsCurrentPage: getManualJournalsItems(state, props, query),
manualJournalsTableQuery: query,
manualJournalsViews: getResourceViews(state, props, 'manual_journals'),
manualJournalsItems: state.manualJournals.items,
manualJournalsPagination: state.manualJournals.paginationMeta,
manualJournalsPagination: getManualJournalsPagination(state, props, query),
manualJournalsLoading: state.manualJournals.loading,
};
return mapState ? mapState(mapped, state, props) : mapped;

View File

@@ -14,9 +14,11 @@ const mapActionsToProps = (dispatch) => ({
requestFetchManualJournal: (id) => dispatch(fetchManualJournal({ id })),
requestPublishManualJournal: (id) => dispatch(publishManualJournal({ id })),
requestDeleteBulkManualJournals: (ids) => dispatch(deleteBulkManualJournals({ ids })),
changeCurrentView: (id) => dispatch({
changeManualJournalCurrentView: (id) => dispatch({
type: t.MANUAL_JOURNALS_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),
payload: {
currentViewId: parseInt(id, 10),
}
}),
addManualJournalsTableQueries: (queries) => dispatch({
type: t.MANUAL_JOURNALS_TABLE_QUERIES_ADD,

View File

@@ -59,20 +59,22 @@ function AccountsChart({
const [selectedRows, setSelectedRows] = useState([]);
const [bulkActivate, setBulkActivate] = useState(false);
const [bulkInactiveAccounts, setBulkInactiveAccounts] = useState(false);
const [tableLoading, setTableLoading] = useState(false);
// Fetch accounts resource views and fields.
const fetchHook = useQuery('resource-accounts', () => {
return Promise.all([
requestFetchResourceViews('accounts'),
requestFetchResourceFields('accounts'),
]);
});
const fetchResourceViews = useQuery(
['resource-views', 'accounts'],
(key, resourceName) => requestFetchResourceViews(resourceName),
);
const fetchResourceFields = useQuery(
['resource-fields', 'accounts'],
(key, resourceName) => requestFetchResourceFields(resourceName),
);
// Fetch accounts list according to the given custom view id.
const fetchAccountsHook = useQuery(
['accounts-table', accountsTableQuery],
() => requestFetchAccountsTable(),
(key, q) => requestFetchAccountsTable(),
);
useEffect(() => {
@@ -89,6 +91,7 @@ function AccountsChart({
setDeleteAccount(false);
}, []);
// Handle delete errors in bulk and singular.
const handleDeleteErrors = (errors) => {
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
AppToaster.show({
@@ -221,17 +224,6 @@ function AccountsChart({
fetchAccountsHook.refetch();
}, [fetchAccountsHook]);
// Refetch accounts data table when current custom view changed.
const handleViewChanged = useCallback(async () => {
setTableLoading(true);
}, [fetchAccountsHook]);
useEffect(() => {
if (tableLoading && !fetchAccountsHook.isFetching) {
setTableLoading(false);
}
}, [tableLoading, fetchAccountsHook.isFetching]);
// Handle fetch data of accounts datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
@@ -243,7 +235,6 @@ function AccountsChart({
}
: {}),
});
fetchAccountsHook.refetch();
},
[fetchAccountsHook, addAccountsTableQueries],
);
@@ -314,7 +305,10 @@ function AccountsChart({
}, [requestBulkInactiveAccounts, bulkInactiveAccounts]);
return (
<DashboardInsider loading={fetchHook.isFetching} name={'accounts-chart'}>
<DashboardInsider
loading={fetchResourceFields.isFetching || fetchResourceViews.isFetching}
name={'accounts-chart'}
>
<DashboardActionsBar
selectedRows={selectedRows}
onFilterChanged={handleFilterChanged}
@@ -323,17 +317,15 @@ function AccountsChart({
onBulkActivate={handleBulkActivate}
onBulkInactive={handleBulkInactive}
/>
<DashboardPageContent>
<Switch>
<Route
exact={true}
path={['/accounts/:custom_view_id/custom_view', '/accounts']}
>
<AccountsViewsTabs onViewChanged={handleViewChanged} />
<AccountsViewsTabs />
<AccountsDataTable
loading={fetchAccountsHook.isFetching}
onDeleteAccount={handleDeleteAccount}
onInactiveAccount={handleInactiveAccount}
onActivateAccount={handleActivateAccount}
@@ -341,7 +333,6 @@ function AccountsChart({
onEditAccount={handleEditAccount}
onFetchData={handleFetchData}
onSelectedRowsChange={handleSelectedRowsChange}
/>
</Route>
</Switch>

View File

@@ -12,13 +12,13 @@ import {
} from '@blueprintjs/core';
import { withRouter } from 'react-router';
import { FormattedMessage as T, useIntl } from 'react-intl';
import Icon from 'components/Icon';
import {
Icon,
DataTable,
Money,
If,
} from 'components';
import { compose } from 'utils';
import LoadingIndicator from 'components/LoadingIndicator';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { useUpdateEffect } from 'hooks';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
@@ -27,7 +27,53 @@ import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withCurrentView from 'containers/Views/withCurrentView';
import { If } from 'components';
function NormalCell({ cell }) {
const { formatMessage } = useIntl();
const account = cell.row.original;
const normal = account?.type?.normal || '';
const arrowDirection = normal === 'credit' ? 'down' : 'up';
return (
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={formatMessage({ id: normal })}
position={Position.RIGHT}
hoverOpenDelay={100}
>
<Icon icon={`arrow-${arrowDirection}`} />
</Tooltip>
);
}
function BalanceCell({ cell }) {
const account = cell.row.original;
const { balance = null } = account;
return balance ? (
<span>
<Money amount={balance.amount} currency={balance.currency_code} />
</span>
) : (
<span class="placeholder">--</span>
);
}
function AccountNameAccessor(row) {
return row.description ? (
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.RIGHT_TOP}
hoverOpenDelay={500}
>
{row.name}
</Tooltip>
) : (
row.name
);
}
function AccountsDataTable({
// #withDashboardActions
@@ -40,7 +86,6 @@ function AccountsDataTable({
currentViewId,
// own properties
loading,
onFetchData,
onSelectedRowsChange,
onDeleteAccount,
@@ -125,20 +170,7 @@ function AccountsDataTable({
{
id: 'name',
Header: formatMessage({ id: 'account_name' }),
accessor: (row) => {
return row.description ? (
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={row.description}
position={Position.RIGHT_TOP}
hoverOpenDelay={500}
>
{row.name}
</Tooltip>
) : (
row.name
);
},
accessor: AccountNameAccessor,
className: 'account_name',
width: 220,
},
@@ -159,40 +191,22 @@ function AccountsDataTable({
{
id: 'normal',
Header: formatMessage({ id: 'normal' }),
Cell: ({ cell }) => {
const account = cell.row.original;
const normal = account.type ? account.type.normal : '';
const arrowDirection = normal === 'credit' ? 'down' : 'up';
return (
<Tooltip
className={Classes.TOOLTIP_INDICATOR}
content={formatMessage({ id: normal })}
position={Position.RIGHT}
hoverOpenDelay={500}
>
<Icon icon={`arrow-${arrowDirection}`} />
</Tooltip>
);
},
Cell: NormalCell,
accessor: 'type.normal',
className: 'normal',
width: 115,
},
{
id: 'currency',
Header: formatMessage({ id: 'currency' }),
accessor: (row) => 'USD',
width: 100,
},
{
id: 'balance',
Header: formatMessage({ id: 'balance' }),
Cell: ({ cell }) => {
const account = cell.row.original;
const { balance = null } = account;
return balance ? (
<span>
<Money amount={balance.amount} currency={balance.currency_code} />
</span>
) : (
<span class="placeholder">--</span>
);
},
accessor: 'balance',
Cell: BalanceCell,
width: 150,
},
{
@@ -235,23 +249,19 @@ function AccountsDataTable({
);
return (
<LoadingIndicator loading={loading && !isMounted} mount={false}>
<DataTable
noInitialFetch={true}
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
treeGraph={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !isMounted}
spinnerProps={{ size: 30 }}
rowContextMenu={rowContextMenu}
/>
</LoadingIndicator>
<DataTable
noInitialFetch={true}
columns={columns}
data={accounts}
onFetchData={handleDatatableFetchData}
manualSortBy={true}
selectionColumn={selectionColumn}
expandable={true}
sticky={true}
onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !isMounted}
rowContextMenu={rowContextMenu}
/>
);
}

View File

@@ -5,17 +5,12 @@ import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button,
} from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { pick, debounce } from 'lodash';
import Icon from 'components/Icon';
import { FormattedMessage as T } from 'react-intl';
import { useUpdateEffect } from 'hooks';
import { DashboardViewsTabs } from 'components';
import { DashboardViewsTabs, Icon } from 'components';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccounts from 'containers/Accounts/withAccounts';
import withAccountsTableActions from 'containers/Accounts/withAccountsTableActions';
@@ -49,7 +44,7 @@ function AccountsViewsTabs({
useEffect(() => {
changeAccountsCurrentView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
changePageSubtitle((customViewId && viewItem) ? viewItem.name : '');
addAccountsTableQueries({
custom_view_id: customViewId,
@@ -92,6 +87,7 @@ function AccountsViewsTabs({
<Navbar className="navbar--dashboard-views">
<NavbarGroup align={Alignment.LEFT}>
<DashboardViewsTabs
initialViewId={customViewId}
baseUrl={'/accounts'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
@@ -116,5 +112,5 @@ export default compose(
accountsViews,
})),
withAccountsTableActions,
withViewDetail,
withViewDetail(),
)(AccountsViewsTabs);

View File

@@ -1,10 +1,8 @@
import { connect } from 'react-redux';
import {
getItemById
} from 'store/selectors';
import { getAccountById } from 'store/accounts/accounts.selectors';
const mapStateToProps = (state, props) => ({
account: getItemById(state.accounts.items, props.accountId),
account: getAccountById(state, props),
});
export default connect(mapStateToProps);

View File

@@ -1,32 +1,24 @@
import { connect } from 'react-redux';
import { compose } from 'utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import DialogReduxConnect from 'components/DialogReduxConnect';
import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
import withDialogRedux from 'components/DialogReduxConnect';
import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withAccountDetail from 'containers/Accounts/withAccountDetail';
import withAccounts from 'containers/Accounts/withAccounts';
export const mapStateToProps = (state, props) => {
const dialogPayload = getDialogPayload(state, 'account-form');
return {
name: 'account-form',
payload: { action: 'new', id: null, ...dialogPayload },
accountId: dialogPayload?.id || null,
};
};
export const mapStateToProps = (state, props) => ({
dialogName: 'account-form',
});
const AccountFormDialogConnect = connect(mapStateToProps);
export default compose(
AccountFormDialogConnect,
withDialogRedux(null, 'account-form'),
withAccountsActions,
withAccountDetail,
withAccounts(({ accountsTypes, accounts }) => ({
accountsTypes,
accounts,
})),
DialogReduxConnect,
withDialogActions,
);

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import {
Button,
Classes,
@@ -13,23 +13,25 @@ import {
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { omit, pick } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import Dialog from 'components/Dialog';
import AppToaster from 'components/AppToaster';
import classNames from 'classnames';
import {
ListSelect,
ErrorMessage,
Dialog,
AppToaster,
FieldRequiredHint,
Hint,
} from 'components';
import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container';
import classNames from 'classnames';
import Icon from 'components/Icon';
import ErrorMessage from 'components/ErrorMessage';
import { ListSelect } from 'components';
/**
* Account form dialog.
*/
function AccountFormDialog({
name,
payload,
dialogName,
payload = { action: 'new', id: null },
isOpen,
// #withAccounts
@@ -115,7 +117,7 @@ function AccountFormDialog({
},
})
.then((response) => {
closeDialog(name);
closeDialog(dialogName);
queryCache.refetchQueries('accounts-table', { force: true });
AppToaster.show({
@@ -137,7 +139,7 @@ function AccountFormDialog({
} else {
requestSubmitAccount({ form: { ...omit(values, exclude) } })
.then((response) => {
closeDialog(name);
closeDialog(dialogName);
queryCache.refetchQueries('accounts-table', { force: true });
AppToaster.show({
@@ -190,7 +192,12 @@ function AccountFormDialog({
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem text={item.name} label={item.code} key={item.id} onClick={handleClick} />
<MenuItem
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
};
@@ -213,14 +220,14 @@ function AccountFormDialog({
// Handles dialog close.
const handleClose = useCallback(() => {
closeDialog(name);
}, [closeDialog, name]);
closeDialog(dialogName);
}, [closeDialog, dialogName]);
// Fetches accounts list.
const fetchAccountsList = useQuery(
'accounts-list',
() => requestFetchAccounts(),
{ manual: true },
{ enabled: true },
);
// Fetches accounts types.
@@ -229,14 +236,14 @@ function AccountFormDialog({
async () => {
await requestFetchAccountTypes();
},
{ manual: true },
{ enabled: true },
);
// Fetch the given account id on edit mode.
const fetchAccount = useQuery(
payload.action === 'edit' && ['account', payload.id],
['account', payload.id],
(key, id) => requestFetchAccount(id),
{ manual: true },
{ enabled: false },
);
const isFetching =
@@ -248,8 +255,11 @@ function AccountFormDialog({
const onDialogOpening = useCallback(() => {
fetchAccountsList.refetch();
fetchAccountsTypes.refetch();
fetchAccount.refetch();
}, [fetchAccount, fetchAccountsList, fetchAccountsTypes]);
if (payload.action === 'edit' && payload.id) {
fetchAccount.refetch();
}
}, [payload, fetchAccount, fetchAccountsList, fetchAccountsTypes]);
const onChangeAccountType = useCallback(
(accountType) => {
@@ -270,12 +280,11 @@ function AccountFormDialog({
resetForm();
}, [resetForm]);
const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
const subAccountLabel = useMemo(() => {
return (
<span>
<T id={'sub_account'} /> <Icon icon="info-circle" iconSize={12} />
<T id={'sub_account'} />
<Hint />
</span>
);
}, []);
@@ -284,7 +293,7 @@ function AccountFormDialog({
return (
<Dialog
name={name}
name={dialogName}
title={
payload.action === 'edit' ? (
<T id={'edit_account'} />
@@ -308,7 +317,7 @@ function AccountFormDialog({
<div className={Classes.DIALOG_BODY}>
<FormGroup
label={<T id={'account_type'} />}
labelInfo={requiredSpan}
labelInfo={<FieldRequiredHint />}
className={classNames(
'form-group--account-type',
'form-group--select-list',
@@ -339,7 +348,7 @@ function AccountFormDialog({
<FormGroup
label={<T id={'account_name'} />}
labelInfo={requiredSpan}
labelInfo={<FieldRequiredHint />}
className={'form-group--account-name'}
intent={errors.name && touched.name && Intent.DANGER}
helperText={<ErrorMessage name="name" {...{ errors, touched }} />}
@@ -358,7 +367,7 @@ function AccountFormDialog({
intent={errors.code && touched.code && Intent.DANGER}
helperText={<ErrorMessage name="code" {...{ errors, touched }} />}
inline={true}
labelInfo={infoIcon}
labelInfo={<Hint content={<T id='account_code_hint' />} />}
>
<InputGroup
medium={true}

View File

@@ -15,7 +15,7 @@ import { pick } from 'lodash';
import AppToaster from 'components/AppToaster';
import Dialog from 'components/Dialog';
import DialogReduxConnect from 'components/DialogReduxConnect';
import withDialogRedux from 'components/DialogReduxConnect';
import ErrorMessage from 'components/ErrorMessage';
import classNames from 'classnames';
import withDialogActions from 'containers/Dialog/withDialogActions';
@@ -221,22 +221,16 @@ function CurrencyDialog({
);
}
const mapStateToProps = (state, props) => {
const dialogPayload = getDialogPayload(state, 'currency-form');
return {
name: 'currency-form',
payload: { action: 'new', currencyCode: null, ...dialogPayload },
currencyCode: dialogPayload?.currencyCode || null,
};
};
const mapStateToProps = (state, props) => ({
dialogName: 'currency-form',
});
const withCurrencyFormDialog = connect(mapStateToProps);
export default compose(
withCurrencyFormDialog,
withDialogActions,
DialogReduxConnect,
withCurrenciesActions,
withDialogRedux(null, 'currency-form'),
withCurrency,
withDialogActions,
withCurrenciesActions,
)(CurrencyDialog);

View File

@@ -1,35 +1,29 @@
import { connect } from 'react-redux';
import { compose } from 'utils';
import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
import withDialogActions from 'containers/Dialog/withDialogActions';
import DialogReduxConnect from 'components/DialogReduxConnect';
import withDialogRedux from 'components/DialogReduxConnect';
import withExchangeRatesActions from 'containers/ExchangeRates/withExchangeRatesActions';
import withExchangeRates from 'containers/ExchangeRates/withExchangeRates';
import withCurrencies from 'containers/Currencies/withCurrencies';
const mapStateToProps = (state, props) => {
const dialogPayload = getDialogPayload(state, 'exchangeRate-form');
return {
name: 'exchangeRate-form',
payload: { action: 'new', id: null, ...dialogPayload },
};
};
const mapStateToProps = (state, props) => ({
dialogName: 'exchangeRate-form',
});
const withExchangeRateDialog = connect(mapStateToProps);
export default compose(
withExchangeRateDialog,
withDialogRedux(null, 'exchangeRate-form'),
withCurrencies(({ currenciesList }) => ({
currenciesList,
})),
withExchangeRatesActions,
withExchangeRates(({ exchangeRatesList }) => ({
exchangeRatesList,
})),
DialogReduxConnect,
withExchangeRatesActions,
withDialogActions,
);

View File

@@ -16,18 +16,21 @@ import { useQuery, queryCache } from 'react-query';
import moment from 'moment';
import { DateInput } from '@blueprintjs/datetime';
import { momentFormatter } from 'utils';
import AppToaster from 'components/AppToaster';
import Dialog from 'components/Dialog';
import ErrorMessage from 'components/ErrorMessage';
import {
AppToaster,
Dialog,
ErrorMessage,
ListSelect,
} from 'components';
import classNames from 'classnames';
import { ListSelect } from 'components';
import withExchangeRatesDialog from './ExchangeRateDialog.container';
/**
* Exchange rate dialog.
*/
function ExchangeRateDialog({
name,
payload,
dialogName,
payload = {},
isOpen,
// #withDialog
@@ -91,7 +94,7 @@ function ExchangeRateDialog({
if (payload.action === 'edit') {
requestEditExchangeRate(payload.id, values)
.then((response) => {
closeDialog(name);
closeDialog(dialogName);
AppToaster.show({
message: formatMessage({
id: 'the_exchange_rate_has_been_successfully_edited',
@@ -107,7 +110,7 @@ function ExchangeRateDialog({
} else {
requestSubmitExchangeRate(values)
.then((response) => {
closeDialog(name);
closeDialog(dialogName);
AppToaster.show({
message: formatMessage({
id: 'the_exchange_rate_has_been_successfully_created',
@@ -136,13 +139,13 @@ function ExchangeRateDialog({
const requiredSpan = useMemo(() => <span class="required">*</span>, []);
const handleClose = useCallback(() => {
closeDialog(name);
}, [name, closeDialog]);
closeDialog(dialogName);
}, [dialogName, closeDialog]);
const onDialogClosed = useCallback(() => {
resetForm();
closeDialog(name);
}, [closeDialog, name, resetForm]);
closeDialog(dialogName);
}, [closeDialog, dialogName, resetForm]);
const onDialogOpening = useCallback(() => {
fetchExchangeRatesDialog.refetch();
@@ -197,7 +200,7 @@ function ExchangeRateDialog({
return (
<Dialog
name={name}
name={dialogName}
title={
payload.action === 'edit' ? (
<T id={'edit_exchange_rate'} />

View File

@@ -24,9 +24,8 @@ import { ListSelect } from 'components';
import Dialog from 'components/Dialog';
import withDialogActions from 'containers/Dialog/withDialogActions';
import DialogReduxConnect from 'components/DialogReduxConnect';
import withDialogRedux from 'components/DialogReduxConnect';
import { getDialogPayload } from 'store/dashboard/dashboard.reducer';
import withItemCategoryDetail from 'containers/Items/withItemCategoryDetail';
import withItemCategories from 'containers/Items/withItemCategories';
import withItemCategoriesActions from 'containers/Items/withItemCategoriesActions';
@@ -34,7 +33,7 @@ import withItemCategoriesActions from 'containers/Items/withItemCategoriesAction
import Icon from 'components/Icon';
function ItemCategoryDialog({
name,
dialogName,
payload,
isOpen,
@@ -99,7 +98,7 @@ function ItemCategoryDialog({
if (payload.action === 'edit') {
requestEditItemCategory(payload.id, values)
.then((response) => {
closeDialog(name);
closeDialog(dialogName);
AppToaster.show({
message: formatMessage({
id: 'the_item_category_has_been_successfully_edited',
@@ -117,7 +116,7 @@ function ItemCategoryDialog({
} else {
requestSubmitItemCategory(values)
.then((response) => {
closeDialog(name);
closeDialog(dialogName);
AppToaster.show({
message: formatMessage({
id: 'the_item_category_has_been_successfully_created',
@@ -165,8 +164,8 @@ function ItemCategoryDialog({
// Handle the dialog closing.
const handleClose = useCallback(() => {
closeDialog(name);
}, [name, closeDialog]);
closeDialog(dialogName);
}, [dialogName, closeDialog]);
// Handle the dialog opening.
const onDialogOpening = useCallback(() => {
@@ -183,15 +182,15 @@ function ItemCategoryDialog({
const onDialogClosed = useCallback(() => {
resetForm();
closeDialog(name);
}, [resetForm, closeDialog, name]);
closeDialog(dialogName);
}, [resetForm, closeDialog, dialogName]);
const requiredSpan = useMemo(() => <span class="required">*</span>, []);
const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
return (
<Dialog
name={name}
name={dialogName}
title={
payload.action === 'edit' ? (
<T id={'edit_category'} />
@@ -303,23 +302,17 @@ function ItemCategoryDialog({
);
}
const mapStateToProps = (state, props) => {
const dialogPayload = getDialogPayload(state, 'item-category-form');
return {
name: 'item-category-form',
payload: { action: 'new', id: null, ...dialogPayload },
itemCategoryId: dialogPayload?.id || null,
};
};
const mapStateToProps = (state, props) => ({
itemCategoryId: props?.dialogPayload?.id || null,
});
const withItemCategoryDialog = connect(mapStateToProps);
export default compose(
withDialogRedux(null, 'item-category-form'),
withItemCategoryDialog,
withDialogActions,
DialogReduxConnect,
withItemCategoryDetail,
withItemCategoryDetail(),
withItemCategories(({ categoriesList }) => ({
categoriesList,
})),

View File

@@ -1,9 +1,10 @@
import { connect } from 'react-redux';
import { getExchangeRatesList } from 'store/ExchangeRate/exchange.selector';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
exchangeRatesList: Object.values(state.exchangeRates.exchangeRates),
exchangeRatesList: getExchangeRatesList(state, props),
exchangeRatesLoading: state.exchangeRates.loading,
};
return mapState ? mapState(mapped, state, props) : mapped;

View File

@@ -15,6 +15,7 @@ import {
import classNames from 'classnames';
import { useRouteMatch, useHistory } from 'react-router-dom';
import { FormattedMessage as T } from 'react-intl';
import { connect } from 'react-redux';
import FilterDropdown from 'components/FilterDropdown';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
@@ -28,12 +29,13 @@ import withExpensesActions from 'containers/Expenses/withExpensesActions';
import { compose } from 'utils';
function ExpenseActionsBar({
function ExpensesActionsBar({
// #withResourceDetail
resourceFields,
//#withExpenses
expensesViews,
//#withExpensesActions
addExpensesTableQueries,
@@ -44,17 +46,20 @@ function ExpenseActionsBar({
const { path } = useRouteMatch();
const history = useHistory();
const viewsMenuItems = expensesViews.map((view) => {
return (
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
);
});
const viewsMenuItems = expensesViews.map((view) => (
<MenuItem href={`${path}/${view.id}/custom_view`} text={view.name} />
));
const onClickNewExpense = useCallback(() => {
history.push('/expenses/new');
}, [history]);
const filterDropdown = FilterDropdown({
initialCondition: {
fieldKey: 'reference_no',
compatator: 'contains',
value: '',
},
fields: resourceFields,
onFilterChange: (filterConditions) => {
addExpensesTableQueries({
@@ -97,6 +102,7 @@ function ExpenseActionsBar({
onClick={onClickNewExpense}
/>
<Popover
minimal={true}
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
@@ -118,6 +124,11 @@ function ExpenseActionsBar({
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
@@ -133,7 +144,14 @@ function ExpenseActionsBar({
);
}
const mapStateToProps = (state, props) => ({
resourceName: 'expenses',
});
const withExpensesActionsBar = connect(mapStateToProps);
export default compose(
withExpensesActionsBar,
withDialogActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
@@ -142,4 +160,4 @@ export default compose(
expensesViews,
})),
withExpensesActions,
)(ExpenseActionsBar);
)(ExpensesActionsBar);

View File

@@ -29,10 +29,11 @@ import withViewDetails from 'containers/Views/withViewDetails';
import withExpenses from 'containers/Expenses/withExpenses';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
function ExpenseDataTable({
function ExpensesDataTable({
//#withExpenes
expenses,
expensesCurrentPage,
expensesLoading,
expensesPagination,
// #withDashboardActions
changeCurrentView,
@@ -101,7 +102,7 @@ function ExpenseDataTable({
<MenuItem
text={formatMessage({ id: 'view_details' })} />
<MenuDivider />
<If condition={expenses.published}>
<If condition={expense.published}>
<MenuItem
text={formatMessage({ id: 'publish_expense' })}
onClick={handlePublishExpense(expense)}
@@ -142,7 +143,7 @@ function ExpenseDataTable({
{
id: 'payment_date',
Header: formatMessage({ id: 'payment_date' }),
accessor: () => moment().format('YYYY-MM-DD'),
accessor: () => moment().format('YYYY MMM DD'),
width: 150,
className: 'payment_date',
},
@@ -250,15 +251,19 @@ function ExpenseDataTable({
<LoadingIndicator loading={loading} mount={false}>
<DataTable
columns={columns}
data={expenses}
data={expensesCurrentPage}
onFetchData={handleDataTableFetchData}
manualSortBy={true}
selectionColumn={true}
noInitialFetch={true}
sticky={true}
loading={expensesLoading && !initialMount}
loading={expensesLoading}
onSelectedRowsChange={handleSelectedRowsChange}
rowContextMenu={onRowContextMenu}
pagination={true}
pagesCount={expensesPagination.pagesCount}
initialPageSize={expensesPagination.pageSize}
initialPageIndex={expensesPagination.page - 1}
/>
</LoadingIndicator>
</div>
@@ -269,9 +274,10 @@ export default compose(
withDialogActions,
withDashboardActions,
withExpensesActions,
withExpenses(({ expenses, expensesLoading }) => ({
expenses,
withExpenses(({ expensesCurrentPage, expensesLoading, expensesPagination }) => ({
expensesCurrentPage,
expensesLoading,
expensesPagination,
})),
withViewDetails,
)(ExpenseDataTable);
)(ExpensesDataTable);

View File

@@ -99,7 +99,6 @@ function ExpenseForm({
description: Yup.string()
.trim()
.label(formatMessage({ id: 'description' })),
publish: Yup.boolean().label(formatMessage({ id: 'publish' })),
categories: Yup.array().of(
Yup.object().shape({
@@ -258,8 +257,6 @@ function ExpenseForm({
},
});
console.log(formik.values, 'VALUES');
const handleSubmitClick = useCallback(
(payload) => {
setPayload(payload);
@@ -285,8 +282,6 @@ function ExpenseForm({
},
[setDeletedFiles, deletedFiles],
);
// @todo @mohamed
const fetchHook = useQuery('expense-form', () => requestFetchExpensesTable());
return (
<div className={'dashboard__insider--expense-form'}>
@@ -334,5 +329,5 @@ export default compose(
withAccountsActions,
withDashboardActions,
withMediaActions,
withExpneseDetail,
withExpneseDetail(),
)(ExpenseForm);

View File

@@ -14,9 +14,13 @@ import moment from 'moment';
import { momentFormatter, compose } from 'utils';
import classNames from 'classnames';
import Icon from 'components/Icon';
import ErrorMessage from 'components/ErrorMessage';
import { ListSelect } from 'components';
import {
ListSelect,
ErrorMessage,
Icon,
FieldRequiredHint,
Hint,
} from 'components';
import withCurrencies from 'containers/Currencies/withCurrencies';
import withAccounts from 'containers/Accounts/withAccounts';
@@ -35,11 +39,6 @@ function ExpenseFormHeader({
[setFieldValue],
);
// @todo @mohamed reusable components.
const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
const requiredSpan = useMemo(() => <span className="required">*</span>, []);
const currencyCodeRenderer = useCallback((item, { handleClick }) => {
return (
<MenuItem key={item.id} text={item.currency_code} onClick={handleClick} />
@@ -121,7 +120,7 @@ function ExpenseFormHeader({
<FormGroup
label={<T id={'beneficiary'} />}
className={classNames('form-group--select-list', Classes.FILL)}
labelInfo={infoIcon}
labelInfo={<Hint />}
intent={errors.beneficiary && touched.beneficiary && Intent.DANGER}
helperText={
<ErrorMessage name={'beneficiary'} {...{ errors, touched }} />
@@ -150,7 +149,7 @@ function ExpenseFormHeader({
'form-group--select-list',
Classes.FILL,
)}
labelInfo={requiredSpan}
labelInfo={<FieldRequiredHint />}
intent={
errors.payment_account_id &&
touched.payment_account_id &&
@@ -183,7 +182,7 @@ function ExpenseFormHeader({
<Col width={300}>
<FormGroup
label={<T id={'payment_date'} />}
labelInfo={infoIcon}
labelInfo={<Hint />}
className={classNames('form-group--select-list', Classes.FILL)}
intent={
errors.payment_date && touched.payment_date && Intent.DANGER
@@ -234,10 +233,7 @@ function ExpenseFormHeader({
<Col width={200}>
<FormGroup
label={<T id={'ref_no'} />}
className={classNames(
'form-group--ref_no',
Classes.FILL,
)}
className={classNames('form-group--ref_no', Classes.FILL)}
intent={
errors.reference_no && touched.reference_no && Intent.DANGER
}

View File

@@ -1,113 +1,94 @@
import React, { useEffect } from 'react';
import React, { useEffect, useRef } from 'react';
import { useHistory } from 'react-router';
import {
Alignment,
Navbar,
NavbarGroup,
Tabs,
Tab,
Button,
} from '@blueprintjs/core';
import { useParams, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { FormattedMessage as T } from 'react-intl';
import { pick, debounce } from 'lodash';
import { useUpdateEffect } from 'hooks';
import Icon from 'components/Icon';
import { DashboardViewsTabs } from 'components';
import withExpenses from './withExpenses';
import withExpensesActions from './withExpensesActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withViewDetails from 'containers/Views/withViewDetails';
import { compose } from 'utils';
function ExpenseViewTabs({
//#withExpenses
// #withExpenses
expensesViews,
//#withExpensesActions
// #withViewDetails
viewItem,
// #withExpensesActions
addExpensesTableQueries,
changeExpensesView,
// #withDashboardActions
setTopbarEditView,
// #ownProps
customViewChanged,
onViewChanged,
changePageSubtitle,
}) {
const history = useHistory();
const { custom_view_id: customViewId } = useParams();
useEffect(() => {
changeExpensesView(customViewId || -1);
setTopbarEditView(customViewId);
changePageSubtitle(customViewId && viewItem ? viewItem.name : '');
addExpensesTableQueries({
custom_view_id: customViewId,
});
return () => {
setTopbarEditView(null);
changePageSubtitle('');
changeExpensesView(null);
};
}, [customViewId, addExpensesTableQueries, changeExpensesView]);
const debounceChangeHistory = useRef(
debounce((toUrl) => {
history.push(toUrl);
}, 250),
);
const handleTabsChange = (viewId) => {
const toPath = viewId ? `${viewId}/custom_view` : '';
debounceChangeHistory.current(`/expenses/${toPath}`);
setTopbarEditView(viewId);
};
const tabs = expensesViews.map((view) => ({
...pick(view, ['name', 'id']),
}));
// Handle click a new view tab.
const handleClickNewView = () => {
setTopbarEditView(null);
history.push('/custom_views/expenses/new');
};
const handleViewLinkClick = () => {
setTopbarEditView(customViewId);
};
useUpdateEffect(() => {
customViewChanged && customViewChanged(customViewId);
addExpensesTableQueries({
custom_view_id: customViewId || null,
});
onViewChanged && onViewChanged(customViewId);
}, [customViewId]);
useEffect(() => {
addExpensesTableQueries({
custom_view_id: customViewId,
});
}, [customViewId, addExpensesTableQueries]);
const tabs = expensesViews.map((view) => {
const baseUrl = '/expenses/new';
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={`custom_view_${customViewId}`}
className="tabs--dashboard-views"
>
<Tab
id="all"
title={
<Link to={``}>
<T id={'all'} />
</Link>
}
/>
{tabs}
<Button
className="button--new-view"
icon={<Icon icon="plus" />}
onClick={handleClickNewView}
minimal={true}
/>
</Tabs>
<DashboardViewsTabs
initialViewId={customViewId}
baseUrl={'/expenses'}
tabs={tabs}
onNewViewTabClick={handleClickNewView}
onChange={handleTabsChange}
/>
</NavbarGroup>
</Navbar>
);
}
const mapStateToProps = (state, ownProps) => ({
// Mapping view id from matched route params.
viewId: ownProps.match.params.custom_view_id,
});
@@ -115,6 +96,7 @@ const withExpensesViewTabs = connect(mapStateToProps);
export default compose(
withRouter,
withViewDetails(),
withExpensesViewTabs,
withExpenses(({ expensesViews }) => ({
expensesViews,

View File

@@ -12,18 +12,21 @@ import ExpenseDataTable from 'containers/Expenses/ExpenseDataTable';
import ExpenseActionsBar from 'containers/Expenses/ExpenseActionsBar';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withResourceActions from 'containers/Resources/withResourcesActions';
import withExpenses from 'containers/Expenses/withExpenses';
import withExpensesActions from 'containers/Expenses/withExpensesActions';
import withViewsActions from 'containers/Views/withViewsActions';
import { compose } from 'utils';
function ExpensesList({
// #withDashboardActions
changePageTitle,
// #withViewsActions
requestFetchResourceViews,
requestFetchResourceFields,
// #withExpenses
expensesTableQuery,
@@ -44,13 +47,18 @@ function ExpensesList({
const [selectedRows, setSelectedRows] = useState([]);
const [bulkDelete, setBulkDelete] = useState(false);
const fetchViews = useQuery('expenses-resource-views', () => {
return requestFetchResourceViews('expenses');
});
const fetchResourceViews = useQuery(
['resource-views', 'expenses'],
(key, resourceName) => requestFetchResourceViews(resourceName),
);
const fetchExpenses = useQuery(
['expenses-table', expensesTableQuery],
() => requestFetchExpensesTable(),
const fetchResourceFields = useQuery(
['resource-fields', 'expenses'],
(key, resourceName) => requestFetchResourceFields(resourceName),
);
const fetchExpenses = useQuery(['expenses-table', expensesTableQuery], () =>
requestFetchExpensesTable(),
);
useEffect(() => {
@@ -132,6 +140,8 @@ function ExpensesList({
// Handle fetch data of manual jouranls datatable.
const handleFetchData = useCallback(
({ pageIndex, pageSize, sortBy }) => {
const page = pageIndex + 1;
addExpensesTableQueries({
...(sortBy.length > 0
? {
@@ -139,6 +149,8 @@ function ExpensesList({
sort_order: sortBy[0].desc ? 'desc' : 'asc',
}
: {}),
page_size: pageSize,
page,
});
},
[addExpensesTableQueries],
@@ -166,7 +178,7 @@ function ExpensesList({
return (
<DashboardInsider
loading={fetchViews.isFetching}
loading={fetchResourceViews.isFetching || fetchResourceFields.isFetching}
name={'expenses'}
>
<ExpenseActionsBar
@@ -187,6 +199,7 @@ function ExpensesList({
<ExpenseViewTabs />
<ExpenseDataTable
loading={fetchExpenses.isFetching}
onDeleteExpense={handleDeleteExpense}
onFetchData={handleFetchData}
onEditExpense={handleEidtExpense}
@@ -237,4 +250,5 @@ export default compose(
withExpensesActions,
withExpenses(({ expensesTableQuery }) => ({ expensesTableQuery })),
withViewsActions,
withResourceActions
)(ExpensesList);

View File

@@ -1,8 +1,11 @@
import { connect } from 'react-redux';
import { getExpenseById } from 'store/expenses/expenses.reducer';
import { getExpenseByIdFactory } from 'store/expenses/expenses.selectors';
const mapStateToProps = (state, props) => ({
expenseDetail: getExpenseById(state, props.expenseId),
});
export default () => {
const getExpenseById = getExpenseByIdFactory();
export default connect(mapStateToProps);
const mapStateToProps = (state, props) => ({
expenseDetail: getExpenseById(state, props),
});
return connect(mapStateToProps);
};

View File

@@ -1,14 +1,26 @@
import { connect } from 'react-redux';
import { getResourceViews } from 'store/customViews/customViews.selectors';
import { getExpensesItems } from 'store/expenses/expenses.selectors';
import {
getExpensesCurrentPageFactory,
getExpenseByIdFactory,
getExpensesTableQuery,
getExpensesPaginationMetaFactory,
} from 'store/expenses/expenses.selectors';
export default (mapState) => {
const getExpensesItems = getExpensesCurrentPageFactory();
const getExpensesPaginationMeta = getExpensesPaginationMetaFactory();
const mapStateToProps = (state, props) => {
const query = getExpensesTableQuery(state, props);
const mapped = {
expenses: getExpensesItems(state, state.expenses.currentViewId),
expensesCurrentPage: getExpensesItems(state, props, query),
expensesViews: getResourceViews(state, props, 'expenses'),
expensesItems: state.expenses.items,
expensesTableQuery: state.expenses.tableQuery,
expensesTableQuery: query,
expensesPagination: getExpensesPaginationMeta(state, props),
expensesLoading: state.expenses.loading,
};
return mapState ? mapState(mapped, state, props) : mapped;

View File

@@ -20,7 +20,7 @@ export const mapDispatchToProps = (dispatch) => ({
requestPublishExpense: (id) => dispatch(publishExpense({ id })),
requestDeleteBulkExpenses: (ids) => dispatch(deleteBulkExpenses({ ids })),
changeCurrentView: (id) =>
changeExpensesView: (id) =>
dispatch({
type: t.EXPENSES_SET_CURRENT_VIEW,
currentViewId: parseInt(id, 10),

View File

@@ -29,11 +29,6 @@ function BalanceSheetActionsBar({
toggleBalanceSheetFilter,
refreshBalanceSheet
}) {
const filterDropdown = FilterDropdown({
fields: [],
onFilterChange: (filterConditions) => {},
});
const handleFilterToggleClick = () => {
toggleBalanceSheetFilter();
};
@@ -81,7 +76,7 @@ function BalanceSheetActionsBar({
</If>
<Popover
content={filterDropdown}
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>

View File

@@ -8,33 +8,27 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
function FinancialReportsItem({
title,
desc,
link
}) {
function FinancialReportsItem({ title, desc, link }) {
return (
<div class="financial-reports__item">
<Link class="title" to={link}>{ title }</Link>
<p class="desc">{ desc }</p>
<Link class="title" to={link}>
{title}
</Link>
<p class="desc">{desc}</p>
</div>
);
}
function FinancialReportsSection({
sectionTitle,
reports
}) {
function FinancialReportsSection({ sectionTitle, reports }) {
return (
<div class="financial-reports__section">
<div class="section-title">{ sectionTitle }</div>
<div class="section-title">{sectionTitle}</div>
<div class="financial-reports__list">
<For render={FinancialReportsItem} of={reports} />
</div>
</div>
)
);
}
function FinancialReports({
@@ -45,7 +39,7 @@ function FinancialReports({
useEffect(() => {
changePageTitle(formatMessage({ id: 'all_financial_reports' }));
}, [changePageTitle, formatMessage]);
}, [changePageTitle, formatMessage]);
return (
<div class="financial-reports">
@@ -54,6 +48,4 @@ function FinancialReports({
);
}
export default compose(
withDashboardActions
)(FinancialReports);
export default compose(withDashboardActions)(FinancialReports);

View File

@@ -4,8 +4,7 @@ import { momentFormatter } from 'utils';
import { DateInput } from '@blueprintjs/datetime';
import { useIntl } from 'react-intl';
import { HTMLSelect, FormGroup, Intent, Position } from '@blueprintjs/core';
import Icon from 'components/Icon';
import { FieldHint } from 'components';
import { Hint } from 'components';
import { parseDateRangeQuery } from 'utils';
export default function FinancialStatementDateRange({ formik }) {
@@ -48,14 +47,12 @@ export default function FinancialStatementDateRange({ formik }) {
[formik],
);
const infoIcon = useMemo(() => <Icon icon="info-circle" iconSize={12} />, []);
return (
<>
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'report_date_range' })}
labelInfo={infoIcon}
labelInfo={<Hint />}
minimal={true}
fill={true}
>
@@ -71,7 +68,7 @@ export default function FinancialStatementDateRange({ formik }) {
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'from_date' })}
labelInfo={infoIcon}
labelInfo={<Hint />}
fill={true}
intent={formik.errors.from_date && Intent.DANGER}
>
@@ -89,7 +86,7 @@ export default function FinancialStatementDateRange({ formik }) {
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'to_date' })}
labelInfo={<FieldHint />}
labelInfo={<Hint />}
fill={true}
intent={formik.errors.to_date && Intent.DANGER}
>

View File

@@ -31,13 +31,6 @@ function GeneralLedgerActionsBar({
toggleGeneralLedgerSheetFilter,
refreshGeneralLedgerSheet
}) {
const filterDropdown = FilterDropdown({
fields: [],
onFilterChange: (filterConditions) => {
},
});
const handleFilterClick = () => {
toggleGeneralLedgerSheetFilter();
};
@@ -86,7 +79,6 @@ function GeneralLedgerActionsBar({
</If>
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}>

View File

@@ -29,11 +29,6 @@ function JournalActionsBar({
toggleJournalSheetFilter,
refreshJournalSheet,
}) {
const filterDropdown = FilterDropdown({
fields: [],
onFilterChange: (filterConditions) => {},
});
const handleFilterToggleClick = () => {
toggleJournalSheetFilter();
};
@@ -54,11 +49,10 @@ function JournalActionsBar({
<Button
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={'Re-calc Report'}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<If condition={journalSheetFilter}>
<Button
className={Classes.MINIMAL}
@@ -78,7 +72,6 @@ function JournalActionsBar({
</If>
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>

View File

@@ -12,7 +12,6 @@ import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import DashboardActionsBar from "components/Dashboard/DashboardActionsBar";
import FilterDropdown from 'components/FilterDropdown';
import Icon from 'components/Icon';
import { If } from 'components';
@@ -30,11 +29,6 @@ function ReceivableAgingSummaryActionsBar({
toggleFilterReceivableAgingSummary,
refreshReceivableAgingSummary,
}) {
const filterDropdown = FilterDropdown({
fields: [],
onFilterChange: (filterConditions) => {},
});
const handleFilterToggleClick = () => {
toggleFilterReceivableAgingSummary();
};
@@ -62,7 +56,6 @@ function ReceivableAgingSummaryActionsBar({
icon={<Icon icon="refresh-16" iconSize={16} />}
onClick={handleRecalcReport}
/>
<If condition={receivableAgingFilter}>
<Button
className={Classes.MINIMAL}
@@ -82,7 +75,6 @@ function ReceivableAgingSummaryActionsBar({
</If>
<Popover
content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>

View File

@@ -1,9 +1,15 @@
import { connect } from 'react-redux';
import {
getItemsCategoriesListFactory
} from 'store/itemCategories/ItemsCategories.selectors';
export default (mapState) => {
const getItemsCategoriesList = getItemsCategoriesListFactory();
const mapStateToProps = (state, props) => {
const mapped = {
categoriesList: Object.values(state.itemCategories.categories),
categoriesList: getItemsCategoriesList(state, props),
categoriesTableLoading: state.itemCategories.loading,
};
return mapState ? mapState(mapped, state, props) : mapState;

View File

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

View File

@@ -1,15 +1,16 @@
import {connect} from 'react-redux';
import {
getViewItem,
getViewMeta,
getViewItemFactory,
getViewMetaFactory,
} from 'store/customViews/customViews.selectors';
export default () => {
const getViewItem = getViewItemFactory();
const getViewMeta = getViewMetaFactory();
export const mapStateToProps = (state, props) => {
return {
const mapStateToProps = (state, props) => ({
viewMeta: getViewMeta(state, props),
viewItem: getViewItem(state, props),
};
};
export default connect(mapStateToProps);
});
return connect(mapStateToProps);
};

View File

@@ -457,7 +457,7 @@ export default {
customer_type: 'Customer Type',
business: 'business',
individual: 'Individual',
display_name:'Display Name',
display_name: 'Display Name',
the_customer_has_been_successfully_created:
'The customer has been successfully created.',
select_contact: 'Select contact',
@@ -470,7 +470,8 @@ export default {
all_reports: 'All Reports',
next: 'Next',
previous: 'Previous',
showing_current_page_to_total: 'Showing {currentPage} to {totalPages} of {total} entries',
showing_current_page_to_total:
'Showing {currentPage} to {totalPages} of {total} entries',
new_child_account: 'New Child Account',
display_name: 'Display Name',
contact_name: 'Contact Name',
@@ -489,14 +490,14 @@ export default {
shipping_address: 'Shipping Address',
customers_list: 'Customers List',
edit_customer_details: 'Edit Customer Details',
receivable_balance:'Receivable balance',
receivable_balance: 'Receivable balance',
the_customer_has_been_successfully_created:
'The customer has been successfully created.',
the_customer_has_been_successfully_deleted:
'The customer has been successfully deleted.',
the_customers_has_been_successfully_deleted:
'The customers have been successfully deleted.',
the_item_customer_has_been_successfully_edited:
the_item_customer_has_been_successfully_edited:
'The item customer has been successfully edited.',
once_delete_this_customer_you_will_able_to_restore_it: `Once you delete this customer, you won\'t be able to restore it later. Are you sure you want to delete this cusomter?`,
once_delete_these_customers_you_will_not_able_restore_them:
@@ -510,4 +511,15 @@ export default {
create_a_new_view: 'Create a new view',
in: 'In',
not_equals: 'Not Equals',
select_journal_type: 'Select journal type',
journal_type: 'Journal Type',
journal_reference_hint:
'A unique reference for this journal. It is limited to 10 characters and can comprise of letters, digitals and underscore.',
contact_column_hint:
'Contact column to record receivable and payable to customer or vendor.',
make_journal_entry: 'Make Journal Entry',
recalc_report: 'Re-calc Report',
journal_number_is_already_used: 'Journal number is already used.',
account_code_hint:
'A unique code/number for this account (limited to 10 characters)',
};

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
const exchangeRateItemsSelector = state => state.exchangeRates.exchangeRates;
export const getExchangeRatesList = createSelector(
exchangeRateItemsSelector,
(exchangeRateItems) => {
return Object.values(exchangeRateItems);
},
)

View File

@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
import { pickItemsFromIds } from 'store/selectors';
import { pickItemsFromIds, getItemById } from 'store/selectors';
const accountsViewsSelector = (state) => state.accounts.views;
const accountsDataSelector = (state) => state.accounts.items;
const accountsCurrentViewSelector = (state) => state.accounts.currentViewId;
const accountIdPropSelector = (state, props) => props.accountId;
export const getAccountsItems = createSelector(
accountsViewsSelector,
@@ -17,3 +18,11 @@ export const getAccountsItems = createSelector(
: [];
},
);
export const getAccountById = createSelector(
accountsDataSelector,
accountIdPropSelector,
(accountsItems, accountId) => {
return getItemById(accountsItems, accountId);
}
);

View File

@@ -6,7 +6,10 @@ const resourceViewsIdsSelector = (state, props, resourceName) =>
state.views.resourceViews[resourceName] || [];
const viewsSelector = (state) => state.views.views;
const viewByIdSelector = (state, props) => state.views.viewsMeta[props.viewId] || {};
const viewByIdSelector = (state, props) => state.views.views[props.viewId] || {};
const viewColumnsSelector = (state, props) => {
};
export const getResourceViews = createSelector(
resourceViewsIdsSelector,
@@ -16,22 +19,21 @@ export const getResourceViews = createSelector(
},
);
export const getViewMeta = (state, viewId) => {
const view = { ...state.views.viewsMeta[viewId] } || {};
if (view.columns) {
view.columns = view.columns.map((column) => {
return {
...getResourceColumn(state, column.field_id),
};
});
export const getViewMetaFactory = () => createSelector(
viewByIdSelector,
// viewColumnsSelector,
(view, viewColumns) => {
return view;
}
return view;
};
);
export const getViewItem = (state, viewId) => {
return state.views.views[viewId] || {};
};
export const getViewItemFactory = () => createSelector(
viewByIdSelector,
// viewColumnsSelector,
(view, viewColumns) => {
return view;
}
);
export const getViewPages = (resourceViews, viewId) => {
return typeof resourceViews[viewId] === 'undefined'

View File

@@ -1,18 +1,17 @@
import { createSelector } from "@reduxjs/toolkit";
const dialogByNameSelector = (dialogName) => (state) => state.dashboard.dialogs?.[dialogName];
const dialogByNameSelector = (state, props) => state.dashboard.dialogs[props.name];
export const isDialogOpen = createSelector(
dialogByNameSelector,
export const isDialogOpenFactory = (dialogName) => createSelector(
dialogByNameSelector(dialogName),
(dialog) => {
return dialog && dialog.isOpen;
},
);
export const getDialogPayload = createSelector(
dialogByNameSelector,
export const getDialogPayloadFactory = (dialogName) => createSelector(
dialogByNameSelector(dialogName),
(dialog) => {
return dialog?.payload;
return { ...dialog?.payload };
},
);

View File

@@ -19,6 +19,7 @@ export const fetchExpensesTable = ({ query } = {}) => {
type: t.EXPENSES_PAGE_SET,
payload: {
expenses: response.data.expenses.results,
pagination: response.data.expenses.pagination,
customViewId: response.data.customViewId || -1,
},
});
@@ -28,6 +29,13 @@ export const fetchExpensesTable = ({ query } = {}) => {
expenses: response.data.expenses.results,
}
});
dispatch({
type: t.EXPENSES_PAGINATION_SET,
payload: {
pagination: response.data.expenses.pagination,
customViewId: response.data.customViewId || -1,
}
});
dispatch({
type: t.EXPENSES_TABLE_LOADING,
payload: {

View File

@@ -7,6 +7,10 @@ const initialState = {
items: {},
views: {},
loading: false,
tableQuery: {
page_size: 4,
page: 1,
},
currentViewId: -1,
};
@@ -46,13 +50,42 @@ const reducer = createReducer(initialState, {
},
[t.EXPENSES_PAGE_SET]: (state, action) => {
const { customViewId, expenses } = action.payload;
const { customViewId, expenses, pagination } = action.payload;
const viewId = customViewId || -1;
const view = state.views[viewId] || {};
state.views[viewId] = {
...view,
ids: expenses.map((i) => i.id),
pages: {
...(state.views?.[viewId]?.pages || {}),
[pagination.page]: {
ids: expenses.map((i) => i.id),
},
},
};
},
[t.EXPENSES_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,
},
};
},

View File

@@ -1,19 +1,53 @@
import { createSelector } from '@reduxjs/toolkit';
import { pickItemsFromIds } from 'store/selectors';
import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
const expensesViewsSelector = state => state.expenses.views;
const expensesItemsSelector = state => state.expenses.items;
const expensesCurrentViewSelector = state => state.expenses.currentViewId;
const expensesTableQuery = state => state.expenses.tableQuery;
export const getExpensesItems = createSelector(
expensesViewsSelector,
export const getExpensesTableQuery = createSelector(
paginationLocationQuery,
expensesTableQuery,
(locationQuery, tableQuery) => {
return {
...locationQuery,
...tableQuery,
};
},
);
const expensesPageSelector = (state, props, query) => {
const viewId = state.expenses.currentViewId;
return state.expenses.views?.[viewId]?.pages?.[query.page];
};
const expensesItemsSelector = (state) => state.expenses.items;
export const getExpensesCurrentPageFactory = () => createSelector(
expensesPageSelector,
expensesItemsSelector,
expensesCurrentViewSelector,
(expensesViews, expensesItems, currentViewId) => {
const expensesView = expensesViews[currentViewId || -1];
return (typeof expensesView === 'object')
? (pickItemsFromIds(expensesItems, expensesView.ids) || [])
(expensesPage, expensesItems) => {
return typeof expensesPage === 'object'
? pickItemsFromIds(expensesItems, expensesPage.ids) || []
: [];
},
);
const expenseByIdSelector = (state, props) => state.expenses.items[props.expenseId];
export const getExpenseByIdFactory = () => createSelector(
expenseByIdSelector,
(expense) => {
return expense;
}
);
const manualJournalsPaginationSelector = (state, props) => {
const viewId = state.expenses.currentViewId;
return state.expenses.views?.[viewId];
};
export const getExpensesPaginationMetaFactory = () => createSelector(
manualJournalsPaginationSelector,
(expensesPage) => {
return expensesPage?.paginationMeta || {};
},
);

View File

@@ -9,4 +9,5 @@ export default {
EXPENSES_TABLE_LOADING: 'EXPENSES_TABLE_LOADING',
EXPENSES_PAGE_SET: 'EXPENSES_PAGE_SET',
EXPENSES_ITEMS_SET: 'EXPENSES_ITEMS_SET',
EXPENSES_PAGINATION_SET: 'EXPENSES_PAGINATION_SET',
};

View File

@@ -0,0 +1,18 @@
import { createSelector } from 'reselect';
import { getItemById } from 'store/selectors';
const itemsCateogoriesDataSelector = (state) => state.itemCategories.categories;
const itemCategoryIdFromProps = (state, props) => props.itemCategoryId;
export const getItemsCategoriesListFactory = () =>
createSelector(itemsCateogoriesDataSelector, (itemsCategories) => {
return Object.values(itemsCategories);
});
export const getItemCategoryByIdFactory = () => createSelector(
itemsCateogoriesDataSelector,
itemCategoryIdFromProps,
(itemsCategories, itemCategoryid) => {
return getItemById(itemsCategories, itemCategoryid);
},
);

View File

@@ -1,3 +1,4 @@
import { omit } from 'lodash';
import ApiService from 'services/ApiService';
import t from 'store/types';
@@ -107,19 +108,27 @@ export const publishManualJournal = ({ id }) => {
export const fetchManualJournalsTable = ({ query } = {}) => {
return (dispatch, getState) =>
new Promise((resolve, reject) => {
let pageQuery = getState().manualJournals.tableQuery;
if (pageQuery.filter_roles) {
pageQuery = {
...omit(pageQuery, ['filter_roles']),
stringified_filter_roles: JSON.stringify(pageQuery.filter_roles) || '',
};
}
dispatch({
type: t.MANUAL_JOURNALS_TABLE_LOADING,
loading: true,
});
ApiService.get('accounting/manual-journals', {
params: { ...query },
params: { ...pageQuery, ...query },
})
.then((response) => {
dispatch({
type: t.MANUAL_JOURNALS_PAGE_SET,
payload: {
manualJournals: response.data.manualJournals.results,
customViewId: response.data.customViewId || -1,
customViewId: response.data.manualJournals?.viewMeta?.customViewId || -1,
pagination: response.data.manualJournals.pagination,
}
});
@@ -131,6 +140,7 @@ export const fetchManualJournalsTable = ({ query } = {}) => {
type: 'MANUAL_JOURNALS_PAGINATION_SET',
payload: {
pagination: response.data.manualJournals.pagination,
customViewId: response.data.manualJournals?.viewMeta?.customViewId || -1,
},
});
dispatch({

View File

@@ -30,7 +30,7 @@ const reducer = createReducer(initialState, {
[t.MANUAL_JOURNAL_PUBLISH]: (state, action) => {
const { id } = action.payload;
const item = state.items[id] || {};
const item = state.items[id] || {}
state.items[id] = { ...item, status: 1 };
},
@@ -72,7 +72,8 @@ const reducer = createReducer(initialState, {
},
[t.MANUAL_JOURNALS_SET_CURRENT_VIEW]: (state, action) => {
state.currentViewId = action.currentViewId;
const { currentViewId } = action.payload;
state.currentViewId = currentViewId;
},
[t.MANUAL_JOURNAL_REMOVE]: (state, action) => {
@@ -93,19 +94,26 @@ const reducer = createReducer(initialState, {
},
[t.MANUAL_JOURNALS_PAGINATION_SET]: (state, action) => {
const { pagination } = action.payload;
const { pagination, customViewId } = action.payload;
const mapped = {
pageSize: parseInt(pagination.pageSize, 10),
page: parseInt(pagination.page, 10),
total: parseInt(pagination.total, 10),
};
state.paginationMeta = {
...state.paginationMeta,
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,
},
};
}
});

View File

@@ -1,11 +1,44 @@
import { pickItemsFromIds } from 'store/selectors';
import { createSelector } from 'reselect';
import { pickItemsFromIds, paginationLocationQuery } from 'store/selectors';
export const getManualJournalsItems = (state, viewId, pageNumber) => {
const accountsViewPages = state.manualJournals.views[viewId || -1];
const accountsView = accountsViewPages?.pages?.[pageNumber]?.ids || {};
const accountsItems = state.manualJournals.items;
return typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView) || []
: [];
const manualJournalsPageSelector = (state, props, query) => {
const viewId = state.manualJournals.currentViewId;
return state.manualJournals.views?.[viewId]?.pages?.[query.page];
};
const manualJournalsPaginationSelector = (state, props) => {
const viewId = state.manualJournals.currentViewId;
return state.manualJournals.views?.[viewId];
};
const manualJournalsTableQuery = (state) => state.manualJournals.tableQuery;
const manualJournalsDataSelector = (state) => state.manualJournals.items;
export const getManualJournalsItems = createSelector(
manualJournalsPageSelector,
manualJournalsDataSelector,
(manualJournalsPage, manualJournalsItems) => {
return typeof manualJournalsPage === 'object'
? pickItemsFromIds(manualJournalsItems, manualJournalsPage.ids) || []
: [];
},
);
export const getManualJournalsPagination = createSelector(
manualJournalsPaginationSelector,
(manualJournalsPage) => {
return manualJournalsPage?.paginationMeta || {};
},
);
export const getManualJournalsTableQuery = createSelector(
paginationLocationQuery,
manualJournalsTableQuery,
(locationQuery, tableQuery) => {
return {
...locationQuery,
...tableQuery,
};
},
);

View File

@@ -1,4 +1,4 @@
import {pick, at} from 'lodash';
import { pick, at, mapValues } from 'lodash';
export const getItemById = (items, id) => {
return items[id] || null;
@@ -33,4 +33,18 @@ export const getAllResults = (items, pagination, name) => {
}
}
return Object.values(pick(items || [], allPagesIds))
}
export const paginationLocationQuery = (state, props) => {
const queryParams = props.location
? new URLSearchParams(props.location.search)
: null;
const queryParamsKeys = ['page_size', 'page'];
return queryParams
? mapValues(pick(Object.fromEntries(queryParams), queryParamsKeys), (v) =>
parseInt(v, 10),
)
: null;
}

View File

@@ -125,12 +125,49 @@ body.authentication {
margin-right: 6px;
}
.hint{
color: #a1b2c5;
margin-left: 6px;
position: relative;
top: -1px;
.bp3-icon{
color: #A1B2C5;
}
.bp3-popover-target:hover .bp3-icon{
color: #90a1b5;
}
.bp3-icon{
vertical-align: middle;
}
}
.bp3-select-popover .bp3-menu {
max-height: 300px;
max-width: 400px;
overflow: auto;
}
.pt-tooltip {
.pt-popover-content {
max-width: 100px;
}
}
.bp3-form-group .bp3-label{
.hint{
.bp3-popover-wrapper{
display: inline;
}
}
&:not(.bp3-inline) .hint .bp3-popover-target{
display: inline;
margin-left: 0;
}
}
.bp3-popover.bp3-tooltip{
max-width: 300px;
}

View File

@@ -23,9 +23,9 @@
.th{
padding: 0.75rem 0.5rem;
background: #F8FAFA;
background: #fafafa;
font-size: 14px;
color: #444;
color: rgb(59, 71, 91);
font-weight: 500;
border-bottom: 1px solid rgb(224, 224, 224);
}
@@ -106,16 +106,39 @@
background: #1183DA;
}
}
.bp3-control.bp3-checkbox .bp3-control-indicator{
border: 2px solid #d7d7d7;
&,
&:hover{
height: 16px;
width: 16px;
}
}
.bp3-control.bp3-checkbox{
input:checked ~ .bp3-control-indicator,
input:indeterminate ~ .bp3-control-indicator,{
border-color: #0052ff;
}
}
}
.tbody{
width: 100%;
overflow: auto;
.tbody-inner{
> .loading{
padding-top: 40px;
}
}
.tr .td{
border-bottom: 1px solid #E8E8E8;
align-items: center;
color: #252833;
color: #141720;
.placeholder{
color: #999;

View File

@@ -68,13 +68,6 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
.#{$ns}-label{
margin-bottom: 6px;
.#{$ns}-icon-info-circle{
margin-left: 3px;
position: relative;
top: -1px;
color: #A1B2C5;
}
}
}

View File

@@ -3,10 +3,19 @@
.dashboard{
display: flex;
height: 100vh;
&:before{
content: "";
height: 2px;
background: #01194e;
position: fixed;
top: 0;
width: 100%;
}
&__topbar{
width: 100%;
min-height: 65px;
min-height: 66px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #F2EFEF;
@@ -56,6 +65,10 @@
margin-right: 5px;
}
}
.bp3-navbar-divider{
margin: 0 8px;
}
}
&-user{
@@ -198,7 +211,7 @@
h1{
font-size: 26px;
font-weight: 300;
color: #393939;
color: #333;
margin: 0;
}
h3{

View File

@@ -66,7 +66,9 @@
border-left: 1px dotted rgb(195, 195, 195);
&.index {
span {
> span,
> div {
text-align: center;
width: 100%;
font-weight: 500;
}
@@ -107,7 +109,6 @@
padding-left: 8px;
padding-right: 8px;
}
.form-group--select-list {
&.bp3-intent-danger {
.bp3-button:not(.bp3-minimal) {

View File

@@ -34,6 +34,7 @@
border-left: 1px dotted rgb(195, 195, 195);
&.index{
text-align: center;
span{
width: 100%;
@@ -49,6 +50,10 @@
font-size: 14px;
font-weight: 500;
color: #1e1c3e;
&.index > div{
width: 100%;
}
}
}
@@ -60,7 +65,6 @@
&.index{
background-color: #F2F5FA;
text-align: center;
> span{
margin-top: auto;
@@ -114,7 +118,7 @@
.debit.td,
.credit.td{
> span{
padding-top: 6px;
padding-top: 2px;
}
}
.debit.td,
@@ -158,7 +162,7 @@
}
.dropzone-container{
align-self: end;
margin-left: auto;
}
.dropzone{

View File

@@ -1,8 +1,13 @@
$sidebar-background: #01194e;
$sidebar-text-color: #fff;
$sidebar-width: 220px;
$sidebar-menu-item-color: #cbd1dd;
$sidebar-menu-item-color: rgba(255, 255, 255, 0.85);
$sidebar-popover-submenu-bg: rgb(1, 20, 62);
$sidebar-menu-label-color: rgba(255, 255, 255, 0.5);
$sidebar-submenu-item-color: rgba(255, 255, 255, 0.55);
$sidebar-submenu-item-hover-color: rgba(255, 255, 255, 0.8);
$sidebar-logo-opacity: 0.55;
$sidebar-submenu-item-bg-color: #01287d;
.sidebar {
background: $sidebar-background;
@@ -41,7 +46,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
top: 2px;
svg{
opacity: 0.5;
opacity: $sidebar-logo-opacity;
}
}
}
@@ -66,7 +71,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
&:hover,
&.bp3-active {
background: #012470;
background: $sidebar-submenu-item-bg-color;
color: $sidebar-menu-item-color;
}
&:focus,
@@ -82,11 +87,11 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
> .#{$ns}-icon-caret-right {
margin-right: -4px;
margin-top: 3px;
color: #354569;
color: rgba(255, 255, 255, 0.2);
}
&-label{
display: block;
color: rgba(255, 255, 255, 0.5);
color: $sidebar-menu-label-color;
font-size: 12px;
padding: 6px 16px;
margin-top: 4px;
@@ -95,7 +100,6 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
.#{$ns}-submenu {
.#{$ns}-collapse {
border-left: 2px solid rgba(255, 255, 255, 0.15);
&-body {
background-color: rgb(11, 34, 85);
@@ -106,12 +110,16 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
.#{$ns}-menu-item {
padding: 7px 16px 7px 18px;
font-size: 15px;
color: #8a95b6;
color: $sidebar-submenu-item-color;
&:hover,
&.bp3-active {
background: transparent;
color: #c5cbe3;
color: $sidebar-submenu-item-hover-color;
}
&.bp3-active{
font-weight: 500;
}
}
}
@@ -127,7 +135,7 @@ $sidebar-popover-submenu-bg: rgb(1, 20, 62);
color: $sidebar-menu-item-color;
}
.#{$ns}-menu-divider {
border-top-color: rgba(255, 255, 255, 0.15);
border-top-color: rgba(255, 255, 255, 0.125);
color: #6b708c;
margin: 4px 0;
}