Merge branch 'feature/notify-via-SMS' into develop

This commit is contained in:
a.bouhuolia
2021-11-10 23:58:34 +02:00
73 changed files with 2546 additions and 132 deletions

View File

@@ -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',

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,9 @@ import ItemsListCell from './ItemsListCell';
import PercentFieldCell from './PercentFieldCell';
import { 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,
};

View File

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

View File

@@ -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 = (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,9 +23,6 @@ import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect';
import 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,11 @@ 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';
const Hint = FieldHint;
@@ -120,9 +119,6 @@ export {
LoadingIndicator,
DashboardActionViewsList,
AppToaster,
Dialog,
DialogContent,
DialogSuspense,
InputPrependButton,
CategoriesSelectList,
Col,
@@ -158,5 +154,5 @@ export {
ItemsMultiSelect,
Card,
AvaterCell,
MoreVertMenutItems,
MoreMenuItems,
};

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
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: 'Sale estimate details',
};
function NotifyEstimateViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
estimateId,
dialogName,
estimateSMSDetail,
createNotifyEstimateBySMSMutate,
} = useEstimateViaSMSContext();
// 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 });
}
setSubmitting(false);
};
createNotifyEstimateBySMSMutate([estimateId, values])
.then(onSuccess)
.catch(onError);
};
const initialValues = {
...estimateSMSDetail,
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onCancel={handleFormCancel}
onSubmit={handleFormSubmit}
/>
);
}
export default compose(withDialogActions)(NotifyEstimateViaSMSForm);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,103 @@
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']);
};
/**
* 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);
}
};
// Momerize the notification types.
const notificationTypes = React.useMemo(
() => [
{ key: 'details', label: 'Invoice details' },
{ key: 'reminder', label: 'Invoice reminder' },
],
[],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationTypes}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
onValuesChange={handleValuesChange}
calloutCodes={calloutCode}
/>
);
}
export default compose(withDialogActions)(NotifyInvoiceViaSMSForm);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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 { 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: 'Payment receive thank you.',
};
/**
* Notify Payment Recive Via SMS Form.
*/
function NotifyPaymentReceiveViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
paymentReceiveId,
paymentReceiveMSDetail,
createNotifyPaymentReceivetBySMSMutate,
} = useNotifyPaymentReceiveViaSMSContext();
// 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 });
}
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 }),
[paymentReceiveMSDetail],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
/>
);
}
export default compose(withDialogActions)(NotifyPaymentReceiveViaSMSForm);

View File

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

View File

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

View File

