Compare commits

...

52 Commits

Author SHA1 Message Date
a.bouhuolia
11d7029568 Merge branch 'develop' 2021-11-11 17:56:23 +02:00
a.bouhuolia
1990ce7562 fix: personal phone number placeholder. 2021-11-11 17:50:58 +02:00
a.bouhuolia
b6f0f6c2d5 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 17:20:57 +02:00
a.bouhuolia
4c58e49169 fix: SMS notification types. 2021-11-11 17:20:53 +02:00
elforjani13
376a16fd65 fix: force-width 2021-11-11 16:15:45 +02:00
elforjani13
918cd4aef3 feat: add display name defaultText 2021-11-11 15:39:42 +02:00
elforjani13
ec844637c3 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 15:23:52 +02:00
elforjani13
5803760c61 fix: rowClassNames. 2021-11-11 15:23:23 +02:00
a.bouhuolia
2e34df5d63 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 12:47:14 +02:00
a.bouhuolia
35d755e417 fix: SMS notification messages context menu. 2021-11-11 12:46:58 +02:00
elforjani13
66641ca56e fix: rename in eng file. 2021-11-11 12:13:14 +02:00
a.bouhuolia
307aaf0aa4 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2021-11-11 11:27:06 +02:00
a.bouhuolia
eb5a82d413 feat: optimize SMS notifications RTL. 2021-11-11 11:26:49 +02:00
elforjani13
ce9169b24d fix: add calloutCodes. 2021-11-11 11:19:58 +02:00
a.bouhuolia
22069f4795 feat: optimize Arabic localization of SMS notifications module. 2021-11-11 10:21:41 +02:00
a.bouhuolia
567b4da7e9 fix: merge conflict quick create list field. 2021-11-11 00:05:57 +02:00
a.bouhuolia
06345a5615 Merge branch 'feature/notify-via-SMS' into develop 2021-11-10 23:58:34 +02:00
a.bouhuolia
6b8178f643 feat: Reset to defailt SMS message. 2021-11-10 23:53:39 +02:00
a.bouhuolia
449ff724e1 Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-10 22:02:46 +02:00
a.bouhuolia
95e75f0e8f fix: notify invoice notification key query. 2021-11-10 22:02:00 +02:00
elforjani13
1a63ac69d8 fix: rename sms messages. 2021-11-10 21:31:28 +02:00
a.bouhuolia
da67217d74 feat: quick create action on select/suggest items fields. 2021-11-10 20:49:50 +02:00
elforjani13
e0c03141f0 fix: localization arabic. 2021-11-10 16:32:19 +02:00
elforjani13
4d563e3ddd feat: add localization arabic. 2021-11-10 15:17:17 +02:00
a.bouhuolia
56fdf245d3 Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-09 18:19:24 +02:00
a.bouhuolia
5a8c61396f feat: SMS message preview with variables. 2021-11-09 18:16:22 +02:00
elforjani13
5fcf32dcaa feat add localization again. 2021-11-09 16:39:13 +02:00
elforjani13
acf457c0a0 Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-09 16:24:31 +02:00
elforjani13
e205c0b9a3 feat add localization. 2021-11-09 16:20:18 +02:00
a.bouhuolia
85f1c5584b feat: SMS notification handle response errors. 2021-11-09 13:56:59 +02:00
a.bouhuolia
9e5fddf294 feat: SMS notification handle errors. 2021-11-09 13:49:16 +02:00
a.bouhuolia
3039e43767 feat: SMS message text preview words break. 2021-11-09 12:41:31 +02:00
a.bouhuolia
7371557482 feat: optimize style of SMS notifications module. 2021-11-09 12:34:55 +02:00
a.bouhuolia
4b5e06f50c feat: SMS notification module. 2021-11-09 11:08:47 +02:00
a.bouhuolia
8daefb6946 fix: add notification id to sms messages templates table. 2021-11-09 09:57:12 +02:00
a.bouhuolia
6bf605f9ea Merge branch 'feature/notify-via-SMS' of https://github.com/bigcapitalhq/client into feature/notify-via-SMS 2021-11-09 09:56:53 +02:00
a.bouhuolia
48221a7af1 feat: Optimize SMS notification module. 2021-11-09 09:51:38 +02:00
elforjani13
7a1c9caa70 feat: add context menu in sms message table. 2021-11-08 16:41:36 +02:00
elforjani13
8c2d138976 fix: disable sort in SMS integration table. 2021-11-08 16:10:54 +02:00
elforjani13
5b09d8279e feat: handle error sms messgae dialog. 2021-11-08 15:13:41 +02:00
elforjani13
92d8096f3a feat: add Invalidate queries. 2021-11-08 15:00:44 +02:00
elforjani13
adc6b336e0 fix: handle error. 2021-11-08 14:54:11 +02:00
elforjani13
6d67d6163d feat: handle error. 2021-11-08 13:20:49 +02:00
elforjani13
4d89f1e0e0 feat: add notify by sms . 2021-11-07 20:11:15 +02:00
elforjani13
7706d2992c feat: add notify via SMS Form. 2021-11-07 16:40:02 +02:00
elforjani13
6dcb98a438 feat: add preferneces menu. 2021-11-07 13:44:20 +02:00
elforjani13
834d365a97 feat: Add SMS Integration & SMS Message Form. 2021-11-07 13:39:29 +02:00
elforjani13
d26ef01afc feat: notify by SMS. 2021-11-06 21:47:17 +02:00
elforjani13
2bd4c5f724 fix: SMS message templates. 2021-11-06 00:08:25 +02:00
elforjani13
2c71d07512 feat: add localization. 2021-11-04 17:05:38 +02:00
elforjani13
17a4744e58 feat: add SMS message template. 2021-11-04 16:55:37 +02:00
elforjani13
46f6380fe6 feat: add notify via SMS. 2021-11-04 15:46:14 +02:00
148 changed files with 4562 additions and 913 deletions

View File

@@ -66,6 +66,7 @@ const CLASSES = {
PREFERENCES_PAGE_INSIDE_CONTENT_USERS: 'preferences-page__inside-content--users', PREFERENCES_PAGE_INSIDE_CONTENT_USERS: 'preferences-page__inside-content--users',
PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies', PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies',
PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant', PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT: 'preferences-page__inside-content--accountant',
PREFERENCES_PAGE_INSIDE_CONTENT_SMS_INTEGRATION: 'preferences-page__inside-content--sms-integration',
FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report', FINANCIAL_REPORT_INSIDER: 'dashboard__insider--financial-report',

View File

@@ -10,4 +10,8 @@ export const DRAWERS = {
BILL_DRAWER: 'bill-drawer', BILL_DRAWER: 'bill-drawer',
INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer', INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-drawer',
CASHFLOW_TRNASACTION_DRAWER: 'cashflow-transaction-drawer', CASHFLOW_TRNASACTION_DRAWER: 'cashflow-transaction-drawer',
QUICK_WRITE_VENDOR: 'quick-write-vendor',
QUICK_CREATE_CUSTOMER: 'quick-create-customer',
QUICK_CREATE_ITEM: 'quick-create-item',
}; };

View File

@@ -1,13 +1,55 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { MenuItem, Button } from '@blueprintjs/core'; import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select'; import { Select } from '@blueprintjs/select';
import { MenuItemNestedText, FormattedMessage as T } from 'components'; import * as R from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils'; import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils'; import { nestedArrayToflatten } from 'utils';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
export default function AccountsSelectList({ import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Filters accounts items.
const filterAccountsPredicater = (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;
}
};
/**
* Accounts select list.
*/
function AccountsSelectList({
// #withDialogActions
openDialog,
// #ownProps
accounts, accounts,
initialAccountId, initialAccountId,
selectedAccountId, selectedAccountId,
@@ -21,6 +63,8 @@ export default function AccountsSelectList({
filterByNormal, filterByNormal,
filterByRootTypes, filterByRootTypes,
allowCreate,
buttonProps = {}, buttonProps = {},
}) { }) {
const flattenAccounts = useMemo( const flattenAccounts = useMemo(
@@ -51,6 +95,7 @@ export default function AccountsSelectList({
[initialAccountId, filteredAccounts], [initialAccountId, filteredAccounts],
); );
// Select account item.
const [selectedAccount, setSelectedAccount] = useState( const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null, initialAccount || null,
); );
@@ -76,31 +121,25 @@ export default function AccountsSelectList({
); );
}, []); }, []);
const onAccountSelect = useCallback( // Handle the account item select.
const handleAccountSelect = useCallback(
(account) => { (account) => {
setSelectedAccount({ ...account }); if (account.id) {
onAccountSelected && onAccountSelected(account); setSelectedAccount({ ...account });
}, 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 { } else {
return ( openDialog('account-form');
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
} }
}, },
[], [setSelectedAccount, onAccountSelected, openDialog],
); );
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return ( return (
<Select <Select
items={filteredAccounts} items={filteredAccounts}
@@ -113,11 +152,13 @@ export default function AccountsSelectList({
inline: popoverFill, inline: popoverFill,
}} }}
filterable={true} filterable={true}
onItemSelect={onAccountSelect} onItemSelect={handleAccountSelect}
disabled={disabled} disabled={disabled}
className={classNames('form-group--select-list', { className={classNames('form-group--select-list', {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
> >
<Button <Button
disabled={disabled} disabled={disabled}
@@ -127,3 +168,5 @@ export default function AccountsSelectList({
</Select> </Select>
); );
} }
export default R.compose(withDialogActions)(AccountsSelectList);

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
import { Suggest } from '@blueprintjs/select'; import { Suggest } from '@blueprintjs/select';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import * as R from 'ramda';
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
@@ -10,10 +11,55 @@ import { MenuItemNestedText, FormattedMessage as T } from 'components';
import { filterAccountsByQuery } from './utils'; import { filterAccountsByQuery } from './utils';
import { nestedArrayToflatten } from 'utils'; import { nestedArrayToflatten } from 'utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
// Create new account renderer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
onClick={handleClick}
/>
);
};
// Create new item from the given query string.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
// Filters accounts items.
const filterAccountsPredicater = (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;
}
};
/** /**
* Accounts suggest field. * Accounts suggest field.
*/ */
export default function AccountsSuggestField({ function AccountsSuggestField({
// #withDialogActions
openDialog,
// #ownProps
accounts, accounts,
initialAccountId, initialAccountId,
selectedAccountId, selectedAccountId,
@@ -26,6 +72,8 @@ export default function AccountsSuggestField({
filterByNormal, filterByNormal,
filterByRootTypes = [], filterByRootTypes = [],
allowCreate,
...suggestProps ...suggestProps
}) { }) {
const flattenAccounts = useMemo( const flattenAccounts = useMemo(
@@ -69,23 +117,6 @@ export default function AccountsSuggestField({
} }
}, [selectedAccountId, filteredAccounts, setSelectedAccount]); }, [selectedAccountId, filteredAccounts, setSelectedAccount]);
// 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
);
}
},
[],
);
// Account item of select accounts field. // Account item of select accounts field.
const accountItem = useCallback((item, { handleClick, modifiers, query }) => { const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
return ( return (
@@ -98,28 +129,31 @@ export default function AccountsSuggestField({
); );
}, []); }, []);
const handleInputValueRenderer = (inputValue) => { const handleAccountSelect = useCallback(
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
const onAccountSelect = useCallback(
(account) => { (account) => {
setSelectedAccount({ ...account }); if (account.id) {
onAccountSelected && onAccountSelected(account); setSelectedAccount({ ...account });
onAccountSelected && onAccountSelected(account);
} else {
openDialog('account-form');
}
}, },
[setSelectedAccount, onAccountSelected], [setSelectedAccount, onAccountSelected, openDialog],
); );
// Maybe inject new item props to select component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return ( return (
<Suggest <Suggest
items={filteredAccounts} items={filteredAccounts}
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />} noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
itemRenderer={accountItem} itemRenderer={accountItem}
itemPredicate={filterAccountsPredicater} itemPredicate={filterAccountsPredicater}
onItemSelect={onAccountSelect} onItemSelect={handleAccountSelect}
selectedItem={selectedAccount} selectedItem={selectedAccount}
inputProps={{ placeholder: defaultSelectText }} inputProps={{ placeholder: defaultSelectText }}
resetOnClose={true} resetOnClose={true}
@@ -129,7 +163,11 @@ export default function AccountsSuggestField({
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,
})} })}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
{...suggestProps} {...suggestProps}
/> />
); );
} }
export default R.compose(withDialogActions)(AccountsSuggestField);

View File

@@ -1,23 +1,13 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Button } from '@blueprintjs/core';
export const ButtonLink = styled(Button)` export const ButtonLink = styled.button`
line-height: inherit; color: #0052cc;
border: 0;
background: transparent;
cursor: pointer;
&.bp3-small { &:hover,
min-height: auto; &:active {
min-width: auto; text-decoration: underline;
padding: 0;
}
&:not([class*='bp3-intent-']) {
&,
&:hover {
color: #0052cc;
background: transparent;
}
&:hover {
text-decoration: underline;
}
} }
`; `;

View File

