@blackcode_sa/metaestetics-api 1.7.20 → 1.7.21
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/admin/index.d.mts +0 -1
- package/dist/admin/index.d.ts +0 -1
- package/dist/index.d.mts +39 -19
- package/dist/index.d.ts +39 -19
- package/dist/index.js +1116 -951
- package/dist/index.mjs +1132 -965
- package/package.json +1 -1
- package/src/services/patient/patient.service.ts +162 -10
- package/src/services/patient/utils/profile.utils.ts +124 -135
- package/src/services/patient/utils/sensitive.utils.ts +76 -2
- package/src/types/patient/index.ts +25 -23
- package/src/validations/patient.schema.ts +4 -4
|
@@ -16,9 +16,8 @@ import {
|
|
|
16
16
|
limit,
|
|
17
17
|
startAfter,
|
|
18
18
|
doc,
|
|
19
|
-
} from
|
|
20
|
-
import {
|
|
21
|
-
import { z } from 'zod';
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { z } from "zod";
|
|
22
21
|
import {
|
|
23
22
|
PatientProfile,
|
|
24
23
|
CreatePatientProfileData,
|
|
@@ -26,43 +25,42 @@ import {
|
|
|
26
25
|
SearchPatientsParams,
|
|
27
26
|
RequesterInfo,
|
|
28
27
|
PATIENTS_COLLECTION,
|
|
29
|
-
} from
|
|
28
|
+
} from "../../../types/patient";
|
|
30
29
|
import {
|
|
31
30
|
patientProfileSchema,
|
|
32
31
|
createPatientProfileSchema,
|
|
33
32
|
searchPatientsSchema,
|
|
34
33
|
requesterInfoSchema,
|
|
35
|
-
} from
|
|
34
|
+
} from "../../../validations/patient.schema";
|
|
36
35
|
import {
|
|
37
36
|
getPatientDocRef,
|
|
38
37
|
getPatientDocRefByUserRef,
|
|
39
38
|
initSensitiveInfoDocIfNotExists,
|
|
40
39
|
getSensitiveInfoDocRef,
|
|
41
40
|
getMedicalInfoDocRef,
|
|
42
|
-
} from
|
|
43
|
-
import { ensureMedicalInfoExists } from
|
|
44
|
-
import { DEFAULT_MEDICAL_INFO } from
|
|
41
|
+
} from "./docs.utils";
|
|
42
|
+
import { ensureMedicalInfoExists } from "./medical.utils";
|
|
43
|
+
import { DEFAULT_MEDICAL_INFO } from "../../../types/patient/medical-info.types";
|
|
45
44
|
|
|
46
45
|
// Funkcije za rad sa profilom
|
|
47
46
|
export const createPatientProfileUtil = async (
|
|
48
47
|
db: Firestore,
|
|
49
48
|
data: CreatePatientProfileData,
|
|
50
|
-
generateId: () => string
|
|
49
|
+
generateId: () => string
|
|
51
50
|
): Promise<PatientProfile> => {
|
|
52
51
|
try {
|
|
53
|
-
console.log(
|
|
52
|
+
console.log("[createPatientProfileUtil] Starting patient profile creation");
|
|
54
53
|
const validatedData = createPatientProfileSchema.parse(data);
|
|
55
54
|
const patientId = generateId();
|
|
56
55
|
console.log(`[createPatientProfileUtil] Generated patientId: ${patientId}`);
|
|
57
56
|
|
|
58
|
-
const patientData: Omit<PatientProfile,
|
|
57
|
+
const patientData: Omit<PatientProfile, "createdAt" | "updatedAt"> & {
|
|
59
58
|
createdAt: ReturnType<typeof serverTimestamp>;
|
|
60
59
|
updatedAt: ReturnType<typeof serverTimestamp>;
|
|
61
60
|
} = {
|
|
62
61
|
id: patientId,
|
|
63
62
|
userRef: validatedData.userRef,
|
|
64
63
|
displayName: validatedData.displayName,
|
|
65
|
-
profilePhoto: validatedData.profilePhoto || null,
|
|
66
64
|
expoTokens: validatedData.expoTokens,
|
|
67
65
|
gamification: validatedData.gamification || {
|
|
68
66
|
level: 1,
|
|
@@ -93,16 +91,21 @@ export const createPatientProfileUtil = async (
|
|
|
93
91
|
const sensitiveInfoCreated = await initSensitiveInfoDocIfNotExists(
|
|
94
92
|
db,
|
|
95
93
|
patientId,
|
|
96
|
-
validatedData.userRef
|
|
94
|
+
validatedData.userRef
|
|
97
95
|
);
|
|
98
96
|
console.log(
|
|
99
97
|
`[createPatientProfileUtil] Sensitive info document creation result: ${
|
|
100
|
-
sensitiveInfoCreated
|
|
101
|
-
|
|
98
|
+
sensitiveInfoCreated
|
|
99
|
+
? "Document already existed"
|
|
100
|
+
: "New document created"
|
|
101
|
+
}`
|
|
102
102
|
);
|
|
103
103
|
sensitiveInfoSuccess = true;
|
|
104
104
|
} catch (sensitiveError) {
|
|
105
|
-
console.error(
|
|
105
|
+
console.error(
|
|
106
|
+
`[createPatientProfileUtil] Error creating sensitive info:`,
|
|
107
|
+
sensitiveError
|
|
108
|
+
);
|
|
106
109
|
// Don't throw the error, we'll try the direct method
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -111,21 +114,33 @@ export const createPatientProfileUtil = async (
|
|
|
111
114
|
let medicalInfoSuccess = false;
|
|
112
115
|
try {
|
|
113
116
|
await ensureMedicalInfoExists(db, patientId, validatedData.userRef);
|
|
114
|
-
console.log(
|
|
117
|
+
console.log(
|
|
118
|
+
`[createPatientProfileUtil] Medical info document created successfully`
|
|
119
|
+
);
|
|
115
120
|
medicalInfoSuccess = true;
|
|
116
121
|
} catch (medicalError) {
|
|
117
|
-
console.error(
|
|
122
|
+
console.error(
|
|
123
|
+
`[createPatientProfileUtil] Error creating medical info:`,
|
|
124
|
+
medicalError
|
|
125
|
+
);
|
|
118
126
|
// Don't throw the error, we'll try the direct method
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
// If either utility function failed, try the direct method
|
|
122
130
|
if (!sensitiveInfoSuccess || !medicalInfoSuccess) {
|
|
123
|
-
console.log(
|
|
131
|
+
console.log(
|
|
132
|
+
`[createPatientProfileUtil] Using fallback method to create documents`
|
|
133
|
+
);
|
|
124
134
|
try {
|
|
125
135
|
await testCreateSubDocuments(db, patientId, validatedData.userRef);
|
|
126
|
-
console.log(
|
|
136
|
+
console.log(
|
|
137
|
+
`[createPatientProfileUtil] Fallback method completed successfully`
|
|
138
|
+
);
|
|
127
139
|
} catch (fallbackError) {
|
|
128
|
-
console.error(
|
|
140
|
+
console.error(
|
|
141
|
+
`[createPatientProfileUtil] Fallback method failed:`,
|
|
142
|
+
fallbackError
|
|
143
|
+
);
|
|
129
144
|
// Still continue to return the patient profile
|
|
130
145
|
}
|
|
131
146
|
}
|
|
@@ -133,16 +148,23 @@ export const createPatientProfileUtil = async (
|
|
|
133
148
|
console.log(`[createPatientProfileUtil] Verifying patient document exists`);
|
|
134
149
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
135
150
|
if (!patientDoc.exists()) {
|
|
136
|
-
console.error(
|
|
137
|
-
|
|
151
|
+
console.error(
|
|
152
|
+
`[createPatientProfileUtil] Patient document not found after creation`
|
|
153
|
+
);
|
|
154
|
+
throw new Error("Failed to create patient profile");
|
|
138
155
|
}
|
|
139
156
|
|
|
140
|
-
console.log(
|
|
157
|
+
console.log(
|
|
158
|
+
`[createPatientProfileUtil] Patient profile creation completed successfully`
|
|
159
|
+
);
|
|
141
160
|
return patientDoc.data() as PatientProfile;
|
|
142
161
|
} catch (error) {
|
|
143
|
-
console.error(
|
|
162
|
+
console.error(
|
|
163
|
+
`[createPatientProfileUtil] Error in patient profile creation:`,
|
|
164
|
+
error
|
|
165
|
+
);
|
|
144
166
|
if (error instanceof z.ZodError) {
|
|
145
|
-
throw new Error(
|
|
167
|
+
throw new Error("Invalid patient data: " + error.message);
|
|
146
168
|
}
|
|
147
169
|
throw error;
|
|
148
170
|
}
|
|
@@ -150,7 +172,7 @@ export const createPatientProfileUtil = async (
|
|
|
150
172
|
|
|
151
173
|
export const getPatientProfileUtil = async (
|
|
152
174
|
db: Firestore,
|
|
153
|
-
patientId: string
|
|
175
|
+
patientId: string
|
|
154
176
|
): Promise<PatientProfile | null> => {
|
|
155
177
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
156
178
|
return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
|
|
@@ -158,7 +180,7 @@ export const getPatientProfileUtil = async (
|
|
|
158
180
|
|
|
159
181
|
export const getPatientProfileByUserRefUtil = async (
|
|
160
182
|
db: Firestore,
|
|
161
|
-
userRef: string
|
|
183
|
+
userRef: string
|
|
162
184
|
): Promise<PatientProfile | null> => {
|
|
163
185
|
try {
|
|
164
186
|
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
@@ -173,7 +195,7 @@ export const getPatientProfileByUserRefUtil = async (
|
|
|
173
195
|
export const addExpoTokenUtil = async (
|
|
174
196
|
db: Firestore,
|
|
175
197
|
patientId: string,
|
|
176
|
-
token: string
|
|
198
|
+
token: string
|
|
177
199
|
): Promise<void> => {
|
|
178
200
|
await updateDoc(getPatientDocRef(db, patientId), {
|
|
179
201
|
expoTokens: arrayUnion(token),
|
|
@@ -184,7 +206,7 @@ export const addExpoTokenUtil = async (
|
|
|
184
206
|
export const removeExpoTokenUtil = async (
|
|
185
207
|
db: Firestore,
|
|
186
208
|
patientId: string,
|
|
187
|
-
token: string
|
|
209
|
+
token: string
|
|
188
210
|
): Promise<void> => {
|
|
189
211
|
await updateDoc(getPatientDocRef(db, patientId), {
|
|
190
212
|
expoTokens: arrayRemove(token),
|
|
@@ -195,10 +217,10 @@ export const removeExpoTokenUtil = async (
|
|
|
195
217
|
export const addPointsUtil = async (
|
|
196
218
|
db: Firestore,
|
|
197
219
|
patientId: string,
|
|
198
|
-
points: number
|
|
220
|
+
points: number
|
|
199
221
|
): Promise<void> => {
|
|
200
222
|
await updateDoc(getPatientDocRef(db, patientId), {
|
|
201
|
-
|
|
223
|
+
"gamification.points": increment(points),
|
|
202
224
|
updatedAt: serverTimestamp(),
|
|
203
225
|
});
|
|
204
226
|
};
|
|
@@ -206,7 +228,7 @@ export const addPointsUtil = async (
|
|
|
206
228
|
export const updatePatientProfileUtil = async (
|
|
207
229
|
db: Firestore,
|
|
208
230
|
patientId: string,
|
|
209
|
-
data: Partial<Omit<PatientProfile,
|
|
231
|
+
data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
|
|
210
232
|
): Promise<PatientProfile> => {
|
|
211
233
|
try {
|
|
212
234
|
const updateData = {
|
|
@@ -218,7 +240,7 @@ export const updatePatientProfileUtil = async (
|
|
|
218
240
|
|
|
219
241
|
const updatedDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
220
242
|
if (!updatedDoc.exists()) {
|
|
221
|
-
throw new Error(
|
|
243
|
+
throw new Error("Patient profile not found after update");
|
|
222
244
|
}
|
|
223
245
|
|
|
224
246
|
return updatedDoc.data() as PatientProfile;
|
|
@@ -231,89 +253,24 @@ export const updatePatientProfileUtil = async (
|
|
|
231
253
|
export const updatePatientProfileByUserRefUtil = async (
|
|
232
254
|
db: Firestore,
|
|
233
255
|
userRef: string,
|
|
234
|
-
data: Partial<Omit<PatientProfile,
|
|
256
|
+
data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
|
|
235
257
|
): Promise<PatientProfile> => {
|
|
236
258
|
try {
|
|
237
259
|
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
238
260
|
const patientDoc = await getDoc(docRef);
|
|
239
261
|
|
|
240
262
|
if (!patientDoc.exists()) {
|
|
241
|
-
throw new Error(
|
|
263
|
+
throw new Error("Patient profile not found");
|
|
242
264
|
}
|
|
243
265
|
|
|
244
266
|
const patientData = patientDoc.data() as PatientProfile;
|
|
245
267
|
return updatePatientProfileUtil(db, patientData.id, data);
|
|
246
268
|
} catch (error: unknown) {
|
|
247
269
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
248
|
-
throw new Error(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Pomoćne funkcije za profilnu sliku
|
|
253
|
-
export const uploadProfilePhotoUtil = async (
|
|
254
|
-
storage: FirebaseStorage,
|
|
255
|
-
patientId: string,
|
|
256
|
-
file: File,
|
|
257
|
-
): Promise<string> => {
|
|
258
|
-
const photoRef = ref(storage, `patient-photos/${patientId}/profile-photo`);
|
|
259
|
-
await uploadBytes(photoRef, file);
|
|
260
|
-
return getDownloadURL(photoRef);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
export const updateProfilePhotoUtil = async (
|
|
264
|
-
storage: FirebaseStorage,
|
|
265
|
-
db: Firestore,
|
|
266
|
-
patientId: string,
|
|
267
|
-
file: File,
|
|
268
|
-
): Promise<string> => {
|
|
269
|
-
// Prvo obrišemo staru sliku ako postoji
|
|
270
|
-
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
271
|
-
if (!patientDoc.exists()) throw new Error('Patient profile not found');
|
|
272
|
-
|
|
273
|
-
const patientData = patientDoc.data() as PatientProfile;
|
|
274
|
-
if (patientData.profilePhoto) {
|
|
275
|
-
try {
|
|
276
|
-
const oldPhotoRef = ref(storage, patientData.profilePhoto);
|
|
277
|
-
await deleteObject(oldPhotoRef);
|
|
278
|
-
} catch (error) {
|
|
279
|
-
console.warn('Failed to delete old profile photo:', error);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Zatim uploadujemo novu sliku
|
|
284
|
-
const newPhotoUrl = await uploadProfilePhotoUtil(storage, patientId, file);
|
|
285
|
-
|
|
286
|
-
// Ažuriramo profil sa novim URL-om
|
|
287
|
-
await updateDoc(getPatientDocRef(db, patientId), {
|
|
288
|
-
profilePhoto: newPhotoUrl,
|
|
289
|
-
updatedAt: serverTimestamp(),
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
return newPhotoUrl;
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
export const deleteProfilePhotoUtil = async (
|
|
296
|
-
storage: FirebaseStorage,
|
|
297
|
-
db: Firestore,
|
|
298
|
-
patientId: string,
|
|
299
|
-
): Promise<void> => {
|
|
300
|
-
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
301
|
-
if (!patientDoc.exists()) throw new Error('Patient profile not found');
|
|
302
|
-
|
|
303
|
-
const patientData = patientDoc.data() as PatientProfile;
|
|
304
|
-
if (patientData.profilePhoto) {
|
|
305
|
-
try {
|
|
306
|
-
const photoRef = ref(storage, patientData.profilePhoto);
|
|
307
|
-
await deleteObject(photoRef);
|
|
308
|
-
} catch (error) {
|
|
309
|
-
console.warn('Failed to delete profile photo:', error);
|
|
310
|
-
}
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Failed to update patient profile by user ref: ${errorMessage}`
|
|
272
|
+
);
|
|
311
273
|
}
|
|
312
|
-
|
|
313
|
-
await updateDoc(getPatientDocRef(db, patientId), {
|
|
314
|
-
profilePhoto: null,
|
|
315
|
-
updatedAt: serverTimestamp(),
|
|
316
|
-
});
|
|
317
274
|
};
|
|
318
275
|
|
|
319
276
|
/**
|
|
@@ -323,39 +280,45 @@ export const deleteProfilePhotoUtil = async (
|
|
|
323
280
|
export const testCreateSubDocuments = async (
|
|
324
281
|
db: Firestore,
|
|
325
282
|
patientId: string,
|
|
326
|
-
userRef: string
|
|
283
|
+
userRef: string
|
|
327
284
|
): Promise<void> => {
|
|
328
285
|
console.log(
|
|
329
|
-
`[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}
|
|
286
|
+
`[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}`
|
|
330
287
|
);
|
|
331
288
|
|
|
332
289
|
try {
|
|
333
290
|
// Test sensitive info creation
|
|
334
291
|
console.log(`[testCreateSubDocuments] Testing sensitive info creation`);
|
|
335
292
|
const sensitiveInfoRef = getSensitiveInfoDocRef(db, patientId);
|
|
336
|
-
console.log(
|
|
293
|
+
console.log(
|
|
294
|
+
`[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`
|
|
295
|
+
);
|
|
337
296
|
|
|
338
297
|
const defaultSensitiveInfo = {
|
|
339
298
|
patientId,
|
|
340
299
|
userRef,
|
|
341
|
-
photoUrl:
|
|
342
|
-
firstName:
|
|
343
|
-
lastName:
|
|
300
|
+
photoUrl: "",
|
|
301
|
+
firstName: "Name",
|
|
302
|
+
lastName: "Surname",
|
|
344
303
|
dateOfBirth: Timestamp.now(),
|
|
345
304
|
gender: Gender.PREFER_NOT_TO_SAY,
|
|
346
|
-
email:
|
|
347
|
-
phoneNumber:
|
|
305
|
+
email: "test@example.com",
|
|
306
|
+
phoneNumber: "",
|
|
348
307
|
createdAt: Timestamp.now(),
|
|
349
308
|
updatedAt: Timestamp.now(),
|
|
350
309
|
};
|
|
351
310
|
|
|
352
311
|
await setDoc(sensitiveInfoRef, defaultSensitiveInfo);
|
|
353
|
-
console.log(
|
|
312
|
+
console.log(
|
|
313
|
+
`[testCreateSubDocuments] Sensitive info document created directly`
|
|
314
|
+
);
|
|
354
315
|
|
|
355
316
|
// Test medical info creation
|
|
356
317
|
console.log(`[testCreateSubDocuments] Testing medical info creation`);
|
|
357
318
|
const medicalInfoRef = getMedicalInfoDocRef(db, patientId);
|
|
358
|
-
console.log(
|
|
319
|
+
console.log(
|
|
320
|
+
`[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`
|
|
321
|
+
);
|
|
359
322
|
|
|
360
323
|
const defaultMedicalInfo = {
|
|
361
324
|
...DEFAULT_MEDICAL_INFO,
|
|
@@ -365,7 +328,9 @@ export const testCreateSubDocuments = async (
|
|
|
365
328
|
};
|
|
366
329
|
|
|
367
330
|
await setDoc(medicalInfoRef, defaultMedicalInfo);
|
|
368
|
-
console.log(
|
|
331
|
+
console.log(
|
|
332
|
+
`[testCreateSubDocuments] Medical info document created directly`
|
|
333
|
+
);
|
|
369
334
|
|
|
370
335
|
console.log(`[testCreateSubDocuments] Test completed successfully`);
|
|
371
336
|
} catch (error) {
|
|
@@ -386,7 +351,7 @@ export const testCreateSubDocuments = async (
|
|
|
386
351
|
export const searchPatientsUtil = async (
|
|
387
352
|
db: Firestore,
|
|
388
353
|
params: SearchPatientsParams,
|
|
389
|
-
requester: RequesterInfo
|
|
354
|
+
requester: RequesterInfo
|
|
390
355
|
): Promise<PatientProfile[]> => {
|
|
391
356
|
// Validate input
|
|
392
357
|
searchPatientsSchema.parse(params);
|
|
@@ -397,52 +362,65 @@ export const searchPatientsUtil = async (
|
|
|
397
362
|
|
|
398
363
|
// --- Security Checks & Initial Filtering ---
|
|
399
364
|
|
|
400
|
-
if (requester.role ===
|
|
365
|
+
if (requester.role === "clinic_admin") {
|
|
401
366
|
// Clinic admin can only search within their own clinic
|
|
402
367
|
if (!requester.associatedClinicId) {
|
|
403
|
-
throw new Error(
|
|
368
|
+
throw new Error(
|
|
369
|
+
"Associated clinic ID is required for clinic admin search."
|
|
370
|
+
);
|
|
404
371
|
}
|
|
405
372
|
// If the search params specify a different clinic, it's an invalid request for this admin.
|
|
406
373
|
if (params.clinicId && params.clinicId !== requester.associatedClinicId) {
|
|
407
374
|
console.warn(
|
|
408
|
-
`Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})
|
|
375
|
+
`Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`
|
|
409
376
|
);
|
|
410
377
|
return []; // Or throw an error
|
|
411
378
|
}
|
|
412
379
|
|
|
413
380
|
// **Mandatory filter**: Ensure patients belong to the admin's clinic.
|
|
414
|
-
constraints.push(
|
|
381
|
+
constraints.push(
|
|
382
|
+
where("clinicIds", "array-contains", requester.associatedClinicId)
|
|
383
|
+
);
|
|
415
384
|
|
|
416
385
|
// Optional filter: If practitionerId is also provided, filter by that practitioner *within the admin's clinic*.
|
|
417
386
|
if (params.practitionerId) {
|
|
418
|
-
constraints.push(
|
|
387
|
+
constraints.push(
|
|
388
|
+
where("doctorIds", "array-contains", params.practitionerId)
|
|
389
|
+
);
|
|
419
390
|
// We might need an additional check here if the practitioner MUST work at the admin's clinic.
|
|
420
391
|
// This would require fetching practitioner data or having denormalized clinic IDs on the practitioner.
|
|
421
392
|
}
|
|
422
|
-
} else if (requester.role ===
|
|
393
|
+
} else if (requester.role === "practitioner") {
|
|
423
394
|
// Practitioner can only search for their own patients.
|
|
424
395
|
if (!requester.associatedPractitionerId) {
|
|
425
|
-
throw new Error(
|
|
396
|
+
throw new Error(
|
|
397
|
+
"Associated practitioner ID is required for practitioner search."
|
|
398
|
+
);
|
|
426
399
|
}
|
|
427
400
|
// If the search params specify a different practitioner, it's invalid.
|
|
428
|
-
if (
|
|
401
|
+
if (
|
|
402
|
+
params.practitionerId &&
|
|
403
|
+
params.practitionerId !== requester.associatedPractitionerId
|
|
404
|
+
) {
|
|
429
405
|
console.warn(
|
|
430
|
-
`Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})
|
|
406
|
+
`Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`
|
|
431
407
|
);
|
|
432
408
|
return []; // Or throw an error
|
|
433
409
|
}
|
|
434
410
|
|
|
435
411
|
// **Mandatory filter**: Ensure patients are associated with this practitioner.
|
|
436
|
-
constraints.push(
|
|
412
|
+
constraints.push(
|
|
413
|
+
where("doctorIds", "array-contains", requester.associatedPractitionerId)
|
|
414
|
+
);
|
|
437
415
|
|
|
438
416
|
// Optional filter: If clinicId is provided, filter by patients of this practitioner *at that specific clinic*.
|
|
439
417
|
if (params.clinicId) {
|
|
440
|
-
constraints.push(where(
|
|
418
|
+
constraints.push(where("clinicIds", "array-contains", params.clinicId));
|
|
441
419
|
// Similar to above, we might need to check if the practitioner actually works at this clinic.
|
|
442
420
|
}
|
|
443
421
|
} else {
|
|
444
422
|
// Should not happen due to validation, but good practice to handle.
|
|
445
|
-
throw new Error(
|
|
423
|
+
throw new Error("Invalid requester role.");
|
|
446
424
|
}
|
|
447
425
|
|
|
448
426
|
// --- Execute Query ---
|
|
@@ -450,12 +428,16 @@ export const searchPatientsUtil = async (
|
|
|
450
428
|
const finalQuery = query(patientsCollectionRef, ...constraints);
|
|
451
429
|
const querySnapshot = await getDocs(finalQuery);
|
|
452
430
|
|
|
453
|
-
const patients = querySnapshot.docs.map(
|
|
431
|
+
const patients = querySnapshot.docs.map(
|
|
432
|
+
(doc) => doc.data() as PatientProfile
|
|
433
|
+
);
|
|
454
434
|
|
|
455
|
-
console.log(
|
|
435
|
+
console.log(
|
|
436
|
+
`[searchPatientsUtil] Found ${patients.length} patients matching criteria.`
|
|
437
|
+
);
|
|
456
438
|
return patients;
|
|
457
439
|
} catch (error) {
|
|
458
|
-
console.error(
|
|
440
|
+
console.error("[searchPatientsUtil] Error searching patients:", error);
|
|
459
441
|
// Consider logging more details or re-throwing a specific error type
|
|
460
442
|
return []; // Return empty array on error
|
|
461
443
|
}
|
|
@@ -472,10 +454,13 @@ export const searchPatientsUtil = async (
|
|
|
472
454
|
*/
|
|
473
455
|
export const getAllPatientsUtil = async (
|
|
474
456
|
db: Firestore,
|
|
475
|
-
options?: { limit?: number; startAfter?: string }
|
|
457
|
+
options?: { limit?: number; startAfter?: string }
|
|
476
458
|
): Promise<PatientProfile[]> => {
|
|
477
459
|
try {
|
|
478
|
-
console.log(
|
|
460
|
+
console.log(
|
|
461
|
+
`[getAllPatientsUtil] Fetching patients with options:`,
|
|
462
|
+
options
|
|
463
|
+
);
|
|
479
464
|
|
|
480
465
|
const patientsCollection = collection(db, PATIENTS_COLLECTION);
|
|
481
466
|
|
|
@@ -488,7 +473,9 @@ export const getAllPatientsUtil = async (
|
|
|
488
473
|
|
|
489
474
|
// If startAfter is provided, get that document and use it for pagination
|
|
490
475
|
if (options?.startAfter) {
|
|
491
|
-
const startAfterDoc = await getDoc(
|
|
476
|
+
const startAfterDoc = await getDoc(
|
|
477
|
+
doc(db, PATIENTS_COLLECTION, options.startAfter)
|
|
478
|
+
);
|
|
492
479
|
if (startAfterDoc.exists()) {
|
|
493
480
|
q = query(q, startAfter(startAfterDoc));
|
|
494
481
|
}
|
|
@@ -506,7 +493,9 @@ export const getAllPatientsUtil = async (
|
|
|
506
493
|
} catch (error) {
|
|
507
494
|
console.error(`[getAllPatientsUtil] Error fetching patients:`, error);
|
|
508
495
|
throw new Error(
|
|
509
|
-
`Failed to retrieve patients: ${
|
|
496
|
+
`Failed to retrieve patients: ${
|
|
497
|
+
error instanceof Error ? error.message : String(error)
|
|
498
|
+
}`
|
|
510
499
|
);
|
|
511
500
|
}
|
|
512
501
|
};
|
|
@@ -16,12 +16,54 @@ import {
|
|
|
16
16
|
getSensitiveInfoDocRef,
|
|
17
17
|
initSensitiveInfoDocIfNotExists,
|
|
18
18
|
} from "./docs.utils";
|
|
19
|
+
import {
|
|
20
|
+
MediaService,
|
|
21
|
+
MediaAccessLevel,
|
|
22
|
+
MediaResource,
|
|
23
|
+
} from "../../media/media.service";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Handles photoUrl upload for sensitive info (supports MediaResource)
|
|
27
|
+
* @param photoUrl - MediaResource (File, Blob, or URL string)
|
|
28
|
+
* @param patientId - ID of the patient
|
|
29
|
+
* @param mediaService - MediaService instance
|
|
30
|
+
* @returns URL string of the uploaded or existing photo
|
|
31
|
+
*/
|
|
32
|
+
const handlePhotoUrlUpload = async (
|
|
33
|
+
photoUrl: MediaResource | undefined | null,
|
|
34
|
+
patientId: string,
|
|
35
|
+
mediaService: MediaService
|
|
36
|
+
): Promise<string | null> => {
|
|
37
|
+
if (!photoUrl) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If it's already a URL string, return it as is
|
|
42
|
+
if (typeof photoUrl === "string") {
|
|
43
|
+
return photoUrl;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If it's a File or Blob, upload it
|
|
47
|
+
if (photoUrl instanceof File || photoUrl instanceof Blob) {
|
|
48
|
+
const mediaMetadata = await mediaService.uploadMedia(
|
|
49
|
+
photoUrl,
|
|
50
|
+
patientId, // Using patientId as ownerId
|
|
51
|
+
MediaAccessLevel.PRIVATE, // Sensitive info should be private
|
|
52
|
+
"patient_sensitive_photos",
|
|
53
|
+
photoUrl instanceof File ? photoUrl.name : `sensitive_photo_${patientId}`
|
|
54
|
+
);
|
|
55
|
+
return mediaMetadata.url;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
19
60
|
|
|
20
61
|
// Funkcije za rad sa osetljivim informacijama
|
|
21
62
|
export const createSensitiveInfoUtil = async (
|
|
22
63
|
db: Firestore,
|
|
23
64
|
data: CreatePatientSensitiveInfoData,
|
|
24
|
-
requesterUserId: string
|
|
65
|
+
requesterUserId: string,
|
|
66
|
+
mediaService?: MediaService
|
|
25
67
|
): Promise<PatientSensitiveInfo> => {
|
|
26
68
|
try {
|
|
27
69
|
if (data.userRef !== requesterUserId) {
|
|
@@ -38,8 +80,21 @@ export const createSensitiveInfoUtil = async (
|
|
|
38
80
|
throw new Error("Sensitive information already exists for this patient");
|
|
39
81
|
}
|
|
40
82
|
|
|
83
|
+
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
84
|
+
let processedPhotoUrl: string | null = null;
|
|
85
|
+
if (validatedData.photoUrl && mediaService) {
|
|
86
|
+
processedPhotoUrl = await handlePhotoUrlUpload(
|
|
87
|
+
validatedData.photoUrl,
|
|
88
|
+
data.patientId,
|
|
89
|
+
mediaService
|
|
90
|
+
);
|
|
91
|
+
} else if (typeof validatedData.photoUrl === "string") {
|
|
92
|
+
processedPhotoUrl = validatedData.photoUrl;
|
|
93
|
+
}
|
|
94
|
+
|
|
41
95
|
const sensitiveInfoData = {
|
|
42
96
|
...validatedData,
|
|
97
|
+
photoUrl: processedPhotoUrl,
|
|
43
98
|
createdAt: serverTimestamp(),
|
|
44
99
|
updatedAt: serverTimestamp(),
|
|
45
100
|
};
|
|
@@ -82,7 +137,8 @@ export const updateSensitiveInfoUtil = async (
|
|
|
82
137
|
db: Firestore,
|
|
83
138
|
patientId: string,
|
|
84
139
|
data: UpdatePatientSensitiveInfoData,
|
|
85
|
-
requesterUserId: string
|
|
140
|
+
requesterUserId: string,
|
|
141
|
+
mediaService?: MediaService
|
|
86
142
|
): Promise<PatientSensitiveInfo> => {
|
|
87
143
|
// if (data.userRef !== requesterUserId) {
|
|
88
144
|
// throw new Error("Only patient can update their sensitive information");
|
|
@@ -91,8 +147,26 @@ export const updateSensitiveInfoUtil = async (
|
|
|
91
147
|
// Inicijalizacija dokumenta ako ne postoji
|
|
92
148
|
await initSensitiveInfoDocIfNotExists(db, patientId, requesterUserId);
|
|
93
149
|
|
|
150
|
+
// Process photoUrl if it's a MediaResource and mediaService is provided
|
|
151
|
+
let processedPhotoUrl: string | null | undefined = undefined;
|
|
152
|
+
if (data.photoUrl !== undefined) {
|
|
153
|
+
if (mediaService) {
|
|
154
|
+
processedPhotoUrl = await handlePhotoUrlUpload(
|
|
155
|
+
data.photoUrl,
|
|
156
|
+
patientId,
|
|
157
|
+
mediaService
|
|
158
|
+
);
|
|
159
|
+
} else if (typeof data.photoUrl === "string" || data.photoUrl === null) {
|
|
160
|
+
processedPhotoUrl = data.photoUrl;
|
|
161
|
+
} else {
|
|
162
|
+
// If photoUrl is a File/Blob but no mediaService provided, throw error
|
|
163
|
+
throw new Error("MediaService required to process photo upload");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
94
167
|
const updateData = {
|
|
95
168
|
...data,
|
|
169
|
+
photoUrl: processedPhotoUrl,
|
|
96
170
|
updatedAt: serverTimestamp(),
|
|
97
171
|
};
|
|
98
172
|
|