@blackcode_sa/metaestetics-api 1.5.16 → 1.5.18
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/backoffice/index.d.mts +2 -2
- package/dist/backoffice/index.d.ts +2 -2
- package/dist/index.d.mts +233 -40
- package/dist/index.d.ts +233 -40
- package/dist/index.js +748 -427
- package/dist/index.mjs +1073 -739
- package/package.json +1 -1
- package/src/index.ts +5 -0
- package/src/services/calendar/calendar-refactored.service.ts +147 -0
- package/src/services/calendar/utils/calendar-event.utils.ts +136 -0
- package/src/services/calendar/utils/index.ts +5 -5
- package/src/services/patient/patient.service.ts +100 -127
- package/src/services/patient/utils/medical-stuff.utils.ts +69 -18
- package/src/services/patient/utils/profile.utils.ts +206 -94
- package/src/types/calendar/index.ts +41 -0
- package/src/types/patient/index.ts +31 -1
- package/src/types/practitioner/index.ts +13 -14
- package/src/validations/patient.schema.ts +43 -0
|
@@ -8,47 +8,54 @@ import {
|
|
|
8
8
|
increment,
|
|
9
9
|
Firestore,
|
|
10
10
|
Timestamp,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
collection,
|
|
12
|
+
query,
|
|
13
|
+
where,
|
|
14
|
+
getDocs,
|
|
15
|
+
QueryConstraint,
|
|
16
|
+
limit,
|
|
17
|
+
startAfter,
|
|
18
|
+
doc,
|
|
19
|
+
} from 'firebase/firestore';
|
|
20
|
+
import { ref, uploadBytes, getDownloadURL, deleteObject, FirebaseStorage } from 'firebase/storage';
|
|
21
|
+
import { z } from 'zod';
|
|
20
22
|
import {
|
|
21
23
|
PatientProfile,
|
|
22
24
|
CreatePatientProfileData,
|
|
23
25
|
Gender,
|
|
24
|
-
|
|
26
|
+
SearchPatientsParams,
|
|
27
|
+
RequesterInfo,
|
|
28
|
+
PATIENTS_COLLECTION,
|
|
29
|
+
} from '../../../types/patient';
|
|
25
30
|
import {
|
|
26
31
|
patientProfileSchema,
|
|
27
32
|
createPatientProfileSchema,
|
|
28
|
-
|
|
33
|
+
searchPatientsSchema,
|
|
34
|
+
requesterInfoSchema,
|
|
35
|
+
} from '../../../validations/patient.schema';
|
|
29
36
|
import {
|
|
30
37
|
getPatientDocRef,
|
|
31
38
|
getPatientDocRefByUserRef,
|
|
32
39
|
initSensitiveInfoDocIfNotExists,
|
|
33
40
|
getSensitiveInfoDocRef,
|
|
34
41
|
getMedicalInfoDocRef,
|
|
35
|
-
} from
|
|
36
|
-
import { ensureMedicalInfoExists } from
|
|
37
|
-
import { DEFAULT_MEDICAL_INFO } from
|
|
42
|
+
} from './docs.utils';
|
|
43
|
+
import { ensureMedicalInfoExists } from './medical.utils';
|
|
44
|
+
import { DEFAULT_MEDICAL_INFO } from '../../../types/patient/medical-info.types';
|
|
38
45
|
|
|
39
46
|
// Funkcije za rad sa profilom
|
|
40
47
|
export const createPatientProfileUtil = async (
|
|
41
48
|
db: Firestore,
|
|
42
49
|
data: CreatePatientProfileData,
|
|
43
|
-
generateId: () => string
|
|
50
|
+
generateId: () => string,
|
|
44
51
|
): Promise<PatientProfile> => {
|
|
45
52
|
try {
|
|
46
|
-
console.log(
|
|
53
|
+
console.log('[createPatientProfileUtil] Starting patient profile creation');
|
|
47
54
|
const validatedData = createPatientProfileSchema.parse(data);
|
|
48
55
|
const patientId = generateId();
|
|
49
56
|
console.log(`[createPatientProfileUtil] Generated patientId: ${patientId}`);
|
|
50
57
|
|
|
51
|
-
const patientData: Omit<PatientProfile,
|
|
58
|
+
const patientData: Omit<PatientProfile, 'createdAt' | 'updatedAt'> & {
|
|
52
59
|
createdAt: ReturnType<typeof serverTimestamp>;
|
|
53
60
|
updatedAt: ReturnType<typeof serverTimestamp>;
|
|
54
61
|
} = {
|
|
@@ -65,6 +72,8 @@ export const createPatientProfileUtil = async (
|
|
|
65
72
|
isVerified: validatedData.isVerified,
|
|
66
73
|
doctors: validatedData.doctors || [],
|
|
67
74
|
clinics: validatedData.clinics || [],
|
|
75
|
+
doctorIds: validatedData.doctors?.map((d) => d.userRef) || [],
|
|
76
|
+
clinicIds: validatedData.clinics?.map((c) => c.clinicId) || [],
|
|
68
77
|
createdAt: serverTimestamp(),
|
|
69
78
|
updatedAt: serverTimestamp(),
|
|
70
79
|
};
|
|
@@ -84,21 +93,16 @@ export const createPatientProfileUtil = async (
|
|
|
84
93
|
const sensitiveInfoCreated = await initSensitiveInfoDocIfNotExists(
|
|
85
94
|
db,
|
|
86
95
|
patientId,
|
|
87
|
-
validatedData.userRef
|
|
96
|
+
validatedData.userRef,
|
|
88
97
|
);
|
|
89
98
|
console.log(
|
|
90
99
|
`[createPatientProfileUtil] Sensitive info document creation result: ${
|
|
91
|
-
sensitiveInfoCreated
|
|
92
|
-
|
|
93
|
-
: "New document created"
|
|
94
|
-
}`
|
|
100
|
+
sensitiveInfoCreated ? 'Document already existed' : 'New document created'
|
|
101
|
+
}`,
|
|
95
102
|
);
|
|
96
103
|
sensitiveInfoSuccess = true;
|
|
97
104
|
} catch (sensitiveError) {
|
|
98
|
-
console.error(
|
|
99
|
-
`[createPatientProfileUtil] Error creating sensitive info:`,
|
|
100
|
-
sensitiveError
|
|
101
|
-
);
|
|
105
|
+
console.error(`[createPatientProfileUtil] Error creating sensitive info:`, sensitiveError);
|
|
102
106
|
// Don't throw the error, we'll try the direct method
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -107,33 +111,21 @@ export const createPatientProfileUtil = async (
|
|
|
107
111
|
let medicalInfoSuccess = false;
|
|
108
112
|
try {
|
|
109
113
|
await ensureMedicalInfoExists(db, patientId, validatedData.userRef);
|
|
110
|
-
console.log(
|
|
111
|
-
`[createPatientProfileUtil] Medical info document created successfully`
|
|
112
|
-
);
|
|
114
|
+
console.log(`[createPatientProfileUtil] Medical info document created successfully`);
|
|
113
115
|
medicalInfoSuccess = true;
|
|
114
116
|
} catch (medicalError) {
|
|
115
|
-
console.error(
|
|
116
|
-
`[createPatientProfileUtil] Error creating medical info:`,
|
|
117
|
-
medicalError
|
|
118
|
-
);
|
|
117
|
+
console.error(`[createPatientProfileUtil] Error creating medical info:`, medicalError);
|
|
119
118
|
// Don't throw the error, we'll try the direct method
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
// If either utility function failed, try the direct method
|
|
123
122
|
if (!sensitiveInfoSuccess || !medicalInfoSuccess) {
|
|
124
|
-
console.log(
|
|
125
|
-
`[createPatientProfileUtil] Using fallback method to create documents`
|
|
126
|
-
);
|
|
123
|
+
console.log(`[createPatientProfileUtil] Using fallback method to create documents`);
|
|
127
124
|
try {
|
|
128
125
|
await testCreateSubDocuments(db, patientId, validatedData.userRef);
|
|
129
|
-
console.log(
|
|
130
|
-
`[createPatientProfileUtil] Fallback method completed successfully`
|
|
131
|
-
);
|
|
126
|
+
console.log(`[createPatientProfileUtil] Fallback method completed successfully`);
|
|
132
127
|
} catch (fallbackError) {
|
|
133
|
-
console.error(
|
|
134
|
-
`[createPatientProfileUtil] Fallback method failed:`,
|
|
135
|
-
fallbackError
|
|
136
|
-
);
|
|
128
|
+
console.error(`[createPatientProfileUtil] Fallback method failed:`, fallbackError);
|
|
137
129
|
// Still continue to return the patient profile
|
|
138
130
|
}
|
|
139
131
|
}
|
|
@@ -141,23 +133,16 @@ export const createPatientProfileUtil = async (
|
|
|
141
133
|
console.log(`[createPatientProfileUtil] Verifying patient document exists`);
|
|
142
134
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
143
135
|
if (!patientDoc.exists()) {
|
|
144
|
-
console.error(
|
|
145
|
-
|
|
146
|
-
);
|
|
147
|
-
throw new Error("Failed to create patient profile");
|
|
136
|
+
console.error(`[createPatientProfileUtil] Patient document not found after creation`);
|
|
137
|
+
throw new Error('Failed to create patient profile');
|
|
148
138
|
}
|
|
149
139
|
|
|
150
|
-
console.log(
|
|
151
|
-
`[createPatientProfileUtil] Patient profile creation completed successfully`
|
|
152
|
-
);
|
|
140
|
+
console.log(`[createPatientProfileUtil] Patient profile creation completed successfully`);
|
|
153
141
|
return patientDoc.data() as PatientProfile;
|
|
154
142
|
} catch (error) {
|
|
155
|
-
console.error(
|
|
156
|
-
`[createPatientProfileUtil] Error in patient profile creation:`,
|
|
157
|
-
error
|
|
158
|
-
);
|
|
143
|
+
console.error(`[createPatientProfileUtil] Error in patient profile creation:`, error);
|
|
159
144
|
if (error instanceof z.ZodError) {
|
|
160
|
-
throw new Error(
|
|
145
|
+
throw new Error('Invalid patient data: ' + error.message);
|
|
161
146
|
}
|
|
162
147
|
throw error;
|
|
163
148
|
}
|
|
@@ -165,7 +150,7 @@ export const createPatientProfileUtil = async (
|
|
|
165
150
|
|
|
166
151
|
export const getPatientProfileUtil = async (
|
|
167
152
|
db: Firestore,
|
|
168
|
-
patientId: string
|
|
153
|
+
patientId: string,
|
|
169
154
|
): Promise<PatientProfile | null> => {
|
|
170
155
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
171
156
|
return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
|
|
@@ -173,7 +158,7 @@ export const getPatientProfileUtil = async (
|
|
|
173
158
|
|
|
174
159
|
export const getPatientProfileByUserRefUtil = async (
|
|
175
160
|
db: Firestore,
|
|
176
|
-
userRef: string
|
|
161
|
+
userRef: string,
|
|
177
162
|
): Promise<PatientProfile | null> => {
|
|
178
163
|
try {
|
|
179
164
|
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
@@ -188,7 +173,7 @@ export const getPatientProfileByUserRefUtil = async (
|
|
|
188
173
|
export const addExpoTokenUtil = async (
|
|
189
174
|
db: Firestore,
|
|
190
175
|
patientId: string,
|
|
191
|
-
token: string
|
|
176
|
+
token: string,
|
|
192
177
|
): Promise<void> => {
|
|
193
178
|
await updateDoc(getPatientDocRef(db, patientId), {
|
|
194
179
|
expoTokens: arrayUnion(token),
|
|
@@ -199,7 +184,7 @@ export const addExpoTokenUtil = async (
|
|
|
199
184
|
export const removeExpoTokenUtil = async (
|
|
200
185
|
db: Firestore,
|
|
201
186
|
patientId: string,
|
|
202
|
-
token: string
|
|
187
|
+
token: string,
|
|
203
188
|
): Promise<void> => {
|
|
204
189
|
await updateDoc(getPatientDocRef(db, patientId), {
|
|
205
190
|
expoTokens: arrayRemove(token),
|
|
@@ -210,10 +195,10 @@ export const removeExpoTokenUtil = async (
|
|
|
210
195
|
export const addPointsUtil = async (
|
|
211
196
|
db: Firestore,
|
|
212
197
|
patientId: string,
|
|
213
|
-
points: number
|
|
198
|
+
points: number,
|
|
214
199
|
): Promise<void> => {
|
|
215
200
|
await updateDoc(getPatientDocRef(db, patientId), {
|
|
216
|
-
|
|
201
|
+
'gamification.points': increment(points),
|
|
217
202
|
updatedAt: serverTimestamp(),
|
|
218
203
|
});
|
|
219
204
|
};
|
|
@@ -221,7 +206,7 @@ export const addPointsUtil = async (
|
|
|
221
206
|
export const updatePatientProfileUtil = async (
|
|
222
207
|
db: Firestore,
|
|
223
208
|
patientId: string,
|
|
224
|
-
data: Partial<Omit<PatientProfile,
|
|
209
|
+
data: Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>>,
|
|
225
210
|
): Promise<PatientProfile> => {
|
|
226
211
|
try {
|
|
227
212
|
const updateData = {
|
|
@@ -233,7 +218,7 @@ export const updatePatientProfileUtil = async (
|
|
|
233
218
|
|
|
234
219
|
const updatedDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
235
220
|
if (!updatedDoc.exists()) {
|
|
236
|
-
throw new Error(
|
|
221
|
+
throw new Error('Patient profile not found after update');
|
|
237
222
|
}
|
|
238
223
|
|
|
239
224
|
return updatedDoc.data() as PatientProfile;
|
|
@@ -246,23 +231,21 @@ export const updatePatientProfileUtil = async (
|
|
|
246
231
|
export const updatePatientProfileByUserRefUtil = async (
|
|
247
232
|
db: Firestore,
|
|
248
233
|
userRef: string,
|
|
249
|
-
data: Partial<Omit<PatientProfile,
|
|
234
|
+
data: Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>>,
|
|
250
235
|
): Promise<PatientProfile> => {
|
|
251
236
|
try {
|
|
252
237
|
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
253
238
|
const patientDoc = await getDoc(docRef);
|
|
254
239
|
|
|
255
240
|
if (!patientDoc.exists()) {
|
|
256
|
-
throw new Error(
|
|
241
|
+
throw new Error('Patient profile not found');
|
|
257
242
|
}
|
|
258
243
|
|
|
259
244
|
const patientData = patientDoc.data() as PatientProfile;
|
|
260
245
|
return updatePatientProfileUtil(db, patientData.id, data);
|
|
261
246
|
} catch (error: unknown) {
|
|
262
247
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
263
|
-
throw new Error(
|
|
264
|
-
`Failed to update patient profile by user ref: ${errorMessage}`
|
|
265
|
-
);
|
|
248
|
+
throw new Error(`Failed to update patient profile by user ref: ${errorMessage}`);
|
|
266
249
|
}
|
|
267
250
|
};
|
|
268
251
|
|
|
@@ -270,7 +253,7 @@ export const updatePatientProfileByUserRefUtil = async (
|
|
|
270
253
|
export const uploadProfilePhotoUtil = async (
|
|
271
254
|
storage: FirebaseStorage,
|
|
272
255
|
patientId: string,
|
|
273
|
-
file: File
|
|
256
|
+
file: File,
|
|
274
257
|
): Promise<string> => {
|
|
275
258
|
const photoRef = ref(storage, `patient-photos/${patientId}/profile-photo`);
|
|
276
259
|
await uploadBytes(photoRef, file);
|
|
@@ -281,11 +264,11 @@ export const updateProfilePhotoUtil = async (
|
|
|
281
264
|
storage: FirebaseStorage,
|
|
282
265
|
db: Firestore,
|
|
283
266
|
patientId: string,
|
|
284
|
-
file: File
|
|
267
|
+
file: File,
|
|
285
268
|
): Promise<string> => {
|
|
286
269
|
// Prvo obrišemo staru sliku ako postoji
|
|
287
270
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
288
|
-
if (!patientDoc.exists()) throw new Error(
|
|
271
|
+
if (!patientDoc.exists()) throw new Error('Patient profile not found');
|
|
289
272
|
|
|
290
273
|
const patientData = patientDoc.data() as PatientProfile;
|
|
291
274
|
if (patientData.profilePhoto) {
|
|
@@ -293,7 +276,7 @@ export const updateProfilePhotoUtil = async (
|
|
|
293
276
|
const oldPhotoRef = ref(storage, patientData.profilePhoto);
|
|
294
277
|
await deleteObject(oldPhotoRef);
|
|
295
278
|
} catch (error) {
|
|
296
|
-
console.warn(
|
|
279
|
+
console.warn('Failed to delete old profile photo:', error);
|
|
297
280
|
}
|
|
298
281
|
}
|
|
299
282
|
|
|
@@ -312,10 +295,10 @@ export const updateProfilePhotoUtil = async (
|
|
|
312
295
|
export const deleteProfilePhotoUtil = async (
|
|
313
296
|
storage: FirebaseStorage,
|
|
314
297
|
db: Firestore,
|
|
315
|
-
patientId: string
|
|
298
|
+
patientId: string,
|
|
316
299
|
): Promise<void> => {
|
|
317
300
|
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
318
|
-
if (!patientDoc.exists()) throw new Error(
|
|
301
|
+
if (!patientDoc.exists()) throw new Error('Patient profile not found');
|
|
319
302
|
|
|
320
303
|
const patientData = patientDoc.data() as PatientProfile;
|
|
321
304
|
if (patientData.profilePhoto) {
|
|
@@ -323,7 +306,7 @@ export const deleteProfilePhotoUtil = async (
|
|
|
323
306
|
const photoRef = ref(storage, patientData.profilePhoto);
|
|
324
307
|
await deleteObject(photoRef);
|
|
325
308
|
} catch (error) {
|
|
326
|
-
console.warn(
|
|
309
|
+
console.warn('Failed to delete profile photo:', error);
|
|
327
310
|
}
|
|
328
311
|
}
|
|
329
312
|
|
|
@@ -340,45 +323,39 @@ export const deleteProfilePhotoUtil = async (
|
|
|
340
323
|
export const testCreateSubDocuments = async (
|
|
341
324
|
db: Firestore,
|
|
342
325
|
patientId: string,
|
|
343
|
-
userRef: string
|
|
326
|
+
userRef: string,
|
|
344
327
|
): Promise<void> => {
|
|
345
328
|
console.log(
|
|
346
|
-
`[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}
|
|
329
|
+
`[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}`,
|
|
347
330
|
);
|
|
348
331
|
|
|
349
332
|
try {
|
|
350
333
|
// Test sensitive info creation
|
|
351
334
|
console.log(`[testCreateSubDocuments] Testing sensitive info creation`);
|
|
352
335
|
const sensitiveInfoRef = getSensitiveInfoDocRef(db, patientId);
|
|
353
|
-
console.log(
|
|
354
|
-
`[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`
|
|
355
|
-
);
|
|
336
|
+
console.log(`[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`);
|
|
356
337
|
|
|
357
338
|
const defaultSensitiveInfo = {
|
|
358
339
|
patientId,
|
|
359
340
|
userRef,
|
|
360
|
-
photoUrl:
|
|
361
|
-
firstName:
|
|
362
|
-
lastName:
|
|
341
|
+
photoUrl: '',
|
|
342
|
+
firstName: 'Name',
|
|
343
|
+
lastName: 'Surname',
|
|
363
344
|
dateOfBirth: Timestamp.now(),
|
|
364
345
|
gender: Gender.PREFER_NOT_TO_SAY,
|
|
365
|
-
email:
|
|
366
|
-
phoneNumber:
|
|
346
|
+
email: 'test@example.com',
|
|
347
|
+
phoneNumber: '',
|
|
367
348
|
createdAt: Timestamp.now(),
|
|
368
349
|
updatedAt: Timestamp.now(),
|
|
369
350
|
};
|
|
370
351
|
|
|
371
352
|
await setDoc(sensitiveInfoRef, defaultSensitiveInfo);
|
|
372
|
-
console.log(
|
|
373
|
-
`[testCreateSubDocuments] Sensitive info document created directly`
|
|
374
|
-
);
|
|
353
|
+
console.log(`[testCreateSubDocuments] Sensitive info document created directly`);
|
|
375
354
|
|
|
376
355
|
// Test medical info creation
|
|
377
356
|
console.log(`[testCreateSubDocuments] Testing medical info creation`);
|
|
378
357
|
const medicalInfoRef = getMedicalInfoDocRef(db, patientId);
|
|
379
|
-
console.log(
|
|
380
|
-
`[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`
|
|
381
|
-
);
|
|
358
|
+
console.log(`[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`);
|
|
382
359
|
|
|
383
360
|
const defaultMedicalInfo = {
|
|
384
361
|
...DEFAULT_MEDICAL_INFO,
|
|
@@ -388,9 +365,7 @@ export const testCreateSubDocuments = async (
|
|
|
388
365
|
};
|
|
389
366
|
|
|
390
367
|
await setDoc(medicalInfoRef, defaultMedicalInfo);
|
|
391
|
-
console.log(
|
|
392
|
-
`[testCreateSubDocuments] Medical info document created directly`
|
|
393
|
-
);
|
|
368
|
+
console.log(`[testCreateSubDocuments] Medical info document created directly`);
|
|
394
369
|
|
|
395
370
|
console.log(`[testCreateSubDocuments] Test completed successfully`);
|
|
396
371
|
} catch (error) {
|
|
@@ -398,3 +373,140 @@ export const testCreateSubDocuments = async (
|
|
|
398
373
|
throw error;
|
|
399
374
|
}
|
|
400
375
|
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Searches for patient profiles based on clinic and/or practitioner association.
|
|
379
|
+
* Applies security checks based on the requester's role and associations.
|
|
380
|
+
*
|
|
381
|
+
* @param {Firestore} db - Firestore instance.
|
|
382
|
+
* @param {SearchPatientsParams} params - Search criteria (clinicId, practitionerId).
|
|
383
|
+
* @param {RequesterInfo} requester - Information about the user performing the search.
|
|
384
|
+
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of matching patient profiles.
|
|
385
|
+
*/
|
|
386
|
+
export const searchPatientsUtil = async (
|
|
387
|
+
db: Firestore,
|
|
388
|
+
params: SearchPatientsParams,
|
|
389
|
+
requester: RequesterInfo,
|
|
390
|
+
): Promise<PatientProfile[]> => {
|
|
391
|
+
// Validate input
|
|
392
|
+
searchPatientsSchema.parse(params);
|
|
393
|
+
requesterInfoSchema.parse(requester);
|
|
394
|
+
|
|
395
|
+
const constraints: QueryConstraint[] = [];
|
|
396
|
+
const patientsCollectionRef = collection(db, PATIENTS_COLLECTION);
|
|
397
|
+
|
|
398
|
+
// --- Security Checks & Initial Filtering ---
|
|
399
|
+
|
|
400
|
+
if (requester.role === 'clinic_admin') {
|
|
401
|
+
// Clinic admin can only search within their own clinic
|
|
402
|
+
if (!requester.associatedClinicId) {
|
|
403
|
+
throw new Error('Associated clinic ID is required for clinic admin search.');
|
|
404
|
+
}
|
|
405
|
+
// If the search params specify a different clinic, it's an invalid request for this admin.
|
|
406
|
+
if (params.clinicId && params.clinicId !== requester.associatedClinicId) {
|
|
407
|
+
console.warn(
|
|
408
|
+
`Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`,
|
|
409
|
+
);
|
|
410
|
+
return []; // Or throw an error
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// **Mandatory filter**: Ensure patients belong to the admin's clinic.
|
|
414
|
+
constraints.push(where('clinicIds', 'array-contains', requester.associatedClinicId));
|
|
415
|
+
|
|
416
|
+
// Optional filter: If practitionerId is also provided, filter by that practitioner *within the admin's clinic*.
|
|
417
|
+
if (params.practitionerId) {
|
|
418
|
+
constraints.push(where('doctorIds', 'array-contains', params.practitionerId));
|
|
419
|
+
// We might need an additional check here if the practitioner MUST work at the admin's clinic.
|
|
420
|
+
// This would require fetching practitioner data or having denormalized clinic IDs on the practitioner.
|
|
421
|
+
}
|
|
422
|
+
} else if (requester.role === 'practitioner') {
|
|
423
|
+
// Practitioner can only search for their own patients.
|
|
424
|
+
if (!requester.associatedPractitionerId) {
|
|
425
|
+
throw new Error('Associated practitioner ID is required for practitioner search.');
|
|
426
|
+
}
|
|
427
|
+
// If the search params specify a different practitioner, it's invalid.
|
|
428
|
+
if (params.practitionerId && params.practitionerId !== requester.associatedPractitionerId) {
|
|
429
|
+
console.warn(
|
|
430
|
+
`Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`,
|
|
431
|
+
);
|
|
432
|
+
return []; // Or throw an error
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// **Mandatory filter**: Ensure patients are associated with this practitioner.
|
|
436
|
+
constraints.push(where('doctorIds', 'array-contains', requester.associatedPractitionerId));
|
|
437
|
+
|
|
438
|
+
// Optional filter: If clinicId is provided, filter by patients of this practitioner *at that specific clinic*.
|
|
439
|
+
if (params.clinicId) {
|
|
440
|
+
constraints.push(where('clinicIds', 'array-contains', params.clinicId));
|
|
441
|
+
// Similar to above, we might need to check if the practitioner actually works at this clinic.
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
// Should not happen due to validation, but good practice to handle.
|
|
445
|
+
throw new Error('Invalid requester role.');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// --- Execute Query ---
|
|
449
|
+
try {
|
|
450
|
+
const finalQuery = query(patientsCollectionRef, ...constraints);
|
|
451
|
+
const querySnapshot = await getDocs(finalQuery);
|
|
452
|
+
|
|
453
|
+
const patients = querySnapshot.docs.map((doc) => doc.data() as PatientProfile);
|
|
454
|
+
|
|
455
|
+
console.log(`[searchPatientsUtil] Found ${patients.length} patients matching criteria.`);
|
|
456
|
+
return patients;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error('[searchPatientsUtil] Error searching patients:', error);
|
|
459
|
+
// Consider logging more details or re-throwing a specific error type
|
|
460
|
+
return []; // Return empty array on error
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Retrieves all patient profiles from the database.
|
|
466
|
+
*
|
|
467
|
+
* @param {Firestore} db - Firestore instance
|
|
468
|
+
* @param {Object} options - Optional parameters for pagination
|
|
469
|
+
* @param {number} options.limit - Maximum number of profiles to return
|
|
470
|
+
* @param {string} options.startAfter - The ID of the document to start after (for pagination)
|
|
471
|
+
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of all patient profiles
|
|
472
|
+
*/
|
|
473
|
+
export const getAllPatientsUtil = async (
|
|
474
|
+
db: Firestore,
|
|
475
|
+
options?: { limit?: number; startAfter?: string },
|
|
476
|
+
): Promise<PatientProfile[]> => {
|
|
477
|
+
try {
|
|
478
|
+
console.log(`[getAllPatientsUtil] Fetching patients with options:`, options);
|
|
479
|
+
|
|
480
|
+
const patientsCollection = collection(db, PATIENTS_COLLECTION);
|
|
481
|
+
|
|
482
|
+
let q = query(patientsCollection);
|
|
483
|
+
|
|
484
|
+
// Apply pagination if needed
|
|
485
|
+
if (options?.limit) {
|
|
486
|
+
q = query(q, limit(options.limit));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// If startAfter is provided, get that document and use it for pagination
|
|
490
|
+
if (options?.startAfter) {
|
|
491
|
+
const startAfterDoc = await getDoc(doc(db, PATIENTS_COLLECTION, options.startAfter));
|
|
492
|
+
if (startAfterDoc.exists()) {
|
|
493
|
+
q = query(q, startAfter(startAfterDoc));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const patientsSnapshot = await getDocs(q);
|
|
498
|
+
|
|
499
|
+
const patients: PatientProfile[] = [];
|
|
500
|
+
patientsSnapshot.forEach((doc) => {
|
|
501
|
+
patients.push(doc.data() as PatientProfile);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
console.log(`[getAllPatientsUtil] Found ${patients.length} patients`);
|
|
505
|
+
return patients;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
console.error(`[getAllPatientsUtil] Error fetching patients:`, error);
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Failed to retrieve patients: ${error instanceof Error ? error.message : String(error)}`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
@@ -185,3 +185,44 @@ export interface UpdateAppointmentParams {
|
|
|
185
185
|
* Collection names for calendar
|
|
186
186
|
*/
|
|
187
187
|
export const CALENDAR_COLLECTION = "calendar";
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Enum for specifying the primary search location for calendar events
|
|
191
|
+
*/
|
|
192
|
+
export enum SearchLocationEnum {
|
|
193
|
+
PRACTITIONER = "practitioner",
|
|
194
|
+
PATIENT = "patient",
|
|
195
|
+
CLINIC = "clinic", // Represents searching events associated with a clinic
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Interface for defining a date range
|
|
200
|
+
*/
|
|
201
|
+
export interface DateRange {
|
|
202
|
+
start: Timestamp;
|
|
203
|
+
end: Timestamp;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Interface for general calendar event search parameters
|
|
208
|
+
*/
|
|
209
|
+
export interface SearchCalendarEventsParams {
|
|
210
|
+
/** The primary location to search within (practitioner, patient, or clinic calendar). */
|
|
211
|
+
searchLocation: SearchLocationEnum;
|
|
212
|
+
/** The ID of the entity (practitioner, patient, or clinic) whose calendar/events are being searched. */
|
|
213
|
+
entityId: string;
|
|
214
|
+
/** Optional filter for clinic ID. If searchLocation is CLINIC, this is implicitly applied using entityId. */
|
|
215
|
+
clinicId?: string;
|
|
216
|
+
/** Optional filter for practitioner ID. */
|
|
217
|
+
practitionerId?: string;
|
|
218
|
+
/** Optional filter for patient ID. */
|
|
219
|
+
patientId?: string;
|
|
220
|
+
/** Optional filter for procedure ID. */
|
|
221
|
+
procedureId?: string;
|
|
222
|
+
/** Optional filter for a specific date range (based on event start time). */
|
|
223
|
+
dateRange?: DateRange;
|
|
224
|
+
/** Optional filter for event status. */
|
|
225
|
+
eventStatus?: CalendarEventStatus;
|
|
226
|
+
/** Optional filter for event type. */
|
|
227
|
+
eventType?: CalendarEventType;
|
|
228
|
+
}
|
|
@@ -172,6 +172,8 @@ export interface PatientProfile {
|
|
|
172
172
|
dateOfBirth?: Timestamp | null;
|
|
173
173
|
doctors: PatientDoctor[]; // Lista doktora pacijenta
|
|
174
174
|
clinics: PatientClinic[]; // Lista klinika pacijenta
|
|
175
|
+
doctorIds: string[]; // Denormalized array for querying
|
|
176
|
+
clinicIds: string[]; // Denormalized array for querying
|
|
175
177
|
createdAt: Timestamp;
|
|
176
178
|
updatedAt: Timestamp;
|
|
177
179
|
}
|
|
@@ -188,14 +190,42 @@ export interface CreatePatientProfileData {
|
|
|
188
190
|
isVerified: boolean;
|
|
189
191
|
doctors?: PatientDoctor[];
|
|
190
192
|
clinics?: PatientClinic[];
|
|
193
|
+
doctorIds?: string[]; // Initialize as empty or with initial doctors
|
|
194
|
+
clinicIds?: string[]; // Initialize as empty or with initial clinics
|
|
191
195
|
}
|
|
192
196
|
|
|
193
197
|
/**
|
|
194
198
|
* Tip za ažuriranje Patient profila
|
|
195
199
|
*/
|
|
196
200
|
export interface UpdatePatientProfileData
|
|
197
|
-
extends Partial<
|
|
201
|
+
extends Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">> {
|
|
202
|
+
// Use Omit to exclude base fields
|
|
198
203
|
updatedAt?: FieldValue;
|
|
204
|
+
// Note: doctors, clinics, doctorIds, clinicIds should ideally be updated via specific methods (add/removeDoctor/Clinic)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parameters for searching patient profiles.
|
|
209
|
+
*/
|
|
210
|
+
export interface SearchPatientsParams {
|
|
211
|
+
/** Optional: Filter patients associated with this clinic ID. */
|
|
212
|
+
clinicId?: string;
|
|
213
|
+
/** Optional: Filter patients associated with this practitioner ID. */
|
|
214
|
+
practitionerId?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Information about the entity requesting the patient search.
|
|
219
|
+
*/
|
|
220
|
+
export interface RequesterInfo {
|
|
221
|
+
/** ID of the clinic admin user or practitioner user making the request. */
|
|
222
|
+
id: string;
|
|
223
|
+
/** Role of the requester, determining the search context. */
|
|
224
|
+
role: "clinic_admin" | "practitioner";
|
|
225
|
+
/** If role is 'clinic_admin', this is the associated clinic ID. */
|
|
226
|
+
associatedClinicId?: string;
|
|
227
|
+
/** If role is 'practitioner', this is the associated practitioner profile ID. */
|
|
228
|
+
associatedPractitionerId?: string;
|
|
199
229
|
}
|
|
200
230
|
|
|
201
231
|
export * from "./medical-info.types";
|