@digihmis/esm-administration-app 1.0.0 → 1.0.2

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 (67) hide show
  1. package/dist/145.js +1 -0
  2. package/dist/145.js.map +1 -0
  3. package/dist/153.js +1 -1
  4. package/dist/153.js.map +1 -1
  5. package/dist/402.js +1 -0
  6. package/dist/402.js.map +1 -0
  7. package/dist/594.js +1 -0
  8. package/dist/594.js.map +1 -0
  9. package/dist/598.js +1 -1
  10. package/dist/598.js.map +1 -1
  11. package/dist/625.js +15 -0
  12. package/dist/{916.js.map → 625.js.map} +1 -1
  13. package/dist/{685.js → 631.js} +1 -1
  14. package/dist/631.js.map +1 -0
  15. package/dist/681.js +1 -0
  16. package/dist/681.js.map +1 -0
  17. package/dist/737.js +1 -1
  18. package/dist/737.js.map +1 -1
  19. package/dist/{266.js → 947.js} +1 -1
  20. package/dist/947.js.map +1 -0
  21. package/dist/digihmis-esm-administration-app.js +2 -2
  22. package/dist/digihmis-esm-administration-app.js.buildmanifest.json +140 -140
  23. package/dist/digihmis-esm-administration-app.js.map +1 -1
  24. package/dist/main.js +6 -6
  25. package/dist/main.js.map +1 -1
  26. package/dist/routes.json +1 -1
  27. package/package.json +3 -2
  28. package/release-version.js +4 -4
  29. package/src/billing-management/billing-charge-items/billing-charge-items-modal/billing-charge-items.resource.ts +2 -1
  30. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-items.resource.ts +30 -7
  31. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-service.workspace.tsx +79 -107
  32. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.scss +16 -0
  33. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/price.component.tsx +30 -20
  34. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/schemas/form-schemas.ts +19 -14
  35. package/src/billing-management/billing-charge-items/billing-charge-items.component.tsx +26 -35
  36. package/src/billing-management/billing-charge-items/billing-charge-items.resource.ts +1 -1
  37. package/src/billing-management/billing-charge-items/type/index.ts +10 -10
  38. package/src/billing-management/billing-charge-items/utils/index.ts +32 -74
  39. package/src/billing-management/billing-payment-modes/billing-payment-modes.component.tsx +24 -166
  40. package/src/billing-management/billing-payment-modes/billing-payment-modes.resource.ts +24 -14
  41. package/src/billing-management/billing-payment-points/billing-payment-point-modal/billing-delete-payment-point.modal.tsx +1 -1
  42. package/src/billing-management/billing-payment-points/billing-payment-point.resource.ts +1 -1
  43. package/src/billing-management/billing-payment-points/billing-payment-point.scss +47 -0
  44. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-point.resource.ts +18 -23
  45. package/src/billing-management/billing-payment-points/billing-payment-points-workspace/billing-payment-points.workspace.tsx +1 -1
  46. package/src/config-schema.ts +17 -0
  47. package/src/dashboard.meta.ts +8 -0
  48. package/src/document-branding/document-branding-workspace/document-branding.workspace.scss +34 -0
  49. package/src/document-branding/document-branding-workspace/document-branding.workspace.tsx +160 -0
  50. package/src/document-branding/document-branding.component.tsx +185 -0
  51. package/src/document-branding/document-branding.resource.ts +54 -0
  52. package/src/document-branding/document-branding.scss +69 -0
  53. package/src/index.ts +15 -9
  54. package/src/routes.json +34 -13
  55. package/dist/101.js +0 -1
  56. package/dist/101.js.map +0 -1
  57. package/dist/190.js +0 -1
  58. package/dist/190.js.map +0 -1
  59. package/dist/266.js.map +0 -1
  60. package/dist/606.js +0 -1
  61. package/dist/606.js.map +0 -1
  62. package/dist/685.js.map +0 -1
  63. package/dist/747.js +0 -1
  64. package/dist/747.js.map +0 -1
  65. package/dist/916.js +0 -15
  66. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-item.workspace.tsx +0 -212
  67. package/src/billing-management/billing-charge-items/billing-charge-items-workspace/components/stock-search.component.tsx +0 -107