@@ -0,0 +1,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 { 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: 'Sale receipt details',
};
/**
* Notify Receipt Via SMS Form.
*/
function NotifyReceiptViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const {
dialogName,
receiptId,
receiptSMSDetail,
createNotifyReceiptBySMSMutate,
} = useNotifyReceiptViaSMSContext();
// 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 });
}
setSubmitting(false);
};
createNotifyReceiptBySMSMutate([receiptId, values])
.then(onSuccess)
.catch(onError);
};
// Handle the form cancel.
const handleFormCancel = () => {
closeDialog(dialogName);
};
// Initial values.
const initialValues = React.useMemo(
() => ({
...receiptSMSDetail,
}),
[receiptSMSDetail],
);
return (
<NotifyViaSMSForm
initialValues={initialValues}
notificationTypes={notificationType}
onSubmit={handleFormSubmit}
onCancel={handleFormCancel}
/>
);
}
export default compose(withDialogActions)(NotifyReceiptViaSMSForm);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,156 @@
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';
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 (
<SMSPreviewSectionRoot>
<SMSMessagePreview message={sms_message} />
<SMSPreviewSectionNote>
{intl.formatHTMLMessage(
{ id: 'notiify_via_sms.dialog.sms_note' },
{
value: messagesUnits,
},
)}
</SMSPreviewSectionNote>
</SMSPreviewSectionRoot>
);
}
/**
* 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 (
<Formik
enableReinitialize={true}
validationSchema={CreateNotifyViaSMSFormSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form>
<div className={Classes.DIALOG_BODY}>
<NotifyContent>
<NotifyFieldsSection>
<NotifyViaSMSAlerts calloutCodes={calloutCodes} />
<NotifyViaSMSFormFields
notificationTypes={formattedNotificationTypes}
/>
</NotifyFieldsSection>
<SMSMessagePreviewSection />
</NotifyContent>
</div>
<NotifyViaSMSFormFloatingActions onCancel={onCancel} />
<NotifyObserveValuesChange onChange={onValuesChange} />
</Form>
</Formik>
);
}
/**
* Observes the values change of notify form.
*/
function NotifyObserveValuesChange({ onChange }) {
const { values } = useFormikContext();
// Handle the form change observe.
const handleChange = () => {
safeInvoke(onChange, values);
};
return <FormObserver values={values} onChange={handleChange} />;
}
/**
* Notify via SMS form alerts.
*/
function NotifyViaSMSAlerts({ calloutCodes }) {
return [
includes(calloutCodes, 100) && (
<Callout icon={null} intent={Intent.DANGER}>
{intl.get('notify_Via_sms.dialog.customer_phone_number_does_not_eixst')}
</Callout>
),
includes(calloutCodes, 200) && (
<Callout icon={null} intent={Intent.DANGER}>
{intl.get('notify_Via_sms.dialog.customer_phone_number_invalid')}
</Callout>
),
];
}
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%;
padding-left: 25px;
margin-left: 25px;
border-left: 1px solid #dcdcdd;
`;
const SMSPreviewSectionNote = styled.div`
font-size: 12px;
opacity: 0.7;
`;

View File

@@ -0,0 +1,11 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
customer_name: Yup.string().required(),
customer_phone_number: Yup.number(),
sms_message: Yup.string().required().trim().max(DATATYPES_LENGTH.TEXT),
});
export const CreateNotifyViaSMSFormSchema = Schema;

View File

@@ -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 (
<NotifyViaSMSFormFieldsRoot>
<FastField name={'notification_key'}>
{({ form, meta: { error, touched } }) => (
<FormGroup
label={<T id={'notify_via_sms.dialog.notification_type'} />}
className={classNames(CLASSES.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_name'} />}
>
<ListSelect
items={notificationTypes}
selectedItemProp={'key'}
selectedItem={'details'}
textProp={'label'}
popoverProps={{ minimal: true }}
filterable={false}
onItemSelect={(notification) => {
form.setFieldValue('notification_key', notification.key);
}}
disabled={notificationTypes.length < 2}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Send Notification to ----------- */}
<FastField name={'customer_name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'notify_via_sms.dialog.send_notification_to'} />}
className={classNames('form-group--customer-name', CLASSES.FILL)}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'customer_name'} />}
>
<InputGroup
intent={inputIntent({ error, touched })}
disabled={true}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Phone number ----------- */}
<FastField name={'customer_phone_number'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'phone_number'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="customer_phone_number" />}
className={classNames(
'form-group--customer_phone_number',
CLASSES.FILL,
)}
>
<InputGroup
intent={inputIntent({ error, touched })}
disabled={true}
{...field}
/>
</FormGroup>
)}
</FastField>
</NotifyViaSMSFormFieldsRoot>
);
}
const NotifyViaSMSFormFieldsRoot = styled.div``;

View File

@@ -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 (
<div className={Classes.DIALOG_FOOTER}>
<DialogFooterActions alignment={'left'}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '110px' }}
type="submit"
>
<T id={'send_sms'} />
</Button>
<Button
disabled={isSubmitting}
onClick={handleCancelBtnClick}
style={{ minWidth: '75px' }}
>
<T id={'cancel'} />
</Button>
</DialogFooterActions>
</div>
);
}

View File

@@ -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);
};

View File

@@ -0,0 +1,39 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { useSettings, useSettingSMSNotifications } from 'hooks/query';
import PreferencesPageLoader from '../PreferencesPageLoader';
const SMSIntegrationContext = React.createContext();
/**
* SMS Integration provider.
*/
function SMSIntegrationProvider({ ...props }) {
//Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings();
const { data: notifications, isLoading: isSMSNotificationsLoading } =
useSettingSMSNotifications();
// Provider state.
const provider = {
notifications,
isSMSNotificationsLoading,
};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_SMS_INTEGRATION,
)}
>
<SMSIntegrationContext.Provider value={provider} {...props} />
</div>
);
}
const useSMSIntegrationContext = () => React.useContext(SMSIntegrationContext);
export { SMSIntegrationProvider, useSMSIntegrationContext };

View File

@@ -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 (
<div className={classNames(CLASSES.CARD)}>
<div className={classNames(CLASSES.PREFERENCES_PAGE_TABS)}>
<Tabs animate={true} defaultSelectedTabId={'sms_messages'}>
<Tab
id="overview"
title={intl.get('sms_integration.label.overview')}
/>
<Tab
id="sms_messages"
title={intl.get('sms_integration.label.sms_messages')}
panel={<SMSMessagesDataTable />}
/>
</Tabs>
</div>
</div>
);
}
export default compose(withDashboardActions)(SMSIntegrationTabs);

View File

@@ -0,0 +1,86 @@
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();
// Handle notification switch change.
const handleNotificationSwitchChange = React.useCallback(
(event, value, notification) => {
editSMSNotificationMutate({
notification_key: notification.key,
is_notification_enabled: value,
}).then(() => {
AppToaster.show({
message: intl.get(
'sms_messages.notification_switch_change_success_message',
),
intent: Intent.SUCCESS,
});
});
},
[editSMSNotificationMutate],
);
// Table columns.
const columns = useSMSIntegrationTableColumns({
onSwitchChange: handleNotificationSwitchChange,
});
const { notifications, isSMSNotificationsLoading } =
useSMSIntegrationContext();
// handle edit message link click
const handleEditMessageText = ({ key }) => {
openDialog('sms-message-form', { notificationkey: key });
};
const handleEnableNotification = () => {};
return (
<SMSNotificationsTable
columns={columns}
data={notifications}
loading={isSMSNotificationsLoading}
progressBarLoading={isSMSNotificationsLoading}
TableLoadingRenderer={TableSkeletonRows}
noInitialFetch={true}
ContextMenu={ActionsMenu}
payload={{
onEditMessageText: handleEditMessageText,
onEnableNotification: handleEnableNotification,
}}
/>
);
}
export default compose(withDialogActions)(SMSMessagesDataTable);
const SMSNotificationsTable = styled(DataTable)`
.table .tbody .tr .td {
align-items: flex-start;
}
.table .tbody .td {
padding: 0.8rem;
}
`;

View File

@@ -0,0 +1,137 @@
import React from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
import { Intent, Button, Menu, MenuItem } from '@blueprintjs/core';
import { SwitchFieldCell } from 'components/DataTableCells';
import { safeInvoke } from 'utils';
/**
* Notification accessor.
*/
export const NotificationAccessor = (row) => {
return (
<span className="notification">
<NotificationLabel>{row.notification_label}</NotificationLabel>
<NotificationDescription>
{row.notification_description}
</NotificationDescription>
</span>
);
};
/**
* SMS notification message cell.
*/
export const SMSMessageCell = ({
payload: { onEditMessageText },
row: { original },
}) => (
<div>
<MessageBox>{original.sms_message}</MessageBox>
<MessageBoxActions>
<Button
minimal={true}
small={true}
intent={Intent.NONE}
onClick={() => safeInvoke(onEditMessageText, original)}
>
{intl.get('sms_messages.label_edit_message')}
</Button>
</MessageBoxActions>
</div>
);
/**
* Context menu of SMS notification messages.
*/
export function ActionsMenu({
payload: { onEditMessageText, onEnableNotification },
row: { original },
}) {
return (
<Menu>
<MenuItem
text={intl.get('edit_message_text')}
onClick={safeInvoke(onEditMessageText, original)}
/>
<MenuItem
text={intl.get('enable_notification')}
onClick={safeInvoke(onEnableNotification, original)}
/>
</Menu>
);
}
/**
* Retrieve SMS notifications messages table columns
* @returns
*/
export function useSMSIntegrationTableColumns({ onSwitchChange }) {
return React.useMemo(
() => [
{
id: 'notification',
Header: intl.get('sms_messages.label_notification'),
accessor: NotificationAccessor,
className: 'notification',
width: '180',
disableSortBy: true,
},
{
Header: intl.get('service'),
accessor: 'module_formatted',
className: 'service',
width: '80',
disableSortBy: true,
},
{
Header: intl.get('sms_messages.label_mesage'),
accessor: 'sms_message',
Cell: SMSMessageCell,
className: 'sms_message',
width: '180',
disableSortBy: true,
},
{
Header: intl.get('sms_messages.label_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;
}
`;