@@ -11,12 +11,14 @@ export default function ContactSelecetList({
contactsList, contactsList,
initialContactId, initialContactId,
selectedContactId, selectedContactId,
selectedContactType, createNewItemFrom,
defaultSelectText = <T id={'select_contact'} />, defaultSelectText = <T id={'select_contact'} />,
onContactSelected, onContactSelected,
popoverFill = false, popoverFill = false,
disabled = false, disabled = false,
buttonProps, buttonProps,
...restProps
}) { }) {
const contacts = useMemo( const contacts = useMemo(
() => () =>
@@ -65,7 +67,7 @@ export default function ContactSelecetList({
); );
// Filter Contact List // Filter Contact List
const filterContacts = (query, contact, index, exactMatch) => { const itemPredicate = (query, contact, index, exactMatch) => {
const normalizedTitle = contact.display_name.toLowerCase(); const normalizedTitle = contact.display_name.toLowerCase();
const normalizedQuery = query.toLowerCase(); const normalizedQuery = query.toLowerCase();
if (exactMatch) { if (exactMatch) {
@@ -83,7 +85,7 @@ export default function ContactSelecetList({
items={contacts} items={contacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />} noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer} itemRenderer={handleContactRenderer}
itemPredicate={filterContacts} itemPredicate={itemPredicate}
filterable={true} filterable={true}
disabled={disabled} disabled={disabled}
onItemSelect={onContactSelect} onItemSelect={onContactSelect}
@@ -92,8 +94,9 @@ export default function ContactSelecetList({
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill, [CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})} })}
inputProps={{ inputProps={{
placeholder: intl.get('filter_') placeholder: intl.get('filter_'),
}} }}
{...restProps}
> >
<Button <Button
disabled={disabled} disabled={disabled}

View File

@@ -0,0 +1,86 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { itemPredicate, handleContactRenderer } from './utils';
export default function ContactSelectField({
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
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 handleContactSelect = useCallback(
(contact) => {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
},
[setSelectedContact, onContactSelected],
);
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}

View File

@@ -0,0 +1,116 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemRenderer,
createNewItemFromQuery,
} from './utils';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
function CustomerSelectField({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
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 handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_CREATE_CUSTOMER);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(CustomerSelectField);

View File

@@ -0,0 +1,115 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import * as R from 'ramda';
import { MenuItem, Button } from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
itemPredicate,
handleContactRenderer,
createNewItemFromQuery,
createNewItemRenderer,
} from './utils';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
function VendorSelectField({
// #withDrawerActions
openDrawer,
// #ownProps
contacts,
initialContactId,
selectedContactId,
defaultSelectText = <T id={'select_contact'} />,
onContactSelected,
popoverFill = false,
disabled = false,
allowCreate,
buttonProps,
...restProps
}) {
const localContacts = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
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 handleContactSelect = useCallback(
(contact) => {
if (contact.id) {
setSelectedContact({ ...contact });
onContactSelected && onContactSelected(contact);
} else {
openDrawer(DRAWERS.QUICK_WRITE_VENDOR);
}
},
[setSelectedContact, onContactSelected, openDrawer],
);
// Maybe inject create new item props to suggest component.
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
const maybeCreateNewItemFromQuery = allowCreate
? createNewItemFromQuery
: null;
return (
<Select
items={localContacts}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
itemRenderer={handleContactRenderer}
itemPredicate={itemPredicate}
filterable={true}
disabled={disabled}
onItemSelect={handleContactSelect}
popoverProps={{ minimal: true, usePortal: !popoverFill }}
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
})}
inputProps={{
placeholder: intl.get('filter_'),
}}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...restProps}
>
<Button
disabled={disabled}
text={
selecetedContact ? selecetedContact.display_name : defaultSelectText
}
{...buttonProps}
/>
</Select>
);
}
export default R.compose(withDrawerActions)(VendorSelectField);

View File

@@ -0,0 +1,5 @@
import ContactSelectField from './ContactSelectField';
import CustomerSelectField from './CustomerSelectField';
import VendorSelectField from './VendorSelectField';
export { ContactSelectField, CustomerSelectField, VendorSelectField };

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { MenuItem } from '@blueprintjs/core';
// Filter Contact List
export const itemPredicate = (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
);
}
};
export const handleContactRenderer = (contact, { handleClick }) => (
<MenuItem
key={contact.id}
text={contact.display_name}
onClick={handleClick}
/>
);
// Creates a new item from query.
export const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle quick create new customer.
export const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar'; import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute'; import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary'; import DashboardErrorBoundary from './DashboardErrorBoundary';
export default React.forwardRef(({}, ref) => { export default React.forwardRef(({}, ref) => {
@@ -11,7 +10,6 @@ export default React.forwardRef(({}, ref) => {
<div className="dashboard-content" id="dashboard" ref={ref}> <div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar /> <DashboardTopbar />
<DashboardContentRoutes /> <DashboardContentRoutes />
<DashboardFooter />
</div> </div>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -17,6 +17,8 @@ export default function AccountCellRenderer({
accountsDataProp, accountsDataProp,
filterAccountsByRootTypes, filterAccountsByRootTypes,
filterAccountsByTypes, filterAccountsByTypes,
fieldProps,
formGroupProps,
}, },
row: { index, original }, row: { index, original },
cell: { value: initialValue }, cell: { value: initialValue },
@@ -53,6 +55,7 @@ export default function AccountCellRenderer({
'form-group--account', 'form-group--account',
Classes.FILL, Classes.FILL,
)} )}
{...formGroupProps}
> >
<AccountsSuggestField <AccountsSuggestField
accounts={accounts} accounts={accounts}
@@ -66,6 +69,7 @@ export default function AccountCellRenderer({
}} }}
openOnKeyDown={true} openOnKeyDown={true}
blurOnSelectClose={false} blurOnSelectClose={false}
{...fieldProps}
/> />
</FormGroup> </FormGroup>
); );

View File

