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

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