feat: notify by SMS.

This commit is contained in:
elforjani13
2021-11-06 21:47:17 +02:00
parent 2bd4c5f724
commit d26ef01afc
17 changed files with 307 additions and 106 deletions

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

@@ -61,7 +61,7 @@ import Card from './Card';
import AvaterCell from './AvaterCell';
import { ItemsMultiSelect } from './Items';
import MoreVertMenutItems from './MoreVertMenutItems';
import MoreMenuItems from './MoreMenutItems';
export * from './Menu';
export * from './AdvancedFilter/AdvancedFilterDropdown';
@@ -83,7 +83,7 @@ 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';
@@ -158,5 +158,5 @@ export {
ItemsMultiSelect,
Card,
AvaterCell,
MoreVertMenutItems,
MoreMenuItems
};

View File

@@ -1,6 +1,6 @@
import React from 'react';
import '../../../style/pages/NotifyConactViaSMS/NotifyConactViaSMSDialog.scss';
import 'style/pages/NotifyConactViaSMS/NotifyConactViaSMSDialog.scss';
import { NotifyContactViaSMSFormProvider } from './NotifyContactViaSMSFormProvider';
import NotifyContactViaSMSForm from './NotifyContactViaSMSForm';
@@ -10,9 +10,13 @@ import NotifyContactViaSMSForm from './NotifyContactViaSMSForm';
export default function NotifyContactViaSMSContent({
// #ownProps
dialogName,
invoice,
}) {
return (
<NotifyContactViaSMSFormProvider dialogName={dialogName}>
<NotifyContactViaSMSFormProvider
invoiceId={invoice}
dialogName={dialogName}
>
<NotifyContactViaSMSForm />
</NotifyContactViaSMSFormProvider>
);

View File

@@ -13,27 +13,54 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
import { useNotifyContactViaSMSContext } from './NotifyContactViaSMSFormProvider';
import { compose } from 'utils';
import { transformToForm, compose } from 'utils';
const defaultInitialValues = {
name: '',
phone: '',
note: '',
customer_name: '',
customer_personal_phone: '',
sms_message: '',
};
function NotifyContactViaSMSForm({
// #withDialogActions
closeDialog,
}) {
const { dialogName } = useNotifyContactViaSMSContext();
const {
invoiceId,
invoiceSMSDetail,
dialogName,
createNotifyInvoiceBySMSMutate,
} = useNotifyContactViaSMSContext();
// Initial form values
const initialValues = {
...defaultInitialValues,
...transformToForm(invoiceSMSDetail, defaultInitialValues),
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {};
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
// Handle request response success.
const onSuccess = (response) => {
AppToaster.show({
message: intl.get('notify_via_sms.dialog.success_message'),
intent: Intent.SUCCESS,
});
closeDialog(dialogName);
};
// Handle request response errors.
const onError = ({
response: {
data: { errors },
},
}) => {
setSubmitting(false);
};
createNotifyInvoiceBySMSMutate([invoiceId, values])
.then(onSuccess)
.catch(onError);
};
return (
<Formik

View File

@@ -3,9 +3,9 @@ import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from 'common/dataTypes';
const Schema = Yup.object().shape({
customer_id: Yup.string().required(),
phone: Yup.number().required(),
note: Yup.string().required().trim().max(DATATYPES_LENGTH.TEXT),
customer_name: Yup.string().required(),
customer_personal_phone: Yup.number().required(),
sms_message: Yup.string().required().trim().max(DATATYPES_LENGTH.TEXT),
});
export const CreateNotifyContactViaSMSFormSchema = Schema;

View File

@@ -24,48 +24,60 @@ function NotifyContactViaSMSFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/* ----------- Send Notification to ----------- */}
<FastField name={'customer_id'}>
<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_id'} />}
helperText={<ErrorMessage name={'customer_name'} />}
>
<InputGroup intent={inputIntent({ error, touched })} {...field} />
<InputGroup
intent={inputIntent({ error, touched })}
disabled={true}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Phone number ----------- */}
<FastField name={'phone'}>
<FastField name={'customer_personal_phone'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'phone_number'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="phone" />}
className={classNames('form-group--phone', CLASSES.FILL)}
helperText={<ErrorMessage name="customer_personal_phone" />}
className={classNames(
'form-group--customer_personal_phone',
CLASSES.FILL,
)}
>
<InputGroup intent={inputIntent({ error, touched })} {...field} />
<InputGroup
intent={inputIntent({ error, touched })}
disabled={true}
{...field}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Message Text ----------- */}
<FastField name={'note'}>
<FastField name={'sms_message'}>
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'notify_via_sms.dialog.message_text'} />}
labelInfo={<FieldRequiredHint />}
className={'form-group--note'}
className={'form-group--sms_message'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'note'} />}
helperText={<ErrorMessage name={'sms_message'} />}
>
<TextArea
growVertically={true}
large={true}
disabled={true}
intent={inputIntent({ error, touched })}
{...field}
/>

