@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.
- package/dist/499.js +1 -1
- package/dist/695.js +2 -0
- package/dist/main.js +1 -1
- package/dist/openmrs-esm-patient-lists-app.js +1 -1
- package/dist/openmrs-esm-patient-lists-app.js.buildmanifest.json +50 -29
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/prescription-form/prescription-form.component.tsx +237 -0
- package/src/prescription.store.tsx +81 -0
- package/src/prescriptions-actions/add-drug-prescription/add-drug-prescription.page.tsx +65 -0
- package/src/root.component.tsx +20 -277
- package/src/root.scss +26 -0
- package/dist/818.js +0 -2
- /package/dist/{818.js.LICENSE.txt → 695.js.LICENSE.txt} +0 -0
|
@@ -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;
|