@@ -2,38 +2,33 @@ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
2
  import { type PaymentPoint } from '../type';
3
3
  import { mutate } from 'swr';
4
4
 
5
+ // The DigiHMIS billing module takes payment-point fields as request params (name, description).
6
+ const toUrl = (payload: Partial<PaymentPoint>, uuid?: string) => {
7
+ const params = new URLSearchParams();
8
+ if (payload.name != null) {
9
+ params.set('name', payload.name);
10
+ }
11
+ if (payload.description != null) {
12
+ params.set('description', payload.description);
13
+ }
14
+ const base = `${restBaseUrl}/digihmisbilling/paymentpoint`;
15
+ return uuid ? `${base}/${uuid}?${params.toString()}` : `${base}?${params.toString()}`;
16
+ };
17
+
5
18
  export const createPaymentPoint = (payload: Partial<PaymentPoint>) => {
6
- const url = `${restBaseUrl}/cashier/cashPoint?v=full`;
7
- return openmrsFetch(url, {
8
- method: 'POST',
9
- body: payload,
10
- headers: {
11
- 'Content-Type': 'application/json',
12
- },
13
- });
19
+ return openmrsFetch(toUrl(payload), { method: 'POST' });
14
20
  };
15
21
 
16
22
  export const updatePaymentPoint = (uuid: string, payload: Partial<PaymentPoint>) => {
17
- const url = `${restBaseUrl}/cashier/cashPoint/${uuid}`;
18
- return openmrsFetch(url, {
19
- method: 'POST',
20
- body: payload,
21
- headers: {
22
- 'Content-Type': 'application/json',
23
- },
24
- });
23
+ return openmrsFetch(toUrl(payload, uuid), { method: 'POST' });
25
24
  };
26
25
 
