@blackcode_sa/metaestetics-api 1.13.5 → 1.13.6
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 +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/package.json +121 -121
- 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 +1984 -1984
- 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 +966 -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 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- 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 +81 -81
- 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 +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- 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 +10 -10
- 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 +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- 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 +67 -67
- 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 +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- 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/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- 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 +14 -14
- 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 +2200 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- 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/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -498
- 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 +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- 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 -132
- 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 +494 -494
- 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 +217 -217
- 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 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,552 +1,552 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Firestore,
|
|
3
|
-
collection,
|
|
4
|
-
doc,
|
|
5
|
-
getDoc,
|
|
6
|
-
getDocs,
|
|
7
|
-
query,
|
|
8
|
-
where,
|
|
9
|
-
setDoc,
|
|
10
|
-
updateDoc,
|
|
11
|
-
serverTimestamp,
|
|
12
|
-
Timestamp,
|
|
13
|
-
orderBy,
|
|
14
|
-
limit,
|
|
15
|
-
startAfter,
|
|
16
|
-
QueryConstraint,
|
|
17
|
-
DocumentSnapshot,
|
|
18
|
-
} from 'firebase/firestore';
|
|
19
|
-
import {
|
|
20
|
-
Appointment,
|
|
21
|
-
AppointmentStatus,
|
|
22
|
-
CreateAppointmentData,
|
|
23
|
-
UpdateAppointmentData,
|
|
24
|
-
APPOINTMENTS_COLLECTION,
|
|
25
|
-
SearchAppointmentsParams,
|
|
26
|
-
PaymentStatus,
|
|
27
|
-
} from '../../../types/appointment';
|
|
28
|
-
import { CalendarEvent, CALENDAR_COLLECTION } from '../../../types/calendar';
|
|
29
|
-
import { ProcedureSummaryInfo } from '../../../types/procedure';
|
|
30
|
-
import { ClinicInfo, PatientProfileInfo, PractitionerProfileInfo } from '../../../types/profile';
|
|
31
|
-
import { BlockingCondition } from '../../../backoffice/types/static/blocking-condition.types';
|
|
32
|
-
import { Requirement } from '../../../backoffice/types/requirement.types';
|
|
33
|
-
import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
|
|
34
|
-
import { CLINICS_COLLECTION } from '../../../types/clinic';
|
|
35
|
-
import { PATIENTS_COLLECTION } from '../../../types/patient';
|
|
36
|
-
import { PROCEDURES_COLLECTION } from '../../../types/procedure';
|
|
37
|
-
import { Technology, TECHNOLOGIES_COLLECTION } from '../../../backoffice/types/technology.types';
|
|
38
|
-
import type { ContraindicationDynamic } from '../../../backoffice';
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Fetches all the necessary information for an appointment by IDs.
|
|
42
|
-
*
|
|
43
|
-
* @param db Firestore instance
|
|
44
|
-
* @param clinicId Clinic ID
|
|
45
|
-
* @param practitionerId Practitioner ID
|
|
46
|
-
* @param patientId Patient ID
|
|
47
|
-
* @param procedureId Procedure ID
|
|
48
|
-
* @returns Object containing the aggregated information
|
|
49
|
-
*/
|
|
50
|
-
export async function fetchAggregatedInfoUtil(
|
|
51
|
-
db: Firestore,
|
|
52
|
-
clinicId: string,
|
|
53
|
-
practitionerId: string,
|
|
54
|
-
patientId: string,
|
|
55
|
-
procedureId: string,
|
|
56
|
-
): Promise<{
|
|
57
|
-
clinicInfo: ClinicInfo;
|
|
58
|
-
practitionerInfo: PractitionerProfileInfo;
|
|
59
|
-
patientInfo: PatientProfileInfo;
|
|
60
|
-
procedureInfo: ProcedureSummaryInfo;
|
|
61
|
-
blockingConditions: BlockingCondition[];
|
|
62
|
-
contraindications: ContraindicationDynamic[];
|
|
63
|
-
preProcedureRequirements: Requirement[];
|
|
64
|
-
postProcedureRequirements: Requirement[];
|
|
65
|
-
}> {
|
|
66
|
-
try {
|
|
67
|
-
// Fetch all data in parallel for efficiency
|
|
68
|
-
const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] = await Promise.all([
|
|
69
|
-
getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
|
|
70
|
-
getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
|
|
71
|
-
getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
|
|
72
|
-
getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
// Check if all required entities exist
|
|
76
|
-
if (!clinicDoc.exists()) {
|
|
77
|
-
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
78
|
-
}
|
|
79
|
-
if (!practitionerDoc.exists()) {
|
|
80
|
-
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
81
|
-
}
|
|
82
|
-
if (!patientDoc.exists()) {
|
|
83
|
-
throw new Error(`Patient with ID ${patientId} not found`);
|
|
84
|
-
}
|
|
85
|
-
if (!procedureDoc.exists()) {
|
|
86
|
-
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const clinicData = clinicDoc.data();
|
|
90
|
-
const practitionerData = practitionerDoc.data();
|
|
91
|
-
const patientData = patientDoc.data();
|
|
92
|
-
const procedureData = procedureDoc.data();
|
|
93
|
-
|
|
94
|
-
// Extract relevant info for ClinicInfo
|
|
95
|
-
const clinicInfo: ClinicInfo = {
|
|
96
|
-
id: clinicId,
|
|
97
|
-
featuredPhoto: clinicData.featuredPhotos?.[0] || '',
|
|
98
|
-
name: clinicData.name,
|
|
99
|
-
description: clinicData.description || null,
|
|
100
|
-
location: clinicData.location,
|
|
101
|
-
contactInfo: clinicData.contactInfo,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Extract relevant info for PractitionerProfileInfo
|
|
105
|
-
const practitionerInfo: PractitionerProfileInfo = {
|
|
106
|
-
id: practitionerId,
|
|
107
|
-
practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
|
|
108
|
-
name: `${practitionerData.basicInfo?.firstName || ''} ${
|
|
109
|
-
practitionerData.basicInfo?.lastName || ''
|
|
110
|
-
}`.trim(),
|
|
111
|
-
email: practitionerData.basicInfo?.email || '',
|
|
112
|
-
phone: practitionerData.basicInfo?.phoneNumber || null,
|
|
113
|
-
certification: practitionerData.certification,
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// Extract relevant info for PatientProfileInfo
|
|
117
|
-
// Note: This may need adjustment depending on how patient data is structured
|
|
118
|
-
const patientInfo: PatientProfileInfo = {
|
|
119
|
-
id: patientId,
|
|
120
|
-
fullName: patientData.displayName || '',
|
|
121
|
-
email: patientData.email || '',
|
|
122
|
-
phone: patientData.phoneNumber || null,
|
|
123
|
-
dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
|
|
124
|
-
gender: patientData.gender || 'other',
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Extract procedureInfo from the procedure document
|
|
128
|
-
// Assuming procedureData already has a procedureInfo property or similar structure
|
|
129
|
-
const procedureInfo: ProcedureSummaryInfo = {
|
|
130
|
-
id: procedureId,
|
|
131
|
-
name: procedureData.name,
|
|
132
|
-
description: procedureData.description,
|
|
133
|
-
photo: procedureData.photo || '',
|
|
134
|
-
family: procedureData.family,
|
|
135
|
-
categoryName: procedureData.category?.name || '',
|
|
136
|
-
subcategoryName: procedureData.subcategory?.name || '',
|
|
137
|
-
technologyName: procedureData.technology?.name || '',
|
|
138
|
-
brandName: procedureData.product?.brandName || '', // Safe: optional chaining
|
|
139
|
-
productName: procedureData.product?.name || '', // Safe: optional chaining
|
|
140
|
-
price: procedureData.price || 0,
|
|
141
|
-
pricingMeasure: procedureData.pricingMeasure,
|
|
142
|
-
currency: procedureData.currency,
|
|
143
|
-
duration: procedureData.duration || 0,
|
|
144
|
-
clinicId: clinicId,
|
|
145
|
-
clinicName: clinicInfo.name,
|
|
146
|
-
practitionerId: practitionerId,
|
|
147
|
-
practitionerName: practitionerInfo.name,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
// Fetch the technology document to get procedure requirements
|
|
151
|
-
let technologyId = '';
|
|
152
|
-
if (procedureData.technology?.id) {
|
|
153
|
-
technologyId = procedureData.technology.id;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let blockingConditions: BlockingCondition[] = [];
|
|
157
|
-
let contraindications: ContraindicationDynamic[] = [];
|
|
158
|
-
let preProcedureRequirements: Requirement[] = [];
|
|
159
|
-
let postProcedureRequirements: Requirement[] = [];
|
|
160
|
-
|
|
161
|
-
// If we have a technology ID, fetch its details
|
|
162
|
-
if (technologyId) {
|
|
163
|
-
const technologyDoc = await getDoc(doc(db, TECHNOLOGIES_COLLECTION, technologyId));
|
|
164
|
-
if (technologyDoc.exists()) {
|
|
165
|
-
const technologyData = technologyDoc.data() as Technology;
|
|
166
|
-
|
|
167
|
-
// Extract technology-related info
|
|
168
|
-
blockingConditions = technologyData.blockingConditions || [];
|
|
169
|
-
contraindications = technologyData.contraindications || [];
|
|
170
|
-
preProcedureRequirements = technologyData.requirements?.pre || [];
|
|
171
|
-
postProcedureRequirements = technologyData.requirements?.post || [];
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
// Fallback to procedure-level data if technology not available
|
|
175
|
-
blockingConditions = procedureData.blockingConditions || [];
|
|
176
|
-
contraindications = procedureData.contraindications || [];
|
|
177
|
-
preProcedureRequirements = procedureData.preRequirements || [];
|
|
178
|
-
postProcedureRequirements = procedureData.postRequirements || [];
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
clinicInfo,
|
|
183
|
-
practitionerInfo,
|
|
184
|
-
patientInfo,
|
|
185
|
-
procedureInfo,
|
|
186
|
-
blockingConditions,
|
|
187
|
-
contraindications,
|
|
188
|
-
preProcedureRequirements,
|
|
189
|
-
postProcedureRequirements,
|
|
190
|
-
};
|
|
191
|
-
} catch (error) {
|
|
192
|
-
console.error('Error fetching aggregated info:', error);
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Creates a new appointment in Firestore.
|
|
199
|
-
*
|
|
200
|
-
* @param db Firestore instance
|
|
201
|
-
* @param data Data needed to create the appointment
|
|
202
|
-
* @param aggregatedInfo Already fetched and aggregated info
|
|
203
|
-
* @param generateId Function to generate a unique ID
|
|
204
|
-
* @returns The created Appointment
|
|
205
|
-
*/
|
|
206
|
-
// export async function createAppointmentUtil(
|
|
207
|
-
// db: Firestore,
|
|
208
|
-
// data: CreateAppointmentData,
|
|
209
|
-
// aggregatedInfo: {
|
|
210
|
-
// clinicInfo: ClinicInfo;
|
|
211
|
-
// practitionerInfo: PractitionerProfileInfo;
|
|
212
|
-
// patientInfo: PatientProfileInfo;
|
|
213
|
-
// procedureInfo: ProcedureSummaryInfo;
|
|
214
|
-
// blockingConditions: BlockingCondition[];
|
|
215
|
-
// contraindications: ContraindicationDynamic[];
|
|
216
|
-
// preProcedureRequirements: Requirement[];
|
|
217
|
-
// postProcedureRequirements: Requirement[];
|
|
218
|
-
// },
|
|
219
|
-
// generateId: () => string
|
|
220
|
-
// ): Promise<Appointment> {
|
|
221
|
-
// try {
|
|
222
|
-
// const appointmentId = generateId();
|
|
223
|
-
|
|
224
|
-
// // Create appointment object
|
|
225
|
-
// const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
|
|
226
|
-
// createdAt: any;
|
|
227
|
-
// updatedAt: any;
|
|
228
|
-
// } = {
|
|
229
|
-
// id: appointmentId,
|
|
230
|
-
// calendarEventId: data.calendarEventId,
|
|
231
|
-
// clinicBranchId: data.clinicBranchId,
|
|
232
|
-
// clinicInfo: aggregatedInfo.clinicInfo,
|
|
233
|
-
// practitionerId: data.practitionerId,
|
|
234
|
-
// practitionerInfo: aggregatedInfo.practitionerInfo,
|
|
235
|
-
// patientId: data.patientId,
|
|
236
|
-
// patientInfo: aggregatedInfo.patientInfo,
|
|
237
|
-
// procedureId: data.procedureId,
|
|
238
|
-
// procedureInfo: aggregatedInfo.procedureInfo,
|
|
239
|
-
// status: data.initialStatus,
|
|
240
|
-
// bookingTime: Timestamp.now(),
|
|
241
|
-
// appointmentStartTime: data.appointmentStartTime,
|
|
242
|
-
// appointmentEndTime: data.appointmentEndTime,
|
|
243
|
-
// patientNotes: data.patientNotes || null,
|
|
244
|
-
// cost: data.cost,
|
|
245
|
-
// currency: data.currency,
|
|
246
|
-
// paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
|
|
247
|
-
// blockingConditions: aggregatedInfo.blockingConditions,
|
|
248
|
-
// contraindications: aggregatedInfo.contraindications,
|
|
249
|
-
// preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
|
|
250
|
-
// postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
|
|
251
|
-
// completedPreRequirements: [],
|
|
252
|
-
// completedPostRequirements: [],
|
|
253
|
-
// createdAt: serverTimestamp(),
|
|
254
|
-
// updatedAt: serverTimestamp(),
|
|
255
|
-
// };
|
|
256
|
-
|
|
257
|
-
// // Add additional fields for confirmation if appointment is already confirmed
|
|
258
|
-
// if (data.initialStatus === AppointmentStatus.CONFIRMED) {
|
|
259
|
-
// appointment.confirmationTime = Timestamp.now();
|
|
260
|
-
// }
|
|
261
|
-
|
|
262
|
-
// // Save to Firestore
|
|
263
|
-
// await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
|
|
264
|
-
|
|
265
|
-
// // Update the calendar event with the appointment ID
|
|
266
|
-
// const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
|
|
267
|
-
// await updateDoc(calendarEventRef, {
|
|
268
|
-
// appointmentId: appointmentId,
|
|
269
|
-
// updatedAt: serverTimestamp(),
|
|
270
|
-
// });
|
|
271
|
-
|
|
272
|
-
// // Return the created appointment
|
|
273
|
-
// // Convert serverTimestamp to regular Timestamp for immediate use
|
|
274
|
-
// const now = Timestamp.now();
|
|
275
|
-
// return {
|
|
276
|
-
// ...appointment,
|
|
277
|
-
// createdAt: now,
|
|
278
|
-
// updatedAt: now,
|
|
279
|
-
// } as Appointment;
|
|
280
|
-
// } catch (error) {
|
|
281
|
-
// console.error("Error creating appointment:", error);
|
|
282
|
-
// throw error;
|
|
283
|
-
// }
|
|
284
|
-
// }
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Updates an existing appointment in Firestore.
|
|
288
|
-
*
|
|
289
|
-
* @param db Firestore instance
|
|
290
|
-
* @param appointmentId ID of the appointment to update
|
|
291
|
-
* @param data Update data for the appointment
|
|
292
|
-
* @returns The updated Appointment
|
|
293
|
-
*/
|
|
294
|
-
export async function updateAppointmentUtil(
|
|
295
|
-
db: Firestore,
|
|
296
|
-
appointmentId: string,
|
|
297
|
-
data: UpdateAppointmentData,
|
|
298
|
-
): Promise<Appointment> {
|
|
299
|
-
try {
|
|
300
|
-
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
301
|
-
const appointmentDoc = await getDoc(appointmentRef);
|
|
302
|
-
|
|
303
|
-
if (!appointmentDoc.exists()) {
|
|
304
|
-
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const currentAppointment = appointmentDoc.data() as Appointment;
|
|
308
|
-
|
|
309
|
-
// Handle requirement completion tracking
|
|
310
|
-
let completedPreRequirements = currentAppointment.completedPreRequirements || [];
|
|
311
|
-
let completedPostRequirements = currentAppointment.completedPostRequirements || [];
|
|
312
|
-
|
|
313
|
-
if (data.completedPreRequirements) {
|
|
314
|
-
// Validate that all IDs exist in the pre-requirements
|
|
315
|
-
const validPreReqIds = currentAppointment.preProcedureRequirements.map(req => req.id);
|
|
316
|
-
|
|
317
|
-
// Only perform validation and merging if the input is an array
|
|
318
|
-
if (Array.isArray(data.completedPreRequirements)) {
|
|
319
|
-
const invalidPreReqIds = data.completedPreRequirements.filter(
|
|
320
|
-
id => !validPreReqIds.includes(id),
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
if (invalidPreReqIds.length > 0) {
|
|
324
|
-
throw new Error(`Invalid pre-requirement IDs: ${invalidPreReqIds.join(', ')}`);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Update the completed pre-requirements
|
|
328
|
-
completedPreRequirements = [
|
|
329
|
-
...new Set([...completedPreRequirements, ...data.completedPreRequirements]),
|
|
330
|
-
];
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (data.completedPostRequirements) {
|
|
335
|
-
// Validate that all IDs exist in the post-requirements
|
|
336
|
-
const validPostReqIds = currentAppointment.postProcedureRequirements.map(req => req.id);
|
|
337
|
-
|
|
338
|
-
if (Array.isArray(data.completedPostRequirements)) {
|
|
339
|
-
const invalidPostReqIds = data.completedPostRequirements.filter(
|
|
340
|
-
id => !validPostReqIds.includes(id),
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
if (invalidPostReqIds.length > 0) {
|
|
344
|
-
throw new Error(`Invalid post-requirement IDs: ${invalidPostReqIds.join(', ')}`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Update the completed post-requirements
|
|
348
|
-
completedPostRequirements = [
|
|
349
|
-
...new Set([...completedPostRequirements, ...data.completedPostRequirements]),
|
|
350
|
-
];
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Prepare update data
|
|
355
|
-
const updateData: any = {
|
|
356
|
-
...data,
|
|
357
|
-
completedPreRequirements: Array.isArray(data.completedPreRequirements)
|
|
358
|
-
? completedPreRequirements
|
|
359
|
-
: data.completedPreRequirements,
|
|
360
|
-
completedPostRequirements: Array.isArray(data.completedPostRequirements)
|
|
361
|
-
? completedPostRequirements
|
|
362
|
-
: data.completedPostRequirements,
|
|
363
|
-
updatedAt: serverTimestamp(),
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// Remove undefined fields
|
|
367
|
-
Object.keys(updateData).forEach(key => {
|
|
368
|
-
if (updateData[key] === undefined) {
|
|
369
|
-
delete updateData[key];
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Handle status changes
|
|
374
|
-
if (data.status && data.status !== currentAppointment.status) {
|
|
375
|
-
// Handle confirmation
|
|
376
|
-
if (data.status === AppointmentStatus.CONFIRMED && !updateData.confirmationTime) {
|
|
377
|
-
updateData.confirmationTime = Timestamp.now();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Update the related calendar event status if needed
|
|
381
|
-
if (currentAppointment.calendarEventId) {
|
|
382
|
-
await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Update the appointment
|
|
387
|
-
await updateDoc(appointmentRef, updateData);
|
|
388
|
-
|
|
389
|
-
// Fetch the updated appointment
|
|
390
|
-
const updatedAppointmentDoc = await getDoc(appointmentRef);
|
|
391
|
-
if (!updatedAppointmentDoc.exists()) {
|
|
392
|
-
throw new Error(`Failed to retrieve updated appointment ${appointmentId}`);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return updatedAppointmentDoc.data() as Appointment;
|
|
396
|
-
} catch (error) {
|
|
397
|
-
console.error(`Error updating appointment ${appointmentId}:`, error);
|
|
398
|
-
throw error;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Updates the status of a calendar event based on appointment status changes.
|
|
404
|
-
*
|
|
405
|
-
* @param db Firestore instance
|
|
406
|
-
* @param calendarEventId ID of the calendar event
|
|
407
|
-
* @param appointmentStatus New appointment status
|
|
408
|
-
*/
|
|
409
|
-
async function updateCalendarEventStatus(
|
|
410
|
-
db: Firestore,
|
|
411
|
-
calendarEventId: string,
|
|
412
|
-
appointmentStatus: AppointmentStatus,
|
|
413
|
-
): Promise<void> {
|
|
414
|
-
try {
|
|
415
|
-
const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
|
|
416
|
-
const calendarEventDoc = await getDoc(calendarEventRef);
|
|
417
|
-
|
|
418
|
-
if (!calendarEventDoc.exists()) {
|
|
419
|
-
console.warn(`Calendar event with ID ${calendarEventId} not found`);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Map appointment status to calendar event status
|
|
424
|
-
let calendarStatus;
|
|
425
|
-
switch (appointmentStatus) {
|
|
426
|
-
case AppointmentStatus.CONFIRMED:
|
|
427
|
-
calendarStatus = 'confirmed';
|
|
428
|
-
break;
|
|
429
|
-
case AppointmentStatus.CANCELED_PATIENT:
|
|
430
|
-
case AppointmentStatus.CANCELED_CLINIC:
|
|
431
|
-
calendarStatus = 'canceled';
|
|
432
|
-
break;
|
|
433
|
-
case AppointmentStatus.RESCHEDULED_BY_CLINIC:
|
|
434
|
-
calendarStatus = 'rescheduled';
|
|
435
|
-
break;
|
|
436
|
-
case AppointmentStatus.COMPLETED:
|
|
437
|
-
calendarStatus = 'completed';
|
|
438
|
-
break;
|
|
439
|
-
default:
|
|
440
|
-
// For other states, don't update the calendar status
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
await updateDoc(calendarEventRef, {
|
|
445
|
-
status: calendarStatus,
|
|
446
|
-
updatedAt: serverTimestamp(),
|
|
447
|
-
});
|
|
448
|
-
} catch (error) {
|
|
449
|
-
console.error(`Error updating calendar event ${calendarEventId}:`, error);
|
|
450
|
-
// Don't throw error to avoid failing the appointment update
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* Gets an appointment by its ID.
|
|
456
|
-
*
|
|
457
|
-
* @param db Firestore instance
|
|
458
|
-
* @param appointmentId Appointment ID
|
|
459
|
-
* @returns The appointment or null if not found
|
|
460
|
-
*/
|
|
461
|
-
export async function getAppointmentByIdUtil(
|
|
462
|
-
db: Firestore,
|
|
463
|
-
appointmentId: string,
|
|
464
|
-
): Promise<Appointment | null> {
|
|
465
|
-
try {
|
|
466
|
-
const appointmentDoc = await getDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId));
|
|
467
|
-
|
|
468
|
-
if (!appointmentDoc.exists()) {
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return appointmentDoc.data() as Appointment;
|
|
473
|
-
} catch (error) {
|
|
474
|
-
console.error(`Error getting appointment ${appointmentId}:`, error);
|
|
475
|
-
throw error;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Searches for appointments based on various criteria.
|
|
481
|
-
*
|
|
482
|
-
* @param db Firestore instance
|
|
483
|
-
* @param params Search parameters
|
|
484
|
-
* @returns Found appointments and the last document for pagination
|
|
485
|
-
*/
|
|
486
|
-
export async function searchAppointmentsUtil(
|
|
487
|
-
db: Firestore,
|
|
488
|
-
params: SearchAppointmentsParams,
|
|
489
|
-
): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
|
|
490
|
-
try {
|
|
491
|
-
const constraints: QueryConstraint[] = [];
|
|
492
|
-
|
|
493
|
-
// Add filters based on provided params
|
|
494
|
-
if (params.patientId) {
|
|
495
|
-
constraints.push(where('patientId', '==', params.patientId));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (params.practitionerId) {
|
|
499
|
-
constraints.push(where('practitionerId', '==', params.practitionerId));
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (params.clinicBranchId) {
|
|
503
|
-
constraints.push(where('clinicBranchId', '==', params.clinicBranchId));
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (params.startDate) {
|
|
507
|
-
constraints.push(where('appointmentStartTime', '>=', Timestamp.fromDate(params.startDate)));
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (params.endDate) {
|
|
511
|
-
constraints.push(where('appointmentStartTime', '<=', Timestamp.fromDate(params.endDate)));
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
if (params.status) {
|
|
515
|
-
if (Array.isArray(params.status)) {
|
|
516
|
-
// If multiple statuses, use in operator
|
|
517
|
-
constraints.push(where('status', 'in', params.status));
|
|
518
|
-
} else {
|
|
519
|
-
// Single status
|
|
520
|
-
constraints.push(where('status', '==', params.status));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Add ordering
|
|
525
|
-
constraints.push(orderBy('appointmentStartTime', 'asc'));
|
|
526
|
-
|
|
527
|
-
// Add pagination if specified
|
|
528
|
-
if (params.limit) {
|
|
529
|
-
constraints.push(limit(params.limit));
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (params.startAfter) {
|
|
533
|
-
constraints.push(startAfter(params.startAfter));
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Execute query
|
|
537
|
-
const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
|
|
538
|
-
const querySnapshot = await getDocs(q);
|
|
539
|
-
|
|
540
|
-
// Extract results
|
|
541
|
-
const appointments = querySnapshot.docs.map(doc => doc.data() as Appointment);
|
|
542
|
-
|
|
543
|
-
// Get last document for pagination
|
|
544
|
-
const lastDoc =
|
|
545
|
-
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
546
|
-
|
|
547
|
-
return { appointments, lastDoc };
|
|
548
|
-
} catch (error) {
|
|
549
|
-
console.error('Error searching appointments:', error);
|
|
550
|
-
throw error;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Firestore,
|
|
3
|
+
collection,
|
|
4
|
+
doc,
|
|
5
|
+
getDoc,
|
|
6
|
+
getDocs,
|
|
7
|
+
query,
|
|
8
|
+
where,
|
|
9
|
+
setDoc,
|
|
10
|
+
updateDoc,
|
|
11
|
+
serverTimestamp,
|
|
12
|
+
Timestamp,
|
|
13
|
+
orderBy,
|
|
14
|
+
limit,
|
|
15
|
+
startAfter,
|
|
16
|
+
QueryConstraint,
|
|
17
|
+
DocumentSnapshot,
|
|
18
|
+
} from 'firebase/firestore';
|
|
19
|
+
import {
|
|
20
|
+
Appointment,
|
|
21
|
+
AppointmentStatus,
|
|
22
|
+
CreateAppointmentData,
|
|
23
|
+
UpdateAppointmentData,
|
|
24
|
+
APPOINTMENTS_COLLECTION,
|
|
25
|
+
SearchAppointmentsParams,
|
|
26
|
+
PaymentStatus,
|
|
27
|
+
} from '../../../types/appointment';
|
|
28
|
+
import { CalendarEvent, CALENDAR_COLLECTION } from '../../../types/calendar';
|
|
29
|
+
import { ProcedureSummaryInfo } from '../../../types/procedure';
|
|
30
|
+
import { ClinicInfo, PatientProfileInfo, PractitionerProfileInfo } from '../../../types/profile';
|
|
31
|
+
import { BlockingCondition } from '../../../backoffice/types/static/blocking-condition.types';
|
|
32
|
+
import { Requirement } from '../../../backoffice/types/requirement.types';
|
|
33
|
+
import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
|
|
34
|
+
import { CLINICS_COLLECTION } from '../../../types/clinic';
|
|
35
|
+
import { PATIENTS_COLLECTION } from '../../../types/patient';
|
|
36
|
+
import { PROCEDURES_COLLECTION } from '../../../types/procedure';
|
|
37
|
+
import { Technology, TECHNOLOGIES_COLLECTION } from '../../../backoffice/types/technology.types';
|
|
38
|
+
import type { ContraindicationDynamic } from '../../../backoffice';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fetches all the necessary information for an appointment by IDs.
|
|
42
|
+
*
|
|
43
|
+
* @param db Firestore instance
|
|
44
|
+
* @param clinicId Clinic ID
|
|
45
|
+
* @param practitionerId Practitioner ID
|
|
46
|
+
* @param patientId Patient ID
|
|
47
|
+
* @param procedureId Procedure ID
|
|
48
|
+
* @returns Object containing the aggregated information
|
|
49
|
+
*/
|
|
50
|
+
export async function fetchAggregatedInfoUtil(
|
|
51
|
+
db: Firestore,
|
|
52
|
+
clinicId: string,
|
|
53
|
+
practitionerId: string,
|
|
54
|
+
patientId: string,
|
|
55
|
+
procedureId: string,
|
|
56
|
+
): Promise<{
|
|
57
|
+
clinicInfo: ClinicInfo;
|
|
58
|
+
practitionerInfo: PractitionerProfileInfo;
|
|
59
|
+
patientInfo: PatientProfileInfo;
|
|
60
|
+
procedureInfo: ProcedureSummaryInfo;
|
|
61
|
+
blockingConditions: BlockingCondition[];
|
|
62
|
+
contraindications: ContraindicationDynamic[];
|
|
63
|
+
preProcedureRequirements: Requirement[];
|
|
64
|
+
postProcedureRequirements: Requirement[];
|
|
65
|
+
}> {
|
|
66
|
+
try {
|
|
67
|
+
// Fetch all data in parallel for efficiency
|
|
68
|
+
const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] = await Promise.all([
|
|
69
|
+
getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
|
|
70
|
+
getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
|
|
71
|
+
getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
|
|
72
|
+
getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// Check if all required entities exist
|
|
76
|
+
if (!clinicDoc.exists()) {
|
|
77
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
78
|
+
}
|
|
79
|
+
if (!practitionerDoc.exists()) {
|
|
80
|
+
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
81
|
+
}
|
|
82
|
+
if (!patientDoc.exists()) {
|
|
83
|
+
throw new Error(`Patient with ID ${patientId} not found`);
|
|
84
|
+
}
|
|
85
|
+
if (!procedureDoc.exists()) {
|
|
86
|
+
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const clinicData = clinicDoc.data();
|
|
90
|
+
const practitionerData = practitionerDoc.data();
|
|
91
|
+
const patientData = patientDoc.data();
|
|
92
|
+
const procedureData = procedureDoc.data();
|
|
93
|
+
|
|
94
|
+
// Extract relevant info for ClinicInfo
|
|
95
|
+
const clinicInfo: ClinicInfo = {
|
|
96
|
+
id: clinicId,
|
|
97
|
+
featuredPhoto: clinicData.featuredPhotos?.[0] || '',
|
|
98
|
+
name: clinicData.name,
|
|
99
|
+
description: clinicData.description || null,
|
|
100
|
+
location: clinicData.location,
|
|
101
|
+
contactInfo: clinicData.contactInfo,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Extract relevant info for PractitionerProfileInfo
|
|
105
|
+
const practitionerInfo: PractitionerProfileInfo = {
|
|
106
|
+
id: practitionerId,
|
|
107
|
+
practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
|
|
108
|
+
name: `${practitionerData.basicInfo?.firstName || ''} ${
|
|
109
|
+
practitionerData.basicInfo?.lastName || ''
|
|
110
|
+
}`.trim(),
|
|
111
|
+
email: practitionerData.basicInfo?.email || '',
|
|
112
|
+
phone: practitionerData.basicInfo?.phoneNumber || null,
|
|
113
|
+
certification: practitionerData.certification,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Extract relevant info for PatientProfileInfo
|
|
117
|
+
// Note: This may need adjustment depending on how patient data is structured
|
|
118
|
+
const patientInfo: PatientProfileInfo = {
|
|
119
|
+
id: patientId,
|
|
120
|
+
fullName: patientData.displayName || '',
|
|
121
|
+
email: patientData.email || '',
|
|
122
|
+
phone: patientData.phoneNumber || null,
|
|
123
|
+
dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
|
|
124
|
+
gender: patientData.gender || 'other',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Extract procedureInfo from the procedure document
|
|
128
|
+
// Assuming procedureData already has a procedureInfo property or similar structure
|
|
129
|
+
const procedureInfo: ProcedureSummaryInfo = {
|
|
130
|
+
id: procedureId,
|
|
131
|
+
name: procedureData.name,
|
|
132
|
+
description: procedureData.description,
|
|
133
|
+
photo: procedureData.photo || '',
|
|
134
|
+
family: procedureData.family,
|
|
135
|
+
categoryName: procedureData.category?.name || '',
|
|
136
|
+
subcategoryName: procedureData.subcategory?.name || '',
|
|
137
|
+
technologyName: procedureData.technology?.name || '',
|
|
138
|
+
brandName: procedureData.product?.brandName || '', // Safe: optional chaining
|
|
139
|
+
productName: procedureData.product?.name || '', // Safe: optional chaining
|
|
140
|
+
price: procedureData.price || 0,
|
|
141
|
+
pricingMeasure: procedureData.pricingMeasure,
|
|
142
|
+
currency: procedureData.currency,
|
|
143
|
+
duration: procedureData.duration || 0,
|
|
144
|
+
clinicId: clinicId,
|
|
145
|
+
clinicName: clinicInfo.name,
|
|
146
|
+
practitionerId: practitionerId,
|
|
147
|
+
practitionerName: practitionerInfo.name,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Fetch the technology document to get procedure requirements
|
|
151
|
+
let technologyId = '';
|
|
152
|
+
if (procedureData.technology?.id) {
|
|
153
|
+
technologyId = procedureData.technology.id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let blockingConditions: BlockingCondition[] = [];
|
|
157
|
+
let contraindications: ContraindicationDynamic[] = [];
|
|
158
|
+
let preProcedureRequirements: Requirement[] = [];
|
|
159
|
+
let postProcedureRequirements: Requirement[] = [];
|
|
160
|
+
|
|
161
|
+
// If we have a technology ID, fetch its details
|
|
162
|
+
if (technologyId) {
|
|
163
|
+
const technologyDoc = await getDoc(doc(db, TECHNOLOGIES_COLLECTION, technologyId));
|
|
164
|
+
if (technologyDoc.exists()) {
|
|
165
|
+
const technologyData = technologyDoc.data() as Technology;
|
|
166
|
+
|
|
167
|
+
// Extract technology-related info
|
|
168
|
+
blockingConditions = technologyData.blockingConditions || [];
|
|
169
|
+
contraindications = technologyData.contraindications || [];
|
|
170
|
+
preProcedureRequirements = technologyData.requirements?.pre || [];
|
|
171
|
+
postProcedureRequirements = technologyData.requirements?.post || [];
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// Fallback to procedure-level data if technology not available
|
|
175
|
+
blockingConditions = procedureData.blockingConditions || [];
|
|
176
|
+
contraindications = procedureData.contraindications || [];
|
|
177
|
+
preProcedureRequirements = procedureData.preRequirements || [];
|
|
178
|
+
postProcedureRequirements = procedureData.postRequirements || [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
clinicInfo,
|
|
183
|
+
practitionerInfo,
|
|
184
|
+
patientInfo,
|
|
185
|
+
procedureInfo,
|
|
186
|
+
blockingConditions,
|
|
187
|
+
contraindications,
|
|
188
|
+
preProcedureRequirements,
|
|
189
|
+
postProcedureRequirements,
|
|
190
|
+
};
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('Error fetching aggregated info:', error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Creates a new appointment in Firestore.
|
|
199
|
+
*
|
|
200
|
+
* @param db Firestore instance
|
|
201
|
+
* @param data Data needed to create the appointment
|
|
202
|
+
* @param aggregatedInfo Already fetched and aggregated info
|
|
203
|
+
* @param generateId Function to generate a unique ID
|
|
204
|
+
* @returns The created Appointment
|
|
205
|
+
*/
|
|
206
|
+
// export async function createAppointmentUtil(
|
|
207
|
+
// db: Firestore,
|
|
208
|
+
// data: CreateAppointmentData,
|
|
209
|
+
// aggregatedInfo: {
|
|
210
|
+
// clinicInfo: ClinicInfo;
|
|
211
|
+
// practitionerInfo: PractitionerProfileInfo;
|
|
212
|
+
// patientInfo: PatientProfileInfo;
|
|
213
|
+
// procedureInfo: ProcedureSummaryInfo;
|
|
214
|
+
// blockingConditions: BlockingCondition[];
|
|
215
|
+
// contraindications: ContraindicationDynamic[];
|
|
216
|
+
// preProcedureRequirements: Requirement[];
|
|
217
|
+
// postProcedureRequirements: Requirement[];
|
|
218
|
+
// },
|
|
219
|
+
// generateId: () => string
|
|
220
|
+
// ): Promise<Appointment> {
|
|
221
|
+
// try {
|
|
222
|
+
// const appointmentId = generateId();
|
|
223
|
+
|
|
224
|
+
// // Create appointment object
|
|
225
|
+
// const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
|
|
226
|
+
// createdAt: any;
|
|
227
|
+
// updatedAt: any;
|
|
228
|
+
// } = {
|
|
229
|
+
// id: appointmentId,
|
|
230
|
+
// calendarEventId: data.calendarEventId,
|
|
231
|
+
// clinicBranchId: data.clinicBranchId,
|
|
232
|
+
// clinicInfo: aggregatedInfo.clinicInfo,
|
|
233
|
+
// practitionerId: data.practitionerId,
|
|
234
|
+
// practitionerInfo: aggregatedInfo.practitionerInfo,
|
|
235
|
+
// patientId: data.patientId,
|
|
236
|
+
// patientInfo: aggregatedInfo.patientInfo,
|
|
237
|
+
// procedureId: data.procedureId,
|
|
238
|
+
// procedureInfo: aggregatedInfo.procedureInfo,
|
|
239
|
+
// status: data.initialStatus,
|
|
240
|
+
// bookingTime: Timestamp.now(),
|
|
241
|
+
// appointmentStartTime: data.appointmentStartTime,
|
|
242
|
+
// appointmentEndTime: data.appointmentEndTime,
|
|
243
|
+
// patientNotes: data.patientNotes || null,
|
|
244
|
+
// cost: data.cost,
|
|
245
|
+
// currency: data.currency,
|
|
246
|
+
// paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
|
|
247
|
+
// blockingConditions: aggregatedInfo.blockingConditions,
|
|
248
|
+
// contraindications: aggregatedInfo.contraindications,
|
|
249
|
+
// preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
|
|
250
|
+
// postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
|
|
251
|
+
// completedPreRequirements: [],
|
|
252
|
+
// completedPostRequirements: [],
|
|
253
|
+
// createdAt: serverTimestamp(),
|
|
254
|
+
// updatedAt: serverTimestamp(),
|
|
255
|
+
// };
|
|
256
|
+
|
|
257
|
+
// // Add additional fields for confirmation if appointment is already confirmed
|
|
258
|
+
// if (data.initialStatus === AppointmentStatus.CONFIRMED) {
|
|
259
|
+
// appointment.confirmationTime = Timestamp.now();
|
|
260
|
+
// }
|
|
261
|
+
|
|
262
|
+
// // Save to Firestore
|
|
263
|
+
// await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
|
|
264
|
+
|
|
265
|
+
// // Update the calendar event with the appointment ID
|
|
266
|
+
// const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
|
|
267
|
+
// await updateDoc(calendarEventRef, {
|
|
268
|
+
// appointmentId: appointmentId,
|
|
269
|
+
// updatedAt: serverTimestamp(),
|
|
270
|
+
// });
|
|
271
|
+
|
|
272
|
+
// // Return the created appointment
|
|
273
|
+
// // Convert serverTimestamp to regular Timestamp for immediate use
|
|
274
|
+
// const now = Timestamp.now();
|
|
275
|
+
// return {
|
|
276
|
+
// ...appointment,
|
|
277
|
+
// createdAt: now,
|
|
278
|
+
// updatedAt: now,
|
|
279
|
+
// } as Appointment;
|
|
280
|
+
// } catch (error) {
|
|
281
|
+
// console.error("Error creating appointment:", error);
|
|
282
|
+
// throw error;
|
|
283
|
+
// }
|
|
284
|
+
// }
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Updates an existing appointment in Firestore.
|
|
288
|
+
*
|
|
289
|
+
* @param db Firestore instance
|
|
290
|
+
* @param appointmentId ID of the appointment to update
|
|
291
|
+
* @param data Update data for the appointment
|
|
292
|
+
* @returns The updated Appointment
|
|
293
|
+
*/
|
|
294
|
+
export async function updateAppointmentUtil(
|
|
295
|
+
db: Firestore,
|
|
296
|
+
appointmentId: string,
|
|
297
|
+
data: UpdateAppointmentData,
|
|
298
|
+
): Promise<Appointment> {
|
|
299
|
+
try {
|
|
300
|
+
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
301
|
+
const appointmentDoc = await getDoc(appointmentRef);
|
|
302
|
+
|
|
303
|
+
if (!appointmentDoc.exists()) {
|
|
304
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const currentAppointment = appointmentDoc.data() as Appointment;
|
|
308
|
+
|
|
309
|
+
// Handle requirement completion tracking
|
|
310
|
+
let completedPreRequirements = currentAppointment.completedPreRequirements || [];
|
|
311
|
+
let completedPostRequirements = currentAppointment.completedPostRequirements || [];
|
|
312
|
+
|
|
313
|
+
if (data.completedPreRequirements) {
|
|
314
|
+
// Validate that all IDs exist in the pre-requirements
|
|
315
|
+
const validPreReqIds = currentAppointment.preProcedureRequirements.map(req => req.id);
|
|
316
|
+
|
|
317
|
+
// Only perform validation and merging if the input is an array
|
|
318
|
+
if (Array.isArray(data.completedPreRequirements)) {
|
|
319
|
+
const invalidPreReqIds = data.completedPreRequirements.filter(
|
|
320
|
+
id => !validPreReqIds.includes(id),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (invalidPreReqIds.length > 0) {
|
|
324
|
+
throw new Error(`Invalid pre-requirement IDs: ${invalidPreReqIds.join(', ')}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Update the completed pre-requirements
|
|
328
|
+
completedPreRequirements = [
|
|
329
|
+
...new Set([...completedPreRequirements, ...data.completedPreRequirements]),
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (data.completedPostRequirements) {
|
|
335
|
+
// Validate that all IDs exist in the post-requirements
|
|
336
|
+
const validPostReqIds = currentAppointment.postProcedureRequirements.map(req => req.id);
|
|
337
|
+
|
|
338
|
+
if (Array.isArray(data.completedPostRequirements)) {
|
|
339
|
+
const invalidPostReqIds = data.completedPostRequirements.filter(
|
|
340
|
+
id => !validPostReqIds.includes(id),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (invalidPostReqIds.length > 0) {
|
|
344
|
+
throw new Error(`Invalid post-requirement IDs: ${invalidPostReqIds.join(', ')}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Update the completed post-requirements
|
|
348
|
+
completedPostRequirements = [
|
|
349
|
+
...new Set([...completedPostRequirements, ...data.completedPostRequirements]),
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Prepare update data
|
|
355
|
+
const updateData: any = {
|
|
356
|
+
...data,
|
|
357
|
+
completedPreRequirements: Array.isArray(data.completedPreRequirements)
|
|
358
|
+
? completedPreRequirements
|
|
359
|
+
: data.completedPreRequirements,
|
|
360
|
+
completedPostRequirements: Array.isArray(data.completedPostRequirements)
|
|
361
|
+
? completedPostRequirements
|
|
362
|
+
: data.completedPostRequirements,
|
|
363
|
+
updatedAt: serverTimestamp(),
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Remove undefined fields
|
|
367
|
+
Object.keys(updateData).forEach(key => {
|
|
368
|
+
if (updateData[key] === undefined) {
|
|
369
|
+
delete updateData[key];
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Handle status changes
|
|
374
|
+
if (data.status && data.status !== currentAppointment.status) {
|
|
375
|
+
// Handle confirmation
|
|
376
|
+
if (data.status === AppointmentStatus.CONFIRMED && !updateData.confirmationTime) {
|
|
377
|
+
updateData.confirmationTime = Timestamp.now();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Update the related calendar event status if needed
|
|
381
|
+
if (currentAppointment.calendarEventId) {
|
|
382
|
+
await updateCalendarEventStatus(db, currentAppointment.calendarEventId, data.status);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Update the appointment
|
|
387
|
+
await updateDoc(appointmentRef, updateData);
|
|
388
|
+
|
|
389
|
+
// Fetch the updated appointment
|
|
390
|
+
const updatedAppointmentDoc = await getDoc(appointmentRef);
|
|
391
|
+
if (!updatedAppointmentDoc.exists()) {
|
|
392
|
+
throw new Error(`Failed to retrieve updated appointment ${appointmentId}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return updatedAppointmentDoc.data() as Appointment;
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.error(`Error updating appointment ${appointmentId}:`, error);
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Updates the status of a calendar event based on appointment status changes.
|
|
404
|
+
*
|
|
405
|
+
* @param db Firestore instance
|
|
406
|
+
* @param calendarEventId ID of the calendar event
|
|
407
|
+
* @param appointmentStatus New appointment status
|
|
408
|
+
*/
|
|
409
|
+
async function updateCalendarEventStatus(
|
|
410
|
+
db: Firestore,
|
|
411
|
+
calendarEventId: string,
|
|
412
|
+
appointmentStatus: AppointmentStatus,
|
|
413
|
+
): Promise<void> {
|
|
414
|
+
try {
|
|
415
|
+
const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
|
|
416
|
+
const calendarEventDoc = await getDoc(calendarEventRef);
|
|
417
|
+
|
|
418
|
+
if (!calendarEventDoc.exists()) {
|
|
419
|
+
console.warn(`Calendar event with ID ${calendarEventId} not found`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Map appointment status to calendar event status
|
|
424
|
+
let calendarStatus;
|
|
425
|
+
switch (appointmentStatus) {
|
|
426
|
+
case AppointmentStatus.CONFIRMED:
|
|
427
|
+
calendarStatus = 'confirmed';
|
|
428
|
+
break;
|
|
429
|
+
case AppointmentStatus.CANCELED_PATIENT:
|
|
430
|
+
case AppointmentStatus.CANCELED_CLINIC:
|
|
431
|
+
calendarStatus = 'canceled';
|
|
432
|
+
break;
|
|
433
|
+
case AppointmentStatus.RESCHEDULED_BY_CLINIC:
|
|
434
|
+
calendarStatus = 'rescheduled';
|
|
435
|
+
break;
|
|
436
|
+
case AppointmentStatus.COMPLETED:
|
|
437
|
+
calendarStatus = 'completed';
|
|
438
|
+
break;
|
|
439
|
+
default:
|
|
440
|
+
// For other states, don't update the calendar status
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await updateDoc(calendarEventRef, {
|
|
445
|
+
status: calendarStatus,
|
|
446
|
+
updatedAt: serverTimestamp(),
|
|
447
|
+
});
|
|
448
|
+
} catch (error) {
|
|
449
|
+
console.error(`Error updating calendar event ${calendarEventId}:`, error);
|
|
450
|
+
// Don't throw error to avoid failing the appointment update
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Gets an appointment by its ID.
|
|
456
|
+
*
|
|
457
|
+
* @param db Firestore instance
|
|
458
|
+
* @param appointmentId Appointment ID
|
|
459
|
+
* @returns The appointment or null if not found
|
|
460
|
+
*/
|
|
461
|
+
export async function getAppointmentByIdUtil(
|
|
462
|
+
db: Firestore,
|
|
463
|
+
appointmentId: string,
|
|
464
|
+
): Promise<Appointment | null> {
|
|
465
|
+
try {
|
|
466
|
+
const appointmentDoc = await getDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId));
|
|
467
|
+
|
|
468
|
+
if (!appointmentDoc.exists()) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return appointmentDoc.data() as Appointment;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
console.error(`Error getting appointment ${appointmentId}:`, error);
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Searches for appointments based on various criteria.
|
|
481
|
+
*
|
|
482
|
+
* @param db Firestore instance
|
|
483
|
+
* @param params Search parameters
|
|
484
|
+
* @returns Found appointments and the last document for pagination
|
|
485
|
+
*/
|
|
486
|
+
export async function searchAppointmentsUtil(
|
|
487
|
+
db: Firestore,
|
|
488
|
+
params: SearchAppointmentsParams,
|
|
489
|
+
): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
|
|
490
|
+
try {
|
|
491
|
+
const constraints: QueryConstraint[] = [];
|
|
492
|
+
|
|
493
|
+
// Add filters based on provided params
|
|
494
|
+
if (params.patientId) {
|
|
495
|
+
constraints.push(where('patientId', '==', params.patientId));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (params.practitionerId) {
|
|
499
|
+
constraints.push(where('practitionerId', '==', params.practitionerId));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (params.clinicBranchId) {
|
|
503
|
+
constraints.push(where('clinicBranchId', '==', params.clinicBranchId));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (params.startDate) {
|
|
507
|
+
constraints.push(where('appointmentStartTime', '>=', Timestamp.fromDate(params.startDate)));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (params.endDate) {
|
|
511
|
+
constraints.push(where('appointmentStartTime', '<=', Timestamp.fromDate(params.endDate)));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (params.status) {
|
|
515
|
+
if (Array.isArray(params.status)) {
|
|
516
|
+
// If multiple statuses, use in operator
|
|
517
|
+
constraints.push(where('status', 'in', params.status));
|
|
518
|
+
} else {
|
|
519
|
+
// Single status
|
|
520
|
+
constraints.push(where('status', '==', params.status));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Add ordering
|
|
525
|
+
constraints.push(orderBy('appointmentStartTime', 'asc'));
|
|
526
|
+
|
|
527
|
+
// Add pagination if specified
|
|
528
|
+
if (params.limit) {
|
|
529
|
+
constraints.push(limit(params.limit));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (params.startAfter) {
|
|
533
|
+
constraints.push(startAfter(params.startAfter));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Execute query
|
|
537
|
+
const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
|
|
538
|
+
const querySnapshot = await getDocs(q);
|
|
539
|
+
|
|
540
|
+
// Extract results
|
|
541
|
+
const appointments = querySnapshot.docs.map(doc => doc.data() as Appointment);
|
|
542
|
+
|
|
543
|
+
// Get last document for pagination
|
|
544
|
+
const lastDoc =
|
|
545
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
546
|
+
|
|
547
|
+
return { appointments, lastDoc };
|
|
548
|
+
} catch (error) {
|
|
549
|
+
console.error('Error searching appointments:', error);
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|