@blackcode_sa/metaestetics-api 1.12.61 → 1.12.62
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 +2 -0
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.js +45 -4
- package/dist/admin/index.mjs +45 -4
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +53 -11
- package/dist/index.mjs +53 -11
- package/package.json +119 -119
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +75 -75
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +40 -40
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +318 -318
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +8 -8
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +395 -395
- package/src/backoffice/services/technology.service.ts +1070 -1070
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +62 -62
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +161 -161
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +163 -163
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2082 -2082
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +13 -13
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +1682 -1682
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +683 -636
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/appointment/index.ts +453 -453
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +489 -489
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +44 -44
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +265 -265
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +273 -273
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -130
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +493 -493
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +216 -216
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -189
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,510 +1,510 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getDoc,
|
|
3
|
-
setDoc,
|
|
4
|
-
updateDoc,
|
|
5
|
-
arrayUnion,
|
|
6
|
-
arrayRemove,
|
|
7
|
-
serverTimestamp,
|
|
8
|
-
increment,
|
|
9
|
-
Firestore,
|
|
10
|
-
Timestamp,
|
|
11
|
-
collection,
|
|
12
|
-
query,
|
|
13
|
-
where,
|
|
14
|
-
getDocs,
|
|
15
|
-
QueryConstraint,
|
|
16
|
-
limit,
|
|
17
|
-
startAfter,
|
|
18
|
-
doc,
|
|
19
|
-
} from "firebase/firestore";
|
|
20
|
-
import { z } from "zod";
|
|
21
|
-
import {
|
|
22
|
-
PatientProfile,
|
|
23
|
-
CreatePatientProfileData,
|
|
24
|
-
Gender,
|
|
25
|
-
SearchPatientsParams,
|
|
26
|
-
RequesterInfo,
|
|
27
|
-
PATIENTS_COLLECTION,
|
|
28
|
-
} from "../../../types/patient";
|
|
29
|
-
import {
|
|
30
|
-
patientProfileSchema,
|
|
31
|
-
createPatientProfileSchema,
|
|
32
|
-
searchPatientsSchema,
|
|
33
|
-
requesterInfoSchema,
|
|
34
|
-
} from "../../../validations/patient.schema";
|
|
35
|
-
import {
|
|
36
|
-
getPatientDocRef,
|
|
37
|
-
getPatientDocRefByUserRef,
|
|
38
|
-
initSensitiveInfoDocIfNotExists,
|
|
39
|
-
getSensitiveInfoDocRef,
|
|
40
|
-
getMedicalInfoDocRef,
|
|
41
|
-
} from "./docs.utils";
|
|
42
|
-
import { ensureMedicalInfoExists } from "./medical.utils";
|
|
43
|
-
import { DEFAULT_MEDICAL_INFO } from "../../../types/patient/medical-info.types";
|
|
44
|
-
|
|
45
|
-
// Funkcije za rad sa profilom
|
|
46
|
-
export const createPatientProfileUtil = async (
|
|
47
|
-
db: Firestore,
|
|
48
|
-
data: CreatePatientProfileData,
|
|
49
|
-
generateId: () => string
|
|
50
|
-
): Promise<PatientProfile> => {
|
|
51
|
-
try {
|
|
52
|
-
console.log("[createPatientProfileUtil] Starting patient profile creation");
|
|
53
|
-
const validatedData = createPatientProfileSchema.parse(data);
|
|
54
|
-
|
|
55
|
-
// This utility is for creating standard profiles, so userRef is required here.
|
|
56
|
-
if (!validatedData.userRef) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
"userRef is required to create a standard patient profile."
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const patientId = generateId();
|
|
63
|
-
console.log(`[createPatientProfileUtil] Generated patientId: ${patientId}`);
|
|
64
|
-
|
|
65
|
-
const patientData: Omit<PatientProfile, "createdAt" | "updatedAt"> & {
|
|
66
|
-
createdAt: ReturnType<typeof serverTimestamp>;
|
|
67
|
-
updatedAt: ReturnType<typeof serverTimestamp>;
|
|
68
|
-
} = {
|
|
69
|
-
id: patientId,
|
|
70
|
-
userRef: validatedData.userRef,
|
|
71
|
-
displayName: validatedData.displayName,
|
|
72
|
-
expoTokens: validatedData.expoTokens,
|
|
73
|
-
gamification: validatedData.gamification || {
|
|
74
|
-
level: 1,
|
|
75
|
-
points: 0,
|
|
76
|
-
},
|
|
77
|
-
isActive: validatedData.isActive,
|
|
78
|
-
isVerified: validatedData.isVerified,
|
|
79
|
-
isManual: validatedData.isManual,
|
|
80
|
-
doctors: validatedData.doctors || [],
|
|
81
|
-
clinics: validatedData.clinics || [],
|
|
82
|
-
doctorIds: validatedData.doctors?.map((d) => d.userRef) || [],
|
|
83
|
-
clinicIds: validatedData.clinics?.map((c) => c.clinicId) || [],
|
|
84
|
-
createdAt: serverTimestamp(),
|
|
85
|
-
updatedAt: serverTimestamp(),
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
patientProfileSchema.parse({
|
|
89
|
-
...patientData,
|
|
90
|
-
createdAt: Timestamp.now(),
|
|
91
|
-
updatedAt: Timestamp.now(),
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
await setDoc(getPatientDocRef(db, patientId), patientData);
|
|
95
|
-
|
|
96
|
-
// Create blank sensitive info document using the utility function
|
|
97
|
-
console.log(`[createPatientProfileUtil] Creating sensitive info document`);
|
|
98
|
-
let sensitiveInfoSuccess = false;
|
|
99
|
-
try {
|
|
100
|
-
const sensitiveInfoCreated = await initSensitiveInfoDocIfNotExists(
|
|
101
|
-
db,
|
|
102
|
-
patientId,
|
|
103
|
-
validatedData.userRef
|
|
104
|
-
);
|
|
105
|
-
console.log(
|
|
106
|
-
`[createPatientProfileUtil] Sensitive info document creation result: ${
|
|
107
|
-
sensitiveInfoCreated
|
|
108
|
-
? "Document already existed"
|
|
109
|
-
: "New document created"
|
|
110
|
-
}`
|
|
111
|
-
);
|
|
112
|
-
sensitiveInfoSuccess = true;
|
|
113
|
-
} catch (sensitiveError) {
|
|
114
|
-
console.error(
|
|
115
|
-
`[createPatientProfileUtil] Error creating sensitive info:`,
|
|
116
|
-
sensitiveError
|
|
117
|
-
);
|
|
118
|
-
// Don't throw the error, we'll try the direct method
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Create blank medical info document using the utility function
|
|
122
|
-
console.log(`[createPatientProfileUtil] Creating medical info document`);
|
|
123
|
-
let medicalInfoSuccess = false;
|
|
124
|
-
try {
|
|
125
|
-
await ensureMedicalInfoExists(db, patientId, validatedData.userRef);
|
|
126
|
-
console.log(
|
|
127
|
-
`[createPatientProfileUtil] Medical info document created successfully`
|
|
128
|
-
);
|
|
129
|
-
medicalInfoSuccess = true;
|
|
130
|
-
} catch (medicalError) {
|
|
131
|
-
console.error(
|
|
132
|
-
`[createPatientProfileUtil] Error creating medical info:`,
|
|
133
|
-
medicalError
|
|
134
|
-
);
|
|
135
|
-
// Don't throw the error, we'll try the direct method
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// If either utility function failed, try the direct method
|
|
139
|
-
if (!sensitiveInfoSuccess || !medicalInfoSuccess) {
|
|
140
|
-
console.log(
|
|
141
|
-
`[createPatientProfileUtil] Using fallback method to create documents`
|
|
142
|
-
);
|
|
143
|
-
try {
|
|
144
|
-
await testCreateSubDocuments(db, patientId, validatedData.userRef);
|
|
145
|
-
console.log(
|
|
146
|
-
`[createPatientProfileUtil] Fallback method completed successfully`
|
|
147
|
-
);
|
|
148
|
-
} catch (fallbackError) {
|
|
149
|
-
console.error(
|
|
150
|
-
`[createPatientProfileUtil] Fallback method failed:`,
|
|
151
|
-
fallbackError
|
|
152
|
-
);
|
|
153
|
-
// Still continue to return the patient profile
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
console.log(`[createPatientProfileUtil] Verifying patient document exists`);
|
|
158
|
-
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
159
|
-
if (!patientDoc.exists()) {
|
|
160
|
-
console.error(
|
|
161
|
-
`[createPatientProfileUtil] Patient document not found after creation`
|
|
162
|
-
);
|
|
163
|
-
throw new Error("Failed to create patient profile");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
console.log(
|
|
167
|
-
`[createPatientProfileUtil] Patient profile creation completed successfully`
|
|
168
|
-
);
|
|
169
|
-
return patientDoc.data() as PatientProfile;
|
|
170
|
-
} catch (error) {
|
|
171
|
-
console.error(
|
|
172
|
-
`[createPatientProfileUtil] Error in patient profile creation:`,
|
|
173
|
-
error
|
|
174
|
-
);
|
|
175
|
-
if (error instanceof z.ZodError) {
|
|
176
|
-
throw new Error("Invalid patient data: " + error.message);
|
|
177
|
-
}
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
export const getPatientProfileUtil = async (
|
|
183
|
-
db: Firestore,
|
|
184
|
-
patientId: string
|
|
185
|
-
): Promise<PatientProfile | null> => {
|
|
186
|
-
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
187
|
-
return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
export const getPatientProfileByUserRefUtil = async (
|
|
191
|
-
db: Firestore,
|
|
192
|
-
userRef: string
|
|
193
|
-
): Promise<PatientProfile | null> => {
|
|
194
|
-
try {
|
|
195
|
-
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
196
|
-
const patientDoc = await getDoc(docRef);
|
|
197
|
-
return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
|
|
198
|
-
} catch (error) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// Pomoćne funkcije za tokene i poene
|
|
204
|
-
export const addExpoTokenUtil = async (
|
|
205
|
-
db: Firestore,
|
|
206
|
-
patientId: string,
|
|
207
|
-
token: string
|
|
208
|
-
): Promise<void> => {
|
|
209
|
-
await updateDoc(getPatientDocRef(db, patientId), {
|
|
210
|
-
expoTokens: arrayUnion(token),
|
|
211
|
-
updatedAt: serverTimestamp(),
|
|
212
|
-
});
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
export const removeExpoTokenUtil = async (
|
|
216
|
-
db: Firestore,
|
|
217
|
-
patientId: string,
|
|
218
|
-
token: string
|
|
219
|
-
): Promise<void> => {
|
|
220
|
-
await updateDoc(getPatientDocRef(db, patientId), {
|
|
221
|
-
expoTokens: arrayRemove(token),
|
|
222
|
-
updatedAt: serverTimestamp(),
|
|
223
|
-
});
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
export const addPointsUtil = async (
|
|
227
|
-
db: Firestore,
|
|
228
|
-
patientId: string,
|
|
229
|
-
points: number
|
|
230
|
-
): Promise<void> => {
|
|
231
|
-
await updateDoc(getPatientDocRef(db, patientId), {
|
|
232
|
-
"gamification.points": increment(points),
|
|
233
|
-
updatedAt: serverTimestamp(),
|
|
234
|
-
});
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
export const updatePatientProfileUtil = async (
|
|
238
|
-
db: Firestore,
|
|
239
|
-
patientId: string,
|
|
240
|
-
data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
|
|
241
|
-
): Promise<PatientProfile> => {
|
|
242
|
-
try {
|
|
243
|
-
const updateData = {
|
|
244
|
-
...data,
|
|
245
|
-
updatedAt: serverTimestamp(),
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
await updateDoc(getPatientDocRef(db, patientId), updateData);
|
|
249
|
-
|
|
250
|
-
const updatedDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
251
|
-
if (!updatedDoc.exists()) {
|
|
252
|
-
throw new Error("Patient profile not found after update");
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return updatedDoc.data() as PatientProfile;
|
|
256
|
-
} catch (error: unknown) {
|
|
257
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
258
|
-
throw new Error(`Failed to update patient profile: ${errorMessage}`);
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
export const updatePatientProfileByUserRefUtil = async (
|
|
263
|
-
db: Firestore,
|
|
264
|
-
userRef: string,
|
|
265
|
-
data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
|
|
266
|
-
): Promise<PatientProfile> => {
|
|
267
|
-
try {
|
|
268
|
-
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
269
|
-
const patientDoc = await getDoc(docRef);
|
|
270
|
-
|
|
271
|
-
if (!patientDoc.exists()) {
|
|
272
|
-
throw new Error("Patient profile not found");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const patientData = patientDoc.data() as PatientProfile;
|
|
276
|
-
return updatePatientProfileUtil(db, patientData.id, data);
|
|
277
|
-
} catch (error: unknown) {
|
|
278
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
279
|
-
throw new Error(
|
|
280
|
-
`Failed to update patient profile by user ref: ${errorMessage}`
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Test function to directly create sensitive and medical info documents
|
|
287
|
-
* This is for debugging purposes only
|
|
288
|
-
*/
|
|
289
|
-
export const testCreateSubDocuments = async (
|
|
290
|
-
db: Firestore,
|
|
291
|
-
patientId: string,
|
|
292
|
-
userRef: string
|
|
293
|
-
): Promise<void> => {
|
|
294
|
-
console.log(
|
|
295
|
-
`[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}`
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
// Test sensitive info creation
|
|
300
|
-
console.log(`[testCreateSubDocuments] Testing sensitive info creation`);
|
|
301
|
-
const sensitiveInfoRef = getSensitiveInfoDocRef(db, patientId);
|
|
302
|
-
console.log(
|
|
303
|
-
`[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
const defaultSensitiveInfo = {
|
|
307
|
-
patientId,
|
|
308
|
-
userRef,
|
|
309
|
-
photoUrl: "",
|
|
310
|
-
firstName: "Name",
|
|
311
|
-
lastName: "Surname",
|
|
312
|
-
dateOfBirth: Timestamp.now(),
|
|
313
|
-
gender: Gender.PREFER_NOT_TO_SAY,
|
|
314
|
-
email: "test@example.com",
|
|
315
|
-
phoneNumber: "",
|
|
316
|
-
createdAt: Timestamp.now(),
|
|
317
|
-
updatedAt: Timestamp.now(),
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
await setDoc(sensitiveInfoRef, defaultSensitiveInfo);
|
|
321
|
-
console.log(
|
|
322
|
-
`[testCreateSubDocuments] Sensitive info document created directly`
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
// Test medical info creation
|
|
326
|
-
console.log(`[testCreateSubDocuments] Testing medical info creation`);
|
|
327
|
-
const medicalInfoRef = getMedicalInfoDocRef(db, patientId);
|
|
328
|
-
console.log(
|
|
329
|
-
`[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
const defaultMedicalInfo = {
|
|
333
|
-
...DEFAULT_MEDICAL_INFO,
|
|
334
|
-
patientId,
|
|
335
|
-
lastUpdated: Timestamp.now(),
|
|
336
|
-
updatedBy: userRef,
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
await setDoc(medicalInfoRef, defaultMedicalInfo);
|
|
340
|
-
console.log(
|
|
341
|
-
`[testCreateSubDocuments] Medical info document created directly`
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
console.log(`[testCreateSubDocuments] Test completed successfully`);
|
|
345
|
-
} catch (error) {
|
|
346
|
-
console.error(`[testCreateSubDocuments] Error:`, error);
|
|
347
|
-
throw error;
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Searches for patient profiles based on clinic and/or practitioner association.
|
|
353
|
-
* Applies security checks based on the requester's role and associations.
|
|
354
|
-
*
|
|
355
|
-
* @param {Firestore} db - Firestore instance.
|
|
356
|
-
* @param {SearchPatientsParams} params - Search criteria (clinicId, practitionerId).
|
|
357
|
-
* @param {RequesterInfo} requester - Information about the user performing the search.
|
|
358
|
-
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of matching patient profiles.
|
|
359
|
-
*/
|
|
360
|
-
export const searchPatientsUtil = async (
|
|
361
|
-
db: Firestore,
|
|
362
|
-
params: SearchPatientsParams,
|
|
363
|
-
requester: RequesterInfo
|
|
364
|
-
): Promise<PatientProfile[]> => {
|
|
365
|
-
// Validate input
|
|
366
|
-
searchPatientsSchema.parse(params);
|
|
367
|
-
requesterInfoSchema.parse(requester);
|
|
368
|
-
|
|
369
|
-
const constraints: QueryConstraint[] = [];
|
|
370
|
-
const patientsCollectionRef = collection(db, PATIENTS_COLLECTION);
|
|
371
|
-
|
|
372
|
-
// --- Security Checks & Initial Filtering ---
|
|
373
|
-
|
|
374
|
-
if (requester.role === "clinic_admin") {
|
|
375
|
-
// Clinic admin can only search within their own clinic
|
|
376
|
-
if (!requester.associatedClinicId) {
|
|
377
|
-
throw new Error(
|
|
378
|
-
"Associated clinic ID is required for clinic admin search."
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
// If the search params specify a different clinic, it's an invalid request for this admin.
|
|
382
|
-
if (params.clinicId && params.clinicId !== requester.associatedClinicId) {
|
|
383
|
-
console.warn(
|
|
384
|
-
`Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`
|
|
385
|
-
);
|
|
386
|
-
return []; // Or throw an error
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
// **Mandatory filter**: Ensure patients belong to the admin's clinic.
|
|
390
|
-
constraints.push(
|
|
391
|
-
where("clinicIds", "array-contains", requester.associatedClinicId)
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
// Optional filter: If practitionerId is also provided, filter by that practitioner *within the admin's clinic*.
|
|
395
|
-
if (params.practitionerId) {
|
|
396
|
-
constraints.push(
|
|
397
|
-
where("doctorIds", "array-contains", params.practitionerId)
|
|
398
|
-
);
|
|
399
|
-
// We might need an additional check here if the practitioner MUST work at the admin's clinic.
|
|
400
|
-
// This would require fetching practitioner data or having denormalized clinic IDs on the practitioner.
|
|
401
|
-
}
|
|
402
|
-
} else if (requester.role === "practitioner") {
|
|
403
|
-
// Practitioner can only search for their own patients.
|
|
404
|
-
if (!requester.associatedPractitionerId) {
|
|
405
|
-
throw new Error(
|
|
406
|
-
"Associated practitioner ID is required for practitioner search."
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
// If the search params specify a different practitioner, it's invalid.
|
|
410
|
-
if (
|
|
411
|
-
params.practitionerId &&
|
|
412
|
-
params.practitionerId !== requester.associatedPractitionerId
|
|
413
|
-
) {
|
|
414
|
-
console.warn(
|
|
415
|
-
`Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`
|
|
416
|
-
);
|
|
417
|
-
return []; // Or throw an error
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// **Mandatory filter**: Ensure patients are associated with this practitioner.
|
|
421
|
-
constraints.push(
|
|
422
|
-
where("doctorIds", "array-contains", requester.associatedPractitionerId)
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
// Optional filter: If clinicId is provided, filter by patients of this practitioner *at that specific clinic*.
|
|
426
|
-
if (params.clinicId) {
|
|
427
|
-
constraints.push(where("clinicIds", "array-contains", params.clinicId));
|
|
428
|
-
// Similar to above, we might need to check if the practitioner actually works at this clinic.
|
|
429
|
-
}
|
|
430
|
-
} else {
|
|
431
|
-
// Should not happen due to validation, but good practice to handle.
|
|
432
|
-
throw new Error("Invalid requester role.");
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// --- Execute Query ---
|
|
436
|
-
try {
|
|
437
|
-
const finalQuery = query(patientsCollectionRef, ...constraints);
|
|
438
|
-
const querySnapshot = await getDocs(finalQuery);
|
|
439
|
-
|
|
440
|
-
const patients = querySnapshot.docs.map(
|
|
441
|
-
(doc) => doc.data() as PatientProfile
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
console.log(
|
|
445
|
-
`[searchPatientsUtil] Found ${patients.length} patients matching criteria.`
|
|
446
|
-
);
|
|
447
|
-
return patients;
|
|
448
|
-
} catch (error) {
|
|
449
|
-
console.error("[searchPatientsUtil] Error searching patients:", error);
|
|
450
|
-
// Consider logging more details or re-throwing a specific error type
|
|
451
|
-
return []; // Return empty array on error
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Retrieves all patient profiles from the database.
|
|
457
|
-
*
|
|
458
|
-
* @param {Firestore} db - Firestore instance
|
|
459
|
-
* @param {Object} options - Optional parameters for pagination
|
|
460
|
-
* @param {number} options.limit - Maximum number of profiles to return
|
|
461
|
-
* @param {string} options.startAfter - The ID of the document to start after (for pagination)
|
|
462
|
-
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of all patient profiles
|
|
463
|
-
*/
|
|
464
|
-
export const getAllPatientsUtil = async (
|
|
465
|
-
db: Firestore,
|
|
466
|
-
options?: { limit?: number; startAfter?: string }
|
|
467
|
-
): Promise<PatientProfile[]> => {
|
|
468
|
-
try {
|
|
469
|
-
console.log(
|
|
470
|
-
`[getAllPatientsUtil] Fetching patients with options:`,
|
|
471
|
-
options
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
const patientsCollection = collection(db, PATIENTS_COLLECTION);
|
|
475
|
-
|
|
476
|
-
let q = query(patientsCollection);
|
|
477
|
-
|
|
478
|
-
// Apply pagination if needed
|
|
479
|
-
if (options?.limit) {
|
|
480
|
-
q = query(q, limit(options.limit));
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// If startAfter is provided, get that document and use it for pagination
|
|
484
|
-
if (options?.startAfter) {
|
|
485
|
-
const startAfterDoc = await getDoc(
|
|
486
|
-
doc(db, PATIENTS_COLLECTION, options.startAfter)
|
|
487
|
-
);
|
|
488
|
-
if (startAfterDoc.exists()) {
|
|
489
|
-
q = query(q, startAfter(startAfterDoc));
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const patientsSnapshot = await getDocs(q);
|
|
494
|
-
|
|
495
|
-
const patients: PatientProfile[] = [];
|
|
496
|
-
patientsSnapshot.forEach((doc) => {
|
|
497
|
-
patients.push(doc.data() as PatientProfile);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
console.log(`[getAllPatientsUtil] Found ${patients.length} patients`);
|
|
501
|
-
return patients;
|
|
502
|
-
} catch (error) {
|
|
503
|
-
console.error(`[getAllPatientsUtil] Error fetching patients:`, error);
|
|
504
|
-
throw new Error(
|
|
505
|
-
`Failed to retrieve patients: ${
|
|
506
|
-
error instanceof Error ? error.message : String(error)
|
|
507
|
-
}`
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
getDoc,
|
|
3
|
+
setDoc,
|
|
4
|
+
updateDoc,
|
|
5
|
+
arrayUnion,
|
|
6
|
+
arrayRemove,
|
|
7
|
+
serverTimestamp,
|
|
8
|
+
increment,
|
|
9
|
+
Firestore,
|
|
10
|
+
Timestamp,
|
|
11
|
+
collection,
|
|
12
|
+
query,
|
|
13
|
+
where,
|
|
14
|
+
getDocs,
|
|
15
|
+
QueryConstraint,
|
|
16
|
+
limit,
|
|
17
|
+
startAfter,
|
|
18
|
+
doc,
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import {
|
|
22
|
+
PatientProfile,
|
|
23
|
+
CreatePatientProfileData,
|
|
24
|
+
Gender,
|
|
25
|
+
SearchPatientsParams,
|
|
26
|
+
RequesterInfo,
|
|
27
|
+
PATIENTS_COLLECTION,
|
|
28
|
+
} from "../../../types/patient";
|
|
29
|
+
import {
|
|
30
|
+
patientProfileSchema,
|
|
31
|
+
createPatientProfileSchema,
|
|
32
|
+
searchPatientsSchema,
|
|
33
|
+
requesterInfoSchema,
|
|
34
|
+
} from "../../../validations/patient.schema";
|
|
35
|
+
import {
|
|
36
|
+
getPatientDocRef,
|
|
37
|
+
getPatientDocRefByUserRef,
|
|
38
|
+
initSensitiveInfoDocIfNotExists,
|
|
39
|
+
getSensitiveInfoDocRef,
|
|
40
|
+
getMedicalInfoDocRef,
|
|
41
|
+
} from "./docs.utils";
|
|
42
|
+
import { ensureMedicalInfoExists } from "./medical.utils";
|
|
43
|
+
import { DEFAULT_MEDICAL_INFO } from "../../../types/patient/medical-info.types";
|
|
44
|
+
|
|
45
|
+
// Funkcije za rad sa profilom
|
|
46
|
+
export const createPatientProfileUtil = async (
|
|
47
|
+
db: Firestore,
|
|
48
|
+
data: CreatePatientProfileData,
|
|
49
|
+
generateId: () => string
|
|
50
|
+
): Promise<PatientProfile> => {
|
|
51
|
+
try {
|
|
52
|
+
console.log("[createPatientProfileUtil] Starting patient profile creation");
|
|
53
|
+
const validatedData = createPatientProfileSchema.parse(data);
|
|
54
|
+
|
|
55
|
+
// This utility is for creating standard profiles, so userRef is required here.
|
|
56
|
+
if (!validatedData.userRef) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
"userRef is required to create a standard patient profile."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const patientId = generateId();
|
|
63
|
+
console.log(`[createPatientProfileUtil] Generated patientId: ${patientId}`);
|
|
64
|
+
|
|
65
|
+
const patientData: Omit<PatientProfile, "createdAt" | "updatedAt"> & {
|
|
66
|
+
createdAt: ReturnType<typeof serverTimestamp>;
|
|
67
|
+
updatedAt: ReturnType<typeof serverTimestamp>;
|
|
68
|
+
} = {
|
|
69
|
+
id: patientId,
|
|
70
|
+
userRef: validatedData.userRef,
|
|
71
|
+
displayName: validatedData.displayName,
|
|
72
|
+
expoTokens: validatedData.expoTokens,
|
|
73
|
+
gamification: validatedData.gamification || {
|
|
74
|
+
level: 1,
|
|
75
|
+
points: 0,
|
|
76
|
+
},
|
|
77
|
+
isActive: validatedData.isActive,
|
|
78
|
+
isVerified: validatedData.isVerified,
|
|
79
|
+
isManual: validatedData.isManual,
|
|
80
|
+
doctors: validatedData.doctors || [],
|
|
81
|
+
clinics: validatedData.clinics || [],
|
|
82
|
+
doctorIds: validatedData.doctors?.map((d) => d.userRef) || [],
|
|
83
|
+
clinicIds: validatedData.clinics?.map((c) => c.clinicId) || [],
|
|
84
|
+
createdAt: serverTimestamp(),
|
|
85
|
+
updatedAt: serverTimestamp(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
patientProfileSchema.parse({
|
|
89
|
+
...patientData,
|
|
90
|
+
createdAt: Timestamp.now(),
|
|
91
|
+
updatedAt: Timestamp.now(),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await setDoc(getPatientDocRef(db, patientId), patientData);
|
|
95
|
+
|
|
96
|
+
// Create blank sensitive info document using the utility function
|
|
97
|
+
console.log(`[createPatientProfileUtil] Creating sensitive info document`);
|
|
98
|
+
let sensitiveInfoSuccess = false;
|
|
99
|
+
try {
|
|
100
|
+
const sensitiveInfoCreated = await initSensitiveInfoDocIfNotExists(
|
|
101
|
+
db,
|
|
102
|
+
patientId,
|
|
103
|
+
validatedData.userRef
|
|
104
|
+
);
|
|
105
|
+
console.log(
|
|
106
|
+
`[createPatientProfileUtil] Sensitive info document creation result: ${
|
|
107
|
+
sensitiveInfoCreated
|
|
108
|
+
? "Document already existed"
|
|
109
|
+
: "New document created"
|
|
110
|
+
}`
|
|
111
|
+
);
|
|
112
|
+
sensitiveInfoSuccess = true;
|
|
113
|
+
} catch (sensitiveError) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[createPatientProfileUtil] Error creating sensitive info:`,
|
|
116
|
+
sensitiveError
|
|
117
|
+
);
|
|
118
|
+
// Don't throw the error, we'll try the direct method
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create blank medical info document using the utility function
|
|
122
|
+
console.log(`[createPatientProfileUtil] Creating medical info document`);
|
|
123
|
+
let medicalInfoSuccess = false;
|
|
124
|
+
try {
|
|
125
|
+
await ensureMedicalInfoExists(db, patientId, validatedData.userRef);
|
|
126
|
+
console.log(
|
|
127
|
+
`[createPatientProfileUtil] Medical info document created successfully`
|
|
128
|
+
);
|
|
129
|
+
medicalInfoSuccess = true;
|
|
130
|
+
} catch (medicalError) {
|
|
131
|
+
console.error(
|
|
132
|
+
`[createPatientProfileUtil] Error creating medical info:`,
|
|
133
|
+
medicalError
|
|
134
|
+
);
|
|
135
|
+
// Don't throw the error, we'll try the direct method
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// If either utility function failed, try the direct method
|
|
139
|
+
if (!sensitiveInfoSuccess || !medicalInfoSuccess) {
|
|
140
|
+
console.log(
|
|
141
|
+
`[createPatientProfileUtil] Using fallback method to create documents`
|
|
142
|
+
);
|
|
143
|
+
try {
|
|
144
|
+
await testCreateSubDocuments(db, patientId, validatedData.userRef);
|
|
145
|
+
console.log(
|
|
146
|
+
`[createPatientProfileUtil] Fallback method completed successfully`
|
|
147
|
+
);
|
|
148
|
+
} catch (fallbackError) {
|
|
149
|
+
console.error(
|
|
150
|
+
`[createPatientProfileUtil] Fallback method failed:`,
|
|
151
|
+
fallbackError
|
|
152
|
+
);
|
|
153
|
+
// Still continue to return the patient profile
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(`[createPatientProfileUtil] Verifying patient document exists`);
|
|
158
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
159
|
+
if (!patientDoc.exists()) {
|
|
160
|
+
console.error(
|
|
161
|
+
`[createPatientProfileUtil] Patient document not found after creation`
|
|
162
|
+
);
|
|
163
|
+
throw new Error("Failed to create patient profile");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(
|
|
167
|
+
`[createPatientProfileUtil] Patient profile creation completed successfully`
|
|
168
|
+
);
|
|
169
|
+
return patientDoc.data() as PatientProfile;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error(
|
|
172
|
+
`[createPatientProfileUtil] Error in patient profile creation:`,
|
|
173
|
+
error
|
|
174
|
+
);
|
|
175
|
+
if (error instanceof z.ZodError) {
|
|
176
|
+
throw new Error("Invalid patient data: " + error.message);
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const getPatientProfileUtil = async (
|
|
183
|
+
db: Firestore,
|
|
184
|
+
patientId: string
|
|
185
|
+
): Promise<PatientProfile | null> => {
|
|
186
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
187
|
+
return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export const getPatientProfileByUserRefUtil = async (
|
|
191
|
+
db: Firestore,
|
|
192
|
+
userRef: string
|
|
193
|
+
): Promise<PatientProfile | null> => {
|
|
194
|
+
try {
|
|
195
|
+
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
196
|
+
const patientDoc = await getDoc(docRef);
|
|
197
|
+
return patientDoc.exists() ? (patientDoc.data() as PatientProfile) : null;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Pomoćne funkcije za tokene i poene
|
|
204
|
+
export const addExpoTokenUtil = async (
|
|
205
|
+
db: Firestore,
|
|
206
|
+
patientId: string,
|
|
207
|
+
token: string
|
|
208
|
+
): Promise<void> => {
|
|
209
|
+
await updateDoc(getPatientDocRef(db, patientId), {
|
|
210
|
+
expoTokens: arrayUnion(token),
|
|
211
|
+
updatedAt: serverTimestamp(),
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const removeExpoTokenUtil = async (
|
|
216
|
+
db: Firestore,
|
|
217
|
+
patientId: string,
|
|
218
|
+
token: string
|
|
219
|
+
): Promise<void> => {
|
|
220
|
+
await updateDoc(getPatientDocRef(db, patientId), {
|
|
221
|
+
expoTokens: arrayRemove(token),
|
|
222
|
+
updatedAt: serverTimestamp(),
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const addPointsUtil = async (
|
|
227
|
+
db: Firestore,
|
|
228
|
+
patientId: string,
|
|
229
|
+
points: number
|
|
230
|
+
): Promise<void> => {
|
|
231
|
+
await updateDoc(getPatientDocRef(db, patientId), {
|
|
232
|
+
"gamification.points": increment(points),
|
|
233
|
+
updatedAt: serverTimestamp(),
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const updatePatientProfileUtil = async (
|
|
238
|
+
db: Firestore,
|
|
239
|
+
patientId: string,
|
|
240
|
+
data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
|
|
241
|
+
): Promise<PatientProfile> => {
|
|
242
|
+
try {
|
|
243
|
+
const updateData = {
|
|
244
|
+
...data,
|
|
245
|
+
updatedAt: serverTimestamp(),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
await updateDoc(getPatientDocRef(db, patientId), updateData);
|
|
249
|
+
|
|
250
|
+
const updatedDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
251
|
+
if (!updatedDoc.exists()) {
|
|
252
|
+
throw new Error("Patient profile not found after update");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return updatedDoc.data() as PatientProfile;
|
|
256
|
+
} catch (error: unknown) {
|
|
257
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
258
|
+
throw new Error(`Failed to update patient profile: ${errorMessage}`);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export const updatePatientProfileByUserRefUtil = async (
|
|
263
|
+
db: Firestore,
|
|
264
|
+
userRef: string,
|
|
265
|
+
data: Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">>
|
|
266
|
+
): Promise<PatientProfile> => {
|
|
267
|
+
try {
|
|
268
|
+
const docRef = await getPatientDocRefByUserRef(db, userRef);
|
|
269
|
+
const patientDoc = await getDoc(docRef);
|
|
270
|
+
|
|
271
|
+
if (!patientDoc.exists()) {
|
|
272
|
+
throw new Error("Patient profile not found");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const patientData = patientDoc.data() as PatientProfile;
|
|
276
|
+
return updatePatientProfileUtil(db, patientData.id, data);
|
|
277
|
+
} catch (error: unknown) {
|
|
278
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Failed to update patient profile by user ref: ${errorMessage}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Test function to directly create sensitive and medical info documents
|
|
287
|
+
* This is for debugging purposes only
|
|
288
|
+
*/
|
|
289
|
+
export const testCreateSubDocuments = async (
|
|
290
|
+
db: Firestore,
|
|
291
|
+
patientId: string,
|
|
292
|
+
userRef: string
|
|
293
|
+
): Promise<void> => {
|
|
294
|
+
console.log(
|
|
295
|
+
`[testCreateSubDocuments] Starting test for patientId: ${patientId}, userRef: ${userRef}`
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Test sensitive info creation
|
|
300
|
+
console.log(`[testCreateSubDocuments] Testing sensitive info creation`);
|
|
301
|
+
const sensitiveInfoRef = getSensitiveInfoDocRef(db, patientId);
|
|
302
|
+
console.log(
|
|
303
|
+
`[testCreateSubDocuments] Sensitive info path: ${sensitiveInfoRef.path}`
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const defaultSensitiveInfo = {
|
|
307
|
+
patientId,
|
|
308
|
+
userRef,
|
|
309
|
+
photoUrl: "",
|
|
310
|
+
firstName: "Name",
|
|
311
|
+
lastName: "Surname",
|
|
312
|
+
dateOfBirth: Timestamp.now(),
|
|
313
|
+
gender: Gender.PREFER_NOT_TO_SAY,
|
|
314
|
+
email: "test@example.com",
|
|
315
|
+
phoneNumber: "",
|
|
316
|
+
createdAt: Timestamp.now(),
|
|
317
|
+
updatedAt: Timestamp.now(),
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
await setDoc(sensitiveInfoRef, defaultSensitiveInfo);
|
|
321
|
+
console.log(
|
|
322
|
+
`[testCreateSubDocuments] Sensitive info document created directly`
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Test medical info creation
|
|
326
|
+
console.log(`[testCreateSubDocuments] Testing medical info creation`);
|
|
327
|
+
const medicalInfoRef = getMedicalInfoDocRef(db, patientId);
|
|
328
|
+
console.log(
|
|
329
|
+
`[testCreateSubDocuments] Medical info path: ${medicalInfoRef.path}`
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const defaultMedicalInfo = {
|
|
333
|
+
...DEFAULT_MEDICAL_INFO,
|
|
334
|
+
patientId,
|
|
335
|
+
lastUpdated: Timestamp.now(),
|
|
336
|
+
updatedBy: userRef,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
await setDoc(medicalInfoRef, defaultMedicalInfo);
|
|
340
|
+
console.log(
|
|
341
|
+
`[testCreateSubDocuments] Medical info document created directly`
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
console.log(`[testCreateSubDocuments] Test completed successfully`);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(`[testCreateSubDocuments] Error:`, error);
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Searches for patient profiles based on clinic and/or practitioner association.
|
|
353
|
+
* Applies security checks based on the requester's role and associations.
|
|
354
|
+
*
|
|
355
|
+
* @param {Firestore} db - Firestore instance.
|
|
356
|
+
* @param {SearchPatientsParams} params - Search criteria (clinicId, practitionerId).
|
|
357
|
+
* @param {RequesterInfo} requester - Information about the user performing the search.
|
|
358
|
+
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of matching patient profiles.
|
|
359
|
+
*/
|
|
360
|
+
export const searchPatientsUtil = async (
|
|
361
|
+
db: Firestore,
|
|
362
|
+
params: SearchPatientsParams,
|
|
363
|
+
requester: RequesterInfo
|
|
364
|
+
): Promise<PatientProfile[]> => {
|
|
365
|
+
// Validate input
|
|
366
|
+
searchPatientsSchema.parse(params);
|
|
367
|
+
requesterInfoSchema.parse(requester);
|
|
368
|
+
|
|
369
|
+
const constraints: QueryConstraint[] = [];
|
|
370
|
+
const patientsCollectionRef = collection(db, PATIENTS_COLLECTION);
|
|
371
|
+
|
|
372
|
+
// --- Security Checks & Initial Filtering ---
|
|
373
|
+
|
|
374
|
+
if (requester.role === "clinic_admin") {
|
|
375
|
+
// Clinic admin can only search within their own clinic
|
|
376
|
+
if (!requester.associatedClinicId) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
"Associated clinic ID is required for clinic admin search."
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
// If the search params specify a different clinic, it's an invalid request for this admin.
|
|
382
|
+
if (params.clinicId && params.clinicId !== requester.associatedClinicId) {
|
|
383
|
+
console.warn(
|
|
384
|
+
`Clinic admin (${requester.id}) attempted to search outside their associated clinic (${requester.associatedClinicId})`
|
|
385
|
+
);
|
|
386
|
+
return []; // Or throw an error
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// **Mandatory filter**: Ensure patients belong to the admin's clinic.
|
|
390
|
+
constraints.push(
|
|
391
|
+
where("clinicIds", "array-contains", requester.associatedClinicId)
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
// Optional filter: If practitionerId is also provided, filter by that practitioner *within the admin's clinic*.
|
|
395
|
+
if (params.practitionerId) {
|
|
396
|
+
constraints.push(
|
|
397
|
+
where("doctorIds", "array-contains", params.practitionerId)
|
|
398
|
+
);
|
|
399
|
+
// We might need an additional check here if the practitioner MUST work at the admin's clinic.
|
|
400
|
+
// This would require fetching practitioner data or having denormalized clinic IDs on the practitioner.
|
|
401
|
+
}
|
|
402
|
+
} else if (requester.role === "practitioner") {
|
|
403
|
+
// Practitioner can only search for their own patients.
|
|
404
|
+
if (!requester.associatedPractitionerId) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
"Associated practitioner ID is required for practitioner search."
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
// If the search params specify a different practitioner, it's invalid.
|
|
410
|
+
if (
|
|
411
|
+
params.practitionerId &&
|
|
412
|
+
params.practitionerId !== requester.associatedPractitionerId
|
|
413
|
+
) {
|
|
414
|
+
console.warn(
|
|
415
|
+
`Practitioner (${requester.id}) attempted to search for patients of another practitioner (${params.practitionerId})`
|
|
416
|
+
);
|
|
417
|
+
return []; // Or throw an error
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// **Mandatory filter**: Ensure patients are associated with this practitioner.
|
|
421
|
+
constraints.push(
|
|
422
|
+
where("doctorIds", "array-contains", requester.associatedPractitionerId)
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Optional filter: If clinicId is provided, filter by patients of this practitioner *at that specific clinic*.
|
|
426
|
+
if (params.clinicId) {
|
|
427
|
+
constraints.push(where("clinicIds", "array-contains", params.clinicId));
|
|
428
|
+
// Similar to above, we might need to check if the practitioner actually works at this clinic.
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
// Should not happen due to validation, but good practice to handle.
|
|
432
|
+
throw new Error("Invalid requester role.");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// --- Execute Query ---
|
|
436
|
+
try {
|
|
437
|
+
const finalQuery = query(patientsCollectionRef, ...constraints);
|
|
438
|
+
const querySnapshot = await getDocs(finalQuery);
|
|
439
|
+
|
|
440
|
+
const patients = querySnapshot.docs.map(
|
|
441
|
+
(doc) => doc.data() as PatientProfile
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
console.log(
|
|
445
|
+
`[searchPatientsUtil] Found ${patients.length} patients matching criteria.`
|
|
446
|
+
);
|
|
447
|
+
return patients;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error("[searchPatientsUtil] Error searching patients:", error);
|
|
450
|
+
// Consider logging more details or re-throwing a specific error type
|
|
451
|
+
return []; // Return empty array on error
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Retrieves all patient profiles from the database.
|
|
457
|
+
*
|
|
458
|
+
* @param {Firestore} db - Firestore instance
|
|
459
|
+
* @param {Object} options - Optional parameters for pagination
|
|
460
|
+
* @param {number} options.limit - Maximum number of profiles to return
|
|
461
|
+
* @param {string} options.startAfter - The ID of the document to start after (for pagination)
|
|
462
|
+
* @returns {Promise<PatientProfile[]>} A promise resolving to an array of all patient profiles
|
|
463
|
+
*/
|
|
464
|
+
export const getAllPatientsUtil = async (
|
|
465
|
+
db: Firestore,
|
|
466
|
+
options?: { limit?: number; startAfter?: string }
|
|
467
|
+
): Promise<PatientProfile[]> => {
|
|
468
|
+
try {
|
|
469
|
+
console.log(
|
|
470
|
+
`[getAllPatientsUtil] Fetching patients with options:`,
|
|
471
|
+
options
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const patientsCollection = collection(db, PATIENTS_COLLECTION);
|
|
475
|
+
|
|
476
|
+
let q = query(patientsCollection);
|
|
477
|
+
|
|
478
|
+
// Apply pagination if needed
|
|
479
|
+
if (options?.limit) {
|
|
480
|
+
q = query(q, limit(options.limit));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// If startAfter is provided, get that document and use it for pagination
|
|
484
|
+
if (options?.startAfter) {
|
|
485
|
+
const startAfterDoc = await getDoc(
|
|
486
|
+
doc(db, PATIENTS_COLLECTION, options.startAfter)
|
|
487
|
+
);
|
|
488
|
+
if (startAfterDoc.exists()) {
|
|
489
|
+
q = query(q, startAfter(startAfterDoc));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const patientsSnapshot = await getDocs(q);
|
|
494
|
+
|
|
495
|
+
const patients: PatientProfile[] = [];
|
|
496
|
+
patientsSnapshot.forEach((doc) => {
|
|
497
|
+
patients.push(doc.data() as PatientProfile);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
console.log(`[getAllPatientsUtil] Found ${patients.length} patients`);
|
|
501
|
+
return patients;
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error(`[getAllPatientsUtil] Error fetching patients:`, error);
|
|
504
|
+
throw new Error(
|
|
505
|
+
`Failed to retrieve patients: ${
|
|
506
|
+
error instanceof Error ? error.message : String(error)
|
|
507
|
+
}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
};
|