View File

@@ -0,0 +1,15 @@
import React from 'react';
import { SMSIntegrationProvider } from './SMSIntegrationProvider';
import SMSIntegrationTabs from './SMSIntegrationTabs';
/**
* SMS SMS Integration
*/
export default function SMSIntegration() {
return (
<SMSIntegrationProvider>
<SMSIntegrationTabs />
</SMSIntegrationProvider>
);
}

View File

@@ -78,7 +78,7 @@ export function ActionsMenu({
*/
function StatusAccessor(user) {
return !user.is_invite_accepted ? (
<Tag minimal={true}>
<Tag minimal={true} >
<T id={'inviting'} />
</Tag>
) : user.active ? (

View File

@@ -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.

View File

@@ -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,
},
);
}

View File

@@ -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,
},
);
}

View File

@@ -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,
},
);
}

View File

@@ -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,
},
);
}

View File

@@ -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,
},
);
}

View File

@@ -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 = {

View File

@@ -1439,5 +1439,36 @@
"bad_debt.dialog.header_note": "يمكن للبائع تحميل مبلغ الفاتورة على حساب مصروفات الديون المعدومة عندما يكون من المؤكد أن الفاتورة لن يتم دفعها.",
"bad_debt.dialog.success_message":"تم شطب فاتورة البيع المقدمة بنجاح.",
"bad_debt.cancel_alert.success_message":"تم إلغاء شطب فاتورة البيع المقدمة بنجاح.",
"bad_debt.cancel_alert.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": "<strong>ملاحظة :</strong> يمكن أن تحتوي رسالة قصيرة الواحدة على 160 حرفًا كحد أقصى. <strong>{value}</strong> سيتم استخدام وحدات الرسائل القصيرة لإرسال هذا إشعار عبر الرسائل القصيرة. ",
"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.label_mesage":"رسالة ",
"sms_messages.label_notification":"إشعار",
"sms_messages.label_auto":"تلقائي",
"sms_messages.label_edit_message": "تعديل الرسالة ",
"sms_messages.notification_switch_change_success_message":"تم تمكين إشعار الرسائل القصيرة بنجاح.",
"sms_message.dialog.label": "رسالة قصيرة",
"sms_message.dialog.sms_note": "<strong>ملاحظة :</strong> One SMS unit can contain amaximum of 160 characters. <strong>{value}</strong> SMS units will be used to send this SMS notification.",
"sms_message.dialog.success_message": "تم تحديث إعدادات إشعار الرسائل القصيرة بنجاح. ",
"sms_message.dialog.unsupported_variables_error_message": "متغيرات غير مدعومة",
"sms_message.dialog.message_variable_description":"<strong>{value}</strong> إشارة لاسم الشركة الحالي.",
"edit_message_text":"تعديل نص رسالة",
"enable_notification":"تفعيل الإشعارات",
"send_sms":"إرسال رسالة قصيرة",
"save_sms_message":"حفظ رسالة قصيرة"
}

View File

@@ -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,37 @@
"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": "<strong>Note :</strong> One SMS unit can contain a maximum of 160 characters. <strong>{value}</strong> 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.phone_invalid_error_message": "Sms notification cannot be sent, customer personal phone number is invalid, please enter a valid one and try again.",
"notify_via_sms.dialog.customer_no_phone_error_message": "The customer has no phone number.",
"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.label_notification": "Notification",
"sms_messages.label_mesage": "Message",
"sms_messages.label_auto": "Auto",
"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": "<strong>Note :</strong> One SMS unit can contain a maximum of 160 characters. <strong>{value}</strong> 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": "<strong>{value}</strong> References to the current company name.",
"edit_message_text": "Edit message text",
"enable_notification": "Enable notification",
"send_sms": "Send SMS",
"save_sms_message": "Save SMS Message"
}

View File

@@ -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,

View File

@@ -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",
}
};

View File

@@ -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;
}
}

View File

@@ -17,8 +17,7 @@
&__inside-content {
display: flex;
flex-direction: column;
height: 100%;
&--tabable {
margin-left: -25px;
margin-right: -25px;

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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) => {