View File

@@ -1,27 +1,41 @@
import React from 'react';
import { DialogContent } from 'components';
import { useCustomers } from 'hooks/query';
import {
useInvoice,
useCreateNotifyInvoiceBySMS,
useInvocieSMSDetails,
} from 'hooks/query';
const NotifyContactViaSMSContext = React.createContext();
/**
* Notify contact via SMS provider.
*/
function NotifyContactViaSMSFormProvider({ dialogName, ...props }) {
// Fetches customers list.
const {
data: { customers },
isLoading: isCustomersLoading,
} = useCustomers();
function NotifyContactViaSMSFormProvider({ invoiceId, dialogName, ...props }) {
// Handle fetch invoice data.
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
enabled: !!invoiceId,
});
const { data: invoiceSMSDetail, isLoading: isInvoiceSMSDetailLoading } =
useInvocieSMSDetails(invoiceId, {
enabled: !!invoiceId,
});
// Create notfiy invoice by sms mutations.
const { mutateAsync: createNotifyInvoiceBySMSMutate } =
useCreateNotifyInvoiceBySMS();
// State provider.
const provider = {
invoiceId,
invoiceSMSDetail,
dialogName,
customers,
createNotifyInvoiceBySMSMutate,
};
return (
<DialogContent isLoading={isCustomersLoading}>
<DialogContent isLoading={isInvoiceLoading || isInvoiceSMSDetailLoading}>
<NotifyContactViaSMSContext.Provider value={provider} {...props} />
</DialogContent>
);

View File

