Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
802f7cc442 | ||
|
|
5f0700b5e5 | ||
|
|
48348da663 | ||
|
|
b32abc0417 | ||
|
|
11d7029568 | ||
|
|
1990ce7562 | ||
|
|
b6f0f6c2d5 | ||
|
|
4c58e49169 | ||
|
|
376a16fd65 | ||
|
|
918cd4aef3 | ||
|
|
ec844637c3 | ||
|
|
5803760c61 | ||
|
|
2e34df5d63 | ||
|
|
35d755e417 | ||
|
|
66641ca56e | ||
|
|
307aaf0aa4 | ||
|
|
eb5a82d413 | ||
|
|
ce9169b24d | ||
|
|
22069f4795 | ||
|
|
567b4da7e9 | ||
|
|
06345a5615 | ||
|
|
6b8178f643 | ||
|
|
449ff724e1 | ||
|
|
95e75f0e8f | ||
|
|
1a63ac69d8 | ||
|
|
da67217d74 | ||
|
|
e0c03141f0 | ||
|
|
4d563e3ddd | ||
|
|
56fdf245d3 | ||
|
|
5a8c61396f | ||
|
|
5fcf32dcaa | ||
|
|
acf457c0a0 | ||
|
|
e205c0b9a3 | ||
|
|
85f1c5584b | ||
|
|
9e5fddf294 | ||
|
|
3039e43767 | ||
|
|
7371557482 | ||
|
|
4b5e06f50c | ||
|
|
8daefb6946 | ||
|
|
6bf605f9ea | ||
|
|
48221a7af1 | ||
|
|
7a1c9caa70 | ||
|
|
8c2d138976 | ||
|
|
5b09d8279e | ||
|
|
92d8096f3a | ||
|
|
adc6b336e0 | ||
|
|
6d67d6163d | ||
|
|
4d89f1e0e0 | ||
|
|
7706d2992c | ||
|
|
6dcb98a438 | ||
|
|
834d365a97 | ||
|
|
d26ef01afc | ||
|
|
2bd4c5f724 | ||
|
|
2c71d07512 | ||
|
|
17a4744e58 | ||
|
|
46f6380fe6 |
@@ -66,6 +66,7 @@ const CLASSES = {
|
||||
PREFERENCES_PAGE_INSIDE_CONTENT_USERS: 'preferences-page__inside-content--users',
|
||||
PREFERENCES_PAGE_INSIDE_CONTENT_CURRENCIES: 'preferences-page__inside-content--currencies',
|
||||
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',
|
||||
|
||||
|
||||
@@ -10,4 +10,8 @@ export const DRAWERS = {
|
||||
BILL_DRAWER: 'bill-drawer',
|
||||
INVENTORY_ADJUSTMENT_DRAWER: 'inventory-adjustment-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',
|
||||
};
|
||||
|
||||
@@ -1,13 +1,56 @@
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { MenuItem, Button } from '@blueprintjs/core';
|
||||
import { Select } from '@blueprintjs/select';
|
||||
import { MenuItemNestedText, FormattedMessage as T } from 'components';
|
||||
import * as R from 'ramda';
|
||||
import classNames from 'classnames';
|
||||
import intl from 'react-intl-universal'
|
||||
|
||||
import { MenuItemNestedText, FormattedMessage as T } from 'components';
|
||||
import { filterAccountsByQuery } from './utils';
|
||||
import { nestedArrayToflatten } from 'utils';
|
||||
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={intl.get('list.create', { value: `"${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,
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
@@ -21,6 +64,8 @@ export default function AccountsSelectList({
|
||||
filterByNormal,
|
||||
filterByRootTypes,
|
||||
|
||||
allowCreate,
|
||||
|
||||
buttonProps = {},
|
||||
}) {
|
||||
const flattenAccounts = useMemo(
|
||||
@@ -51,6 +96,7 @@ export default function AccountsSelectList({
|
||||
[initialAccountId, filteredAccounts],
|
||||
);
|
||||
|
||||
// Select account item.
|
||||
const [selectedAccount, setSelectedAccount] = useState(
|
||||
initialAccount || null,
|
||||
);
|
||||
@@ -76,31 +122,25 @@ export default function AccountsSelectList({
|
||||
);
|
||||
}, []);
|
||||
|
||||
const onAccountSelect = useCallback(
|
||||
// Handle the account item select.
|
||||
const handleAccountSelect = useCallback(
|
||||
(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;
|
||||
if (account.id) {
|
||||
setSelectedAccount({ ...account });
|
||||
onAccountSelected && onAccountSelected(account);
|
||||
} else {
|
||||
return (
|
||||
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
|
||||
);
|
||||
openDialog('account-form');
|
||||
}
|
||||
},
|
||||
[],
|
||||
[setSelectedAccount, onAccountSelected, openDialog],
|
||||
);
|
||||
|
||||
// Maybe inject new item props to select component.
|
||||
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
|
||||
const maybeCreateNewItemFromQuery = allowCreate
|
||||
? createNewItemFromQuery
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={filteredAccounts}
|
||||
@@ -113,11 +153,13 @@ export default function AccountsSelectList({
|
||||
inline: popoverFill,
|
||||
}}
|
||||
filterable={true}
|
||||
onItemSelect={onAccountSelect}
|
||||
onItemSelect={handleAccountSelect}
|
||||
disabled={disabled}
|
||||
className={classNames('form-group--select-list', {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||
createNewItemFromQuery={maybeCreateNewItemFromQuery}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
@@ -127,3 +169,5 @@ export default function AccountsSelectList({
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDialogActions)(AccountsSelectList);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import { Suggest } from '@blueprintjs/select';
|
||||
import intl from 'react-intl-universal';
|
||||
import * as R from 'ramda';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
@@ -10,10 +11,55 @@ import { MenuItemNestedText, FormattedMessage as T } from 'components';
|
||||
import { filterAccountsByQuery } 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={intl.get('list.create', { value: `"${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.
|
||||
*/
|
||||
export default function AccountsSuggestField({
|
||||
function AccountsSuggestField({
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
|
||||
// #ownProps
|
||||
accounts,
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
@@ -26,6 +72,8 @@ export default function AccountsSuggestField({
|
||||
filterByNormal,
|
||||
filterByRootTypes = [],
|
||||
|
||||
allowCreate,
|
||||
|
||||
...suggestProps
|
||||
}) {
|
||||
const flattenAccounts = useMemo(
|
||||
@@ -69,23 +117,6 @@ export default function AccountsSuggestField({
|
||||
}
|
||||
}, [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.
|
||||
const accountItem = useCallback((item, { handleClick, modifiers, query }) => {
|
||||
return (
|
||||
@@ -98,28 +129,31 @@ export default function AccountsSuggestField({
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleInputValueRenderer = (inputValue) => {
|
||||
if (inputValue) {
|
||||
return inputValue.name.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const onAccountSelect = useCallback(
|
||||
const handleAccountSelect = useCallback(
|
||||
(account) => {
|
||||
setSelectedAccount({ ...account });
|
||||
onAccountSelected && onAccountSelected(account);
|
||||
if (account.id) {
|
||||
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 (
|
||||
<Suggest
|
||||
items={filteredAccounts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_accounts'} />} />}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccountsPredicater}
|
||||
onItemSelect={onAccountSelect}
|
||||
onItemSelect={handleAccountSelect}
|
||||
selectedItem={selectedAccount}
|
||||
inputProps={{ placeholder: defaultSelectText }}
|
||||
resetOnClose={true}
|
||||
@@ -129,7 +163,11 @@ export default function AccountsSuggestField({
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||
createNewItemFromQuery={maybeCreateNewItemFromQuery}
|
||||
{...suggestProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDialogActions)(AccountsSuggestField);
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
import { Button } from '@blueprintjs/core';
|
||||
|
||||
export const ButtonLink = styled(Button)`
|
||||
line-height: inherit;
|
||||
export const ButtonLink = styled.button`
|
||||
color: #0052cc;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
&.bp3-small {
|
||||
min-height: auto;
|
||||
min-width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
&:not([class*='bp3-intent-']) {
|
||||
&,
|
||||
&:hover {
|
||||
color: #0052cc;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -11,12 +11,14 @@ export default function ContactSelecetList({
|
||||
contactsList,
|
||||
initialContactId,
|
||||
selectedContactId,
|
||||
selectedContactType,
|
||||
createNewItemFrom,
|
||||
defaultSelectText = <T id={'select_contact'} />,
|
||||
onContactSelected,
|
||||
popoverFill = false,
|
||||
disabled = false,
|
||||
buttonProps,
|
||||
|
||||
...restProps
|
||||
}) {
|
||||
const contacts = useMemo(
|
||||
() =>
|
||||
@@ -65,7 +67,7 @@ export default function ContactSelecetList({
|
||||
);
|
||||
|
||||
// Filter Contact List
|
||||
const filterContacts = (query, contact, index, exactMatch) => {
|
||||
const itemPredicate = (query, contact, index, exactMatch) => {
|
||||
const normalizedTitle = contact.display_name.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
if (exactMatch) {
|
||||
@@ -83,7 +85,7 @@ export default function ContactSelecetList({
|
||||
items={contacts}
|
||||
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
|
||||
itemRenderer={handleContactRenderer}
|
||||
itemPredicate={filterContacts}
|
||||
itemPredicate={itemPredicate}
|
||||
filterable={true}
|
||||
disabled={disabled}
|
||||
onItemSelect={onContactSelect}
|
||||
@@ -92,8 +94,9 @@ export default function ContactSelecetList({
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
inputProps={{
|
||||
placeholder: intl.get('filter_')
|
||||
placeholder: intl.get('filter_'),
|
||||
}}
|
||||
{...restProps}
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
|
||||
86
src/components/Contacts/ContactSelectField.js
Normal file
86
src/components/Contacts/ContactSelectField.js
Normal 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>
|
||||
);
|
||||
}
|
||||
116
src/components/Contacts/CustomerSelectField.js
Normal file
116
src/components/Contacts/CustomerSelectField.js
Normal 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);
|
||||
115
src/components/Contacts/VendorSelectField.js
Normal file
115
src/components/Contacts/VendorSelectField.js
Normal 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);
|
||||
5
src/components/Contacts/index.js
Normal file
5
src/components/Contacts/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import ContactSelectField from './ContactSelectField';
|
||||
import CustomerSelectField from './CustomerSelectField';
|
||||
import VendorSelectField from './VendorSelectField';
|
||||
|
||||
export { ContactSelectField, CustomerSelectField, VendorSelectField };
|
||||
44
src/components/Contacts/utils.js
Normal file
44
src/components/Contacts/utils.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
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={intl.get('list.create', { value: `"${query}"` })}
|
||||
active={active}
|
||||
shouldDismissPopover={false}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
|
||||
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
|
||||
import DashboardFooter from 'components/Dashboard/DashboardFooter';
|
||||
import DashboardErrorBoundary from './DashboardErrorBoundary';
|
||||
|
||||
export default React.forwardRef(({}, ref) => {
|
||||
@@ -11,7 +10,6 @@ export default React.forwardRef(({}, ref) => {
|
||||
<div className="dashboard-content" id="dashboard" ref={ref}>
|
||||
<DashboardTopbar />
|
||||
<DashboardContentRoutes />
|
||||
<DashboardFooter />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,8 @@ export default function AccountCellRenderer({
|
||||
accountsDataProp,
|
||||
filterAccountsByRootTypes,
|
||||
filterAccountsByTypes,
|
||||
fieldProps,
|
||||
formGroupProps,
|
||||
},
|
||||
row: { index, original },
|
||||
cell: { value: initialValue },
|
||||
@@ -53,6 +55,7 @@ export default function AccountCellRenderer({
|
||||
'form-group--account',
|
||||
Classes.FILL,
|
||||
)}
|
||||
{...formGroupProps}
|
||||
>
|
||||
<AccountsSuggestField
|
||||
accounts={accounts}
|
||||
@@ -66,6 +69,7 @@ export default function AccountCellRenderer({
|
||||
}}
|
||||
openOnKeyDown={true}
|
||||
blurOnSelectClose={false}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
// import ItemsListField from 'components/ItemsListField';
|
||||
import ItemsSuggestField from 'components/ItemsSuggestField';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
import ItemsSuggestField from 'components/ItemsSuggestField';
|
||||
|
||||
import { useCellAutoFocus } from 'hooks';
|
||||
|
||||
/**
|
||||
* Items list cell.
|
||||
*/
|
||||
export default function ItemsListCell({
|
||||
column: { id, filterSellable, filterPurchasable },
|
||||
column: { id, filterSellable, filterPurchasable, fieldProps, formGroupProps },
|
||||
row: { index },
|
||||
cell: { value: initialValue },
|
||||
payload: { items, updateData, errors, autoFocus },
|
||||
@@ -19,6 +21,7 @@ export default function ItemsListCell({
|
||||
// Auto-focus the items list input field.
|
||||
useCellAutoFocus(fieldRef, autoFocus, id, index);
|
||||
|
||||
// Handle the item selected.
|
||||
const handleItemSelected = useCallback(
|
||||
(item) => {
|
||||
updateData(index, id, item.id);
|
||||
@@ -32,6 +35,7 @@ export default function ItemsListCell({
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames('form-group--select-list', Classes.FILL)}
|
||||
{...formGroupProps}
|
||||
>
|
||||
<ItemsSuggestField
|
||||
items={items}
|
||||
@@ -45,6 +49,7 @@ export default function ItemsListCell({
|
||||
}}
|
||||
openOnKeyDown={true}
|
||||
blurOnSelectClose={false}
|
||||
{...fieldProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
||||
51
src/components/DataTableCells/SwitchFieldCell.js
Normal file
51
src/components/DataTableCells/SwitchFieldCell.js
Normal 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;
|
||||
42
src/components/DataTableCells/TextAreaCell.js
Normal file
42
src/components/DataTableCells/TextAreaCell.js
Normal 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;
|
||||
@@ -6,7 +6,9 @@ import ItemsListCell from './ItemsListCell';
|
||||
import PercentFieldCell from './PercentFieldCell';
|
||||
import { DivFieldCell, EmptyDiv } from './DivFieldCell';
|
||||
import NumericInputCell from './NumericInputCell';
|
||||
import CheckBoxFieldCell from './CheckBoxFieldCell'
|
||||
import CheckBoxFieldCell from './CheckBoxFieldCell';
|
||||
import SwitchFieldCell from './SwitchFieldCell';
|
||||
import TextAreaCell from './TextAreaCell';
|
||||
|
||||
export {
|
||||
AccountsListFieldCell,
|
||||
@@ -18,5 +20,7 @@ export {
|
||||
DivFieldCell,
|
||||
EmptyDiv,
|
||||
NumericInputCell,
|
||||
CheckBoxFieldCell
|
||||
CheckBoxFieldCell,
|
||||
SwitchFieldCell,
|
||||
TextAreaCell,
|
||||
};
|
||||
|
||||
@@ -9,16 +9,16 @@ function DialogComponent(props) {
|
||||
const { name, children, closeDialog, onClose } = props;
|
||||
|
||||
const handleClose = (event) => {
|
||||
closeDialog(name)
|
||||
closeDialog(name);
|
||||
onClose && onClose(event);
|
||||
};
|
||||
return (
|
||||
<Dialog {...props} onClose={handleClose}>
|
||||
{ children }
|
||||
{children}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withDialogActions,
|
||||
)(DialogComponent);
|
||||
const DialogRoot = compose(withDialogActions)(DialogComponent);
|
||||
|
||||
export { DialogRoot as Dialog };
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Spinner, Classes } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function DialogContent(props) {
|
||||
export function DialogContent(props) {
|
||||
const { isLoading, children } = props;
|
||||
|
||||
const loadingContent = (
|
||||
|
||||
26
src/components/Dialog/DialogFooterActions.js
Normal file
26
src/components/Dialog/DialogFooterActions.js
Normal 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;
|
||||
}
|
||||
`;
|
||||
@@ -5,7 +5,7 @@ function LoadingContent() {
|
||||
return (<div className={Classes.DIALOG_BODY}><Spinner size={30} /></div>);
|
||||
}
|
||||
|
||||
export default function DialogSuspense({
|
||||
export function DialogSuspense({
|
||||
children
|
||||
}) {
|
||||
return (
|
||||
|
||||
6
src/components/Dialog/index.js
Normal file
6
src/components/Dialog/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
export * from './Dialog';
|
||||
export * from './DialogFooterActions';
|
||||
export * from './DialogSuspense';
|
||||
export * from './DialogContent';
|
||||
@@ -20,6 +20,11 @@ import ReceiptPdfPreviewDialog from '../containers/Dialogs/ReceiptPdfPreviewDial
|
||||
import MoneyInDialog from '../containers/Dialogs/MoneyInDialog';
|
||||
import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog';
|
||||
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.
|
||||
@@ -45,7 +50,14 @@ export default function DialogsContainer() {
|
||||
<ReceiptPdfPreviewDialog dialogName={'receipt-pdf-preview'} />
|
||||
<MoneyInDialog dialogName={'money-in'} />
|
||||
<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'} />
|
||||
<SMSMessageDialog dialogName={'sms-message-form'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ListSelect from "./ListSelect";
|
||||
import intl from 'react-intl-universal';
|
||||
import ListSelect from './ListSelect';
|
||||
|
||||
export default function DisplayNameList({
|
||||
salutation,
|
||||
@@ -9,25 +10,32 @@ export default function DisplayNameList({
|
||||
...restProps
|
||||
}) {
|
||||
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: [1, 2] },
|
||||
{ format: '{1}', values: [company], required: [1] }
|
||||
{ format: '{1}', values: [company], required: [1] },
|
||||
];
|
||||
|
||||
const formatOptions = formats
|
||||
.filter((format) => !format.values.some((value, index) => {
|
||||
return !value && format.required.indexOf(index + 1) !== -1;
|
||||
}))
|
||||
.filter(
|
||||
(format) =>
|
||||
!format.values.some((value, index) => {
|
||||
return !value && format.required.indexOf(index + 1) !== -1;
|
||||
}),
|
||||
)
|
||||
.map((formatOption) => {
|
||||
const { format, values } = formatOption;
|
||||
let label = format;
|
||||
|
||||
values.forEach((value, index) => {
|
||||
const replaceWith = (value || '');
|
||||
const replaceWith = value || '';
|
||||
label = label.replace(`{${index + 1}}`, replaceWith).trim();
|
||||
});
|
||||
return { label: label.replace(/\s+/g, " ") };
|
||||
return { label: label.replace(/\s+/g, ' ') };
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -35,9 +43,9 @@ export default function DisplayNameList({
|
||||
items={formatOptions}
|
||||
selectedItemProp={'label'}
|
||||
textProp={'label'}
|
||||
defaultText={'Select display name as'}
|
||||
defaultText={intl.get('select_display_name_as')}
|
||||
filterable={false}
|
||||
{ ...restProps }
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Position, Drawer } from '@blueprintjs/core';
|
||||
|
||||
import 'style/components/Drawer.scss';
|
||||
|
||||
import { DrawerProvider } from './DrawerProvider';
|
||||
import withDrawerActions from 'containers/Drawer/withDrawerActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
@@ -27,7 +28,7 @@ function DrawerComponent(props) {
|
||||
portalClassName={'drawer-portal'}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DrawerProvider {...props}>{children}</DrawerProvider>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
16
src/components/Drawer/DrawerProvider.js
Normal file
16
src/components/Drawer/DrawerProvider.js
Normal 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 };
|
||||
@@ -14,6 +14,9 @@ import CustomerDetailsDrawer from '../containers/Drawers/CustomerDetailsDrawer';
|
||||
import VendorDetailsDrawer from '../containers/Drawers/VendorDetailsDrawer';
|
||||
import InventoryAdjustmentDetailDrawer from '../containers/Drawers/InventoryAdjustmentDetailDrawer';
|
||||
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';
|
||||
|
||||
@@ -38,7 +41,12 @@ export default function DrawersContainer() {
|
||||
<InventoryAdjustmentDetailDrawer
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,68 @@
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { MenuItem } from '@blueprintjs/core';
|
||||
import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
import { Suggest } from '@blueprintjs/select';
|
||||
import classNames from 'classnames';
|
||||
import * as R from 'ramda';
|
||||
import intl from 'react-intl-universal';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
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={intl.get('list.create', { value: `"${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,
|
||||
initialItemId,
|
||||
selectedItemId,
|
||||
@@ -18,6 +73,10 @@ export default function ItemsSuggestField({
|
||||
sellable = false,
|
||||
purchasable = false,
|
||||
popoverFill = false,
|
||||
|
||||
allowCreate = true,
|
||||
|
||||
openDrawer,
|
||||
...suggestProps
|
||||
}) {
|
||||
// Filters items based on filter props.
|
||||
@@ -36,28 +95,23 @@ export default function ItemsSuggestField({
|
||||
// Find initial item object.
|
||||
const initialItem = useMemo(
|
||||
() => filteredItems.some((a) => a.id === initialItemId),
|
||||
[initialItemId],
|
||||
[initialItemId, filteredItems],
|
||||
);
|
||||
|
||||
const [selectedItem, setSelectedItem] = useState(initialItem || null);
|
||||
|
||||
const onItemSelect = useCallback(
|
||||
(item) => {
|
||||
setSelectedItem({ ...item });
|
||||
onItemSelected && onItemSelected(item);
|
||||
if (item.id) {
|
||||
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(() => {
|
||||
if (typeof selectedItemId !== 'undefined') {
|
||||
const item = selectedItemId
|
||||
@@ -67,27 +121,12 @@ export default function ItemsSuggestField({
|
||||
}
|
||||
}, [selectedItemId, filteredItems, setSelectedItem]);
|
||||
|
||||
const handleInputValueRenderer = (inputValue) => {
|
||||
if (inputValue) {
|
||||
return inputValue.name.toString();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
// Maybe inject create new item props to suggest component.
|
||||
const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
|
||||
const maybeCreateNewItemFromQuery = allowCreate
|
||||
? createNewItemFromQuery
|
||||
: 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 (
|
||||
<Suggest
|
||||
items={filteredItems}
|
||||
@@ -104,7 +143,12 @@ export default function ItemsSuggestField({
|
||||
className={classNames(CLASSES.FORM_GROUP_LIST_SELECT, {
|
||||
[CLASSES.SELECT_LIST_FILL_POPOVER]: popoverFill,
|
||||
})}
|
||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||
createNewItemFromQuery={maybeCreateNewItemFromQuery}
|
||||
createNewItemPosition={'top'}
|
||||
{...suggestProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default R.compose(withDrawerActions)(ItemsSuggestField);
|
||||
|
||||
36
src/components/MoreMenutItems.js
Normal file
36
src/components/MoreMenutItems.js
Normal 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;
|
||||
@@ -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;
|
||||
46
src/components/SMSPreview/index.js
Normal file
46
src/components/SMSPreview/index.js
Normal 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;
|
||||
`;
|
||||
@@ -23,9 +23,6 @@ import AccountsSelectList from './AccountsSelectList';
|
||||
import AccountsTypesSelect from './AccountsTypesSelect';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
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 CategoriesSelectList from './CategoriesSelectList';
|
||||
import Row from './Grid/Row';
|
||||
@@ -61,8 +58,9 @@ import Card from './Card';
|
||||
import AvaterCell from './AvaterCell';
|
||||
|
||||
import { ItemsMultiSelect } from './Items';
|
||||
import MoreVertMenutItems from './MoreVertMenutItems';
|
||||
import MoreMenuItems from './MoreMenutItems';
|
||||
|
||||
export * from './Dialog';
|
||||
export * from './Menu';
|
||||
export * from './AdvancedFilter/AdvancedFilterDropdown';
|
||||
export * from './AdvancedFilter/AdvancedFilterPopover';
|
||||
@@ -83,10 +81,12 @@ export * from './MultiSelectTaggable';
|
||||
export * from './Utils/FormatNumber';
|
||||
export * from './Utils/FormatDate';
|
||||
export * from './BankAccounts';
|
||||
export * from './IntersectionObserver'
|
||||
export * from './IntersectionObserver';
|
||||
export * from './Datatable/CellForceWidth';
|
||||
export * from './Button';
|
||||
export * from './IntersectionObserver';
|
||||
export * from './SMSPreview';
|
||||
export * from './Contacts';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
@@ -120,9 +120,6 @@ export {
|
||||
LoadingIndicator,
|
||||
DashboardActionViewsList,
|
||||
AppToaster,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogSuspense,
|
||||
InputPrependButton,
|
||||
CategoriesSelectList,
|
||||
Col,
|
||||
@@ -158,5 +155,5 @@ export {
|
||||
ItemsMultiSelect,
|
||||
Card,
|
||||
AvaterCell,
|
||||
MoreVertMenutItems,
|
||||
MoreMenuItems,
|
||||
};
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
import React from 'react'
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import React from 'react';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
|
||||
export default [
|
||||
{
|
||||
text: <T id={'general'}/>,
|
||||
text: <T id={'general'} />,
|
||||
disabled: false,
|
||||
href: '/preferences/general',
|
||||
},
|
||||
{
|
||||
text: <T id={'users'}/>,
|
||||
text: <T id={'users'} />,
|
||||
href: '/preferences/users',
|
||||
},
|
||||
{
|
||||
text: <T id={'currencies'}/>,
|
||||
|
||||
text: <T id={'currencies'} />,
|
||||
|
||||
href: '/preferences/currencies',
|
||||
},
|
||||
{
|
||||
text: <T id={'accountant'}/>,
|
||||
text: <T id={'accountant'} />,
|
||||
disabled: false,
|
||||
href: '/preferences/accountant',
|
||||
},
|
||||
{
|
||||
text: <T id={'items'}/>,
|
||||
text: <T id={'items'} />,
|
||||
disabled: false,
|
||||
href: '/preferences/items',
|
||||
},
|
||||
{
|
||||
text: <T id={'sms_integration.label'} />,
|
||||
disabled: false,
|
||||
href: '/preferences/sms-message',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -128,6 +128,7 @@ export const useJournalTableEntriesColumns = () => {
|
||||
className: 'account',
|
||||
disableSortBy: true,
|
||||
width: 160,
|
||||
fieldProps: { allowCreate: true }
|
||||
},
|
||||
{
|
||||
Header: CreditHeaderCell,
|
||||
|
||||
@@ -9,23 +9,24 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'components';
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useFormikContext } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { FormattedMessage as T } from 'components';
|
||||
|
||||
import { CLASSES } from 'common/classes';
|
||||
import { Icon } from 'components';
|
||||
import { useCustomerFormContext } from './CustomerFormProvider';
|
||||
|
||||
import { safeInvoke } from 'utils';
|
||||
|
||||
/**
|
||||
* Customer floating actions bar.
|
||||
*/
|
||||
export default function CustomerFloatingActions() {
|
||||
const history = useHistory();
|
||||
|
||||
export default function CustomerFloatingActions({ onCancel }) {
|
||||
// Customer form context.
|
||||
const { isNewMode,setSubmitPayload } = useCustomerFormContext();
|
||||
const { isNewMode, setSubmitPayload } = useCustomerFormContext();
|
||||
|
||||
// Formik context.
|
||||
const { resetForm, submitForm, isSubmitting } = useFormikContext();
|
||||
@@ -37,7 +38,7 @@ export default function CustomerFloatingActions() {
|
||||
|
||||
// Handle cancel button click.
|
||||
const handleCancelBtnClick = (event) => {
|
||||
history.goBack();
|
||||
safeInvoke(onCancel, event);
|
||||
};
|
||||
|
||||
// handle clear button clicl.
|
||||
@@ -55,7 +56,7 @@ export default function CustomerFloatingActions() {
|
||||
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}>
|
||||
<ButtonGroup>
|
||||
{/* ----------- Save and New ----------- */}
|
||||
<Button
|
||||
<SaveButton
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
@@ -83,6 +84,7 @@ export default function CustomerFloatingActions() {
|
||||
/>
|
||||
</Popover>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* ----------- Clear & Reset----------- */}
|
||||
<Button
|
||||
className={'ml1'}
|
||||
@@ -99,3 +101,7 @@ export default function CustomerFloatingActions() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SaveButton = styled(Button)`
|
||||
min-width: 100px;
|
||||
`;
|
||||
|
||||
@@ -1,152 +1,14 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import moment from 'moment';
|
||||
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'),
|
||||
};
|
||||
import React from 'react';
|
||||
import { CustomerFormProvider } from './CustomerFormProvider';
|
||||
import CustomerFormFormik from './CustomerFormFormik';
|
||||
|
||||
/**
|
||||
* Customer form.
|
||||
* Abstructed customer form.
|
||||
*/
|
||||
function CustomerForm({ organization: { base_currency } }) {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
export default function CustomerForm({ customerId }) {
|
||||
return (
|
||||
<div className={classNames(CLASSES.PAGE_FORM, CLASSES.PAGE_FORM_CUSTOMER)}>
|
||||
<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 />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
<CustomerFormProvider customerId={customerId}>
|
||||
<CustomerFormFormik />
|
||||
</CustomerFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withCurrentOrganization())(CustomerForm);
|
||||
|
||||
@@ -6,7 +6,6 @@ import intl from 'react-intl-universal';
|
||||
import { inputIntent } from 'utils';
|
||||
|
||||
export default function CustomerFormAfterPrimarySection({}) {
|
||||
|
||||
return (
|
||||
<div class="customer-form__after-primary-section-content">
|
||||
{/*------------ Customer email -----------*/}
|
||||
@@ -31,21 +30,21 @@ export default function CustomerFormAfterPrimarySection({}) {
|
||||
inline={true}
|
||||
>
|
||||
<ControlGroup>
|
||||
<FastField name={'work_phone'}>
|
||||
<FastField name={'personal_phone'}>
|
||||
{({ field, meta: { error, touched } }) => (
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
placeholder={intl.get('work')}
|
||||
placeholder={intl.get('personal')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<FastField name={'personal_phone'}>
|
||||
<FastField name={'work_phone'}>
|
||||
{({ field, meta: { error, touched } }) => (
|
||||
<InputGroup
|
||||
intent={inputIntent({ error, touched })}
|
||||
placeholder={intl.get('Mobile')}
|
||||
placeholder={intl.get('work')}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
|
||||
127
src/containers/Customers/CustomerForm/CustomerFormFormik.js
Normal file
127
src/containers/Customers/CustomerForm/CustomerFormFormik.js
Normal 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);
|
||||
@@ -1,20 +1,74 @@
|
||||
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 CustomerForm from './CustomerForm';
|
||||
import { CustomerFormProvider } from './CustomerFormProvider';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
|
||||
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 (
|
||||
<CustomerFormProvider customerId={id}>
|
||||
<DashboardCard page>
|
||||
<CustomerForm />
|
||||
</DashboardCard>
|
||||
<CustomerDashboardInsider loading={isFormLoading}>
|
||||
{children}
|
||||
</CustomerDashboardInsider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const CustomerFormPageFormik = styled(CustomerFormFormik)`
|
||||
.page-form {
|
||||
&__floating-actions {
|
||||
margin-left: -40px;
|
||||
margin-right: -40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CustomerDashboardInsider = styled(DashboardInsider)`
|
||||
padding-bottom: 64px;
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, createContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import {
|
||||
useCustomer,
|
||||
useCurrencies,
|
||||
@@ -24,7 +23,7 @@ function CustomerFormProvider({ customerId, ...props }) {
|
||||
// Handle fetch contact duplicate details.
|
||||
const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
|
||||
contactId,
|
||||
{ enabled: !!contactId, },
|
||||
{ enabled: !!contactId },
|
||||
);
|
||||
// Handle fetch Currencies data table
|
||||
const { data: currencies, isLoading: isCurrenciesLoading } = useCurrencies();
|
||||
@@ -38,6 +37,9 @@ function CustomerFormProvider({ customerId, ...props }) {
|
||||
// determines whether the form new or duplicate mode.
|
||||
const isNewMode = contactId || !customerId;
|
||||
|
||||
const isFormLoading =
|
||||
isCustomerLoading || isCurrenciesLoading || isContactLoading;
|
||||
|
||||
const provider = {
|
||||
customerId,
|
||||
customer,
|
||||
@@ -48,24 +50,14 @@ function CustomerFormProvider({ customerId, ...props }) {
|
||||
|
||||
isCustomerLoading,
|
||||
isCurrenciesLoading,
|
||||
isFormLoading,
|
||||
|
||||
setSubmitPayload,
|
||||
editCustomerMutate,
|
||||
createCustomerMutate,
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardInsider
|
||||
loading={
|
||||
isCustomerLoading ||
|
||||
isCurrenciesLoading ||
|
||||
isContactLoading
|
||||
}
|
||||
name={'customer-form'}
|
||||
>
|
||||
<CustomerFormContext.Provider value={provider} {...props} />
|
||||
</DashboardInsider>
|
||||
);
|
||||
return <CustomerFormContext.Provider value={provider} {...props} />;
|
||||
}
|
||||
|
||||
const useCustomerFormContext = () => React.useContext(CustomerFormContext);
|
||||
|
||||
38
src/containers/Customers/CustomerForm/utils.js
Normal file
38
src/containers/Customers/CustomerForm/utils.js
Normal 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'),
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
export const transformApiErrors = (errors) => {
|
||||
const fields = {};
|
||||
@@ -42,12 +43,18 @@ const mergeWithAccount = R.curry((transformed, account) => {
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Default account payload transformer.
|
||||
*/
|
||||
const defaultPayloadTransform = () => ({});
|
||||
|
||||
/**
|
||||
* Defined payload transformers.
|
||||
*/
|
||||
function getConditions() {
|
||||
return [
|
||||
['edit', transformEditMode],
|
||||
['edit'],
|
||||
['new_child', transformEditMode],
|
||||
['NEW_ACCOUNT_DEFINED_TYPE', transformNewAccountDefinedType],
|
||||
];
|
||||
}
|
||||
@@ -59,9 +66,13 @@ export const transformAccountToForm = (account, payload) => {
|
||||
const conditions = getConditions();
|
||||
|
||||
const results = conditions.map((condition) => {
|
||||
const transformer = !isEmpty(condition[1])
|
||||
? condition[1]
|
||||
: defaultPayloadTransform;
|
||||
|
||||
return [
|
||||
condition[0] === payload.action ? R.T : R.F,
|
||||
mergeWithAccount(condition[1](payload)),
|
||||
mergeWithAccount(transformer(payload)),
|
||||
];
|
||||
});
|
||||
return R.cond(results)(account);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
36
src/containers/Dialogs/NotifyEstimateViaSMSDialog/index.js
Normal file
36
src/containers/Dialogs/NotifyEstimateViaSMSDialog/index.js
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
36
src/containers/Dialogs/NotifyInvoiceViaSMSDialog/index.js
Normal file
36
src/containers/Dialogs/NotifyInvoiceViaSMSDialog/index.js
Normal 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);
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
35
src/containers/Dialogs/NotifyReceiptViaSMSDialog/index.js
Normal file
35
src/containers/Dialogs/NotifyReceiptViaSMSDialog/index.js
Normal 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);
|
||||
@@ -37,7 +37,7 @@ export default function QuickPaymentMadeFormFields() {
|
||||
const { accounts } = useQuickPaymentMadeContext();
|
||||
|
||||
// Intl context.
|
||||
|
||||
|
||||
const paymentMadeFieldRef = useAutofocus();
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
80
src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.js
Normal file
80
src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.js
Normal 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);
|
||||
@@ -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;
|
||||
119
src/containers/Dialogs/SMSMessageDialog/SMSMessageFormContent.js
Normal file
119
src/containers/Dialogs/SMSMessageDialog/SMSMessageFormContent.js
Normal 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;
|
||||
`)}
|
||||
`;
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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);
|
||||
39
src/containers/Dialogs/SMSMessageDialog/index.js
Normal file
39
src/containers/Dialogs/SMSMessageDialog/index.js
Normal 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);
|
||||
19
src/containers/Dialogs/SMSMessageDialog/utils.js
Normal file
19
src/containers/Dialogs/SMSMessageDialog/utils.js
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -73,6 +73,10 @@ export default function CustomerDetailsHeader() {
|
||||
label={<T id={'customer.drawer.label.currency'} />}
|
||||
children={customer?.currency_code}
|
||||
/>
|
||||
<DetailItem
|
||||
label={<T id={'customer.drawer.label.note'} />}
|
||||
children={defaultTo(customer?.note, '--')}
|
||||
/>
|
||||
</DetailsMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
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';
|
||||
|
||||
@@ -51,6 +51,10 @@ function EstimateDetailActionsBar({
|
||||
const handlePrintEstimate = () => {
|
||||
openDialog('estimate-pdf-preview', { estimateId });
|
||||
};
|
||||
// Handle notify via SMS.
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-estimate-via-sms', { estimateId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
@@ -75,6 +79,12 @@ function EstimateDetailActionsBar({
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeleteEstimate}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<MoreMenuItems
|
||||
payload={{
|
||||
onNotifyViaSMS: handleNotifyViaSMS,
|
||||
}}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
|
||||
@@ -6,28 +6,17 @@ import {
|
||||
NavbarGroup,
|
||||
Classes,
|
||||
NavbarDivider,
|
||||
Popover,
|
||||
PopoverInteractionKind,
|
||||
Position,
|
||||
Intent,
|
||||
MenuItem,
|
||||
Menu,
|
||||
} from '@blueprintjs/core';
|
||||
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
|
||||
|
||||
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
|
||||
import { moreVertOptions } from '../../../common/moreVertOptions';
|
||||
|
||||
import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
import withDrawerActions from 'containers/Drawer/withDrawerActions';
|
||||
|
||||
import {
|
||||
If,
|
||||
Icon,
|
||||
FormattedMessage as T,
|
||||
// MoreVertMenutItems,
|
||||
} from 'components';
|
||||
import { If, Icon, FormattedMessage as T } from 'components';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
@@ -76,6 +65,10 @@ function InvoiceDetailActionsBar({
|
||||
const handleBadDebtInvoice = () => {
|
||||
openDialog('write-off-bad-debt', { invoiceId });
|
||||
};
|
||||
// Handle notify via SMS.
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-invoice-via-sms', { invoiceId });
|
||||
};
|
||||
|
||||
// Handle cancele write-off invoice.
|
||||
const handleCancelBadDebtInvoice = () => {
|
||||
@@ -116,9 +109,11 @@ function InvoiceDetailActionsBar({
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<BadDebtMenuItem
|
||||
invoice={invoice}
|
||||
onAlert={handleCancelBadDebtInvoice}
|
||||
onDialog={handleBadDebtInvoice}
|
||||
payload={{
|
||||
onBadDebt: handleBadDebtInvoice,
|
||||
onCancelBadDebt: handleCancelBadDebtInvoice,
|
||||
onNotifyViaSMS: handleNotifyViaSMS,
|
||||
}}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@blueprintjs/core';
|
||||
import { Icon, FormattedMessage as T, Choose } from 'components';
|
||||
import { FormatNumberCell } from '../../../components';
|
||||
import { useInvoiceDetailDrawerContext } from './InvoiceDetailDrawerProvider';
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Popover
|
||||
minimal={true}
|
||||
@@ -73,16 +78,20 @@ export const BadDebtMenuItem = ({ invoice, onDialog, onAlert }) => {
|
||||
<Choose.When condition={!invoice.is_writtenoff}>
|
||||
<MenuItem
|
||||
text={<T id={'bad_debt.dialog.bad_debt'} />}
|
||||
onClick={onDialog}
|
||||
onClick={onBadDebt}
|
||||
/>
|
||||
</Choose.When>
|
||||
<Choose.When condition={invoice.is_writtenoff}>
|
||||
<MenuItem
|
||||
onClick={onAlert}
|
||||
onClick={onCancelBadDebt}
|
||||
text={<T id={'bad_debt.dialog.cancel_bad_debt'} />}
|
||||
/>
|
||||
</Choose.When>
|
||||
</Choose>
|
||||
<MenuItem
|
||||
onClick={onNotifyViaSMS}
|
||||
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
|
||||
/>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
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';
|
||||
|
||||
@@ -29,6 +29,9 @@ function PaymentReceiveActionsBar({
|
||||
|
||||
// #withDrawerActions
|
||||
closeDrawer,
|
||||
|
||||
// #withDialogActions
|
||||
openDialog,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
@@ -46,6 +49,11 @@ function PaymentReceiveActionsBar({
|
||||
openAlert('payment-receive-delete', { paymentReceiveId });
|
||||
};
|
||||
|
||||
// Handle notify via SMS.
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-payment-via-sms', { paymentReceiveId });
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -63,6 +71,12 @@ function PaymentReceiveActionsBar({
|
||||
intent={Intent.DANGER}
|
||||
onClick={handleDeletePaymentReceive}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<MoreMenuItems
|
||||
payload={{
|
||||
onNotifyViaSMS: handleNotifyViaSMS,
|
||||
}}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
35
src/containers/Drawers/QuickCreateCustomerDrawer/index.js
Normal file
35
src/containers/Drawers/QuickCreateCustomerDrawer/index.js
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
37
src/containers/Drawers/QuickCreateItemDrawer/index.js
Normal file
37
src/containers/Drawers/QuickCreateItemDrawer/index.js
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,26 @@
|
||||
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={<T id={'create_a_new_vendor'} />}
|
||||
|
||||
/>
|
||||
<DrawerBody>
|
||||
<QuickVendorFormDrawer displayName={displayName} />
|
||||
</DrawerBody>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
36
src/containers/Drawers/QuickWriteVendorDrawer/index.js
Normal file
36
src/containers/Drawers/QuickWriteVendorDrawer/index.js
Normal 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);
|
||||
@@ -14,7 +14,7 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
|
||||
import withAlertsActions from 'containers/Alert/withAlertActions';
|
||||
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 { safeCallback, compose } from 'utils';
|
||||
@@ -46,6 +46,11 @@ function ReceiptDetailActionBar({
|
||||
const onPrintReceipt = () => {
|
||||
openDialog('receipt-pdf-preview', { receiptId });
|
||||
};
|
||||
|
||||
// Handle notify via SMS.
|
||||
const handleNotifyViaSMS = () => {
|
||||
openDialog('notify-receipt-via-sms', { receiptId });
|
||||
};
|
||||
return (
|
||||
<DashboardActionsBar>
|
||||
<NavbarGroup>
|
||||
@@ -69,6 +74,12 @@ function ReceiptDetailActionBar({
|
||||
intent={Intent.DANGER}
|
||||
onClick={safeCallback(onDeleteReceipt)}
|
||||
/>
|
||||
<NavbarDivider />
|
||||
<MoreMenuItems
|
||||
payload={{
|
||||
onNotifyViaSMS: handleNotifyViaSMS,
|
||||
}}
|
||||
/>
|
||||
</NavbarGroup>
|
||||
</DashboardActionsBar>
|
||||
);
|
||||
|
||||
@@ -68,6 +68,11 @@ export default function VendorDetailsHeader() {
|
||||
label={<T id={'vendor.drawer.label.currency'} />}
|
||||
children={vendor?.currency_code}
|
||||
/>
|
||||
<DetailItem
|
||||
label={<T id={'vendor.drawer.label.note'} />}
|
||||
children={defaultTo(vendor?.note, '--')}
|
||||
|
||||
/>
|
||||
</DetailsMenu>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -108,7 +108,9 @@ const LandedCostHeaderCell = () => {
|
||||
/**
|
||||
* Retrieve editable items entries columns.
|
||||
*/
|
||||
export function useEditableItemsEntriesColumns({ landedCost }) {
|
||||
export function useEditableItemsEntriesColumns({
|
||||
landedCost,
|
||||
}) {
|
||||
return React.useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -129,6 +131,7 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
|
||||
disableSortBy: true,
|
||||
width: 130,
|
||||
className: 'item',
|
||||
fieldProps: { allowCreate: true },
|
||||
},
|
||||
{
|
||||
Header: intl.get('description'),
|
||||
|
||||
@@ -164,3 +164,12 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
|
||||
updateTableRow(rowIndex, newRow),
|
||||
)(rows);
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} entries
|
||||
* @returns
|
||||
*/
|
||||
export const composeControlledEntries = (entries) => {
|
||||
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { customersFieldShouldUpdate, accountsFieldShouldUpdate } from './utils';
|
||||
import {
|
||||
CurrencySelectList,
|
||||
ContactSelecetList,
|
||||
CustomerSelectField,
|
||||
AccountsSelectList,
|
||||
FieldRequiredHint,
|
||||
Hint,
|
||||
@@ -78,6 +78,7 @@ export default function ExpenseFormHeader() {
|
||||
defaultSelectText={<T id={'select_payment_account'} />}
|
||||
selectedAccountId={value}
|
||||
filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
@@ -137,13 +138,14 @@ export default function ExpenseFormHeader() {
|
||||
helperText={<ErrorMessage name={'assign_to_customer'} />}
|
||||
inline={true}
|
||||
>
|
||||
<ContactSelecetList
|
||||
contactsList={customers}
|
||||
<CustomerSelectField
|
||||
contacts={customers}
|
||||
selectedContactId={value}
|
||||
defaultSelectText={<T id={'select_customer_account'} />}
|
||||
onContactSelected={(customer) => {
|
||||
form.setFieldValue('customer_id', customer.id);
|
||||
}}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
@@ -113,6 +113,7 @@ export function useExpenseFormTableColumns({ landedCost }) {
|
||||
disableSortBy: true,
|
||||
width: 40,
|
||||
filterAccountsByRootTypes: ['expense'],
|
||||
fieldProps: { allowCreate: true },
|
||||
},
|
||||
{
|
||||
Header: ExpenseAmountHeaderCell,
|
||||
|
||||
@@ -27,7 +27,7 @@ export function BalanceSheetAlerts() {
|
||||
<Icon icon="info-block" iconSize={12} />{' '}
|
||||
<T id={'just_a_moment_we_re_calculating_your_cost_transactions'} />
|
||||
<Button onClick={handleRecalcReport} minimal={true} small={true}>
|
||||
<T id={'refresh'} />
|
||||
<T id={'report.compute_running.refresh'} />
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function CustomersBalanceSummaryTable({
|
||||
const columns = useCustomersSummaryColumns();
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return [`row-type--${row.original.rowTypes}`];
|
||||
return [`row-type--${row.original.row_types}`];
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function CustomersTransactionsTable({
|
||||
]);
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return [`row-type--${row.original.rowTypes}`];
|
||||
return [`row-type--${row.original.row_types}`];
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,14 +22,14 @@ export const useCustomersTransactionsColumns = () => {
|
||||
return (
|
||||
<span
|
||||
className={'force-width'}
|
||||
style={{ minWidth: getForceWidth(cells[0].key) }}
|
||||
style={{ minWidth: getForceWidth(cells[0].value) }}
|
||||
>
|
||||
{cells[0].value}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
className: 'customer_name',
|
||||
textOverview: true,
|
||||
// textOverview: true,
|
||||
},
|
||||
{
|
||||
Header: intl.get('account_name'),
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useGeneralLedgerTableColumns() {
|
||||
return row.date;
|
||||
},
|
||||
className: 'date',
|
||||
textOverview: true,
|
||||
// textOverview: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function InventoryItemDetailsTable({
|
||||
);
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return [`row-type--${row.original.rowTypes}`];
|
||||
return [`row-type--${row.original.row_types}`];
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ const columnsMapper = (data, index, column) => ({
|
||||
id: column.key,
|
||||
key: column.key,
|
||||
Header: column.label,
|
||||
// Cell: CellForceWidth,
|
||||
Cell: CellForceWidth,
|
||||
accessor: `cells[${index}].value`,
|
||||
forceWidthAccess: `cells[0].value`,
|
||||
className: column.key,
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function VendorsBalanceSummaryTable({
|
||||
const columns = useVendorsBalanceColumns();
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return [`row-type--${row.original.rowTypes}`];
|
||||
return [`row-type--${row.original.row_types}`];
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function VendorsTransactionsTable({
|
||||
]);
|
||||
|
||||
const rowClassNames = (row) => {
|
||||
return [`row-type--${row.original.rowTypes}`];
|
||||
return [`row-type--${row.original.row_types}`];
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -21,14 +21,14 @@ export const useVendorsTransactionsColumns = () => {
|
||||
return (
|
||||
<span
|
||||
className={'force-width'}
|
||||
style={{ minWidth: getForceWidth(cells[0].key) }}
|
||||
style={{ minWidth: getForceWidth(cells[0].value) }}
|
||||
>
|
||||
{cells[0].value}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
className: 'vendor_name',
|
||||
textOverview: true,
|
||||
// textOverview: true,
|
||||
// width: 240,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,110 +1,95 @@
|
||||
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 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 AppToaster from 'components/AppToaster';
|
||||
import ItemFormPrimarySection from './ItemFormPrimarySection';
|
||||
import ItemFormBody from './ItemFormBody';
|
||||
import ItemFormFloatingActions from './ItemFormFloatingActions';
|
||||
import ItemFormInventorySection from './ItemFormInventorySection';
|
||||
import ItemFormFormik from './ItemFormFormik';
|
||||
|
||||
import {
|
||||
transformSubmitRequestErrors,
|
||||
useItemFormInitialValues,
|
||||
} from './utils';
|
||||
import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
|
||||
|
||||
import { useItemFormContext } from './ItemFormProvider';
|
||||
import DashboardCard from 'components/Dashboard/DashboardCard';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
|
||||
/**
|
||||
* Item form.
|
||||
* Item form dashboard title.
|
||||
* @returns {null}
|
||||
*/
|
||||
export default function ItemForm() {
|
||||
// Item form context.
|
||||
const {
|
||||
itemId,
|
||||
item,
|
||||
accounts,
|
||||
createItemMutate,
|
||||
editItemMutate,
|
||||
submitPayload,
|
||||
isNewMode,
|
||||
} = useItemFormContext();
|
||||
function ItemFormDashboardTitle() {
|
||||
// Change page title dispatcher.
|
||||
const changePageTitle = useDashboardPageTitle();
|
||||
|
||||
// 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.
|
||||
const history = useHistory();
|
||||
|
||||
// Initial values in create and edit mode.
|
||||
const initialValues = useItemFormInitialValues(item);
|
||||
|
||||
// Handles the form submit.
|
||||
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 the form submit success.
|
||||
const handleSubmitSuccess = (values, form, submitPayload) => {
|
||||
if (submitPayload.redirect) {
|
||||
history.push('/items');
|
||||
}
|
||||
};
|
||||
// Handle cancel button click.
|
||||
const handleFormCancel = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={classNames(CLASSES.PAGE_FORM_ITEM)}>
|
||||
<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>
|
||||
<ItemFormProvider itemId={itemId}>
|
||||
<ItemFormDashboardTitle />
|
||||
|
||||
<ItemFormFloatingActions />
|
||||
</Form>
|
||||
</Formik>
|
||||
</div>
|
||||
<ItemFormPageLoading>
|
||||
<DashboardCard page>
|
||||
<ItemFormPageFormik
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useFormikContext, FastField, Field, ErrorMessage } from 'formik';
|
||||
import { useFormikContext, FastField, ErrorMessage } from 'formik';
|
||||
import {
|
||||
FormGroup,
|
||||
Classes,
|
||||
@@ -122,6 +122,7 @@ function ItemFormBody({ organization: { base_currency } }) {
|
||||
disabled={!form.values.sellable}
|
||||
filterByParentTypes={[ACCOUNT_PARENT_TYPE.INCOME]}
|
||||
popoverFill={true}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
@@ -230,6 +231,7 @@ function ItemFormBody({ organization: { base_currency } }) {
|
||||
disabled={!form.values.purchasable}
|
||||
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
|
||||
popoverFill={true}
|
||||
allowCreate={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user