@palladium-ethiopia/esm-appointments-admin-app 5.4.2-pre.243

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.turbo/turbo-build.log +7 -0
  2. package/dist/117.js +1 -0
  3. package/dist/145.js +1 -0
  4. package/dist/145.js.map +1 -0
  5. package/dist/15.js +1 -0
  6. package/dist/15.js.map +1 -0
  7. package/dist/152.js +1 -0
  8. package/dist/152.js.map +1 -0
  9. package/dist/184.js +1 -0
  10. package/dist/184.js.map +1 -0
  11. package/dist/204.js +43 -0
  12. package/dist/204.js.map +1 -0
  13. package/dist/209.js +1 -0
  14. package/dist/209.js.map +1 -0
  15. package/dist/357.js +1 -0
  16. package/dist/357.js.map +1 -0
  17. package/dist/442.js +1 -0
  18. package/dist/442.js.map +1 -0
  19. package/dist/466.js +1 -0
  20. package/dist/466.js.map +1 -0
  21. package/dist/61.js +1 -0
  22. package/dist/61.js.map +1 -0
  23. package/dist/611.js +1 -0
  24. package/dist/611.js.map +1 -0
  25. package/dist/689.js +1 -0
  26. package/dist/689.js.map +1 -0
  27. package/dist/697.js +1 -0
  28. package/dist/697.js.map +1 -0
  29. package/dist/711.js +1 -0
  30. package/dist/711.js.map +1 -0
  31. package/dist/712.js +1 -0
  32. package/dist/712.js.map +1 -0
  33. package/dist/771.js +1 -0
  34. package/dist/771.js.map +1 -0
  35. package/dist/789.js +1 -0
  36. package/dist/789.js.map +1 -0
  37. package/dist/806.js +1 -0
  38. package/dist/868.js +1 -0
  39. package/dist/868.js.map +1 -0
  40. package/dist/878.js +1 -0
  41. package/dist/878.js.map +1 -0
  42. package/dist/88.js +15 -0
  43. package/dist/88.js.map +1 -0
  44. package/dist/ethiopia-esm-appointments-admin-app.js +6 -0
  45. package/dist/ethiopia-esm-appointments-admin-app.js.buildmanifest.json +701 -0
  46. package/dist/ethiopia-esm-appointments-admin-app.js.map +1 -0
  47. package/dist/main.js +6 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/routes.json +1 -0
  50. package/jest.config.js +6 -0
  51. package/package.json +52 -0
  52. package/rspack.config.js +1 -0
  53. package/src/api/appointment-service.resource.ts +46 -0
  54. package/src/components/nav-tile-link.component.tsx +26 -0
  55. package/src/components/nav-tile-link.scss +29 -0
  56. package/src/config-schema.ts +20 -0
  57. package/src/constants.ts +21 -0
  58. package/src/declarations.d.ts +2 -0
  59. package/src/extensions/appointment-service-admin-nav-link.extension.tsx +29 -0
  60. package/src/home/appointment-services-table.component.tsx +126 -0
  61. package/src/home/appointment-services-table.scss +15 -0
  62. package/src/home/dashboard.component.tsx +17 -0
  63. package/src/home/home.component.tsx +25 -0
  64. package/src/home/home.scss +14 -0
  65. package/src/index.ts +33 -0
  66. package/src/routes.json +35 -0
  67. package/src/types/index.ts +63 -0
  68. package/src/workspace/appointment-service-admin.workspace.scss +81 -0
  69. package/src/workspace/appointment-service-admin.workspace.tsx +335 -0
  70. package/src/workspace/appointment-service-form.helper.ts +112 -0
  71. package/src/workspace/copy-day-blocks-modal.component.tsx +136 -0
  72. package/src/workspace/copy-day-blocks-modal.scss +13 -0
  73. package/src/workspace/copy-day-blocks.helper.ts +48 -0
  74. package/translations/am.json +55 -0
  75. package/translations/en.json +55 -0
  76. package/tsconfig.json +5 -0
