diff --git a/src/common/classes.js b/src/common/classes.js
index f20587caa..bea33a718 100644
--- a/src/common/classes.js
+++ b/src/common/classes.js
@@ -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',
diff --git a/src/common/drawers.js b/src/common/drawers.js
index 449f51485..3a1b21032 100644
--- a/src/common/drawers.js
+++ b/src/common/drawers.js
@@ -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',
};
diff --git a/src/components/AccountsSelectList.js b/src/components/AccountsSelectList.js
index 75a7d3e09..9ed337284 100644
--- a/src/components/AccountsSelectList.js
+++ b/src/components/AccountsSelectList.js
@@ -1,13 +1,55 @@
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 { 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 (
+
+ );
+};
+
+// 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 +63,8 @@ export default function AccountsSelectList({
filterByNormal,
filterByRootTypes,
+ allowCreate,
+
buttonProps = {},
}) {
const flattenAccounts = useMemo(
@@ -51,6 +95,7 @@ export default function AccountsSelectList({
[initialAccountId, filteredAccounts],
);
+ // Select account item.
const [selectedAccount, setSelectedAccount] = useState(
initialAccount || null,
);
@@ -76,31 +121,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 (
+ );
+}
diff --git a/src/components/Contacts/CustomerSelectField.js b/src/components/Contacts/CustomerSelectField.js
new file mode 100644
index 000000000..322e7b85b
--- /dev/null
+++ b/src/components/Contacts/CustomerSelectField.js
@@ -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 = ,
+ 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 (
+ } />}
+ 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}
+ >
+
+
+ );
+}
+
+export default R.compose(withDrawerActions)(CustomerSelectField);
diff --git a/src/components/Contacts/VendorSelectField.js b/src/components/Contacts/VendorSelectField.js
new file mode 100644
index 000000000..1f0560f74
--- /dev/null
+++ b/src/components/Contacts/VendorSelectField.js
@@ -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 = ,
+ 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 (
+ } />}
+ 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}
+ >
+
+
+ );
+}
+
+export default R.compose(withDrawerActions)(VendorSelectField);
diff --git a/src/components/Contacts/index.js b/src/components/Contacts/index.js
new file mode 100644
index 000000000..d6c1821c2
--- /dev/null
+++ b/src/components/Contacts/index.js
@@ -0,0 +1,5 @@
+import ContactSelectField from './ContactSelectField';
+import CustomerSelectField from './CustomerSelectField';
+import VendorSelectField from './VendorSelectField';
+
+export { ContactSelectField, CustomerSelectField, VendorSelectField };
diff --git a/src/components/Contacts/utils.js b/src/components/Contacts/utils.js
new file mode 100644
index 000000000..59d0a4a8c
--- /dev/null
+++ b/src/components/Contacts/utils.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import { MenuItem } from '@blueprintjs/core';
+
+// Filter Contact List
+export const itemPredicate = (query, contact, index, exactMatch) => {
+ const normalizedTitle = contact.display_name.toLowerCase();
+ const normalizedQuery = query.toLowerCase();
+ if (exactMatch) {
+ return normalizedTitle === normalizedQuery;
+ } else {
+ return (
+ `${contact.display_name} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
+ );
+ }
+};
+
+export const handleContactRenderer = (contact, { handleClick }) => (
+
+);
+
+// Creates a new item from query.
+export const createNewItemFromQuery = (name) => {
+ return {
+ name,
+ };
+};
+
+// Handle quick create new customer.
+export const createNewItemRenderer = (query, active, handleClick) => {
+ return (
+
+ );
+};
diff --git a/src/components/Dashboard/DashboardContent.js b/src/components/Dashboard/DashboardContent.js
index 3c75d2288..739b405e7 100644
--- a/src/components/Dashboard/DashboardContent.js
+++ b/src/components/Dashboard/DashboardContent.js
@@ -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) => {
-
);
diff --git a/src/components/DataTableCells/AccountsListFieldCell.js b/src/components/DataTableCells/AccountsListFieldCell.js
index 1f27453a1..690e64b24 100644
--- a/src/components/DataTableCells/AccountsListFieldCell.js
+++ b/src/components/DataTableCells/AccountsListFieldCell.js
@@ -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}
>
);
diff --git a/src/components/DataTableCells/ItemsListCell.js b/src/components/DataTableCells/ItemsListCell.js
index 9ff7ddae1..ed6c024c6 100644
--- a/src/components/DataTableCells/ItemsListCell.js
+++ b/src/components/DataTableCells/ItemsListCell.js
@@ -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({
);
diff --git a/src/components/DataTableCells/SwitchFieldCell.js b/src/components/DataTableCells/SwitchFieldCell.js
new file mode 100644
index 000000000..a25e96ae4
--- /dev/null
+++ b/src/components/DataTableCells/SwitchFieldCell.js
@@ -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 (
+
+
+
+ );
+};
+
+export default SwitchEditableCell;
\ No newline at end of file
diff --git a/src/components/DataTableCells/TextAreaCell.js b/src/components/DataTableCells/TextAreaCell.js
new file mode 100644
index 000000000..c8ff97a0c
--- /dev/null
+++ b/src/components/DataTableCells/TextAreaCell.js
@@ -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 (
+
+
+
+ );
+};
+
+export default TextAreaEditableCell;
diff --git a/src/components/DataTableCells/index.js b/src/components/DataTableCells/index.js
index 2a0688ad2..fed349eed 100644
--- a/src/components/DataTableCells/index.js
+++ b/src/components/DataTableCells/index.js
@@ -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,
};
diff --git a/src/components/Dialog/Dialog.js b/src/components/Dialog/Dialog.js
index 95e1fdcbf..c5f793251 100644
--- a/src/components/Dialog/Dialog.js
+++ b/src/components/Dialog/Dialog.js
@@ -9,16 +9,16 @@ function DialogComponent(props) {
const { name, children, closeDialog, onClose } = props;
const handleClose = (event) => {
- closeDialog(name)
+ closeDialog(name);
onClose && onClose(event);
};
return (
);
}
-export default compose(
- withDialogActions,
-)(DialogComponent);
\ No newline at end of file
+const DialogRoot = compose(withDialogActions)(DialogComponent);
+
+export { DialogRoot as Dialog };
diff --git a/src/components/Dialog/DialogContent.js b/src/components/Dialog/DialogContent.js
index 265182bc5..cb438f499 100644
--- a/src/components/Dialog/DialogContent.js
+++ b/src/components/Dialog/DialogContent.js
@@ -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 = (
diff --git a/src/components/Dialog/DialogFooterActions.js b/src/components/Dialog/DialogFooterActions.js
new file mode 100644
index 000000000..91c856e9a
--- /dev/null
+++ b/src/components/Dialog/DialogFooterActions.js
@@ -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 (
+
+ {children}
+
+ );
+}
+
+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;
+ }
+`;
diff --git a/src/components/Dialog/DialogSuspense.js b/src/components/Dialog/DialogSuspense.js
index 6513d28c5..56ed625b9 100644
--- a/src/components/Dialog/DialogSuspense.js
+++ b/src/components/Dialog/DialogSuspense.js
@@ -5,7 +5,7 @@ function LoadingContent() {
return (
);
}
-export default function DialogSuspense({
+export function DialogSuspense({
children
}) {
return (
diff --git a/src/components/Dialog/index.js b/src/components/Dialog/index.js
new file mode 100644
index 000000000..c7c0982fb
--- /dev/null
+++ b/src/components/Dialog/index.js
@@ -0,0 +1,6 @@
+
+
+export * from './Dialog';
+export * from './DialogFooterActions';
+export * from './DialogSuspense';
+export * from './DialogContent';
\ No newline at end of file
diff --git a/src/components/DialogsContainer.js b/src/components/DialogsContainer.js
index 1a31006dc..db9f883c2 100644
--- a/src/components/DialogsContainer.js
+++ b/src/components/DialogsContainer.js
@@ -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() {
+
+
+
+
+
+
+
);
}
diff --git a/src/components/DisplayNameList.js b/src/components/DisplayNameList.js
index cb09cb4ac..3deba980f 100644
--- a/src/components/DisplayNameList.js
+++ b/src/components/DisplayNameList.js
@@ -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}
/>
);
-}
\ No newline at end of file
+}
diff --git a/src/components/Drawer/Drawer.js b/src/components/Drawer/Drawer.js
index 96f7e0b5a..fe660deb2 100644
--- a/src/components/Drawer/Drawer.js
+++ b/src/components/Drawer/Drawer.js
@@ -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}
+ {children}
);
}
diff --git a/src/components/Drawer/DrawerProvider.js b/src/components/Drawer/DrawerProvider.js
new file mode 100644
index 000000000..7b086cd32
--- /dev/null
+++ b/src/components/Drawer/DrawerProvider.js
@@ -0,0 +1,16 @@
+import React, { createContext, useContext } from 'react';
+
+const DrawerContext = createContext();
+
+/**
+ * Account form provider.
+ */
+function DrawerProvider({ ...props }) {
+ const provider = { ...props };
+
+ return ;
+}
+
+const useDrawerContext = () => useContext(DrawerContext);
+
+export { DrawerProvider, useDrawerContext };
diff --git a/src/components/DrawersContainer.js b/src/components/DrawersContainer.js
index f240fd1bf..135dcb8a3 100644
--- a/src/components/DrawersContainer.js
+++ b/src/components/DrawersContainer.js
@@ -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() {
-
+
+
+
+
);
}
diff --git a/src/components/ItemsSuggestField.js b/src/components/ItemsSuggestField.js
index a7223bfe9..7473a6bb5 100644
--- a/src/components/ItemsSuggestField.js
+++ b/src/components/ItemsSuggestField.js
@@ -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 { 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 (
+
+ );
+};
+
+// Item renderer.
+const itemRenderer = (item, { modifiers, 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 }) => (
-
- ));
-
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 (
);
}
+
+export default R.compose(withDrawerActions)(ItemsSuggestField);
diff --git a/src/components/MoreMenutItems.js b/src/components/MoreMenutItems.js
new file mode 100644
index 000000000..2201a7206
--- /dev/null
+++ b/src/components/MoreMenutItems.js
@@ -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 (
+
+ }
+ />
+
+ }
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ modifiers={{
+ offset: { offset: '0, 4' },
+ }}
+ >
+ } minimal={true} />
+
+ );
+}
+
+export default MoreMenuItems;
diff --git a/src/components/MoreVertMenutItems.js b/src/components/MoreVertMenutItems.js
deleted file mode 100644
index 9282f33c1..000000000
--- a/src/components/MoreVertMenutItems.js
+++ /dev/null
@@ -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 }) => (
-
- );
- const handleMenuSelect = (type) => {
- onItemSelect && onItemSelect(type);
- };
-
- return (
-
- );
-}
-
-export default MoreVertMenutItems;
diff --git a/src/components/SMSPreview/index.js b/src/components/SMSPreview/index.js
new file mode 100644
index 000000000..0b53aaf4b
--- /dev/null
+++ b/src/components/SMSPreview/index.js
@@ -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 (
+
+
+ {message}
+
+ );
+}
+
+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;
+`;
diff --git a/src/components/index.js b/src/components/index.js
index aa156307e..f30398145 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -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,
};
diff --git a/src/config/preferencesMenu.js b/src/config/preferencesMenu.js
index 3e1b981dd..031dfe0e1 100644
--- a/src/config/preferencesMenu.js
+++ b/src/config/preferencesMenu.js
@@ -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: ,
+ text: ,
disabled: false,
href: '/preferences/general',
},
{
- text: ,
+ text: ,
href: '/preferences/users',
},
{
- text: ,
-
+ text: ,
+
href: '/preferences/currencies',
},
{
- text: ,
+ text: ,
disabled: false,
href: '/preferences/accountant',
},
{
- text: ,
+ text: ,
disabled: false,
href: '/preferences/items',
},
+ {
+ text: ,
+ disabled: false,
+ href: '/preferences/sms-message',
+ },
];
diff --git a/src/containers/Accounting/MakeJournal/components.js b/src/containers/Accounting/MakeJournal/components.js
index e7a5dbf86..f79926091 100644
--- a/src/containers/Accounting/MakeJournal/components.js
+++ b/src/containers/Accounting/MakeJournal/components.js
@@ -128,6 +128,7 @@ export const useJournalTableEntriesColumns = () => {
className: 'account',
disableSortBy: true,
width: 160,
+ fieldProps: { allowCreate: true }
},
{
Header: CreditHeaderCell,
diff --git a/src/containers/Customers/CustomerForm/CustomerFloatingActions.js b/src/containers/Customers/CustomerForm/CustomerFloatingActions.js
index 2db9b862b..58d0a5f27 100644
--- a/src/containers/Customers/CustomerForm/CustomerFloatingActions.js
+++ b/src/containers/Customers/CustomerForm/CustomerFloatingActions.js
@@ -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() {
{/* ----------- Save and New ----------- */}
-
+
{/* ----------- Clear & Reset----------- */}
);
}
+
+const SaveButton = styled(Button)`
+ min-width: 100px;
+`;
diff --git a/src/containers/Customers/CustomerForm/CustomerForm.js b/src/containers/Customers/CustomerForm/CustomerForm.js
index c8648bfd0..7adf26da5 100644
--- a/src/containers/Customers/CustomerForm/CustomerForm.js
+++ b/src/containers/Customers/CustomerForm/CustomerForm.js
@@ -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 (
-
+
+
+
);
}
-
-export default compose(withCurrentOrganization())(CustomerForm);
diff --git a/src/containers/Customers/CustomerForm/CustomerFormAfterPrimarySection.js b/src/containers/Customers/CustomerForm/CustomerFormAfterPrimarySection.js
index d409b9d37..cabaea3ff 100644
--- a/src/containers/Customers/CustomerForm/CustomerFormAfterPrimarySection.js
+++ b/src/containers/Customers/CustomerForm/CustomerFormAfterPrimarySection.js
@@ -6,7 +6,6 @@ import intl from 'react-intl-universal';
import { inputIntent } from 'utils';
export default function CustomerFormAfterPrimarySection({}) {
-
return (
{/*------------ Customer email -----------*/}
@@ -31,21 +30,21 @@ export default function CustomerFormAfterPrimarySection({}) {
inline={true}
>
-
+
{({ field, meta: { error, touched } }) => (
)}
-
+
{({ field, meta: { error, touched } }) => (
)}
diff --git a/src/containers/Customers/CustomerForm/CustomerFormFormik.js b/src/containers/Customers/CustomerForm/CustomerFormFormik.js
new file mode 100644
index 000000000..ff5851d20
--- /dev/null
+++ b/src/containers/Customers/CustomerForm/CustomerFormFormik.js
@@ -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 (
+
+ );
+}
+
+export default compose(withCurrentOrganization())(CustomerFormFormik);
diff --git a/src/containers/Customers/CustomerForm/CustomerFormPage.js b/src/containers/Customers/CustomerForm/CustomerFormPage.js
index 50d264d1b..5cf1d3971 100644
--- a/src/containers/Customers/CustomerForm/CustomerFormPage.js
+++ b/src/containers/Customers/CustomerForm/CustomerFormPage.js
@@ -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 (
-
-
-
-
+
+ {children}
+
+ );
+}
+
+/**
+ * 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 (
+
+
+
+
+
+
);
-}
\ No newline at end of file
+}
+
+const CustomerFormPageFormik = styled(CustomerFormFormik)`
+ .page-form {
+ &__floating-actions {
+ margin-left: -40px;
+ margin-right: -40px;
+ }
+ }
+`;
+
+const CustomerDashboardInsider = styled(DashboardInsider)`
+ padding-bottom: 64px;
+`;
diff --git a/src/containers/Customers/CustomerForm/CustomerFormProvider.js b/src/containers/Customers/CustomerForm/CustomerFormProvider.js
index 64fa78685..ae678b4b8 100644
--- a/src/containers/Customers/CustomerForm/CustomerFormProvider.js
+++ b/src/containers/Customers/CustomerForm/CustomerFormProvider.js
@@ -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 (
-
-
-
- );
+ return ;
}
const useCustomerFormContext = () => React.useContext(CustomerFormContext);
diff --git a/src/containers/Customers/CustomerForm/utils.js b/src/containers/Customers/CustomerForm/utils.js
new file mode 100644
index 000000000..d72cfb92d
--- /dev/null
+++ b/src/containers/Customers/CustomerForm/utils.js
@@ -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'),
+};
diff --git a/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSDialogContent.js b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSDialogContent.js
new file mode 100644
index 000000000..6384dcc1e
--- /dev/null
+++ b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSDialogContent.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { NotifyEstimateViaSMSFormProvider } from './NotifyEstimateViaSMSFormProvider';
+import NotifyEstimateViaSMSForm from './NotifyEstimateViaSMSForm';
+
+export default function NotifyEstimateViaSMSDialogContent({
+ // #ownProps
+ dialogName,
+ estimate,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSForm.js b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSForm.js
new file mode 100644
index 000000000..13bcefe70
--- /dev/null
+++ b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSForm.js
@@ -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 (
+
+ );
+}
+
+export default compose(withDialogActions)(NotifyEstimateViaSMSForm);
diff --git a/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSFormProvider.js b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSFormProvider.js
new file mode 100644
index 000000000..f913bdc23
--- /dev/null
+++ b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/NotifyEstimateViaSMSFormProvider.js
@@ -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 (
+
+
+
+ );
+}
+
+const useEstimateViaSMSContext = () =>
+ React.useContext(NotifyEstimateViaSMSContext);
+
+export { NotifyEstimateViaSMSFormProvider, useEstimateViaSMSContext };
diff --git a/src/containers/Dialogs/NotifyEstimateViaSMSDialog/index.js b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/index.js
new file mode 100644
index 000000000..a935e5852
--- /dev/null
+++ b/src/containers/Dialogs/NotifyEstimateViaSMSDialog/index.js
@@ -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 (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--notify-vis-sms'}
+ >
+
+
+
+
+ );
+}
+
+export default compose(withDialogRedux())(NotifyEstimateViaSMSDialog);
diff --git a/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSDialogContent.js b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSDialogContent.js
new file mode 100644
index 000000000..349312e89
--- /dev/null
+++ b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSDialogContent.js
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { NotifyInvoiceViaSMSFormProvider } from './NotifyInvoiceViaSMSFormProvider';
+import NotifyInvoiceViaSMSForm from './NotifyInvoiceViaSMSForm';
+
+export default function NotifyInvoiceViaSMSDialogContent({
+ // #ownProps
+ dialogName,
+ invoiceId,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSForm.js b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSForm.js
new file mode 100644
index 000000000..4ce2349fe
--- /dev/null
+++ b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSForm.js
@@ -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 (
+
+ );
+}
+
+export default compose(withDialogActions)(NotifyInvoiceViaSMSForm);
diff --git a/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSFormProvider.js b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSFormProvider.js
new file mode 100644
index 000000000..2d1bec964
--- /dev/null
+++ b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/NotifyInvoiceViaSMSFormProvider.js
@@ -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 (
+
+
+
+ );
+}
+
+const useNotifyInvoiceViaSMSContext = () =>
+ React.useContext(NotifyInvoiceViaSMSContext);
+
+export { NotifyInvoiceViaSMSFormProvider, useNotifyInvoiceViaSMSContext };
diff --git a/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/index.js b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/index.js
new file mode 100644
index 000000000..91f0c75e6
--- /dev/null
+++ b/src/containers/Dialogs/NotifyInvoiceViaSMSDialog/index.js
@@ -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 (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--notify-vis-sms'}
+ >
+
+
+
+
+ );
+}
+
+export default compose(withDialogRedux())(NotifyInvoiceViaSMSDialog);
diff --git a/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaFormProvider.js b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaFormProvider.js
new file mode 100644
index 000000000..46c164a0f
--- /dev/null
+++ b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaFormProvider.js
@@ -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 (
+
+
+
+ );
+}
+
+const useNotifyPaymentReceiveViaSMSContext = () =>
+ React.useContext(NotifyPaymentReceiveViaSMSContext);
+
+export {
+ NotifyPaymentReceiveViaFormProvider,
+ useNotifyPaymentReceiveViaSMSContext,
+};
diff --git a/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaSMSContent.js b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaSMSContent.js
new file mode 100644
index 000000000..8b1f49934
--- /dev/null
+++ b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaSMSContent.js
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { NotifyPaymentReceiveViaFormProvider } from './NotifyPaymentReceiveViaFormProvider';
+import NotifyPaymentReceiveViaSMSForm from './NotifyPaymentReceiveViaSMSForm';
+
+export default function NotifyPaymentReceiveViaSMSContent({
+ // #ownProps
+ dialogName,
+ paymentReceive,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaSMSForm.js b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaSMSForm.js
new file mode 100644
index 000000000..42604817e
--- /dev/null
+++ b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/NotifyPaymentReceiveViaSMSForm.js
@@ -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 (
+
+ );
+}
+export default compose(withDialogActions)(NotifyPaymentReceiveViaSMSForm);
diff --git a/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/index.js b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/index.js
new file mode 100644
index 000000000..f9ce607f9
--- /dev/null
+++ b/src/containers/Dialogs/NotifyPaymentReceiveViaSMSDialog/index.js
@@ -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 (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--notify-vis-sms'}
+ >
+
+
+
+
+ );
+}
+export default compose(withDialogRedux())(NotifyPaymentReciveViaSMSDialog);
diff --git a/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSDialogContent.js b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSDialogContent.js
new file mode 100644
index 000000000..7f4e40e04
--- /dev/null
+++ b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSDialogContent.js
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { NotifyReceiptViaSMSFormProvider } from './NotifyReceiptViaSMSFormProvider';
+import NotifyReceiptViaSMSForm from './NotifyReceiptViaSMSForm';
+
+export default function NotifyReceiptViaSMSDialogContent({
+ // #ownProps
+ dialogName,
+ receipt,
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSForm.js b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSForm.js
new file mode 100644
index 000000000..185d25301
--- /dev/null
+++ b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSForm.js
@@ -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 (
+
+ );
+}
+
+export default compose(withDialogActions)(NotifyReceiptViaSMSForm);
diff --git a/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSFormProvider.js b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSFormProvider.js
new file mode 100644
index 000000000..cc2a00089
--- /dev/null
+++ b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/NotifyReceiptViaSMSFormProvider.js
@@ -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 (
+
+
+
+ );
+}
+
+const useNotifyReceiptViaSMSContext = () =>
+ React.useContext(NotifyReceiptViaSMSContext);
+
+export { NotifyReceiptViaSMSFormProvider, useNotifyReceiptViaSMSContext };
diff --git a/src/containers/Dialogs/NotifyReceiptViaSMSDialog/index.js b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/index.js
new file mode 100644
index 000000000..67ae48813
--- /dev/null
+++ b/src/containers/Dialogs/NotifyReceiptViaSMSDialog/index.js
@@ -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 (
+ }
+ isOpen={isOpen}
+ canEscapeJeyClose={true}
+ autoFocus={true}
+ className={'dialog--notify-vis-sms'}
+ >
+
+
+
+
+ );
+}
+
+export default compose(withDialogRedux())(NotifyReceiptViaSMSDialog);
diff --git a/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js b/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js
index 967fb68e0..852dca76e 100644
--- a/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js
+++ b/src/containers/Dialogs/QuickPaymentMadeFormDialog/QuickPaymentMadeFormFields.js
@@ -37,7 +37,7 @@ export default function QuickPaymentMadeFormFields() {
const { accounts } = useQuickPaymentMadeContext();
// Intl context.
-
+
const paymentMadeFieldRef = useAutofocus();
return (
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageDialogContent.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageDialogContent.js
new file mode 100644
index 000000000..b7cedf1b3
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageDialogContent.js
@@ -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 (
+
+
+
+ );
+}
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageDialogProvider.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageDialogProvider.js
new file mode 100644
index 000000000..8aedd7e53
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageDialogProvider.js
@@ -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 (
+
+
+
+ );
+}
+
+const useSMSMessageDialogContext = () =>
+ React.useContext(SMSMessageDialogContext);
+
+export { SMSMessageDialogProvider, useSMSMessageDialogContext };
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.js
new file mode 100644
index 000000000..3f2e8ecc9
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.js
@@ -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 (
+
+ );
+}
+
+export default compose(withDialogActions)(SMSMessageForm);
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.schema.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.schema.js
new file mode 100644
index 000000000..441e73381
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageForm.schema.js
@@ -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;
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormContent.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormContent.js
new file mode 100644
index 000000000..f9f72adad
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormContent.js
@@ -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 (
+
+ );
+}
+
+/**
+ * SMS Message preview section.
+ * @returns {JSX}
+ */
+function SMSMessagePreviewSection() {
+ const {
+ values: { message_text: message },
+ } = useFormikContext();
+
+ const messagesUnits = getSMSUnits(message);
+
+ return (
+
+
+
+ {intl.formatHTMLMessage(
+ { id: 'sms_message.dialog.sms_note' },
+ {
+ value: messagesUnits,
+ },
+ )}
+
+
+ );
+}
+
+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;
+ `)}
+`;
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFields.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFields.js
new file mode 100644
index 000000000..1ba600edc
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFields.js
@@ -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 (
+
+ {/* ----------- Message Text ----------- */}
+
+ {({ field, meta: { error, touched } }) => (
+ }
+ className={'form-group--message_text'}
+ intent={inputIntent({ error, touched })}
+ helperText={
+ <>
+
+
+
+
+ >
+ }
+ >
+
+
+ )}
+
+
+ );
+}
+
+const ResetButton = styled(Button)`
+ font-size: 12px;
+`;
diff --git a/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js
new file mode 100644
index 000000000..e823fba9d
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/SMSMessageFormFloatingActions.js
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(withDialogActions)(SMSMessageFormFloatingActions);
diff --git a/src/containers/Dialogs/SMSMessageDialog/index.js b/src/containers/Dialogs/SMSMessageDialog/index.js
new file mode 100644
index 000000000..aadb1b13e
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/index.js
@@ -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 (
+
+ );
+}
+
+export default compose(withDialogRedux())(SMSMessageDialog);
diff --git a/src/containers/Dialogs/SMSMessageDialog/utils.js b/src/containers/Dialogs/SMSMessageDialog/utils.js
new file mode 100644
index 000000000..6b94176bd
--- /dev/null
+++ b/src/containers/Dialogs/SMSMessageDialog/utils.js
@@ -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,
+ });
+ }
+};
diff --git a/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.js b/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.js
index 0c468be41..1a7c3c051 100644
--- a/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.js
+++ b/src/containers/Drawers/EstimateDetailDrawer/EstimateDetailActionsBar.js
@@ -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 (
@@ -75,6 +79,12 @@ function EstimateDetailActionsBar({
intent={Intent.DANGER}
onClick={handleDeleteEstimate}
/>
+
+
);
diff --git a/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js b/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js
index b83b8a6ed..b4fa903b7 100644
--- a/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js
+++ b/src/containers/Drawers/InvoiceDetailDrawer/InvoiceDetailActionsBar.js
@@ -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({
/>
diff --git a/src/containers/Drawers/InvoiceDetailDrawer/utils.js b/src/containers/Drawers/InvoiceDetailDrawer/utils.js
index 26bd03fab..68c4cbc29 100644
--- a/src/containers/Drawers/InvoiceDetailDrawer/utils.js
+++ b/src/containers/Drawers/InvoiceDetailDrawer/utils.js
@@ -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 (
{
}
- onClick={onDialog}
+ onClick={onBadDebt}
/>
}
/>
+ }
+ />
}
>
diff --git a/src/containers/Drawers/PaymentReceiveDetailDrawer/PaymentReceiveActionsBar.js b/src/containers/Drawers/PaymentReceiveDetailDrawer/PaymentReceiveActionsBar.js
index 1f4939feb..94bb19c65 100644
--- a/src/containers/Drawers/PaymentReceiveDetailDrawer/PaymentReceiveActionsBar.js
+++ b/src/containers/Drawers/PaymentReceiveDetailDrawer/PaymentReceiveActionsBar.js
@@ -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 (
@@ -63,6 +71,12 @@ function PaymentReceiveActionsBar({
intent={Intent.DANGER}
onClick={handleDeletePaymentReceive}
/>
+
+
);
diff --git a/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCreateCustomerDrawerContent.js b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCreateCustomerDrawerContent.js
new file mode 100644
index 000000000..260b5ef15
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCreateCustomerDrawerContent.js
@@ -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 (
+
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCustomerFormDrawer.js b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCustomerFormDrawer.js
new file mode 100644
index 000000000..1b665c384
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateCustomerDrawer/QuickCustomerFormDrawer.js
@@ -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 {children};
+}
+
+/**
+ * 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 (
+
+
+
+
+
+
+
+ );
+}
+
+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;
+ }
+ }
+`;
diff --git a/src/containers/Drawers/QuickCreateCustomerDrawer/index.js b/src/containers/Drawers/QuickCreateCustomerDrawer/index.js
new file mode 100644
index 000000000..ce970532e
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateCustomerDrawer/index.js
@@ -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 (
+
+
+
+
+
+ );
+}
+
+export default compose(withDrawers())(QuickCreateCustomerDrawer);
diff --git a/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerContent.js b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerContent.js
new file mode 100644
index 000000000..b470822f8
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerContent.js
@@ -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 (
+
+ }
+ />
+
+
+
+
+ );
+}
diff --git a/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerForm.js b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerForm.js
new file mode 100644
index 000000000..ada5848bb
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateItemDrawer/QuickCreateItemDrawerForm.js
@@ -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 {children};
+}
+
+/**
+ * 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 (
+
+
+
+
+
+
+
+ );
+}
+
+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;
+ }
+`;
diff --git a/src/containers/Drawers/QuickCreateItemDrawer/index.js b/src/containers/Drawers/QuickCreateItemDrawer/index.js
new file mode 100644
index 000000000..972288d2a
--- /dev/null
+++ b/src/containers/Drawers/QuickCreateItemDrawer/index.js
@@ -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 (
+
+
+
+
+
+ );
+}
+
+export default compose(withDrawers())(QuickCreateItemDrawer);
diff --git a/src/containers/Drawers/QuickWriteVendorDrawer/QuickVendorFormDrawer.js b/src/containers/Drawers/QuickWriteVendorDrawer/QuickVendorFormDrawer.js
new file mode 100644
index 000000000..6996dac2a
--- /dev/null
+++ b/src/containers/Drawers/QuickWriteVendorDrawer/QuickVendorFormDrawer.js
@@ -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 {children};
+}
+
+/**
+ * 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 (
+
+
+
+
+
+
+
+ );
+}
+
+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;
+ }
+ }
+`;
diff --git a/src/containers/Drawers/QuickWriteVendorDrawer/QuickWriteVendorDrawerContent.js b/src/containers/Drawers/QuickWriteVendorDrawer/QuickWriteVendorDrawerContent.js
new file mode 100644
index 000000000..b686d2211
--- /dev/null
+++ b/src/containers/Drawers/QuickWriteVendorDrawer/QuickWriteVendorDrawerContent.js
@@ -0,0 +1,25 @@
+import React from 'react';
+import {
+ DrawerHeaderContent,
+ DrawerBody,
+ FormattedMessage as T,
+} from 'components';
+
+import QuickVendorFormDrawer from './QuickVendorFormDrawer';
+
+/**
+ * Quick create/edit vendor drawer.
+ */
+export default function QuickWriteVendorDrawerContent({ displayName }) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/Drawers/QuickWriteVendorDrawer/index.js b/src/containers/Drawers/QuickWriteVendorDrawer/index.js
new file mode 100644
index 000000000..6bd8ea2a4
--- /dev/null
+++ b/src/containers/Drawers/QuickWriteVendorDrawer/index.js
@@ -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 (
+
+
+
+
+
+ );
+}
+
+export default R.compose(withDrawers())(QuickWriteVendorDrawer);
diff --git a/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailActionBar.js b/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailActionBar.js
index b6e17c191..608805a00 100644
--- a/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailActionBar.js
+++ b/src/containers/Drawers/ReceiptDetailDrawer/ReceiptDetailActionBar.js
@@ -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 (
@@ -69,6 +74,12 @@ function ReceiptDetailActionBar({
intent={Intent.DANGER}
onClick={safeCallback(onDeleteReceipt)}
/>
+
+
);
diff --git a/src/containers/Entries/components.js b/src/containers/Entries/components.js
index 85c057e04..80fcce6e3 100644
--- a/src/containers/Entries/components.js
+++ b/src/containers/Entries/components.js
@@ -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'),
diff --git a/src/containers/Entries/utils.js b/src/containers/Entries/utils.js
index 7bfde9d47..22d8adb54 100644
--- a/src/containers/Entries/utils.js
+++ b/src/containers/Entries/utils.js
@@ -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);
+};
diff --git a/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js b/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js
index 407abb290..5208759fc 100644
--- a/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js
+++ b/src/containers/Expenses/ExpenseForm/ExpenseFormHeaderFields.js
@@ -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={}
selectedAccountId={value}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.CURRENT_ASSET]}
+ allowCreate={true}
/>
)}
@@ -137,13 +138,14 @@ export default function ExpenseFormHeader() {
helperText={}
inline={true}
>
- }
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Expenses/ExpenseForm/components.js b/src/containers/Expenses/ExpenseForm/components.js
index 64f275d00..fb52ac569 100644
--- a/src/containers/Expenses/ExpenseForm/components.js
+++ b/src/containers/Expenses/ExpenseForm/components.js
@@ -113,6 +113,7 @@ export function useExpenseFormTableColumns({ landedCost }) {
disableSortBy: true,
width: 40,
filterAccountsByRootTypes: ['expense'],
+ fieldProps: { allowCreate: true },
},
{
Header: ExpenseAmountHeaderCell,
diff --git a/src/containers/FinancialStatements/BalanceSheet/components.js b/src/containers/FinancialStatements/BalanceSheet/components.js
index 9439acb74..be3962ed9 100644
--- a/src/containers/FinancialStatements/BalanceSheet/components.js
+++ b/src/containers/FinancialStatements/BalanceSheet/components.js
@@ -27,7 +27,7 @@ export function BalanceSheetAlerts() {
{' '}
-
+
diff --git a/src/containers/FinancialStatements/CustomersBalanceSummary/CustomersBalanceSummaryTable.js b/src/containers/FinancialStatements/CustomersBalanceSummary/CustomersBalanceSummaryTable.js
index 6eb727d9d..29cd28088 100644
--- a/src/containers/FinancialStatements/CustomersBalanceSummary/CustomersBalanceSummaryTable.js
+++ b/src/containers/FinancialStatements/CustomersBalanceSummary/CustomersBalanceSummaryTable.js
@@ -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 (
diff --git a/src/containers/FinancialStatements/CustomersTransactions/CustomersTransactionsTable.js b/src/containers/FinancialStatements/CustomersTransactions/CustomersTransactionsTable.js
index 1215da2f7..295725768 100644
--- a/src/containers/FinancialStatements/CustomersTransactions/CustomersTransactionsTable.js
+++ b/src/containers/FinancialStatements/CustomersTransactions/CustomersTransactionsTable.js
@@ -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 (
diff --git a/src/containers/FinancialStatements/CustomersTransactions/components.js b/src/containers/FinancialStatements/CustomersTransactions/components.js
index 989a6c811..938e7b019 100644
--- a/src/containers/FinancialStatements/CustomersTransactions/components.js
+++ b/src/containers/FinancialStatements/CustomersTransactions/components.js
@@ -22,14 +22,14 @@ export const useCustomersTransactionsColumns = () => {
return (
{cells[0].value}
);
},
className: 'customer_name',
- textOverview: true,
+ // textOverview: true,
},
{
Header: intl.get('account_name'),
diff --git a/src/containers/FinancialStatements/GeneralLedger/components.js b/src/containers/FinancialStatements/GeneralLedger/components.js
index 9ccb69a7f..ee60301f0 100644
--- a/src/containers/FinancialStatements/GeneralLedger/components.js
+++ b/src/containers/FinancialStatements/GeneralLedger/components.js
@@ -35,7 +35,7 @@ export function useGeneralLedgerTableColumns() {
return row.date;
},
className: 'date',
- textOverview: true,
+ // textOverview: true,
width: 120,
},
{
diff --git a/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js b/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js
index 5f730f76d..5ab72ed72 100644
--- a/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js
+++ b/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js
@@ -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 (
diff --git a/src/containers/FinancialStatements/InventoryItemDetails/utils.js b/src/containers/FinancialStatements/InventoryItemDetails/utils.js
index 5824c4d52..35d2e29f1 100644
--- a/src/containers/FinancialStatements/InventoryItemDetails/utils.js
+++ b/src/containers/FinancialStatements/InventoryItemDetails/utils.js
@@ -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,
diff --git a/src/containers/FinancialStatements/VendorsBalanceSummary/VendorsBalanceSummaryTable.js b/src/containers/FinancialStatements/VendorsBalanceSummary/VendorsBalanceSummaryTable.js
index 7cb510ab9..55d57644a 100644
--- a/src/containers/FinancialStatements/VendorsBalanceSummary/VendorsBalanceSummaryTable.js
+++ b/src/containers/FinancialStatements/VendorsBalanceSummary/VendorsBalanceSummaryTable.js
@@ -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 (
diff --git a/src/containers/FinancialStatements/VendorsTransactions/VendorsTransactionsTable.js b/src/containers/FinancialStatements/VendorsTransactions/VendorsTransactionsTable.js
index 98343c537..637115d88 100644
--- a/src/containers/FinancialStatements/VendorsTransactions/VendorsTransactionsTable.js
+++ b/src/containers/FinancialStatements/VendorsTransactions/VendorsTransactionsTable.js
@@ -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 (
diff --git a/src/containers/FinancialStatements/VendorsTransactions/components.js b/src/containers/FinancialStatements/VendorsTransactions/components.js
index 14c9f3145..bfb8e0fe5 100644
--- a/src/containers/FinancialStatements/VendorsTransactions/components.js
+++ b/src/containers/FinancialStatements/VendorsTransactions/components.js
@@ -21,14 +21,14 @@ export const useVendorsTransactionsColumns = () => {
return (
{cells[0].value}
);
},
className: 'vendor_name',
- textOverview: true,
+ // textOverview: true,
// width: 240,
},
{
diff --git a/src/containers/Items/ItemForm.js b/src/containers/Items/ItemForm.js
index 99847d298..6638fe57d 100644
--- a/src/containers/Items/ItemForm.js
+++ b/src/containers/Items/ItemForm.js
@@ -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 (
+
+ {children}
+
+ );
+}
+
+/**
+ * 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 (
-
+
+
+
+
+
+
);
}
+
+const DashboardItemFormPageInsider = styled(DashboardInsider)`
+ padding-bottom: 64px;
+`;
+
+const ItemFormPageFormik = styled(ItemFormFormik)`
+ .page-form {
+ &__floating-actions {
+ margin-left: -40px;
+ margin-right: -40px;
+ }
+ }
+`;
diff --git a/src/containers/Items/ItemFormBody.js b/src/containers/Items/ItemFormBody.js
index 5305e8ba2..2a7e06a19 100644
--- a/src/containers/Items/ItemFormBody.js
+++ b/src/containers/Items/ItemFormBody.js
@@ -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}
/>
)}
@@ -230,6 +231,7 @@ function ItemFormBody({ organization: { base_currency } }) {
disabled={!form.values.purchasable}
filterByParentTypes={[ACCOUNT_PARENT_TYPE.EXPENSE]}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Items/ItemFormFloatingActions.js b/src/containers/Items/ItemFormFloatingActions.js
index eacce65db..fbdb1a152 100644
--- a/src/containers/Items/ItemFormFloatingActions.js
+++ b/src/containers/Items/ItemFormFloatingActions.js
@@ -1,20 +1,19 @@
import React from 'react';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
-import { FormattedMessage as T } from 'components';
-import { useHistory } from 'react-router-dom';
-import classNames from 'classnames';
+import styled from 'styled-components';
import { FastField, useFormikContext } from 'formik';
+import classNames from 'classnames';
+
+import { FormattedMessage as T } from 'components';
import { CLASSES } from 'common/classes';
import { useItemFormContext } from './ItemFormProvider';
+import { saveInvoke } from '../../utils';
/**
* Item form floating actions.
*/
-export default function ItemFormFloatingActions() {
- // History context.
- const history = useHistory();
-
+export default function ItemFormFloatingActions({ onCancel }) {
// Item form context.
const { setSubmitPayload, isNewMode } = useItemFormContext();
@@ -23,7 +22,7 @@ export default function ItemFormFloatingActions() {
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
- history.goBack();
+ saveInvoke(onCancel, event);
};
// Handle submit button click.
@@ -38,7 +37,7 @@ export default function ItemFormFloatingActions() {
return (
-
{isNewMode ? : }
-
+
);
}
+
+const SaveButton = styled(Button)`
+ min-width: 100px;
+`;
\ No newline at end of file
diff --git a/src/containers/Items/ItemFormFormik.js b/src/containers/Items/ItemFormFormik.js
new file mode 100644
index 000000000..7876cd6ed
--- /dev/null
+++ b/src/containers/Items/ItemFormFormik.js
@@ -0,0 +1,112 @@
+import React from 'react';
+import { Formik, Form } from 'formik';
+import { Intent } from '@blueprintjs/core';
+import intl from 'react-intl-universal';
+import classNames from 'classnames';
+
+import 'style/pages/Items/Form.scss';
+
+import { CLASSES } from 'common/classes';
+import AppToaster from 'components/AppToaster';
+import ItemFormPrimarySection from './ItemFormPrimarySection';
+import ItemFormBody from './ItemFormBody';
+import ItemFormFloatingActions from './ItemFormFloatingActions';
+import ItemFormInventorySection from './ItemFormInventorySection';
+
+import {
+ transformSubmitRequestErrors,
+ useItemFormInitialValues,
+} from './utils';
+import { EditItemFormSchema, CreateItemFormSchema } from './ItemForm.schema';
+
+import { useItemFormContext } from './ItemFormProvider';
+import { safeInvoke } from '@blueprintjs/core/lib/esm/common/utils';
+
+/**
+ * Item form.
+ */
+export default function ItemFormFormik({
+ // #ownProps
+ initialValues: initialValuesComponent,
+ onSubmitSuccess,
+ onSubmitError,
+ onCancel,
+ className,
+}) {
+ // Item form context.
+ const {
+ itemId,
+ item,
+ accounts,
+ createItemMutate,
+ editItemMutate,
+ submitPayload,
+ isNewMode,
+ } = useItemFormContext();
+
+ // Initial values in create and edit mode.
+ const initialValues = useItemFormInitialValues(item, initialValuesComponent);
+
+ // Handles the form submit.
+ const handleFormSubmit = (values, form) => {
+ const { setSubmitting, resetForm, setErrors } = form;
+ const formValues = { ...values };
+
+ setSubmitting(true);
+
+ // Handle response succes.
+ const onSuccess = (response) => {
+ AppToaster.show({
+ message: intl.get(
+ isNewMode
+ ? 'the_item_has_been_created_successfully'
+ : 'the_item_has_been_edited_successfully',
+ {
+ number: itemId,
+ },
+ ),
+ intent: Intent.SUCCESS,
+ });
+ resetForm();
+ setSubmitting(false);
+
+ safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
+ };
+ // Handle response error.
+ const onError = (errors) => {
+ setSubmitting(false);
+
+ if (errors) {
+ const _errors = transformSubmitRequestErrors(errors);
+ setErrors({ ..._errors });
+ }
+ safeInvoke(onSubmitError, values, form, submitPayload, errors);
+ };
+ if (isNewMode) {
+ createItemMutate(formValues).then(onSuccess).catch(onError);
+ } else {
+ editItemMutate([itemId, formValues]).then(onSuccess).catch(onError);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/containers/Items/ItemFormPage.js b/src/containers/Items/ItemFormPage.js
index 5852b1bdd..c57f03661 100644
--- a/src/containers/Items/ItemFormPage.js
+++ b/src/containers/Items/ItemFormPage.js
@@ -1,9 +1,7 @@
import React from 'react';
import { useParams } from 'react-router-dom';
-import { ItemFormProvider } from './ItemFormProvider';
-import DashboardCard from 'components/Dashboard/DashboardCard';
-import ItemForm from 'containers/Items/ItemForm';
+import ItemForm from './ItemForm';
/**
* Item form page.
@@ -12,11 +10,5 @@ export default function ItemFormPage() {
const { id } = useParams();
const idInteger = parseInt(id, 10);
- return (
-
-
-
-
-
- );
-}
\ No newline at end of file
+ return ;
+}
diff --git a/src/containers/Items/ItemFormProvider.js b/src/containers/Items/ItemFormProvider.js
index b491a2d5b..85dcd7276 100644
--- a/src/containers/Items/ItemFormProvider.js
+++ b/src/containers/Items/ItemFormProvider.js
@@ -1,8 +1,5 @@
-import React, { useEffect, createContext, useState } from 'react';
-import intl from 'react-intl-universal';
+import React, { createContext, useState } from 'react';
import { useLocation } from 'react-router-dom';
-
-import DashboardInsider from 'components/Dashboard/DashboardInsider';
import {
useItem,
useSettingsItems,
@@ -11,7 +8,6 @@ import {
useEditItem,
useAccounts,
} from 'hooks/query';
-import { useDashboardPageTitle } from 'hooks/state';
import { useWatchItemError } from './utils';
const ItemFormContext = createContext();
@@ -59,6 +55,13 @@ function ItemFormProvider({ itemId, ...props }) {
// Detarmines whether the form new mode.
const isNewMode = duplicateId || !itemId;
+ // Detarmines the form loading state.
+ const isFormLoading =
+ isItemsSettingsLoading ||
+ isAccountsLoading ||
+ isItemsCategoriesLoading ||
+ isItemLoading;
+
// Provider state.
const provider = {
itemId,
@@ -68,6 +71,7 @@ function ItemFormProvider({ itemId, ...props }) {
submitPayload,
isNewMode,
+ isFormLoading,
isAccountsLoading,
isItemsCategoriesLoading,
isItemLoading,
@@ -77,27 +81,7 @@ function ItemFormProvider({ itemId, ...props }) {
setSubmitPayload,
};
- // Change page title dispatcher.
- const changePageTitle = useDashboardPageTitle();
-
- // Changes the page title in new and edit mode.
- useEffect(() => {
- isNewMode
- ? changePageTitle(intl.get('new_item'))
- : changePageTitle(intl.get('edit_item_details'));
- }, [changePageTitle, isNewMode]);
-
- const loading =
- isItemsSettingsLoading ||
- isAccountsLoading ||
- isItemsCategoriesLoading ||
- isItemLoading;
-
- return (
-
-
-
- );
+ return ;
}
const useItemFormContext = () => React.useContext(ItemFormContext);
diff --git a/src/containers/Items/utils.js b/src/containers/Items/utils.js
index 5d5dbf156..406746786 100644
--- a/src/containers/Items/utils.js
+++ b/src/containers/Items/utils.js
@@ -33,7 +33,7 @@ const defaultInitialValues = {
/**
* Initial values in create and edit mode.
*/
-export const useItemFormInitialValues = (item) => {
+export const useItemFormInitialValues = (item, initialValues) => {
const { items: itemsSettings } = useSettingsSelector();
return useMemo(
@@ -54,8 +54,9 @@ export const useItemFormInitialValues = (item) => {
transformItemFormData(item, defaultInitialValues),
defaultInitialValues,
),
+ ...initialValues,
}),
- [item, itemsSettings],
+ [item, itemsSettings, initialValues],
);
};
diff --git a/src/containers/NotifyViaSMS/NotifyViaSMSForm.js b/src/containers/NotifyViaSMS/NotifyViaSMSForm.js
new file mode 100644
index 000000000..5a51e958b
--- /dev/null
+++ b/src/containers/NotifyViaSMS/NotifyViaSMSForm.js
@@ -0,0 +1,166 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import { castArray, includes } from 'lodash';
+import { Formik, Form, useFormikContext } from 'formik';
+import styled from 'styled-components';
+import { Callout, Classes, Intent } from '@blueprintjs/core';
+
+import 'style/pages/NotifyConactViaSMS/NotifyConactViaSMSDialog.scss';
+
+import { CreateNotifyViaSMSFormSchema } from './NotifyViaSMSForm.schema';
+import NotifyViaSMSFormFields from './NotifyViaSMSFormFields';
+import NotifyViaSMSFormFloatingActions from './NotifyViaSMSFormFloatingActions';
+import { FormObserver, SMSMessagePreview } from 'components';
+
+import { transformToForm, safeInvoke } from 'utils';
+import { getSMSUnits } from './utils';
+
+import { whenRtl, whenLtr } from 'utils/styled-components';
+
+const defaultInitialValues = {
+ notification_key: '',
+ customer_name: '',
+ customer_phone_number: '',
+ sms_message: '',
+};
+
+/**
+ * Notify via sms - SMS message preview section.
+ */
+function SMSMessagePreviewSection() {
+ const {
+ values: { sms_message },
+ } = useFormikContext();
+
+ // Calculates the SMS units of message.
+ const messagesUnits = getSMSUnits(sms_message);
+
+ return (
+
+
+
+ {intl.formatHTMLMessage(
+ { id: 'notiify_via_sms.dialog.sms_note' },
+ {
+ value: messagesUnits,
+ },
+ )}
+
+
+ );
+}
+
+/**
+ * Notify Via SMS Form.
+ */
+function NotifyViaSMSForm({
+ initialValues: initialValuesComponent,
+ notificationTypes,
+ onSubmit,
+ onCancel,
+ onValuesChange,
+ calloutCodes,
+ formikProps,
+}) {
+ // Initial form values
+ const initialValues = {
+ ...defaultInitialValues,
+ ...transformToForm(initialValuesComponent, defaultInitialValues),
+ };
+ // Ensure always returns array.
+ const formattedNotificationTypes = React.useMemo(
+ () => castArray(notificationTypes),
+ [notificationTypes],
+ );
+
+ return (
+
+
+
+ );
+}
+
+/**
+ * Observes the values change of notify form.
+ */
+function NotifyObserveValuesChange({ onChange }) {
+ const { values } = useFormikContext();
+
+ // Handle the form change observe.
+ const handleChange = () => {
+ safeInvoke(onChange, values);
+ };
+ return ;
+}
+
+/**
+ * Notify via SMS form alerts.
+ */
+function NotifyViaSMSAlerts({ calloutCodes }) {
+ return [
+ includes(calloutCodes, 100) && (
+
+ {intl.get('notify_Via_sms.dialog.customer_phone_number_does_not_eixst')}
+
+ ),
+ includes(calloutCodes, 200) && (
+
+ {intl.get('notify_Via_sms.dialog.customer_phone_number_invalid')}
+
+ ),
+ ];
+}
+
+export default NotifyViaSMSForm;
+
+const NotifyContent = styled.div`
+ display: flex;
+`;
+
+const NotifyFieldsSection = styled.div`
+ flex: 1;
+ width: 65%;
+`;
+
+const SMSPreviewSectionRoot = 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;
+ `)}
+`;
+
+const SMSPreviewSectionNote = styled.div`
+ font-size: 12px;
+ opacity: 0.7;
+`;
diff --git a/src/containers/NotifyViaSMS/NotifyViaSMSForm.schema.js b/src/containers/NotifyViaSMS/NotifyViaSMSForm.schema.js
new file mode 100644
index 000000000..ff3114609
--- /dev/null
+++ b/src/containers/NotifyViaSMS/NotifyViaSMSForm.schema.js
@@ -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({
+ customer_name: Yup.string().required(),
+ customer_phone_number: Yup.number(),
+ sms_message: Yup.string().required().trim().max(DATATYPES_LENGTH.TEXT),
+});
+
+export const CreateNotifyViaSMSFormSchema = Schema;
diff --git a/src/containers/NotifyViaSMS/NotifyViaSMSFormFields.js b/src/containers/NotifyViaSMS/NotifyViaSMSFormFields.js
new file mode 100644
index 000000000..d8dcb4aa0
--- /dev/null
+++ b/src/containers/NotifyViaSMS/NotifyViaSMSFormFields.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import { FastField, ErrorMessage } from 'formik';
+import { FormGroup, InputGroup } from '@blueprintjs/core';
+import classNames from 'classnames';
+import styled from 'styled-components';
+
+import {
+ ListSelect,
+ FieldRequiredHint,
+ FormattedMessage as T,
+} from 'components';
+import { CLASSES } from 'common/classes';
+import { inputIntent } from 'utils';
+
+export default function NotifyViaSMSFormFields({ notificationTypes }) {
+ return (
+
+
+ {({ form, field: { value }, meta: { error, touched } }) => (
+ }
+ className={classNames(CLASSES.FILL)}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ >
+ {
+ form.setFieldValue('notification_key', notification.key);
+ }}
+ disabled={notificationTypes.length < 2}
+ />
+
+ )}
+
+
+ {/* ----------- Send Notification to ----------- */}
+
+ {({ form, field, meta: { error, touched } }) => (
+ }
+ className={classNames('form-group--customer-name', CLASSES.FILL)}
+ labelInfo={}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ >
+
+
+ )}
+
+
+ {/* ----------- Phone number ----------- */}
+
+ {({ form, field, meta: { error, touched } }) => (
+ }
+ labelInfo={}
+ intent={inputIntent({ error, touched })}
+ helperText={}
+ className={classNames(
+ 'form-group--customer_phone_number',
+ CLASSES.FILL,
+ )}
+ >
+
+
+ )}
+
+
+ );
+}
+
+const NotifyViaSMSFormFieldsRoot = styled.div``;
diff --git a/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js b/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js
new file mode 100644
index 000000000..dbe701aaf
--- /dev/null
+++ b/src/containers/NotifyViaSMS/NotifyViaSMSFormFloatingActions.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import { useFormikContext } from 'formik';
+import { Intent, Button, Classes } from '@blueprintjs/core';
+
+import { DialogFooterActions, FormattedMessage as T } from 'components';
+
+/**
+ *
+ */
+export default function NotifyViaSMSFormFloatingActions({ onCancel }) {
+ // Formik context.
+ const { isSubmitting } = useFormikContext();
+
+ // Handle close button click.
+ const handleCancelBtnClick = (event) => {
+ onCancel && onCancel(event);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/containers/NotifyViaSMS/utils.js b/src/containers/NotifyViaSMS/utils.js
new file mode 100644
index 000000000..2d6b78593
--- /dev/null
+++ b/src/containers/NotifyViaSMS/utils.js
@@ -0,0 +1,22 @@
+import intl from 'react-intl-universal';
+
+export const transformErrors = (errors, { setErrors, setCalloutCode }) => {
+ if (errors.some((e) => e.type === 'CUSTOMER_SMS_NOTIFY_PHONE_INVALID')) {
+ setCalloutCode([200]);
+ setErrors({
+ customer_phone_number: 'The personal phone number is invalid.',
+ });
+ }
+ if (errors.find((error) => error.type === 'CUSTOMER_HAS_NO_PHONE_NUMBER')) {
+ setCalloutCode([100]);
+ setErrors({
+ customer_phone_number: intl.get(
+ 'notify_via_sms.dialog.customer_no_phone_error_message',
+ ),
+ });
+ }
+};
+
+export const getSMSUnits = (message, threshold = 140) => {
+ return Math.ceil(message.length / threshold);
+};
diff --git a/src/containers/Preferences/SMSIntegration/SMSIntegrationProvider.js b/src/containers/Preferences/SMSIntegration/SMSIntegrationProvider.js
new file mode 100644
index 000000000..523ff944e
--- /dev/null
+++ b/src/containers/Preferences/SMSIntegration/SMSIntegrationProvider.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+import { useSettings, useSettingSMSNotifications } from 'hooks/query';
+
+const SMSIntegrationContext = React.createContext();
+
+/**
+ * SMS Integration provider.
+ */
+function SMSIntegrationProvider({ ...props }) {
+ //Fetches Organization Settings.
+ const { isLoading: isSettingsLoading } = useSettings();
+
+ const {
+ data: notifications,
+ isLoading: isSMSNotificationsLoading,
+ isFetching: isSMSNotificationsFetching,
+ } = useSettingSMSNotifications();
+
+ // Provider state.
+ const provider = {
+ notifications,
+ isSMSNotificationsLoading,
+ isSMSNotificationsFetching,
+ };
+
+ return (
+
+
+
+ );
+}
+
+const useSMSIntegrationContext = () => React.useContext(SMSIntegrationContext);
+
+export { SMSIntegrationProvider, useSMSIntegrationContext };
diff --git a/src/containers/Preferences/SMSIntegration/SMSIntegrationTabs.js b/src/containers/Preferences/SMSIntegration/SMSIntegrationTabs.js
new file mode 100644
index 000000000..ebad8a644
--- /dev/null
+++ b/src/containers/Preferences/SMSIntegration/SMSIntegrationTabs.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+
+import { Tabs, Tab } from '@blueprintjs/core';
+import classNames from 'classnames';
+import { CLASSES } from 'common/classes';
+import SMSMessagesDataTable from './SMSMessagesDataTable';
+
+import '../../../style/pages/Preferences/SMSIntegration.scss';
+
+import withDashboardActions from 'containers/Dashboard/withDashboardActions';
+
+import { compose } from 'utils';
+
+function SMSIntegrationTabs({
+ // #withDashboardActions
+ changePreferencesPageTitle,
+}) {
+ React.useEffect(() => {
+ changePreferencesPageTitle(intl.get('sms_integration.label'));
+ }, [changePreferencesPageTitle]);
+
+ return (
+
+ );
+}
+
+export default compose(withDashboardActions)(SMSIntegrationTabs);
diff --git a/src/containers/Preferences/SMSIntegration/SMSMessagesDataTable.js b/src/containers/Preferences/SMSIntegration/SMSMessagesDataTable.js
new file mode 100644
index 000000000..3dd79c381
--- /dev/null
+++ b/src/containers/Preferences/SMSIntegration/SMSMessagesDataTable.js
@@ -0,0 +1,99 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import styled from 'styled-components';
+import { Intent } from '@blueprintjs/core';
+
+import { DataTable, AppToaster } from 'components';
+import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
+
+import { useSMSIntegrationTableColumns, ActionsMenu } from './components';
+import { useSMSIntegrationContext } from './SMSIntegrationProvider';
+import { useSettingEditSMSNotification } from 'hooks/query';
+
+import withDialogActions from 'containers/Dialog/withDialogActions';
+import { compose } from 'utils';
+
+/**
+ * SMS Message data table.
+ */
+function SMSMessagesDataTable({
+ // #withDialogAction
+ openDialog,
+}) {
+ // Edit SMS message notification mutations.
+ const { mutateAsync: editSMSNotificationMutate } =
+ useSettingEditSMSNotification();
+
+ const toggleSmsNotification = (notificationKey, value) => {
+ editSMSNotificationMutate({
+ notification_key: notificationKey,
+ is_notification_enabled: value,
+ }).then(() => {
+ AppToaster.show({
+ message: intl.get(
+ 'sms_messages.notification_switch_change_success_message',
+ ),
+ intent: Intent.SUCCESS,
+ });
+ });
+ };
+
+ // Handle notification switch change.
+ const handleNotificationSwitchChange = React.useCallback(
+ (event, value, notification) => {
+ toggleSmsNotification(notification.key, value);
+ },
+ [editSMSNotificationMutate],
+ );
+
+ // Table columns.
+ const columns = useSMSIntegrationTableColumns({
+ onSwitchChange: handleNotificationSwitchChange,
+ });
+
+ const {
+ notifications,
+ isSMSNotificationsLoading,
+ isSMSNotificationsFetching,
+ } = useSMSIntegrationContext();
+
+ // handle edit message link click
+ const handleEditMessageText = ({ key }) => {
+ openDialog('sms-message-form', { notificationkey: key });
+ };
+
+ const handleEnableNotification = (notification) => {
+ toggleSmsNotification(notification.key, true);
+ };
+
+ const handleDisableNotification = (notification) => {
+ toggleSmsNotification(notification.key, false);
+ };
+
+ return (
+
+ );
+}
+
+export default compose(withDialogActions)(SMSMessagesDataTable);
+
+const SMSNotificationsTable = styled(DataTable)`
+ .table .tbody .tr .td {
+ align-items: flex-start;
+ }
+ .table .tbody .td {
+ padding: 0.8rem;
+ }
+`;
diff --git a/src/containers/Preferences/SMSIntegration/components.js b/src/containers/Preferences/SMSIntegration/components.js
new file mode 100644
index 000000000..ba2f37845
--- /dev/null
+++ b/src/containers/Preferences/SMSIntegration/components.js
@@ -0,0 +1,144 @@
+import React from 'react';
+import intl from 'react-intl-universal';
+import styled from 'styled-components';
+import { Intent, Button, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
+
+import { SwitchFieldCell } from 'components/DataTableCells';
+import { safeInvoke } from 'utils';
+
+/**
+ * Notification accessor.
+ */
+export const NotificationAccessor = (row) => {
+ return (
+
+ {row.notification_label}
+
+ {row.notification_description}
+
+
+ );
+};
+
+/**
+ * SMS notification message cell.
+ */
+export const SMSMessageCell = ({
+ payload: { onEditMessageText },
+ row: { original },
+}) => (
+
+ {original.sms_message}
+
+ safeInvoke(onEditMessageText, original)}
+ >
+ {intl.get('sms_messages.label_edit_message')}
+
+
+
+);
+
+/**
+ * Context menu of SMS notification messages.
+ */
+export function ActionsMenu({
+ payload: { onEditMessageText, onEnableNotification, onDisableNotification },
+ row: { original },
+}) {
+ return (
+
+ );
+}
+
+/**
+ * Retrieve SMS notifications messages table columns
+ * @returns
+ */
+export function useSMSIntegrationTableColumns({ onSwitchChange }) {
+ return React.useMemo(
+ () => [
+ {
+ id: 'notification',
+ Header: intl.get('sms_messages.column.notification'),
+ accessor: NotificationAccessor,
+ className: 'notification',
+ width: '180',
+ disableSortBy: true,
+ },
+ {
+ Header: intl.get('sms_messages.column.service'),
+ accessor: 'module_formatted',
+ className: 'service',
+ width: '80',
+ disableSortBy: true,
+ },
+ {
+ Header: intl.get('sms_messages.column.message'),
+ accessor: 'sms_message',
+ Cell: SMSMessageCell,
+ className: 'sms_message',
+ width: '180',
+ disableSortBy: true,
+ },
+ {
+ Header: intl.get('sms_messages.column.auto'),
+ accessor: 'is_notification_enabled',
+ Cell: SwitchFieldCell,
+ className: 'is_notification_enabled',
+ disableResizing: true,
+ disableSortBy: true,
+ width: '80',
+ onSwitchChange,
+ },
+ ],
+ [onSwitchChange],
+ );
+}
+
+const NotificationLabel = styled.div`
+ font-weight: 500;
+`;
+
+const NotificationDescription = styled.div`
+ font-size: 14px;
+ margin-top: 6px;
+ display: block;
+ opacity: 0.75;
+`;
+
+const MessageBox = styled.div`
+ padding: 10px;
+ background-color: #fbfbfb;
+ border: 1px dashed #dcdcdc;
+ font-size: 14px;
+ line-height: 1.45;
+`;
+
+const MessageBoxActions = styled.div`
+ margin-top: 2px;
+
+ button {
+ font-size: 12px;
+ }
+`;
diff --git a/src/containers/Preferences/SMSIntegration/index.js b/src/containers/Preferences/SMSIntegration/index.js
new file mode 100644
index 000000000..bd3d7d5cc
--- /dev/null
+++ b/src/containers/Preferences/SMSIntegration/index.js
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import { SMSIntegrationProvider } from './SMSIntegrationProvider';
+import SMSIntegrationTabs from './SMSIntegrationTabs';
+
+/**
+ * SMS SMS Integration
+ */
+export default function SMSIntegration() {
+ return (
+
+
+
+ );
+}
diff --git a/src/containers/Preferences/Users/components.js b/src/containers/Preferences/Users/components.js
index 6b288a0df..853852a61 100644
--- a/src/containers/Preferences/Users/components.js
+++ b/src/containers/Preferences/Users/components.js
@@ -78,7 +78,7 @@ export function ActionsMenu({
*/
function StatusAccessor(user) {
return !user.is_invite_accepted ? (
-
+
) : user.active ? (
diff --git a/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js b/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js
index dfa2a5d28..44bd7d75e 100644
--- a/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js
+++ b/src/containers/Purchases/Bills/BillForm/BillFormHeaderFields.js
@@ -6,7 +6,7 @@ import { FastField, ErrorMessage } from 'formik';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
-import { ContactSelecetList, FieldRequiredHint, Icon } from 'components';
+import { VendorSelectField, FieldRequiredHint, Icon } from 'components';
import { vendorsFieldShouldUpdate } from './utils';
import { useBillFormContext } from './BillFormProvider';
@@ -43,14 +43,15 @@ function BillFormHeader() {
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(contact) => {
form.setFieldValue('vendor_id', contact.id);
}}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js b/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js
index d59d8994c..d2177f3ed 100644
--- a/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js
+++ b/src/containers/Purchases/PaymentMades/PaymentForm/PaymentMadeFormHeaderFields.js
@@ -15,7 +15,7 @@ import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
- ContactSelecetList,
+ VendorSelectField,
FieldRequiredHint,
InputPrependText,
Money,
@@ -90,8 +90,8 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(contact) => {
@@ -100,6 +100,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
}}
disabled={!isNewMode}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js b/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js
index fce425f3a..e83642bf8 100644
--- a/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js
+++ b/src/containers/Sales/Estimates/EstimateForm/EstimateFormHeaderFields.js
@@ -19,7 +19,7 @@ import { customersFieldShouldUpdate } from './utils';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import {
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
@@ -82,8 +82,8 @@ function EstimateFormHeader({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(customer) => {
@@ -91,6 +91,7 @@ function EstimateFormHeader({
}}
popoverFill={true}
intent={inputIntent({ error, touched })}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js b/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js
index 1775d8d78..d5c8aaca6 100644
--- a/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js
+++ b/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormHeaderFields.js
@@ -10,20 +10,23 @@ import { FastField, Field, ErrorMessage } from 'formik';
import { FormattedMessage as T } from 'components';
import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames';
+
import {
useObserveInvoiceNoSettings,
customerNameFieldShouldUpdate,
} from './utils';
import { CLASSES } from 'common/classes';
import {
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
} from 'components';
import { useInvoiceFormContext } from './InvoiceFormProvider';
+
import withSettings from 'containers/Settings/withSettings';
import withDialogActions from 'containers/Dialog/withDialogActions';
+
import { inputIntent, handleDateChange } from 'utils';
/**
@@ -84,14 +87,15 @@ function InvoiceFormHeaderFields({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(customer) => {
form.setFieldValue('customer_id', customer.id);
}}
popoverFill={true}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js b/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js
index 452ca971d..743e97d32 100644
--- a/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js
+++ b/src/containers/Sales/PaymentReceives/PaymentReceiveForm/PaymentReceiveHeaderFields.js
@@ -23,7 +23,7 @@ import {
} from 'utils';
import {
AccountsSelectList,
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
@@ -134,8 +134,8 @@ function PaymentReceiveHeaderFields({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(customer) => {
@@ -147,6 +147,7 @@ function PaymentReceiveHeaderFields({
buttonProps={{
elementRef: (ref) => (customerFieldRef.current = ref),
}}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js b/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js
index 60ec206bb..9f384dd77 100644
--- a/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js
+++ b/src/containers/Sales/Receipts/ReceiptForm/ReceiptFormHeaderFields.js
@@ -12,7 +12,7 @@ import { FastField, ErrorMessage } from 'formik';
import { CLASSES } from 'common/classes';
import {
AccountsSelectList,
- ContactSelecetList,
+ CustomerSelectField,
FieldRequiredHint,
Icon,
InputPrependButton,
@@ -88,14 +88,15 @@ function ReceiptFormHeader({
intent={inputIntent({ error, touched })}
helperText={}
>
- }
onContactSelected={(contact) => {
form.setFieldValue('customer_id', contact.id);
}}
popoverFill={true}
+ allowCreate={true}
/>
)}
@@ -129,6 +130,7 @@ function ReceiptFormHeader({
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
]}
+ allowCreate={true}
/>
)}
diff --git a/src/containers/Vendors/VendorForm/VendorFloatingActions.js b/src/containers/Vendors/VendorForm/VendorFloatingActions.js
index 1da3670da..085efd598 100644
--- a/src/containers/Vendors/VendorForm/VendorFloatingActions.js
+++ b/src/containers/Vendors/VendorForm/VendorFloatingActions.js
@@ -9,42 +9,43 @@ import {
Menu,
MenuItem,
} from '@blueprintjs/core';
-import { FormattedMessage as T } from 'components';
+import styled from 'styled-components';
import classNames from 'classnames';
-import { CLASSES } from 'common/classes';
import { useFormikContext } from 'formik';
-import { useHistory } from 'react-router-dom';
+
+import { FormattedMessage as T } from 'components';
+
+import { CLASSES } from 'common/classes';
import { Icon } from 'components';
import { useVendorFormContext } from './VendorFormProvider';
+import { safeInvoke } from 'utils';
+
/**
* Vendor floating actions bar.
*/
-export default function VendorFloatingActions() {
+export default function VendorFloatingActions({ onCancel }) {
// Formik context.
const { resetForm, isSubmitting, submitForm } = useFormikContext();
// Vendor form context.
const { isNewMode, setSubmitPayload } = useVendorFormContext();
- // History.
- const history = useHistory();
-
// Handle the submit button.
const handleSubmitBtnClick = (event) => {
- setSubmitPayload({ noRedirect: false, });
+ setSubmitPayload({ noRedirect: false });
submitForm();
};
// Handle the submit & new button click.
const handleSubmitAndNewClick = (event) => {
submitForm();
- setSubmitPayload({ noRedirect: true, });
+ setSubmitPayload({ noRedirect: true });
};
// Handle cancel button click.
const handleCancelBtnClick = (event) => {
- history.goBack();
+ safeInvoke(onCancel, event);
};
// Handle clear button click.
@@ -56,7 +57,7 @@ export default function VendorFloatingActions() {
{/* ----------- Save and New ----------- */}
-
);
}
+
+const SaveButton = styled(Button)`
+ min-width: 100px;
+`;
diff --git a/src/containers/Vendors/VendorForm/VendorForm.js b/src/containers/Vendors/VendorForm/VendorFormFormik.js
similarity index 63%
rename from src/containers/Vendors/VendorForm/VendorForm.js
rename to src/containers/Vendors/VendorForm/VendorFormFormik.js
index db9474aa1..c45bb2e59 100644
--- a/src/containers/Vendors/VendorForm/VendorForm.js
+++ b/src/containers/Vendors/VendorForm/VendorFormFormik.js
@@ -1,13 +1,10 @@
-import React, { useMemo, useEffect } from 'react';
+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 { FormattedMessage as T } from 'components';
import AppToaster from 'components/AppToaster';
import {
CreateVendorFormSchema,
@@ -19,56 +16,27 @@ import VendorFormAfterPrimarySection from './VendorFormAfterPrimarySection';
import VendorTabs from './VendorsTabs';
import VendorFloatingActions from './VendorFloatingActions';
-import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import { useVendorFormContext } from './VendorFormProvider';
-import { compose, transformToForm } from 'utils';
+import { compose, transformToForm, safeInvoke } from 'utils';
-const defaultInitialValues = {
- salutation: '',
- first_name: '',
- last_name: '',
- company_name: '',
- display_name: '',
+import { defaultInitialValues } from './utils';
- 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 'style/pages/Vendors/Form.scss';
/**
* Vendor form.
*/
-function VendorForm({
- // #withDashboardActions
- changePageTitle,
-
+function VendorFormFormik({
// #withCurrentOrganization
organization: { base_currency },
+
+ // #ownProps
+ onSubmitSuccess,
+ onSubmitError,
+ onCancel,
+ className,
}) {
// Vendor form context.
const {
@@ -82,11 +50,6 @@ function VendorForm({
isNewMode,
} = useVendorFormContext();
- // const isNewMode = !vendorId;
-
- // History context.
- const history = useHistory();
-
/**
* Initial values in create and edit mode.
*/
@@ -101,14 +64,13 @@ function VendorForm({
);
// Handles the form submit.
- const handleFormSubmit = (
- values,
- { setSubmitting, resetForm, setErrors },
- ) => {
+ const handleFormSubmit = (values, form) => {
+ const { setSubmitting, resetForm } = form;
const requestForm = { ...values };
+
setSubmitting(true);
- const onSuccess = () => {
+ const onSuccess = (response) => {
AppToaster.show({
message: intl.get(
isNewMode
@@ -121,16 +83,15 @@ function VendorForm({
setSubmitting(false);
resetForm();
- if (!submitPayload.noRedirect) {
- history.push('/vendors');
- }
+ safeInvoke(onSubmitSuccess, values, form, submitPayload, response);
};
const onError = () => {
setSubmitPayload(false);
setSubmitting(false);
- };
+ safeInvoke(onSubmitError, values, form, submitPayload);
+ };
if (isNewMode) {
createVendorMutate(requestForm).then(onSuccess).catch(onError);
} else {
@@ -139,7 +100,13 @@ function VendorForm({
};
return (
-
);
}
-export default compose(
- withDashboardActions,
- withCurrentOrganization(),
-)(VendorForm);
+export default compose(withCurrentOrganization())(VendorFormFormik);
diff --git a/src/containers/Vendors/VendorForm/VendorFormPage.js b/src/containers/Vendors/VendorForm/VendorFormPage.js
index 329644097..5553dba57 100644
--- a/src/containers/Vendors/VendorForm/VendorFormPage.js
+++ b/src/containers/Vendors/VendorForm/VendorFormPage.js
@@ -1,26 +1,69 @@
import React from 'react';
-import { useParams } from 'react-router-dom';
+import { useParams, useHistory } from 'react-router-dom';
+import styled from 'styled-components';
import 'style/pages/Vendors/PageForm.scss';
-import { DashboardCard } from 'components';
-import VendorFrom from './VendorForm';
+import { DashboardCard, DashboardInsider } from 'components';
+import VendorFormFormik from './VendorFormFormik';
-import { VendorFormProvider } from './VendorFormProvider';
+import { VendorFormProvider, useVendorFormContext } from './VendorFormProvider';
+
+/**
+ * Vendor form page loading wrapper.
+ * @returns {JSX}
+ */
+function VendorFormPageLoading({ children }) {
+ const { isFormLoading } = useVendorFormContext();
+
+ return (
+
+ {children}
+
+ );
+}
/**
* Vendor form page.
*/
-function VendorFormPage() {
+export default function VendorFormPage() {
+ const history = useHistory();
const { id } = useParams();
-
+
+ // Handle the form submit success.
+ const handleSubmitSuccess = (values, formArgs, submitPayload) => {
+ if (!submitPayload.noRedirect) {
+ history.push('/vendors');
+ }
+ };
+ // Handle the form cancel button click.
+ const handleFormCancel = () => {
+ history.goBack();
+ };
+
return (
-
-
-
+
+
+
+
+
);
}
-export default VendorFormPage;
+const VendorFormPageFormik = styled(VendorFormFormik)`
+ .page-form {
+ &__floating-actions {
+ margin-left: -40px;
+ margin-right: -40px;
+ }
+ }
+`;
+
+const VendorDashboardInsider = styled(DashboardInsider)`
+ padding-bottom: 64px;
+`;
diff --git a/src/containers/Vendors/VendorForm/VendorFormProvider.js b/src/containers/Vendors/VendorForm/VendorFormProvider.js
index 107b86af5..a40d2ed4f 100644
--- a/src/containers/Vendors/VendorForm/VendorFormProvider.js
+++ b/src/containers/Vendors/VendorForm/VendorFormProvider.js
@@ -6,7 +6,6 @@ import {
useVendor,
useContact,
useCurrencies,
- useCustomer,
useCreateVendor,
useEditVendor,
} from 'hooks/query';
@@ -30,11 +29,10 @@ function VendorFormProvider({ vendorId, ...props }) {
});
// Handle fetch contact duplicate details.
- const {
- data: contactDuplicate,
- isLoading: isContactLoading,
- } = useContact(contactId, { enabled: !!contactId });
-
+ const { data: contactDuplicate, isLoading: isContactLoading } = useContact(
+ contactId,
+ { enabled: !!contactId },
+ );
// Create and edit vendor mutations.
const { mutateAsync: createVendorMutate } = useCreateVendor();
const { mutateAsync: editVendorMutate } = useEditVendor();
@@ -45,27 +43,25 @@ function VendorFormProvider({ vendorId, ...props }) {
// determines whether the form new or duplicate mode.
const isNewMode = contactId || !vendorId;
+ const isFormLoading =
+ isVendorLoading || isContactLoading || isCurrenciesLoading;
+
const provider = {
vendorId,
currencies,
vendor,
contactDuplicate: { ...omit(contactDuplicate, ['opening_balance_at']) },
submitPayload,
+
isNewMode,
+ isFormLoading,
createVendorMutate,
editVendorMutate,
setSubmitPayload,
};
- return (
-
-
-
- );
+ return ;
}
const useVendorFormContext = () => React.useContext(VendorFormContext);
diff --git a/src/containers/Vendors/VendorForm/utils.js b/src/containers/Vendors/VendorForm/utils.js
new file mode 100644
index 000000000..8c9a70509
--- /dev/null
+++ b/src/containers/Vendors/VendorForm/utils.js
@@ -0,0 +1,36 @@
+import moment from 'moment';
+
+export const defaultInitialValues = {
+ 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'),
+};
diff --git a/src/hooks/query/customers.js b/src/hooks/query/customers.js
index 3e397972a..75a6d6d3b 100644
--- a/src/hooks/query/customers.js
+++ b/src/hooks/query/customers.js
@@ -20,6 +20,12 @@ const commonInvalidateQueries = (queryClient) => {
// Invalidate the financial reports.
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
+
+ // Invalidate SMS details.
+ queryClient.invalidateQueries(t.SALE_ESTIMATE_SMS_DETAIL);
+ queryClient.invalidateQueries(t.SALE_INVOICE_SMS_DETAIL);
+ queryClient.invalidateQueries(t.SALE_RECEIPT_SMS_DETAIL);
+ queryClient.invalidateQueries(t.PAYMENT_RECEIVE_SMS_DETAIL);
};
// Customers response selector.
diff --git a/src/hooks/query/estimates.js b/src/hooks/query/estimates.js
index 8c25c4baa..b73eae6ce 100644
--- a/src/hooks/query/estimates.js
+++ b/src/hooks/query/estimates.js
@@ -189,3 +189,49 @@ export function useRefreshEstimates() {
},
};
}
+
+/**
+ *
+ */
+export function useCreateNotifyEstimateBySMS(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ([id, values]) =>
+ apiRequest.post(`sales/estimates/${id}/notify-by-sms`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ // Invalidate
+ queryClient.invalidateQueries([t.NOTIFY_SALE_ESTIMATE_BY_SMS, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
+/**
+ *
+ * @param {*} estimateId
+ * @param {*} props
+ * @param {*} requestProps
+ * @returns
+ */
+export function useEstimateSMSDetail(estimateId, props, requestProps) {
+ return useRequestQuery(
+ [t.SALE_ESTIMATE_SMS_DETAIL, estimateId],
+ {
+ method: 'get',
+ url: `sales/estimates/${estimateId}/sms-details`,
+ ...requestProps,
+ },
+ {
+ select: (res) => res.data.data,
+ defaultData: {},
+ ...props,
+ },
+ );
+}
diff --git a/src/hooks/query/invoices.js b/src/hooks/query/invoices.js
index 67debc80c..aabe2fa07 100644
--- a/src/hooks/query/invoices.js
+++ b/src/hooks/query/invoices.js
@@ -219,14 +219,53 @@ export function useCancelBadDebt(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
- return useMutation((id) => apiRequest.post(`sales/invoices/${id}/writeoff/cancel`), {
- onSuccess: (res, id) => {
- // Invalidate
- queryClient.invalidateQueries([t.CANCEL_BAD_DEBT, id]);
+ return useMutation(
+ (id) => apiRequest.post(`sales/invoices/${id}/writeoff/cancel`),
+ {
+ onSuccess: (res, id) => {
+ // Invalidate
+ queryClient.invalidateQueries([t.CANCEL_BAD_DEBT, id]);
- // Common invalidate queries.
- commonInvalidateQueries(queryClient);
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
},
- ...props,
- });
+ );
+}
+
+export function useCreateNotifyInvoiceBySMS(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ([id, values]) =>
+ apiRequest.post(`sales/invoices/${id}/notify-by-sms`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ // Invalidate
+ queryClient.invalidateQueries([t.NOTIFY_SALE_INVOICE_BY_SMS, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
+export function useInvoiceSMSDetail(invoiceId, query, props) {
+ return useRequestQuery(
+ [t.SALE_INVOICE_SMS_DETAIL, invoiceId, query],
+ {
+ method: 'get',
+ url: `sales/invoices/${invoiceId}/sms-details`,
+ params: query,
+ },
+ {
+ select: (res) => res.data.data,
+ defaultData: {},
+ ...props,
+ },
+ );
}
diff --git a/src/hooks/query/paymentReceives.js b/src/hooks/query/paymentReceives.js
index 6f8a0f28a..06e7b39cb 100644
--- a/src/hooks/query/paymentReceives.js
+++ b/src/hooks/query/paymentReceives.js
@@ -174,3 +174,43 @@ export function useRefreshPaymentReceive() {
},
};
}
+
+export function useCreateNotifyPaymentReceiveBySMS(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ ([id, values]) =>
+ apiRequest.post(`sales/payment_receives/${id}/notify-by-sms`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ // Invalidate
+ queryClient.invalidateQueries([t.NOTIFY_PAYMENT_RECEIVE_BY_SMS, id]);
+
+ // Common invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
+export function usePaymentReceiveSMSDetail(
+ paymentReceiveId,
+ props,
+ requestProps,
+) {
+ return useRequestQuery(
+ [t.PAYMENT_RECEIVE_SMS_DETAIL, paymentReceiveId],
+ {
+ method: 'get',
+ url: `sales/payment_receives/${paymentReceiveId}/sms-details`,
+ ...requestProps,
+ },
+ {
+ select: (res) => res.data.data,
+ defaultData: {},
+ ...props,
+ },
+ );
+}
diff --git a/src/hooks/query/receipts.js b/src/hooks/query/receipts.js
index ebefb6c26..a9b0c705b 100644
--- a/src/hooks/query/receipts.js
+++ b/src/hooks/query/receipts.js
@@ -23,7 +23,7 @@ const commonInvalidateQueries = (queryClient) => {
// Invalidate the cashflow transactions.
queryClient.invalidateQueries(t.CASH_FLOW_TRANSACTIONS);
queryClient.invalidateQueries(t.CASHFLOW_ACCOUNT_TRANSACTIONS_INFINITY);
-
+
// Invalidate the settings.
queryClient.invalidateQueries([t.SETTING, t.SETTING_RECEIPTS]);
};
@@ -163,3 +163,37 @@ export function useRefreshReceipts() {
},
};
}
+
+export function useCreateNotifyReceiptBySMS(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+ return useMutation(
+ ([id, values]) =>
+ apiRequest.post(`sales/receipts/${id}/notify-by-sms`, values),
+ {
+ onSuccess: (res, [id, values]) => {
+ queryClient.invalidateQueries([t.NOTIFY_SALE_RECEIPT_BY_SMS, id]);
+
+ // Invalidate queries.
+ commonInvalidateQueries(queryClient);
+ },
+ ...props,
+ },
+ );
+}
+
+export function useReceiptSMSDetail(receiptId, props, requestProps) {
+ return useRequestQuery(
+ [t.SALE_RECEIPT_SMS_DETAIL, receiptId],
+ {
+ method: 'get',
+ url: `sales/receipts/${receiptId}/sms-details`,
+ ...requestProps,
+ },
+ {
+ select: (res) => res.data.data,
+ defaultData: {},
+ ...props,
+ },
+ );
+}
diff --git a/src/hooks/query/settings.js b/src/hooks/query/settings.js
index 22e25c72b..b7c70b335 100644
--- a/src/hooks/query/settings.js
+++ b/src/hooks/query/settings.js
@@ -1,4 +1,3 @@
-import { useEffect } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest';
@@ -123,3 +122,61 @@ export function useSettingCashFlow(props) {
props,
);
}
+
+/**
+ * Retrieve SMS Notifications settings.
+ */
+export function useSettingSMSNotifications(props) {
+ return useRequestQuery(
+ [t.SETTING_SMS_NOTIFICATIONS],
+ { method: 'get', url: `settings/sms-notifications` },
+ {
+ select: (res) => res.data.notifications,
+ defaultData: [],
+ ...props,
+ },
+ );
+}
+
+/**
+ * Retrieve Specific SMS Notification settings.
+ */
+export function useSettingSMSNotification(key, props) {
+ return useRequestQuery(
+ [t.SETTING_SMS_NOTIFICATIONS, key],
+ {
+ method: 'get',
+ url: `settings/sms-notification/${key}`,
+ },
+ {
+ select: (res) => res.data.notification,
+ defaultData: {
+ smsNotification: [],
+ },
+ ...props,
+ },
+ );
+}
+
+/**
+ * Retrieve Edit SMS Notification settings.
+ */
+export function useSettingEditSMSNotification(props) {
+ const queryClient = useQueryClient();
+ const apiRequest = useApiRequest();
+
+ return useMutation(
+ (values) => apiRequest.post(`settings/sms-notification`, values),
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries([t.SETTING_SMS_NOTIFICATIONS]);
+
+ queryClient.invalidateQueries(t.SALE_INVOICE_SMS_DETAIL);
+ queryClient.invalidateQueries(t.SALE_RECEIPT_SMS_DETAIL);
+ queryClient.invalidateQueries(t.PAYMENT_RECEIVE_SMS_DETAIL);
+ queryClient.invalidateQueries(t.SALE_ESTIMATE_SMS_DETAIL);
+ },
+ ...props,
+ },
+ );
+}
diff --git a/src/hooks/query/types.js b/src/hooks/query/types.js
index 54952a47c..1aebbd8e1 100644
--- a/src/hooks/query/types.js
+++ b/src/hooks/query/types.js
@@ -51,11 +51,15 @@ const ITEMS = {
const SALE_ESTIMATES = {
SALE_ESTIMATES: 'SALE_ESTIMATES',
SALE_ESTIMATE: 'SALE_ESTIMATE',
+ SALE_ESTIMATE_SMS_DETAIL: 'SALE_ESTIMATE_SMS_DETAIL',
+ NOTIFY_SALE_ESTIMATE_BY_SMS: 'NOTIFY_SALE_ESTIMATE_BY_SMS',
};
const SALE_RECEIPTS = {
SALE_RECEIPTS: 'SALE_RECEIPTS',
SALE_RECEIPT: 'SALE_RECEIPT',
+ SALE_RECEIPT_SMS_DETAIL: 'SALE_RECEIPT_SMS_DETAIL',
+ NOTIFY_SALE_RECEIPT_BY_SMS: 'NOTIFY_SALE_RECEIPT_BY_SMS',
};
const INVENTORY_ADJUSTMENTS = {
@@ -79,12 +83,16 @@ const PAYMENT_RECEIVES = {
PAYMENT_RECEIVE: 'PAYMENT_RECEIVE',
PAYMENT_RECEIVE_NEW_ENTRIES: 'PAYMENT_RECEIVE_NEW_ENTRIES',
PAYMENT_RECEIVE_EDIT_PAGE: 'PAYMENT_RECEIVE_EDIT_PAGE',
+ PAYMENT_RECEIVE_SMS_DETAIL: 'PAYMENT_RECEIVE_SMS_DETAIL',
+ NOTIFY_PAYMENT_RECEIVE_BY_SMS: 'NOTIFY_PAYMENT_RECEIVE_BY_SMS',
};
const SALE_INVOICES = {
SALE_INVOICES: 'SALE_INVOICES',
SALE_INVOICE: 'SALE_INVOICE',
SALE_INVOICES_DUE: 'SALE_INVOICES_DUE',
+ SALE_INVOICE_SMS_DETAIL: 'SALE_INVOICE_SMS_DETAIL',
+ NOTIFY_SALE_INVOICE_BY_SMS: 'NOTIFY_SALE_INVOICE_BY_SMS',
BAD_DEBT: 'BAD_DEBT',
CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
};
@@ -103,6 +111,9 @@ const SETTING = {
SETTING_MANUAL_JOURNALS: 'SETTING_MANUAL_JOURNALS',
SETTING_ITEMS: 'SETTING_ITEMS',
SETTING_CASHFLOW: 'SETTING_CASHFLOW',
+ SETTING_SMS_NOTIFICATION: 'SETTING_SMS_NOTIFICATION',
+ SETTING_SMS_NOTIFICATIONS: 'SETTING_SMS_NOTIFICATIONS',
+ SETTING_EDIT_SMS_NOTIFICATION: 'SETTING_EDIT_SMS_NOTIFICATION',
};
const ORGANIZATIONS = {
diff --git a/src/lang/ar/index.json b/src/lang/ar/index.json
index 945c453cb..9731ca8ad 100644
--- a/src/lang/ar/index.json
+++ b/src/lang/ar/index.json
@@ -1437,7 +1437,47 @@
"bad_debt.dialog.bad_debt": "الديون المعدومة",
"bad_debt.dialog.cancel_bad_debt": "إلغاء الديون المعدومة",
"bad_debt.dialog.header_note": "يمكن للبائع تحميل مبلغ الفاتورة على حساب مصروفات الديون المعدومة عندما يكون من المؤكد أن الفاتورة لن يتم دفعها.",
- "bad_debt.dialog.success_message":"تم شطب فاتورة البيع المقدمة بنجاح.",
- "bad_debt.cancel_alert.success_message":"تم إلغاء شطب فاتورة البيع المقدمة بنجاح.",
- "bad_debt.cancel_alert.message": "هل أنت متأكد أنك تريد شطب هذه الفاتورة؟ "
+ "bad_debt.dialog.success_message": "تم شطب فاتورة البيع المقدمة بنجاح.",
+ "bad_debt.cancel_alert.success_message": "تم إلغاء شطب فاتورة البيع المقدمة بنجاح.",
+ "bad_debt.cancel_alert.message": "هل أنت متأكد أنك تريد شطب هذه الفاتورة؟ ",
+ "notify_via_sms.dialog.send_notification_to": "إرسال إشعار إلى ",
+ "notify_via_sms.dialog.message_text": "نص الرسالة ",
+ "notify_via_sms.dialog.notification_type": "نوع الإشعار",
+ "notify_via_sms.dialog.notify_via_sms": "إشعار عبر رسائل قصيرة",
+ "notiify_via_sms.dialog.sms_note": "ملاحظة : يمكن أن تحتوي رسالة قصيرة الواحدة على 160 حرفًا كحد أقصى {value}. سيتم استخدام وحدات الرسائل القصيرة لإرسال هذا إشعار عبر الرسائل القصيرة. ",
+ "notify_Via_sms.dialog.customer_phone_number_does_not_eixst": "رقم هاتف الزبون غير موجود ، يرجى إدخال رقم هاتف للعميل. ",
+ "notify_Via_sms.dialog.customer_phone_number_invalid": "رقم هاتف الزبون غير صالح ، يرجى إدخال رقم هاتف صحيح للعميل. ",
+ "notify_via_sms.dialog.phone_invalid_error_message": "لا يمكن إرسال إشعار الرسائل القصيرة ، رقم الهاتف للعميل غير صالح ، يرجى إدخال رقم صالح والمحاولة مرة أخرى.",
+ "notify_via_sms.dialog.customer_no_phone_error_message": "الزبون ليس لديه رقم هاتف.",
+ "notify_invoice_via_sms.dialog.success_message": "تم إرسال إشعار فاتورة البيع عبر الرسائل القصيرة بنجاح ",
+ "notify_estimate_via_sms.dialog.success_message": "تم إرسال إشعار العرض البيع عبر الرسائل القصيرة بنجاح ",
+ "notify_receipt_via_sms.dialog.success_message": "تم إرسال إشعار إيصال البيع عبر الرسائل القصيرة بنجاح ",
+ "notify_payment_receive_via_sms.dialog.success_message": "تم إرسال إشعار الدفع بنجاح. ",
+ "sms_integration.label": "الرسائل القصيرة",
+ "sms_integration.label.overview": "نظرة عامة",
+ "sms_integration.label.sms_messages": "الرسائل القصيرة ",
+ "sms_messages.column.message": "الرسالة",
+ "sms_messages.column.notification": "إشعار",
+ "sms_messages.column.auto": "تلقائي",
+ "sms_messages.column.service": "الخدمة",
+ "sms_messages.label_edit_message": "تعديل الرسالة ",
+ "sms_messages.notification_switch_change_success_message": "تم تفعيل الإشعار رسائل القصيرة بنجاح.",
+ "sms_message.dialog.label": "رسالة قصيرة",
+ "sms_message.dialog.sms_note": "ملاحظة: تحتوي رسالة SMS الواحدة على 160 حرفًا كحد أقصى. سيتم استخدام عدد {value} رسائل القصيرة لإرسال هذا الإشعار.",
+ "sms_message.dialog.success_message": "تم تحديث إعدادات إشعار الرسائل القصيرة بنجاح. ",
+ "sms_message.dialog.unsupported_variables_error_message": "متغيرات غير مدعومة",
+ "sms_message.dialog.message_variable_description": "{value} إشارة لاسم الشركة الحالي.",
+ "sms_notifications.edit_message_text": "تعديل نص رسالة",
+ "sms_notifications.enable_notification": "تفعيل الإشعارات",
+ "sms_notifications.disable_notification": "تعطيل الإشعارات",
+ "send_sms": "إرسال رسالة قصيرة",
+ "save_sms_message": "حفظ رسالة قصيرة",
+ "sms_message.edit_form.reset_to_default_message": "إعادة التعيين إلى الرسالة الافتراضية.",
+ "report.compute_running.refresh": "تحديث",
+ "sms_notification.invoice_details.type": "تفاصيل الفاتورة",
+ "sms_notification.invoice_reminder.type": "تذكير بالفاتورة",
+ "sms_notification.estimate_details.type": "تفاصيل فاتورة عرض الاسعار",
+ "sms_notification.payment_details.type": "شكر علي عملية الدفع",
+ "sms_notification.receipt_details.type": "تفاصيل إيصال البيع",
+ "personal": "المحمول"
}
\ No newline at end of file
diff --git a/src/lang/en/index.json b/src/lang/en/index.json
index 058cfe230..a13f1b07c 100644
--- a/src/lang/en/index.json
+++ b/src/lang/en/index.json
@@ -1407,7 +1407,6 @@
"cash_flow_money_out": "Money Out",
"cash_flow_transaction.switch_item": "Transactions {value}",
"cash_flow_transaction.balance_in_bigcapital": "Balance in Bigcapital",
-
"AR_aging_summary.filter_customers.all_customers": "All customers",
"AR_aging_summary.filter_customers.all_customers.hint": "All customers, include that ones have zero-balance.",
"AR_aging_summary.filter_customers.without_zero_balance": "Customers without zero balance",
@@ -1425,8 +1424,47 @@
"bad_debt.dialog.bad_debt": "Bad debt",
"bad_debt.dialog.cancel_bad_debt": "Cancel bad debt",
"bad_debt.dialog.header_note": "The seller can charge the amount of an invoice to the bad debt expense account when it is certain that the invoice will not be paid.",
- "bad_debt.dialog.success_message":"The given sale invoice has been writte-off successfully.",
- "bad_debt.cancel_alert.success_message":"The given sale invoice has been canceled write-off successfully.",
- "bad_debt.cancel_alert.message": "Are you sure you want to write off this invoice?"
-
-}
+ "bad_debt.dialog.success_message": "The given sale invoice has been writte-off successfully.",
+ "bad_debt.cancel_alert.success_message": "The given sale invoice has been canceled write-off successfully.",
+ "bad_debt.cancel_alert.message": "Are you sure you want to write off this invoice?",
+ "notify_via_sms.dialog.send_notification_to": "Send notification to",
+ "notify_via_sms.dialog.message_text": "Message Text",
+ "notify_via_sms.dialog.notification_type": "Notification type",
+ "notify_via_sms.dialog.notify_via_sms": "Notify vis SMS",
+ "notiify_via_sms.dialog.sms_note": "Note : One SMS unit can contain a maximum of 160 characters. {value} SMS units will be used to send this SMS notification.",
+ "notify_Via_sms.dialog.customer_phone_number_does_not_eixst": "The customer phone number does not eixst, please enter a personal phone number to the customer.",
+ "notify_Via_sms.dialog.customer_phone_number_invalid": "The customer phone number is invalid, please enter a valid personal phone number to the customer.",
+ "notify_via_sms.dialog.customer_no_phone_error_message": "The customer phone number does not eixst.",
+ "notify_via_sms.dialog.personal_phone_invalid_error_message": "The personal phone number is invalid.",
+ "notify_invoice_via_sms.dialog.success_message": "The sale invoice sms notification has been sent successfully.",
+ "notify_estimate_via_sms.dialog.success_message": "The sale estimate sms notification has been sent successfully",
+ "notify_receipt_via_sms.dialog.success_message": "The sale receipt sms notification has been sent successfully",
+ "notify_payment_receive_via_sms.dialog.success_message": "The payment notification has been sent successfully.",
+ "sms_integration.label": "SMS Integration",
+ "sms_integration.label.overview": "Overview",
+ "sms_integration.label.sms_messages": "SMS Messages",
+ "sms_messages.column.notification": "Notification",
+ "sms_messages.column.message": "Message",
+ "sms_messages.column.auto": "Auto",
+ "sms_messages.column.service": "Service",
+ "sms_messages.label_edit_message": "Edit Message",
+ "sms_messages.notification_switch_change_success_message": "SMS notification hs been enabled successfully.",
+ "sms_message.dialog.label": "SMS message",
+ "sms_message.dialog.sms_note": "Note : One SMS unit can contain a maximum of 160 characters. {value} SMS units will be used to send this SMS notification.",
+ "sms_message.dialog.success_message": "Sms notification settings has been updated successfully.",
+ "sms_message.dialog.unsupported_variables_error_message": "Unsupported variables",
+ "sms_message.dialog.message_variable_description": "{value} References to the current company name.",
+ "sms_notifications.edit_message_text": "Edit message text",
+ "sms_notifications.enable_notification": "Enable notification",
+ "sms_notifications.disable_notification": "Disable notification",
+ "send_sms": "Send SMS",
+ "save_sms_message": "Save SMS Message",
+ "sms_message.edit_form.reset_to_default_message": "Reset to default message.",
+ "report.compute_running.refresh": "Refresh",
+ "sms_notification.invoice_details.type": "Invoice details",
+ "sms_notification.invoice_reminder.type": "Invoice reminder",
+ "sms_notification.estimate_details.type": "Sale estimate details",
+ "sms_notification.payment_details.type": "Payment receive thank you.",
+ "sms_notification.receipt_details.type": "Sale receipt details",
+ "personal": "Personal"
+}
\ No newline at end of file
diff --git a/src/routes/preferences.js b/src/routes/preferences.js
index cc539a664..98ff2e46b 100644
--- a/src/routes/preferences.js
+++ b/src/routes/preferences.js
@@ -4,6 +4,7 @@ import Accountant from 'containers/Preferences/Accountant/Accountant';
// import Accounts from 'containers/Preferences/Accounts/Accounts';
import Currencies from 'containers/Preferences/Currencies/Currencies';
import Item from 'containers/Preferences/Item';
+import SMSIntegration from '../containers/Preferences/SMSIntegration';
import DefaultRoute from '../containers/Preferences/DefaultRoute';
const BASE_URL = '/preferences';
@@ -34,6 +35,11 @@ export default [
component: Item,
exact: true,
},
+ {
+ path: `${BASE_URL}/sms-message`,
+ component: SMSIntegration,
+ exact: true,
+ },
{
path: `${BASE_URL}/`,
component: DefaultRoute,
diff --git a/src/static/json/icons.js b/src/static/json/icons.js
index 42a191624..fc6ffe780 100644
--- a/src/static/json/icons.js
+++ b/src/static/json/icons.js
@@ -509,4 +509,11 @@ export default {
],
viewBox: '0 0 24 24',
},
+ "sms-message-preview": {
+ path: [
+ 'M8.341,375.3573H399.3271v-.0015l-390.9861-.07ZM363.2382,0H44.43A44.4508,44.4508,0,0,0,0,44.371V375.284l8.341.0016V44.371A36.0651,36.0651,0,0,1,44.43,8.33H90.7089a4.6454,4.6454,0,0,1,4.6482,4.6423v1.9718a23.8588,23.8588,0,0,0,23.8742,23.843H288.9146a23.8586,23.8586,0,0,0,23.8741-23.843V12.972A4.6456,4.6456,0,0,1,317.4372,8.33h45.801A36.0651,36.0651,0,0,1,399.3271,44.371V375.3558l8.341.0015V44.371A44.4508,44.4508,0,0,0,363.2382,0Z',
+ "M1199.9485,803.1623"
+ ],
+ viewBox: "0 0 407.6681 375.3573",
+ }
};
diff --git a/src/style/pages/Customers/Form.scss b/src/style/pages/Customers/Form.scss
new file mode 100644
index 000000000..6a90e84ed
--- /dev/null
+++ b/src/style/pages/Customers/Form.scss
@@ -0,0 +1,157 @@
+@import '../../Base.scss';
+
+.page-form--customer {
+ $self: '.page-form';
+ padding: 20px;
+
+ #{$self}__header {
+ padding: 0;
+ }
+ #{$self}__primary-section {
+ padding: 10px 0 0;
+ margin: 0 0 20px;
+ overflow: hidden;
+ border-bottom: 1px solid #e4e4e4;
+ max-width: 1000px;
+ }
+
+ .bp3-form-group {
+ max-width: 500px;
+
+ .bp3-control {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+
+ &.bp3-inline {
+ .bp3-label {
+ min-width: 150px;
+ }
+ }
+ .bp3-form-content {
+ width: 100%;
+ }
+ }
+
+ .form-group--contact_name {
+ max-width: 600px;
+
+ .bp3-control-group > * {
+ flex-shrink: unset;
+
+ &:not(:last-child) {
+ padding-right: 10px;
+ }
+ &.input-group--salutation-list {
+ width: 25%;
+ }
+ &.input-group--first-name,
+ &.input-group--last-name {
+ width: 37%;
+ }
+ }
+ }
+
+ .bp3-form-group {
+ margin-bottom: 14px;
+ }
+
+ .bp3-tab-panel {
+ margin-top: 26px;
+ }
+
+ .form-group--phone-number {
+ .bp3-control-group > * {
+ flex-shrink: unset;
+ padding-right: 5px;
+ padding-left: 5px;
+
+ &:first-child {
+ padding-left: 0;
+ }
+ &:last-child {
+ padding-right: 0;
+ }
+ }
+ }
+
+ #{$self}__tabs {
+ margin-top: 20px;
+ max-width: 1000px;
+
+ h4 {
+ font-weight: 500;
+ color: #888;
+ margin-bottom: 1.2rem;
+ font-size: 14px;
+ }
+ // Tab panels.
+ .tab-panel {
+ &--address {
+ .bp3-form-group {
+ max-width: 440px;
+
+ &.bp3-inline {
+ .bp3-label {
+ min-width: 145px;
+ }
+ }
+
+ .bp3-form-content {
+ width: 100%;
+ }
+
+ textarea.bp3-input {
+ max-width: 100%;
+ width: 100%;
+ min-height: 50px;
+ }
+ }
+ }
+ &--note {
+ .form-group--note {
+ .bp3-form-group {
+ max-width: 600px;
+ }
+ textarea {
+ width: 100%;
+ min-height: 100px;
+ }
+ }
+ }
+ }
+
+ .dropzone-container {
+ max-width: 600px;
+ }
+ }
+
+ .bp3-tabs {
+ .bp3-tab-list {
+ position: relative;
+
+ &:before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 2px;
+ background: #f0f0f0;
+ }
+
+ > *:not(:last-child) {
+ margin-right: 25px;
+ }
+
+ &.bp3-large > .bp3-tab {
+ font-size: 15px;
+ color: #555;
+
+ &[aria-selected='true'],
+ &:not([aria-disabled='true']):hover {
+ color: $pt-link-color;
+ }
+ }
+ }
+ }
+}
diff --git a/src/style/pages/FinancialStatements/CashFlowStatement.scss b/src/style/pages/FinancialStatements/CashFlowStatement.scss
index 2ff22b8f2..419d7c8c1 100644
--- a/src/style/pages/FinancialStatements/CashFlowStatement.scss
+++ b/src/style/pages/FinancialStatements/CashFlowStatement.scss
@@ -10,7 +10,11 @@
}
.tbody {
.tr:not(.no-results) {
- &.row-type--CASH_END_PERIOD{
+ // &.row-type--AGGREGATE,
+ &.row-type--ACCOUNTS {
+ border-top: 1px solid #bbb;
+ }
+ &.row-type--CASH_END_PERIOD {
border-bottom: 3px double #333;
}
.td {
@@ -30,7 +34,7 @@
.tr.is-expanded {
.td.total,
- .td.date-period{
+ .td.date-period {
.cell-inner {
display: none;
}
diff --git a/src/style/pages/FinancialStatements/ContactsBalanceSummary.scss b/src/style/pages/FinancialStatements/ContactsBalanceSummary.scss
index 5a2ce5c5e..def379b49 100644
--- a/src/style/pages/FinancialStatements/ContactsBalanceSummary.scss
+++ b/src/style/pages/FinancialStatements/ContactsBalanceSummary.scss
@@ -11,6 +11,10 @@
}
.tbody {
.tr:not(.no-results) {
+ &.row-type--CUSTOMER,
+ &.row-type--VENDOR {
+ border-top: 1px solid #BBB
+ }
.td {
border-bottom: 0;
padding-top: 0.4rem;
diff --git a/src/style/pages/FinancialStatements/ContactsTransactions.scss b/src/style/pages/FinancialStatements/ContactsTransactions.scss
index 507a8d374..7fb3ff80a 100644
--- a/src/style/pages/FinancialStatements/ContactsTransactions.scss
+++ b/src/style/pages/FinancialStatements/ContactsTransactions.scss
@@ -64,12 +64,11 @@
display: none;
}
}
- }
- &--CUSTOMER:last-child {
- .td {
- border-bottom: 1px solid #ddd;
+ &:not(:first-child).is-expanded .td {
+ border-top: 1px solid #ddd;
}
}
+ &--CUSTOMER:last-child,
&--VENDOR:last-child {
.td {
border-bottom: 1px solid #ddd;
diff --git a/src/style/pages/Items/PageForm.scss b/src/style/pages/Items/Form.scss
similarity index 85%
rename from src/style/pages/Items/PageForm.scss
rename to src/style/pages/Items/Form.scss
index 9cc4db1d1..31575856d 100644
--- a/src/style/pages/Items/PageForm.scss
+++ b/src/style/pages/Items/Form.scss
@@ -1,16 +1,4 @@
-body.page-item-new,
-body.page-item-edit{
-
- .dashboard__footer{
- display: none;
- }
-}
-
-.dashboard__insider--item-form{
- padding-bottom: 64px;
-}
-
.page-form--item {
$self: '.page-form';
padding: 20px;
@@ -85,18 +73,14 @@ body.page-item-edit{
}
#{$self}__floating-actions {
- margin-left: -40px;
- margin-right: -40px;
+ // margin-left: -40px;
+ // margin-right: -40px;
.form-group--active {
display: inline-block;
margin: 0;
margin-left: 40px;
}
-
- .btn--submit{
- min-width: 65px;
- }
}
.bp3-tooltip-indicator {
diff --git a/src/style/pages/NotifyConactViaSMS/NotifyConactViaSMSDialog.scss b/src/style/pages/NotifyConactViaSMS/NotifyConactViaSMSDialog.scss
new file mode 100644
index 000000000..f4e334703
--- /dev/null
+++ b/src/style/pages/NotifyConactViaSMS/NotifyConactViaSMSDialog.scss
@@ -0,0 +1,31 @@
+.dialog--notify-vis-sms {
+ width: 800px;
+
+ .bp3-dialog-body {
+ .bp3-form-group {
+ margin-bottom: 15px;
+ margin-top: 15px;
+
+ label.bp3-label {
+ margin-bottom: 3px;
+ font-size: 13px;
+ }
+ }
+
+ .form-group {
+ &--sms_message {
+ .bp3-form-content {
+ textarea {
+ width: 100%;
+ min-width: 100%;
+ font-size: 14px;
+ }
+ }
+ }
+ }
+ }
+
+ .bp3-dialog-footer {
+ padding-top: 10px;
+ }
+}
diff --git a/src/style/pages/Preferences/Page.scss b/src/style/pages/Preferences/Page.scss
index 1f996676e..d156b96c4 100644
--- a/src/style/pages/Preferences/Page.scss
+++ b/src/style/pages/Preferences/Page.scss
@@ -17,8 +17,7 @@
&__inside-content {
display: flex;
flex-direction: column;
- height: 100%;
-
+
&--tabable {
margin-left: -25px;
margin-right: -25px;
diff --git a/src/style/pages/Preferences/SMSIntegration.scss b/src/style/pages/Preferences/SMSIntegration.scss
new file mode 100644
index 000000000..d2a52edf0
--- /dev/null
+++ b/src/style/pages/Preferences/SMSIntegration.scss
@@ -0,0 +1,38 @@
+// SMS Integration.
+// ---------------------------------
+
+.preferences-page__inside-content--sms-integration {
+ .bigcapital-datatable {
+ .table {
+ .tbody {
+ .notification {
+ &__label {
+ font-weight: 500;
+ }
+
+ &__desc {
+ font-size: 13px;
+ margin-top: 3px;
+ line-height: 1.25;
+ display: block;
+ }
+ }
+
+ .sms_message.td {
+ .edit-text {
+ display: inline-block;
+ font-size: 11.5px;
+ color: #1652c8;
+ margin-left: 2px;
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+ }
+ .bp3-tabs {
+ .bp3-tab-panel {
+ margin-top: 0;
+ }
+ }
+}
diff --git a/src/style/pages/SMSMessage/SMSMessage.scss b/src/style/pages/SMSMessage/SMSMessage.scss
new file mode 100644
index 000000000..bd9b46c90
--- /dev/null
+++ b/src/style/pages/SMSMessage/SMSMessage.scss
@@ -0,0 +1,29 @@
+.dialog--sms-message {
+ width: 800px;
+
+ .bp3-form-group {
+ margin-bottom: 15px;
+
+ label.bp3-label {
+ font-size: 13px;
+ margin-bottom: 6px;
+ }
+ }
+
+ .form-group {
+ &--message_text {
+ .bp3-form-content {
+ textarea {
+ width: 100%;
+ min-width: 100%;
+ min-height: 90px;
+ font-size: 14px;
+ }
+ }
+ }
+ }
+
+ .bp3-dialog-footer {
+ padding-top: 10px;
+ }
+}
diff --git a/src/style/pages/SaleInvoice/PageForm.scss b/src/style/pages/SaleInvoice/PageForm.scss
index 2951ad0d4..bfb6be69c 100644
--- a/src/style/pages/SaleInvoice/PageForm.scss
+++ b/src/style/pages/SaleInvoice/PageForm.scss
@@ -1,13 +1,11 @@
-
body.page-invoice-new,
-body.page-invoice-edit{
-
- .dashboard__footer{
+body.page-invoice-edit {
+ .dashboard__footer {
display: none;
}
}
-.dashboard__insider--invoice-form{
+.dashboard__insider--invoice-form {
padding-bottom: 64px;
}
@@ -29,7 +27,6 @@ body.page-invoice-edit{
}
.bp3-form-group {
-
&.bp3-inline {
max-width: 450px;
}
diff --git a/src/style/pages/Customers/PageForm.scss b/src/style/pages/Vendors/Form.scss
similarity index 91%
rename from src/style/pages/Customers/PageForm.scss
rename to src/style/pages/Vendors/Form.scss
index 57512123f..6a341d76c 100644
--- a/src/style/pages/Customers/PageForm.scss
+++ b/src/style/pages/Vendors/Form.scss
@@ -1,19 +1,6 @@
@import '../../Base.scss';
-
-body.page-customer-new,
-body.page-customer-edit{
-
- .dashboard__footer{
- display: none;
- }
-}
-
-.dashboard__insider--customer-form{
- padding-bottom: 64px;
-}
-
-.page-form--customer {
+.page-form--vendor {
$self: '.page-form';
padding: 20px;
@@ -169,7 +156,7 @@ body.page-customer-edit{
}
#{$self}__floating-actions {
- margin-left: -40px;
- margin-right: -40px;
+ // margin-left: -40px;
+ // margin-right: -40px;
}
}
diff --git a/src/style/pages/Vendors/PageForm.scss b/src/style/pages/Vendors/PageForm.scss
index dd7e4a372..251f8b3da 100644
--- a/src/style/pages/Vendors/PageForm.scss
+++ b/src/style/pages/Vendors/PageForm.scss
@@ -10,166 +10,4 @@ body.page-vendor-edit{
.dashboard__insider--vendor-form{
padding-bottom: 64px;
-}
-
-
-.page-form--vendor {
- $self: '.page-form';
- padding: 20px;
-
- #{$self}__header {
- padding: 0;
- }
- #{$self}__primary-section {
- padding: 10px 0 0;
- margin: 0 0 20px;
- overflow: hidden;
- border-bottom: 1px solid #e4e4e4;
- max-width: 1000px;
- }
-
- .bp3-form-group {
- max-width: 500px;
-
- .bp3-control {
- margin-top: 8px;
- margin-bottom: 8px;
- }
-
- &.bp3-inline {
- .bp3-label {
- min-width: 150px;
- }
- }
- .bp3-form-content {
- width: 100%;
- }
- }
-
- .form-group--contact_name {
- max-width: 600px;
-
- .bp3-control-group > * {
- flex-shrink: unset;
-
- &:not(:last-child) {
- padding-right: 10px;
- }
- &.input-group--salutation-list {
- width: 25%;
- }
- &.input-group--first-name,
- &.input-group--last-name {
- width: 37%;
- }
- }
- }
-
- .bp3-form-group {
- margin-bottom: 14px;
- }
-
- .bp3-tab-panel {
- margin-top: 26px;
- }
-
- .form-group--phone-number {
- .bp3-control-group > * {
- flex-shrink: unset;
- padding-right: 5px;
- padding-left: 5px;
-
- &:first-child {
- padding-left: 0;
- }
- &:last-child {
- padding-right: 0;
- }
- }
- }
-
- #{$self}__tabs {
- margin-top: 20px;
- max-width: 1000px;
-
- h4 {
- font-weight: 500;
- color: #888;
- margin-bottom: 1.2rem;
- font-size: 14px;
- }
- // Tab panels.
- .tab-panel {
- &--address {
- .bp3-form-group {
- max-width: 440px;
-
- &.bp3-inline {
- .bp3-label {
- min-width: 145px;
- }
- }
-
- .bp3-form-content {
- width: 100%;
- }
-
- textarea.bp3-input {
- max-width: 100%;
- width: 100%;
- min-height: 50px;
- }
- }
- }
- &--note {
- .form-group--note {
- .bp3-form-group {
- max-width: 600px;
- }
- textarea {
- width: 100%;
- min-height: 100px;
- }
- }
- }
- }
-
- .dropzone-container {
- max-width: 600px;
- }
- }
-
- .bp3-tabs {
- .bp3-tab-list {
- position: relative;
-
- &:before {
- content: '';
- position: absolute;
- bottom: 0;
- width: 100%;
- height: 2px;
- background: #f0f0f0;
- }
-
- > *:not(:last-child) {
- margin-right: 25px;
- }
-
- &.bp3-large > .bp3-tab {
- font-size: 15px;
- color: #555;
-
- &[aria-selected='true'],
- &:not([aria-disabled='true']):hover {
- color: $pt-link-color;
- }
- }
- }
- }
-
- #{$self}__floating-actions {
- margin-left: -40px;
- margin-right: -40px;
- }
-}
+}
\ No newline at end of file
diff --git a/src/utils/index.js b/src/utils/index.js
index c5f67fd32..f91986577 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -341,7 +341,7 @@ export const saveInvoke = (func, ...rest) => {
};
export const safeInvoke = (func, ...rest) => {
- return func && func(...rest);
+ func && func(...rest);
};
export const transformToForm = (obj, emptyInitialValues) => {