@cc-openmrs/cc-esm-active-prescriptions 1.0.70 → 1.0.72

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.
@@ -0,0 +1,237 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ Button,
6
+ Checkbox,
7
+ ComboBox,
8
+ InlineLoading,
9
+ InlineNotification,
10
+ Tab,
11
+ TabList,
12
+ TabPanel,
13
+ TabPanels,
14
+ Tabs,
15
+ TextArea,
16
+ TextInput,
17
+ } from '@carbon/react';
18
+ import { useLocations, type Location } from '@openmrs/esm-framework';
19
+ import { PrescriptionService } from '../prescriptions.service';
20
+ import { usePrescriptionStore } from '../prescription.store';
21
+ import styles from '../root.scss';
22
+
23
+ interface PrescriptionFormProps {
24
+ patientUuid?: string;
25
+ patient?: {
26
+ name?: Array<{ given?: string[]; family?: string }>;
27
+ birthDate?: string;
28
+ gender?: string;
29
+ };
30
+ }
31
+
32
+ const PrescriptionForm: React.FC<PrescriptionFormProps> = ({ patientUuid, patient }) => {
33
+ const { t } = useTranslation();
34
+ const navigate = useNavigate();
35
+ const { state, dispatch } = usePrescriptionStore();
36
+
37
+ const [activeTab, setActiveTab] = useState(0);
38
+ const [isSubmitting, setIsSubmitting] = useState(false);
39
+ const [submitError, setSubmitError] = useState<string | null>(null);
40
+
41
+ const locations = useLocations();
42
+ const locationItems = useMemo(() => locations ?? [], [locations]);
43
+
44
+ const handleSubmit = async () => {
45
+ if (!patientUuid || !state.location || state.orders.length === 0) return;
46
+ setIsSubmitting(true);
47
+ setSubmitError(null);
48
+ try {
49
+ const payload = {
50
+ patientUuid,
51
+ patientName: patient?.name?.[0]
52
+ ? `${patient.name[0].given?.join(' ')} ${patient.name[0].family}`.trim()
53
+ : '',
54
+ patientBirthDate: patient?.birthDate ?? '',
55
+ patientGender: patient?.gender ?? '',
56
+ providerUuid: '',
57
+ providerName: '',
58
+ locationUuid: state.location.uuid,
59
+ locationName: state.location.display,
60
+ policyNumber: state.policyNumber,
61
+ orderUuids: state.orders.map((o) => o.drug.uuid),
62
+ digitallySigned: state.digitallySigned,
63
+ printRequested: state.printRequested,
64
+ observations: state.observations,
65
+ };
66
+ await PrescriptionService.createPrescription(payload as any);
67
+ dispatch({ type: 'RESET' });
68
+ } catch (e: any) {
69
+ setSubmitError(e?.message ?? t('submitError', 'Erro ao salvar a prescrição.'));
70
+ } finally {
71
+ setIsSubmitting(false);
72
+ }
73
+ };
74
+
75
+ return (
76
+ <div className={styles.workspace}>
77
+ <div className={styles.header}>
78
+ <h2>{t('activePrescriptionsWorkspaceTitle', 'Prescrições ativas')}</h2>
79
+ <p className={styles.subtitle}>
80
+ {t(
81
+ 'activePrescriptionsHelper',
82
+ 'Revise o contexto do paciente, escolha os detalhes da visita e adicione os medicamentos à prescrição.',
83
+ )}
84
+ </p>
85
+ </div>
86
+
87
+ <section className={styles.section}>
88
+ <p className={styles.fieldLabel}>{t('visitTimingLabel', 'A visita é')}</p>
89
+ <Tabs selectedIndex={activeTab} onChange={({ selectedIndex }) => setActiveTab(selectedIndex)}>
90
+ <TabList aria-label={t('visitTimingLabel', 'A visita é')}>
91
+ <Tab>{t('visitTimingNew', 'Nova')}</Tab>
92
+ <Tab>{t('visitTimingPast', 'No passado')}</Tab>
93
+ </TabList>
94
+ <TabPanels>
95
+ <TabPanel className={styles.tabContent}>
96
+ <div className={styles.fieldGroup}>
97
+ <ComboBox
98
+ id="visit-location"
99
+ items={locationItems}
100
+ itemToString={(item) => item?.display ?? ''}
101
+ selectedItem={state.location}
102
+ onChange={({ selectedItem }) =>
103
+ dispatch({ type: 'SET_LOCATION', payload: (selectedItem as Location) ?? null })
104
+ }
105
+ placeholder={t('visitLocationPlaceholder', 'Selecione o local')}
106
+ titleText={t('visitLocationHeading', 'Local da visita')}
107
+ helperText={t('visitLocationHelper', 'Selecione onde a visita ocorrerá.')}
108
+ />
109
+ </div>
110
+
111
+ <div className={styles.fieldGroup}>
112
+ <TextInput
113
+ id="insurance-policy"
114
+ labelText={t('insurancePolicyNumberLabel', 'Número da apólice (opcional)')}
115
+ placeholder={t('insurancePolicyNumberPlaceholder', 'Digite o número da apólice')}
116
+ value={state.policyNumber}
117
+ onChange={(e) => dispatch({ type: 'SET_POLICY_NUMBER', payload: e.target.value })}
118
+ />
119
+ </div>
120
+
121
+ <div className={styles.fieldGroup}>
122
+ <Checkbox
123
+ id="sign-prescription-checkbox"
124
+ labelText={t('signPrescriptionLabel', 'Assinar digitalmente a prescrição')}
125
+ checked={state.digitallySigned}
126
+ onChange={(_: any, { checked }: any) =>
127
+ dispatch({ type: 'SET_DIGITALLY_SIGNED', payload: checked })
128
+ }
129
+ />
130
+ <Checkbox
131
+ id="print-prescription-checkbox"
132
+ labelText={t('printPrescriptionLabel', 'Imprimir a prescrição')}
133
+ checked={state.printRequested}
134
+ onChange={(_: any, { checked }: any) =>
135
+ dispatch({ type: 'SET_PRINT_REQUESTED', payload: checked })
136
+ }
137
+ />
138
+ </div>
139
+
140
+ <div className={styles.fieldGroup}>
141
+ <TextArea
142
+ id="observations"
143
+ labelText={t('observations', 'Observações')}
144
+ placeholder={t('observationsPlaceholder', 'Observações da prescrição...')}
145
+ value={state.observations}
146
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
147
+ dispatch({ type: 'SET_OBSERVATIONS', payload: e.target.value })
148
+ }
149
+ rows={3}
150
+ />
151
+ </div>
152
+
153
+ <div className={styles.fieldGroup}>
154
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
155
+ <h3 style={{ margin: 0 }}>{t('medicationsList', 'Medicamentos')}</h3>
156
+ <Button
157
+ size="sm"
158
+ hasIconOnly
159
+ kind="ghost"
160
+ renderIcon={() => (
161
+ <svg width="20" height="20" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
162
+ <path d="M16 6V26" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
163
+ <path d="M6 16H26" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
164
+ </svg>
165
+ )}
166
+ iconDescription={t('addMedication', 'Adicionar medicamento')}
167
+ onClick={() => navigate('/medications/new')}
168
+ />
169
+ </div>
170
+
171
+ {state.orders.length === 0 ? (
172
+ <p>{t('noMedicationsAdded', 'Nenhum medicamento adicionado ainda.')}</p>
173
+ ) : (
174
+ <ul className={styles.medicationsList}>
175
+ {state.orders.map((order, idx) => (
176
+ <li key={idx} className={styles.medicationItem}>
177
+ <span>
178
+ <b>{order.drug.display}</b> — {order.dosage} {order.unit}
179
+ {order.route ? ` · ${order.route}` : ''} · {order.frequency}
180
+ </span>
181
+ <Button size="sm" kind="ghost" onClick={() => navigate(`/medications/${idx}/edit`)}>
182
+ {t('edit', 'Editar')}
183
+ </Button>
184
+ <Button
185
+ size="sm"
186
+ kind="danger--ghost"
187
+ onClick={() => dispatch({ type: 'REMOVE_ORDER', index: idx })}
188
+ >
189
+ {t('remove', 'Remover')}
190
+ </Button>
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ )}
195
+ </div>
196
+
197
+ {submitError && (
198
+ <InlineNotification
199
+ lowContrast
200
+ kind="error"
201
+ title={t('submitError', 'Erro ao salvar a prescrição.')}
202
+ subtitle={submitError}
203
+ onCloseButtonClick={() => setSubmitError(null)}
204
+ />
205
+ )}
206
+
207
+ <div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
208
+ <Button
209
+ kind="primary"
210
+ disabled={!patientUuid || !state.location || state.orders.length === 0 || isSubmitting}
211
+ onClick={handleSubmit}
212
+ >
213
+ {isSubmitting ? (
214
+ <InlineLoading description={t('saving', 'Salvando...')} />
215
+ ) : (
216
+ t('createPrescriptionButtonLabel', 'Salvar Prescrição')
217
+ )}
218
+ </Button>
219
+ </div>
220
+ </TabPanel>
221
+
222
+ <TabPanel className={styles.tabContent}>
223
+ <InlineNotification
224
+ lowContrast
225
+ kind="info"
226
+ title={t('inPastTabPlaceholderTitle', 'Receitas históricas em breve')}
227
+ subtitle={t('inPastTabPlaceholderSubtitle', 'Volte para Nova para gerenciar medicamentos atuais.')}
228
+ />
229
+ </TabPanel>
230
+ </TabPanels>
231
+ </Tabs>
232
+ </section>
233
+ </div>
234
+ );
235
+ };
236
+
237
+ export default PrescriptionForm;
@@ -0,0 +1,81 @@
1
+ import React, { createContext, useContext, useReducer } from 'react';
2
+ import type { Location } from '@openmrs/esm-framework';
3
+ import type { PrescriptionDrugItem } from './prescriptions-actions/add-drug-prescription/add-drug-prescription.component';
4
+
5
+ // ─── State ───────────────────────────────────────────────────────────────────
6
+
7
+ export interface PrescriptionState {
8
+ orders: PrescriptionDrugItem[];
9
+ location: Location | null;
10
+ policyNumber: string;
11
+ digitallySigned: boolean;
12
+ printRequested: boolean;
13
+ observations: string;
14
+ }
15
+
16
+ const initialState: PrescriptionState = {
17
+ orders: [],
18
+ location: null,
19
+ policyNumber: '',
20
+ digitallySigned: false,
21
+ printRequested: false,
22
+ observations: '',
23
+ };
24
+
25
+ // ─── Actions ─────────────────────────────────────────────────────────────────
26
+
27
+ export type PrescriptionAction =
28
+ | { type: 'ADD_ORDER'; payload: PrescriptionDrugItem }
29
+ | { type: 'UPDATE_ORDER'; index: number; payload: PrescriptionDrugItem }
30
+ | { type: 'REMOVE_ORDER'; index: number }
31
+ | { type: 'SET_LOCATION'; payload: Location | null }
32
+ | { type: 'SET_POLICY_NUMBER'; payload: string }
33
+ | { type: 'SET_DIGITALLY_SIGNED'; payload: boolean }
34
+ | { type: 'SET_PRINT_REQUESTED'; payload: boolean }
35
+ | { type: 'SET_OBSERVATIONS'; payload: string }
36
+ | { type: 'RESET' };
37
+
38
+ // ─── Reducer ─────────────────────────────────────────────────────────────────
39
+
40
+ function prescriptionReducer(state: PrescriptionState, action: PrescriptionAction): PrescriptionState {
41
+ switch (action.type) {
42
+ case 'ADD_ORDER':
43
+ return { ...state, orders: [...state.orders, action.payload] };
44
+ case 'UPDATE_ORDER':
45
+ return { ...state, orders: state.orders.map((o, i) => (i === action.index ? action.payload : o)) };
46
+ case 'REMOVE_ORDER':
47
+ return { ...state, orders: state.orders.filter((_, i) => i !== action.index) };
48
+ case 'SET_LOCATION':
49
+ return { ...state, location: action.payload };
50
+ case 'SET_POLICY_NUMBER':
51
+ return { ...state, policyNumber: action.payload };
52
+ case 'SET_DIGITALLY_SIGNED':
53
+ return { ...state, digitallySigned: action.payload };
54
+ case 'SET_PRINT_REQUESTED':
55
+ return { ...state, printRequested: action.payload };
56
+ case 'SET_OBSERVATIONS':
57
+ return { ...state, observations: action.payload };
58
+ case 'RESET':
59
+ return initialState;
60
+ default:
61
+ return state;
62
+ }
63
+ }
64
+
65
+ // ─── Context ─────────────────────────────────────────────────────────────────
66
+
67
+ const PrescriptionContext = createContext<{
68
+ state: PrescriptionState;
69
+ dispatch: React.Dispatch<PrescriptionAction>;
70
+ } | null>(null);
71
+
72
+ export function PrescriptionProvider({ children }: { children: React.ReactNode }) {
73
+ const [state, dispatch] = useReducer(prescriptionReducer, initialState);
74
+ return <PrescriptionContext.Provider value={{ state, dispatch }}>{children}</PrescriptionContext.Provider>;
75
+ }
76
+
77
+ export function usePrescriptionStore() {
78
+ const ctx = useContext(PrescriptionContext);
79
+ if (!ctx) throw new Error('usePrescriptionStore must be used within PrescriptionProvider');
80
+ return ctx;
81
+ }
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { useNavigate, useParams } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button } from '@carbon/react';
5
+ import { usePrescriptionStore } from '../../prescription.store';
6
+ import AddDrugPrescription, {
7
+ type PrescriptionDrugItem,
8
+ } from './add-drug-prescription.component';
9
+ import styles from '../../root.scss';
10
+
11
+ const AddDrugPrescriptionPage: React.FC = () => {
12
+ const { index } = useParams<{ index: string }>();
13
+ const navigate = useNavigate();
14
+ const { t } = useTranslation();
15
+ const { state, dispatch } = usePrescriptionStore();
16
+
17
+ const editIndex = index !== undefined ? parseInt(index, 10) : null;
18
+ const initialData = editIndex !== null ? state.orders[editIndex] : undefined;
19
+
20
+ const handleSave = (item: PrescriptionDrugItem) => {
21
+ if (editIndex !== null) {
22
+ dispatch({ type: 'UPDATE_ORDER', index: editIndex, payload: item });
23
+ } else {
24
+ dispatch({ type: 'ADD_ORDER', payload: item });
25
+ }
26
+ navigate('/');
27
+ };
28
+
29
+ return (
30
+ <div className={styles.workspace}>
31
+ <div className={styles.header}>
32
+ <Button
33
+ size="sm"
34
+ kind="ghost"
35
+ onClick={() => navigate('/')}
36
+ renderIcon={() => (
37
+ <svg width="16" height="16" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
38
+ <path
39
+ d="M20 6L10 16L20 26"
40
+ stroke="currentColor"
41
+ strokeWidth="2"
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ />
45
+ </svg>
46
+ )}
47
+ >
48
+ {t('backToPrescription', 'Voltar à prescrição')}
49
+ </Button>
50
+ <h2 style={{ margin: 0 }}>
51
+ {editIndex !== null
52
+ ? t('editMedication', 'Editar medicamento')
53
+ : t('addMedication', 'Adicionar medicamento')}
54
+ </h2>
55
+ </div>
56
+ <AddDrugPrescription
57
+ initialData={initialData}
58
+ onSave={handleSave}
59
+ onCancel={() => navigate('/')}
60
+ />
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export default AddDrugPrescriptionPage;