@@ -12,18 +12,25 @@ const NotifyContactViaSMSDialogContent = React.lazy(() =>
/**
* Notify contact via SMS.
*/
function NotifyContactViaSMSDialog({ dialogName, payload, isOpen }) {
function NotifyContactViaSMSDialog({
dialogName,
payload: { invoiceId },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={'Notify via SMS'}
title={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
className={'dialog--notify-vis-sms'}
>
<DialogSuspense>
<NotifyContactViaSMSDialogContent dialogName={dialogName} />
<NotifyContactViaSMSDialogContent
dialogName={dialogName}
invoice={invoiceId}
/>
</DialogSuspense>
</Dialog>
);

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';
@@ -78,7 +67,7 @@ function InvoiceDetailActionsBar({
};
// Handle notify via SMS.
const handleNotifyViaSMS = () => {
openDialog('notify-via-sms', {});
openDialog('notify-via-sms', { invoiceId });
};
// Handle cancele write-off invoice.
@@ -120,7 +109,6 @@ function InvoiceDetailActionsBar({
/>
<NavbarDivider />
<BadDebtMenuItem
invoice={invoice}
payload={{
onBadDebt: handleBadDebtInvoice,
onCancelBadDebt: handleCancelBadDebtInvoice,

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.
@@ -59,9 +60,10 @@ export const useInvoiceReadonlyEntriesColumns = () =>
);
export const BadDebtMenuItem = ({
invoice,
payload: { onCancelBadDebt, onBadDebt, onNotifyViaSMS },
}) => {
const { invoice } = useInvoiceDetailDrawerContext();
return (
<Popover
minimal={true}

View File

@@ -189,3 +189,38 @@ export function useRefreshEstimates() {
},
};
}
export function useCreateNotifyEstimateBySMS(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(id) => apiRequest.post(`sales/estimates/${id}/notify-by-sms`),
{
onSuccess: (res, id) => {
// Common invalidate queries.
commonInvalidateQueries(queryClient);
// Invalidate sale estimate.
queryClient.invalidateQueries([t.NOTIFY_SALE_ESTIMATE_BY_SMS, id]);
},
...props,
},
);
}
export function useEstimateSMS(estimateId, props, requestProps) {
return useRequestQuery(
[t.SALE_ESTIMATE_SMS, estimateId],
{
method: 'get',
url: `sales/estimates/${estimateId}/sms-details`,
...requestProps,
},
{
select: (res) => res.data,
defaultData: {},
...props,
},
);
}

View File

@@ -219,14 +219,52 @@ 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) => apiRequest.post(`sales/invoices/${id}/notify-by-sms`),
{
onSuccess: (res, id) => {
// Invalidate
queryClient.invalidateQueries([t.NOTIFY_SALE_INVOICE_BY_SMS, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function useInvocieSMSDetails(invoiceId, props, requestProps) {
return useRequestQuery(
[t.SALE_INVOICE_SMS, invoiceId],
{
method: 'get',
url: `sales/invoices/${invoiceId}/sms-details`,
...requestProps,
},
{
select: (res) => res.data.data,
defaultData: {},
...props,
},
);
}

View File

@@ -174,3 +174,34 @@ export function useRefreshPaymentReceive() {
},
};
}
export function useCreateNotifyPaymentReceiveBySMS(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(id) => apiRequest.post(`sales/payment_receives/${id}/notify-by-sms`),
{
onSuccess: (res, id) => {
// Invalidate
queryClient.invalidateQueries([t.NOTIFY_PAYMENT_RECEIVE_BY_SMS, id]);
// Common invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function usePaymentReceiveSMS(id, props, requestProps) {
return useRequestQuery(
[t.PAYMENT_RECEIVE_SMS, id],
{ method: 'get', url: `sales/payment_receives/${id}/sms-details`, ...requestProps },
{
select: (res) => res.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,32 @@ export function useRefreshReceipts() {
},
};
}
export function useCreateNotifyReceiptBySMS(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(id) => apiRequest.post(`sales/receipts/${id}/notify-by-sms`),
{
onSuccess: (res, id) => {
queryClient.invalidateQueries([t.NOTIFY_SALE_RECEIPT_BY_SMS, id]);
// Invalidate queries.
commonInvalidateQueries(queryClient);
},
...props,
},
);
}
export function useReceiptSMS(receiptId, props, requestProps) {
return useRequestQuery(
[t.SALE_RECEIPT_SMS, receiptId],
{ method: 'get', url: `sales/receipts/${receiptId}/sms-details`, ...requestProps },
{
select: (res) => res.data,
defaultData: {},
...props,
},
);
}

View File

@@ -123,3 +123,18 @@ export function useSettingCashFlow(props) {
props,
);
}
/**
* Retrieve SMS settings.
*/
export function useSettingSMSNotifications(props) {
return useRequestQuery(
[t.SETTING_SMS_NOTIFICATIONS],
{ method: 'get', url: `settings/sms-notifications` },
{
select: (res) => res.data.notifications,
defaultData: [],
...props,
},
);
}

View File

@@ -51,11 +51,15 @@ const ITEMS = {
const SALE_ESTIMATES = {
SALE_ESTIMATES: 'SALE_ESTIMATES',
SALE_ESTIMATE: 'SALE_ESTIMATE',
SALE_ESTIMATE_SMS: 'SALE_ESTIMATE_SMS',
NOTIFY_SALE_ESTIMATE_BY_SMS: 'NOTIFY_SALE_ESTIMATE_BY_SMS',
};
const SALE_RECEIPTS = {
SALE_RECEIPTS: 'SALE_RECEIPTS',
SALE_RECEIPT: 'SALE_RECEIPT',
SALE_RECEIPT_SMS: 'SALE_RECEIPT_SMS',
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: 'PAYMENT_RECEIVE_SMS',
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: 'SALE_INVOICE_SMS',
NOTIFY_SALE_INVOICE_BY_SMS: 'NOTIFY_SALE_INVOICE_BY_SMS',
BAD_DEBT: 'BAD_DEBT',
CANCEL_BAD_DEBT: 'CANCEL_BAD_DEBT',
};
@@ -103,6 +111,7 @@ const SETTING = {
SETTING_MANUAL_JOURNALS: 'SETTING_MANUAL_JOURNALS',
SETTING_ITEMS: 'SETTING_ITEMS',
SETTING_CASHFLOW: 'SETTING_CASHFLOW',
SETTING_SMS_NOTIFICATIONS: 'SETTING_SMS_NOTIFICATIONS',
};
const ORGANIZATIONS = {