Merge remote-tracking branch 'origin/master'

This commit is contained in:
Ahmed Bouhuolia
2020-11-29 00:17:05 +02:00
39 changed files with 1410 additions and 680 deletions

View File

@@ -1,42 +1,47 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { ListSelect } from 'components'; import { MenuItem, Button } from '@blueprintjs/core';
import { MenuItem } from '@blueprintjs/core'; import { Select } from '@blueprintjs/select';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
export default function ContactSelecetList({ export default function ContactSelecetList({
contactsList, contactsList,
initialContactId,
selectedContactId, selectedContactId,
selectedContactType,
defaultSelectText = <T id={'select_contact'} />, defaultSelectText = <T id={'select_contact'} />,
onContactSelected, onContactSelected,
popoverFill = false, popoverFill = false,
...restProps disabled = false,
}) { }) {
const [selecetedContact, setSelectedContact] = useState(null); const contacts = useMemo(
() =>
// Filter Contact List contactsList.map((contact) => ({
const FilterContacts = (query, contact, index, exactMatch) => { ...contact,
const normalizedTitle = contact.display_name.toLowerCase(); _id: `${contact.id}_${contact.contact_type}`,
const normalizedQuery = query.toLowerCase(); })),
if (exactMatch) { [contactsList],
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
); );
const initialContact = useMemo(
() => contacts.find((a) => a.id === initialContactId),
[initialContactId, contacts],
);
const [selecetedContact, setSelectedContact] = useState(
initialContact || null,
);
useEffect(() => {
if (typeof selectedContactId !== 'undefined') {
const account = selectedContactId
? contacts.find((a) => a.id === selectedContactId)
: null;
setSelectedContact(account);
}
}, [selectedContactId, contacts, setSelectedContact]);
const handleContactRenderer = useCallback( const handleContactRenderer = useCallback(
(contact, { handleClick }) => ( (contact, { handleClick }) => (
<MenuItem <MenuItem
@@ -48,21 +53,48 @@ export default function ContactSelecetList({
[], [],
); );
const onContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
// Filter Contact List
const filterContacts = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >=
0
);
}
};
return ( return (
<ListSelect <Select
items={contactsList} items={contacts}
selectedItemProp={'id'} noResults={<MenuItem disabled={true} text="No results." />}
selectedItem={selectedContactId}
labelProp={'display_name'}
defaultText={defaultSelectText}
onItemSelect={onContactSelect}
itemPredicate={FilterContacts}
itemRenderer={handleContactRenderer} itemRenderer={handleContactRenderer}
itemPredicate={filterContacts}
filterable={true}
disabled={disabled}
onItemSelect={onContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }} popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, { className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
{...restProps} >
/> <Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
/>
</Select>
); );
} }

View File

@@ -1,57 +0,0 @@
import React, { useCallback, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import ListSelect from 'components/ListSelect';
import { FormattedMessage as T } from 'react-intl';
export default function ContactsListField({
contacts,
onContactSelected,
selectedContactId,
selectedContactType,
defautlSelectText = <T id={'select_contact'} />,
popoverFill = false,
}) {
// Contact item of select accounts field.
const contactRenderer = useCallback(
(item, { handleClick, modifiers, query }) => (
<MenuItem text={item.display_name} key={item.id} onClick={handleClick} />
),
[],
);
const onContactSelect = useCallback(
(contact) => {
onContactSelected && onContactSelected(contact);
},
[onContactSelected],
);
const items = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
return (
<ListSelect
items={items}
noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={contactRenderer}
filterable={true}
onItemSelect={onContactSelect}
labelProp={'display_name'}
selectedItem={`${selectedContactId}_${selectedContactType}`}
selectedItemProp={'_id'}
defaultText={defautlSelectText}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
/>
);
}

View File

@@ -1,16 +1,17 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { ListSelect } from 'components'; import { MenuItem, Button } from '@blueprintjs/core';
import { MenuItem } from '@blueprintjs/core'; import { Select } from '@blueprintjs/select';
export default function CurrencySelectList({ export default function CurrencySelectList({
currenciesList, currenciesList,
selectedCurrencyCode, selectedCurrencyCode,
defaultSelectText = <T id={'select_currency_code'} />, defaultSelectText = <T id={'select_currency_code'} />,
onCurrencySelected, onCurrencySelected,
className, popoverFill = false,
...restProps disabled = false,
}) { }) {
const [selectedCurrency, setSelectedCurrency] = useState(null); const [selectedCurrency, setSelectedCurrency] = useState(null);
@@ -45,19 +46,37 @@ export default function CurrencySelectList({
); );
}, []); }, []);
useEffect(() => {
if (typeof selectedCurrencyCode !== 'undefined') {
const currency = selectedCurrencyCode
? currenciesList.find((a) => a.currency_code === selectedCurrencyCode)
: null;
setSelectedCurrency(currency);
}
}, [selectedCurrencyCode, currenciesList, setSelectedCurrency]);
return ( return (
<ListSelect <Select
items={currenciesList} items={currenciesList}
selectedItemProp={'currency_code'}
selectedItem={selectedCurrencyCode}
labelProp={'currency_code'}
defaultText={defaultSelectText}
onItemSelect={onCurrencySelect}
itemPredicate={filterCurrencies}
itemRenderer={currencyCodeRenderer} itemRenderer={currencyCodeRenderer}
popoverProps={{ minimal: true }} itemPredicate={filterCurrencies}
className={classNames('form-group--select-list', className)} onItemSelect={onCurrencySelect}
{...restProps} filterable={true}
/> popoverProps={{
minimal: true,
usePortal: !popoverFill,
inline: popoverFill,
}}
className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
>
<Button
disabled={disabled}
text={
selectedCurrency ? selectedCurrency.currency_code : defaultSelectText
}
/>
</Select>
); );
} }

View File

