mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
- 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:
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { FieldRequiredHint } from "components"
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export default function FieldRequiredHint() {
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
})),
|
||||
],
|
||||
}}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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)',
|
||||
};
|
||||
|
||||
10
client/src/store/ExchangeRate/exchange.selector.js
Normal file
10
client/src/store/ExchangeRate/exchange.selector.js
Normal 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);
|
||||
},
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
@@ -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'
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
);
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -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 || {};
|
||||
},
|
||||
);
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
18
client/src/store/itemCategories/ItemsCategories.selectors.js
Normal file
18
client/src/store/itemCategories/ItemsCategories.selectors.js
Normal 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);
|
||||
},
|
||||
);
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user