@@ -0,0 +1,21 @@
1
+ export const moduleName = '@palladium-ethiopia/esm-appointments-admin-app';
2
+
3
+ export const appointmentServiceAdminBasePath = `${window.spaBase}/appointment-service-admin`;
4
+
5
+ /** Workspace registration names. */
6
+ export const APPOINTMENT_SERVICE_ADMIN_WORKSPACE = 'appointment-service-admin-workspace';
7
+
8
+ /** WorkspaceContainer context key for the admin page. */
9
+ export const APPOINTMENT_SERVICE_ADMIN_CONTEXT_KEY = 'appointment-service-admin';
10
+
11
+ /** REST endpoints for Bahmni appointment service definitions. */
12
+ export const appointmentServiceListUrl = '/ws/rest/v1/appointmentService/all/full';
13
+
14
+ /** Single service by uuid: GET ?uuid={uuid}; POST to create/update. */
15
+ export const appointmentServiceUrl = '/ws/rest/v1/appointmentService';
16
+
17
+ export const DAYS_OF_WEEK = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'] as const;
18
+
19
+ export type DayOfWeek = (typeof DAYS_OF_WEEK)[number];
20
+
21
+ export const WEEKDAYS: Array<DayOfWeek> = ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY'];
@@ -0,0 +1,2 @@
1
+ declare module '*.css';
2
+ declare module '*.scss';
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar } from '@carbon/react/icons';
4
+ import { UserHasAccess, useConfig } from '@openmrs/esm-framework';
5
+ import NavTileLink from '../components/nav-tile-link.component';
6
+ import { appointmentServiceAdminBasePath } from '../constants';
7
+ import type { ConfigObject } from '../config-schema';
8
+
9
+ interface AppointmentServiceAdminNavLinkProps {
10
+ hideOverlay: (hide: boolean) => void;
11
+ }
12
+
13
+ const AppointmentServiceAdminNavLink: React.FC<AppointmentServiceAdminNavLinkProps> = ({ hideOverlay }) => {
14
+ const { t } = useTranslation();
15
+ const { appointmentServiceAdminPrivilege } = useConfig<ConfigObject>();
16
+
17
+ return (
18
+ <UserHasAccess privilege={appointmentServiceAdminPrivilege}>
19
+ <NavTileLink
20
+ hideOverlay={hideOverlay}
21
+ icon={<Calendar size={24} />}
22
+ label={t('appointmentServiceAdmin', 'Appointment service admin')}
23
+ to={appointmentServiceAdminBasePath}
24
+ />
25
+ </UserHasAccess>
26
+ );
27
+ };
28
+
29
+ export default AppointmentServiceAdminNavLink;
@@ -0,0 +1,126 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ Button,
5
+ DataTable,
6
+ DataTableSkeleton,
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableContainer,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from '@carbon/react';
15
+ import { launchWorkspace, useLayoutType } from '@openmrs/esm-framework';
16
+ import { useAppointmentServices } from '../api/appointment-service.resource';
17
+ import { APPOINTMENT_SERVICE_ADMIN_WORKSPACE } from '../constants';
18
+ import type { AppointmentService } from '../types';
19
+ import styles from './appointment-services-table.scss';
20
+
21
+ function getWeeklyBlockCount(service: AppointmentService): number {
22
+ return service.weeklyAvailability?.length ?? 0;
23
+ }
24
+
25
+ const AppointmentServicesTable: React.FC = () => {
26
+ const { t } = useTranslation();
27
+ const layout = useLayoutType();
28
+ const { appointmentServices, isLoading, error } = useAppointmentServices();
29
+ const buttonSize = layout === 'tablet' ? 'md' : 'sm';
30
+
31
+ const headers = useMemo(
32
+ () => [
33
+ { key: 'name', header: t('serviceName', 'Service name') },
34
+ { key: 'maxAppointmentsLimit', header: t('maxAppointmentsLimit', 'Max appointments') },
35
+ { key: 'weeklyBlocks', header: t('weeklyBlocks', 'Weekly blocks') },
36
+ { key: 'actions', header: t('actions', 'Actions') },
37
+ ],
38
+ [t],
39
+ );
40
+
41
+ const rows = useMemo(
42
+ () =>
43
+ appointmentServices.map((service) => {
44
+ const weeklyBlockCount = getWeeklyBlockCount(service);
45
+ return {
46
+ id: service.uuid,
47
+ name: service.name,
48
+ maxAppointmentsLimit:
49
+ service.maxAppointmentsLimit != null ? String(service.maxAppointmentsLimit) : t('notSet', 'Not set'),
50
+ weeklyBlocks:
51
+ weeklyBlockCount > 0
52
+ ? t('weeklyBlocksCount', '{{count}} block(s)', { count: weeklyBlockCount })
53
+ : t('notConfigured', 'Not configured'),
54
+ actions: service.uuid,
55
+ };
56
+ }),
57
+ [appointmentServices, t],
58
+ );
59
+
60
+ const handleConfigure = (serviceUuid: string) => {
61
+ const service = appointmentServices.find((item) => item.uuid === serviceUuid);
62
+ if (!service) {
63
+ return;
64
+ }
65
+
66
+ launchWorkspace(APPOINTMENT_SERVICE_ADMIN_WORKSPACE, {
67
+ workspaceTitle: t('configureServiceTitle', 'Configure {{serviceName}}', { serviceName: service.name }),
68
+ appointmentService: service,
69
+ });
70
+ };
71
+
72
+ if (isLoading) {
73
+ return <DataTableSkeleton columnCount={headers.length} rowCount={5} />;
74
+ }
75
+
76
+ if (error) {
77
+ return <p>{t('errorLoadingServices', 'Error loading appointment services')}</p>;
78
+ }
79
+
80
+ if (appointmentServices.length === 0) {
81
+ return <p>{t('noAppointmentServices', 'No appointment services found')}</p>;
82
+ }
83
+
84
+ return (
85
+ <DataTable headers={headers} rows={rows} size={buttonSize}>
86
+ {({ rows: tableRows, headers: tableHeaders, getTableProps, getHeaderProps, getRowProps }) => (
87
+ <TableContainer title={t('appointmentServices', 'Appointment services')} className={styles.tableContainer}>
88
+ <Table {...getTableProps()}>
89
+ <TableHead>
90
+ <TableRow>
91
+ {tableHeaders.map((header) => (
92
+ <TableHeader {...getHeaderProps({ header })} key={header.key}>
93
+ {header.header}
94
+ </TableHeader>
95
+ ))}
96
+ </TableRow>
97
+ </TableHead>
98
+ <TableBody>
99
+ {tableRows.map((row) => (
100
+ <TableRow {...getRowProps({ row })} key={row.id}>
101
+ {row.cells.map((cell) =>
102
+ cell.info.header === 'actions' ? (
103
+ <TableCell key={cell.id}>
104
+ <Button
105
+ className={styles.configureButton}
106
+ kind="ghost"
107
+ size={buttonSize}
108
+ onClick={() => handleConfigure(cell.value)}>
109
+ {t('configure', 'Configure')}
110
+ </Button>
111
+ </TableCell>
112
+ ) : (
113
+ <TableCell key={cell.id}>{cell.value}</TableCell>
114
+ ),
115
+ )}
116
+ </TableRow>
117
+ ))}
118
+ </TableBody>
119
+ </Table>
120
+ </TableContainer>
121
+ )}
122
+ </DataTable>
123
+ );
124
+ };
125
+
126
+ export default AppointmentServicesTable;
@@ -0,0 +1,15 @@
1
+ @use '@carbon/layout';
2
+
3
+ .toolbar {
4
+ display: flex;
5
+ justify-content: flex-end;
6
+ margin-bottom: layout.$spacing-05;
7
+ }
8
+
9
+ .tableContainer {
10
+ block-size: 100%;
11
+ }
12
+
13
+ .configureButton {
14
+ max-inline-size: fit-content;
15
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import AppointmentServicesTable from './appointment-services-table.component';
4
+ import styles from './home.scss';
5
+
6
+ const AppointmentServiceAdminDashboard: React.FC = () => {
7
+ const { t } = useTranslation();
8
+
9
+ return (
10
+ <div className={styles.dashboard}>
11
+ <h4>{t('appointmentServiceAdmin', 'Appointment service admin')}</h4>
12
+ <AppointmentServicesTable />
13
+ </div>
14
+ );
15
+ };
16
+
17
+ export default AppointmentServiceAdminDashboard;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { WorkspaceContainer } from '@openmrs/esm-framework';
3
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
4
+ import { APPOINTMENT_SERVICE_ADMIN_CONTEXT_KEY, appointmentServiceAdminBasePath } from '../constants';
5
+ import AppointmentServiceAdminDashboard from './dashboard.component';
6
+ import styles from './home.scss';
7
+
8
+ const AppointmentServiceAdminHome: React.FC = () => {
9
+ return (
10
+ <BrowserRouter basename={appointmentServiceAdminBasePath}>
11
+ <main className={styles.container}>
12
+ <Routes>
13
+ <Route path="/" element={<AppointmentServiceAdminDashboard />} />
14
+ <Route path="/*" element={<AppointmentServiceAdminDashboard />} />
15
+ </Routes>
16
+ </main>
17
+ <WorkspaceContainer
18
+ key={APPOINTMENT_SERVICE_ADMIN_CONTEXT_KEY}
19
+ contextKey={APPOINTMENT_SERVICE_ADMIN_CONTEXT_KEY}
20
+ />
21
+ </BrowserRouter>
22
+ );
23
+ };
24
+
25
+ export default AppointmentServiceAdminHome;
@@ -0,0 +1,14 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .container {
5
+ background-color: colors.$white-0;
6
+ }
7
+
8
+ :global(.omrs-breakpoint-gt-tablet) .container {
9
+ margin-left: var(--omrs-sidenav-width);
10
+ }
11
+
12
+ .dashboard {
13
+ padding: layout.$spacing-05;
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle } from '@openmrs/esm-framework';
2
+ import { configSchema } from './config-schema';
3
+ import { moduleName } from './constants';
4
+
5
+ // Extensions
6
+ import AppointmentServiceAdminNavLink from './extensions/appointment-service-admin-nav-link.extension';
7
+
8
+ // Workspaces
9
+ import AppointmentServiceAdminWorkspace from './workspace/appointment-service-admin.workspace';
10
+
11
+ const options = {
12
+ featureName: 'appointment-service-admin',
13
+ moduleName,
14
+ };
15
+
16
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
17
+
18
+ // Register synchronously so frontend config keys are recognized at bootstrap.
19
+ defineConfigSchema(moduleName, configSchema);
20
+
21
+ export function startupApp() {}
22
+
23
+ export const appointmentServiceAdminHome = getAsyncLifecycle(() => import('./home/home.component'), options);
24
+
25
+ export const appointmentServiceAdminNavLink = getSyncLifecycle(AppointmentServiceAdminNavLink, {
26
+ featureName: 'appointment-service-admin-nav-link',
27
+ moduleName,
28
+ });
29
+
30
+ export const appointmentServiceAdminWorkspace = getSyncLifecycle(AppointmentServiceAdminWorkspace, {
31
+ featureName: 'appointment-service-admin-workspace',
32
+ moduleName,
33
+ });
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "backendDependencies": {
4
+ "webservices.rest": ">=2.2.0",
5
+ "appointments": ">=2.0.0"
6
+ },
7
+ "pages": [
8
+ {
9
+ "component": "appointmentServiceAdminHome",
10
+ "route": "appointment-service-admin",
11
+ "online": true,
12
+ "offline": false
13
+ }
14
+ ],
15
+ "extensions": [
16
+ {
17
+ "name": "appointment-service-admin-nav-link",
18
+ "component": "appointmentServiceAdminNavLink",
19
+ "slot": "navbar-items-slot",
20
+ "online": true,
21
+ "offline": false
22
+ }
23
+ ],
24
+ "workspaces": [
25
+ {
26
+ "name": "appointment-service-admin-workspace",
27
+ "component": "appointmentServiceAdminWorkspace",
28
+ "title": "Configure appointment service",
29
+ "type": "other-form",
30
+ "width": "extra-wide",
31
+ "canHide": true,
32
+ "canMaximize": true
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,63 @@
1
+ export interface WeeklyAvailability {
2
+ dayOfWeek: string;
3
+ startTime: string;
4
+ endTime: string;
5
+ maxAppointmentsLimit?: number | null;
6
+ uuid?: string;
7
+ voided?: boolean;
8
+ }
9
+
10
+ export interface AppointmentService {
11
+ uuid: string;
12
+ name: string;
13
+ description?: string;
14
+ startTime?: string;
15
+ endTime?: string;
16
+ maxAppointmentsLimit?: number | null;
17
+ durationMins?: number | null;
18
+ weeklyAvailability?: Array<WeeklyAvailability>;
19
+ color?: string;
20
+ initialAppointmentStatus?: string;
21
+ location?: { uuid: string; display?: string; name?: string };
22
+ speciality?: { uuid: string; display?: string; name?: string };
23
+ serviceTypes?: Array<{ uuid: string; name: string; duration?: number }>;
24
+ }
25
+
26
+ export interface WeeklyAvailabilityPayload {
27
+ uuid?: string;
28
+ dayOfWeek: string;
29
+ startTime?: string;
30
+ endTime?: string;
31
+ maxAppointmentsLimit?: number | null;
32
+ voided?: boolean;
33
+ }
34
+
35
+ export interface AppointmentServiceSavePayload {
36
+ uuid: string;
37
+ name: string;
38
+ description?: string;
39
+ specialityUuid?: string;
40
+ locationUuid?: string;
41
+ startTime?: string;
42
+ endTime?: string;
43
+ maxAppointmentsLimit?: number | null;
44
+ durationMins?: number | null;
45
+ color?: string;
46
+ initialAppointmentStatus?: string;
47
+ weeklyAvailability: Array<WeeklyAvailabilityPayload>;
48
+ }
49
+
50
+ export interface AvailabilityBlockFormValue {
51
+ clientId: string;
52
+ uuid?: string;
53
+ dayOfWeek: string;
54
+ startTime: string;
55
+ endTime: string;
56
+ maxAppointmentsLimit?: number | null;
57
+ voided?: boolean;
58
+ }
59
+
60
+ export interface AppointmentServiceFormValues {
61
+ maxAppointmentsLimit: number | null;
62
+ blocks: Array<AvailabilityBlockFormValue>;
63
+ }
@@ -0,0 +1,81 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+
4
+ .form {
5
+ display: flex;
6
+ flex-direction: column;
7
+ block-size: 100%;
8
+ }
9
+
10
+ .formContainer {
11
+ flex: 1;
12
+ overflow-y: auto;
13
+ padding: layout.$spacing-05;
14
+ }
15
+
16
+ .serviceName {
17
+ @include type.type-style('heading-03');
18
+ margin-block-end: layout.$spacing-05;
19
+ }
20
+
21
+ .sectionTitle {
22
+ @include type.type-style('heading-compact-01');
23
+ margin-block: layout.$spacing-05 layout.$spacing-03;
24
+ }
25
+
26
+ .daySection {
27
+ border-block-end: 1px solid var(--cds-border-subtle);
28
+ margin-block-end: layout.$spacing-05;
29
+ padding-block-end: layout.$spacing-05;
30
+ }
31
+
32
+ .dayHeader {
33
+ align-items: center;
34
+ display: flex;
35
+ justify-content: space-between;
36
+ margin-block-end: layout.$spacing-03;
37
+ }
38
+
39
+ .dayActions {
40
+ display: flex;
41
+ flex-wrap: wrap;
42
+ gap: layout.$spacing-02;
43
+ }
44
+
45
+ .dayTitle {
46
+ @include type.type-style('heading-compact-01');
47
+ }
48
+
49
+ .dayTotal {
50
+ @include type.type-style('helper-text-01');
51
+ color: var(--cds-text-secondary);
52
+ margin-block-start: layout.$spacing-03;
53
+ }
54
+
55
+ .blockRow {
56
+ align-items: flex-end;
57
+ display: grid;
58
+ gap: layout.$spacing-03;
59
+ grid-template-columns: 1fr 1fr 1fr auto;
60
+ margin-block-end: layout.$spacing-03;
61
+ }
62
+
63
+ .emptyDay {
64
+ @include type.type-style('body-compact-01');
65
+ color: var(--cds-text-secondary);
66
+ margin-block-end: layout.$spacing-03;
67
+ }
68
+
69
+ .buttonSet {
70
+ padding: layout.$spacing-05;
71
+ }
72
+
73
+ .buttonSetTablet {
74
+ padding-block-end: layout.$spacing-09;
75
+ }
76
+
77
+ .inlineLoading {
78
+ align-items: center;
79
+ display: flex;
80
+ gap: layout.$spacing-03;
81
+ }