@@ -1,15 +1,17 @@
import React, { useCallback, useRef } from 'react'; import React, { useCallback, useRef } from 'react';
// import ItemsListField from 'components/ItemsListField';
import ItemsSuggestField from 'components/ItemsSuggestField';
import classNames from 'classnames'; import classNames from 'classnames';
import { FormGroup, Classes, Intent } from '@blueprintjs/core'; import { FormGroup, Classes, Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import ItemsSuggestField from 'components/ItemsSuggestField';
import { useCellAutoFocus } from 'hooks'; import { useCellAutoFocus } from 'hooks';
/**
* Items list cell.
*/
export default function ItemsListCell({ export default function ItemsListCell({
column: { id, filterSellable, filterPurchasable }, column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
row: { index }, row: { index },
cell: { value: initialValue }, cell: { value: initialValue },
payload: { items, updateData, errors, autoFocus }, payload: { items, updateData, errors, autoFocus },
@@ -19,6 +21,7 @@ export default function ItemsListCell({
// Auto-focus the items list input field. // Auto-focus the items list input field.
useCellAutoFocus(fieldRef, autoFocus, id, index); useCellAutoFocus(fieldRef, autoFocus, id, index);
// Handle the item selected.
const handleItemSelected = useCallback( const handleItemSelected = useCallback(
(item) => { (item) => {
updateData(index, id, item.id); updateData(index, id, item.id);
@@ -32,6 +35,7 @@ export default function ItemsListCell({
<FormGroup <FormGroup
intent={error ? Intent.DANGER : null} intent={error ? Intent.DANGER : null}
className={classNames('form-group--select-list', Classes.FILL)} className={classNames('form-group--select-list', Classes.FILL)}
{...formGroupProps}
> >
<ItemsSuggestField <ItemsSuggestField
items={items} items={items}
@@ -45,6 +49,7 @@ export default function ItemsListCell({
}} }}
openOnKeyDown={true} openOnKeyDown={true}
blurOnSelectClose={false} blurOnSelectClose={false}
{...fieldProps}
/> />
</FormGroup> </FormGroup>
); );

View File

@@ -0,0 +1,51 @@
import React from 'react';
import classNames from 'classnames';
import { Classes, Switch, FormGroup, Intent } from '@blueprintjs/core';
import { safeInvoke } from 'utils';
/**
* Switch editable cell.
*/
const SwitchEditableCell = ({
row: { index, original },
column: { id, switchProps, onSwitchChange },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = React.useState(initialValue);
// Handle the switch change.
const onChange = (e) => {
const newValue = e.target.checked;
setValue(newValue);
safeInvoke(payload.updateData, index, id, newValue);
safeInvoke(onSwitchChange, e, newValue, original);
};
React.useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<Switch
value={value}
onChange={onChange}
checked={initialValue}
minimal={true}
className="ml2"
{...switchProps}
/>
</FormGroup>
);
};
export default SwitchEditableCell;

View File

@@ -0,0 +1,42 @@
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import { Classes, TextArea, FormGroup, Intent } from '@blueprintjs/core';
const TextAreaEditableCell = ({
row: { index },
column: { id },
cell: { value: initialValue },
payload,
}) => {
const [value, setValue] = useState(initialValue);
const onChange = (e) => {
setValue(e.target.value);
};
const onBlur = () => {
payload.updateData(index, id, value);
};
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
const error = payload.errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}
className={classNames(Classes.FILL)}
>
<TextArea
growVertically={true}
large={true}
value={value}
onChange={onChange}
onBlur={onBlur}
fill={true}
/>
</FormGroup>
);
};
export default TextAreaEditableCell;

View File

@@ -6,7 +6,9 @@ import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell'; import PercentFieldCell from './PercentFieldCell';
import { DivFieldCell, EmptyDiv } from './DivFieldCell'; import { DivFieldCell, EmptyDiv } from './DivFieldCell';
import NumericInputCell from './NumericInputCell'; import NumericInputCell from './NumericInputCell';
import CheckBoxFieldCell from './CheckBoxFieldCell' import CheckBoxFieldCell from './CheckBoxFieldCell';
import SwitchFieldCell from './SwitchFieldCell';
import TextAreaCell from './TextAreaCell';
export { export {
AccountsListFieldCell, AccountsListFieldCell,
@@ -18,5 +20,7 @@ export {
DivFieldCell, DivFieldCell,
EmptyDiv, EmptyDiv,
NumericInputCell, NumericInputCell,
CheckBoxFieldCell CheckBoxFieldCell,
SwitchFieldCell,
TextAreaCell,
}; };

View File

@@ -9,16 +9,16 @@ function DialogComponent(props) {
const { name, children, closeDialog, onClose } = props; const { name, children, closeDialog, onClose } = props;
const handleClose = (event) => { const handleClose = (event) => {
closeDialog(name) closeDialog(name);
onClose && onClose(event); onClose && onClose(event);
}; };
return ( return (
<Dialog {...props} onClose={handleClose}> <Dialog {...props} onClose={handleClose}>
{ children } {children}
</Dialog> </Dialog>
); );
} }
export default compose( const DialogRoot = compose(withDialogActions)(DialogComponent);
withDialogActions,
)(DialogComponent); export { DialogRoot as Dialog };

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Spinner, Classes } from '@blueprintjs/core'; import { Spinner, Classes } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
export default function DialogContent(props) { export function DialogContent(props) {
const { isLoading, children } = props; const { isLoading, children } = props;
const loadingContent = ( const loadingContent = (

View File

@@ -0,0 +1,26 @@
import React from 'react';
import styled from 'styled-components';
import { Classes } from '@blueprintjs/core';
export function DialogFooterActions({ alignment = 'right', children }) {
return (
<DialogFooterActionsRoot
className={Classes.DIALOG_FOOTER_ACTIONS}
alignment={alignment}
>
{children}
</DialogFooterActionsRoot>
);
}
const DialogFooterActionsRoot = styled.div`
margin-left: -10px;
margin-right: -10px;
justify-content: ${(props) =>
props.alignment === 'right' ? 'flex-end' : 'flex-start'};
.bp3-button {
margin-left: 10px;
margin-left: 10px;
}
`;

View File

@@ -5,7 +5,7 @@ function LoadingContent() {
return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>); return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>);
} }
export default function DialogSuspense({ export function DialogSuspense({
children children
}) { }) {
return ( return (

View File

@@ -0,0 +1,6 @@
export * from './Dialog';
export * from './DialogFooterActions';
export * from './DialogSuspense';
export * from './DialogContent';

View File

@@ -20,6 +20,11 @@ import ReceiptPdfPreviewDialog from '../containers/Dialogs/ReceiptPdfPreviewDial
import MoneyInDialog from '../containers/Dialogs/MoneyInDialog'; import MoneyInDialog from '../containers/Dialogs/MoneyInDialog';
import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog'; import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog';
import BadDebtDialog from '../containers/Dialogs/BadDebtDialog'; import BadDebtDialog from '../containers/Dialogs/BadDebtDialog';
import NotifyInvoiceViaSMSDialog from '../containers/Dialogs/NotifyInvoiceViaSMSDialog';
import NotifyReceiptViaSMSDialog from '../containers/Dialogs/NotifyReceiptViaSMSDialog';
import NotifyEstimateViaSMSDialog from '../containers/Dialogs/NotifyEstimateViaSMSDialog';
import NotifyPaymentReceiveViaSMSDialog from '../containers/Dialogs/NotifyPaymentReceiveViaSMSDialog'
import SMSMessageDialog from '../containers/Dialogs/SMSMessageDialog';
/** /**
* Dialogs container. * Dialogs container.
@@ -45,7 +50,14 @@ export default function DialogsContainer() {
<ReceiptPdfPreviewDialog dialogName={'receipt-pdf-preview'} /> <ReceiptPdfPreviewDialog dialogName={'receipt-pdf-preview'} />
<MoneyInDialog dialogName={'money-in'} /> <MoneyInDialog dialogName={'money-in'} />
<MoneyOutDialog dialogName={'money-out'} /> <MoneyOutDialog dialogName={'money-out'} />
<NotifyInvoiceViaSMSDialog dialogName={'notify-invoice-via-sms'} />
<NotifyReceiptViaSMSDialog dialogName={'notify-receipt-via-sms'} />
<NotifyEstimateViaSMSDialog dialogName={'notify-estimate-via-sms'} />
<NotifyPaymentReceiveViaSMSDialog dialogName={'notify-payment-via-sms'} />
<BadDebtDialog dialogName={'write-off-bad-debt'} /> <BadDebtDialog dialogName={'write-off-bad-debt'} />
<SMSMessageDialog dialogName={'sms-message-form'} />
</div> </div>
); );
} }

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ListSelect from "./ListSelect"; import intl from 'react-intl-universal';
import ListSelect from './ListSelect';
export default function DisplayNameList({ export default function DisplayNameList({
salutation, salutation,
@@ -9,25 +10,32 @@ export default function DisplayNameList({
...restProps ...restProps
}) { }) {
const formats = [ const formats = [
{ format: '{1} {2} {3}', values: [salutation, firstName, lastName], required: [1] }, {
format: '{1} {2} {3}',
values: [salutation, firstName, lastName],
required: [1],
},
{ format: '{1} {2}', values: [firstName, lastName], required: [] }, { format: '{1} {2}', values: [firstName, lastName], required: [] },
{ format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] }, { format: '{1}, {2}', values: [firstName, lastName], required: [1, 2] },
{ format: '{1}', values: [company], required: [1] } { format: '{1}', values: [company], required: [1] },
]; ];
const formatOptions = formats const formatOptions = formats
.filter((format) => !format.values.some((value, index) => { .filter(
return !value && format.required.indexOf(index + 1) !== -1; (format) =>
})) !format.values.some((value, index) => {
return !value && format.required.indexOf(index + 1) !== -1;
}),
)
.map((formatOption) => { .map((formatOption) => {
const { format, values } = formatOption; const { format, values } = formatOption;
let label = format; let label = format;
values.forEach((value, index) => { values.forEach((value, index) => {
const replaceWith = (value || ''); const replaceWith = value || '';
label = label.replace(`{${index + 1}}`, replaceWith).trim(); label = label.replace(`{${index + 1}}`, replaceWith).trim();
}); });
return { label: label.replace(/\s+/g, " ") }; return { label: label.replace(/\s+/g, ' ') };
}); });
return ( return (
@@ -35,9 +43,9 @@ export default function DisplayNameList({
items={formatOptions} items={formatOptions}
selectedItemProp={'label'} selectedItemProp={'label'}
textProp={'label'} textProp={'label'}
defaultText={'Select display name as'} defaultText={intl.get('select_display_name_as')}
filterable={false} filterable={false}
{ ...restProps } {...restProps}
/> />
); );
} }

View File

@@ -3,6 +3,7 @@ import { Position, Drawer } from '@blueprintjs/core';
import 'style/components/Drawer.scss'; import 'style/components/Drawer.scss';
import { DrawerProvider } from './DrawerProvider';
import withDrawerActions from 'containers/Drawer/withDrawerActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -27,7 +28,7 @@ function DrawerComponent(props) {
portalClassName={'drawer-portal'} portalClassName={'drawer-portal'}
{...props} {...props}
> >
{children} <DrawerProvider {...props}>{children}</DrawerProvider>
</Drawer> </Drawer>
); );
} }

View File

@@ -0,0 +1,16 @@
import React, { createContext, useContext } from 'react';
const DrawerContext = createContext();
/**
* Account form provider.
*/
function DrawerProvider({ ...props }) {
const provider = { ...props };
return <DrawerContext.Provider value={provider} {...props} />;
}
const useDrawerContext = () => useContext(DrawerContext);
export { DrawerProvider, useDrawerContext };

View File

@@ -14,6 +14,9 @@ import CustomerDetailsDrawer from '../containers/Drawers/CustomerDetailsDrawer';
import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer'; import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer';
import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer'; import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer';
import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTransactionDetailDrawer'; import CashflowTransactionDetailDrawer from '../containers/Drawers/CashflowTransactionDetailDrawer';
import QuickCreateCustomerDrawer from '../containers/Drawers/QuickCreateCustomerDrawer';
import QuickCreateItemDrawer from '../containers/Drawers/QuickCreateItemDrawer';
import QuickWriteVendorDrawer from '../containers/Drawers/QuickWriteVendorDrawer';
import { DRAWERS } from 'common/drawers'; import { DRAWERS } from 'common/drawers';
@@ -38,7 +41,12 @@ export default function DrawersContainer() {
<InventoryAdjustmentDetailDrawer <InventoryAdjustmentDetailDrawer
name={DRAWERS.INVENTORY_ADJUSTMENT_DRAWER} name={DRAWERS.INVENTORY_ADJUSTMENT_DRAWER}
/> />
<CashflowTransactionDetailDrawer name={DRAWERS.CASHFLOW_TRNASACTION_DRAWER} /> <CashflowTransactionDetailDrawer
name={DRAWERS.CASHFLOW_TRNASACTION_DRAWER}
/>
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} />
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
</div> </div>
); );
} }

View File

@@ -1,13 +1,68 @@
import React, { useState, useCallback, useEffect, useMemo } from 'react'; import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { Suggest } from '@blueprintjs/select'; import { Suggest } from '@blueprintjs/select';
import classNames from 'classnames';
import * as R from 'ramda';
import { CLASSES } from 'common/classes';
import { FormattedMessage as T } from 'components'; import { FormattedMessage as T } from 'components';
export default function ItemsSuggestField({ import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { DRAWERS } from 'common/drawers';
// Creates a new item from query.
const createNewItemFromQuery = (name) => {
return {
name,
};
};
// Handle quick create new customer.
const createNewItemRenderer = (query, active, handleClick) => {
return (
<MenuItem
icon="add"
text={`Create "${query}"`}
active={active}
shouldDismissPopover={false}
onClick={handleClick}
/>
);
};
// Item renderer.
const itemRenderer = (item, { modifiers, handleClick }) => (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
);
// Filters items.
const filterItemsPredicater = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
}
};
// Handle input value renderer.
const handleInputValueRenderer = (inputValue) => {
if (inputValue) {
return inputValue.name.toString();
}
return '';
};
function ItemsSuggestField({
items, items,
initialItemId, initialItemId,
selectedItemId, selectedItemId,
@@ -18,6 +73,10 @@ export default function ItemsSuggestField({
sellable = false, sellable = false,
purchasable = false, purchasable = false,
popoverFill = false, popoverFill = false,
allowCreate = true,
openDrawer,
...suggestProps ...suggestProps
}) { }) {
// Filters items based on filter props. // Filters items based on filter props.
@@ -36,28 +95,23 @@ export default function ItemsSuggestField({
// Find initial item object. // Find initial item object.
const initialItem = useMemo( const initialItem = useMemo(
() => filteredItems.some((a) => a.id === initialItemId), () => filteredItems.some((a) => a.id === initialItemId),
[initialItemId], [initialItemId, filteredItems],
); );
const [selectedItem, setSelectedItem] = useState(initialItem || null); const [selectedItem, setSelectedItem] = useState(initialItem || null);
const onItemSelect = useCallback( const onItemSelect = useCallback(
(item) => { (item) => {
setSelectedItem({ ...item }); if (item.id) {
onItemSelected && onItemSelected(item); setSelectedItem({ ...item });
onItemSelected && onItemSelected(item);
} else {
openDrawer(DRAWERS.QUICK_CREATE_ITEM);
}
}, },
[setSelectedItem, onItemSelected], [setSelectedItem, onItemSelected, openDrawer],
); );
const itemRenderer = useCallback((item, { modifiers, handleClick }) => (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
));
useEffect(() => { useEffect(() => {
if (typeof selectedItemId !== 'undefined') { if (typeof selectedItemId !== 'undefined') {
const item = selectedItemId const item = selectedItemId
@@ -67,27 +121,12 @@ export default function ItemsSuggestField({
} }
}, [selectedItemId, filteredItems, setSelectedItem]); }, [selectedItemId, filteredItems, setSelectedItem]);
const handleInputValueRenderer = (inputValue) => { // Maybe inject create new item props to suggest component.
if (inputValue) { const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
return inputValue.name.toString(); const maybeCreateNewItemFromQuery = allowCreate
} ? createNewItemFromQuery
return ''; : null;
};
// Filters items.
const filterItemsPredicater = useCallback(
(query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return `${normalizedTitle} ${item.code}`.indexOf(normalizedQuery) >= 0;
}
},
[],
);
return ( return (
<Suggest <Suggest
items={filteredItems} items={filteredItems}
@@ -104,7 +143,12 @@ export default function ItemsSuggestField({
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,
})} })}
createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemPosition={'top'}
{...suggestProps} {...suggestProps}
/> />
); );
} }
export default R.compose(withDrawerActions)(ItemsSuggestField);

View File

@@ -0,0 +1,36 @@
import React from 'react';
import {
Button,
Popover,
PopoverInteractionKind,
Position,
MenuItem,
Menu,
} from '@blueprintjs/core';
import { Icon, FormattedMessage as T } from 'components';
function MoreMenuItems({ payload: { onNotifyViaSMS } }) {
return (
<Popover
minimal={true}
content={
<Menu>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
</Menu>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
modifiers={{
offset: { offset: '0, 4' },
}}
>
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
</Popover>
);
}
export default MoreMenuItems;

View File

@@ -1,46 +0,0 @@
import React from 'react';
import {
Button,
PopoverInteractionKind,
MenuItem,
Position,
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select';
import { Icon } from 'components';
function MoreVertMenutItems({ text, items, onItemSelect, buttonProps }) {
// Menu items renderer.
const itemsRenderer = (item, { handleClick, modifiers, query }) => (
<MenuItem text={item.name} label={item.label} onClick={handleClick} />
);
const handleMenuSelect = (type) => {
onItemSelect && onItemSelect(type);
};
return (
<Select
items={items}
itemRenderer={itemsRenderer}
onItemSelect={handleMenuSelect}
popoverProps={{
minimal: true,
position: Position.BOTTOM_LEFT,
interactionKind: PopoverInteractionKind.CLICK,
modifiers: {
offset: { offset: '0, 4' },
},
}}
filterable={false}
>
<Button
text={text}
icon={<Icon icon={'more-vert'} iconSize={16} />}
minimal={true}
{...buttonProps}
/>
</Select>
);
}
export default MoreVertMenutItems;

View File

@@ -0,0 +1,46 @@
import React from 'react';
import styled from 'styled-components';
import { Icon } from 'components';
/**
* SMS Message preview.
*/
export function SMSMessagePreview({
message,
iconWidth = '265px',
iconHeight = '287px',
iconColor = '#adadad',
}) {
return (
<SMSMessagePreviewBase>
<Icon
icon={'sms-message-preview'}
width={iconWidth}
height={iconHeight}
color={iconColor}
/>
<SMSMessageText>{message}</SMSMessageText>
</SMSMessagePreviewBase>
);
}
const SMSMessageText = styled.div`
position: absolute;
top: 60px;
padding: 12px;
color: #fff;
border-radius: 12px;
margin-left: 12px;
margin-right: 12px;
word-break: break-word;
background: #2fa2e4;
font-size: 13px;
line-height: 1.6;
`;
const SMSMessagePreviewBase = styled.div`
position: relative;
width: 265px;
margin: 0 auto;
`;

View File

@@ -23,9 +23,6 @@ import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect'; import AccountsTypesSelect from './AccountsTypesSelect';
import LoadingIndicator from './LoadingIndicator'; import LoadingIndicator from './LoadingIndicator';
import DashboardActionViewsList from './Dashboard/DashboardActionViewsList'; import DashboardActionViewsList from './Dashboard/DashboardActionViewsList';
import Dialog from './Dialog/Dialog';
import DialogContent from './Dialog/DialogContent';
import DialogSuspense from './Dialog/DialogSuspense';
import InputPrependButton from './Forms/InputPrependButton'; import InputPrependButton from './Forms/InputPrependButton';
import CategoriesSelectList from './CategoriesSelectList'; import CategoriesSelectList from './CategoriesSelectList';
import Row from './Grid/Row'; import Row from './Grid/Row';
@@ -61,8 +58,9 @@ import Card from './Card';
import AvaterCell from './AvaterCell'; import AvaterCell from './AvaterCell';
import { ItemsMultiSelect } from './Items'; import { ItemsMultiSelect } from './Items';
import MoreVertMenutItems from './MoreVertMenutItems'; import MoreMenuItems from './MoreMenutItems';
export * from './Dialog';
export * from './Menu'; export * from './Menu';
export * from './AdvancedFilter/AdvancedFilterDropdown'; export * from './AdvancedFilter/AdvancedFilterDropdown';
export * from './AdvancedFilter/AdvancedFilterPopover'; export * from './AdvancedFilter/AdvancedFilterPopover';
@@ -83,10 +81,12 @@ export * from './MultiSelectTaggable';
export * from './Utils/FormatNumber'; export * from './Utils/FormatNumber';
export * from './Utils/FormatDate'; export * from './Utils/FormatDate';
export * from './BankAccounts'; export * from './BankAccounts';
export * from './IntersectionObserver' export * from './IntersectionObserver';
export * from './Datatable/CellForceWidth'; export * from './Datatable/CellForceWidth';
export * from './Button'; export * from './Button';
export * from './IntersectionObserver'; export * from './IntersectionObserver';
export * from './SMSPreview';
export * from './Contacts';
const Hint = FieldHint; const Hint = FieldHint;
@@ -120,9 +120,6 @@ export {
LoadingIndicator, LoadingIndicator,
DashboardActionViewsList, DashboardActionViewsList,
AppToaster, AppToaster,
Dialog,
DialogContent,
DialogSuspense,
InputPrependButton, InputPrependButton,
CategoriesSelectList, CategoriesSelectList,
Col, Col,
@@ -158,5 +155,5 @@ export {
ItemsMultiSelect, ItemsMultiSelect,
Card, Card,
AvaterCell, AvaterCell,
MoreVertMenutItems, MoreMenuItems,
}; };

View File

@@ -1,29 +1,34 @@
import React from 'react' import React from 'react';
import { FormattedMessage as T } from 'components'; import { FormattedMessage as T } from 'components';
export default [ export default [
{ {
text: <T id={'general'}/>, text: <T id={'general'} />,
disabled: false, disabled: false,
href: '/preferences/general', href: '/preferences/general',
}, },
{ {
text: <T id={'users'}/>, text: <T id={'users'} />,
href: '/preferences/users', href: '/preferences/users',
}, },
{ {
text: <T id={'currencies'}/>, text: <T id={'currencies'} />,
href: '/preferences/currencies', href: '/preferences/currencies',
}, },
{ {
text: <T id={'accountant'}/>, text: <T id={'accountant'} />,
disabled: false, disabled: false,
href: '/preferences/accountant', href: '/preferences/accountant',
}, },
{ {
text: <T id={'items'}/>, text: <T id={'items'} />,
disabled: false, disabled: false,
href: '/preferences/items', href: '/preferences/items',
}, },
{
text: <T id={'sms_integration.label'} />,
disabled: false,
href: '/preferences/sms-message',
},
]; ];

View File

@@ -128,6 +128,7 @@ export const useJournalTableEntriesColumns = () => {
className: 'account', className: 'account',
disableSortBy: true, disableSortBy: true,
width: 160, width: 160,
fieldProps: { allowCreate: true }
}, },
{ {
Header: CreditHeaderCell, Header: CreditHeaderCell,

View File

@@ -9,23 +9,24 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import classNames from 'classnames'; import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import styled from 'styled-components';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { Icon } from 'components'; import { Icon } from 'components';
import { useCustomerFormContext } from './CustomerFormProvider'; import { useCustomerFormContext } from './CustomerFormProvider';
import { safeInvoke } from 'utils';
/** /**
* Customer floating actions bar. * Customer floating actions bar.
*/ */
export default function CustomerFloatingActions() { export default function CustomerFloatingActions({ onCancel }) {
const history = useHistory();
// Customer form context. // Customer form context.
const { isNewMode,setSubmitPayload } = useCustomerFormContext(); const { isNewMode, setSubmitPayload } = useCustomerFormContext();
// Formik context. // Formik context.
const { resetForm, submitForm, isSubmitting } = useFormikContext(); const { resetForm, submitForm, isSubmitting } = useFormikContext();
@@ -37,7 +38,7 @@ export default function CustomerFloatingActions() {
// Handle cancel button click. // Handle cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
history.goBack(); safeInvoke(onCancel, event);
}; };
// handle clear button clicl. // handle clear button clicl.
@@ -55,7 +56,7 @@ export default function CustomerFloatingActions() {
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<ButtonGroup> <ButtonGroup>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<Button <SaveButton
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
@@ -83,6 +84,7 @@ export default function CustomerFloatingActions() {
/> />
</Popover> </Popover>
</ButtonGroup> </ButtonGroup>
{/* ----------- Clear & Reset----------- */} {/* ----------- Clear & Reset----------- */}
<Button <Button
className={'ml1'} className={'ml1'}
@@ -99,3 +101,7 @@ export default function CustomerFloatingActions() {
</div> </div>
); );
} }
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -1,152 +1,14 @@
import React, { useMemo } from 'react'; import React from 'react';
import { Formik, Form } from 'formik'; import { CustomerFormProvider } from './CustomerFormProvider';
import moment from 'moment'; import CustomerFormFormik from './CustomerFormFormik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import CustomerFormPrimarySection from './CustomerFormPrimarySection';
import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose, transformToForm } from 'utils';
import { useCustomerFormContext } from './CustomerFormProvider';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
const defaultInitialValues = {
customer_type: 'business',
salutation: '',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
email: '',
work_phone: '',
personal_phone: '',
website: '',
note: '',
active: true,
billing_address_country: '',
billing_address_1: '',
billing_address_2: '',
billing_address_city: '',
billing_address_state: '',
billing_address_postcode: '',
billing_address_phone: '',
shipping_address_country: '',
shipping_address_1: '',
shipping_address_2: '',
shipping_address_city: '',
shipping_address_state: '',
shipping_address_postcode: '',
shipping_address_phone: '',
opening_balance: '',
currency_code: '',
opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
};
/** /**
* Customer form. * Abstructed customer form.
*/ */
function CustomerForm({ organization: { base_currency } }) { export default function CustomerForm({ customerId }) {
const {
customer,
customerId,
submitPayload,
contactDuplicate,
editCustomerMutate,
createCustomerMutate,
isNewMode,
} = useCustomerFormContext();
// const isNewMode = !customerId;
const history = useHistory();
/**
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
() => ({
...defaultInitialValues,
currency_code: base_currency,
...transformToForm(contactDuplicate || customer, defaultInitialValues),
}),
[customer, contactDuplicate, base_currency],
);
//Handles the form submit.
const handleFormSubmit = (
values,
{ setSubmitting, resetForm, setErrors },
) => {
const formValues = { ...values };
const onSuccess = () => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_customer_has_been_created_successfully'
: 'the_item_customer_has_been_edited_successfully',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
if (!submitPayload.noRedirect) {
history.push('/customers');
}
};
const onError = () => {
setSubmitting(false);
};
if (isNewMode) {
createCustomerMutate(formValues).then(onSuccess).catch(onError);
} else {
editCustomerMutate([customer.id, formValues])
.then(onSuccess)
.catch(onError);
}
};
return ( return (
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_CUSTOMER)}> <CustomerFormProvider customerId={customerId}>
<Formik <CustomerFormFormik />
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm} </CustomerFormProvider>
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions />
</Form>
</Formik>
</div>
); );
} }
export default compose(withCurrentOrganization())(CustomerForm);

View File

@@ -6,7 +6,6 @@ import intl from 'react-intl-universal';
import { inputIntent } from 'utils'; import { inputIntent } from 'utils';
export default function CustomerFormAfterPrimarySection({}) { export default function CustomerFormAfterPrimarySection({}) {
return ( return (
<div class="customer-form__after-primary-section-content"> <div class="customer-form__after-primary-section-content">
{/*------------ Customer email -----------*/} {/*------------ Customer email -----------*/}
@@ -31,21 +30,21 @@ export default function CustomerFormAfterPrimarySection({}) {
inline={true} inline={true}
> >
<ControlGroup> <ControlGroup>
<FastField name={'work_phone'}> <FastField name={'personal_phone'}>
{({ field, meta: { error, touched } }) => ( {({ field, meta: { error, touched } }) => (
<InputGroup <InputGroup
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
placeholder={intl.get('work')} placeholder={intl.get('personal')}
{...field} {...field}
/> />
)} )}
</FastField> </FastField>
<FastField name={'personal_phone'}> <FastField name={'work_phone'}>
{({ field, meta: { error, touched } }) => ( {({ field, meta: { error, touched } }) => (
<InputGroup <InputGroup
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
placeholder={intl.get('Mobile')} placeholder={intl.get('work')}
{...field} {...field}
/> />
)} )}

View File

@@ -0,0 +1,127 @@
import React, { useMemo } from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import { CreateCustomerForm, EditCustomerForm } from './CustomerForm.schema';
import CustomerFormPrimarySection from './CustomerFormPrimarySection';
import CustomerFormAfterPrimarySection from './CustomerFormAfterPrimarySection';
import CustomersTabs from './CustomersTabs';
import CustomerFloatingActions from './CustomerFloatingActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { compose, transformToForm, saveInvoke } from 'utils';
import { useCustomerFormContext } from './CustomerFormProvider';
import { defaultInitialValues } from './utils';
import 'style/pages/Customers/Form.scss';
/**
* Customer form.
*/
function CustomerFormFormik({
organization: { base_currency },
// #ownProps
initialValues: initialCustomerValues,
onSubmitSuccess,
onSubmitError,
onCancel,
className,
}) {
const {
customer,
submitPayload,
contactDuplicate,
editCustomerMutate,
createCustomerMutate,
isNewMode,
} = useCustomerFormContext();
/**
* Initial values in create and edit mode.
*/
const initialValues = useMemo(
() => ({
...defaultInitialValues,
currency_code: base_currency,
...transformToForm(contactDuplicate || customer, defaultInitialValues),
...initialCustomerValues,
}),
[customer, contactDuplicate, base_currency, initialCustomerValues],
);
// Handles the form submit.
const handleFormSubmit = (values, formArgs) => {
const { setSubmitting, resetForm } = formArgs;
const formValues = { ...values };
const onSuccess = () => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_customer_has_been_created_successfully'
: 'the_item_customer_has_been_edited_successfully',
),
intent: Intent.SUCCESS,
});
setSubmitting(false);
resetForm();
saveInvoke(onSubmitSuccess, values, formArgs, submitPayload);
};
const onError = () => {
setSubmitting(false);
saveInvoke(onSubmitError, values, formArgs, submitPayload);
};
if (isNewMode) {
createCustomerMutate(formValues).then(onSuccess).catch(onError);
} else {
editCustomerMutate([customer.id, formValues])
.then(onSuccess)
.catch(onError);
}
};
return (
<div
className={classNames(
CLASSES.PAGE_FORM,
CLASSES.PAGE_FORM_CUSTOMER,
className,
)}
>
<Formik
validationSchema={isNewMode ? CreateCustomerForm : EditCustomerForm}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div className={classNames(CLASSES.PAGE_FORM_HEADER_PRIMARY)}>
<CustomerFormPrimarySection />
</div>
<div className={'page-form__after-priamry-section'}>
<CustomerFormAfterPrimarySection />
</div>
<div className={classNames(CLASSES.PAGE_FORM_TABS)}>
<CustomersTabs />
</div>
<CustomerFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}
export default compose(withCurrentOrganization())(CustomerFormFormik);

View File

@@ -1,20 +1,74 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { DashboardCard } from 'components'; import { DashboardCard } from 'components';
import CustomerForm from './CustomerForm'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { CustomerFormProvider } from './CustomerFormProvider';
import 'style/pages/Customers/PageForm.scss'; import CustomerFormFormik from './CustomerFormFormik';
import {
CustomerFormProvider,
useCustomerFormContext,
} from './CustomerFormProvider';
export default function CustomerFormPage() { /**
const { id } = useParams(); * Customer form page loading.
* @returns {JSX}
*/
function CustomerFormPageLoading({ children }) {
const { isFormLoading } = useCustomerFormContext();
return ( return (
<CustomerFormProvider customerId={id}> <CustomerDashboardInsider loading={isFormLoading}>
<DashboardCard page> {children}
<CustomerForm /> </CustomerDashboardInsider>
</DashboardCard> );
}
/**
* Customer form page.
* @returns {JSX}
*/
export default function CustomerFormPage() {
const history = useHistory();
const { id } = useParams();
const customerId = parseInt(id, 10);
// Handle the form submit success.
const handleSubmitSuccess = (values, formArgs, submitPayload) => {
if (!submitPayload.noRedirect) {
history.push('/customers');
}
};
// Handle the form cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return (
<CustomerFormProvider customerId={customerId}>
<CustomerFormPageLoading>
<DashboardCard page>
<CustomerFormPageFormik
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</CustomerFormPageLoading>
</CustomerFormProvider> </CustomerFormProvider>
); );
} }
const CustomerFormPageFormik = styled(CustomerFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;
const CustomerDashboardInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;

View File

@@ -1,6 +1,5 @@
import React, { useState, createContext } from 'react'; import React, { useState, createContext } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { import {
useCustomer, useCustomer,
useCurrencies, useCurrencies,
@@ -24,7 +23,7 @@ function CustomerFormProvider({ customerId, ...props }) {
// Handle fetch contact duplicate details. // Handle fetch contact duplicate details.
const { data: contactDuplicate, isLoading: isContactLoading } = useContact( const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
contactId, contactId,
{ enabled: !!contactId, }, { enabled: !!contactId },
); );
// Handle fetch Currencies data table // Handle fetch Currencies data table
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies(); const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
@@ -38,6 +37,9 @@ function CustomerFormProvider({ customerId, ...props }) {
// determines whether the form new or duplicate mode. // determines whether the form new or duplicate mode.
const isNewMode = contactId || !customerId; const isNewMode = contactId || !customerId;
const isFormLoading =
isCustomerLoading || isCurrenciesLoading || isContactLoading;
const provider = { const provider = {
customerId, customerId,
customer, customer,
@@ -48,24 +50,14 @@ function CustomerFormProvider({ customerId, ...props }) {
isCustomerLoading, isCustomerLoading,
isCurrenciesLoading, isCurrenciesLoading,
isFormLoading,
setSubmitPayload, setSubmitPayload,
editCustomerMutate, editCustomerMutate,
createCustomerMutate, createCustomerMutate,
}; };
return ( return <CustomerFormContext.Provider value={provider} {...props} />;
<DashboardInsider
loading={
isCustomerLoading ||
isCurrenciesLoading ||
isContactLoading
}
name={'customer-form'}
>
<CustomerFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
} }
const useCustomerFormContext = () => React.useContext(CustomerFormContext); const useCustomerFormContext = () => React.useContext(CustomerFormContext);

View File

@@ -0,0 +1,38 @@
import moment from 'moment';
export const defaultInitialValues = {
customer_type: 'business',
salutation: '',
first_name: '',
last_name: '',
company_name: '',
display_name: '',
email: '',
work_phone: '',
personal_phone: '',
website: '',
note: '',
active: true,
billing_address_country: '',
billing_address_1: '',
billing_address_2: '',
billing_address_city: '',
billing_address_state: '',
billing_address_postcode: '',
billing_address_phone: '',
shipping_address_country: '',
shipping_address_1: '',
shipping_address_2: '',
shipping_address_city: '',
shipping_address_state: '',
shipping_address_postcode: '',
shipping_address_phone: '',
opening_balance: '',
currency_code: '',
opening_balance_at: moment(new Date()).format('YYYY-MM-DD'),
};

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { NotifyEstimateViaSMSFormProvider } from './NotifyEstimateViaSMSFormProvider';
import NotifyEstimateViaSMSForm from './NotifyEstimateViaSMSForm';
export default function NotifyEstimateViaSMSDialogContent({
// #ownProps
dialogName,
estimate,
}) {
return (
<NotifyEstimateViaSMSFormProvider
estimateId={estimate}
dialogName={dialogName}
>
<NotifyEstimateViaSMSForm />
</NotifyEstimateViaSMSFormProvider>
);
}

View File

@@ -0,0 +1,81 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useEstimateViaSMSContext } from './NotifyEstimateViaSMSFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const notificationType = {
key: 'sale-estimate-details',
label: intl.get('sms_notification.estimate_details.type'),
};
function NotifyEstimateViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
estimateId,
dialogName,
estimateSMSDetail,
createNotifyEstimateBySMSMutate,
} = useEstimateViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_estimate_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
setSubmitting(false);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
createNotifyEstimateBySMSMutate([estimateId, values])
.then(onSuccess)
.catch(onError);
};
const initialValues = {
...estimateSMSDetail,
notification_key: notificationType.key,
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={[notificationType]}
onCancel={handleFormCancel}
onSubmit={handleFormSubmit}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyEstimateViaSMSForm);

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { DialogContent } from 'components';
import {
useEstimateSMSDetail,
useCreateNotifyEstimateBySMS,
} from 'hooks/query';
const NotifyEstimateViaSMSContext = React.createContext();
function NotifyEstimateViaSMSFormProvider({
estimateId,
dialogName,
...props
}) {
const { data: estimateSMSDetail, isLoading: isEstimateSMSDetailLoading } =
useEstimateSMSDetail(estimateId, {
enabled: !!estimateId,
});
// Create notfiy estimate by sms mutations.
const { mutateAsync: createNotifyEstimateBySMSMutate } =
useCreateNotifyEstimateBySMS();
// State provider.
const provider = {
estimateId,
dialogName,
estimateSMSDetail,
createNotifyEstimateBySMSMutate,
};
return (
<DialogContent isLoading={isEstimateSMSDetailLoading}>
<NotifyEstimateViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useEstimateViaSMSContext = () =>
React.useContext(NotifyEstimateViaSMSContext);
export { NotifyEstimateViaSMSFormProvider, useEstimateViaSMSContext };

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyEstimateViaSMSDialogContent = React.lazy(() =>
import('./NotifyEstimateViaSMSDialogContent'),
);
function NotifyEstimateViaSMSDialog({
dialogName,
payload: { estimateId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyEstimateViaSMSDialogContent
dialogName={dialogName}
estimate={estimateId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyEstimateViaSMSDialog);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { NotifyInvoiceViaSMSFormProvider } from './NotifyInvoiceViaSMSFormProvider';
import NotifyInvoiceViaSMSForm from './NotifyInvoiceViaSMSForm';
export default function NotifyInvoiceViaSMSDialogContent({
// #ownProps
dialogName,
invoiceId,
}) {
return (
<NotifyInvoiceViaSMSFormProvider
invoiceId={invoiceId}
dialogName={dialogName}
>
<NotifyInvoiceViaSMSForm />
</NotifyInvoiceViaSMSFormProvider>
);
}

View File

@@ -0,0 +1,108 @@
import React from 'react';
import intl from 'react-intl-universal';
import { pick } from 'lodash';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useNotifyInvoiceViaSMSContext } from './NotifyInvoiceViaSMSFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const transformFormValuesToRequest = (values) => {
return pick(values, ['notification_key']);
};
// Momerize the notification types.
const notificationTypes = [
{
key: 'details',
label: intl.get('sms_notification.invoice_details.type'),
},
{
key: 'reminder',
label: intl.get('sms_notification.invoice_reminder.type'),
},
];
/**
* Notify Invoice Via SMS Form.
*/
function NotifyInvoiceViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
createNotifyInvoiceBySMSMutate,
invoiceId,
invoiceSMSDetail,
dialogName,
notificationType,
setNotificationType,
} = useNotifyInvoiceViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_invoice_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
setSubmitting(false);
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
// Transformes the form values to request.
const requestValues = transformFormValuesToRequest(values);
// Submits invoice SMS notification.
createNotifyInvoiceBySMSMutate([invoiceId, requestValues])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = React.useCallback(() => {
closeDialog(dialogName);
}, [closeDialog, dialogName]);
const initialValues = {
notification_key: notificationType,
...invoiceSMSDetail,
};
// Handle form values change.
const handleValuesChange = (values) => {
if (values.notification_key !== notificationType) {
setNotificationType(values.notification_key);
}
};
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationTypes}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
onValuesChange={handleValuesChange}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyInvoiceViaSMSForm);

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { DialogContent } from 'components';
import { useCreateNotifyInvoiceBySMS, useInvoiceSMSDetail } from 'hooks/query';
const NotifyInvoiceViaSMSContext = React.createContext();
/**
* Invoice SMS notification provider.
*/
function NotifyInvoiceViaSMSFormProvider({ invoiceId, dialogName, ...props }) {
const [notificationType, setNotificationType] = React.useState('details');
// Retrieve the invoice sms notification message details.
const { data: invoiceSMSDetail, isLoading: isInvoiceSMSDetailLoading } =
useInvoiceSMSDetail(
invoiceId,
{
notification_key: notificationType,
},
{
enabled: !!invoiceId,
keepPreviousData: true,
},
);
// Create notfiy invoice by sms mutations.
const { mutateAsync: createNotifyInvoiceBySMSMutate } =
useCreateNotifyInvoiceBySMS();
// State provider.
const provider = {
invoiceId,
invoiceSMSDetail,
dialogName,
createNotifyInvoiceBySMSMutate,
notificationType,
setNotificationType,
};
return (
<DialogContent isLoading={isInvoiceSMSDetailLoading}>
<NotifyInvoiceViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useNotifyInvoiceViaSMSContext = () =>
React.useContext(NotifyInvoiceViaSMSContext);
export { NotifyInvoiceViaSMSFormProvider, useNotifyInvoiceViaSMSContext };

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyInvoiceViaSMSDialogContent = React.lazy(() =>
import('./NotifyInvoiceViaSMSDialogContent'),
);
function NotifyInvoiceViaSMSDialog({
dialogName,
payload: { invoiceId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyInvoiceViaSMSDialogContent
dialogName={dialogName}
invoiceId={invoiceId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyInvoiceViaSMSDialog);

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { DialogContent } from 'components';
import {
useCreateNotifyPaymentReceiveBySMS,
usePaymentReceiveSMSDetail,
} from 'hooks/query';
const NotifyPaymentReceiveViaSMSContext = React.createContext();
function NotifyPaymentReceiveViaFormProvider({
paymentReceiveId,
dialogName,
...props
}) {
// Create notfiy receipt via sms mutations.
const { mutateAsync: createNotifyPaymentReceivetBySMSMutate } =
useCreateNotifyPaymentReceiveBySMS();
const {
data: paymentReceiveMSDetail,
isLoading: isPaymentReceiveSMSDetailLoading,
} = usePaymentReceiveSMSDetail(paymentReceiveId, {
enabled: !!paymentReceiveId,
});
// State provider.
const provider = {
paymentReceiveId,
dialogName,
paymentReceiveMSDetail,
createNotifyPaymentReceivetBySMSMutate,
};
return (
<DialogContent isLoading={isPaymentReceiveSMSDetailLoading}>
<NotifyPaymentReceiveViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useNotifyPaymentReceiveViaSMSContext = () =>
React.useContext(NotifyPaymentReceiveViaSMSContext);
export {
NotifyPaymentReceiveViaFormProvider,
useNotifyPaymentReceiveViaSMSContext,
};

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { NotifyPaymentReceiveViaFormProvider } from './NotifyPaymentReceiveViaFormProvider';
import NotifyPaymentReceiveViaSMSForm from './NotifyPaymentReceiveViaSMSForm';
export default function NotifyPaymentReceiveViaSMSContent({
// #ownProps
dialogName,
paymentReceive,
}) {
return (
<NotifyPaymentReceiveViaFormProvider
paymentReceiveId={paymentReceive}
dialogName={dialogName}
>
<NotifyPaymentReceiveViaSMSForm />
</NotifyPaymentReceiveViaFormProvider>
);
}

View File

@@ -0,0 +1,87 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useNotifyPaymentReceiveViaSMSContext } from './NotifyPaymentReceiveViaFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const notificationType = {
key: 'payment-receive-details',
label: intl.get('sms_notification.payment_details.type'),
};
/**
* Notify Payment Recive Via SMS Form.
*/
function NotifyPaymentReceiveViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
paymentReceiveId,
paymentReceiveMSDetail,
createNotifyPaymentReceivetBySMSMutate,
} = useNotifyPaymentReceiveViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
'notify_payment_receive_via_sms.dialog.success_message',
),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
createNotifyPaymentReceivetBySMSMutate([paymentReceiveId, values])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
// Form initial values.
const initialValues = React.useMemo(
() => ({
...paymentReceiveMSDetail,
notification_key: notificationType.key,
}),
[paymentReceiveMSDetail],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyPaymentReceiveViaSMSForm);

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyPaymentReceiveViaSMSDialogContent = React.lazy(() =>
import('./NotifyPaymentReceiveViaSMSContent'),
);
function NotifyPaymentReciveViaSMSDialog({
dialogName,
payload: { paymentReceiveId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyPaymentReceiveViaSMSDialogContent
dialogName={dialogName}
paymentReceive={paymentReceiveId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyPaymentReciveViaSMSDialog);

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { NotifyReceiptViaSMSFormProvider } from './NotifyReceiptViaSMSFormProvider';
import NotifyReceiptViaSMSForm from './NotifyReceiptViaSMSForm';
export default function NotifyReceiptViaSMSDialogContent({
// #ownProps
dialogName,
receipt,
}) {
return (
<NotifyReceiptViaSMSFormProvider
receiptId={receipt}
dialogName={dialogName}
>
<NotifyReceiptViaSMSForm />
</NotifyReceiptViaSMSFormProvider>
);
}

View File

@@ -0,0 +1,85 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import NotifyViaSMSForm from '../../NotifyViaSMS/NotifyViaSMSForm';
import { useNotifyReceiptViaSMSContext } from './NotifyReceiptViaSMSFormProvider';
import { transformErrors } from '../../../containers/NotifyViaSMS/utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
const notificationType = {
key: 'sale-receipt-details',
label: intl.get('sms_notification.receipt_details.type'),
};
/**
* Notify Receipt Via SMS Form.
*/
function NotifyReceiptViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
receiptId,
receiptSMSDetail,
createNotifyReceiptBySMSMutate,
} = useNotifyReceiptViaSMSContext();
const [calloutCode, setCalloutCode] = React.useState([]);
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_receipt_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors, setCalloutCode });
}
setSubmitting(false);
};
createNotifyReceiptBySMSMutate([receiptId, values])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
// Initial values.
const initialValues = React.useMemo(
() => ({
...receiptSMSDetail,
notification_key: notificationType.key,
}),
[receiptSMSDetail],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyReceiptViaSMSForm);

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { DialogContent } from 'components';
import { useCreateNotifyReceiptBySMS, useReceiptSMSDetail } from 'hooks/query';
const NotifyReceiptViaSMSContext = React.createContext();
/**
*
*/
function NotifyReceiptViaSMSFormProvider({ receiptId, dialogName, ...props }) {
// Create notfiy receipt via SMS mutations.
const { mutateAsync: createNotifyReceiptBySMSMutate } =
useCreateNotifyReceiptBySMS();
// Retrieve the receipt SMS notification details.
const { data: receiptSMSDetail, isLoading: isReceiptSMSDetailLoading } =
useReceiptSMSDetail(receiptId, {
enabled: !!receiptId,
});
// State provider.
const provider = {
receiptId,
dialogName,
receiptSMSDetail,
createNotifyReceiptBySMSMutate,
};
return (
<DialogContent isLoading={isReceiptSMSDetailLoading}>
<NotifyReceiptViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useNotifyReceiptViaSMSContext = () =>
React.useContext(NotifyReceiptViaSMSContext);
export { NotifyReceiptViaSMSFormProvider, useNotifyReceiptViaSMSContext };

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const NotifyReceiptViaSMSDialogContent = React.lazy(() =>
import('./NotifyReceiptViaSMSDialogContent'),
);
function NotifyReceiptViaSMSDialog({
dialogName,
payload: { receiptId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyReceiptViaSMSDialogContent
dialogName={dialogName}
receipt={receiptId}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(NotifyReceiptViaSMSDialog);

View File

@@ -0,0 +1,23 @@
import React from 'react';
import '../../../style/pages/SMSMessage/SMSMessage.scss';
import { SMSMessageDialogProvider } from './SMSMessageDialogProvider';
import SMSMessageForm from './SMSMessageForm';
/**
* SMS message dialog content.
*/
export default function SMSMessageDialogContent({
// #ownProps
dialogName,
notificationkey,
}) {
return (
<SMSMessageDialogProvider
dialogName={dialogName}
notificationkey={notificationkey}
>
<SMSMessageForm />
</SMSMessageDialogProvider>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { DialogContent } from 'components';
import {
useSettingEditSMSNotification,
useSettingSMSNotification,
} from 'hooks/query';
const SMSMessageDialogContext = React.createContext();
/**
* SMS Message dialog provider.
*/
function SMSMessageDialogProvider({ notificationkey, dialogName, ...props }) {
// Edit SMS message notification mutations.
const { mutateAsync: editSMSNotificationMutate } =
useSettingEditSMSNotification();
// SMS notificiation details
const { data: smsNotification, isLoading: isSMSNotificationLoading } =
useSettingSMSNotification(notificationkey);
// provider.
const provider = {
dialogName,
smsNotification,
editSMSNotificationMutate,
};
return (
<DialogContent isLoading={isSMSNotificationLoading}>
<SMSMessageDialogContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useSMSMessageDialogContext = () =>
React.useContext(SMSMessageDialogContext);
export { SMSMessageDialogProvider, useSMSMessageDialogContext };

View File

@@ -0,0 +1,80 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Formik } from 'formik';
import { omit } from 'lodash';
import { Intent } from '@blueprintjs/core';
import { AppToaster } from 'components';
import SMSMessageFormContent from './SMSMessageFormContent';
import { CreateSMSMessageFormSchema } from './SMSMessageForm.schema';
import { useSMSMessageDialogContext } from './SMSMessageDialogProvider';
import { transformErrors } from './utils';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose, transformToForm } from 'utils';
const defaultInitialValues = {
notification_key: '',
is_notification_enabled: '',
message_text: '',
};
/**
* SMS Message form.
*/
function SMSMessageForm({
// #withDialogActions
closeDialog,
}) {
const { dialogName, smsNotification, editSMSNotificationMutate } =
useSMSMessageDialogContext();
// Initial form values.
const initialValues = {
...defaultInitialValues,
...transformToForm(smsNotification, defaultInitialValues),
notification_key: smsNotification.key,
message_text: smsNotification.sms_message,
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const form = {
...omit(values, ['is_notification_enabled', 'sms_message']),
notification_key: smsNotification.key,
};
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('sms_message.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
if (errors) {
transformErrors(errors, { setErrors });
}
setSubmitting(false);
};
editSMSNotificationMutate(form).then(onSuccess).catch(onError);
};
return (
<Formik
validationSchema={CreateSMSMessageFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
component={SMSMessageFormContent}
/>
);
}
export default compose(withDialogActions)(SMSMessageForm);

View File

@@ -0,0 +1,11 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
notification_key: Yup.string().required(),
is_notification_enabled: Yup.boolean(),
message_text: Yup.string().min(3).max(DATATYPES_LENGTH.TEXT),
});
export const CreateSMSMessageFormSchema = Schema;

View File

@@ -0,0 +1,119 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Form, useFormikContext } from 'formik';
import styled from 'styled-components';
import { Classes } from '@blueprintjs/core';
import { castArray } from 'lodash';
import SMSMessageFormFields from './SMSMessageFormFields';
import SMSMessageFormFloatingActions from './SMSMessageFormFloatingActions';
import { useSMSMessageDialogContext } from './SMSMessageDialogProvider';
import { SMSMessagePreview } from 'components';
import { getSMSUnits } from '../../NotifyViaSMS/utils';
import { whenRtl, whenLtr } from 'utils/styled-components';
/**
* SMS message form content.
*/
export default function SMSMessageFormContent() {
// SMS message dialog context.
const { smsNotification } = useSMSMessageDialogContext();
// Ensure always returns array.
const messageVariables = React.useMemo(
() => castArray(smsNotification.allowed_variables),
[smsNotification.allowed_variables],
);
return (
<Form>
<div className={Classes.DIALOG_BODY}>
<FormContent>
<FormFields>
<SMSMessageFormFields />
<SMSMessageVariables>
{messageVariables.map(({ variable, description }) => (
<MessageVariable>
<strong>{`{${variable}}`}</strong> {description}
</MessageVariable>
))}
</SMSMessageVariables>
</FormFields>
<FormPreview>
<SMSMessagePreviewSection />
</FormPreview>
</FormContent>
</div>
<SMSMessageFormFloatingActions />
</Form>
);
}
/**
* SMS Message preview section.
* @returns {JSX}
*/
function SMSMessagePreviewSection() {
const {
values: { message_text: message },
} = useFormikContext();
const messagesUnits = getSMSUnits(message);
return (
<SMSPreviewSectionRoot>
<SMSMessagePreview message={message} />
<SMSPreviewSectionNote>
{intl.formatHTMLMessage(
{ id: 'sms_message.dialog.sms_note' },
{
value: messagesUnits,
},
)}
</SMSPreviewSectionNote>
</SMSPreviewSectionRoot>
);
}
const SMSPreviewSectionRoot = styled.div``;
const SMSPreviewSectionNote = styled.div`
font-size: 12px;
opacity: 0.7;
`;
const SMSMessageVariables = styled.div`
list-style: none;
font-size: 12px;
opacity: 0.9;
`;
const MessageVariable = styled.div`
margin-bottom: 8px;
`;
const FormContent = styled.div`
display: flex;
`;
const FormFields = styled.div`
width: 55%;
`;
const FormPreview = styled.div`
display: flex;
flex-direction: column;
width: 45%;
${whenLtr(`
padding-left: 25px;
margin-left: 25px;
border-left: 1px solid #dcdcdd;
`)}
${whenRtl(`
padding-right: 25px;
margin-right: 25px;
border-right: 1px solid #dcdcdd;
`)}
`;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import styled from 'styled-components';
import { useFormikContext, FastField, ErrorMessage } from 'formik';
import { Intent, Button, FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { useSMSMessageDialogContext } from './SMSMessageDialogProvider';
import { inputIntent } from 'utils';
/**
*
*/
export default function SMSMessageFormFields() {
// SMS message dialog context.
const { smsNotification } = useSMSMessageDialogContext();
// Form formik context.
const { setFieldValue } = useFormikContext();
// Handle the button click.
const handleBtnClick = () => {
setFieldValue('message_text', smsNotification.default_sms_message);
};
return (
<div>
{/* ----------- Message Text ----------- */}
<FastField name={'message_text'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'notify_via_sms.dialog.message_text'} />}
className={'form-group--message_text'}
intent={inputIntent({ error, touched })}
helperText={
<>
<ErrorMessage name={'message_text'} />
<ResetButton
minimal={true}
small={true}
intent={Intent.PRIMARY}
onClick={handleBtnClick}
>
<T id={'sms_message.edit_form.reset_to_default_message'} />
</ResetButton>
</>
}
>
<TextArea
growVertically={true}
large={true}
intent={inputIntent({ error, touched })}
{...field}
/>
</FormGroup>
)}
</FastField>
</div>
);
}
const ResetButton = styled(Button)`
font-size: 12px;
`;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { DialogFooterActions, FormattedMessage as T } from 'components';
import { useSMSMessageDialogContext } from './SMSMessageDialogProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
/**
* SMS Message Form floating actions.
*/
function SMSMessageFormFloatingActions({
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
// SMS Message dialog contxt.
const { dialogName } = useSMSMessageDialogContext();
// Handle close button click.
const handleCancelBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<DialogFooterActions alignment={'left'}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
<T id={'save_sms_message'} />
</Button>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
<T id={'cancel'} />
</Button>
</DialogFooterActions>
</div>
);
}
export default compose(withDialogActions)(SMSMessageFormFloatingActions);

View File

@@ -0,0 +1,39 @@
import React from 'react';
import intl from 'react-intl-universal';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'redux';
const SMSMessageDialogContent = React.lazy(() =>
import('./SMSMessageDialogContent'),
);
/**
* SMS Message dialog.
*/
function SMSMessageDialog({
dialogName,
payload: { notificationkey },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={intl.get('sms_message.dialog.label')}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--sms-message'}
>
<DialogSuspense>
<SMSMessageDialogContent
dialogName={dialogName}
notificationkey={notificationkey}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(SMSMessageDialog);

View File

@@ -0,0 +1,19 @@
import { Intent } from '@blueprintjs/core';
import { castArray } from 'lodash';
export const transformErrors = (errors, { setErrors }) => {
let unsupportedVariablesError = errors.find(
(error) => error.type === 'UNSUPPORTED_SMS_MESSAGE_VARIABLES',
);
if (unsupportedVariablesError) {
const variables = castArray(
unsupportedVariablesError.data.unsupported_args,
);
const stringifiedVariables = variables.join(', ');
setErrors({
message_text: `The SMS message has unsupported variables - ${stringifiedVariables}`,
intent: Intent.DANGER,
});
}
};

View File

@@ -15,7 +15,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions'; import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { Icon, FormattedMessage as T } from 'components'; import { Icon, FormattedMessage as T, MoreMenuItems } from 'components';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -51,6 +51,10 @@ function EstimateDetailActionsBar({
const handlePrintEstimate = () => { const handlePrintEstimate = () => {
openDialog('estimate-pdf-preview', { estimateId }); openDialog('estimate-pdf-preview', { estimateId });
}; };
// Handle notify via SMS.
const handleNotifyViaSMS = () => {
openDialog('notify-estimate-via-sms', { estimateId });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
@@ -75,6 +79,12 @@ function EstimateDetailActionsBar({
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handleDeleteEstimate} onClick={handleDeleteEstimate}
/> />
<NavbarDivider />
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );

View File

@@ -6,28 +6,17 @@ import {
NavbarGroup, NavbarGroup,
Classes, Classes,
NavbarDivider, NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
Intent, Intent,
MenuItem,
Menu,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider'; import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
import { moreVertOptions } from '../../../common/moreVertOptions';
import withDialogActions from 'containers/Dialog/withDialogActions'; import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions'; import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { import { If, Icon, FormattedMessage as T } from 'components';
If,
Icon,
FormattedMessage as T,
// MoreVertMenutItems,
} from 'components';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -76,6 +65,10 @@ function InvoiceDetailActionsBar({
const handleBadDebtInvoice = () => { const handleBadDebtInvoice = () => {
openDialog('write-off-bad-debt', { invoiceId }); openDialog('write-off-bad-debt', { invoiceId });
}; };
// Handle notify via SMS.
const handleNotifyViaSMS = () => {
openDialog('notify-invoice-via-sms', { invoiceId });
};
// Handle cancele write-off invoice. // Handle cancele write-off invoice.
const handleCancelBadDebtInvoice = () => { const handleCancelBadDebtInvoice = () => {
@@ -116,9 +109,11 @@ function InvoiceDetailActionsBar({
/> />
<NavbarDivider /> <NavbarDivider />
<BadDebtMenuItem <BadDebtMenuItem
invoice={invoice} payload={{
onAlert={handleCancelBadDebtInvoice} onBadDebt: handleBadDebtInvoice,
onDialog={handleBadDebtInvoice} onCancelBadDebt: handleCancelBadDebtInvoice,
onNotifyViaSMS: handleNotifyViaSMS,
}}
/> />
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>

View File

@@ -10,6 +10,7 @@ import {
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Icon, FormattedMessage as T, Choose } from 'components'; import { Icon, FormattedMessage as T, Choose } from 'components';
import { FormatNumberCell } from '../../../components'; import { FormatNumberCell } from '../../../components';
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
/** /**
* Retrieve invoice readonly details table columns. * Retrieve invoice readonly details table columns.
@@ -58,7 +59,11 @@ export const useInvoiceReadonlyEntriesColumns = () =>
[], [],
); );
export const BadDebtMenuItem = ({ invoice, onDialog, onAlert }) => { export const BadDebtMenuItem = ({
payload: { onCancelBadDebt, onBadDebt, onNotifyViaSMS },
}) => {
const { invoice } = useInvoiceDetailDrawerContext();
return ( return (
<Popover <Popover
minimal={true} minimal={true}
@@ -73,16 +78,20 @@ export const BadDebtMenuItem = ({ invoice, onDialog, onAlert }) => {
<Choose.When condition={!invoice.is_writtenoff}> <Choose.When condition={!invoice.is_writtenoff}>
<MenuItem <MenuItem
text={<T id={'bad_debt.dialog.bad_debt'} />} text={<T id={'bad_debt.dialog.bad_debt'} />}
onClick={onDialog} onClick={onBadDebt}
/> />
</Choose.When> </Choose.When>
<Choose.When condition={invoice.is_writtenoff}> <Choose.When condition={invoice.is_writtenoff}>
<MenuItem <MenuItem
onClick={onAlert} onClick={onCancelBadDebt}
text={<T id={'bad_debt.dialog.cancel_bad_debt'} />} text={<T id={'bad_debt.dialog.cancel_bad_debt'} />}
/> />
</Choose.When> </Choose.When>
</Choose> </Choose>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
</Menu> </Menu>
} }
> >

View File

@@ -16,7 +16,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions'; import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { Icon, FormattedMessage as T } from 'components'; import { Icon, FormattedMessage as T, MoreMenuItems } from 'components';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -29,6 +29,9 @@ function PaymentReceiveActionsBar({
// #withDrawerActions // #withDrawerActions
closeDrawer, closeDrawer,
// #withDialogActions
openDialog,
}) { }) {
const history = useHistory(); const history = useHistory();
@@ -46,6 +49,11 @@ function PaymentReceiveActionsBar({
openAlert('payment-receive-delete', { paymentReceiveId }); openAlert('payment-receive-delete', { paymentReceiveId });
}; };
// Handle notify via SMS.
const handleNotifyViaSMS = () => {
openDialog('notify-payment-via-sms', { paymentReceiveId });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -63,6 +71,12 @@ function PaymentReceiveActionsBar({
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handleDeletePaymentReceive} onClick={handleDeletePaymentReceive}
/> />
<NavbarDivider />
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );

View File

@@ -0,0 +1,25 @@
import React from 'react';
import {
DrawerHeaderContent,
DrawerBody,
FormattedMessage as T,
} from 'components';
import QuickCustomerFormDrawer from './QuickCustomerFormDrawer';
/**
* Quick create/edit customer drawer.
*/
export default function QuickCreateCustomerDrawerContent({ displayName }) {
return (
<React.Fragment>
<DrawerHeaderContent
name="quick-create-customer"
title={<T id={'create_a_new_customer'} />}
/>
<DrawerBody>
<QuickCustomerFormDrawer displayName={displayName} />
</DrawerBody>
</React.Fragment>
);
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import * as R from 'ramda';
import styled from 'styled-components';
import { Card, DrawerLoading } from 'components';
import {
CustomerFormProvider,
useCustomerFormContext,
} from '../../Customers/CustomerForm/CustomerFormProvider';
import CustomerFormFormik from '../../Customers/CustomerForm/CustomerFormFormik';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
/**
* Drawer customer form loading wrapper.
* @returns {JSX}
*/
function DrawerCustomerFormLoading({ children }) {
const { isFormLoading } = useCustomerFormContext();
return <DrawerLoading loading={isFormLoading}>{children}</DrawerLoading>;
}
/**
* Quick customer form of the drawer.
*/
function QuickCustomerFormDrawer({ displayName, closeDrawer, customerId }) {
// Handle the form submit request success.
const handleSubmitSuccess = () => {
closeDrawer('quick-create-customer');
};
// Handle the form cancel action.
const handleCancelForm = () => {
closeDrawer('quick-create-customer');
};
return (
<CustomerFormProvider customerId={customerId}>
<DrawerCustomerFormLoading>
<CustomerFormCard>
<CustomerFormFormik
initialValues={{ display_name: displayName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm}
/>
</CustomerFormCard>
</DrawerCustomerFormLoading>
</CustomerFormProvider>
);
}
export default R.compose(withDrawerActions)(QuickCustomerFormDrawer);
const CustomerFormCard = styled(Card)`
margin: 15px;
margin-bottom: calc(15px + 65px);
.page-form {
&__floating-actions {
margin-left: -36px;
margin-right: -36px;
}
}
`;

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const QuickCreateCustomerDrawerContent = React.lazy(() =>
import('./QuickCreateCustomerDrawerContent'),
);
/**
* Quick Create customer
*/
function QuickCreateCustomerDrawer({
name,
// #withDrawer
isOpen,
payload,
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'80%'}
>
<DrawerSuspense>
<QuickCreateCustomerDrawerContent displayName={payload.displayName} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(QuickCreateCustomerDrawer);

View File

@@ -0,0 +1,25 @@
import React from 'react';
import {
DrawerHeaderContent,
DrawerBody,
FormattedMessage as T,
} from 'components';
import QuickCreateItemDrawerForm from './QuickCreateItemDrawerForm';
/**
* Quick create/edit item drawer content.
*/
export default function QuickCreateItemDrawerContent({ itemName }) {
return (
<React.Fragment>
<DrawerHeaderContent
name="quick-create-item"
title={<T id={'create_a_new_item'} />}
/>
<DrawerBody>
<QuickCreateItemDrawerForm itemName={itemName} />
</DrawerBody>
</React.Fragment>
);
}

View File

@@ -0,0 +1,86 @@
import React from 'react';
import * as R from 'ramda';
import styled from 'styled-components';
import { Card, DrawerLoading } from 'components';
import ItemFormFormik from '../../Items/ItemFormFormik';
import {
ItemFormProvider,
useItemFormContext,
} from '../../Items/ItemFormProvider';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import withDashboardActions from '../../Dashboard/withDashboardActions';
import { useDrawerContext } from 'components/Drawer/DrawerProvider';
/**
* Drawer item form loading.
* @returns {JSX}
*/
function DrawerItemFormLoading({ children }) {
const { isFormLoading } = useItemFormContext();
return <DrawerLoading loading={isFormLoading}>{children}</DrawerLoading>;
}
/**
* Quick create/edit item drawer form.
*/
function QuickCreateItemDrawerForm({
itemId,
itemName,
closeDrawer,
// #withDashboardActions
addQuickActionEvent,
}) {
// Drawer context.
const { payload } = useDrawerContext();
// Handle the form submit request success.
const handleSubmitSuccess = (values, form, submitPayload, response) => {
if (submitPayload.redirect) {
closeDrawer('quick-create-item');
}
if (payload.quickActionEvent) {
addQuickActionEvent(payload.quickActionEvent, {
itemId: response.data.id,
});
}
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDrawer('quick-create-item');
};
return (
<ItemFormProvider itemId={itemId}>
<DrawerItemFormLoading>
<ItemFormCard>
<ItemFormFormik
initialValues={{ name: itemName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</ItemFormCard>
</DrawerItemFormLoading>
</ItemFormProvider>
);
}
export default R.compose(
withDrawerActions,
withDashboardActions,
)(QuickCreateItemDrawerForm);
const ItemFormCard = styled(Card)`
margin: 15px;
margin-bottom: calc(15px + 65px);
.page-form__floating-actions {
margin-left: -36px;
margin-right: -36px;
}
`;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
import { compose } from 'utils';
const QuickCretaeItemDrawerContent = React.lazy(() =>
import('./QuickCreateItemDrawerContent'),
);
/**
* Quick create item.
*/
function QuickCreateItemDrawer({
// #ownProps
name,
// #withDrawer
isOpen,
payload,
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '800px', maxWidth: '1000px' }}
size={'72%'}
payload={payload}
>
<DrawerSuspense>
<QuickCretaeItemDrawerContent itemName={payload.name} />
</DrawerSuspense>
</Drawer>
);
}
export default compose(withDrawers())(QuickCreateItemDrawer);

View File

@@ -0,0 +1,85 @@
import React from 'react';
import * as R from 'ramda';
import styled from 'styled-components';
import { Card, DrawerLoading } from 'components';
import {
VendorFormProvider,
useVendorFormContext,
} from '../../Vendors/VendorForm/VendorFormProvider';
import VendorFormFormik from '../../Vendors/VendorForm/VendorFormFormik';
import withDrawerActions from 'containers/Drawer/withDrawerActions';
import withDashboardActions from '../../Dashboard/withDashboardActions';
import { useDrawerContext } from 'components/Drawer/DrawerProvider';
/**
* Drawer vendor form loading wrapper.
* @returns {JSX}
*/
function DrawerVendorFormLoading({ children }) {
const { isFormLoading } = useVendorFormContext();
return <DrawerLoading loading={isFormLoading}>{children}</DrawerLoading>;
}
/**
* Quick vendor form of the drawer.
*/
function QuickVendorFormDrawer({
displayName,
closeDrawer,
vendorId,
addQuickActionEvent,
}) {
const { payload } = useDrawerContext();
// Handle the form submit request success.
const handleSubmitSuccess = (values, form, submitPayload, response) => {
if (!submitPayload.noRedirect) {
closeDrawer('quick-write-vendor');
}
if (payload.quickActionEvent) {
addQuickActionEvent(payload.quickActionEvent, {
vendorId: response.data.id,
});
}
};
// Handle the form cancel action.
const handleCancelForm = () => {
closeDrawer('quick-write-vendor');
};
return (
<VendorFormProvider vendorId={vendorId}>
<DrawerVendorFormLoading>
<VendorFormCard>
<VendorFormFormik
initialValues={{ display_name: displayName }}
onSubmitSuccess={handleSubmitSuccess}
onCancel={handleCancelForm}
/>
</VendorFormCard>
</DrawerVendorFormLoading>
</VendorFormProvider>
);
}
export default R.compose(
withDrawerActions,
withDashboardActions,
)(QuickVendorFormDrawer);
const VendorFormCard = styled(Card)`
margin: 15px;
margin-bottom: calc(15px + 65px);
.page-form {
&__floating-actions {
margin-left: -36px;
margin-right: -36px;
}
}
`;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import {
DrawerHeaderContent,
DrawerBody,
FormattedMessage as T,
} from 'components';
import QuickVendorFormDrawer from './QuickVendorFormDrawer';
/**
* Quick create/edit vendor drawer.
*/
export default function QuickWriteVendorDrawerContent({ displayName }) {
return (
<React.Fragment>
<DrawerHeaderContent
name="quick-create-customer"
title={"Create a new vendor"}
/>
<DrawerBody>
<QuickVendorFormDrawer displayName={displayName} />
</DrawerBody>
</React.Fragment>
);
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import * as R from 'ramda';
import { Drawer, DrawerSuspense } from 'components';
import withDrawers from 'containers/Drawer/withDrawers';
const QuickWriteVendorDrawerContent = React.lazy(() =>
import('./QuickWriteVendorDrawerContent'),
);
/**
* Quick Write vendor.
*/
function QuickWriteVendorDrawer({
name,
// #withDrawer
isOpen,
payload,
}) {
return (
<Drawer
isOpen={isOpen}
name={name}
style={{ minWidth: '700px', maxWidth: '900px' }}
size={'80%'}
payload={payload}
>
<DrawerSuspense>
<QuickWriteVendorDrawerContent displayName={payload.displayName} />
</DrawerSuspense>
</Drawer>
);
}
export default R.compose(withDrawers())(QuickWriteVendorDrawer);

View File

@@ -14,7 +14,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
import withAlertsActions from 'containers/Alert/withAlertActions'; import withAlertsActions from 'containers/Alert/withAlertActions';
import withDrawerActions from 'containers/Drawer/withDrawerActions'; import withDrawerActions from 'containers/Drawer/withDrawerActions';
import { Icon, FormattedMessage as T } from 'components'; import { Icon, FormattedMessage as T, MoreMenuItems } from 'components';
import { useReceiptDetailDrawerContext } from './ReceiptDetailDrawerProvider'; import { useReceiptDetailDrawerContext } from './ReceiptDetailDrawerProvider';
import { safeCallback, compose } from 'utils'; import { safeCallback, compose } from 'utils';
@@ -46,6 +46,11 @@ function ReceiptDetailActionBar({
const onPrintReceipt = () => { const onPrintReceipt = () => {
openDialog('receipt-pdf-preview', { receiptId }); openDialog('receipt-pdf-preview', { receiptId });
}; };
// Handle notify via SMS.
const handleNotifyViaSMS = () => {
openDialog('notify-receipt-via-sms', { receiptId });
};
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
@@ -69,6 +74,12 @@ function ReceiptDetailActionBar({
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={safeCallback(onDeleteReceipt)} onClick={safeCallback(onDeleteReceipt)}
/> />
<NavbarDivider />
<MoreMenuItems
payload={{
onNotifyViaSMS: handleNotifyViaSMS,
}}
/>
</NavbarGroup> </NavbarGroup>
</DashboardActionsBar> </DashboardActionsBar>
); );

View File

@@ -108,7 +108,9 @@ const LandedCostHeaderCell = () => {
/** /**
* Retrieve editable items entries columns. * Retrieve editable items entries columns.
*/ */
export function useEditableItemsEntriesColumns({ landedCost }) { export function useEditableItemsEntriesColumns({
landedCost,
}) {
return React.useMemo( return React.useMemo(
() => [ () => [
{ {
@@ -129,6 +131,7 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
disableSortBy: true, disableSortBy: true,
width: 130, width: 130,
className: 'item', className: 'item',
fieldProps: { allowCreate: true },
}, },
{ {
Header: intl.get('description'), Header: intl.get('description'),

View File

@@ -164,3 +164,12 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
updateTableRow(rowIndex, newRow), updateTableRow(rowIndex, newRow),
)(rows); )(rows);
}); });
/**
*
* @param {*} entries
* @returns
*/
export const composeControlledEntries = (entries) => {
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
};

View File

@@ -14,7 +14,7 @@ import {
import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils'; import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils';
import { import {
CurrencySelectList, CurrencySelectList,
ContactSelecetList, CustomerSelectField,
AccountsSelectList, AccountsSelectList,
FieldRequiredHint, FieldRequiredHint,
Hint, Hint,
@@ -78,6 +78,7 @@ export default function ExpenseFormHeader() {
defaultSelectText={<T id={'select_payment_account'} />} defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={value} selectedAccountId={value}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]} filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}
@@ -137,13 +138,14 @@ export default function ExpenseFormHeader() {
helperText={<ErrorMessage name={'assign_to_customer'} />} helperText={<ErrorMessage name={'assign_to_customer'} />}
inline={true} inline={true}
> >
<ContactSelecetList <CustomerSelectField
contactsList={customers} contacts={customers}
selectedContactId={value} selectedContactId={value}
defaultSelectText={<T id={'select_customer_account'} />} defaultSelectText={<T id={'select_customer_account'} />}
onContactSelected={(customer) => { onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id); form.setFieldValue('customer_id', customer.id);
}} }}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -113,6 +113,7 @@ export function useExpenseFormTableColumns({ landedCost }) {
disableSortBy: true, disableSortBy: true,
width: 40, width: 40,
filterAccountsByRootTypes: ['expense'], filterAccountsByRootTypes: ['expense'],
fieldProps: { allowCreate: true },
}, },
{ {
Header: ExpenseAmountHeaderCell, Header: ExpenseAmountHeaderCell,

View File

@@ -27,7 +27,7 @@ export function BalanceSheetAlerts() {
<Icon icon="info-block" iconSize={12} />{' '} <Icon icon="info-block" iconSize={12} />{' '}
<T id={'just_a_moment_we_re_calculating_your_cost_transactions'} /> <T id={'just_a_moment_we_re_calculating_your_cost_transactions'} />
<Button onClick={handleRecalcReport} minimal={true} small={true}> <Button onClick={handleRecalcReport} minimal={true} small={true}>
<T id={'refresh'} /> <T id={'report.compute_running.refresh'} />
</Button> </Button>
</div> </div>
</If> </If>

View File

@@ -25,7 +25,7 @@ export default function CustomersBalanceSummaryTable({
const columns = useCustomersSummaryColumns(); const columns = useCustomersSummaryColumns();
const rowClassNames = (row) => { const rowClassNames = (row) => {
return [`row-type--${row.original.rowTypes}`]; return [`row-type--${row.original.row_types}`];
}; };
return ( return (

View File

@@ -31,7 +31,7 @@ export default function CustomersTransactionsTable({
]); ]);
const rowClassNames = (row) => { const rowClassNames = (row) => {
return [`row-type--${row.original.rowTypes}`]; return [`row-type--${row.original.row_types}`];
}; };
return ( return (

View File

@@ -22,14 +22,14 @@ export const useCustomersTransactionsColumns = () => {
return ( return (
<span <span
className={'force-width'} className={'force-width'}
style={{ minWidth: getForceWidth(cells[0].key) }} style={{ minWidth: getForceWidth(cells[0].value) }}
> >
{cells[0].value} {cells[0].value}
</span> </span>
); );
}, },
className: 'customer_name', className: 'customer_name',
textOverview: true, // textOverview: true,
}, },
{ {
Header: intl.get('account_name'), Header: intl.get('account_name'),

View File

@@ -35,7 +35,7 @@ export function useGeneralLedgerTableColumns() {
return row.date; return row.date;
}, },
className: 'date', className: 'date',
textOverview: true, // textOverview: true,
width: 120, width: 120,
}, },
{ {

View File

@@ -29,7 +29,7 @@ export default function InventoryItemDetailsTable({
); );
const rowClassNames = (row) => { const rowClassNames = (row) => {
return [`row-type--${row.original.rowTypes}`]; return [`row-type--${row.original.row_types}`];
}; };
return ( return (

View File

@@ -9,7 +9,7 @@ const columnsMapper = (data, index, column) => ({
id: column.key, id: column.key,
key: column.key, key: column.key,
Header: column.label, Header: column.label,
// Cell: CellForceWidth, Cell: CellForceWidth,
accessor: `cells[${index}].value`, accessor: `cells[${index}].value`,
forceWidthAccess: `cells[0].value`, forceWidthAccess: `cells[0].value`,
className: column.key, className: column.key,

View File

@@ -25,7 +25,7 @@ export default function VendorsBalanceSummaryTable({
const columns = useVendorsBalanceColumns(); const columns = useVendorsBalanceColumns();
const rowClassNames = (row) => { const rowClassNames = (row) => {
return [`row-type--${row.original.rowTypes}`]; return [`row-type--${row.original.row_types}`];
}; };
return ( return (

View File

@@ -32,7 +32,7 @@ export default function VendorsTransactionsTable({
]); ]);
const rowClassNames = (row) => { const rowClassNames = (row) => {
return [`row-type--${row.original.rowTypes}`]; return [`row-type--${row.original.row_types}`];
}; };
return ( return (

View File

@@ -21,14 +21,14 @@ export const useVendorsTransactionsColumns = () => {
return ( return (
<span <span
className={'force-width'} className={'force-width'}
style={{ minWidth: getForceWidth(cells[0].key) }} style={{ minWidth: getForceWidth(cells[0].value) }}
> >
{cells[0].value} {cells[0].value}
</span> </span>
); );
}, },
className: 'vendor_name', className: 'vendor_name',
textOverview: true, // textOverview: true,
// width: 240, // width: 240,
}, },
{ {

View File

@@ -1,110 +1,95 @@
import React from 'react'; import React from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames'; import { useHistory } from 'react-router-dom';
import styled from 'styled-components';
import 'style/pages/Items/PageForm.scss'; import { useDashboardPageTitle } from 'hooks/state';
import { useItemFormContext, ItemFormProvider } from './ItemFormProvider';
import { CLASSES } from 'common/classes'; import ItemFormFormik from './ItemFormFormik';
import AppToaster from 'components/AppToaster';
import ItemFormPrimarySection from './ItemFormPrimarySection';
import ItemFormBody from './ItemFormBody';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import { import DashboardCard from 'components/Dashboard/DashboardCard';
transformSubmitRequestErrors, import DashboardInsider from 'components/Dashboard/DashboardInsider';
useItemFormInitialValues,
} from './utils';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import { useItemFormContext } from './ItemFormProvider';
/** /**
* Item form. * Item form dashboard title.
* @returns {null}
*/ */
export default function ItemForm() { function ItemFormDashboardTitle() {
// Item form context. // Change page title dispatcher.
const { const changePageTitle = useDashboardPageTitle();
itemId,
item,
accounts,
createItemMutate,
editItemMutate,
submitPayload,
isNewMode,
} = useItemFormContext();
// Item form context.
const { isNewMode } = useItemFormContext();
// Changes the page title in new and edit mode.
React.useEffect(() => {
isNewMode
? changePageTitle(intl.get('new_item'))
: changePageTitle(intl.get('edit_item_details'));
}, [changePageTitle, isNewMode]);
return null;
}
/**
* Item form page loading state indicator.
* @returns {JSX}
*/
function ItemFormPageLoading({ children }) {
const { isFormLoading } = useItemFormContext();
return (
<DashboardItemFormPageInsider loading={isFormLoading} name={'item-form'}>
{children}
</DashboardItemFormPageInsider>
);
}
/**
* Item form of the page.
* @returns {JSX}
*/
export default function ItemForm({ itemId }) {
// History context. // History context.
const history = useHistory(); const history = useHistory();
// Initial values in create and edit mode. // Handle the form submit success.
const initialValues = useItemFormInitialValues(item); const handleSubmitSuccess = (values, form, submitPayload) => {
if (submitPayload.redirect) {
// Handles the form submit. history.push('/items');
const handleFormSubmit = (
values,
{ setSubmitting, resetForm, setErrors },
) => {
setSubmitting(true);
const form = { ...values };
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_item_has_been_created_successfully'
: 'the_item_has_been_edited_successfully',
{
number: itemId,
},
),
intent: Intent.SUCCESS,
});
resetForm();
setSubmitting(false);
// Submit payload.
if (submitPayload.redirect) {
history.push('/items');
}
};
// Handle response error.
const onError = (errors) => {
setSubmitting(false);
if (errors) {
const _errors = transformSubmitRequestErrors(errors);
setErrors({ ..._errors });
}
};
if (isNewMode) {
createItemMutate(form).then(onSuccess).catch(onError);
} else {
editItemMutate([itemId, form]).then(onSuccess).catch(onError);
} }
}; };
// Handle cancel button click.
const handleFormCancel = () => {
history.goBack();
};
return ( return (
<div class={classNames(CLASSES.PAGE_FORM_ITEM)}> <ItemFormProvider itemId={itemId}>
<Formik <ItemFormDashboardTitle />
enableReinitialize={true}
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions /> <ItemFormPageLoading>
</Form> <DashboardCard page>
</Formik> <ItemFormPageFormik
</div> onSubmitSuccess={handleSubmitSuccess}
onCancel={handleFormCancel}
/>
</DashboardCard>
</ItemFormPageLoading>
</ItemFormProvider>
); );
} }
const DashboardItemFormPageInsider = styled(DashboardInsider)`
padding-bottom: 64px;
`;
const ItemFormPageFormik = styled(ItemFormFormik)`
.page-form {
&__floating-actions {
margin-left: -40px;
margin-right: -40px;
}
}
`;

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useFormikContext, FastField, Field, ErrorMessage } from 'formik'; import { useFormikContext, FastField, ErrorMessage } from 'formik';
import { import {
FormGroup, FormGroup,
Classes, Classes,
@@ -122,6 +122,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.sellable} disabled={!form.values.sellable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]} filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}
@@ -230,6 +231,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.purchasable} disabled={!form.values.purchasable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]} filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true} popoverFill={true}
allowCreate={true}
/> />
</FormGroup> </FormGroup>
)} )}

View File

@@ -1,20 +1,19 @@
import React from 'react'; import React from 'react';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core'; import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FormattedMessage as T } from 'components'; import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import classNames from 'classnames';
import { FastField, useFormikContext } from 'formik'; import { FastField, useFormikContext } from 'formik';
import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes'; import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
import { saveInvoke } from '../../utils';
/** /**
* Item form floating actions. * Item form floating actions.
*/ */
export default function ItemFormFloatingActions() { export default function ItemFormFloatingActions({ onCancel }) {
// History context.
const history = useHistory();
// Item form context. // Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext(); const { setSubmitPayload, isNewMode } = useItemFormContext();
@@ -23,7 +22,7 @@ export default function ItemFormFloatingActions() {
// Handle cancel button click. // Handle cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
history.goBack(); saveInvoke(onCancel, event);
}; };
// Handle submit button click. // Handle submit button click.
@@ -38,7 +37,7 @@ export default function ItemFormFloatingActions() {
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
<Button <SaveButton
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting} loading={isSubmitting}
@@ -47,7 +46,7 @@ export default function ItemFormFloatingActions() {
className={'btn--submit'} className={'btn--submit'}
> >
{isNewMode ? <T id={'save'} /> : <T id={'edit'} />} {isNewMode ? <T id={'save'} /> : <T id={'edit'} />}
</Button> </SaveButton>
<Button <Button
className={classNames('ml1', 'btn--submit-new')} className={classNames('ml1', 'btn--submit-new')}
@@ -82,3 +81,7 @@ export default function ItemFormFloatingActions() {
</div> </div>
); );
} }
const SaveButton = styled(Button)`
min-width: 100px;
`;

View File

@@ -0,0 +1,112 @@
import React from 'react';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import 'style/pages/Items/Form.scss';
import { CLASSES } from 'common/classes';
import AppToaster from 'components/AppToaster';
import ItemFormPrimarySection from './ItemFormPrimarySection';
import ItemFormBody from './ItemFormBody';
import ItemFormFloatingActions from './ItemFormFloatingActions';
import ItemFormInventorySection from './ItemFormInventorySection';
import {
transformSubmitRequestErrors,
useItemFormInitialValues,
} from './utils';
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
import { useItemFormContext } from './ItemFormProvider';
import { safeInvoke } from '@blueprintjs/core/lib/esm/common/utils';
/**
* Item form.
*/
export default function ItemFormFormik({
// #ownProps
initialValues: initialValuesComponent,
onSubmitSuccess,
onSubmitError,
onCancel,
className,
}) {
// Item form context.
const {
itemId,
item,
accounts,
createItemMutate,
editItemMutate,
submitPayload,
isNewMode,
} = useItemFormContext();
// Initial values in create and edit mode.
const initialValues = useItemFormInitialValues(item, initialValuesComponent);
// Handles the form submit.
const handleFormSubmit = (values, form) => {
const { setSubmitting, resetForm, setErrors } = form;
const formValues = { ...values };
setSubmitting(true);
// Handle response succes.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
? 'the_item_has_been_created_successfully'
: 'the_item_has_been_edited_successfully',
{
number: itemId,
},
),
intent: Intent.SUCCESS,
});
resetForm();
setSubmitting(false);
safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
};
// Handle response error.
const onError = (errors) => {
setSubmitting(false);
if (errors) {
const _errors = transformSubmitRequestErrors(errors);
setErrors({ ..._errors });
}
safeInvoke(onSubmitError, values, form, submitPayload, errors);
};
if (isNewMode) {
createItemMutate(formValues).then(onSuccess).catch(onError);
} else {
editItemMutate([itemId, formValues]).then(onSuccess).catch(onError);
}
};
return (
<div class={classNames(CLASSES.PAGE_FORM_ITEM, className)}>
<Formik
enableReinitialize={true}
validationSchema={isNewMode ? CreateItemFormSchema : EditItemFormSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<Form>
<div class={classNames(CLASSES.PAGE_FORM_BODY)}>
<ItemFormPrimarySection />
<ItemFormBody accounts={accounts} />
<ItemFormInventorySection accounts={accounts} />
</div>
<ItemFormFloatingActions onCancel={onCancel} />
</Form>
</Formik>
</div>
);
}

View File

@@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { ItemFormProvider } from './ItemFormProvider'; import ItemForm from './ItemForm';
import DashboardCard from 'components/Dashboard/DashboardCard';
import ItemForm from 'containers/Items/ItemForm';
/** /**
* Item form page. * Item form page.
@@ -12,11 +10,5 @@ export default function ItemFormPage() {
const { id } = useParams(); const { id } = useParams();
const idInteger = parseInt(id, 10); const idInteger = parseInt(id, 10);
return ( return <ItemForm itemId={idInteger} />;
<ItemFormProvider itemId={idInteger}>
<DashboardCard page>
<ItemForm />
</DashboardCard>
</ItemFormProvider>
);
} }

View File

@@ -1,8 +1,5 @@
import React, { useEffect, createContext, useState } from 'react'; import React, { createContext, useState } from 'react';
import intl from 'react-intl-universal';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { import {
useItem, useItem,
useSettingsItems, useSettingsItems,
@@ -11,7 +8,6 @@ import {
useEditItem, useEditItem,
useAccounts, useAccounts,
} from 'hooks/query'; } from 'hooks/query';
import { useDashboardPageTitle } from 'hooks/state';
import { useWatchItemError } from './utils'; import { useWatchItemError } from './utils';
const ItemFormContext = createContext(); const ItemFormContext = createContext();
@@ -59,6 +55,13 @@ function ItemFormProvider({ itemId, ...props }) {
// Detarmines whether the form new mode. // Detarmines whether the form new mode.
const isNewMode = duplicateId || !itemId; const isNewMode = duplicateId || !itemId;
// Detarmines the form loading state.
const isFormLoading =
isItemsSettingsLoading ||
isAccountsLoading ||
isItemsCategoriesLoading ||
isItemLoading;
// Provider state. // Provider state.
const provider = { const provider = {
itemId, itemId,
@@ -68,6 +71,7 @@ function ItemFormProvider({ itemId, ...props }) {
submitPayload, submitPayload,
isNewMode, isNewMode,
isFormLoading,
isAccountsLoading, isAccountsLoading,
isItemsCategoriesLoading, isItemsCategoriesLoading,
isItemLoading, isItemLoading,
@@ -77,27 +81,7 @@ function ItemFormProvider({ itemId, ...props }) {
setSubmitPayload, setSubmitPayload,
}; };
// Change page title dispatcher. return <ItemFormContext.Provider value={provider} {...props} />;
const changePageTitle = useDashboardPageTitle();
// Changes the page title in new and edit mode.
useEffect(() => {
isNewMode
? changePageTitle(intl.get('new_item'))
: changePageTitle(intl.get('edit_item_details'));
}, [changePageTitle, isNewMode]);
const loading =
isItemsSettingsLoading ||
isAccountsLoading ||
isItemsCategoriesLoading ||
isItemLoading;
return (
<DashboardInsider loading={loading} name={'item-form'}>
<ItemFormContext.Provider value={provider} {...props} />
</DashboardInsider>
);
} }
const useItemFormContext = () => React.useContext(ItemFormContext); const useItemFormContext = () => React.useContext(ItemFormContext);

Some files were not shown because too many files have changed in this diff Show More