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