diff --git a/frontend/packages/common/src/intl/locales/en/system.json b/frontend/packages/common/src/intl/locales/en/system.json index 2848b20a..a474efb9 100644 --- a/frontend/packages/common/src/intl/locales/en/system.json +++ b/frontend/packages/common/src/intl/locales/en/system.json @@ -90,5 +90,8 @@ "label.test-sql-query": "Test SQL Query", "tooltip.manage-recipient-list": "Manage Recipient List", "message.no-parameters": "No parameters found", - "message.no-sensors-selected": "No sensors selected" + "message.no-sensors-selected": "No sensors selected", + "label.select-queries": "Select Queries", + "message.no-queries-selected": "No queries selected", + "label.schedule": "Schedule" } \ No newline at end of file diff --git a/frontend/packages/common/src/ui/layout/tables/DataTable.tsx b/frontend/packages/common/src/ui/layout/tables/DataTable.tsx index 8ae11c60..27c3ec8e 100644 --- a/frontend/packages/common/src/ui/layout/tables/DataTable.tsx +++ b/frontend/packages/common/src/ui/layout/tables/DataTable.tsx @@ -9,10 +9,7 @@ import { Table as MuiTable, Typography, } from '@mui/material'; -import { - BasicSpinner, - useRegisterActions, -} from '@notify-frontend/common'; +import { BasicSpinner, useRegisterActions } from '@notify-frontend/common'; import { TableProps } from './types'; import { DataRow } from './components/DataRow/DataRow'; @@ -108,7 +105,6 @@ export const DataTableComponent = ({ display: 'flex', flexDirection: 'column', flex: 1, - height: '100%', }} > diff --git a/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx index bc48bd61..5f58d8be 100644 --- a/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/Base/BaseNotificationEditPage.tsx @@ -90,11 +90,15 @@ export const BaseNotificationEditPage = ({ }); }; - const requiredParams = (sqlRecipientLists?.nodes ?? []) + const requiredRecipientParams = (sqlRecipientLists?.nodes ?? []) .filter(list => draft.sqlRecipientListIds.includes(list.id)) .map(list => list.parameters) .flat(1); + const requiredConfigParams = draft.requiredParameters; + + const requiredParams = requiredRecipientParams.concat(requiredConfigParams); + const allParamsSet = requiredParams.every(param => { if (param) { return draft.parsedParameters[param] !== undefined; // This allows the user to set the param to an empty string if they edit the field then delete the value diff --git a/frontend/packages/system/src/Notifications/Pages/Base/NotificationDetailPanel.tsx b/frontend/packages/system/src/Notifications/Pages/Base/NotificationDetailPanel.tsx index a0d0410f..facc0ef2 100644 --- a/frontend/packages/system/src/Notifications/Pages/Base/NotificationDetailPanel.tsx +++ b/frontend/packages/system/src/Notifications/Pages/Base/NotificationDetailPanel.tsx @@ -42,6 +42,7 @@ export const NotificationDetailPanel = ({ onUpdateParams(param ?? '', e.target.value)} + error={requiredParams.includes(param) && !params[param ?? '']} /> { // if param is not required allow it to be removed diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx index 05eba005..fd0d8989 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx @@ -44,6 +44,7 @@ const createCCNotification = ( sqlRecipientListIds: seed?.sqlRecipientListIds ?? [], parameters: seed?.parameters ?? '{}', parsedParameters: seed?.parsedParameters ?? {}, + requiredParameters: seed?.requiredParameters ?? [], }); export const CCNotificationEditPage = () => { diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts b/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts index c10dd1e6..30d3e0cf 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts @@ -49,6 +49,7 @@ export function parseColdChainNotificationConfig( status: config.status, parameters: config.parameters, parsedParameters: TeraUtils.keyedParamsFromTeraJson(config.parameters), + requiredParameters: [], }; } catch (e) { showError(); diff --git a/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditForm.tsx b/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditForm.tsx index c9837a06..b35d228b 100644 --- a/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditForm.tsx +++ b/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditForm.tsx @@ -1,129 +1,125 @@ import React from 'react'; import { BasicTextInput, + Box, BufferedTextArea, DateTimeInput, Select, Typography, + useQueryParamsState, useTranslation, } from '@notify-frontend/common'; import { ScheduledNotification } from '../../types'; import { SqlQuerySelector } from '../../components'; - -const dummySqlQueries = [ - { - id: '1', - name: 'Last Sync By Province', - query: `SELECT - category3 AS project, category1_level2 AS province, - COUNT(last_sync_date) AS num_of_stores_synced_once -FROM store_categories sc -JOIN store s ON sc.code = s.code -JOIN ( - SELECT site_id, MAX(date) AS last_sync_date FROM site_log GROUP BY site_id -) sl ON s.sync_id_remote_site = sl.site_id -WHERE mode IN ('store', 'dispensary') -AND sc.disabled = false -AND category3 IN ({{project}}) -AND category1_level2 IN ({{province}}) -GROUP BY category3, category1_level2`, - parameters: ['project', 'province'], - }, - { - id: '2', - name: 'First Stock Take', - query: `SELECT - category3 AS project, category1_level2 AS province, - COUNT(first_stock_take_date) AS first_stock_take_date -FROM store_categories sc -JOIN store s ON sc.code = s.code -JOIN ( - SELECT store_id, MAX(stock_take_created_date) AS fist_stock_take_date FROM stock_take GROUP BY store_id -) st ON s.id = st.store_id -WHERE mode IN ('store', 'dispensary') -AND sc.disabled = false -AND category3 IN ({{project}}) -AND category1_level2 IN ({{province}}) -GROUP BY category3, category1_level2 `, - parameters: ['project', 'province'], - }, -]; +import { useNotificationQueries } from 'packages/system/src/Queries/api'; type ScheduledNotificationEditFormProps = { onUpdate: (patch: Partial) => void; draft: ScheduledNotification; }; +const FormRow = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( + + + {title} + + {children} + +); + export const ScheduledNotificationEditForm = ({ onUpdate, draft, }: ScheduledNotificationEditFormProps) => { const t = useTranslation('system'); - return ( - <> - - onUpdate({ - subjectTemplate: e.target - .value as ScheduledNotification['subjectTemplate'], - }) - } - label={t('label.subject-template')} - InputLabelProps={{ shrink: true }} - /> - onUpdate({ bodyTemplate: e.target.value })} - label={t('label.body-template')} - InputProps={{ sx: { backgroundColor: 'background.menu' } }} - InputLabelProps={{ shrink: true }} - /> - onUpdate({ parameters: e.target.value })} - label={t('label.parameters')} - InputProps={{ sx: { backgroundColor: 'background.menu' } }} - InputLabelProps={{ shrink: true }} - /> - - Queries - - + const { queryParams } = useQueryParamsState(); - - Schedule - - Starting From - - onUpdate({ - scheduleStartTime: d as ScheduledNotification['scheduleStartTime'], - }) - } - date={draft.scheduleStartTime} - /> - Repeat - + onUpdate({ + scheduleFrequency: e.target + .value as ScheduledNotification['scheduleFrequency'], + }) + } + options={[ + { label: t('label.daily'), value: 'daily' }, + { label: t('label.weekly'), value: 'weekly' }, + { label: t('label.monthly'), value: 'monthly' }, + ]} + /> + + ); }; diff --git a/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx index b9e43459..52fb975e 100644 --- a/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/Scheduled/ScheduledNotificationEditPage.tsx @@ -3,6 +3,7 @@ import { useTranslation, useNotification, useParams, + useBreadcrumbs, } from '@notify-frontend/common'; import { ScheduledNotificationEditForm } from './ScheduledNotificationEditForm'; import { BaseNotificationEditPage } from '../Base/BaseNotificationEditPage'; @@ -22,6 +23,7 @@ export const ScheduledNotificationEditPage = () => { const t = useTranslation('system'); const { error } = useNotification(); const parsingErrorSnack = error(t('error.parsing-notification-config')); + const { setSuffix } = useBreadcrumbs(); const { id } = useParams<{ id: string }>(); const [draft, setDraft] = useState( @@ -32,6 +34,7 @@ export const ScheduledNotificationEditPage = () => { const { data, isLoading } = useNotificationConfigs({ filterBy: { id: { equalTo: id } }, }); + useEffect(() => { const entity = data?.nodes[0]; // Once we get the notification config from the API, parse it and load into the draft @@ -40,6 +43,7 @@ export const ScheduledNotificationEditPage = () => { parsingErrorSnack ); setDraft(parsedDraft ?? defaultSchedulerNotification); + if (parsedDraft?.title) setSuffix(parsedDraft?.title); }, [data]); const { mutateAsync: update, isLoading: updateIsLoading } = diff --git a/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts b/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts index a87638f7..a74d4a7c 100644 --- a/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts +++ b/frontend/packages/system/src/Notifications/Pages/Scheduled/parseConfig.ts @@ -43,11 +43,12 @@ export const defaultSchedulerNotification: ScheduledNotification = { sqlRecipientListIds: [], parameters: '{}', parsedParameters: {}, + requiredParameters: [], scheduleFrequency: 'daily', scheduleStartTime: new Date(), subjectTemplate: '', bodyTemplate: '', - sqlQueries: [], + notificationQueryIds: [], status: ConfigStatus.Disabled, }; diff --git a/frontend/packages/system/src/Notifications/components/NotificationQuerySelectionModal.tsx b/frontend/packages/system/src/Notifications/components/NotificationQuerySelectionModal.tsx new file mode 100644 index 00000000..35e087d7 --- /dev/null +++ b/frontend/packages/system/src/Notifications/components/NotificationQuerySelectionModal.tsx @@ -0,0 +1,143 @@ +import React, { FC, useMemo, useState } from 'react'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import { useDialog } from '@common/hooks'; +import { useTranslation } from '@common/intl'; + +import { + AutocompleteMultiList, + AutocompleteOptionRenderer, + Checkbox, + DialogButton, + Tooltip, +} from '@common/components'; +import { NotificationQueryRowFragment } from '../../Queries/api'; +import { Grid } from '@common/ui'; + +interface NotificationQuerySelectionModalProps { + sqlQueries: NotificationQueryRowFragment[]; + initialSelectedIds: string[]; + isOpen: boolean; + onClose: () => void; + setSelection: (input: { + notificationQueryIds: string[]; + requiredParameters: string[]; + }) => void; +} + +interface NotificationQueryOption { + id: string; + name: string; + detail: string; +} + +export const NotificationQuerySelectionModal: FC< + NotificationQuerySelectionModalProps +> = ({ sqlQueries, initialSelectedIds, isOpen, onClose, setSelection }) => { + const t = useTranslation('system'); + const [errorMessage, setErrorMessage] = useState(''); + const [selectedIds, setSelectedIds] = useState([]); + + const { Modal } = useDialog({ isOpen, onClose }); + + const options = useMemo(() => { + return sqlQueries.map(sqlQuery => ({ + id: sqlQuery.id, + name: sqlQuery.name, + detail: sqlQuery.query, + })); + }, [sqlQueries]); + + const onChangeSelectedQueries = (ids: string[]) => { + setSelectedIds(ids); + }; + + const onSubmit = async () => { + const requiredParameters = sqlQueries + .filter(sqlQuery => selectedIds.includes(sqlQuery.id)) + .map(sqlQuery => sqlQuery.requiredParameters) + .flat(); + setSelection({ notificationQueryIds: selectedIds, requiredParameters }); + onClose(); + }; + + const modalHeight = Math.min(window.innerHeight - 100, 700); + const modalWidth = Math.min(window.innerWidth - 100, 924); + + return ( + + } + cancelButton={} + title={t('label.select-queries')} + slideAnimation={false} + > + + {errorMessage ? ( + + { + setErrorMessage(''); + }} + > + {t('error')} + {errorMessage} + + + ) : null} + + `${option.detail} ${option.name}`} + filterProperties={['name', 'detail']} + filterPlaceholder={t('placeholder.search')} + width={modalWidth - 50} + height={modalHeight - 300} + defaultSelection={options.filter(o => + initialSelectedIds.includes(o.id) + )} + /> + + + + ); +}; + +const renderOption: AutocompleteOptionRenderer = ( + props, + option, + { selected } +): JSX.Element => ( +
  • + + + + {option.name} + + + + {option.detail} + +
  • +); diff --git a/frontend/packages/system/src/Notifications/components/SqlQuerySelector.tsx b/frontend/packages/system/src/Notifications/components/SqlQuerySelector.tsx index c29869c1..c0683314 100644 --- a/frontend/packages/system/src/Notifications/components/SqlQuerySelector.tsx +++ b/frontend/packages/system/src/Notifications/components/SqlQuerySelector.tsx @@ -1,55 +1,99 @@ import React, { FC } from 'react'; import { - TableProvider, - DataTable, useColumns, - createTableStore, useTranslation, StringUtils, + useEditModal, + EditIcon, + DataTable, + TableProvider, + createTableStore, + LoadingButton, + Box, } from '@notify-frontend/common'; +import { NotificationQuerySelectionModal } from './NotificationQuerySelectionModal'; +import { NotificationQueryRowFragment } from '../../Queries/api'; -interface SqlQuery { - id: string; - name: string; - query: string; - parameters: string[]; -} +type QueryListProps = { + allQueries: NotificationQueryRowFragment[]; + selectedQueryIds: string[]; + setSelection: (input: { + notificationQueryIds: string[]; + requiredParameters: string[]; + }) => void; + isLoading: boolean; +}; -type QueryListProps = { records: SqlQuery[] }; +export const SqlQuerySelector: FC = ({ + allQueries, + selectedQueryIds, + setSelection, + isLoading, +}) => { + const t = useTranslation('system'); -export const SqlQuerySelector: FC = ({ records }) => { - const t = useTranslation(); + const { isOpen, onClose, onOpen } = useEditModal(); - const columns = useColumns([ + const columns = useColumns([ + { + key: 'referenceName', + label: 'label.reference-name', + width: 200, + sortable: false, + }, { key: 'name', label: 'label.name', width: 150, - sortable: true, + sortable: false, }, { key: 'query', label: 'label.query', width: 150, sortable: false, - accessor: ({ rowData }) => StringUtils.ellipsis(rowData?.query, 35), + accessor: ({ rowData }) => StringUtils.ellipsis(rowData?.query, 100), }, { - key: 'parameters', + key: 'requiredParameters', label: 'label.parameters', sortable: false, - accessor: ({ rowData }) => rowData?.parameters.join(', '), + accessor: ({ rowData }) => rowData?.requiredParameters.join(', '), }, ]); + const selectedQueries = (allQueries ?? []).filter(q => + selectedQueryIds.includes(q.id) + ); + return ( - - + - + + + + + } + > + {t('label.select-queries')} + + + ); }; diff --git a/frontend/packages/system/src/Notifications/components/index.ts b/frontend/packages/system/src/Notifications/components/index.ts index 695a7b08..0af9fa19 100644 --- a/frontend/packages/system/src/Notifications/components/index.ts +++ b/frontend/packages/system/src/Notifications/components/index.ts @@ -1 +1,2 @@ export * from './SqlQuerySelector'; +export * from './NotificationQuerySelectionModal'; diff --git a/frontend/packages/system/src/Notifications/types.ts b/frontend/packages/system/src/Notifications/types.ts index 23cba36f..2a32bd17 100644 --- a/frontend/packages/system/src/Notifications/types.ts +++ b/frontend/packages/system/src/Notifications/types.ts @@ -42,6 +42,7 @@ type BaseConfig = Pick< export interface BaseNotificationConfig extends BaseConfig { parsedParameters: KeyedParams; + requiredParameters: string[]; } export interface CCNotification extends BaseNotificationConfig { @@ -64,5 +65,5 @@ export interface ScheduledNotification extends BaseNotificationConfig { scheduleStartTime: Date; subjectTemplate: string; bodyTemplate: string; - sqlQueries: string[]; + notificationQueryIds: string[]; }