27
26
  export const deletePaymentPoint = (uuid: string) => {
28
- const url = `${restBaseUrl}/cashier/cashPoint/${uuid}`;
29
- return openmrsFetch(url, {
27
+ return openmrsFetch(`${restBaseUrl}/digihmisbilling/paymentpoint/${uuid}?reason=Retired via billing administration`, {
30
28
  method: 'DELETE',
31
- headers: {
32
- 'Content-Type': 'application/json',
33
- },
34
29
  });
35
30
  };
36
31
 
37
- export const handleMutation = (mutateUrl: string) => {
38
- return mutate(mutateUrl);
32
+ export const handleMutation = (url: string) => {
33
+ return mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
39
34
  };
@@ -91,7 +91,7 @@ const PaymentPointWorkspace: React.FC<PaymentPointWorkspaceProps> = ({
91
91
  });
92
92
 
93
93
  closeWorkspaceWithSavedChanges();
94
- handleMutation(`${restBaseUrl}/cashier/cashPoint?v=full`);
94
+ handleMutation(`${restBaseUrl}/digihmisbilling/paymentpoint`);
95
95
  }
96
96
  } catch (error) {
97
97
  const errorObject = error?.responseBody?.error;
@@ -38,6 +38,20 @@ export const configSchema: ConfigSchema = {
38
38
  _description: 'The alternative text for the logo image, displayed when the image cannot be loaded or on hover.',
39
39
  },
40
40
  },
41
+ documentBrandingIcon: {
42
+ src: {
43
+ _type: Type.String,
44
+ _default: '/openmrs/spa/form_builder.svg',
45
+ _description:
46
+ 'The path or URL to the logo image. If set to an empty string, the default OpenMRS SVG sprite will be used.',
47
+ _validators: [validators.isUrl],
48
+ },
49
+ alt: {
50
+ _type: Type.String,
51
+ _default: 'Logo',
52
+ _description: 'The alternative text for the logo image, displayed when the image cannot be loaded or on hover.',
53
+ },
54
+ },
41
55
  inpatientIcon: {
42
56
  src: {
43
57
  _type: Type.String,
@@ -126,6 +140,9 @@ export type ConfigObject = {
126
140
  billingIcon: {
127
141
  src: string;
128
142
  };
143
+ documentBrandingIcon: {
144
+ src: string;
145
+ };
129
146
  inpatientIcon: {
130
147
  src: string;
131
148
  };
@@ -2,6 +2,7 @@ import {
2
2
  DataCenter,
3
3
  DocumentSketch,
4
4
  HospitalBed,
5
+ Image,
5
6
  MessageQueue,
6
7
  PiggyBank,
7
8
  WebServicesTaskDefinitionVersion,
@@ -45,3 +46,10 @@ export const billingManagementDashboardMeta = {
45
46
  title: 'Billing Management',
46
47
  icon: PiggyBank,
47
48
  };
49
+
50
+ export const documentBrandingDashboardMeta = {
51
+ name: 'document-branding',
52
+ slot: 'administration-dashboard-slot',
53
+ title: 'Document Branding',
54
+ icon: Image,
55
+ };
@@ -0,0 +1,34 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .form {
5
+ display: flex;
6
+ flex-direction: column;
7
+ justify-content: space-between;
8
+ height: 100%;
9
+ }
10
+
11
+ .formContainer {
12
+ margin: layout.$spacing-05;
13
+ }
14
+
15
+ .tablet {
16
+ padding: layout.$spacing-06 layout.$spacing-05;
17
+ background-color: colors.$white;
18
+ }
19
+
20
+ .desktop {
21
+ padding: 0;
22
+ }
23
+
24
+ .logoPreview {
25
+ display: block;
26
+ max-height: 4rem;
27
+ max-width: 12rem;
28
+ margin-bottom: layout.$spacing-03;
29
+ object-fit: contain;
30
+ }
31
+
32
+ .loading {
33
+ margin: layout.$spacing-07;
34
+ }
@@ -0,0 +1,160 @@
1
+ import React, { useEffect } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ButtonSet, Form, FormGroup, FileUploader, InlineLoading, Stack, TextArea, TextInput } from '@carbon/react';
4
+ import { Controller, useForm } from 'react-hook-form';
5
+ import classNames from 'classnames';
6
+ import { mutate } from 'swr';
7
+ import { type DefaultWorkspaceProps, ResponsiveWrapper, showSnackbar, useLayoutType } from '@openmrs/esm-framework';
8
+ import { facilityInfoUrl, type FacilityInfo, saveFacilityInfo, useFacilityInfo } from '../document-branding.resource';
9
+ import styles from './document-branding.workspace.scss';
10
+
11
+ const DocumentBrandingWorkspace: React.FC<DefaultWorkspaceProps> = ({
12
+ closeWorkspace,
13
+ closeWorkspaceWithSavedChanges,
14
+ promptBeforeClosing,
15
+ }) => {
16
+ const { t } = useTranslation();
17
+ const isTablet = useLayoutType() === 'tablet';
18
+ const { info, settingUuid, isLoading } = useFacilityInfo();
19
+
20
+ const { control, handleSubmit, reset, setValue, watch, formState: { isDirty, isSubmitting } } = useForm<FacilityInfo>({
21
+ defaultValues: {},
22
+ });
23
+
24
+ // Prefill once the stored value has loaded.
25
+ useEffect(() => {
26
+ if (!isLoading) {
27
+ reset(info);
28
+ }
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, [isLoading, settingUuid]);
31
+
32
+ useEffect(() => {
33
+ promptBeforeClosing(() => isDirty);
34
+ }, [isDirty, promptBeforeClosing]);
35
+
36
+ const logoData = watch('logoData');
37
+
38
+ const onLogo = (event: React.ChangeEvent<HTMLInputElement>) => {
39
+ const file = event.target.files?.[0];
40
+ if (!file) {
41
+ return;
42
+ }
43
+ const reader = new FileReader();
44
+ reader.onload = () => setValue('logoData', String(reader.result), { shouldDirty: true });
45
+ reader.readAsDataURL(file);
46
+ };
47
+
48
+ const onSubmit = async (data: FacilityInfo) => {
49
+ if (!settingUuid) {
50
+ showSnackbar({ kind: 'error', title: t('settingMissing', 'Print module setting not found — is it installed?') });
51
+ return;
52
+ }
53
+ try {
54
+ await saveFacilityInfo(settingUuid, data);
55
+ await mutate(facilityInfoUrl);
56
+ showSnackbar({ kind: 'success', isLowContrast: true, title: t('brandingSaved', 'Document letterhead saved') });
57
+ closeWorkspaceWithSavedChanges();
58
+ } catch (err) {
59
+ showSnackbar({
60
+ kind: 'error',
61
+ title: t('brandingError', 'Could not save the letterhead'),
62
+ subtitle: err?.responseBody?.error?.message ?? err?.message,
63
+ });
64
+ }
65
+ };
66
+
67
+ if (isLoading) {
68
+ return <InlineLoading className={styles.loading} description={t('loading', 'Loading…')} />;
69
+ }
70
+
71
+ return (
72
+ <Form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
73
+ <div className={styles.formContainer}>
74
+ <Stack gap={5}>
75
+ <Controller
76
+ name="facilityName"
77
+ control={control}
78
+ render={({ field }) => (
79
+ <ResponsiveWrapper>
80
+ <TextInput id="facilityName" labelText={t('facilityName', 'Facility name')} {...field} value={field.value ?? ''} />
81
+ </ResponsiveWrapper>
82
+ )}
83
+ />
84
+ <Controller
85
+ name="facilityTagline"
86
+ control={control}
87
+ render={({ field }) => (
88
+ <ResponsiveWrapper>
89
+ <TextInput id="facilityTagline" labelText={t('tagline', 'Tagline')} {...field} value={field.value ?? ''} />
90
+ </ResponsiveWrapper>
91
+ )}
92
+ />
93
+ <Controller
94
+ name="phone"
95
+ control={control}
96
+ render={({ field }) => (
97
+ <ResponsiveWrapper>
98
+ <TextInput id="phone" labelText={t('phone', 'Phone')} {...field} value={field.value ?? ''} />
99
+ </ResponsiveWrapper>
100
+ )}
101
+ />
102
+ <Controller
103
+ name="email"
104
+ control={control}
105
+ render={({ field }) => (
106
+ <ResponsiveWrapper>
107
+ <TextInput id="email" labelText={t('email', 'Email')} {...field} value={field.value ?? ''} />
108
+ </ResponsiveWrapper>
109
+ )}
110
+ />
111
+ <Controller
112
+ name="website"
113
+ control={control}
114
+ render={({ field }) => (
115
+ <ResponsiveWrapper>
116
+ <TextInput id="website" labelText={t('website', 'Website')} {...field} value={field.value ?? ''} />
117
+ </ResponsiveWrapper>
118
+ )}
119
+ />
120
+ <Controller
121
+ name="address"
122
+ control={control}
123
+ render={({ field }) => (
124
+ <ResponsiveWrapper>
125
+ <TextArea id="address" labelText={t('address', 'Address')} rows={2} {...field} value={field.value ?? ''} />
126
+ </ResponsiveWrapper>
127
+ )}
128
+ />
129
+
130
+ <FormGroup legendText={t('logo', 'Logo')}>
131
+ {logoData && <img src={logoData} alt={t('logoPreview', 'Logo preview')} className={styles.logoPreview} />}
132
+ <FileUploader
133
+ accept={['.png', '.jpg', '.jpeg']}
134
+ buttonLabel={t('uploadLogo', 'Upload logo')}
135
+ labelDescription={t('logoHint', 'PNG or JPG; shown top-left on documents.')}
136
+ filenameStatus="edit"
137
+ onChange={onLogo}
138
+ onDelete={() => setValue('logoData', undefined, { shouldDirty: true })}
139
+ />
140
+ </FormGroup>
141
+ </Stack>
142
+ </div>
143
+
144
+ <ButtonSet className={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
145
+ <Button style={{ maxWidth: '50%' }} kind="secondary" onClick={() => closeWorkspace()}>
146
+ {t('cancel', 'Cancel')}
147
+ </Button>
148
+ <Button style={{ maxWidth: '50%' }} kind="primary" type="submit" disabled={isSubmitting || !isDirty}>
149
+ {isSubmitting ? (
150
+ <InlineLoading description={t('saving', 'Saving…')} />
151
+ ) : (
152
+ t('saveAndClose', 'Save & close')
153
+ )}
154
+ </Button>
155
+ </ButtonSet>
156
+ </Form>
157
+ );
158
+ };
159
+
160
+ export default DocumentBrandingWorkspace;
@@ -0,0 +1,185 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ Button,
4
+ DataTable,
5
+ DataTableSkeleton,
6
+ Layer,
7
+ Pagination,
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableContainer,
12
+ TableHead,
13
+ TableHeader,
14
+ TableRow,
15
+ Tile,
16
+ } from '@carbon/react';
17
+ import { Edit } from '@carbon/react/icons';
18
+ import { launchWorkspace, useLayoutType, useConfig, ErrorState } from '@openmrs/esm-framework';
19
+ import { useTranslation } from 'react-i18next';
20
+ import { CardHeader } from '@openmrs/esm-patient-common-lib';
21
+ import { type ConfigObject } from '../config-schema';
22
+ import { CustomPageHeader } from '../components/custom-page-header/custom-page-header.component';
23
+ import styles from './document-branding.scss';
24
+ import { useFacilityInfo } from './document-branding.resource';
25
+
26
+ /**
27
+ * Admin page for the document letterhead — the facility identity (name, tagline, contacts, address)
28
+ * and logo shown on every generated PDF. It lists the stored value and launches a workspace to edit it,
29
+ * writing to the print module's global property so no one has to hand-edit JSON.
30
+ */
31
+ const DocumentBrandingDashboard = () => {
32
+ const { t } = useTranslation();
33
+ const layout = useLayoutType();
34
+ const isTablet = layout === 'tablet';
35
+ const responsiveSize = isTablet ? 'lg' : 'sm';
36
+ const { documentBrandingIcon } = useConfig<ConfigObject>();
37
+
38
+ const { info, isLoading, error } = useFacilityInfo();
39
+
40
+ const headers = [
41
+ { key: 'field', header: t('field', 'Field') },
42
+ { key: 'value', header: t('value', 'Value') },
43
+ ];
44
+
45
+ const fields = useMemo(
46
+ () => [
47
+ { key: 'facilityName', label: t('facilityName', 'Facility name'), value: info.facilityName },
48
+ { key: 'facilityTagline', label: t('tagline', 'Tagline'), value: info.facilityTagline },
49
+ { key: 'phone', label: t('phone', 'Phone'), value: info.phone },
50
+ { key: 'email', label: t('email', 'Email'), value: info.email },
51
+ { key: 'website', label: t('website', 'Website'), value: info.website },
52
+ { key: 'address', label: t('address', 'Address'), value: info.address },
53
+ ],
54
+ [info, t],
55
+ );
56
+
57
+ const hasLogo = Boolean(info.logoData || info.logoPath);
58
+
59
+ const tableRows = useMemo(() => {
60
+ const rows = fields
61
+ .filter((field) => field.value)
62
+ .map((field) => ({ id: field.key, field: field.label, value: field.value as React.ReactNode }));
63
+ if (hasLogo) {
64
+ rows.push({
65
+ id: 'logo',
66
+ field: t('logo', 'Logo'),
67
+ value: (
68
+ <img
69
+ src={info.logoData ?? info.logoPath}
70
+ alt={t('logoPreview', 'Logo preview')}
71
+ className={styles.logoPreview}
72
+ />
73
+ ),
74
+ });
75
+ }
76
+ return rows;
77
+ }, [fields, hasLogo, info.logoData, info.logoPath, t]);
78
+
79
+ const hasBranding = tableRows.length > 0;
80
+
81
+ const [currentPage, setCurrentPage] = useState(1);
82
+ const [currentPageSize, setCurrentPageSize] = useState(10);
83
+ const pageSizes = [10, 20, 30, 40, 50];
84
+
85
+ const paginatedRows = useMemo(() => {
86
+ const startIndex = (currentPage - 1) * currentPageSize;
87
+ return tableRows.slice(startIndex, startIndex + currentPageSize);
88
+ }, [tableRows, currentPage, currentPageSize]);
89
+
90
+ const handleConfigure = () => {
91
+ launchWorkspace('document-branding-workspace', {
92
+ workspaceTitle: t('configureLetterhead', 'Configure letterhead'),
93
+ });
94
+ };
95
+
96
+ const renderBody = () => {
97
+ if (isLoading) {
98
+ return <DataTableSkeleton role="progressbar" compact={!isTablet} zebra />;
99
+ }
100
+ if (error) {
101
+ return <ErrorState error={error} headerTitle={t('facilityLetterhead', 'Facility letterhead')} />;
102
+ }
103
+ return (
104
+ <Layer>
105
+ <DataTable rows={paginatedRows} headers={headers} size={responsiveSize} useZebraStyles>
106
+ {({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => (
107
+ <TableContainer>
108
+ <Table {...getTableProps()} aria-label={t('facilityLetterheadTable', 'Facility letterhead table')}>
109
+ <TableHead>
110
+ <TableRow>
111
+ {headers.map((header) => (
112
+ <TableHeader key={header.key} {...getHeaderProps({ header })}>
113
+ {header.header}
114
+ </TableHeader>
115
+ ))}
116
+ </TableRow>
117
+ </TableHead>
118
+ <TableBody>
119
+ {rows.map((row) => (
120
+ <TableRow key={row.id} {...getRowProps({ row })}>
121
+ {row.cells.map((cell) => (
122
+ <TableCell key={cell.id}>{cell.value}</TableCell>
123
+ ))}
124
+ </TableRow>
125
+ ))}
126
+ </TableBody>
127
+ </Table>
128
+ {!hasBranding ? (
129
+ <div className={styles.tileContainer}>
130
+ <Tile className={styles.tile}>
131
+ <div className={styles.tileContent}>
132
+ <p className={styles.content}>{t('noLetterhead', 'No Facility letterhead configured yet')}</p>
133
+ <p className={styles.emptyStateHelperText}>
134
+ {t(
135
+ 'noLetterheadHelp',
136
+ 'Add the facility name, contacts and logo to brand generated documents.',
137
+ )}
138
+ </p>
139
+ </div>
140
+ </Tile>
141
+ </div>
142
+ ) : null}
143
+ {hasBranding && (
144
+ <Pagination
145
+ forwardText={t('nextPage', 'Next page')}
146
+ backwardText={t('previousPage', 'Previous page')}
147
+ page={currentPage}
148
+ pageSize={currentPageSize}
149
+ pageSizes={pageSizes}
150
+ totalItems={tableRows.length}
151
+ className={styles.pagination}
152
+ onChange={({ pageSize, page }) => {
153
+ if (pageSize !== currentPageSize) {
154
+ setCurrentPageSize(pageSize);
155
+ setCurrentPage(1);
156
+ }
157
+ if (page !== currentPage) {
158
+ setCurrentPage(page);
159
+ }
160
+ }}
161
+ />
162
+ )}
163
+ </TableContainer>
164
+ )}
165
+ </DataTable>
166
+ </Layer>
167
+ );
168
+ };
169
+
170
+ return (
171
+ <>
172
+ <CustomPageHeader subTitle={t('facilityBranding', 'Facility branding')} iconSrc={documentBrandingIcon?.src} />
173
+ <div className={styles.section}>
174
+ <CardHeader title={t('facilityLetterhead', 'Facility letterhead')}>
175
+ <Button kind="ghost" renderIcon={(props) => <Edit size={16} {...props} />} onClick={handleConfigure}>
176
+ {hasBranding ? t('editLetterhead', 'Edit letterhead') : t('configureLetterhead', 'Configure letterhead')}
177
+ </Button>
178
+ </CardHeader>
179
+ {renderBody()}
180
+ </div>
181
+ </>
182
+ );
183
+ };
184
+
185
+ export default DocumentBrandingDashboard;
@@ -0,0 +1,54 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+
4
+ /** The global property the print module reads for the document letterhead. */
5
+ export const facilityInfoGp = 'digihmisprint.facilityInformation';
6
+
7
+ /** SWR key for the stored letterhead — exported so the workspace can revalidate the dashboard after saving. */
8
+ export const facilityInfoUrl = `${restBaseUrl}/systemsetting/${facilityInfoGp}?v=custom:(uuid,value)`;
9
+
10
+ export interface FacilityInfo {
11
+ facilityName?: string;
12
+ facilityTagline?: string;
13
+ phone?: string;
14
+ email?: string;
15
+ website?: string;
16
+ address?: string;
17
+ /** Logo as a base64 / data-URI string (from the upload). */
18
+ logoData?: string;
19
+ /** Or a server file path, as an alternative to an uploaded logo. */
20
+ logoPath?: string;
21
+ }
22
+
23
+ /** The stored letterhead (parsed from the GP JSON) plus the setting uuid needed to save it. */
24
+ export const useFacilityInfo = () => {
25
+ const { data, error, isLoading, mutate } = useSWR<{ data: { uuid: string; value?: string } }>(
26
+ facilityInfoUrl,
27
+ openmrsFetch,
28
+ );
29
+ let info: FacilityInfo = {};
30
+ const raw = data?.data?.value;
31
+ if (raw) {
32
+ try {
33
+ info = JSON.parse(raw);
34
+ } catch {
35
+ info = {};
36
+ }
37
+ }
38
+ return { info, settingUuid: data?.data?.uuid, isLoading, error, mutate };
39
+ };
40
+
41
+ /** Save the letterhead back to the global property as JSON (dropping blank fields). */
42
+ export const saveFacilityInfo = (settingUuid: string, info: FacilityInfo) => {
43
+ const cleaned: Record<string, string> = {};
44
+ Object.entries(info).forEach(([key, value]) => {
45
+ if (value != null && String(value).trim() !== '') {
46
+ cleaned[key] = String(value).trim();
47
+ }
48
+ });
49
+ return openmrsFetch(`${restBaseUrl}/systemsetting/${settingUuid}`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: { value: JSON.stringify(cleaned) },
53
+ });
54
+ };
@@ -0,0 +1,69 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .dashboardContainer {
6
+ display: flex;
7
+ flex-direction: column;
8
+ }
9
+
10
+ .section {
11
+ margin-bottom: layout.$spacing-07;
12
+ padding: layout.$spacing-05;
13
+ background-color: colors.$gray-10;
14
+
15
+
16
+ &:last-child {
17
+ margin-bottom: 0;
18
+ }
19
+ }
20
+
21
+ .subhead {
22
+ @include type.type-style('body-01');
23
+ color: colors.$gray-70;
24
+ margin-bottom: layout.$spacing-05;
25
+ }
26
+
27
+ .logoPreview {
28
+ display: block;
29
+ max-height: 4rem;
30
+ max-width: 12rem;
31
+ margin-bottom: layout.$spacing-05;
32
+ object-fit: contain;
33
+ }
34
+
35
+ .tileContainer {
36
+ border-top: 1px solid colors.$gray-20;
37
+ padding: 3rem 0;
38
+ }
39
+
40
+ .tile {
41
+ margin: auto;
42
+ width: fit-content;
43
+ background: colors.$gray-20;
44
+ }
45
+
46
+ .tileContent {
47
+ display: flex;
48
+ flex-direction: column;
49
+ align-items: center;
50
+ }
51
+
52
+ .content {
53
+ @include type.type-style('heading-compact-02');
54
+ color: colors.$gray-70;
55
+ margin-bottom: layout.$spacing-03;
56
+ }
57
+
58
+ .emptyStateHelperText {
59
+ @include type.type-style('body-compact-01');
60
+ color: colors.$gray-70;
61
+ }
62
+
63
+ .pagination {
64
+ overflow: hidden;
65
+
66
+ &:global(.cds--pagination) {
67
+ border-top: none;
68
+ }
69
+ }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ import { createDashboardLink } from './left-panel/left-panel.component';
4
4
  import {
5
5
  bedManagementDashboardMeta,
6
6
  billingManagementDashboardMeta,
7
+ documentBrandingDashboardMeta,
7
8
  formBuilderDashboardMeta,
8
9
  locationManagementDashboardMeta,
9
10
  queueManagementDashboardMeta,
@@ -15,6 +16,7 @@ import BedManagementDashboard from './bed-management/bed-management.component';
15
16
  import FormBuilderDashboard from './form-builder/form-builder.component';
16
17
  import BillingManagementDashboard from './billing-management/billing-management.component';
17
18
  import SystemInfoDashboard from './system-info/system-info.component';
19
+ import DocumentBrandingDashboard from './document-branding/document-branding.component';
18
20
  import DeletePaymentModeModal from './billing-management/billing-payment-modes/billing-payment-modes-modal/billing-delete-payment-modes.modal';
19
21
  import DeletePaymentPointModal from './billing-management/billing-payment-points/billing-payment-point-modal/billing-delete-payment-point.modal';
20
22
  import DeleteBillableServiceModal from './billing-management/billing-charge-items/billing-charge-items-modal/billing-charge-items.modal';
@@ -35,6 +37,10 @@ export const bedLeftPanelLink = getSyncLifecycle(createDashboardLink(bedManageme
35
37
  export const formBuilderLeftPanelLink = getSyncLifecycle(createDashboardLink(formBuilderDashboardMeta), options);
36
38
  export const billingLeftPanelLink = getSyncLifecycle(createDashboardLink(billingManagementDashboardMeta), options);
37
39
  export const systemInfoLeftPanelLink = getSyncLifecycle(createDashboardLink(systemInfoDashboardMeta), options);
40
+ export const documentBrandingLeftPanelLink = getSyncLifecycle(
41
+ createDashboardLink(documentBrandingDashboardMeta),
42
+ options,
43
+ );
38
44
 
39
45
  export const queueManagementDashboard = getSyncLifecycle(QueueManagementDashboard, options);
40
46
  export const locationManagementDashboard = getSyncLifecycle(LocationManagementDashboard, options);
@@ -42,6 +48,7 @@ export const bedManagementDashboard = getSyncLifecycle(BedManagementDashboard, o
42
48
  export const formBuilderDashboard = getSyncLifecycle(FormBuilderDashboard, options);
43
49
  export const billingManagementDashboard = getSyncLifecycle(BillingManagementDashboard, options);
44
50
  export const systemInfoDashboard = getSyncLifecycle(SystemInfoDashboard, options);
51
+ export const documentBrandingDashboard = getSyncLifecycle(DocumentBrandingDashboard, options);
45
52
 
46
53
  // t('addNewQueueService', 'Add New Queue Service')
47
54
  export const addNewQueueServiceWorkspace = getAsyncLifecycle(
@@ -103,22 +110,21 @@ export const paymentPointWorkspace = getAsyncLifecycle(
103
110
  },
104
111
  );
105
112
 
106
- // t('addCommodityItem', 'Add Commodity Item')
107
- export const commodityForm = getAsyncLifecycle(
113
+ // t('addServiceItem', 'Add Charge Service')
114
+ export const billableServiceForm = getAsyncLifecycle(
108
115
  () =>
109
- import('./billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-item.workspace'),
116
+ import('./billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-service.workspace'),
110
117
  {
111
- featureName: 'billing-charge-item-workspace',
118
+ featureName: 'billing-charge-service-workspace',
112
119
  moduleName,
113
120
  },
114
121
  );
115
122
 
116
- // t('addServiceItem', 'Add Charge Service')
117
- export const billableServiceForm = getAsyncLifecycle(
118
- () =>
119
- import('./billing-management/billing-charge-items/billing-charge-items-workspace/billing-charge-service.workspace'),
123
+ // t('configureLetterhead', 'Configure letterhead')
124
+ export const documentBrandingWorkspace = getAsyncLifecycle(
125
+ () => import('./document-branding/document-branding-workspace/document-branding.workspace'),
120
126
  {
121
- featureName: 'billing-charge-service-workspace',
127
+ featureName: 'document-branding-workspace',
122
128
  moduleName,
123
129
  },
124
130
  );