feat: Add SMS Integration & SMS Message Form.

This commit is contained in:
elforjani13
2021-11-07 13:39:29 +02:00
parent d26ef01afc
commit 834d365a97
28 changed files with 612 additions and 155 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

@@ -31,7 +31,7 @@ const SwitchEditableCell = ({
>
<Switch
value={value}
onChange={onChange}
// onChange={onChange}
checked={initialValue}
minimal={true}
className="ml2"

View File

@@ -21,6 +21,7 @@ import MoneyInDialog from '../containers/Dialogs/MoneyInDialog';
import MoneyOutDialog from '../containers/Dialogs/MoneyOutDialog';
import BadDebtDialog from '../containers/Dialogs/BadDebtDialog';
import NotifyContactViaSMSDialog from '../containers/Dialogs/NotifyContactViaSMSDialog';
import SMSMessageDialog from '../containers/Dialogs/SMSMessageDialog';
/**
* Dialogs container.
@@ -48,6 +49,7 @@ export default function DialogsContainer() {
<MoneyOutDialog dialogName={'money-out'} />
<NotifyContactViaSMSDialog dialogName={'notify-via-sms'} />
<BadDebtDialog dialogName={'write-off-bad-debt'} />
<SMSMessageDialog dialogName={'sms-message-form'} />
</div>
);
}

View File

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

View File

@@ -0,0 +1,38 @@
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();
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,79 @@
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 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 },
},
}) => {
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,17 @@
import React from 'react';
import { Form } from 'formik';
import SMSMessageFormFields from './SMSMessageFormFields';
import SMSMessageFormFloatingActions from './SMSMessageFormFloatingActions';
/**
* SMS message form content.
*/
export default function SMSMessageFormContent() {
return (
<Form>
<SMSMessageFormFields />
<SMSMessageFormFloatingActions />
</Form>
);
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { FastField, Field, ErrorMessage } from 'formik';
import { Classes, FormGroup, TextArea } from '@blueprintjs/core';
import { FormattedMessage as T, FieldRequiredHint } from 'components';
import { inputIntent } from 'utils';
export default function SMSMessageFormFields() {
return (
<div className={Classes.DIALOG_BODY}>
{/* ----------- 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'} />}
>
<TextArea
growVertically={true}
large={true}
intent={inputIntent({ error, touched })}
{...field}
/>
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -0,0 +1,47 @@
import React from 'react';
import { Intent, Button, Classes } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { 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();
const { dialogName } = useSMSMessageDialogContext();
// Handle close button click.
const handleCancelBtnClick = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleCancelBtnClick} style={{ minWidth: '75px' }}>
<T id={'cancel'} />
</Button>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
style={{ minWidth: '75px' }}
type="submit"
>
{<T id={'save'} />}
</Button>
</div>
</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')}
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,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,40 @@
import React from 'react';
import { DataTableEditable, DataTable } from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import { useSMSIntegrationTableColumns } from './components';
import { useSMSIntegrationContext } from './SMSIntegrationProvider';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
function SMSMessagesDataTable({
// #withDialogAction
openDialog,
}) {
// Table columns.
const columns = useSMSIntegrationTableColumns();
const { notifications, isSMSNotificationsLoading } =
useSMSIntegrationContext();
const handleEditSMSMessage = ({ key }) => {
openDialog('sms-message-form', { notificationkey: key });
};
return (
<DataTable
columns={columns}
data={notifications}
loading={isSMSNotificationsLoading}
progressBarLoading={isSMSNotificationsLoading}
TableLoadingRenderer={TableSkeletonRows}
noInitialFetch={true}
payload={{
onEditSMSMessage: handleEditSMSMessage,
}}
/>
);
}
export default compose(withDialogActions)(SMSMessagesDataTable);

View File

@@ -0,0 +1,67 @@
import React from 'react';
import intl from 'react-intl-universal';
import { SwitchFieldCell } from 'components/DataTableCells';
import { safeCallback } from 'utils';
/**
* Notification accessor.
*/
export const NotificationAccessor = (row) => {
return (
<span className="notification">
<span className={'notification__label'}>{row.notification_label}</span>
<span className={'notification__desc'}>
{row.notification_description}
</span>
</span>
);
};
export const SMSMessageCell = ({
payload: { onEditSMSMessage },
row: { original },
}) => (
<div>
{original.sms_message}
<span
className="edit-text"
onClick={safeCallback(onEditSMSMessage, original)}
>
{'Edit'}
</span>
</div>
);
export function useSMSIntegrationTableColumns() {
return React.useMemo(() => [
{
Header: intl.get('sms_message.label_Notification'),
accessor: NotificationAccessor,
className: 'notification',
width: '180',
},
{
Header: intl.get('service'),
accessor: 'module_formatted',
className: 'service',
width: '80',
},
{
Header: intl.get('sms_message.label_mesage'),
accessor: 'sms_message',
Cell: SMSMessageCell,
className: 'sms_message',
clickable: true,
width: '180',
},
{
Header: intl.get('sms_message.label_auto'),
accessor: 'is_notification_enabled',
Cell: SwitchFieldCell,
className: 'is_notification_enabled',
disableSortBy: true,
disableResizing: true,
width: '80',
},
]);
}

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

@@ -1,54 +0,0 @@
import React from 'react';
import intl from 'react-intl-universal';
import {
DataTableEditable,
DataTable,
DashboardContentTable,
} from 'components';
import TableSkeletonRows from 'components/Datatable/TableSkeletonRows';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { useSMSMessagesTemplatesTableColumns } from './components';
import { compose } from 'utils';
function SMSMessagesTemplatesDataTable({
// #withDashboardActions
changePreferencesPageTitle,
}) {
// Table columns.
const columns = useSMSMessagesTemplatesTableColumns();
React.useEffect(() => {
changePreferencesPageTitle(
intl.get('sms_message_template.label.sms_messages_template'),
);
}, [changePreferencesPageTitle]);
const DATA = [
{
notification: 'notification',
service: 'service',
message: 'message',
auto: true,
},
{
notification: 'notification',
service: 'service',
message: 'message',
auto: false,
},
];
return (
<DataTableEditable
columns={columns}
data={[]}
// loading={}
// progressBarLoading={}
TableLoadingRenderer={TableSkeletonRows}
noInitialFetch={true}
/>
);
}
export default compose(withDashboardActions)(SMSMessagesTemplatesDataTable);

View File

@@ -1,35 +0,0 @@
import React from 'react';
import classNames from 'classnames';
import { CLASSES } from 'common/classes';
import { useSettings } from 'hooks/query';
const SMSMessagesTemplatesContext = React.createContext();
/**
* SMS message templates provider.
*/
function SMSMessagesTemplatesProvider({ ...props }) {
//Fetches Organization Settings.
const { isLoading: isSettingsLoading } = useSettings();
// Provider state.
const provider = {};
return (
<div
className={classNames(
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT,
CLASSES.PREFERENCES_PAGE_INSIDE_CONTENT_ACCOUNTANT,
)}
>
<div className={classNames(CLASSES.CARD)}>
<SMSMessagesTemplatesContext.Provider value={provider} {...props} />
</div>
</div>
);
}
const useSMSMessageTemplateContext = () =>
React.useContext(SMSMessagesTemplatesContext);
export { SMSMessagesTemplatesProvider, useSMSMessageTemplateContext };

View File

@@ -1,40 +0,0 @@
import React from 'react';
import intl from 'react-intl-universal';
import {
InputGroupCell,
TextAreaCell,
SwitchFieldCell,
} from 'components/DataTableCells';
export function useSMSMessagesTemplatesTableColumns() {
return React.useMemo(() => [
{
Header: intl.get('sms_message_template.label_Notification'),
accessor: 'notification',
className: 'notification',
width: '150',
},
{
Header: intl.get('service'),
accessor: 'service',
className: 'service',
width: '100',
},
{
Header: intl.get('sms_message_template.label_mesage'),
accessor: 'message',
className: 'message',
width: '180',
},
{
Header: intl.get('sms_message_template.label_auto'),
accessor: 'auto',
Cell: SwitchFieldCell,
className: 'auto',
disableSortBy: true,
disableResizing: true,
width: '80',
},
]);
}

View File

@@ -1,15 +0,0 @@
import React from 'react';
import { SMSMessagesTemplatesProvider } from './SMSMessagesTemplatesProvider';
import SMSMessagesTemplatesDataTable from './SMSMessagesTemplatesDataTable';
/**
* SMS messages templates.
*/
export default function SMSMessagesTemplates() {
return (
<SMSMessagesTemplatesProvider>
<SMSMessagesTemplatesDataTable />
</SMSMessagesTemplatesProvider>
);
}

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

@@ -125,7 +125,7 @@ export function useSettingCashFlow(props) {
}
/**
* Retrieve SMS settings.
* Retrieve SMS Notifications settings.
*/
export function useSettingSMSNotifications(props) {
return useRequestQuery(
@@ -138,3 +138,42 @@ export function useSettingSMSNotifications(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: () => {
// Invalidate
queryClient.invalidateQueries([t.SETTING_SMS_NOTIFICATIONS]);
},
...props,
},
);
}

View File

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

@@ -1430,10 +1430,15 @@
"notify_via_sms.dialog.send_notification_to":"Send notification to",
"notify_via_sms.dialog.message_text":"Message Text",
"notify_via_sms.dialog.notify_via_sms":"Notify vis SMS",
"notify_via_sms.dialog.success_message":"To notify have been successfully",
"send": "Send",
"sms_message_template.label.sms_messages_template":"SMS Messages template",
"sms_message_template.label_mesage":"Message",
"sms_message_template.label_Notification":"Notification",
"sms_message_template.label_auto":"Auto",
"sms_message":"SMS message"
"sms_integration.label":"SMS Integration",
"sms_integration.label.overview":"Overview",
"sms_integration.label.sms_messages":"SMS Messages",
"sms_message.label.sms_messages_template":"SMS Notifications ",
"sms_message.label_mesage":"Message",
"sms_message.label_Notification":"Notification",
"sms_message.label_auto":"Auto",
"sms_message":"SMS message",
"sms_message.dialog.success_message":"Sms notification settings has been updated successfully."
}

View File

@@ -4,7 +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 SMSMessagesTemplates from '../containers/Preferences/SMSMessagesTemplates';
import SMSIntegration from '../containers/Preferences/SMSIntegration';
import DefaultRoute from '../containers/Preferences/DefaultRoute';
const BASE_URL = '/preferences';
@@ -37,7 +37,7 @@ export default [
},
{
path: `${BASE_URL}/sms-message`,
component: SMSMessagesTemplates,
component: SMSIntegration,
exact: true,
},
{

View File

@@ -14,7 +14,7 @@
}
.form-group {
&--note {
&--sms_message {
.bp3-form-content {
textarea {
width: 100%;

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 {
max-width: 350px;
.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;
}
}