@@ -1,20 +1,23 @@
import React, { useState, useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { FormGroup, Intent, Classes } from "@blueprintjs/core"; import { FormGroup, Intent, Classes } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
import ContactsListField from 'components/ContactsListField'; import { ContactSelecetList } from 'components';
export default function ContactsListCellRenderer({ export default function ContactsListCellRenderer({
column: { id }, column: { id },
row: { index, original }, row: { index, original },
cell: { value }, cell: { value },
payload: { contacts, updateData, errors } payload: { contacts, updateData, errors },
}) { }) {
const handleContactSelected = useCallback((contact) => { const handleContactSelected = useCallback(
updateData(index, { (contact) => {
contact_id: contact.id, updateData(index, {
contact_type: contact.contact_type, contact_id: contact.id,
}); contact_type: contact.contact_type,
}, [updateData, index, id]); });
},
[updateData, index, id],
);
const error = errors?.[index]?.[id]; const error = errors?.[index]?.[id];
@@ -27,8 +30,8 @@ export default function ContactsListCellRenderer({
Classes.FILL, Classes.FILL,
)} )}
> >
<ContactsListField <ContactSelecetList
contacts={contacts} contactsList={contacts}
onContactSelected={handleContactSelected} onContactSelected={handleContactSelected}
selectedContactId={original?.contact_id} selectedContactId={original?.contact_id}
selectedContactType={original?.contact_type} selectedContactType={original?.contact_type}

View File

@@ -0,0 +1,199 @@
import React from 'react';
import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { saveInvoke } from 'utils';
import { If, Icon } from 'components';
/**
* Make Journal floating actions bar.
*/
export default function MakeJournalEntriesFooter({
isSubmitting,
onSubmitClick,
onCancelClick,
manualJournalId,
onSubmitForm,
onResetForm,
manualJournalPublished,
}) {
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
};
const handleSubmitPublishAndNewBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
const handleSubmitPublishContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
};
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
};
const handleSubmitDraftAndNewBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
});
};
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
onResetForm();
};
return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save And Publish ----------- */}
<If condition={!manualJournalId || !manualJournalPublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'publish_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
<MenuItem
text={<T id={'publish_continue_editing'} />}
onClick={handleSubmitPublishContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
className={'ml1'}
type="submit"
onClick={handleSubmitDraftBtnClick}
text={<T id={'save_as_draft'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitDraftAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={manualJournalId && manualJournalPublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={manualJournalId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div>
);
}

View File

@@ -17,6 +17,11 @@ import MakeJournalEntriesField from './MakeJournalEntriesField';
import MakeJournalNumberWatcher from './MakeJournalNumberWatcher'; import MakeJournalNumberWatcher from './MakeJournalNumberWatcher';
import MakeJournalFormFooter from './MakeJournalFormFooter'; import MakeJournalFormFooter from './MakeJournalFormFooter';
import {
CreateMakeJournalFormSchema,
EditMakeJournalFormSchema,
} from './MakeJournalEntriesForm.schema';
import withJournalsActions from 'containers/Accounting/withJournalsActions'; import withJournalsActions from 'containers/Accounting/withJournalsActions';
import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail'; import withManualJournalDetail from 'containers/Accounting/withManualJournalDetail';
import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withAccountsActions from 'containers/Accounts/withAccountsActions';
@@ -83,8 +88,8 @@ function MakeJournalEntriesForm({
onFormSubmit, onFormSubmit,
onCancelForm, onCancelForm,
}) { }) {
const isNewMode = !manualJournalId;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const isNewMode = manualJournalId;
const journalNumber = isNewMode const journalNumber = isNewMode
? `${journalNumberPrefix}-${journalNextNumber}` ? `${journalNumberPrefix}-${journalNextNumber}`

View File

@@ -4,7 +4,7 @@ import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({ const Schema = Yup.object().shape({
journal_number: Yup.string() journal_number: Yup.string()
.required() // .required()
.min(1) .min(1)
.max(DATATYPES_LENGTH.STRING) .max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'journal_number_' })), .label(formatMessage({ id: 'journal_number_' })),
@@ -17,8 +17,9 @@ const Schema = Yup.object().shape({
.required() .required()
.label(formatMessage({ id: 'date' })), .label(formatMessage({ id: 'date' })),
currency_code: Yup.string().max(3), currency_code: Yup.string().max(3),
reference: Yup.string().min(1).max(DATATYPES_LENGTH.STRING), reference: Yup.string().nullable().min(1).max(DATATYPES_LENGTH.STRING),
description: Yup.string().min(1).max(DATATYPES_LENGTH.STRING), description: Yup.string().min(1).max(DATATYPES_LENGTH.STRING),
status: Yup.boolean(),
entries: Yup.array().of( entries: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
credit: Yup.number().decimalScale(13).nullable(), credit: Yup.number().decimalScale(13).nullable(),

View File

@@ -21,7 +21,7 @@ export default function ManualJournalsEmptyStatus() {
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
large={true} large={true}
onClick={() => { onClick={() => {
history.push('/invoices/new'); history.push('/make-journal-entry');
}} }}
> >
Make journal Make journal

View File

@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { FormGroup, Position, Classes, ControlGroup } from '@blueprintjs/core'; import { FormGroup, Position, Classes, ControlGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik'; import { FastField, ErrorMessage } from 'formik';
import moment from 'moment';
import { import {
MoneyInputGroup, MoneyInputGroup,
InputPrependText, InputPrependText,
@@ -27,7 +28,6 @@ function CustomerFinancialPanel({
customerId, customerId,
}) { }) {
return ( return (
<div className={'tab-panel--financial'}> <div className={'tab-panel--financial'}>
<Row> <Row>
@@ -44,6 +44,12 @@ function CustomerFinancialPanel({
> >
<DateInput <DateInput
{...momentFormatter('YYYY/MM/DD')} {...momentFormatter('YYYY/MM/DD')}
onChange={(date) => {
form.setFieldValue(
'opening_balance_at',
moment(date).format('YYYY-MM-DD'),
);
}}
value={tansformDateValue(value)} value={tansformDateValue(value)}
popoverProps={{ position: Position.BOTTOM, minimal: true }} popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={customerId} disabled={customerId}
@@ -55,7 +61,7 @@ function CustomerFinancialPanel({
{/*------------ Opening balance -----------*/} {/*------------ Opening balance -----------*/}
<FastField name={'opening_balance'}> <FastField name={'opening_balance'}>
{({ {({
form: { values }, form,
field, field,
field: { value }, field: { value },
meta: { error, touched }, meta: { error, touched },
@@ -70,12 +76,14 @@ function CustomerFinancialPanel({
inline={true} inline={true}
> >
<ControlGroup> <ControlGroup>
<InputPrependText text={values.currency_code} /> <InputPrependText text={form.values.currency_code} />
<MoneyInputGroup <MoneyInputGroup
value={value} value={value}
inputGroupProps={{ fill: true }} inputGroupProps={{ fill: true }}
disabled={customerId} disabled={customerId}
{...field} onChange={(balance) => {
form.setFieldValue('opening_balance', balance);
}}
/> />
</ControlGroup> </ControlGroup>
</FormGroup> </FormGroup>

View File

@@ -78,20 +78,20 @@ export default function CustomerFloatingActions({
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/> />
</Popover> </Popover>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={customerId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</ButtonGroup> </ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={customerId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -61,7 +61,7 @@ export default function CustomerFormAfterPrimarySection({}) {
label={<T id={'website'} />} label={<T id={'website'} />}
inline={true} inline={true}
> >
<InputGroup {...field} /> <InputGroup placeholder={'http://'} {...field} />
</FormGroup> </FormGroup>
)} )}
</FastField> </FastField>

View File

@@ -137,7 +137,9 @@ const CustomerTable = ({
{ {
id: 'receivable_balance', id: 'receivable_balance',
Header: formatMessage({ id: 'receivable_balance' }), Header: formatMessage({ id: 'receivable_balance' }),
accessor: (r) => <Money amount={r.closing_balance} currency={'USD'} />, accessor: (r) => (
<Money amount={r.closing_balance} currency={r.currency_code} />
),
className: 'receivable_balance', className: 'receivable_balance',
width: 100, width: 100,
}, },
@@ -189,7 +191,6 @@ const CustomerTable = ({
customersCurrentViewId === -1, customersCurrentViewId === -1,
customers.length === 0, customers.length === 0,
].every((condition) => condition === true); ].every((condition) => condition === true);
return ( return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}> <div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
<LoadingIndicator loading={customersLoading && !isLoadedBefore}> <LoadingIndicator loading={customersLoading && !isLoadedBefore}>

View File

@@ -62,7 +62,7 @@ export default function ItemCategoryForm({
{({ field, field: { value }, meta: { error, touched } }) => ( {({ field, field: { value }, meta: { error, touched } }) => (
<FormGroup <FormGroup
label={<T id={'category_name'} />} label={<T id={'category_name'} />}
labelInfo={FieldRequiredHint} labelInfo={<FieldRequiredHint />}
className={'form-group--category-name'} className={'form-group--category-name'}
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="name" />} helperText={<ErrorMessage name="name" />}

View File

@@ -124,7 +124,7 @@ function ExpensesDataTable({
text={formatMessage({ id: 'view_details' })} text={formatMessage({ id: 'view_details' })}
/> />
<MenuDivider /> <MenuDivider />
<If condition={!expense.published}> <If condition={!expense.is_published}>
<MenuItem <MenuItem
icon={<Icon icon={'arrow-to-top'} size={16} />} icon={<Icon icon={'arrow-to-top'} size={16} />}
text={formatMessage({ id: 'publish_expense' })} text={formatMessage({ id: 'publish_expense' })}
@@ -209,7 +209,7 @@ function ExpensesDataTable({
id: 'publish', id: 'publish',
Header: formatMessage({ id: 'publish' }), Header: formatMessage({ id: 'publish' }),
accessor: (r) => { accessor: (r) => {
return r.published ? ( return !!r.is_published ? (
<Tag minimal={true}> <Tag minimal={true}>
<T id={'published'} /> <T id={'published'} />
</Tag> </Tag>

View File

@@ -14,7 +14,7 @@ import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { saveInvoke } from 'utils'; import { saveInvoke } from 'utils';
import { Icon } from 'components'; import { Icon, If } from 'components';
/** /**
* Expense form floating actions. * Expense form floating actions.
@@ -25,11 +25,18 @@ export default function ExpenseFloatingFooter({
onCancelClick, onCancelClick,
onDraftClick, onDraftClick,
onClearClick, onClearClick,
onSubmitAndNewClick,
onSubmitForm, onSubmitForm,
onResetForm, onResetForm,
expense, expense,
expensePublished,
}) { }) {
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
};
const handleSubmitPublishAndNewBtnClick = (event) => { const handleSubmitPublishAndNewBtnClick = (event) => {
onSubmitForm(); onSubmitForm();
saveInvoke(onSubmitClick, event, { saveInvoke(onSubmitClick, event, {
@@ -47,6 +54,13 @@ export default function ExpenseFloatingFooter({
}); });
}; };
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
};
const handleSubmitDraftAndNewBtnClick = (event) => { const handleSubmitDraftAndNewBtnClick = (event) => {
onSubmitForm(); onSubmitForm();
saveInvoke(onSubmitClick, event, { saveInvoke(onSubmitClick, event, {
@@ -60,107 +74,129 @@ export default function ExpenseFloatingFooter({
onSubmitForm(); onSubmitForm();
saveInvoke(onSubmitClick, event, { saveInvoke(onSubmitClick, event, {
redirect: false, redirect: false,
publish: true, publish: false,
}); });
}; };
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
onResetForm();
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> {/* ----------- Save And Publish ----------- */}
{/* ----------- Save And Publish ----------- */} <If condition={!expense || !expensePublished}>
<Button <ButtonGroup>
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={(event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
}}
text={
expense && expense.id ? (
<T id={'edit'} />
) : (
<T id={'save_publish'} />
)
}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'publish_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
<MenuItem
text={<T id={'publish_continue_editing'} />}
onClick={handleSubmitPublishContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button <Button
disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
/> />
</Popover> <Popover
content={
<Menu>
<MenuItem
text={<T id={'publish_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
<MenuItem
text={<T id={'publish_continue_editing'} />}
onClick={handleSubmitPublishContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */} {/* ----------- Save As Draft ----------- */}
<Button <ButtonGroup>
disabled={isSubmitting} <Button
className={'ml1'} disabled={isSubmitting}
type="submit" className={'ml1'}
onClick={(event) => { type="submit"
saveInvoke(onSubmitClick, event, { onClick={handleSubmitDraftBtnClick}
redirect: true, text={<T id={'save_as_draft'} />}
publish: false, />
}); <Popover
}} content={
text={<T id={'save_as_draft'} />} <Menu>
/> <MenuItem
<Popover text={<T id={'save_and_new'} />}
content={ onClick={handleSubmitDraftAndNewBtnClick}
<Menu> />
<MenuItem <MenuItem
text={<T id={'save_and_new'} />} text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftAndNewBtnClick} onClick={handleSubmitDraftContinueEditingBtnClick}
/> />
<MenuItem </Menu>
text={<T id={'save_continue_editing'} />} }
onClick={handleSubmitDraftContinueEditingBtnClick} minimal={true}
/> interactionKind={PopoverInteractionKind.CLICK}
</Menu> position={Position.BOTTOM_LEFT}
} >
minimal={true} <Button
interactionKind={PopoverInteractionKind.CLICK} rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
position={Position.BOTTOM_LEFT} />
> </Popover>
<Button rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} /> </ButtonGroup>
</Popover> </If>
{/* ----------- Save and New ----------- */}
{/* ----------- Clear ----------- */} <If condition={expense && expensePublished}>
<Button <ButtonGroup>
className={'ml1'} <Button
disabled={isSubmitting} disabled={isSubmitting}
onClick={(event) => { intent={Intent.PRIMARY}
onResetForm(); type="submit"
saveInvoke(onClearClick, event); onClick={handleSubmitPublishBtnClick}
}} text={<T id={'save'} />}
text={expense && expense.id ? <T id={'reset'} /> : <T id={'clear'} />} />
/> <Popover
{/* ----------- Cancel ----------- */} content={
<Button <Menu>
className={'ml1'} <MenuItem
onClick={(event) => { text={<T id={'save_and_new'} />}
saveInvoke(onCancelClick, event); onClick={handleSubmitPublishAndNewBtnClick}
}} />
text={<T id={'cancel'} />} </Menu>
/> }
</ButtonGroup> minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={expense ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -22,7 +22,7 @@ const Schema = Yup.object().shape({
.max(DATATYPES_LENGTH.TEXT) .max(DATATYPES_LENGTH.TEXT)
.nullable() .nullable()
.label(formatMessage({ id: 'description' })), .label(formatMessage({ id: 'description' })),
publish: Yup.boolean().label(formatMessage({ id: 'publish' })), is_published: Yup.boolean(),
categories: Yup.array().of( categories: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
index: Yup.number().min(1).max(DATATYPES_LENGTH.INT_10).nullable(), index: Yup.number().min(1).max(DATATYPES_LENGTH.INT_10).nullable(),

View File

@@ -17,14 +17,14 @@ const Schema = Yup.object().shape({
.max(DATATYPES_LENGTH.STRING) .max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'item_type_' })), .label(formatMessage({ id: 'item_type_' })),
code: Yup.string().trim().min(0).max(DATATYPES_LENGTH.STRING), code: Yup.string().trim().min(0).max(DATATYPES_LENGTH.STRING),
cost_price: Yup.number().when(['purchasable'], { cost_price: Yup.number().min(0).when(['purchasable'], {
is: true, is: true,
then: Yup.number() then: Yup.number()
.required() .required()
.label(formatMessage({ id: 'cost_price_' })), .label(formatMessage({ id: 'cost_price_' })),
otherwise: Yup.number().nullable(true), otherwise: Yup.number().nullable(true),
}), }),
sell_price: Yup.number().when(['sellable'], { sell_price: Yup.number().min(0).when(['sellable'], {
is: true, is: true,
then: Yup.number() then: Yup.number()
.required() .required()

View File

@@ -1,62 +1,201 @@
import React from 'react'; import React from 'react';
import { Intent, Button } from '@blueprintjs/core'; import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { saveInvoke } from 'utils'; import { saveInvoke } from 'utils';
import { If, Icon } from 'components';
/**
* Bill floating actions bar.
*/
export default function BillFloatingActions({ export default function BillFloatingActions({
isSubmitting, isSubmitting,
onSubmitClick, onSubmitClick,
onSubmitAndNewClick,
onCancelClick, onCancelClick,
onClearClick, onClearClick,
billId, billId,
billPublished,
}) { }) {
const { resetForm, submitForm } = useFormikContext();
const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
};
const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
};
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
};
const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
});
};
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
resetForm();
};
return ( return (
<div className={'form__floating-footer'}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button {/* ----------- Save And Publish ----------- */}
disabled={isSubmitting} <If condition={!billId || !billPublished}>
loading={isSubmitting} <ButtonGroup>
intent={Intent.PRIMARY} <Button
type="submit" disabled={isSubmitting}
onClick={(event) => { intent={Intent.PRIMARY}
saveInvoke(onSubmitClick, event); type="submit"
}} onClick={handleSubmitPublishBtnClick}
> text={<T id={'save_publish'} />}
{billId ? <T id={'edit'} /> : <T id={'save'} />} />
</Button> <Popover
content={
<Button <Menu>
disabled={isSubmitting} <MenuItem
loading={isSubmitting} text={<T id={'publish_and_new'} />}
intent={Intent.PRIMARY} onClick={handleSubmitPublishAndNewBtnClick}
className={'ml1'} />
type="submit" <MenuItem
onClick={(event) => { text={<T id={'publish_continue_editing'} />}
saveInvoke(onSubmitAndNewClick, event); onClick={handleSubmitPublishContinueEditingBtnClick}
}} />
> </Menu>
<T id={'save_new'} /> }
</Button> minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
className={'ml1'}
type="submit"
onClick={handleSubmitDraftBtnClick}
text={<T id={'save_as_draft'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitDraftAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={billId && billPublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button <Button
className={'ml1'} className={'ml1'}
disabled={isSubmitting} disabled={isSubmitting}
onClick={(event) => { onClick={handleClearBtnClick}
saveInvoke(onClearClick, event); text={billId ? <T id={'reset'} /> : <T id={'clear'} />}
}} />
> {/* ----------- Cancel ----------- */}
<T id={'clear'} />
</Button>
<Button <Button
className={'ml1'} className={'ml1'}
type="submit" onClick={handleCancelBtnClick}
onClick={(event) => { text={<T id={'cancel'} />}
saveInvoke(onCancelClick, event); />
}}
>
<T id={'close'} />
</Button>
</div> </div>
); );
} }

View File

@@ -46,6 +46,9 @@ const defaultInitialValues = {
entries: [...repeatValue(defaultBill, MIN_LINES_NUMBER)], entries: [...repeatValue(defaultBill, MIN_LINES_NUMBER)],
}; };
/**
* Bill form.
*/
function BillForm({ function BillForm({
//#WithMedia //#WithMedia
requestSubmitMedia, requestSubmitMedia,
@@ -75,12 +78,12 @@ function BillForm({
const isNewMode = !billId; const isNewMode = !billId;
useEffect(() => { useEffect(() => {
if (bill && bill.id) { if (!isNewMode) {
changePageTitle(formatMessage({ id: 'edit_bill' })); changePageTitle(formatMessage({ id: 'edit_bill' }));
} else { } else {
changePageTitle(formatMessage({ id: 'new_bill' })); changePageTitle(formatMessage({ id: 'new_bill' }));
} }
}, [changePageTitle, bill, formatMessage]); }, [changePageTitle, isNewMode, formatMessage]);
// Initial values in create and edit mode. // Initial values in create and edit mode.
const initialValues = useMemo( const initialValues = useMemo(
@@ -146,8 +149,8 @@ function BillForm({
message: formatMessage( message: formatMessage(
{ {
id: isNewMode id: isNewMode
? 'the_bill_has_been_successfully_created' ? 'the_bill_has_been_successfully_created' :
: 'the_bill_has_been_successfully_edited', 'the_bill_has_been_successfully_edited',
}, },
{ number: values.bill_number }, { number: values.bill_number },
), ),
@@ -155,11 +158,13 @@ function BillForm({
}); });
setSubmitting(false); setSubmitting(false);
resetForm();
changePageSubtitle(''); changePageSubtitle('');
if (submitPayload.redirect) { if (submitPayload.redirect) {
history.go('/bills'); history.push('/bills');
}
if (submitPayload.resetForm) {
resetForm();
} }
}; };
// Handle the request error. // Handle the request error.
@@ -167,7 +172,7 @@ function BillForm({
handleErrors(errors, { setErrors }); handleErrors(errors, { setErrors });
setSubmitting(false); setSubmitting(false);
}; };
if (isNewMode) { if (bill && bill.id) {
requestEditBill(bill.id, form).then(onSuccess).catch(onError); requestEditBill(bill.id, form).then(onSuccess).catch(onError);
} else { } else {
requestSubmitBill(form).then(onSuccess).catch(onError); requestSubmitBill(form).then(onSuccess).catch(onError);
@@ -190,9 +195,12 @@ function BillForm({
[changePageSubtitle], [changePageSubtitle],
); );
const handleSubmitClick = useCallback(() => { const handleSubmitClick = useCallback(
setSubmitPayload({ redirect: true }); (event, payload) => {
}, [setSubmitPayload]); setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
const handleCancelClick = useCallback(() => { const handleCancelClick = useCallback(() => {
history.goBack(); history.goBack();
@@ -209,7 +217,7 @@ function BillForm({
initialValues={initialValues} initialValues={initialValues}
onSubmit={handleFormSubmit} onSubmit={handleFormSubmit}
> >
{({ isSubmitting, values }) => ( {({ isSubmitting }) => (
<Form> <Form>
<BillFormHeader onBillNumberChanged={handleBillNumberChanged} /> <BillFormHeader onBillNumberChanged={handleBillNumberChanged} />
<BillFormBody defaultBill={defaultBill} /> <BillFormBody defaultBill={defaultBill} />
@@ -220,8 +228,9 @@ function BillForm({
<BillFloatingActions <BillFloatingActions
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
billId={billId} billId={billId}
billPublished={true}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}
onCancelForm={handleCancelClick} onCancelClick={handleCancelClick}
/> />
</Form> </Form>
)} )}

View File

@@ -1,9 +1,19 @@
import React from 'react'; import React from 'react';
import { Intent, Button } from '@blueprintjs/core'; import {
Intent,
Button,
ButtonGroup,
Popover,
PopoverInteractionKind,
Position,
Menu,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { saveInvoke } from 'utils';
import { Icon } from 'components';
/** /**
* Payment made floating actions bar. * Payment made floating actions bar.
@@ -13,56 +23,85 @@ export default function PaymentMadeFloatingActions({
onSubmitClick, onSubmitClick,
onCancelClick, onCancelClick,
onClearBtnClick, onClearBtnClick,
onSubmitForm,
paymentMadeId,
}) { }) {
const handleSubmitBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
});
};
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {
onClearBtnClick && onClearBtnClick(event); onClearBtnClick && onClearBtnClick(event);
}; };
const handleSubmitClick = (event) => { const handleCancelBtnClick = (event) => {
onSubmitClick && onSubmitClick(event, { redirect: true });
};
const handleCancelClick = (event) => {
onCancelClick && onCancelClick(event); onCancelClick && onCancelClick(event);
saveInvoke(onCancelClick, event);
}; };
const handleSubmitAndNewClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
});
};
const handleSubmitContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button {/* ----------- Save and New ----------- */}
disabled={isSubmitting} <ButtonGroup>
intent={Intent.PRIMARY} <Button
type="submit" disabled={isSubmitting}
onClick={handleSubmitClick} intent={Intent.PRIMARY}
> type="submit"
<T id={'save_send'} /> onClick={handleSubmitBtnClick}
</Button> text={paymentMadeId ? <T id={'edit'} /> : <T id={'save'} />}
/>
<Button <Popover
disabled={isSubmitting} content={
intent={Intent.PRIMARY} <Menu>
className={'ml1'} <MenuItem
name={'save'} text={<T id={'save_and_new'} />}
type="submit" onClick={handleSubmitAndNewClick}
onClick={handleSubmitClick} />
> <MenuItem
<T id={'save'} /> text={<T id={'save_continue_editing'} />}
</Button> onClick={handleSubmitContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button <Button
className={'ml1'} className={'ml1'}
disabled={isSubmitting} disabled={isSubmitting}
onClick={handleClearBtnClick} onClick={handleClearBtnClick}
> text={paymentMadeId ? <T id={'reset'} /> : <T id={'clear'} />}
<T id={'clear'} /> />
</Button> {/* ----------- Cancel ----------- */}
<Button <Button
className={'ml1'} className={'ml1'}
type="submit" onClick={handleCancelBtnClick}
onClick={handleCancelClick} text={<T id={'cancel'} />}
> />
<T id={'close'} />
</Button>
</div> </div>
); );
} }

View File

@@ -81,6 +81,7 @@ function PaymentMadeForm({
const [clearLinesAlert, setClearLinesAlert] = useState(false); const [clearLinesAlert, setClearLinesAlert] = useState(false);
const [clearFormAlert, setClearFormAlert] = useState(false); const [clearFormAlert, setClearFormAlert] = useState(false);
const [fullAmount, setFullAmount] = useState(null); const [fullAmount, setFullAmount] = useState(null);
const [submitPayload, setSubmitPayload] = useState({});
const [localPaymentEntries, setLocalPaymentEntries] = useState( const [localPaymentEntries, setLocalPaymentEntries] = useState(
paymentMadeEntries, paymentMadeEntries,
@@ -155,8 +156,13 @@ function PaymentMadeForm({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
resetForm(); // resetForm();
changePageSubtitle(''); changePageSubtitle('');
if (submitPayload.redirect) {
history.push('/payment-mades');
}
}; };
const onError = (errors) => { const onError = (errors) => {
@@ -187,6 +193,7 @@ function PaymentMadeForm({
values, values,
handleSubmit, handleSubmit,
isSubmitting, isSubmitting,
submitForm,
} = useFormik({ } = useFormik({
validationSchema, validationSchema,
initialValues, initialValues,
@@ -300,6 +307,14 @@ function PaymentMadeForm({
[values.entries], [values.entries],
); );
const handleSubmitClick = useCallback(
(event, payload) => {
setSubmitPayload({ ...payload });
},
[setSubmitPayload],
);
return ( return (
<div <div
className={classNames( className={classNames(
@@ -379,8 +394,11 @@ function PaymentMadeForm({
<PaymentMadeFloatingActions <PaymentMadeFloatingActions
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
paymentMadeId={paymentMadeId}
onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick} onCancelClick={handleCancelClick}
onClearBtnClick={handleClearBtnClick} onClearBtnClick={handleClearBtnClick}
onSubmitForm={submitForm}
/> />
{/* <Dragzone {/* <Dragzone

View File

@@ -87,7 +87,7 @@ function PaymentMadeList({
}, [deletePaymentMade, requestDeletePaymentMade, formatMessage]); }, [deletePaymentMade, requestDeletePaymentMade, formatMessage]);
const handleEditPaymentMade = useCallback((payment) => { const handleEditPaymentMade = useCallback((payment) => {
history.push(`/payment-made/${payment.id}/edit`); history.push(`/payment-mades/${payment.id}/edit`);
}); });
// Calculates the selected rows count. // Calculates the selected rows count.

View File

@@ -21,7 +21,7 @@ export default function PaymentMadesEmptyStatus() {
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
large={true} large={true}
onClick={() => { onClick={() => {
history.push('/payment-made/new'); history.push('/payment-mades/new');
}} }}
> >
New bill payment New bill payment

View File

@@ -14,7 +14,7 @@ import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { saveInvoke } from 'utils'; import { saveInvoke } from 'utils';
import { Icon } from 'components'; import { If, Icon } from 'components';
/** /**
* Estimate floating actions bar. * Estimate floating actions bar.
@@ -25,12 +25,55 @@ export default function EstimateFloatingActions({
onCancelClick, onCancelClick,
onClearClick, onClearClick,
estimateId, estimateId,
estimatePublished,
}) { }) {
const { resetForm, submitForm } = useFormikContext(); const { resetForm, submitForm } = useFormikContext();
const handleSubmitBtnClick = (event) => { const handleSubmitPublishBtnClick = (event) => {
saveInvoke(onSubmitClick, event, { saveInvoke(onSubmitClick, event, {
redirect: true, redirect: true,
publish: true
});
};
const handleSubmitPublishAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
};
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
};
const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
}); });
}; };
@@ -43,56 +86,117 @@ export default function EstimateFloatingActions({
resetForm(); resetForm();
}; };
const handleSubmitAndNewClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
});
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> {/* ----------- Save And Publish ----------- */}
{/* ----------- Save and New ----------- */} <If condition={!estimateId || !estimatePublished}>
<Button <ButtonGroup>
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitBtnClick}
text={estimateId ? <T id={'edit'} /> : <T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitAndNewClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button <Button
disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
/> />
</Popover> <Popover
{/* ----------- Clear & Reset----------- */} content={
<Button <Menu>
className={'ml1'} <MenuItem
disabled={isSubmitting} text={<T id={'publish_and_new'} />}
onClick={handleClearBtnClick} onClick={handleSubmitPublishAndNewBtnClick}
text={estimateId ? <T id={'reset'} /> : <T id={'clear'} />} />
/> <MenuItem
{/* ----------- Cancel ----------- */} text={<T id={'publish_continue_editing'} />}
<Button onClick={handleSubmitPublishContinueEditingBtnClick}
className={'ml1'} />
onClick={handleCancelBtnClick} </Menu>
text={<T id={'cancel'} />} }
/> minimal={true}
</ButtonGroup> interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
className={'ml1'}
type="submit"
onClick={handleSubmitDraftBtnClick}
text={<T id={'save_as_draft'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitDraftAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={estimateId && estimatePublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={estimateId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -103,9 +103,9 @@ const EstimateForm = ({
: estimateNextNumber; : estimateNextNumber;
useEffect(() => { useEffect(() => {
const transNumber = !isNewMode ? estimate.estimate_number : estimateNumber; const transNumber = !isNewMode ? estimate.estimate_number : estimateNumber;
if (isNewMode) { if (!isNewMode) {
changePageTitle(formatMessage({ id: 'edit_estimate' })); changePageTitle(formatMessage({ id: 'edit_estimate' }));
} else { } else {
changePageTitle(formatMessage({ id: 'new_estimate' })); changePageTitle(formatMessage({ id: 'new_estimate' }));
@@ -199,11 +199,13 @@ const EstimateForm = ({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
resetForm();
if (submitPayload.redirect) { if (submitPayload.redirect) {
history.push('/estimates'); history.push('/estimates');
} }
if (submitPayload.resetForm) {
resetForm();
}
}; };
const onError = (errors) => { const onError = (errors) => {
@@ -267,8 +269,8 @@ const EstimateForm = ({
estimateId={estimateId} estimateId={estimateId}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick} onCancelClick={handleCancelClick}
estimatePublished={true}
/> />
</Form> </Form>
)} )}
</Formik> </Formik>

View File

@@ -14,7 +14,6 @@ const Schema = Yup.object().shape({
.required() .required()
.label(formatMessage({ id: 'expiration_date_' })), .label(formatMessage({ id: 'expiration_date_' })),
estimate_number: Yup.string() estimate_number: Yup.string()
.nullable()
.max(DATATYPES_LENGTH.STRING) .max(DATATYPES_LENGTH.STRING)
.label(formatMessage({ id: 'estimate_number_' })), .label(formatMessage({ id: 'estimate_number_' })),
reference: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(), reference: Yup.string().min(1).max(DATATYPES_LENGTH.STRING).nullable(),

View File

@@ -14,8 +14,7 @@ import { FormattedMessage as T } from 'react-intl';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { saveInvoke } from 'utils'; import { saveInvoke } from 'utils';
import { Icon } from 'components'; import { If, Icon } from 'components';
/** /**
* Invoice floating actions bar. * Invoice floating actions bar.
@@ -26,151 +25,178 @@ export default function InvoiceFloatingActions({
onCancelClick, onCancelClick,
onClearClick, onClearClick,
invoice, invoice,
invoicePublished,
}) { }) {
const { resetForm, submitForm } = useFormikContext(); const { resetForm, submitForm } = useFormikContext();
const handleSubmitPublishAndNewBtnClick = useCallback( const handleSubmitPublishBtnClick = (event) => {
(event) => { saveInvoke(onSubmitClick, event, {
submitForm(); redirect: true,
saveInvoke(onSubmitClick, event, { publish: true
redirect: false, });
publish: true, };
resetForm: true,
});
},
[submitForm],
);
const handleSubmitPublishContinueEditingBtnClick = useCallback( const handleSubmitPublishAndNewBtnClick = (event) => {
(event) => { submitForm();
submitForm(); saveInvoke(onSubmitClick, event, {
saveInvoke(onSubmitClick, event, { redirect: false,
redirect: false, publish: true,
publish: true, resetForm: true,
}); });
}, };
[submitForm],
);
const handleSubmitDraftAndNewBtnClick = useCallback( const handleSubmitPublishContinueEditingBtnClick = (event) => {
(event) => { submitForm();
submitForm(); saveInvoke(onSubmitClick, event, {
saveInvoke(onSubmitClick, event, { redirect: false,
redirect: false, publish: true,
publish: false, });
resetForm: true, };
});
},
[submitForm],
);
const handleSubmitDraftContinueEditingBtnClick = useCallback( const handleSubmitDraftBtnClick = (event) => {
(event) => { saveInvoke(onSubmitClick, event, {
submitForm(); redirect: true,
saveInvoke(onSubmitClick, event, { publish: false,
redirect: false, });
publish: true, };
});
}, const handleSubmitDraftAndNewBtnClick = (event) => {
[submitForm], submitForm();
); saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
});
};
const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event);
};
const handleClearBtnClick = (event) => {
// saveInvoke(onClearClick, event);
resetForm();
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> {/* ----------- Save And Publish ----------- */}
{/* ----------- Save And Publish ----------- */} <If condition={!invoice || !invoicePublished}>
<Button <ButtonGroup>
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={(event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: true,
});
}}
text={
invoice && invoice.id ? (
<T id={'edit'} />
) : (
<T id={'save_publish'} />
)
}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'publish_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
<MenuItem
text={<T id={'publish_continue_editing'} />}
onClick={handleSubmitPublishContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button <Button
disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
/> />
</Popover> <Popover
content={
<Menu>
<MenuItem
text={<T id={'publish_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
<MenuItem
text={<T id={'publish_continue_editing'} />}
onClick={handleSubmitPublishContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */} {/* ----------- Save As Draft ----------- */}
<Button <ButtonGroup>
disabled={isSubmitting} <Button
className={'ml1'} disabled={isSubmitting}
type="submit" className={'ml1'}
onClick={(event) => { type="submit"
saveInvoke(onSubmitClick, event, { onClick={handleSubmitDraftBtnClick}
redirect: true, text={<T id={'save_as_draft'} />}
publish: false, />
}); <Popover
}} content={
text={<T id={'save_as_draft'} />} <Menu>
/> <MenuItem
<Popover text={<T id={'save_and_new'} />}
content={ onClick={handleSubmitDraftAndNewBtnClick}
<Menu> />
<MenuItem <MenuItem
text={<T id={'save_and_new'} />} text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftAndNewBtnClick} onClick={handleSubmitDraftContinueEditingBtnClick}
/> />
<MenuItem </Menu>
text={<T id={'save_continue_editing'} />} }
onClick={handleSubmitDraftContinueEditingBtnClick} minimal={true}
/> interactionKind={PopoverInteractionKind.CLICK}
</Menu> position={Position.BOTTOM_LEFT}
} >
minimal={true} <Button
interactionKind={PopoverInteractionKind.CLICK} rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
position={Position.BOTTOM_LEFT} />
> </Popover>
<Button rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} /> </ButtonGroup>
</Popover> </If>
{/* ----------- Clear ----------- */} {/* ----------- Save and New ----------- */}
<Button <If condition={invoice && invoicePublished}>
className={'ml1'} <ButtonGroup>
disabled={isSubmitting} <Button
onClick={(event) => { disabled={isSubmitting}
resetForm(); intent={Intent.PRIMARY}
saveInvoke(onClearClick, event); type="submit"
}} onClick={handleSubmitPublishBtnClick}
text={invoice && invoice.id ? <T id={'reset'} /> : <T id={'clear'} />} text={<T id={'save'} />}
/> />
{/* ----------- Cancel ----------- */} <Popover
<Button content={
className={'ml1'} <Menu>
onClick={(event) => { <MenuItem
saveInvoke(onCancelClick, event); text={<T id={'save_and_new'} />}
}} onClick={handleSubmitPublishAndNewBtnClick}
text={<T id={'cancel'} />} />
/> </Menu>
</ButtonGroup> }
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={invoice ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -186,7 +186,6 @@ function InvoiceForm({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
resetForm();
if (submitPayload.redirect) { if (submitPayload.redirect) {
history.push('/invoices'); history.push('/invoices');
@@ -260,9 +259,10 @@ function InvoiceForm({
<InvoiceFormFooter /> <InvoiceFormFooter />
<InvoiceFloatingActions <InvoiceFloatingActions
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
invoice={invoice} invoice={invoiceId}
onCancelClick={handleCancelClick} onCancelClick={handleCancelClick}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}
invoicePublished={true}
/> />
</Form> </Form>
)} )}

View File

@@ -46,19 +46,28 @@ export default function PaymentReceiveFormFloatingActions({
onSubmitForm(); onSubmitForm();
saveInvoke(onSubmitClick, event, { saveInvoke(onSubmitClick, event, {
redirect: false, redirect: false,
resetForm: true,
});
};
const handleSubmitContinueEditingBtnClick = (event) => {
onSubmitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
}); });
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
{/* ----------- Save and New ----------- */}
<ButtonGroup> <ButtonGroup>
{/* ----------- Save and New ----------- */}
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
type="submit" type="submit"
onClick={handleSubmitBtnClick} onClick={handleSubmitBtnClick}
text={paymentReceiveId ? <T id={'edit'} /> : <T id={'save_and_send'} />} text={paymentReceiveId ? <T id={'edit'} /> : <T id={'save'} />}
/> />
<Popover <Popover
content={ content={
@@ -67,6 +76,10 @@ export default function PaymentReceiveFormFloatingActions({
text={<T id={'save_and_new'} />} text={<T id={'save_and_new'} />}
onClick={handleSubmitAndNewClick} onClick={handleSubmitAndNewClick}
/> />
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitContinueEditingBtnClick}
/>
</Menu> </Menu>
} }
minimal={true} minimal={true}
@@ -78,20 +91,20 @@ export default function PaymentReceiveFormFloatingActions({
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/> />
</Popover> </Popover>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={paymentReceiveId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</ButtonGroup> </ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={paymentReceiveId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -190,11 +190,13 @@ function PaymentReceiveForm({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
resetForm();
if (submitPayload.redirect) { if (submitPayload.redirect) {
history.push('/payment-receives'); history.push('/payment-receives');
} }
if (submitPayload.resetForm) {
resetForm();
}
}; };
// Handle request response errors. // Handle request response errors.
const onError = (errors) => { const onError = (errors) => {

View File

@@ -201,11 +201,13 @@ function ReceiptForm({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
resetForm();
if (submitPayload.redirect) { if (submitPayload.redirect) {
history.push('/receipts'); history.push('/receipts');
} }
if (submitPayload.resetForm) {
resetForm();
}
}; };
// Handle the request error. // Handle the request error.
@@ -268,6 +270,7 @@ function ReceiptForm({
<ReceiptFormFloatingActions <ReceiptFormFloatingActions
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
receiptId={receiptId} receiptId={receiptId}
receiptPublished={true}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}
onCancelClick={handleCancelClick} onCancelClick={handleCancelClick}
/> />

View File

@@ -14,7 +14,7 @@ import { useFormikContext } from 'formik';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { saveInvoke } from 'utils'; import { saveInvoke } from 'utils';
import { Icon } from 'components'; import { If, Icon } from 'components';
/** /**
* Receipt floating actions bar. * Receipt floating actions bar.
@@ -22,21 +22,60 @@ import { Icon } from 'components';
export default function ReceiptFormFloatingActions({ export default function ReceiptFormFloatingActions({
isSubmitting, isSubmitting,
receiptId, receiptId,
receiptPublished,
onSubmitClick, onSubmitClick,
onCancelClick, onCancelClick,
onClearClick, onClearClick,
}) { }) {
const { resetForm, submitForm } = useFormikContext(); const { resetForm, submitForm } = useFormikContext();
const handleSubmitAndNewClick = useCallback( const handleSubmitPublishBtnClick = (event) => {
(event) => { saveInvoke(onSubmitClick, event, {
submitForm(); redirect: true,
saveInvoke(onSubmitClick, event, { publish: true,
redirect: false, });
}); };
},
[submitForm], const handleSubmitPublishAndNewBtnClick = (event) => {
); submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
resetForm: true,
});
};
const handleSubmitPublishContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: true,
});
};
const handleSubmitDraftBtnClick = (event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
publish: false,
});
};
const handleSubmitDraftAndNewBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
resetForm: true,
});
};
const handleSubmitDraftContinueEditingBtnClick = (event) => {
submitForm();
saveInvoke(onSubmitClick, event, {
redirect: false,
publish: false,
});
};
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
saveInvoke(onCancelClick, event); saveInvoke(onCancelClick, event);
@@ -49,51 +88,114 @@ export default function ReceiptFormFloatingActions({
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> {/* ----------- Save And Publish ----------- */}
{/* ----------- Save and New ----------- */} <If condition={!receiptId || !receiptPublished}>
<Button <ButtonGroup>
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={(event) => {
saveInvoke(onSubmitClick, event, {
redirect: true,
});
}}
text={receiptId ? <T id={'edit'} /> : <T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitAndNewClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button <Button
disabled={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save_publish'} />}
/> />
</Popover> <Popover
{/* ----------- Clear & Reset----------- */} content={
<Button <Menu>
className={'ml1'} <MenuItem
disabled={isSubmitting} text={<T id={'publish_and_new'} />}
onClick={handleClearBtnClick} onClick={handleSubmitPublishAndNewBtnClick}
text={receiptId ? <T id={'reset'} /> : <T id={'clear'} />} />
/> <MenuItem
{/* ----------- Cancel ----------- */} text={<T id={'publish_continue_editing'} />}
<Button onClick={handleSubmitPublishContinueEditingBtnClick}
className={'ml1'} />
onClick={handleCancelBtnClick} </Menu>
text={<T id={'cancel'} />} }
/> minimal={true}
</ButtonGroup> interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
{/* ----------- Save As Draft ----------- */}
<ButtonGroup>
<Button
disabled={isSubmitting}
className={'ml1'}
type="submit"
onClick={handleSubmitDraftBtnClick}
text={<T id={'save_as_draft'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitDraftAndNewBtnClick}
/>
<MenuItem
text={<T id={'save_continue_editing'} />}
onClick={handleSubmitDraftContinueEditingBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Save and New ----------- */}
<If condition={receiptId && receiptPublished}>
<ButtonGroup>
<Button
disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit"
onClick={handleSubmitPublishBtnClick}
text={<T id={'save'} />}
/>
<Popover
content={
<Menu>
<MenuItem
text={<T id={'save_and_new'} />}
onClick={handleSubmitPublishAndNewBtnClick}
/>
</Menu>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
intent={Intent.PRIMARY}
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/>
</Popover>
</ButtonGroup>
</If>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={receiptId ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { FormGroup, ControlGroup, Position, Classes } from '@blueprintjs/core'; import { FormGroup, ControlGroup, Position, Classes } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { FastField, ErrorMessage } from 'formik'; import { FastField, ErrorMessage } from 'formik';
import moment from 'moment';
import { import {
MoneyInputGroup, MoneyInputGroup,
InputPrependText, InputPrependText,
@@ -47,6 +48,12 @@ function VendorFinanicalPanelTab({
> >
<DateInput <DateInput
{...momentFormatter('YYYY/MM/DD')} {...momentFormatter('YYYY/MM/DD')}
onChange={(date) => {
form.setFieldValue(
'opening_balance_at',
moment(date).format('YYYY-MM-DD'),
);
}}
value={tansformDateValue(value)} value={tansformDateValue(value)}
popoverProps={{ position: Position.BOTTOM, minimal: true }} popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={vendorId} disabled={vendorId}
@@ -56,12 +63,7 @@ function VendorFinanicalPanelTab({
</FastField> </FastField>
{/*------------ Opening balance -----------*/} {/*------------ Opening balance -----------*/}
<FastField name={'opening_balance'}> <FastField name={'opening_balance'}>
{({ {({ form, field, field: { value }, meta: { error, touched } }) => (
form: { values },
field,
field: { value },
meta: { error, touched },
}) => (
<FormGroup <FormGroup
label={<T id={'opening_balance'} />} label={<T id={'opening_balance'} />}
className={classNames( className={classNames(
@@ -72,14 +74,15 @@ function VendorFinanicalPanelTab({
inline={true} inline={true}
> >
<ControlGroup> <ControlGroup>
<InputPrependText text={values.currency_code } /> <InputPrependText text={form.values.currency_code} />
<MoneyInputGroup <MoneyInputGroup
value={value} value={value}
onChange={field.onChange}
prefix={'$'}
inputGroupProps={{ inputGroupProps={{
fill: true, fill: true,
...field, // ...field,
}}
onChange={(balance) => {
form.setFieldValue('opening_balance', balance);
}} }}
disabled={vendorId} disabled={vendorId}
/> />

View File

@@ -79,20 +79,20 @@ export default function VendorFloatingActions({
rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />} rightIcon={<Icon icon="arrow-drop-up-16" iconSize={20} />}
/> />
</Popover> </Popover>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={vendor ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</ButtonGroup> </ButtonGroup>
{/* ----------- Clear & Reset----------- */}
<Button
className={'ml1'}
disabled={isSubmitting}
onClick={handleClearBtnClick}
text={vendor ? <T id={'reset'} /> : <T id={'clear'} />}
/>
{/* ----------- Cancel ----------- */}
<Button
className={'ml1'}
onClick={handleCancelBtnClick}
text={<T id={'cancel'} />}
/>
</div> </div>
); );
} }

View File

@@ -6,7 +6,6 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { inputIntent } from 'utils'; import { inputIntent } from 'utils';
/** /**
* Vendor form after primary section. * Vendor form after primary section.
*/ */
@@ -66,7 +65,7 @@ function VendorFormAfterPrimarySection() {
label={<T id={'website'} />} label={<T id={'website'} />}
inline={true} inline={true}
> >
<InputGroup {...field} /> <InputGroup placeholder={'http://'} {...field} />
</FormGroup> </FormGroup>
)} )}
</FastField> </FastField>

View File

@@ -32,6 +32,7 @@ function VendorsTable({
vendorsPageination, vendorsPageination,
vendorTableQuery, vendorTableQuery,
vendorItems, vendorItems,
vendorsCurrentViewId,
// #withVendorsActions // #withVendorsActions
addVendorsTableQueries, addVendorsTableQueries,
@@ -135,7 +136,9 @@ function VendorsTable({
{ {
id: 'receivable_balance', id: 'receivable_balance',
Header: formatMessage({ id: 'receivable_balance' }), Header: formatMessage({ id: 'receivable_balance' }),
accessor: (r) => <Money amount={r.closing_balance} currency={'USD'} />, accessor: (r) => (
<Money amount={r.closing_balance} currency={r.currency_code} />
),
className: 'receivable_balance', className: 'receivable_balance',
width: 100, width: 100,
}, },
@@ -182,6 +185,10 @@ function VendorsTable({
onEditVendor, onEditVendor,
onDeleteVendor, onDeleteVendor,
}); });
const showEmptyStatus = [
vendorsCurrentViewId === -1,
vendorItems.length === 0,
].every((condition) => condition === true);
return ( return (
<div className={classNames(CLASSES.DASHBOARD_DATATABLE)}> <div className={classNames(CLASSES.DASHBOARD_DATATABLE)}>
@@ -190,7 +197,7 @@ function VendorsTable({
mount={false} mount={false}
> >
<Choose> <Choose>
<Choose.When condition={true}> <Choose.When condition={showEmptyStatus}>
<VendorsEmptyStatus /> <VendorsEmptyStatus />
</Choose.When> </Choose.When>
@@ -228,11 +235,13 @@ export default compose(
vendorsLoading, vendorsLoading,
vendorTableQuery, vendorTableQuery,
vendorsPageination, vendorsPageination,
vendorsCurrentViewId,
}) => ({ }) => ({
vendorItems, vendorItems,
vendorsLoading, vendorsLoading,
vendorsPageination, vendorsPageination,
vendorTableQuery, vendorTableQuery,
vendorsCurrentViewId,
}), }),
), ),
withVendorsActions, withVendorsActions,

View File

@@ -5,11 +5,13 @@ import {
getVendorCurrentPageFactory, getVendorCurrentPageFactory,
getVendorsTableQuery, getVendorsTableQuery,
getVendorsPaginationMetaFactory, getVendorsPaginationMetaFactory,
getVendorsCurrentViewIdFactory,
} from 'store/vendors/vendors.selectors'; } from 'store/vendors/vendors.selectors';
export default (mapState) => { export default (mapState) => {
const getVendorsItems = getVendorCurrentPageFactory(); const getVendorsItems = getVendorCurrentPageFactory();
const getVendorsPaginationMeta = getVendorsPaginationMetaFactory(); const getVendorsPaginationMeta = getVendorsPaginationMetaFactory();
const getVendorsCurrentViewId = getVendorsCurrentViewIdFactory();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const query = getVendorsTableQuery(state, props); const query = getVendorsTableQuery(state, props);
@@ -20,6 +22,7 @@ export default (mapState) => {
vendorTableQuery: query, vendorTableQuery: query,
vendorsPageination: getVendorsPaginationMeta(state, props, query), vendorsPageination: getVendorsPaginationMeta(state, props, query),
vendorsLoading: state.vendors.loading, vendorsLoading: state.vendors.loading,
vendorsCurrentViewId: getVendorsCurrentViewId(state, props),
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -10,6 +10,8 @@ const vendorByIdSelector = (state, props) =>
state.vendors.items[props.vendorId]; state.vendors.items[props.vendorId];
const vendorsItemsSelector = (state) => state.vendors.items; const vendorsItemsSelector = (state) => state.vendors.items;
const vendorsCurrentViewIdSelector = (state) => state.vendors.currentViewId;
const vendorsPaginationSelector = (state, props) => { const vendorsPaginationSelector = (state, props) => {
const viewId = state.vendors.currentViewId; const viewId = state.vendors.currentViewId;
return state.vendors.views?.[viewId]; return state.vendors.views?.[viewId];
@@ -57,3 +59,8 @@ export const getVendorByIdFactory = () =>
createSelector(vendorByIdSelector, (vendor) => { createSelector(vendorByIdSelector, (vendor) => {
return vendor; return vendor;
}); });
export const getVendorsCurrentViewIdFactory = () =>
createSelector(vendorsCurrentViewIdSelector, (currentViewId) => {
return currentViewId;
});

View File

@@ -1,28 +1,28 @@
import { Model } from 'objection'; import { Model } from "objection";
import TenantModel from 'models/TenantModel'; import TenantModel from "models/TenantModel";
import { viewRolesBuilder } from 'lib/ViewRolesBuilder'; import { viewRolesBuilder } from "lib/ViewRolesBuilder";
import Media from './Media'; import Media from "./Media";
export default class Expense extends TenantModel { export default class Expense extends TenantModel {
/** /**
* Table name * Table name
*/ */
static get tableName() { static get tableName() {
return 'expenses_transactions'; return "expenses_transactions";
} }
/** /**
* Account transaction reference type. * Account transaction reference type.
*/ */
static get referenceType() { static get referenceType() {
return 'Expense'; return "Expense";
} }
/** /**
* Model timestamps. * Model timestamps.
*/ */
get timestamps() { get timestamps() {
return ['createdAt', 'updatedAt']; return ["createdAt", "updatedAt"];
} }
/** /**
@@ -35,10 +35,17 @@ export default class Expense extends TenantModel {
/** /**
* *
*/ */
static get media () { static get media() {
return true; return true;
} }
static get virtualAttributes() {
return ["isPublished"];
}
isPublished() {
return Boolean(this.publishedAt);
}
/** /**
* Model modifiers. * Model modifiers.
*/ */
@@ -46,28 +53,28 @@ export default class Expense extends TenantModel {
return { return {
filterByDateRange(query, startDate, endDate) { filterByDateRange(query, startDate, endDate) {
if (startDate) { if (startDate) {
query.where('date', '>=', startDate); query.where("date", ">=", startDate);
} }
if (endDate) { if (endDate) {
query.where('date', '<=', endDate); query.where("date", "<=", endDate);
} }
}, },
filterByAmountRange(query, from, to) { filterByAmountRange(query, from, to) {
if (from) { if (from) {
query.where('amount', '>=', from); query.where("amount", ">=", from);
} }
if (to) { if (to) {
query.where('amount', '<=', to); query.where("amount", "<=", to);
} }
}, },
filterByExpenseAccount(query, accountId) { filterByExpenseAccount(query, accountId) {
if (accountId) { if (accountId) {
query.where('expense_account_id', accountId); query.where("expense_account_id", accountId);
} }
}, },
filterByPaymentAccount(query, accountId) { filterByPaymentAccount(query, accountId) {
if (accountId) { if (accountId) {
query.where('payment_account_id', accountId); query.where("payment_account_id", accountId);
} }
}, },
viewRolesBuilder(query, conditionals, expression) { viewRolesBuilder(query, conditionals, expression) {
@@ -80,41 +87,41 @@ export default class Expense extends TenantModel {
* Relationship mapping. * Relationship mapping.
*/ */
static get relationMappings() { static get relationMappings() {
const Account = require('models/Account'); const Account = require("models/Account");
const ExpenseCategory = require('models/ExpenseCategory'); const ExpenseCategory = require("models/ExpenseCategory");
const Media = require('models/Media'); const Media = require("models/Media");
return { return {
paymentAccount: { paymentAccount: {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Account.default, modelClass: Account.default,
join: { join: {
from: 'expenses_transactions.paymentAccountId', from: "expenses_transactions.paymentAccountId",
to: 'accounts.id', to: "accounts.id",
}, },
}, },
categories: { categories: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: ExpenseCategory.default, modelClass: ExpenseCategory.default,
join: { join: {
from: 'expenses_transactions.id', from: "expenses_transactions.id",
to: 'expense_transaction_categories.expenseId', to: "expense_transaction_categories.expenseId",
}, },
}, },
media: { media: {
relation: Model.ManyToManyRelation, relation: Model.ManyToManyRelation,
modelClass: Media.default, modelClass: Media.default,
join: { join: {
from: 'expenses_transactions.id', from: "expenses_transactions.id",
through: { through: {
from: 'media_links.model_id', from: "media_links.model_id",
to: 'media_links.media_id', to: "media_links.media_id",
}, },
to: 'media.id', to: "media.id",
}, },
filter(query) { filter(query) {
query.where('model_name', 'Expense'); query.where("model_name", "Expense");
} },
}, },
}; };
} }
@@ -125,51 +132,50 @@ export default class Expense extends TenantModel {
static get fields() { static get fields() {
return { return {
payment_date: { payment_date: {
label: 'Payment date', label: "Payment date",
column: 'payment_date', column: "payment_date",
columnType: 'date', columnType: "date",
}, },
payment_account: { payment_account: {
label: 'Payment account', label: "Payment account",
column: 'payment_account_id', column: "payment_account_id",
relation: 'accounts.id', relation: "accounts.id",
optionsResource: 'account', optionsResource: "account",
}, },
amount: { amount: {
label: 'Amount', label: "Amount",
column: 'total_amount', column: "total_amount",
columnType: 'number' columnType: "number",
}, },
currency_code: { currency_code: {
label: 'Currency', label: "Currency",
column: 'currency_code', column: "currency_code",
optionsResource: 'currency', optionsResource: "currency",
}, },
reference_no: { reference_no: {
label: 'Reference No.', label: "Reference No.",
column: 'reference_no', column: "reference_no",
columnType: 'string', columnType: "string",
}, },
description: { description: {
label: 'Description', label: "Description",
column: 'description', column: "description",
columnType: 'string', columnType: "string",
}, },
published: { published: {
label: 'Published', label: "Published",
column: 'published', column: "published",
}, },
user: { user: {
label: 'User', label: "User",
column: 'user_id', column: "user_id",
relation: 'users.id', relation: "users.id",
relationColumn: 'users.id', relationColumn: "users.id",
}, },
created_at: { created_at: {
label: 'Created at', label: "Created at",
column: 'created_at', column: "created_at",
columnType: 'date', columnType: "date",
}, },
}; };
} }