@blackcode_sa/metaestetics-api 1.4.17 → 1.5.0
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/index.d.mts +9633 -7023
- package/dist/index.d.ts +9633 -7023
- package/dist/index.js +2773 -150
- package/dist/index.mjs +2809 -150
- package/package.json +4 -3
- package/src/index.ts +48 -1
- package/src/services/calendar/calendar-refactored.service.ts +1531 -0
- package/src/services/calendar/calendar.service.ts +1077 -0
- package/src/services/calendar/synced-calendars.service.ts +743 -0
- package/src/services/calendar/utils/appointment.utils.ts +314 -0
- package/src/services/calendar/utils/calendar-event.utils.ts +510 -0
- package/src/services/calendar/utils/clinic.utils.ts +237 -0
- package/src/services/calendar/utils/docs.utils.ts +157 -0
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -0
- package/src/services/calendar/utils/index.ts +8 -0
- package/src/services/calendar/utils/patient.utils.ts +198 -0
- package/src/services/calendar/utils/practitioner.utils.ts +221 -0
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -0
- package/src/services/clinic/clinic.service.ts +2 -2
- package/src/services/clinic/utils/clinic.utils.ts +49 -47
- package/src/services/practitioner/practitioner.service.ts +1 -0
- package/src/types/calendar/index.ts +187 -0
- package/src/types/calendar/synced-calendar.types.ts +66 -0
- package/src/types/clinic/index.ts +4 -15
- package/src/types/index.ts +4 -0
- package/src/types/practitioner/index.ts +21 -0
- package/src/types/profile/index.ts +39 -0
- package/src/validations/calendar.schema.ts +223 -0
- package/src/validations/clinic.schema.ts +3 -3
- package/src/validations/practitioner.schema.ts +21 -0
- package/src/validations/profile-info.schema.ts +41 -0
package/dist/index.mjs
CHANGED
|
@@ -108,6 +108,31 @@ var FilledDocumentStatus = /* @__PURE__ */ ((FilledDocumentStatus2) => {
|
|
|
108
108
|
return FilledDocumentStatus2;
|
|
109
109
|
})(FilledDocumentStatus || {});
|
|
110
110
|
|
|
111
|
+
// src/types/calendar/index.ts
|
|
112
|
+
var CalendarEventStatus = /* @__PURE__ */ ((CalendarEventStatus3) => {
|
|
113
|
+
CalendarEventStatus3["PENDING"] = "pending";
|
|
114
|
+
CalendarEventStatus3["CONFIRMED"] = "confirmed";
|
|
115
|
+
CalendarEventStatus3["REJECTED"] = "rejected";
|
|
116
|
+
CalendarEventStatus3["CANCELED"] = "canceled";
|
|
117
|
+
CalendarEventStatus3["RESCHEDULED"] = "rescheduled";
|
|
118
|
+
CalendarEventStatus3["COMPLETED"] = "completed";
|
|
119
|
+
return CalendarEventStatus3;
|
|
120
|
+
})(CalendarEventStatus || {});
|
|
121
|
+
var CalendarSyncStatus = /* @__PURE__ */ ((CalendarSyncStatus3) => {
|
|
122
|
+
CalendarSyncStatus3["INTERNAL"] = "internal";
|
|
123
|
+
CalendarSyncStatus3["EXTERNAL"] = "external";
|
|
124
|
+
return CalendarSyncStatus3;
|
|
125
|
+
})(CalendarSyncStatus || {});
|
|
126
|
+
var CalendarEventType = /* @__PURE__ */ ((CalendarEventType2) => {
|
|
127
|
+
CalendarEventType2["APPOINTMENT"] = "appointment";
|
|
128
|
+
CalendarEventType2["BLOCKING"] = "blocking";
|
|
129
|
+
CalendarEventType2["BREAK"] = "break";
|
|
130
|
+
CalendarEventType2["FREE_DAY"] = "free_day";
|
|
131
|
+
CalendarEventType2["OTHER"] = "other";
|
|
132
|
+
return CalendarEventType2;
|
|
133
|
+
})(CalendarEventType || {});
|
|
134
|
+
var CALENDAR_COLLECTION = "calendar";
|
|
135
|
+
|
|
111
136
|
// src/types/index.ts
|
|
112
137
|
var UserRole = /* @__PURE__ */ ((UserRole2) => {
|
|
113
138
|
UserRole2["PATIENT"] = "patient";
|
|
@@ -1276,9 +1301,9 @@ var addAllergyUtil = async (db, patientId, data, userRef) => {
|
|
|
1276
1301
|
var updateAllergyUtil = async (db, patientId, data, userRef) => {
|
|
1277
1302
|
const validatedData = updateAllergySchema.parse(data);
|
|
1278
1303
|
const { allergyIndex, ...updateData } = validatedData;
|
|
1279
|
-
const
|
|
1280
|
-
if (!
|
|
1281
|
-
const medicalInfo =
|
|
1304
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1305
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1306
|
+
const medicalInfo = doc20.data();
|
|
1282
1307
|
if (allergyIndex >= medicalInfo.allergies.length) {
|
|
1283
1308
|
throw new Error("Invalid allergy index");
|
|
1284
1309
|
}
|
|
@@ -1294,9 +1319,9 @@ var updateAllergyUtil = async (db, patientId, data, userRef) => {
|
|
|
1294
1319
|
});
|
|
1295
1320
|
};
|
|
1296
1321
|
var removeAllergyUtil = async (db, patientId, allergyIndex, userRef) => {
|
|
1297
|
-
const
|
|
1298
|
-
if (!
|
|
1299
|
-
const medicalInfo =
|
|
1322
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1323
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1324
|
+
const medicalInfo = doc20.data();
|
|
1300
1325
|
if (allergyIndex >= medicalInfo.allergies.length) {
|
|
1301
1326
|
throw new Error("Invalid allergy index");
|
|
1302
1327
|
}
|
|
@@ -1321,9 +1346,9 @@ var addBlockingConditionUtil = async (db, patientId, data, userRef) => {
|
|
|
1321
1346
|
var updateBlockingConditionUtil = async (db, patientId, data, userRef) => {
|
|
1322
1347
|
const validatedData = updateBlockingConditionSchema.parse(data);
|
|
1323
1348
|
const { conditionIndex, ...updateData } = validatedData;
|
|
1324
|
-
const
|
|
1325
|
-
if (!
|
|
1326
|
-
const medicalInfo =
|
|
1349
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1350
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1351
|
+
const medicalInfo = doc20.data();
|
|
1327
1352
|
if (conditionIndex >= medicalInfo.blockingConditions.length) {
|
|
1328
1353
|
throw new Error("Invalid blocking condition index");
|
|
1329
1354
|
}
|
|
@@ -1339,9 +1364,9 @@ var updateBlockingConditionUtil = async (db, patientId, data, userRef) => {
|
|
|
1339
1364
|
});
|
|
1340
1365
|
};
|
|
1341
1366
|
var removeBlockingConditionUtil = async (db, patientId, conditionIndex, userRef) => {
|
|
1342
|
-
const
|
|
1343
|
-
if (!
|
|
1344
|
-
const medicalInfo =
|
|
1367
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1368
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1369
|
+
const medicalInfo = doc20.data();
|
|
1345
1370
|
if (conditionIndex >= medicalInfo.blockingConditions.length) {
|
|
1346
1371
|
throw new Error("Invalid blocking condition index");
|
|
1347
1372
|
}
|
|
@@ -1366,9 +1391,9 @@ var addContraindicationUtil = async (db, patientId, data, userRef) => {
|
|
|
1366
1391
|
var updateContraindicationUtil = async (db, patientId, data, userRef) => {
|
|
1367
1392
|
const validatedData = updateContraindicationSchema.parse(data);
|
|
1368
1393
|
const { contraindicationIndex, ...updateData } = validatedData;
|
|
1369
|
-
const
|
|
1370
|
-
if (!
|
|
1371
|
-
const medicalInfo =
|
|
1394
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1395
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1396
|
+
const medicalInfo = doc20.data();
|
|
1372
1397
|
if (contraindicationIndex >= medicalInfo.contraindications.length) {
|
|
1373
1398
|
throw new Error("Invalid contraindication index");
|
|
1374
1399
|
}
|
|
@@ -1384,9 +1409,9 @@ var updateContraindicationUtil = async (db, patientId, data, userRef) => {
|
|
|
1384
1409
|
});
|
|
1385
1410
|
};
|
|
1386
1411
|
var removeContraindicationUtil = async (db, patientId, contraindicationIndex, userRef) => {
|
|
1387
|
-
const
|
|
1388
|
-
if (!
|
|
1389
|
-
const medicalInfo =
|
|
1412
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1413
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1414
|
+
const medicalInfo = doc20.data();
|
|
1390
1415
|
if (contraindicationIndex >= medicalInfo.contraindications.length) {
|
|
1391
1416
|
throw new Error("Invalid contraindication index");
|
|
1392
1417
|
}
|
|
@@ -1411,9 +1436,9 @@ var addMedicationUtil = async (db, patientId, data, userRef) => {
|
|
|
1411
1436
|
var updateMedicationUtil = async (db, patientId, data, userRef) => {
|
|
1412
1437
|
const validatedData = updateMedicationSchema.parse(data);
|
|
1413
1438
|
const { medicationIndex, ...updateData } = validatedData;
|
|
1414
|
-
const
|
|
1415
|
-
if (!
|
|
1416
|
-
const medicalInfo =
|
|
1439
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1440
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1441
|
+
const medicalInfo = doc20.data();
|
|
1417
1442
|
if (medicationIndex >= medicalInfo.currentMedications.length) {
|
|
1418
1443
|
throw new Error("Invalid medication index");
|
|
1419
1444
|
}
|
|
@@ -1429,9 +1454,9 @@ var updateMedicationUtil = async (db, patientId, data, userRef) => {
|
|
|
1429
1454
|
});
|
|
1430
1455
|
};
|
|
1431
1456
|
var removeMedicationUtil = async (db, patientId, medicationIndex, userRef) => {
|
|
1432
|
-
const
|
|
1433
|
-
if (!
|
|
1434
|
-
const medicalInfo =
|
|
1457
|
+
const doc20 = await getDoc3(getMedicalInfoDocRef(db, patientId));
|
|
1458
|
+
if (!doc20.exists()) throw new Error("Medical info not found");
|
|
1459
|
+
const medicalInfo = doc20.data();
|
|
1435
1460
|
if (medicationIndex >= medicalInfo.currentMedications.length) {
|
|
1436
1461
|
throw new Error("Invalid medication index");
|
|
1437
1462
|
}
|
|
@@ -2247,14 +2272,14 @@ var TreatmentBenefit = /* @__PURE__ */ ((TreatmentBenefit2) => {
|
|
|
2247
2272
|
})(TreatmentBenefit || {});
|
|
2248
2273
|
|
|
2249
2274
|
// src/backoffice/types/static/pricing.types.ts
|
|
2250
|
-
var PricingMeasure = /* @__PURE__ */ ((
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
return
|
|
2275
|
+
var PricingMeasure = /* @__PURE__ */ ((PricingMeasure3) => {
|
|
2276
|
+
PricingMeasure3["PER_ML"] = "per_ml";
|
|
2277
|
+
PricingMeasure3["PER_ZONE"] = "per_zone";
|
|
2278
|
+
PricingMeasure3["PER_AREA"] = "per_area";
|
|
2279
|
+
PricingMeasure3["PER_SESSION"] = "per_session";
|
|
2280
|
+
PricingMeasure3["PER_TREATMENT"] = "per_treatment";
|
|
2281
|
+
PricingMeasure3["PER_PACKAGE"] = "per_package";
|
|
2282
|
+
return PricingMeasure3;
|
|
2258
2283
|
})(PricingMeasure || {});
|
|
2259
2284
|
var Currency = /* @__PURE__ */ ((Currency2) => {
|
|
2260
2285
|
Currency2["EUR"] = "EUR";
|
|
@@ -2434,7 +2459,7 @@ var clinicSchema = z9.object({
|
|
|
2434
2459
|
workingHours: workingHoursSchema,
|
|
2435
2460
|
tags: z9.array(z9.nativeEnum(ClinicTag)),
|
|
2436
2461
|
featuredPhotos: z9.array(z9.string()),
|
|
2437
|
-
|
|
2462
|
+
coverPhoto: z9.string().nullable(),
|
|
2438
2463
|
photosWithTags: z9.array(
|
|
2439
2464
|
z9.object({
|
|
2440
2465
|
url: z9.string(),
|
|
@@ -2493,7 +2518,7 @@ var createClinicSchema = z9.object({
|
|
|
2493
2518
|
contactInfo: clinicContactInfoSchema,
|
|
2494
2519
|
workingHours: workingHoursSchema,
|
|
2495
2520
|
tags: z9.array(z9.nativeEnum(ClinicTag)),
|
|
2496
|
-
|
|
2521
|
+
coverPhoto: z9.string().nullable(),
|
|
2497
2522
|
photosWithTags: z9.array(
|
|
2498
2523
|
z9.object({
|
|
2499
2524
|
url: z9.string(),
|
|
@@ -2554,7 +2579,7 @@ var clinicBranchSetupSchema = z9.object({
|
|
|
2554
2579
|
workingHours: workingHoursSchema,
|
|
2555
2580
|
tags: z9.array(z9.nativeEnum(ClinicTag)),
|
|
2556
2581
|
logo: z9.string().optional(),
|
|
2557
|
-
|
|
2582
|
+
coverPhoto: z9.string().nullable(),
|
|
2558
2583
|
photosWithTags: z9.array(
|
|
2559
2584
|
z9.object({
|
|
2560
2585
|
url: z9.string(),
|
|
@@ -2748,7 +2773,7 @@ async function getClinicAdminsByGroup(db, clinicGroupId) {
|
|
|
2748
2773
|
where2("clinicGroupId", "==", clinicGroupId)
|
|
2749
2774
|
);
|
|
2750
2775
|
const querySnapshot = await getDocs2(q);
|
|
2751
|
-
return querySnapshot.docs.map((
|
|
2776
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
2752
2777
|
}
|
|
2753
2778
|
async function updateClinicAdmin(db, adminId, data) {
|
|
2754
2779
|
const admin = await getClinicAdmin(db, adminId);
|
|
@@ -3096,6 +3121,21 @@ var practitionerWorkingHoursSchema = z10.object({
|
|
|
3096
3121
|
createdAt: z10.instanceof(Timestamp7),
|
|
3097
3122
|
updatedAt: z10.instanceof(Timestamp7)
|
|
3098
3123
|
});
|
|
3124
|
+
var practitionerClinicWorkingHoursSchema = z10.object({
|
|
3125
|
+
clinicId: z10.string().min(1),
|
|
3126
|
+
workingHours: z10.object({
|
|
3127
|
+
monday: timeSlotSchema,
|
|
3128
|
+
tuesday: timeSlotSchema,
|
|
3129
|
+
wednesday: timeSlotSchema,
|
|
3130
|
+
thursday: timeSlotSchema,
|
|
3131
|
+
friday: timeSlotSchema,
|
|
3132
|
+
saturday: timeSlotSchema,
|
|
3133
|
+
sunday: timeSlotSchema
|
|
3134
|
+
}),
|
|
3135
|
+
isActive: z10.boolean(),
|
|
3136
|
+
createdAt: z10.instanceof(Timestamp7),
|
|
3137
|
+
updatedAt: z10.instanceof(Timestamp7)
|
|
3138
|
+
});
|
|
3099
3139
|
var practitionerReviewSchema = z10.object({
|
|
3100
3140
|
id: z10.string().min(1),
|
|
3101
3141
|
practitionerId: z10.string().min(1),
|
|
@@ -3121,6 +3161,7 @@ var practitionerSchema = z10.object({
|
|
|
3121
3161
|
basicInfo: practitionerBasicInfoSchema,
|
|
3122
3162
|
certification: practitionerCertificationSchema,
|
|
3123
3163
|
clinics: z10.array(z10.string()),
|
|
3164
|
+
clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema),
|
|
3124
3165
|
isActive: z10.boolean(),
|
|
3125
3166
|
isVerified: z10.boolean(),
|
|
3126
3167
|
createdAt: z10.instanceof(Timestamp7),
|
|
@@ -3131,6 +3172,7 @@ var createPractitionerSchema = z10.object({
|
|
|
3131
3172
|
basicInfo: practitionerBasicInfoSchema,
|
|
3132
3173
|
certification: practitionerCertificationSchema,
|
|
3133
3174
|
clinics: z10.array(z10.string()).optional(),
|
|
3175
|
+
clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
3134
3176
|
isActive: z10.boolean(),
|
|
3135
3177
|
isVerified: z10.boolean()
|
|
3136
3178
|
});
|
|
@@ -3180,6 +3222,7 @@ var PractitionerService = class extends BaseService {
|
|
|
3180
3222
|
basicInfo: validatedData.basicInfo,
|
|
3181
3223
|
certification: validatedData.certification,
|
|
3182
3224
|
clinics: validatedData.clinics || [],
|
|
3225
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
3183
3226
|
isActive: validatedData.isActive,
|
|
3184
3227
|
isVerified: validatedData.isVerified,
|
|
3185
3228
|
createdAt: serverTimestamp9(),
|
|
@@ -3242,7 +3285,7 @@ var PractitionerService = class extends BaseService {
|
|
|
3242
3285
|
where3("isActive", "==", true)
|
|
3243
3286
|
);
|
|
3244
3287
|
const querySnapshot = await getDocs3(q);
|
|
3245
|
-
return querySnapshot.docs.map((
|
|
3288
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
3246
3289
|
}
|
|
3247
3290
|
/**
|
|
3248
3291
|
* Ažurira profil zdravstvenog radnika
|
|
@@ -3522,7 +3565,7 @@ var UserService = class extends BaseService {
|
|
|
3522
3565
|
];
|
|
3523
3566
|
const q = query4(collection4(this.db, USERS_COLLECTION), ...constraints);
|
|
3524
3567
|
const querySnapshot = await getDocs4(q);
|
|
3525
|
-
const users = querySnapshot.docs.map((
|
|
3568
|
+
const users = querySnapshot.docs.map((doc20) => doc20.data());
|
|
3526
3569
|
return Promise.all(users.map((userData) => userSchema.parse(userData)));
|
|
3527
3570
|
}
|
|
3528
3571
|
/**
|
|
@@ -3921,7 +3964,7 @@ async function getAllActiveGroups(db) {
|
|
|
3921
3964
|
where5("isActive", "==", true)
|
|
3922
3965
|
);
|
|
3923
3966
|
const querySnapshot = await getDocs5(q);
|
|
3924
|
-
return querySnapshot.docs.map((
|
|
3967
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
3925
3968
|
}
|
|
3926
3969
|
async function updateClinicGroup(db, groupId, data, app) {
|
|
3927
3970
|
console.log("[CLINIC_GROUP] Updating clinic group", { groupId });
|
|
@@ -4388,25 +4431,23 @@ async function createClinic(db, data, creatorAdminId, clinicGroupService, clinic
|
|
|
4388
4431
|
console.error("[CLINIC] Error processing logo:", logoError);
|
|
4389
4432
|
}
|
|
4390
4433
|
}
|
|
4391
|
-
let
|
|
4392
|
-
if (validatedData.
|
|
4393
|
-
console.log("[CLINIC] Processing
|
|
4434
|
+
let processedCoverPhoto = null;
|
|
4435
|
+
if (validatedData.coverPhoto) {
|
|
4436
|
+
console.log("[CLINIC] Processing cover photo");
|
|
4394
4437
|
try {
|
|
4395
|
-
|
|
4396
|
-
validatedData.
|
|
4438
|
+
processedCoverPhoto = await uploadPhoto(
|
|
4439
|
+
validatedData.coverPhoto,
|
|
4397
4440
|
"clinics",
|
|
4398
4441
|
clinicId,
|
|
4399
|
-
"
|
|
4442
|
+
"cover",
|
|
4400
4443
|
app
|
|
4401
4444
|
);
|
|
4402
|
-
console.log("[CLINIC]
|
|
4403
|
-
|
|
4445
|
+
console.log("[CLINIC] Cover photo processed", {
|
|
4446
|
+
coverPhoto: processedCoverPhoto
|
|
4404
4447
|
});
|
|
4405
|
-
} catch (
|
|
4406
|
-
console.error("[CLINIC] Error processing
|
|
4407
|
-
|
|
4408
|
-
(photo) => !photo.startsWith("data:")
|
|
4409
|
-
);
|
|
4448
|
+
} catch (coverPhotoError) {
|
|
4449
|
+
console.error("[CLINIC] Error processing cover photo:", coverPhotoError);
|
|
4450
|
+
processedCoverPhoto = validatedData.coverPhoto.startsWith("data:") ? null : validatedData.coverPhoto;
|
|
4410
4451
|
}
|
|
4411
4452
|
}
|
|
4412
4453
|
let processedFeaturedPhotos = [];
|
|
@@ -4492,7 +4533,7 @@ async function createClinic(db, data, creatorAdminId, clinicGroupService, clinic
|
|
|
4492
4533
|
logo: logoUrl || "",
|
|
4493
4534
|
tags: validatedData.tags || [],
|
|
4494
4535
|
featuredPhotos: processedFeaturedPhotos || [],
|
|
4495
|
-
|
|
4536
|
+
coverPhoto: processedCoverPhoto,
|
|
4496
4537
|
photosWithTags: processedPhotosWithTags,
|
|
4497
4538
|
doctors: [],
|
|
4498
4539
|
doctorsInfo: [],
|
|
@@ -4593,7 +4634,7 @@ async function getClinicsByGroup(db, groupId) {
|
|
|
4593
4634
|
where6("isActive", "==", true)
|
|
4594
4635
|
);
|
|
4595
4636
|
const querySnapshot = await getDocs6(q);
|
|
4596
|
-
return querySnapshot.docs.map((
|
|
4637
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
4597
4638
|
}
|
|
4598
4639
|
async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app) {
|
|
4599
4640
|
console.log("[CLINIC] Starting clinic update", { clinicId, adminId });
|
|
@@ -4647,36 +4688,32 @@ async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app
|
|
|
4647
4688
|
console.error("[CLINIC] Error processing logo update:", logoError);
|
|
4648
4689
|
}
|
|
4649
4690
|
}
|
|
4650
|
-
if (data.
|
|
4651
|
-
console.log("[CLINIC] Processing
|
|
4691
|
+
if (data.coverPhoto) {
|
|
4692
|
+
console.log("[CLINIC] Processing cover photo update");
|
|
4652
4693
|
try {
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
const existingPhotos = data.photos.filter(
|
|
4657
|
-
(photo) => typeof photo === "string" && !photo.startsWith("data:")
|
|
4658
|
-
);
|
|
4659
|
-
if (dataUrlPhotos.length > 0) {
|
|
4660
|
-
const uploadedPhotos = await uploadMultiplePhotos(
|
|
4661
|
-
dataUrlPhotos,
|
|
4694
|
+
if (typeof data.coverPhoto === "string" && data.coverPhoto.startsWith("data:")) {
|
|
4695
|
+
const uploadedPhoto = await uploadPhoto(
|
|
4696
|
+
data.coverPhoto,
|
|
4662
4697
|
"clinics",
|
|
4663
4698
|
clinicId,
|
|
4664
|
-
"
|
|
4699
|
+
"cover",
|
|
4665
4700
|
app
|
|
4666
4701
|
);
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
}
|
|
4670
|
-
|
|
4702
|
+
if (uploadedPhoto) {
|
|
4703
|
+
updatedData.coverPhoto = uploadedPhoto;
|
|
4704
|
+
}
|
|
4705
|
+
} else {
|
|
4706
|
+
updatedData.coverPhoto = data.coverPhoto;
|
|
4671
4707
|
}
|
|
4672
|
-
|
|
4708
|
+
console.log("[CLINIC] Cover photo update processed");
|
|
4709
|
+
} catch (photoError) {
|
|
4673
4710
|
console.error(
|
|
4674
|
-
"[CLINIC] Error processing
|
|
4675
|
-
|
|
4676
|
-
);
|
|
4677
|
-
updatedData.photos = data.photos.filter(
|
|
4678
|
-
(photo) => typeof photo === "string" && !photo.startsWith("data:")
|
|
4711
|
+
"[CLINIC] Error processing cover photo update:",
|
|
4712
|
+
photoError
|
|
4679
4713
|
);
|
|
4714
|
+
if (typeof data.coverPhoto === "string" && !data.coverPhoto.startsWith("data:")) {
|
|
4715
|
+
updatedData.coverPhoto = data.coverPhoto;
|
|
4716
|
+
}
|
|
4680
4717
|
}
|
|
4681
4718
|
}
|
|
4682
4719
|
if (data.featuredPhotos && data.featuredPhotos.length > 0) {
|
|
@@ -4700,6 +4737,8 @@ async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app
|
|
|
4700
4737
|
count: uploadedPhotos.length
|
|
4701
4738
|
});
|
|
4702
4739
|
updatedData.featuredPhotos = [...existingPhotos, ...uploadedPhotos];
|
|
4740
|
+
} else {
|
|
4741
|
+
updatedData.featuredPhotos = existingPhotos;
|
|
4703
4742
|
}
|
|
4704
4743
|
} catch (featuredError) {
|
|
4705
4744
|
console.error(
|
|
@@ -4807,7 +4846,7 @@ async function getClinicsByAdmin(db, adminId, options = {}, clinicAdminService,
|
|
|
4807
4846
|
}
|
|
4808
4847
|
const q = query6(collection6(db, CLINICS_COLLECTION), ...constraints);
|
|
4809
4848
|
const querySnapshot = await getDocs6(q);
|
|
4810
|
-
return querySnapshot.docs.map((
|
|
4849
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
4811
4850
|
}
|
|
4812
4851
|
async function getActiveClinicsByAdmin(db, adminId, clinicAdminService, clinicGroupService) {
|
|
4813
4852
|
return getClinicsByAdmin(
|
|
@@ -4960,8 +4999,8 @@ async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
|
4960
4999
|
}
|
|
4961
5000
|
const q = query7(collection8(db, CLINICS_COLLECTION), ...constraints);
|
|
4962
5001
|
const querySnapshot = await getDocs7(q);
|
|
4963
|
-
for (const
|
|
4964
|
-
const clinic =
|
|
5002
|
+
for (const doc20 of querySnapshot.docs) {
|
|
5003
|
+
const clinic = doc20.data();
|
|
4965
5004
|
const distance = distanceBetween(
|
|
4966
5005
|
[center.latitude, center.longitude],
|
|
4967
5006
|
[clinic.location.latitude, clinic.location.longitude]
|
|
@@ -5139,7 +5178,7 @@ var ClinicService = class extends BaseService {
|
|
|
5139
5178
|
contactInfo: setupData.contactInfo,
|
|
5140
5179
|
workingHours: setupData.workingHours,
|
|
5141
5180
|
tags: setupData.tags,
|
|
5142
|
-
|
|
5181
|
+
coverPhoto: setupData.coverPhoto || null,
|
|
5143
5182
|
photosWithTags: setupData.photosWithTags || [],
|
|
5144
5183
|
doctors: [],
|
|
5145
5184
|
services: [],
|
|
@@ -5152,7 +5191,7 @@ var ClinicService = class extends BaseService {
|
|
|
5152
5191
|
console.log("[CLINIC_SERVICE] Creating clinic branch with data", {
|
|
5153
5192
|
name: createClinicData.name,
|
|
5154
5193
|
hasLogo: !!createClinicData.logo,
|
|
5155
|
-
|
|
5194
|
+
hasCoverPhoto: !!createClinicData.coverPhoto,
|
|
5156
5195
|
featuredPhotosCount: ((_a = createClinicData.featuredPhotos) == null ? void 0 : _a.length) || 0,
|
|
5157
5196
|
photosWithTagsCount: ((_b = createClinicData.photosWithTags) == null ? void 0 : _b.length) || 0
|
|
5158
5197
|
});
|
|
@@ -5835,9 +5874,9 @@ var NotificationService = class extends BaseService {
|
|
|
5835
5874
|
orderBy("notificationTime", "desc")
|
|
5836
5875
|
);
|
|
5837
5876
|
const querySnapshot = await getDocs9(q);
|
|
5838
|
-
return querySnapshot.docs.map((
|
|
5839
|
-
id:
|
|
5840
|
-
...
|
|
5877
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
5878
|
+
id: doc20.id,
|
|
5879
|
+
...doc20.data()
|
|
5841
5880
|
}));
|
|
5842
5881
|
}
|
|
5843
5882
|
/**
|
|
@@ -5851,9 +5890,9 @@ var NotificationService = class extends BaseService {
|
|
|
5851
5890
|
orderBy("notificationTime", "desc")
|
|
5852
5891
|
);
|
|
5853
5892
|
const querySnapshot = await getDocs9(q);
|
|
5854
|
-
return querySnapshot.docs.map((
|
|
5855
|
-
id:
|
|
5856
|
-
...
|
|
5893
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
5894
|
+
id: doc20.id,
|
|
5895
|
+
...doc20.data()
|
|
5857
5896
|
}));
|
|
5858
5897
|
}
|
|
5859
5898
|
/**
|
|
@@ -5925,9 +5964,9 @@ var NotificationService = class extends BaseService {
|
|
|
5925
5964
|
orderBy("notificationTime", "desc")
|
|
5926
5965
|
);
|
|
5927
5966
|
const querySnapshot = await getDocs9(q);
|
|
5928
|
-
return querySnapshot.docs.map((
|
|
5929
|
-
id:
|
|
5930
|
-
...
|
|
5967
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
5968
|
+
id: doc20.id,
|
|
5969
|
+
...doc20.data()
|
|
5931
5970
|
}));
|
|
5932
5971
|
}
|
|
5933
5972
|
/**
|
|
@@ -5940,9 +5979,9 @@ var NotificationService = class extends BaseService {
|
|
|
5940
5979
|
orderBy("notificationTime", "desc")
|
|
5941
5980
|
);
|
|
5942
5981
|
const querySnapshot = await getDocs9(q);
|
|
5943
|
-
return querySnapshot.docs.map((
|
|
5944
|
-
id:
|
|
5945
|
-
...
|
|
5982
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
5983
|
+
id: doc20.id,
|
|
5984
|
+
...doc20.data()
|
|
5946
5985
|
}));
|
|
5947
5986
|
}
|
|
5948
5987
|
};
|
|
@@ -6072,9 +6111,9 @@ var DocumentationTemplateService = class extends BaseService {
|
|
|
6072
6111
|
const querySnapshot = await getDocs10(q);
|
|
6073
6112
|
const templates = [];
|
|
6074
6113
|
let lastVisible = null;
|
|
6075
|
-
querySnapshot.forEach((
|
|
6076
|
-
templates.push(
|
|
6077
|
-
lastVisible =
|
|
6114
|
+
querySnapshot.forEach((doc20) => {
|
|
6115
|
+
templates.push(doc20.data());
|
|
6116
|
+
lastVisible = doc20;
|
|
6078
6117
|
});
|
|
6079
6118
|
return {
|
|
6080
6119
|
templates,
|
|
@@ -6102,9 +6141,9 @@ var DocumentationTemplateService = class extends BaseService {
|
|
|
6102
6141
|
const querySnapshot = await getDocs10(q);
|
|
6103
6142
|
const templates = [];
|
|
6104
6143
|
let lastVisible = null;
|
|
6105
|
-
querySnapshot.forEach((
|
|
6106
|
-
templates.push(
|
|
6107
|
-
lastVisible =
|
|
6144
|
+
querySnapshot.forEach((doc20) => {
|
|
6145
|
+
templates.push(doc20.data());
|
|
6146
|
+
lastVisible = doc20;
|
|
6108
6147
|
});
|
|
6109
6148
|
return {
|
|
6110
6149
|
templates,
|
|
@@ -6131,9 +6170,9 @@ var DocumentationTemplateService = class extends BaseService {
|
|
|
6131
6170
|
const querySnapshot = await getDocs10(q);
|
|
6132
6171
|
const templates = [];
|
|
6133
6172
|
let lastVisible = null;
|
|
6134
|
-
querySnapshot.forEach((
|
|
6135
|
-
templates.push(
|
|
6136
|
-
lastVisible =
|
|
6173
|
+
querySnapshot.forEach((doc20) => {
|
|
6174
|
+
templates.push(doc20.data());
|
|
6175
|
+
lastVisible = doc20;
|
|
6137
6176
|
});
|
|
6138
6177
|
return {
|
|
6139
6178
|
templates,
|
|
@@ -6258,9 +6297,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6258
6297
|
const querySnapshot = await getDocs11(q);
|
|
6259
6298
|
const documents = [];
|
|
6260
6299
|
let lastVisible = null;
|
|
6261
|
-
querySnapshot.forEach((
|
|
6262
|
-
documents.push(
|
|
6263
|
-
lastVisible =
|
|
6300
|
+
querySnapshot.forEach((doc20) => {
|
|
6301
|
+
documents.push(doc20.data());
|
|
6302
|
+
lastVisible = doc20;
|
|
6264
6303
|
});
|
|
6265
6304
|
return {
|
|
6266
6305
|
documents,
|
|
@@ -6287,9 +6326,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6287
6326
|
const querySnapshot = await getDocs11(q);
|
|
6288
6327
|
const documents = [];
|
|
6289
6328
|
let lastVisible = null;
|
|
6290
|
-
querySnapshot.forEach((
|
|
6291
|
-
documents.push(
|
|
6292
|
-
lastVisible =
|
|
6329
|
+
querySnapshot.forEach((doc20) => {
|
|
6330
|
+
documents.push(doc20.data());
|
|
6331
|
+
lastVisible = doc20;
|
|
6293
6332
|
});
|
|
6294
6333
|
return {
|
|
6295
6334
|
documents,
|
|
@@ -6316,9 +6355,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6316
6355
|
const querySnapshot = await getDocs11(q);
|
|
6317
6356
|
const documents = [];
|
|
6318
6357
|
let lastVisible = null;
|
|
6319
|
-
querySnapshot.forEach((
|
|
6320
|
-
documents.push(
|
|
6321
|
-
lastVisible =
|
|
6358
|
+
querySnapshot.forEach((doc20) => {
|
|
6359
|
+
documents.push(doc20.data());
|
|
6360
|
+
lastVisible = doc20;
|
|
6322
6361
|
});
|
|
6323
6362
|
return {
|
|
6324
6363
|
documents,
|
|
@@ -6345,9 +6384,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6345
6384
|
const querySnapshot = await getDocs11(q);
|
|
6346
6385
|
const documents = [];
|
|
6347
6386
|
let lastVisible = null;
|
|
6348
|
-
querySnapshot.forEach((
|
|
6349
|
-
documents.push(
|
|
6350
|
-
lastVisible =
|
|
6387
|
+
querySnapshot.forEach((doc20) => {
|
|
6388
|
+
documents.push(doc20.data());
|
|
6389
|
+
lastVisible = doc20;
|
|
6351
6390
|
});
|
|
6352
6391
|
return {
|
|
6353
6392
|
documents,
|
|
@@ -6374,9 +6413,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6374
6413
|
const querySnapshot = await getDocs11(q);
|
|
6375
6414
|
const documents = [];
|
|
6376
6415
|
let lastVisible = null;
|
|
6377
|
-
querySnapshot.forEach((
|
|
6378
|
-
documents.push(
|
|
6379
|
-
lastVisible =
|
|
6416
|
+
querySnapshot.forEach((doc20) => {
|
|
6417
|
+
documents.push(doc20.data());
|
|
6418
|
+
lastVisible = doc20;
|
|
6380
6419
|
});
|
|
6381
6420
|
return {
|
|
6382
6421
|
documents,
|
|
@@ -6385,55 +6424,2654 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6385
6424
|
}
|
|
6386
6425
|
};
|
|
6387
6426
|
|
|
6388
|
-
// src/
|
|
6427
|
+
// src/services/calendar/calendar-refactored.service.ts
|
|
6428
|
+
import { Timestamp as Timestamp23, serverTimestamp as serverTimestamp18 } from "firebase/firestore";
|
|
6429
|
+
|
|
6430
|
+
// src/types/calendar/synced-calendar.types.ts
|
|
6431
|
+
var SyncedCalendarProvider = /* @__PURE__ */ ((SyncedCalendarProvider3) => {
|
|
6432
|
+
SyncedCalendarProvider3["GOOGLE"] = "google";
|
|
6433
|
+
SyncedCalendarProvider3["OUTLOOK"] = "outlook";
|
|
6434
|
+
SyncedCalendarProvider3["APPLE"] = "apple";
|
|
6435
|
+
return SyncedCalendarProvider3;
|
|
6436
|
+
})(SyncedCalendarProvider || {});
|
|
6437
|
+
var SYNCED_CALENDARS_COLLECTION = "syncedCalendars";
|
|
6438
|
+
|
|
6439
|
+
// src/services/calendar/calendar-refactored.service.ts
|
|
6440
|
+
import {
|
|
6441
|
+
doc as doc19,
|
|
6442
|
+
getDoc as getDoc22,
|
|
6443
|
+
collection as collection17,
|
|
6444
|
+
query as query16,
|
|
6445
|
+
where as where16,
|
|
6446
|
+
getDocs as getDocs16,
|
|
6447
|
+
setDoc as setDoc19,
|
|
6448
|
+
updateDoc as updateDoc20
|
|
6449
|
+
} from "firebase/firestore";
|
|
6450
|
+
|
|
6451
|
+
// src/validations/calendar.schema.ts
|
|
6452
|
+
import { z as z17 } from "zod";
|
|
6453
|
+
import { Timestamp as Timestamp17 } from "firebase/firestore";
|
|
6454
|
+
|
|
6455
|
+
// src/validations/profile-info.schema.ts
|
|
6389
6456
|
import { z as z16 } from "zod";
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6457
|
+
import { Timestamp as Timestamp16 } from "firebase/firestore";
|
|
6458
|
+
var clinicInfoSchema2 = z16.object({
|
|
6459
|
+
id: z16.string(),
|
|
6460
|
+
featuredPhoto: z16.string(),
|
|
6461
|
+
name: z16.string(),
|
|
6462
|
+
description: z16.string(),
|
|
6463
|
+
location: clinicLocationSchema,
|
|
6464
|
+
contactInfo: clinicContactInfoSchema
|
|
6465
|
+
});
|
|
6466
|
+
var practitionerProfileInfoSchema = z16.object({
|
|
6467
|
+
id: z16.string(),
|
|
6468
|
+
practitionerPhoto: z16.string().nullable(),
|
|
6469
|
+
name: z16.string(),
|
|
6470
|
+
email: z16.string().email(),
|
|
6471
|
+
phone: z16.string().nullable(),
|
|
6472
|
+
certification: practitionerCertificationSchema
|
|
6473
|
+
});
|
|
6474
|
+
var patientProfileInfoSchema = z16.object({
|
|
6475
|
+
id: z16.string(),
|
|
6476
|
+
fullName: z16.string(),
|
|
6477
|
+
email: z16.string().email(),
|
|
6478
|
+
phone: z16.string().nullable(),
|
|
6479
|
+
dateOfBirth: z16.instanceof(Timestamp16),
|
|
6480
|
+
gender: z16.nativeEnum(Gender)
|
|
6481
|
+
});
|
|
6482
|
+
|
|
6483
|
+
// src/validations/calendar.schema.ts
|
|
6484
|
+
var MIN_APPOINTMENT_DURATION = 15;
|
|
6485
|
+
var calendarEventTimeSchema = z17.object({
|
|
6486
|
+
start: z17.instanceof(Date).or(z17.instanceof(Timestamp17)),
|
|
6487
|
+
end: z17.instanceof(Date).or(z17.instanceof(Timestamp17))
|
|
6488
|
+
}).refine(
|
|
6489
|
+
(data) => {
|
|
6490
|
+
const startDate = data.start instanceof Timestamp17 ? data.start.toDate() : data.start;
|
|
6491
|
+
const endDate = data.end instanceof Timestamp17 ? data.end.toDate() : data.end;
|
|
6492
|
+
return startDate < endDate;
|
|
6493
|
+
},
|
|
6494
|
+
{
|
|
6495
|
+
message: "End time must be after start time",
|
|
6496
|
+
path: ["end"]
|
|
6497
|
+
}
|
|
6498
|
+
).refine(
|
|
6499
|
+
(data) => {
|
|
6500
|
+
const startDate = data.start instanceof Timestamp17 ? data.start.toDate() : data.start;
|
|
6501
|
+
return startDate > /* @__PURE__ */ new Date();
|
|
6502
|
+
},
|
|
6503
|
+
{
|
|
6504
|
+
message: "Appointment must be scheduled in the future",
|
|
6505
|
+
path: ["start"]
|
|
6506
|
+
}
|
|
6507
|
+
);
|
|
6508
|
+
var timeSlotSchema2 = z17.object({
|
|
6509
|
+
start: z17.date(),
|
|
6510
|
+
end: z17.date(),
|
|
6511
|
+
isAvailable: z17.boolean()
|
|
6512
|
+
}).refine((data) => data.start < data.end, {
|
|
6513
|
+
message: "End time must be after start time",
|
|
6514
|
+
path: ["end"]
|
|
6515
|
+
});
|
|
6516
|
+
var syncedCalendarEventSchema = z17.object({
|
|
6517
|
+
eventId: z17.string(),
|
|
6518
|
+
syncedCalendarProvider: z17.nativeEnum(SyncedCalendarProvider),
|
|
6519
|
+
syncedAt: z17.instanceof(Date).or(z17.instanceof(Timestamp17))
|
|
6520
|
+
});
|
|
6521
|
+
var procedureInfoSchema = z17.object({
|
|
6522
|
+
name: z17.string(),
|
|
6523
|
+
description: z17.string(),
|
|
6524
|
+
duration: z17.number().min(MIN_APPOINTMENT_DURATION),
|
|
6525
|
+
price: z17.number().min(0),
|
|
6526
|
+
currency: z17.nativeEnum(Currency)
|
|
6527
|
+
});
|
|
6528
|
+
var procedureCategorizationSchema = z17.object({
|
|
6529
|
+
procedureFamily: z17.string(),
|
|
6530
|
+
// Replace with proper enum when available
|
|
6531
|
+
procedureCategory: z17.string(),
|
|
6532
|
+
// Replace with proper enum when available
|
|
6533
|
+
procedureSubcategory: z17.string(),
|
|
6534
|
+
// Replace with proper enum when available
|
|
6535
|
+
procedureTechnology: z17.string(),
|
|
6536
|
+
// Replace with proper enum when available
|
|
6537
|
+
procedureProduct: z17.string()
|
|
6538
|
+
// Replace with proper enum when available
|
|
6539
|
+
});
|
|
6540
|
+
var createAppointmentSchema = z17.object({
|
|
6541
|
+
clinicId: z17.string().min(1, "Clinic ID is required"),
|
|
6542
|
+
doctorId: z17.string().min(1, "Doctor ID is required"),
|
|
6543
|
+
patientId: z17.string().min(1, "Patient ID is required"),
|
|
6544
|
+
procedureId: z17.string().min(1, "Procedure ID is required"),
|
|
6545
|
+
eventLocation: clinicLocationSchema,
|
|
6546
|
+
eventTime: calendarEventTimeSchema,
|
|
6547
|
+
description: z17.string().optional()
|
|
6548
|
+
}).refine(
|
|
6549
|
+
(data) => {
|
|
6550
|
+
return true;
|
|
6551
|
+
},
|
|
6552
|
+
{
|
|
6553
|
+
message: "Invalid appointment parameters"
|
|
6554
|
+
}
|
|
6555
|
+
);
|
|
6556
|
+
var updateAppointmentSchema = z17.object({
|
|
6557
|
+
appointmentId: z17.string().min(1, "Appointment ID is required"),
|
|
6558
|
+
clinicId: z17.string().min(1, "Clinic ID is required"),
|
|
6559
|
+
doctorId: z17.string().min(1, "Doctor ID is required"),
|
|
6560
|
+
patientId: z17.string().min(1, "Patient ID is required"),
|
|
6561
|
+
eventTime: calendarEventTimeSchema.optional(),
|
|
6562
|
+
description: z17.string().optional(),
|
|
6563
|
+
status: z17.nativeEnum(CalendarEventStatus).optional()
|
|
6564
|
+
});
|
|
6565
|
+
var createCalendarEventSchema = z17.object({
|
|
6566
|
+
id: z17.string(),
|
|
6567
|
+
clinicBranchId: z17.string().nullable().optional(),
|
|
6568
|
+
clinicBranchInfo: z17.any().nullable().optional(),
|
|
6569
|
+
practitionerProfileId: z17.string().nullable().optional(),
|
|
6570
|
+
practitionerProfileInfo: practitionerProfileInfoSchema.nullable().optional(),
|
|
6571
|
+
patientProfileId: z17.string().nullable().optional(),
|
|
6572
|
+
patientProfileInfo: patientProfileInfoSchema.nullable().optional(),
|
|
6573
|
+
procedureId: z17.string().nullable().optional(),
|
|
6574
|
+
appointmentId: z17.string().nullable().optional(),
|
|
6575
|
+
syncedCalendarEventId: z17.array(syncedCalendarEventSchema).nullable().optional(),
|
|
6576
|
+
eventName: z17.string().min(1, "Event name is required"),
|
|
6577
|
+
eventLocation: clinicLocationSchema.optional(),
|
|
6578
|
+
eventTime: calendarEventTimeSchema,
|
|
6579
|
+
description: z17.string().optional(),
|
|
6580
|
+
status: z17.nativeEnum(CalendarEventStatus),
|
|
6581
|
+
syncStatus: z17.nativeEnum(CalendarSyncStatus),
|
|
6582
|
+
eventType: z17.nativeEnum(CalendarEventType),
|
|
6583
|
+
createdAt: z17.any(),
|
|
6584
|
+
// FieldValue for server timestamp
|
|
6585
|
+
updatedAt: z17.any()
|
|
6586
|
+
// FieldValue for server timestamp
|
|
6587
|
+
});
|
|
6588
|
+
var updateCalendarEventSchema = z17.object({
|
|
6589
|
+
syncedCalendarEventId: z17.array(syncedCalendarEventSchema).nullable().optional(),
|
|
6590
|
+
appointmentId: z17.string().nullable().optional(),
|
|
6591
|
+
eventName: z17.string().optional(),
|
|
6592
|
+
eventTime: calendarEventTimeSchema.optional(),
|
|
6593
|
+
description: z17.string().optional(),
|
|
6594
|
+
status: z17.nativeEnum(CalendarEventStatus).optional(),
|
|
6595
|
+
syncStatus: z17.nativeEnum(CalendarSyncStatus).optional(),
|
|
6596
|
+
eventType: z17.nativeEnum(CalendarEventType).optional(),
|
|
6597
|
+
updatedAt: z17.any()
|
|
6598
|
+
// FieldValue for server timestamp
|
|
6599
|
+
});
|
|
6600
|
+
var calendarEventSchema = z17.object({
|
|
6601
|
+
id: z17.string(),
|
|
6602
|
+
clinicBranchId: z17.string().nullable().optional(),
|
|
6603
|
+
clinicBranchInfo: z17.any().nullable().optional(),
|
|
6604
|
+
// Will be replaced with proper clinic info schema
|
|
6605
|
+
practitionerProfileId: z17.string().nullable().optional(),
|
|
6606
|
+
practitionerProfileInfo: practitionerProfileInfoSchema.nullable().optional(),
|
|
6607
|
+
patientProfileId: z17.string().nullable().optional(),
|
|
6608
|
+
patientProfileInfo: patientProfileInfoSchema.nullable().optional(),
|
|
6609
|
+
procedureId: z17.string().nullable().optional(),
|
|
6610
|
+
procedureInfo: procedureInfoSchema.nullable().optional(),
|
|
6611
|
+
procedureCategorization: procedureCategorizationSchema.nullable().optional(),
|
|
6612
|
+
appointmentId: z17.string().nullable().optional(),
|
|
6613
|
+
syncedCalendarEventId: z17.array(syncedCalendarEventSchema).nullable().optional(),
|
|
6614
|
+
eventName: z17.string(),
|
|
6615
|
+
eventLocation: clinicLocationSchema.optional(),
|
|
6616
|
+
eventTime: calendarEventTimeSchema,
|
|
6617
|
+
description: z17.string().optional(),
|
|
6618
|
+
status: z17.nativeEnum(CalendarEventStatus),
|
|
6619
|
+
syncStatus: z17.nativeEnum(CalendarSyncStatus),
|
|
6620
|
+
eventType: z17.nativeEnum(CalendarEventType),
|
|
6621
|
+
createdAt: z17.instanceof(Date).or(z17.instanceof(Timestamp17)),
|
|
6622
|
+
updatedAt: z17.instanceof(Date).or(z17.instanceof(Timestamp17))
|
|
6623
|
+
});
|
|
6624
|
+
|
|
6625
|
+
// src/services/calendar/utils/clinic.utils.ts
|
|
6626
|
+
import {
|
|
6627
|
+
collection as collection13,
|
|
6628
|
+
doc as doc15,
|
|
6629
|
+
getDoc as getDoc18,
|
|
6630
|
+
getDocs as getDocs12,
|
|
6631
|
+
setDoc as setDoc15,
|
|
6632
|
+
updateDoc as updateDoc16,
|
|
6633
|
+
deleteDoc as deleteDoc8,
|
|
6634
|
+
query as query12,
|
|
6635
|
+
where as where12,
|
|
6636
|
+
orderBy as orderBy4,
|
|
6637
|
+
Timestamp as Timestamp18,
|
|
6638
|
+
serverTimestamp as serverTimestamp14
|
|
6639
|
+
} from "firebase/firestore";
|
|
6640
|
+
|
|
6641
|
+
// src/services/calendar/utils/docs.utils.ts
|
|
6642
|
+
import { doc as doc14 } from "firebase/firestore";
|
|
6643
|
+
function getPractitionerCalendarEventDocRef(db, practitionerId, eventId) {
|
|
6644
|
+
return doc14(
|
|
6645
|
+
db,
|
|
6646
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${CALENDAR_COLLECTION}/${eventId}`
|
|
6647
|
+
);
|
|
6648
|
+
}
|
|
6649
|
+
function getPatientCalendarEventDocRef(db, patientId, eventId) {
|
|
6650
|
+
return doc14(
|
|
6651
|
+
db,
|
|
6652
|
+
`${PATIENTS_COLLECTION}/${patientId}/${CALENDAR_COLLECTION}/${eventId}`
|
|
6653
|
+
);
|
|
6654
|
+
}
|
|
6655
|
+
function getClinicCalendarEventDocRef(db, clinicId, eventId) {
|
|
6656
|
+
return doc14(
|
|
6657
|
+
db,
|
|
6658
|
+
`${CLINICS_COLLECTION}/${clinicId}/${CALENDAR_COLLECTION}/${eventId}`
|
|
6659
|
+
);
|
|
6660
|
+
}
|
|
6661
|
+
function getPractitionerSyncedCalendarDocRef(db, practitionerId, syncedCalendarId) {
|
|
6662
|
+
return doc14(
|
|
6663
|
+
db,
|
|
6664
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/syncedCalendars/${syncedCalendarId}`
|
|
6665
|
+
);
|
|
6666
|
+
}
|
|
6667
|
+
function getPatientSyncedCalendarDocRef(db, patientId, syncedCalendarId) {
|
|
6668
|
+
return doc14(
|
|
6669
|
+
db,
|
|
6670
|
+
`${PATIENTS_COLLECTION}/${patientId}/syncedCalendars/${syncedCalendarId}`
|
|
6671
|
+
);
|
|
6672
|
+
}
|
|
6673
|
+
function getClinicSyncedCalendarDocRef(db, clinicId, syncedCalendarId) {
|
|
6674
|
+
return doc14(
|
|
6675
|
+
db,
|
|
6676
|
+
`${CLINICS_COLLECTION}/${clinicId}/syncedCalendars/${syncedCalendarId}`
|
|
6677
|
+
);
|
|
6678
|
+
}
|
|
6679
|
+
|
|
6680
|
+
// src/services/calendar/utils/clinic.utils.ts
|
|
6681
|
+
async function createClinicCalendarEventUtil(db, clinicId, eventData, generateId2) {
|
|
6682
|
+
const eventId = generateId2();
|
|
6683
|
+
const eventRef = getClinicCalendarEventDocRef(db, clinicId, eventId);
|
|
6684
|
+
const newEvent = {
|
|
6685
|
+
id: eventId,
|
|
6686
|
+
...eventData,
|
|
6687
|
+
createdAt: serverTimestamp14(),
|
|
6688
|
+
updatedAt: serverTimestamp14()
|
|
6689
|
+
};
|
|
6690
|
+
await setDoc15(eventRef, newEvent);
|
|
6691
|
+
return {
|
|
6692
|
+
...newEvent,
|
|
6693
|
+
createdAt: Timestamp18.now(),
|
|
6694
|
+
updatedAt: Timestamp18.now()
|
|
6695
|
+
};
|
|
6696
|
+
}
|
|
6697
|
+
async function updateClinicCalendarEventUtil(db, clinicId, eventId, updateData) {
|
|
6698
|
+
const eventRef = getClinicCalendarEventDocRef(db, clinicId, eventId);
|
|
6699
|
+
const updates = {
|
|
6700
|
+
...updateData,
|
|
6701
|
+
updatedAt: serverTimestamp14()
|
|
6702
|
+
};
|
|
6703
|
+
await updateDoc16(eventRef, updates);
|
|
6704
|
+
const updatedDoc = await getDoc18(eventRef);
|
|
6705
|
+
if (!updatedDoc.exists()) {
|
|
6706
|
+
throw new Error("Event not found after update");
|
|
6707
|
+
}
|
|
6708
|
+
return updatedDoc.data();
|
|
6709
|
+
}
|
|
6710
|
+
async function checkAutoConfirmAppointmentsUtil(db, clinicId) {
|
|
6711
|
+
const clinicDoc = await getDoc18(doc15(db, `clinics/${clinicId}`));
|
|
6712
|
+
if (!clinicDoc.exists()) {
|
|
6713
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
6714
|
+
}
|
|
6715
|
+
const clinicData = clinicDoc.data();
|
|
6716
|
+
const clinicGroupId = clinicData.clinicGroupId;
|
|
6717
|
+
if (!clinicGroupId) {
|
|
6718
|
+
return false;
|
|
6719
|
+
}
|
|
6720
|
+
const clinicGroupDoc = await getDoc18(
|
|
6721
|
+
doc15(db, `${CLINIC_GROUPS_COLLECTION}/${clinicGroupId}`)
|
|
6722
|
+
);
|
|
6723
|
+
if (!clinicGroupDoc.exists()) {
|
|
6724
|
+
return false;
|
|
6725
|
+
}
|
|
6726
|
+
const clinicGroupData = clinicGroupDoc.data();
|
|
6727
|
+
return !!clinicGroupData.autoConfirmAppointments;
|
|
6728
|
+
}
|
|
6729
|
+
|
|
6730
|
+
// src/services/calendar/utils/patient.utils.ts
|
|
6731
|
+
import {
|
|
6732
|
+
collection as collection14,
|
|
6733
|
+
getDoc as getDoc19,
|
|
6734
|
+
getDocs as getDocs13,
|
|
6735
|
+
setDoc as setDoc16,
|
|
6736
|
+
updateDoc as updateDoc17,
|
|
6737
|
+
deleteDoc as deleteDoc9,
|
|
6738
|
+
query as query13,
|
|
6739
|
+
where as where13,
|
|
6740
|
+
orderBy as orderBy5,
|
|
6741
|
+
Timestamp as Timestamp19,
|
|
6742
|
+
serverTimestamp as serverTimestamp15
|
|
6743
|
+
} from "firebase/firestore";
|
|
6744
|
+
async function createPatientCalendarEventUtil(db, patientId, eventData, generateId2) {
|
|
6745
|
+
const eventId = generateId2();
|
|
6746
|
+
const eventRef = getPatientCalendarEventDocRef(db, patientId, eventId);
|
|
6747
|
+
const newEvent = {
|
|
6748
|
+
id: eventId,
|
|
6749
|
+
...eventData,
|
|
6750
|
+
createdAt: serverTimestamp15(),
|
|
6751
|
+
updatedAt: serverTimestamp15()
|
|
6752
|
+
};
|
|
6753
|
+
await setDoc16(eventRef, newEvent);
|
|
6754
|
+
return {
|
|
6755
|
+
...newEvent,
|
|
6756
|
+
createdAt: Timestamp19.now(),
|
|
6757
|
+
updatedAt: Timestamp19.now()
|
|
6758
|
+
};
|
|
6759
|
+
}
|
|
6760
|
+
async function updatePatientCalendarEventUtil(db, patientId, eventId, updateData) {
|
|
6761
|
+
const eventRef = getPatientCalendarEventDocRef(db, patientId, eventId);
|
|
6762
|
+
const updates = {
|
|
6763
|
+
...updateData,
|
|
6764
|
+
updatedAt: serverTimestamp15()
|
|
6765
|
+
};
|
|
6766
|
+
await updateDoc17(eventRef, updates);
|
|
6767
|
+
const updatedDoc = await getDoc19(eventRef);
|
|
6768
|
+
if (!updatedDoc.exists()) {
|
|
6769
|
+
throw new Error("Event not found after update");
|
|
6770
|
+
}
|
|
6771
|
+
return updatedDoc.data();
|
|
6772
|
+
}
|
|
6773
|
+
|
|
6774
|
+
// src/services/calendar/utils/practitioner.utils.ts
|
|
6775
|
+
import {
|
|
6776
|
+
collection as collection15,
|
|
6777
|
+
getDoc as getDoc20,
|
|
6778
|
+
getDocs as getDocs14,
|
|
6779
|
+
setDoc as setDoc17,
|
|
6780
|
+
updateDoc as updateDoc18,
|
|
6781
|
+
deleteDoc as deleteDoc10,
|
|
6782
|
+
query as query14,
|
|
6783
|
+
where as where14,
|
|
6784
|
+
orderBy as orderBy6,
|
|
6785
|
+
Timestamp as Timestamp20,
|
|
6786
|
+
serverTimestamp as serverTimestamp16
|
|
6787
|
+
} from "firebase/firestore";
|
|
6788
|
+
async function createPractitionerCalendarEventUtil(db, practitionerId, eventData, generateId2) {
|
|
6789
|
+
const eventId = generateId2();
|
|
6790
|
+
const eventRef = getPractitionerCalendarEventDocRef(
|
|
6791
|
+
db,
|
|
6792
|
+
practitionerId,
|
|
6793
|
+
eventId
|
|
6794
|
+
);
|
|
6795
|
+
const newEvent = {
|
|
6796
|
+
id: eventId,
|
|
6797
|
+
...eventData,
|
|
6798
|
+
createdAt: serverTimestamp16(),
|
|
6799
|
+
updatedAt: serverTimestamp16()
|
|
6800
|
+
};
|
|
6801
|
+
await setDoc17(eventRef, newEvent);
|
|
6802
|
+
return {
|
|
6803
|
+
...newEvent,
|
|
6804
|
+
createdAt: Timestamp20.now(),
|
|
6805
|
+
updatedAt: Timestamp20.now()
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
async function updatePractitionerCalendarEventUtil(db, practitionerId, eventId, updateData) {
|
|
6809
|
+
const eventRef = getPractitionerCalendarEventDocRef(
|
|
6810
|
+
db,
|
|
6811
|
+
practitionerId,
|
|
6812
|
+
eventId
|
|
6813
|
+
);
|
|
6814
|
+
const updates = {
|
|
6815
|
+
...updateData,
|
|
6816
|
+
updatedAt: serverTimestamp16()
|
|
6817
|
+
};
|
|
6818
|
+
await updateDoc18(eventRef, updates);
|
|
6819
|
+
const updatedDoc = await getDoc20(eventRef);
|
|
6820
|
+
if (!updatedDoc.exists()) {
|
|
6821
|
+
throw new Error("Event not found after update");
|
|
6822
|
+
}
|
|
6823
|
+
return updatedDoc.data();
|
|
6824
|
+
}
|
|
6825
|
+
|
|
6826
|
+
// src/services/calendar/utils/appointment.utils.ts
|
|
6827
|
+
async function createAppointmentUtil(db, clinicId, practitionerId, patientId, eventData, generateId2) {
|
|
6828
|
+
const eventId = generateId2();
|
|
6829
|
+
const autoConfirm = await checkAutoConfirmAppointmentsUtil(db, clinicId);
|
|
6830
|
+
const initialStatus = autoConfirm ? "confirmed" /* CONFIRMED */ : "pending" /* PENDING */;
|
|
6831
|
+
const appointmentData = {
|
|
6832
|
+
...eventData,
|
|
6833
|
+
clinicBranchId: clinicId,
|
|
6834
|
+
practitionerProfileId: practitionerId,
|
|
6835
|
+
patientProfileId: patientId,
|
|
6836
|
+
eventType: "appointment" /* APPOINTMENT */,
|
|
6837
|
+
status: eventData.status || initialStatus
|
|
6838
|
+
};
|
|
6839
|
+
const clinicPromise = createClinicCalendarEventUtil(
|
|
6840
|
+
db,
|
|
6841
|
+
clinicId,
|
|
6842
|
+
appointmentData,
|
|
6843
|
+
() => eventId
|
|
6844
|
+
// Use the same ID for all calendars
|
|
6845
|
+
);
|
|
6846
|
+
const practitionerPromise = createPractitionerCalendarEventUtil(
|
|
6847
|
+
db,
|
|
6848
|
+
practitionerId,
|
|
6849
|
+
appointmentData,
|
|
6850
|
+
() => eventId
|
|
6851
|
+
// Use the same ID for all calendars
|
|
6852
|
+
);
|
|
6853
|
+
const patientPromise = createPatientCalendarEventUtil(
|
|
6854
|
+
db,
|
|
6855
|
+
patientId,
|
|
6856
|
+
appointmentData,
|
|
6857
|
+
() => eventId
|
|
6858
|
+
// Use the same ID for all calendars
|
|
6859
|
+
);
|
|
6860
|
+
const [clinicEvent] = await Promise.all([
|
|
6861
|
+
clinicPromise,
|
|
6862
|
+
practitionerPromise,
|
|
6863
|
+
patientPromise
|
|
6864
|
+
]);
|
|
6865
|
+
return clinicEvent;
|
|
6866
|
+
}
|
|
6867
|
+
async function updateAppointmentUtil(db, clinicId, practitionerId, patientId, eventId, updateData) {
|
|
6868
|
+
const clinicPromise = updateClinicCalendarEventUtil(
|
|
6869
|
+
db,
|
|
6870
|
+
clinicId,
|
|
6871
|
+
eventId,
|
|
6872
|
+
updateData
|
|
6873
|
+
);
|
|
6874
|
+
const practitionerPromise = updatePractitionerCalendarEventUtil(
|
|
6875
|
+
db,
|
|
6876
|
+
practitionerId,
|
|
6877
|
+
eventId,
|
|
6878
|
+
updateData
|
|
6879
|
+
);
|
|
6880
|
+
const patientPromise = updatePatientCalendarEventUtil(
|
|
6881
|
+
db,
|
|
6882
|
+
patientId,
|
|
6883
|
+
eventId,
|
|
6884
|
+
updateData
|
|
6885
|
+
);
|
|
6886
|
+
const [clinicEvent] = await Promise.all([
|
|
6887
|
+
clinicPromise,
|
|
6888
|
+
practitionerPromise,
|
|
6889
|
+
patientPromise
|
|
6890
|
+
]);
|
|
6891
|
+
return clinicEvent;
|
|
6892
|
+
}
|
|
6893
|
+
|
|
6894
|
+
// src/services/calendar/utils/synced-calendar.utils.ts
|
|
6895
|
+
import {
|
|
6896
|
+
collection as collection16,
|
|
6897
|
+
getDoc as getDoc21,
|
|
6898
|
+
getDocs as getDocs15,
|
|
6899
|
+
setDoc as setDoc18,
|
|
6900
|
+
updateDoc as updateDoc19,
|
|
6901
|
+
deleteDoc as deleteDoc11,
|
|
6902
|
+
query as query15,
|
|
6903
|
+
orderBy as orderBy7,
|
|
6904
|
+
Timestamp as Timestamp21,
|
|
6905
|
+
serverTimestamp as serverTimestamp17
|
|
6906
|
+
} from "firebase/firestore";
|
|
6907
|
+
async function createPractitionerSyncedCalendarUtil(db, practitionerId, calendarData, generateId2) {
|
|
6908
|
+
const calendarId = generateId2();
|
|
6909
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
6910
|
+
db,
|
|
6911
|
+
practitionerId,
|
|
6912
|
+
calendarId
|
|
6913
|
+
);
|
|
6914
|
+
const newCalendar = {
|
|
6915
|
+
id: calendarId,
|
|
6916
|
+
...calendarData,
|
|
6917
|
+
createdAt: serverTimestamp17(),
|
|
6918
|
+
updatedAt: serverTimestamp17()
|
|
6919
|
+
};
|
|
6920
|
+
await setDoc18(calendarRef, newCalendar);
|
|
6921
|
+
return {
|
|
6922
|
+
...newCalendar,
|
|
6923
|
+
createdAt: Timestamp21.now(),
|
|
6924
|
+
updatedAt: Timestamp21.now()
|
|
6925
|
+
};
|
|
6926
|
+
}
|
|
6927
|
+
async function createPatientSyncedCalendarUtil(db, patientId, calendarData, generateId2) {
|
|
6928
|
+
const calendarId = generateId2();
|
|
6929
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
6930
|
+
const newCalendar = {
|
|
6931
|
+
id: calendarId,
|
|
6932
|
+
...calendarData,
|
|
6933
|
+
createdAt: serverTimestamp17(),
|
|
6934
|
+
updatedAt: serverTimestamp17()
|
|
6935
|
+
};
|
|
6936
|
+
await setDoc18(calendarRef, newCalendar);
|
|
6937
|
+
return {
|
|
6938
|
+
...newCalendar,
|
|
6939
|
+
createdAt: Timestamp21.now(),
|
|
6940
|
+
updatedAt: Timestamp21.now()
|
|
6941
|
+
};
|
|
6942
|
+
}
|
|
6943
|
+
async function createClinicSyncedCalendarUtil(db, clinicId, calendarData, generateId2) {
|
|
6944
|
+
const calendarId = generateId2();
|
|
6945
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
6946
|
+
const newCalendar = {
|
|
6947
|
+
id: calendarId,
|
|
6948
|
+
...calendarData,
|
|
6949
|
+
createdAt: serverTimestamp17(),
|
|
6950
|
+
updatedAt: serverTimestamp17()
|
|
6951
|
+
};
|
|
6952
|
+
await setDoc18(calendarRef, newCalendar);
|
|
6953
|
+
return {
|
|
6954
|
+
...newCalendar,
|
|
6955
|
+
createdAt: Timestamp21.now(),
|
|
6956
|
+
updatedAt: Timestamp21.now()
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
async function getPractitionerSyncedCalendarUtil(db, practitionerId, calendarId) {
|
|
6960
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
6961
|
+
db,
|
|
6962
|
+
practitionerId,
|
|
6963
|
+
calendarId
|
|
6964
|
+
);
|
|
6965
|
+
const calendarDoc = await getDoc21(calendarRef);
|
|
6966
|
+
if (!calendarDoc.exists()) {
|
|
6967
|
+
return null;
|
|
6968
|
+
}
|
|
6969
|
+
return calendarDoc.data();
|
|
6970
|
+
}
|
|
6971
|
+
async function getPractitionerSyncedCalendarsUtil(db, practitionerId) {
|
|
6972
|
+
const calendarsRef = collection16(
|
|
6973
|
+
db,
|
|
6974
|
+
`practitioners/${practitionerId}/${SYNCED_CALENDARS_COLLECTION}`
|
|
6975
|
+
);
|
|
6976
|
+
const q = query15(calendarsRef, orderBy7("createdAt", "desc"));
|
|
6977
|
+
const querySnapshot = await getDocs15(q);
|
|
6978
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
6979
|
+
}
|
|
6980
|
+
async function getPatientSyncedCalendarUtil(db, patientId, calendarId) {
|
|
6981
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
6982
|
+
const calendarDoc = await getDoc21(calendarRef);
|
|
6983
|
+
if (!calendarDoc.exists()) {
|
|
6984
|
+
return null;
|
|
6985
|
+
}
|
|
6986
|
+
return calendarDoc.data();
|
|
6987
|
+
}
|
|
6988
|
+
async function getPatientSyncedCalendarsUtil(db, patientId) {
|
|
6989
|
+
const calendarsRef = collection16(
|
|
6990
|
+
db,
|
|
6991
|
+
`patients/${patientId}/${SYNCED_CALENDARS_COLLECTION}`
|
|
6992
|
+
);
|
|
6993
|
+
const q = query15(calendarsRef, orderBy7("createdAt", "desc"));
|
|
6994
|
+
const querySnapshot = await getDocs15(q);
|
|
6995
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
6996
|
+
}
|
|
6997
|
+
async function getClinicSyncedCalendarUtil(db, clinicId, calendarId) {
|
|
6998
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
6999
|
+
const calendarDoc = await getDoc21(calendarRef);
|
|
7000
|
+
if (!calendarDoc.exists()) {
|
|
7001
|
+
return null;
|
|
7002
|
+
}
|
|
7003
|
+
return calendarDoc.data();
|
|
7004
|
+
}
|
|
7005
|
+
async function getClinicSyncedCalendarsUtil(db, clinicId) {
|
|
7006
|
+
const calendarsRef = collection16(
|
|
7007
|
+
db,
|
|
7008
|
+
`clinics/${clinicId}/${SYNCED_CALENDARS_COLLECTION}`
|
|
7009
|
+
);
|
|
7010
|
+
const q = query15(calendarsRef, orderBy7("createdAt", "desc"));
|
|
7011
|
+
const querySnapshot = await getDocs15(q);
|
|
7012
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
7013
|
+
}
|
|
7014
|
+
async function updatePractitionerSyncedCalendarUtil(db, practitionerId, calendarId, updateData) {
|
|
7015
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
7016
|
+
db,
|
|
7017
|
+
practitionerId,
|
|
7018
|
+
calendarId
|
|
7019
|
+
);
|
|
7020
|
+
const updates = {
|
|
7021
|
+
...updateData,
|
|
7022
|
+
updatedAt: serverTimestamp17()
|
|
7023
|
+
};
|
|
7024
|
+
await updateDoc19(calendarRef, updates);
|
|
7025
|
+
const updatedDoc = await getDoc21(calendarRef);
|
|
7026
|
+
if (!updatedDoc.exists()) {
|
|
7027
|
+
throw new Error("Synced calendar not found after update");
|
|
7028
|
+
}
|
|
7029
|
+
return updatedDoc.data();
|
|
7030
|
+
}
|
|
7031
|
+
async function updatePatientSyncedCalendarUtil(db, patientId, calendarId, updateData) {
|
|
7032
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
7033
|
+
const updates = {
|
|
7034
|
+
...updateData,
|
|
7035
|
+
updatedAt: serverTimestamp17()
|
|
7036
|
+
};
|
|
7037
|
+
await updateDoc19(calendarRef, updates);
|
|
7038
|
+
const updatedDoc = await getDoc21(calendarRef);
|
|
7039
|
+
if (!updatedDoc.exists()) {
|
|
7040
|
+
throw new Error("Synced calendar not found after update");
|
|
7041
|
+
}
|
|
7042
|
+
return updatedDoc.data();
|
|
7043
|
+
}
|
|
7044
|
+
async function updateClinicSyncedCalendarUtil(db, clinicId, calendarId, updateData) {
|
|
7045
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
7046
|
+
const updates = {
|
|
7047
|
+
...updateData,
|
|
7048
|
+
updatedAt: serverTimestamp17()
|
|
7049
|
+
};
|
|
7050
|
+
await updateDoc19(calendarRef, updates);
|
|
7051
|
+
const updatedDoc = await getDoc21(calendarRef);
|
|
7052
|
+
if (!updatedDoc.exists()) {
|
|
7053
|
+
throw new Error("Synced calendar not found after update");
|
|
7054
|
+
}
|
|
7055
|
+
return updatedDoc.data();
|
|
7056
|
+
}
|
|
7057
|
+
async function deletePractitionerSyncedCalendarUtil(db, practitionerId, calendarId) {
|
|
7058
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
7059
|
+
db,
|
|
7060
|
+
practitionerId,
|
|
7061
|
+
calendarId
|
|
7062
|
+
);
|
|
7063
|
+
await deleteDoc11(calendarRef);
|
|
7064
|
+
}
|
|
7065
|
+
async function deletePatientSyncedCalendarUtil(db, patientId, calendarId) {
|
|
7066
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
7067
|
+
await deleteDoc11(calendarRef);
|
|
7068
|
+
}
|
|
7069
|
+
async function deleteClinicSyncedCalendarUtil(db, clinicId, calendarId) {
|
|
7070
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
7071
|
+
await deleteDoc11(calendarRef);
|
|
7072
|
+
}
|
|
7073
|
+
async function updateLastSyncedTimestampUtil(db, entityType, entityId, calendarId) {
|
|
7074
|
+
const updateData = {
|
|
7075
|
+
lastSyncedAt: Timestamp21.now()
|
|
7076
|
+
};
|
|
7077
|
+
switch (entityType) {
|
|
7078
|
+
case "practitioner":
|
|
7079
|
+
return updatePractitionerSyncedCalendarUtil(
|
|
7080
|
+
db,
|
|
7081
|
+
entityId,
|
|
7082
|
+
calendarId,
|
|
7083
|
+
updateData
|
|
7084
|
+
);
|
|
7085
|
+
case "patient":
|
|
7086
|
+
return updatePatientSyncedCalendarUtil(
|
|
7087
|
+
db,
|
|
7088
|
+
entityId,
|
|
7089
|
+
calendarId,
|
|
7090
|
+
updateData
|
|
7091
|
+
);
|
|
7092
|
+
case "clinic":
|
|
7093
|
+
return updateClinicSyncedCalendarUtil(
|
|
7094
|
+
db,
|
|
7095
|
+
entityId,
|
|
7096
|
+
calendarId,
|
|
7097
|
+
updateData
|
|
7098
|
+
);
|
|
7099
|
+
default:
|
|
7100
|
+
throw new Error(`Invalid entity type: ${entityType}`);
|
|
7101
|
+
}
|
|
7102
|
+
}
|
|
7103
|
+
|
|
7104
|
+
// src/services/calendar/utils/google-calendar.utils.ts
|
|
7105
|
+
import { Timestamp as Timestamp22 } from "firebase/firestore";
|
|
7106
|
+
var GOOGLE_CALENDAR_API_URL = "https://www.googleapis.com/calendar/v3";
|
|
7107
|
+
var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
7108
|
+
var CLIENT_ID = "your-client-id";
|
|
7109
|
+
var CLIENT_SECRET = "your-client-secret";
|
|
7110
|
+
var REDIRECT_URI = "your-redirect-uri";
|
|
7111
|
+
async function makeRequest(method, url, headers, data, params) {
|
|
7112
|
+
const queryParams = params ? "?" + Object.entries(params).map(
|
|
7113
|
+
([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
7114
|
+
).join("&") : "";
|
|
7115
|
+
const finalUrl = url + queryParams;
|
|
7116
|
+
const options = {
|
|
7117
|
+
method,
|
|
7118
|
+
headers,
|
|
7119
|
+
body: data ? JSON.stringify(data) : void 0
|
|
7120
|
+
};
|
|
7121
|
+
const response = await fetch(finalUrl, options);
|
|
7122
|
+
if (!response.ok) {
|
|
7123
|
+
const error = new Error(
|
|
7124
|
+
`Request failed with status ${response.status}`
|
|
7125
|
+
);
|
|
7126
|
+
error.response = response;
|
|
7127
|
+
throw error;
|
|
7128
|
+
}
|
|
7129
|
+
return response.json();
|
|
7130
|
+
}
|
|
7131
|
+
async function authenticateWithGoogleCalendarUtil(authCode) {
|
|
7132
|
+
try {
|
|
7133
|
+
const data = {
|
|
7134
|
+
code: authCode,
|
|
7135
|
+
client_id: CLIENT_ID,
|
|
7136
|
+
client_secret: CLIENT_SECRET,
|
|
7137
|
+
redirect_uri: REDIRECT_URI,
|
|
7138
|
+
grant_type: "authorization_code"
|
|
7139
|
+
};
|
|
7140
|
+
const response = await makeRequest(
|
|
7141
|
+
"post",
|
|
7142
|
+
GOOGLE_OAUTH_URL,
|
|
7143
|
+
{ "Content-Type": "application/json" },
|
|
7144
|
+
data
|
|
7145
|
+
);
|
|
7146
|
+
return {
|
|
7147
|
+
accessToken: response.access_token,
|
|
7148
|
+
refreshToken: response.refresh_token,
|
|
7149
|
+
expiresIn: response.expires_in
|
|
7150
|
+
};
|
|
7151
|
+
} catch (error) {
|
|
7152
|
+
const apiError = error;
|
|
7153
|
+
console.error(
|
|
7154
|
+
"Error authenticating with Google Calendar:",
|
|
7155
|
+
apiError.message || "Unknown error"
|
|
7156
|
+
);
|
|
7157
|
+
throw new Error(
|
|
7158
|
+
`Failed to authenticate with Google Calendar: ${apiError.message || "Unknown error"}`
|
|
7159
|
+
);
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
async function refreshGoogleCalendarTokenUtil(refreshToken) {
|
|
7163
|
+
try {
|
|
7164
|
+
const data = {
|
|
7165
|
+
refresh_token: refreshToken,
|
|
7166
|
+
client_id: CLIENT_ID,
|
|
7167
|
+
client_secret: CLIENT_SECRET,
|
|
7168
|
+
grant_type: "refresh_token"
|
|
7169
|
+
};
|
|
7170
|
+
const response = await makeRequest(
|
|
7171
|
+
"post",
|
|
7172
|
+
GOOGLE_OAUTH_URL,
|
|
7173
|
+
{ "Content-Type": "application/json" },
|
|
7174
|
+
data
|
|
7175
|
+
);
|
|
7176
|
+
return {
|
|
7177
|
+
accessToken: response.access_token,
|
|
7178
|
+
expiresIn: response.expires_in
|
|
7179
|
+
};
|
|
7180
|
+
} catch (error) {
|
|
7181
|
+
const apiError = error;
|
|
7182
|
+
console.error(
|
|
7183
|
+
"Error refreshing Google Calendar token:",
|
|
7184
|
+
apiError.message || "Unknown error"
|
|
7185
|
+
);
|
|
7186
|
+
throw new Error(
|
|
7187
|
+
`Failed to refresh Google Calendar token: ${apiError.message || "Unknown error"}`
|
|
7188
|
+
);
|
|
7189
|
+
}
|
|
7190
|
+
}
|
|
7191
|
+
async function listGoogleCalendarsUtil(accessToken) {
|
|
7192
|
+
try {
|
|
7193
|
+
const response = await makeRequest(
|
|
7194
|
+
"get",
|
|
7195
|
+
`${GOOGLE_CALENDAR_API_URL}/users/me/calendarList`,
|
|
7196
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
7197
|
+
);
|
|
7198
|
+
return response.items.map((calendar) => ({
|
|
7199
|
+
id: calendar.id,
|
|
7200
|
+
name: calendar.summary
|
|
7201
|
+
}));
|
|
7202
|
+
} catch (error) {
|
|
7203
|
+
const apiError = error;
|
|
7204
|
+
console.error(
|
|
7205
|
+
"Error listing Google Calendars:",
|
|
7206
|
+
apiError.message || "Unknown error"
|
|
7207
|
+
);
|
|
7208
|
+
throw new Error(
|
|
7209
|
+
`Failed to list Google Calendars: ${apiError.message || "Unknown error"}`
|
|
7210
|
+
);
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
async function ensureValidToken(db, entityType, entityId, syncedCalendar) {
|
|
7214
|
+
const expiryTime = syncedCalendar.tokenExpiry.toDate();
|
|
7215
|
+
const now = /* @__PURE__ */ new Date();
|
|
7216
|
+
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1e3);
|
|
7217
|
+
if (expiryTime < fiveMinutesFromNow) {
|
|
7218
|
+
const { accessToken, expiresIn } = await refreshGoogleCalendarTokenUtil(
|
|
7219
|
+
syncedCalendar.refreshToken
|
|
7220
|
+
);
|
|
7221
|
+
const tokenExpiry = /* @__PURE__ */ new Date();
|
|
7222
|
+
tokenExpiry.setSeconds(tokenExpiry.getSeconds() + expiresIn);
|
|
7223
|
+
const updateData = {
|
|
7224
|
+
accessToken,
|
|
7225
|
+
tokenExpiry: Timestamp22.fromDate(tokenExpiry)
|
|
7226
|
+
};
|
|
7227
|
+
switch (entityType) {
|
|
7228
|
+
case "practitioner":
|
|
7229
|
+
await updatePractitionerSyncedCalendarUtil(
|
|
7230
|
+
db,
|
|
7231
|
+
entityId,
|
|
7232
|
+
syncedCalendar.id,
|
|
7233
|
+
updateData
|
|
7234
|
+
);
|
|
7235
|
+
break;
|
|
7236
|
+
case "patient":
|
|
7237
|
+
await updatePatientSyncedCalendarUtil(
|
|
7238
|
+
db,
|
|
7239
|
+
entityId,
|
|
7240
|
+
syncedCalendar.id,
|
|
7241
|
+
updateData
|
|
7242
|
+
);
|
|
7243
|
+
break;
|
|
7244
|
+
case "clinic":
|
|
7245
|
+
await updateClinicSyncedCalendarUtil(
|
|
7246
|
+
db,
|
|
7247
|
+
entityId,
|
|
7248
|
+
syncedCalendar.id,
|
|
7249
|
+
updateData
|
|
7250
|
+
);
|
|
7251
|
+
break;
|
|
7252
|
+
}
|
|
7253
|
+
return accessToken;
|
|
7254
|
+
}
|
|
7255
|
+
return syncedCalendar.accessToken;
|
|
7256
|
+
}
|
|
7257
|
+
async function syncEventsToGoogleCalendarUtil(db, entityType, entityId, syncedCalendar, events, existingSyncId) {
|
|
7258
|
+
var _a, _b;
|
|
7259
|
+
try {
|
|
7260
|
+
const { accessToken } = await refreshGoogleCalendarTokenUtil(
|
|
7261
|
+
syncedCalendar.refreshToken
|
|
7262
|
+
);
|
|
7263
|
+
let syncedCount = 0;
|
|
7264
|
+
const errors = [];
|
|
7265
|
+
const eventIds = [];
|
|
7266
|
+
for (const event of events) {
|
|
7267
|
+
try {
|
|
7268
|
+
if (event.syncStatus === "external" /* EXTERNAL */) {
|
|
7269
|
+
continue;
|
|
7270
|
+
}
|
|
7271
|
+
if (entityType === "practitioner" && event.status !== "confirmed" /* CONFIRMED */) {
|
|
7272
|
+
continue;
|
|
7273
|
+
}
|
|
7274
|
+
if (entityType === "patient" && (event.status === "canceled" /* CANCELED */ || event.status === "rejected" /* REJECTED */)) {
|
|
7275
|
+
continue;
|
|
7276
|
+
}
|
|
7277
|
+
if (entityType === "clinic") {
|
|
7278
|
+
continue;
|
|
7279
|
+
}
|
|
7280
|
+
const googleEvent = convertCalendarEventToGoogleEventUtil(event);
|
|
7281
|
+
const headers = {
|
|
7282
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7283
|
+
"Content-Type": "application/json"
|
|
7284
|
+
};
|
|
7285
|
+
let responseId = "";
|
|
7286
|
+
if (existingSyncId) {
|
|
7287
|
+
const response = await makeRequest(
|
|
7288
|
+
"put",
|
|
7289
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSyncId}`,
|
|
7290
|
+
headers,
|
|
7291
|
+
googleEvent
|
|
7292
|
+
);
|
|
7293
|
+
responseId = response.id;
|
|
7294
|
+
} else {
|
|
7295
|
+
const existingSync = (_a = event.syncedCalendarEventId) == null ? void 0 : _a.find(
|
|
7296
|
+
(sync) => sync.syncedCalendarProvider === "google" /* GOOGLE */ && // We should check if this is the same calendar we're syncing with, but that information isn't stored
|
|
7297
|
+
// For now, we'll just use the first Google Calendar sync ID
|
|
7298
|
+
sync.syncedCalendarProvider === syncedCalendar.provider
|
|
7299
|
+
);
|
|
7300
|
+
if (existingSync) {
|
|
7301
|
+
const response = await makeRequest(
|
|
7302
|
+
"put",
|
|
7303
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSync.eventId}`,
|
|
7304
|
+
headers,
|
|
7305
|
+
googleEvent
|
|
7306
|
+
);
|
|
7307
|
+
responseId = response.id;
|
|
7308
|
+
} else {
|
|
7309
|
+
const response = await makeRequest(
|
|
7310
|
+
"post",
|
|
7311
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
7312
|
+
headers,
|
|
7313
|
+
googleEvent
|
|
7314
|
+
);
|
|
7315
|
+
responseId = response.id;
|
|
7316
|
+
}
|
|
7317
|
+
}
|
|
7318
|
+
if (responseId) {
|
|
7319
|
+
eventIds.push(responseId);
|
|
7320
|
+
syncedCount++;
|
|
7321
|
+
}
|
|
7322
|
+
} catch (error) {
|
|
7323
|
+
const apiError = error;
|
|
7324
|
+
errors.push({
|
|
7325
|
+
eventId: event.id,
|
|
7326
|
+
error: apiError.message || "Unknown error",
|
|
7327
|
+
status: (_b = apiError.response) == null ? void 0 : _b.status
|
|
7328
|
+
});
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
await updateLastSyncedTimestampUtil(
|
|
7332
|
+
db,
|
|
7333
|
+
entityType,
|
|
7334
|
+
entityId,
|
|
7335
|
+
syncedCalendar.id
|
|
7336
|
+
);
|
|
7337
|
+
return {
|
|
7338
|
+
success: errors.length === 0,
|
|
7339
|
+
syncedEvents: syncedCount,
|
|
7340
|
+
errors,
|
|
7341
|
+
eventIds
|
|
7342
|
+
};
|
|
7343
|
+
} catch (error) {
|
|
7344
|
+
console.error("Error syncing with Google Calendar:", error);
|
|
7345
|
+
return {
|
|
7346
|
+
success: false,
|
|
7347
|
+
syncedEvents: 0,
|
|
7348
|
+
errors: [{ error: error.message || "Unknown error" }],
|
|
7349
|
+
eventIds: []
|
|
7350
|
+
};
|
|
7351
|
+
}
|
|
7352
|
+
}
|
|
7353
|
+
async function fetchEventsFromGoogleCalendarUtil(db, entityType, entityId, syncedCalendar, startDate, endDate) {
|
|
7354
|
+
try {
|
|
7355
|
+
const accessToken = await ensureValidToken(
|
|
7356
|
+
db,
|
|
7357
|
+
entityType,
|
|
7358
|
+
entityId,
|
|
7359
|
+
syncedCalendar
|
|
7360
|
+
);
|
|
7361
|
+
const timeMin = startDate.toISOString();
|
|
7362
|
+
const timeMax = endDate.toISOString();
|
|
7363
|
+
const response = await makeRequest(
|
|
7364
|
+
"get",
|
|
7365
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
7366
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
7367
|
+
void 0,
|
|
7368
|
+
{
|
|
7369
|
+
timeMin,
|
|
7370
|
+
timeMax,
|
|
7371
|
+
singleEvents: "true",
|
|
7372
|
+
orderBy: "startTime"
|
|
7373
|
+
}
|
|
7374
|
+
);
|
|
7375
|
+
await updateLastSyncedTimestampUtil(
|
|
7376
|
+
db,
|
|
7377
|
+
entityType,
|
|
7378
|
+
entityId,
|
|
7379
|
+
syncedCalendar.id
|
|
7380
|
+
);
|
|
7381
|
+
return response.items;
|
|
7382
|
+
} catch (error) {
|
|
7383
|
+
const apiError = error;
|
|
7384
|
+
console.error(
|
|
7385
|
+
"Error fetching events from Google Calendar:",
|
|
7386
|
+
apiError.message || "Unknown error"
|
|
7387
|
+
);
|
|
7388
|
+
throw new Error(
|
|
7389
|
+
`Failed to fetch events from Google Calendar: ${apiError.message || "Unknown error"}`
|
|
7390
|
+
);
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
function convertGoogleEventToCalendarEventUtil(googleEvent, entityId, entityType) {
|
|
7394
|
+
const start = googleEvent.start.dateTime ? new Date(googleEvent.start.dateTime) : new Date(googleEvent.start.date);
|
|
7395
|
+
const end = googleEvent.end.dateTime ? new Date(googleEvent.end.dateTime) : new Date(googleEvent.end.date);
|
|
7396
|
+
const calendarEvent = {
|
|
7397
|
+
eventName: googleEvent.summary || "External Event",
|
|
7398
|
+
eventLocation: googleEvent.location,
|
|
7399
|
+
eventTime: {
|
|
7400
|
+
start: Timestamp22.fromDate(start),
|
|
7401
|
+
end: Timestamp22.fromDate(end)
|
|
7402
|
+
},
|
|
7403
|
+
description: googleEvent.description || "",
|
|
7404
|
+
// External events are always set as CONFIRMED - status updates will happen externally
|
|
7405
|
+
status: "confirmed" /* CONFIRMED */,
|
|
7406
|
+
// All external events are marked as EXTERNAL to indicate they originated outside our system
|
|
7407
|
+
syncStatus: "external" /* EXTERNAL */,
|
|
7408
|
+
// All external events are treated as BLOCKING events
|
|
7409
|
+
eventType: "blocking" /* BLOCKING */,
|
|
7410
|
+
// Store the original Google Calendar event ID
|
|
7411
|
+
syncedCalendarEventId: [
|
|
7412
|
+
{
|
|
7413
|
+
eventId: googleEvent.id,
|
|
7414
|
+
syncedCalendarProvider: "google" /* GOOGLE */,
|
|
7415
|
+
syncedAt: Timestamp22.now()
|
|
7416
|
+
}
|
|
7417
|
+
]
|
|
7418
|
+
};
|
|
7419
|
+
switch (entityType) {
|
|
7420
|
+
case "practitioner":
|
|
7421
|
+
calendarEvent.practitionerProfileId = entityId;
|
|
7422
|
+
break;
|
|
7423
|
+
case "patient":
|
|
7424
|
+
calendarEvent.patientProfileId = entityId;
|
|
7425
|
+
break;
|
|
7426
|
+
case "clinic":
|
|
7427
|
+
calendarEvent.clinicBranchId = entityId;
|
|
7428
|
+
break;
|
|
7429
|
+
}
|
|
7430
|
+
return calendarEvent;
|
|
7431
|
+
}
|
|
7432
|
+
function convertCalendarEventToGoogleEventUtil(calendarEvent) {
|
|
7433
|
+
const googleEvent = {
|
|
7434
|
+
summary: calendarEvent.eventName,
|
|
7435
|
+
location: calendarEvent.eventLocation,
|
|
7436
|
+
description: calendarEvent.description,
|
|
7437
|
+
start: {
|
|
7438
|
+
dateTime: calendarEvent.eventTime.start.toDate().toISOString(),
|
|
7439
|
+
timeZone: "UTC"
|
|
7440
|
+
},
|
|
7441
|
+
end: {
|
|
7442
|
+
dateTime: calendarEvent.eventTime.end.toDate().toISOString(),
|
|
7443
|
+
timeZone: "UTC"
|
|
7444
|
+
},
|
|
7445
|
+
// Add reminders
|
|
7446
|
+
reminders: {
|
|
7447
|
+
useDefault: false,
|
|
7448
|
+
overrides: [
|
|
7449
|
+
{ method: "email", minutes: 24 * 60 },
|
|
7450
|
+
// 1 day before
|
|
7451
|
+
{ method: "popup", minutes: 30 }
|
|
7452
|
+
// 30 minutes before
|
|
7453
|
+
]
|
|
7454
|
+
}
|
|
7455
|
+
};
|
|
7456
|
+
switch (calendarEvent.status) {
|
|
7457
|
+
case "confirmed" /* CONFIRMED */:
|
|
7458
|
+
googleEvent.status = "confirmed";
|
|
7459
|
+
break;
|
|
7460
|
+
case "canceled" /* CANCELED */:
|
|
7461
|
+
googleEvent.status = "cancelled";
|
|
7462
|
+
break;
|
|
7463
|
+
case "pending" /* PENDING */:
|
|
7464
|
+
googleEvent.status = "tentative";
|
|
7465
|
+
break;
|
|
7466
|
+
default:
|
|
7467
|
+
googleEvent.status = "confirmed";
|
|
7468
|
+
}
|
|
7469
|
+
if (calendarEvent.eventType === "appointment" /* APPOINTMENT */) {
|
|
7470
|
+
googleEvent.attendees = [];
|
|
7471
|
+
if (calendarEvent.practitionerProfileId) {
|
|
7472
|
+
googleEvent.attendees.push({
|
|
7473
|
+
email: "practitioner@example.com",
|
|
7474
|
+
// This would be fetched from the practitioner profile
|
|
7475
|
+
displayName: "Dr. Practitioner",
|
|
7476
|
+
// This would be fetched from the practitioner profile
|
|
7477
|
+
responseStatus: "accepted"
|
|
7478
|
+
});
|
|
7479
|
+
}
|
|
7480
|
+
if (calendarEvent.patientProfileId) {
|
|
7481
|
+
googleEvent.attendees.push({
|
|
7482
|
+
email: "patient@example.com",
|
|
7483
|
+
// This would be fetched from the patient profile
|
|
7484
|
+
displayName: "Patient",
|
|
7485
|
+
// This would be fetched from the patient profile
|
|
7486
|
+
responseStatus: "needsAction"
|
|
7487
|
+
});
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
return googleEvent;
|
|
7491
|
+
}
|
|
7492
|
+
async function deleteGoogleCalendarEventUtil(db, entityType, entityId, syncedCalendar, eventId) {
|
|
7493
|
+
try {
|
|
7494
|
+
const accessToken = await ensureValidToken(
|
|
7495
|
+
db,
|
|
7496
|
+
entityType,
|
|
7497
|
+
entityId,
|
|
7498
|
+
syncedCalendar
|
|
7499
|
+
);
|
|
7500
|
+
await makeRequest(
|
|
7501
|
+
"delete",
|
|
7502
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
|
|
7503
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
7504
|
+
);
|
|
7505
|
+
return true;
|
|
7506
|
+
} catch (error) {
|
|
7507
|
+
const apiError = error;
|
|
7508
|
+
console.error(
|
|
7509
|
+
"Error deleting event from Google Calendar:",
|
|
7510
|
+
apiError.message || "Unknown error"
|
|
7511
|
+
);
|
|
7512
|
+
throw new Error(
|
|
7513
|
+
`Failed to delete event from Google Calendar: ${apiError.message || "Unknown error"}`
|
|
7514
|
+
);
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
function getGoogleCalendarOAuthUrlUtil(scopes = ["https://www.googleapis.com/auth/calendar"]) {
|
|
7518
|
+
const scopeString = encodeURIComponent(scopes.join(" "));
|
|
7519
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
|
|
7520
|
+
REDIRECT_URI
|
|
7521
|
+
)}&response_type=code&scope=${scopeString}&access_type=offline&prompt=consent`;
|
|
7522
|
+
}
|
|
7523
|
+
|
|
7524
|
+
// src/services/calendar/synced-calendars.service.ts
|
|
7525
|
+
var SyncedCalendarsService = class extends BaseService {
|
|
7526
|
+
/**
|
|
7527
|
+
* Creates a new SyncedCalendarsService instance
|
|
7528
|
+
* @param db - Firestore instance
|
|
7529
|
+
* @param auth - Firebase Auth instance
|
|
7530
|
+
* @param app - Firebase App instance
|
|
7531
|
+
*/
|
|
7532
|
+
constructor(db, auth, app) {
|
|
7533
|
+
super(db, auth, app);
|
|
7534
|
+
}
|
|
7535
|
+
// ===== Practitioner Synced Calendars =====
|
|
7536
|
+
/**
|
|
7537
|
+
* Creates a synced calendar for a practitioner
|
|
7538
|
+
* @param practitionerId - ID of the practitioner
|
|
7539
|
+
* @param calendarData - Synced calendar data
|
|
7540
|
+
* @returns Created synced calendar
|
|
7541
|
+
*/
|
|
7542
|
+
async createPractitionerSyncedCalendar(practitionerId, calendarData) {
|
|
7543
|
+
return createPractitionerSyncedCalendarUtil(
|
|
7544
|
+
this.db,
|
|
7545
|
+
practitionerId,
|
|
7546
|
+
calendarData,
|
|
7547
|
+
this.generateId.bind(this)
|
|
7548
|
+
);
|
|
7549
|
+
}
|
|
7550
|
+
/**
|
|
7551
|
+
* Gets a synced calendar for a practitioner
|
|
7552
|
+
* @param practitionerId - ID of the practitioner
|
|
7553
|
+
* @param calendarId - ID of the synced calendar
|
|
7554
|
+
* @returns Synced calendar or null if not found
|
|
7555
|
+
*/
|
|
7556
|
+
async getPractitionerSyncedCalendar(practitionerId, calendarId) {
|
|
7557
|
+
return getPractitionerSyncedCalendarUtil(
|
|
7558
|
+
this.db,
|
|
7559
|
+
practitionerId,
|
|
7560
|
+
calendarId
|
|
7561
|
+
);
|
|
7562
|
+
}
|
|
7563
|
+
/**
|
|
7564
|
+
* Gets all synced calendars for a practitioner
|
|
7565
|
+
* @param practitionerId - ID of the practitioner
|
|
7566
|
+
* @returns Array of synced calendars
|
|
7567
|
+
*/
|
|
7568
|
+
async getPractitionerSyncedCalendars(practitionerId) {
|
|
7569
|
+
return getPractitionerSyncedCalendarsUtil(this.db, practitionerId);
|
|
7570
|
+
}
|
|
7571
|
+
/**
|
|
7572
|
+
* Updates a synced calendar for a practitioner
|
|
7573
|
+
* @param practitionerId - ID of the practitioner
|
|
7574
|
+
* @param calendarId - ID of the synced calendar
|
|
7575
|
+
* @param updateData - Data to update
|
|
7576
|
+
* @returns Updated synced calendar
|
|
7577
|
+
*/
|
|
7578
|
+
async updatePractitionerSyncedCalendar(practitionerId, calendarId, updateData) {
|
|
7579
|
+
return updatePractitionerSyncedCalendarUtil(
|
|
7580
|
+
this.db,
|
|
7581
|
+
practitionerId,
|
|
7582
|
+
calendarId,
|
|
7583
|
+
updateData
|
|
7584
|
+
);
|
|
7585
|
+
}
|
|
7586
|
+
/**
|
|
7587
|
+
* Deletes a synced calendar for a practitioner
|
|
7588
|
+
* @param practitionerId - ID of the practitioner
|
|
7589
|
+
* @param calendarId - ID of the synced calendar
|
|
7590
|
+
*/
|
|
7591
|
+
async deletePractitionerSyncedCalendar(practitionerId, calendarId) {
|
|
7592
|
+
return deletePractitionerSyncedCalendarUtil(
|
|
7593
|
+
this.db,
|
|
7594
|
+
practitionerId,
|
|
7595
|
+
calendarId
|
|
7596
|
+
);
|
|
7597
|
+
}
|
|
7598
|
+
// ===== Patient Synced Calendars =====
|
|
7599
|
+
/**
|
|
7600
|
+
* Creates a synced calendar for a patient
|
|
7601
|
+
* @param patientId - ID of the patient
|
|
7602
|
+
* @param calendarData - Synced calendar data
|
|
7603
|
+
* @returns Created synced calendar
|
|
7604
|
+
*/
|
|
7605
|
+
async createPatientSyncedCalendar(patientId, calendarData) {
|
|
7606
|
+
return createPatientSyncedCalendarUtil(
|
|
7607
|
+
this.db,
|
|
7608
|
+
patientId,
|
|
7609
|
+
calendarData,
|
|
7610
|
+
this.generateId.bind(this)
|
|
7611
|
+
);
|
|
7612
|
+
}
|
|
7613
|
+
/**
|
|
7614
|
+
* Gets a synced calendar for a patient
|
|
7615
|
+
* @param patientId - ID of the patient
|
|
7616
|
+
* @param calendarId - ID of the synced calendar
|
|
7617
|
+
* @returns Synced calendar or null if not found
|
|
7618
|
+
*/
|
|
7619
|
+
async getPatientSyncedCalendar(patientId, calendarId) {
|
|
7620
|
+
return getPatientSyncedCalendarUtil(this.db, patientId, calendarId);
|
|
7621
|
+
}
|
|
7622
|
+
/**
|
|
7623
|
+
* Gets all synced calendars for a patient
|
|
7624
|
+
* @param patientId - ID of the patient
|
|
7625
|
+
* @returns Array of synced calendars
|
|
7626
|
+
*/
|
|
7627
|
+
async getPatientSyncedCalendars(patientId) {
|
|
7628
|
+
return getPatientSyncedCalendarsUtil(this.db, patientId);
|
|
7629
|
+
}
|
|
7630
|
+
/**
|
|
7631
|
+
* Updates a synced calendar for a patient
|
|
7632
|
+
* @param patientId - ID of the patient
|
|
7633
|
+
* @param calendarId - ID of the synced calendar
|
|
7634
|
+
* @param updateData - Data to update
|
|
7635
|
+
* @returns Updated synced calendar
|
|
7636
|
+
*/
|
|
7637
|
+
async updatePatientSyncedCalendar(patientId, calendarId, updateData) {
|
|
7638
|
+
return updatePatientSyncedCalendarUtil(
|
|
7639
|
+
this.db,
|
|
7640
|
+
patientId,
|
|
7641
|
+
calendarId,
|
|
7642
|
+
updateData
|
|
7643
|
+
);
|
|
7644
|
+
}
|
|
7645
|
+
/**
|
|
7646
|
+
* Deletes a synced calendar for a patient
|
|
7647
|
+
* @param patientId - ID of the patient
|
|
7648
|
+
* @param calendarId - ID of the synced calendar
|
|
7649
|
+
*/
|
|
7650
|
+
async deletePatientSyncedCalendar(patientId, calendarId) {
|
|
7651
|
+
return deletePatientSyncedCalendarUtil(this.db, patientId, calendarId);
|
|
7652
|
+
}
|
|
7653
|
+
// ===== Clinic Synced Calendars =====
|
|
7654
|
+
/**
|
|
7655
|
+
* Creates a synced calendar for a clinic
|
|
7656
|
+
* @param clinicId - ID of the clinic
|
|
7657
|
+
* @param calendarData - Synced calendar data
|
|
7658
|
+
* @returns Created synced calendar
|
|
7659
|
+
*/
|
|
7660
|
+
async createClinicSyncedCalendar(clinicId, calendarData) {
|
|
7661
|
+
return createClinicSyncedCalendarUtil(
|
|
7662
|
+
this.db,
|
|
7663
|
+
clinicId,
|
|
7664
|
+
calendarData,
|
|
7665
|
+
this.generateId.bind(this)
|
|
7666
|
+
);
|
|
7667
|
+
}
|
|
7668
|
+
/**
|
|
7669
|
+
* Gets a synced calendar for a clinic
|
|
7670
|
+
* @param clinicId - ID of the clinic
|
|
7671
|
+
* @param calendarId - ID of the synced calendar
|
|
7672
|
+
* @returns Synced calendar or null if not found
|
|
7673
|
+
*/
|
|
7674
|
+
async getClinicSyncedCalendar(clinicId, calendarId) {
|
|
7675
|
+
return getClinicSyncedCalendarUtil(this.db, clinicId, calendarId);
|
|
7676
|
+
}
|
|
7677
|
+
/**
|
|
7678
|
+
* Gets all synced calendars for a clinic
|
|
7679
|
+
* @param clinicId - ID of the clinic
|
|
7680
|
+
* @returns Array of synced calendars
|
|
7681
|
+
*/
|
|
7682
|
+
async getClinicSyncedCalendars(clinicId) {
|
|
7683
|
+
return getClinicSyncedCalendarsUtil(this.db, clinicId);
|
|
7684
|
+
}
|
|
7685
|
+
/**
|
|
7686
|
+
* Updates a synced calendar for a clinic
|
|
7687
|
+
* @param clinicId - ID of the clinic
|
|
7688
|
+
* @param calendarId - ID of the synced calendar
|
|
7689
|
+
* @param updateData - Data to update
|
|
7690
|
+
* @returns Updated synced calendar
|
|
7691
|
+
*/
|
|
7692
|
+
async updateClinicSyncedCalendar(clinicId, calendarId, updateData) {
|
|
7693
|
+
return updateClinicSyncedCalendarUtil(
|
|
7694
|
+
this.db,
|
|
7695
|
+
clinicId,
|
|
7696
|
+
calendarId,
|
|
7697
|
+
updateData
|
|
7698
|
+
);
|
|
7699
|
+
}
|
|
7700
|
+
/**
|
|
7701
|
+
* Deletes a synced calendar for a clinic
|
|
7702
|
+
* @param clinicId - ID of the clinic
|
|
7703
|
+
* @param calendarId - ID of the synced calendar
|
|
7704
|
+
*/
|
|
7705
|
+
async deleteClinicSyncedCalendar(clinicId, calendarId) {
|
|
7706
|
+
return deleteClinicSyncedCalendarUtil(this.db, clinicId, calendarId);
|
|
7707
|
+
}
|
|
7708
|
+
// ===== Google Calendar Integration =====
|
|
7709
|
+
/**
|
|
7710
|
+
* Gets the OAuth URL for Google Calendar
|
|
7711
|
+
* @param scopes - OAuth scopes to request
|
|
7712
|
+
* @returns OAuth URL
|
|
7713
|
+
*/
|
|
7714
|
+
getGoogleCalendarOAuthUrl(scopes = ["https://www.googleapis.com/auth/calendar"]) {
|
|
7715
|
+
return getGoogleCalendarOAuthUrlUtil(scopes);
|
|
7716
|
+
}
|
|
7717
|
+
/**
|
|
7718
|
+
* Authenticates with Google Calendar using an authorization code
|
|
7719
|
+
* @param authCode - Authorization code from Google OAuth
|
|
7720
|
+
* @returns Access token, refresh token, and expiration time
|
|
7721
|
+
*/
|
|
7722
|
+
async authenticateWithGoogleCalendar(authCode) {
|
|
7723
|
+
return authenticateWithGoogleCalendarUtil(authCode);
|
|
7724
|
+
}
|
|
7725
|
+
/**
|
|
7726
|
+
* Lists available Google Calendars for a user
|
|
7727
|
+
* @param accessToken - Google API access token
|
|
7728
|
+
* @returns List of available calendars
|
|
7729
|
+
*/
|
|
7730
|
+
async listGoogleCalendars(accessToken) {
|
|
7731
|
+
return listGoogleCalendarsUtil(accessToken);
|
|
7732
|
+
}
|
|
7733
|
+
/**
|
|
7734
|
+
* Syncs events from our system to Google Calendar for a practitioner
|
|
7735
|
+
* @param practitionerId - ID of the practitioner
|
|
7736
|
+
* @param calendarId - ID of the synced calendar
|
|
7737
|
+
* @param events - Events to sync
|
|
7738
|
+
* @param existingSyncId - Optional existing sync ID for updating an event
|
|
7739
|
+
* @returns Result of the sync operation
|
|
7740
|
+
*/
|
|
7741
|
+
async syncPractitionerEventsToGoogleCalendar(practitionerId, calendarId, events, existingSyncId) {
|
|
7742
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
7743
|
+
practitionerId,
|
|
7744
|
+
calendarId
|
|
7745
|
+
);
|
|
7746
|
+
if (!syncedCalendar) {
|
|
7747
|
+
throw new Error("Synced calendar not found");
|
|
7748
|
+
}
|
|
7749
|
+
return syncEventsToGoogleCalendarUtil(
|
|
7750
|
+
this.db,
|
|
7751
|
+
"practitioner",
|
|
7752
|
+
practitionerId,
|
|
7753
|
+
syncedCalendar,
|
|
7754
|
+
events,
|
|
7755
|
+
existingSyncId
|
|
7756
|
+
);
|
|
7757
|
+
}
|
|
7758
|
+
/**
|
|
7759
|
+
* Syncs events from our system to Google Calendar for a patient
|
|
7760
|
+
* @param patientId - ID of the patient
|
|
7761
|
+
* @param calendarId - ID of the synced calendar
|
|
7762
|
+
* @param events - Events to sync
|
|
7763
|
+
* @param existingSyncId - Optional existing sync ID for updating an event
|
|
7764
|
+
* @returns Result of the sync operation
|
|
7765
|
+
*/
|
|
7766
|
+
async syncPatientEventsToGoogleCalendar(patientId, calendarId, events, existingSyncId) {
|
|
7767
|
+
const syncedCalendar = await this.getPatientSyncedCalendar(
|
|
7768
|
+
patientId,
|
|
7769
|
+
calendarId
|
|
7770
|
+
);
|
|
7771
|
+
if (!syncedCalendar) {
|
|
7772
|
+
throw new Error("Synced calendar not found");
|
|
7773
|
+
}
|
|
7774
|
+
return syncEventsToGoogleCalendarUtil(
|
|
7775
|
+
this.db,
|
|
7776
|
+
"patient",
|
|
7777
|
+
patientId,
|
|
7778
|
+
syncedCalendar,
|
|
7779
|
+
events,
|
|
7780
|
+
existingSyncId
|
|
7781
|
+
);
|
|
7782
|
+
}
|
|
7783
|
+
/**
|
|
7784
|
+
* Syncs events from our system to Google Calendar for a clinic
|
|
7785
|
+
* @param clinicId - ID of the clinic
|
|
7786
|
+
* @param calendarId - ID of the synced calendar
|
|
7787
|
+
* @param events - Events to sync
|
|
7788
|
+
* @returns Result of the sync operation
|
|
7789
|
+
*/
|
|
7790
|
+
async syncClinicEventsToGoogleCalendar(clinicId, calendarId, events) {
|
|
7791
|
+
const syncedCalendar = await this.getClinicSyncedCalendar(
|
|
7792
|
+
clinicId,
|
|
7793
|
+
calendarId
|
|
7794
|
+
);
|
|
7795
|
+
if (!syncedCalendar) {
|
|
7796
|
+
throw new Error("Synced calendar not found");
|
|
7797
|
+
}
|
|
7798
|
+
return syncEventsToGoogleCalendarUtil(
|
|
7799
|
+
this.db,
|
|
7800
|
+
"clinic",
|
|
7801
|
+
clinicId,
|
|
7802
|
+
syncedCalendar,
|
|
7803
|
+
events
|
|
7804
|
+
);
|
|
7805
|
+
}
|
|
7806
|
+
/**
|
|
7807
|
+
* Fetches events from Google Calendar for a practitioner
|
|
7808
|
+
* @param practitionerId - ID of the practitioner
|
|
7809
|
+
* @param calendarId - ID of the synced calendar
|
|
7810
|
+
* @param startDate - Start date for fetching events
|
|
7811
|
+
* @param endDate - End date for fetching events
|
|
7812
|
+
* @returns Events fetched from Google Calendar
|
|
7813
|
+
*/
|
|
7814
|
+
async fetchEventsFromPractitionerGoogleCalendar(practitionerId, calendarId, startDate, endDate) {
|
|
7815
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
7816
|
+
practitionerId,
|
|
7817
|
+
calendarId
|
|
7818
|
+
);
|
|
7819
|
+
if (!syncedCalendar) {
|
|
7820
|
+
throw new Error("Synced calendar not found");
|
|
7821
|
+
}
|
|
7822
|
+
return fetchEventsFromGoogleCalendarUtil(
|
|
7823
|
+
this.db,
|
|
7824
|
+
"practitioner",
|
|
7825
|
+
practitionerId,
|
|
7826
|
+
syncedCalendar,
|
|
7827
|
+
startDate,
|
|
7828
|
+
endDate
|
|
7829
|
+
);
|
|
7830
|
+
}
|
|
7831
|
+
/**
|
|
7832
|
+
* Fetches events from Google Calendar for a patient
|
|
7833
|
+
* @param patientId - ID of the patient
|
|
7834
|
+
* @param calendarId - ID of the synced calendar
|
|
7835
|
+
* @param startDate - Start date for fetching events
|
|
7836
|
+
* @param endDate - End date for fetching events
|
|
7837
|
+
* @returns Events fetched from Google Calendar
|
|
7838
|
+
*/
|
|
7839
|
+
async fetchEventsFromPatientGoogleCalendar(patientId, calendarId, startDate, endDate) {
|
|
7840
|
+
const syncedCalendar = await this.getPatientSyncedCalendar(
|
|
7841
|
+
patientId,
|
|
7842
|
+
calendarId
|
|
7843
|
+
);
|
|
7844
|
+
if (!syncedCalendar) {
|
|
7845
|
+
throw new Error("Synced calendar not found");
|
|
7846
|
+
}
|
|
7847
|
+
return fetchEventsFromGoogleCalendarUtil(
|
|
7848
|
+
this.db,
|
|
7849
|
+
"patient",
|
|
7850
|
+
patientId,
|
|
7851
|
+
syncedCalendar,
|
|
7852
|
+
startDate,
|
|
7853
|
+
endDate
|
|
7854
|
+
);
|
|
7855
|
+
}
|
|
7856
|
+
/**
|
|
7857
|
+
* Fetches events from Google Calendar for a clinic
|
|
7858
|
+
* @param clinicId - ID of the clinic
|
|
7859
|
+
* @param calendarId - ID of the synced calendar
|
|
7860
|
+
* @param startDate - Start date for fetching events
|
|
7861
|
+
* @param endDate - End date for fetching events
|
|
7862
|
+
* @returns Events fetched from Google Calendar
|
|
7863
|
+
*/
|
|
7864
|
+
async fetchEventsFromClinicGoogleCalendar(clinicId, calendarId, startDate, endDate) {
|
|
7865
|
+
const syncedCalendar = await this.getClinicSyncedCalendar(
|
|
7866
|
+
clinicId,
|
|
7867
|
+
calendarId
|
|
7868
|
+
);
|
|
7869
|
+
if (!syncedCalendar) {
|
|
7870
|
+
throw new Error("Synced calendar not found");
|
|
7871
|
+
}
|
|
7872
|
+
return fetchEventsFromGoogleCalendarUtil(
|
|
7873
|
+
this.db,
|
|
7874
|
+
"clinic",
|
|
7875
|
+
clinicId,
|
|
7876
|
+
syncedCalendar,
|
|
7877
|
+
startDate,
|
|
7878
|
+
endDate
|
|
7879
|
+
);
|
|
7880
|
+
}
|
|
7881
|
+
/**
|
|
7882
|
+
* Deletes an event from Google Calendar for a practitioner
|
|
7883
|
+
* @param practitionerId - ID of the practitioner
|
|
7884
|
+
* @param calendarId - ID of the synced calendar
|
|
7885
|
+
* @param eventId - ID of the event in Google Calendar
|
|
7886
|
+
* @returns Success status
|
|
7887
|
+
*/
|
|
7888
|
+
async deletePractitionerGoogleCalendarEvent(practitionerId, calendarId, eventId) {
|
|
7889
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
7890
|
+
practitionerId,
|
|
7891
|
+
calendarId
|
|
7892
|
+
);
|
|
7893
|
+
if (!syncedCalendar) {
|
|
7894
|
+
throw new Error("Synced calendar not found");
|
|
7895
|
+
}
|
|
7896
|
+
return deleteGoogleCalendarEventUtil(
|
|
7897
|
+
this.db,
|
|
7898
|
+
"practitioner",
|
|
7899
|
+
practitionerId,
|
|
7900
|
+
syncedCalendar,
|
|
7901
|
+
eventId
|
|
7902
|
+
);
|
|
7903
|
+
}
|
|
7904
|
+
/**
|
|
7905
|
+
* Deletes an event from Google Calendar for a patient
|
|
7906
|
+
* @param patientId - ID of the patient
|
|
7907
|
+
* @param calendarId - ID of the synced calendar
|
|
7908
|
+
* @param eventId - ID of the event in Google Calendar
|
|
7909
|
+
* @returns Success status
|
|
7910
|
+
*/
|
|
7911
|
+
async deletePatientGoogleCalendarEvent(patientId, calendarId, eventId) {
|
|
7912
|
+
const syncedCalendar = await this.getPatientSyncedCalendar(
|
|
7913
|
+
patientId,
|
|
7914
|
+
calendarId
|
|
7915
|
+
);
|
|
7916
|
+
if (!syncedCalendar) {
|
|
7917
|
+
throw new Error("Synced calendar not found");
|
|
7918
|
+
}
|
|
7919
|
+
return deleteGoogleCalendarEventUtil(
|
|
7920
|
+
this.db,
|
|
7921
|
+
"patient",
|
|
7922
|
+
patientId,
|
|
7923
|
+
syncedCalendar,
|
|
7924
|
+
eventId
|
|
7925
|
+
);
|
|
7926
|
+
}
|
|
7927
|
+
/**
|
|
7928
|
+
* Deletes an event from Google Calendar for a clinic
|
|
7929
|
+
* @param clinicId - ID of the clinic
|
|
7930
|
+
* @param calendarId - ID of the synced calendar
|
|
7931
|
+
* @param eventId - ID of the event in Google Calendar
|
|
7932
|
+
* @returns Success status
|
|
7933
|
+
*/
|
|
7934
|
+
async deleteClinicGoogleCalendarEvent(clinicId, calendarId, eventId) {
|
|
7935
|
+
const syncedCalendar = await this.getClinicSyncedCalendar(
|
|
7936
|
+
clinicId,
|
|
7937
|
+
calendarId
|
|
7938
|
+
);
|
|
7939
|
+
if (!syncedCalendar) {
|
|
7940
|
+
throw new Error("Synced calendar not found");
|
|
7941
|
+
}
|
|
7942
|
+
return deleteGoogleCalendarEventUtil(
|
|
7943
|
+
this.db,
|
|
7944
|
+
"clinic",
|
|
7945
|
+
clinicId,
|
|
7946
|
+
syncedCalendar,
|
|
7947
|
+
eventId
|
|
7948
|
+
);
|
|
7949
|
+
}
|
|
7950
|
+
/**
|
|
7951
|
+
* Converts Google Calendar events to our system's format for a practitioner
|
|
7952
|
+
* @param practitionerId - ID of the practitioner
|
|
7953
|
+
* @param googleEvents - Google Calendar events
|
|
7954
|
+
* @returns Converted calendar events
|
|
7955
|
+
*/
|
|
7956
|
+
convertGoogleEventsToPractitionerEvents(practitionerId, googleEvents) {
|
|
7957
|
+
return googleEvents.map(
|
|
7958
|
+
(event) => convertGoogleEventToCalendarEventUtil(
|
|
7959
|
+
event,
|
|
7960
|
+
practitionerId,
|
|
7961
|
+
"practitioner"
|
|
7962
|
+
)
|
|
7963
|
+
);
|
|
7964
|
+
}
|
|
7965
|
+
/**
|
|
7966
|
+
* Converts Google Calendar events to our system's format for a patient
|
|
7967
|
+
* @param patientId - ID of the patient
|
|
7968
|
+
* @param googleEvents - Google Calendar events
|
|
7969
|
+
* @returns Converted calendar events
|
|
7970
|
+
*/
|
|
7971
|
+
convertGoogleEventsToPatientEvents(patientId, googleEvents) {
|
|
7972
|
+
return googleEvents.map(
|
|
7973
|
+
(event) => convertGoogleEventToCalendarEventUtil(event, patientId, "patient")
|
|
7974
|
+
);
|
|
7975
|
+
}
|
|
7976
|
+
/**
|
|
7977
|
+
* Converts Google Calendar events to our system's format for a clinic
|
|
7978
|
+
* @param clinicId - ID of the clinic
|
|
7979
|
+
* @param googleEvents - Google Calendar events
|
|
7980
|
+
* @returns Converted calendar events
|
|
7981
|
+
*/
|
|
7982
|
+
convertGoogleEventsToClinicEvents(clinicId, googleEvents) {
|
|
7983
|
+
return googleEvents.map(
|
|
7984
|
+
(event) => convertGoogleEventToCalendarEventUtil(event, clinicId, "clinic")
|
|
7985
|
+
);
|
|
7986
|
+
}
|
|
7987
|
+
/**
|
|
7988
|
+
* Fetches a single event from Google Calendar for a practitioner
|
|
7989
|
+
* @param practitionerId - ID of the practitioner
|
|
7990
|
+
* @param calendarId - ID of the synced calendar
|
|
7991
|
+
* @param eventId - ID of the event in Google Calendar
|
|
7992
|
+
* @returns The event data or null if not found
|
|
7993
|
+
*/
|
|
7994
|
+
async fetchEventFromPractitionerGoogleCalendar(practitionerId, calendarId, eventId) {
|
|
7995
|
+
var _a;
|
|
7996
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
7997
|
+
practitionerId,
|
|
7998
|
+
calendarId
|
|
7999
|
+
);
|
|
8000
|
+
if (!syncedCalendar) {
|
|
8001
|
+
throw new Error("Synced calendar not found");
|
|
8002
|
+
}
|
|
8003
|
+
try {
|
|
8004
|
+
const { accessToken } = await refreshGoogleCalendarTokenUtil(
|
|
8005
|
+
syncedCalendar.refreshToken
|
|
8006
|
+
);
|
|
8007
|
+
const response = await makeRequest(
|
|
8008
|
+
"get",
|
|
8009
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
|
|
8010
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
8011
|
+
);
|
|
8012
|
+
await updateLastSyncedTimestampUtil(
|
|
8013
|
+
this.db,
|
|
8014
|
+
"practitioner",
|
|
8015
|
+
practitionerId,
|
|
8016
|
+
syncedCalendar.id
|
|
8017
|
+
);
|
|
8018
|
+
return response;
|
|
8019
|
+
} catch (error) {
|
|
8020
|
+
if (((_a = error.response) == null ? void 0 : _a.status) === 404) {
|
|
8021
|
+
return null;
|
|
8022
|
+
}
|
|
8023
|
+
console.error(
|
|
8024
|
+
`Error fetching event from Google Calendar: ${error.message}`
|
|
8025
|
+
);
|
|
8026
|
+
throw error;
|
|
8027
|
+
}
|
|
8028
|
+
}
|
|
8029
|
+
};
|
|
8030
|
+
|
|
8031
|
+
// src/services/calendar/calendar-refactored.service.ts
|
|
8032
|
+
var MIN_APPOINTMENT_DURATION2 = 15;
|
|
8033
|
+
var CalendarServiceV2 = class extends BaseService {
|
|
8034
|
+
/**
|
|
8035
|
+
* Creates a new CalendarService instance
|
|
8036
|
+
* @param db - Firestore instance
|
|
8037
|
+
* @param auth - Firebase Auth instance
|
|
8038
|
+
* @param app - Firebase App instance
|
|
8039
|
+
*/
|
|
8040
|
+
constructor(db, auth, app) {
|
|
8041
|
+
super(db, auth, app);
|
|
8042
|
+
this.syncedCalendarsService = new SyncedCalendarsService(db, auth, app);
|
|
8043
|
+
}
|
|
8044
|
+
// #region Public API Methods
|
|
8045
|
+
/**
|
|
8046
|
+
* Creates a new appointment with proper validation and scheduling rules
|
|
8047
|
+
* @param params - Appointment creation parameters
|
|
8048
|
+
* @returns Created calendar event
|
|
8049
|
+
*/
|
|
8050
|
+
async createAppointment(params) {
|
|
8051
|
+
await this.validateAppointmentParams(params);
|
|
8052
|
+
await this.validateClinicWorkingHours(params.clinicId, params.eventTime);
|
|
8053
|
+
await this.validateDoctorAvailability(
|
|
8054
|
+
params.doctorId,
|
|
8055
|
+
params.eventTime,
|
|
8056
|
+
params.clinicId
|
|
8057
|
+
);
|
|
8058
|
+
const { clinicInfo, practitionerInfo, patientInfo } = await this.fetchProfileInfoCards(
|
|
8059
|
+
params.clinicId,
|
|
8060
|
+
params.doctorId,
|
|
8061
|
+
params.patientId
|
|
8062
|
+
);
|
|
8063
|
+
const appointmentData = {
|
|
8064
|
+
clinicBranchId: params.clinicId,
|
|
8065
|
+
clinicBranchInfo: clinicInfo,
|
|
8066
|
+
practitionerProfileId: params.doctorId,
|
|
8067
|
+
practitionerProfileInfo: practitionerInfo,
|
|
8068
|
+
patientProfileId: params.patientId,
|
|
8069
|
+
patientProfileInfo: patientInfo,
|
|
8070
|
+
procedureId: params.procedureId,
|
|
8071
|
+
eventLocation: params.eventLocation,
|
|
8072
|
+
eventName: "Appointment",
|
|
8073
|
+
// TODO: Add procedure name when procedure model is available
|
|
8074
|
+
eventTime: params.eventTime,
|
|
8075
|
+
description: params.description || "",
|
|
8076
|
+
status: "pending" /* PENDING */,
|
|
8077
|
+
syncStatus: "internal" /* INTERNAL */,
|
|
8078
|
+
eventType: "appointment" /* APPOINTMENT */
|
|
8079
|
+
};
|
|
8080
|
+
const appointment = await createAppointmentUtil(
|
|
8081
|
+
this.db,
|
|
8082
|
+
params.clinicId,
|
|
8083
|
+
params.doctorId,
|
|
8084
|
+
params.patientId,
|
|
8085
|
+
appointmentData,
|
|
8086
|
+
this.generateId.bind(this)
|
|
8087
|
+
);
|
|
8088
|
+
await this.syncAppointmentWithExternalCalendars(appointment);
|
|
8089
|
+
return appointment;
|
|
8090
|
+
}
|
|
8091
|
+
/**
|
|
8092
|
+
* Updates an existing appointment
|
|
8093
|
+
* @param params - Appointment update parameters
|
|
8094
|
+
* @returns Updated calendar event
|
|
8095
|
+
*/
|
|
8096
|
+
async updateAppointment(params) {
|
|
8097
|
+
await this.validateUpdatePermissions(params);
|
|
8098
|
+
const updateData = {
|
|
8099
|
+
eventTime: params.eventTime,
|
|
8100
|
+
description: params.description,
|
|
8101
|
+
status: params.status
|
|
8102
|
+
};
|
|
8103
|
+
const appointment = await updateAppointmentUtil(
|
|
8104
|
+
this.db,
|
|
8105
|
+
params.clinicId,
|
|
8106
|
+
params.doctorId,
|
|
8107
|
+
params.patientId,
|
|
8108
|
+
params.appointmentId,
|
|
8109
|
+
updateData
|
|
8110
|
+
);
|
|
8111
|
+
await this.syncAppointmentWithExternalCalendars(appointment);
|
|
8112
|
+
return appointment;
|
|
8113
|
+
}
|
|
8114
|
+
/**
|
|
8115
|
+
* Gets available appointment slots for a doctor at a clinic
|
|
8116
|
+
* @param clinicId - ID of the clinic
|
|
8117
|
+
* @param doctorId - ID of the doctor
|
|
8118
|
+
* @param date - Date to check availability for
|
|
8119
|
+
* @returns Array of available time slots
|
|
8120
|
+
*/
|
|
8121
|
+
async getAvailableSlots(clinicId, doctorId, date) {
|
|
8122
|
+
const workingHours = await this.getClinicWorkingHours(clinicId, date);
|
|
8123
|
+
const doctorSchedule = await this.getDoctorSchedule(doctorId, date);
|
|
8124
|
+
const existingAppointments = await this.getDoctorAppointments(
|
|
8125
|
+
doctorId,
|
|
8126
|
+
date
|
|
8127
|
+
);
|
|
8128
|
+
return this.calculateAvailableSlots(
|
|
8129
|
+
workingHours,
|
|
8130
|
+
doctorSchedule,
|
|
8131
|
+
existingAppointments
|
|
8132
|
+
);
|
|
8133
|
+
}
|
|
8134
|
+
/**
|
|
8135
|
+
* Confirms an appointment
|
|
8136
|
+
* @param appointmentId - ID of the appointment
|
|
8137
|
+
* @param clinicId - ID of the clinic
|
|
8138
|
+
* @returns Confirmed calendar event
|
|
8139
|
+
*/
|
|
8140
|
+
async confirmAppointment(appointmentId, clinicId) {
|
|
8141
|
+
return this.updateAppointmentStatus(
|
|
8142
|
+
appointmentId,
|
|
8143
|
+
clinicId,
|
|
8144
|
+
"confirmed" /* CONFIRMED */
|
|
8145
|
+
);
|
|
8146
|
+
}
|
|
8147
|
+
/**
|
|
8148
|
+
* Rejects an appointment
|
|
8149
|
+
* @param appointmentId - ID of the appointment
|
|
8150
|
+
* @param clinicId - ID of the clinic
|
|
8151
|
+
* @returns Rejected calendar event
|
|
8152
|
+
*/
|
|
8153
|
+
async rejectAppointment(appointmentId, clinicId) {
|
|
8154
|
+
return this.updateAppointmentStatus(
|
|
8155
|
+
appointmentId,
|
|
8156
|
+
clinicId,
|
|
8157
|
+
"rejected" /* REJECTED */
|
|
8158
|
+
);
|
|
8159
|
+
}
|
|
8160
|
+
/**
|
|
8161
|
+
* Cancels an appointment
|
|
8162
|
+
* @param appointmentId - ID of the appointment
|
|
8163
|
+
* @param clinicId - ID of the clinic
|
|
8164
|
+
* @returns Canceled calendar event
|
|
8165
|
+
*/
|
|
8166
|
+
async cancelAppointment(appointmentId, clinicId) {
|
|
8167
|
+
return this.updateAppointmentStatus(
|
|
8168
|
+
appointmentId,
|
|
8169
|
+
clinicId,
|
|
8170
|
+
"canceled" /* CANCELED */
|
|
8171
|
+
);
|
|
8172
|
+
}
|
|
8173
|
+
/**
|
|
8174
|
+
* Imports events from external calendars
|
|
8175
|
+
* @param entityType - Type of entity (practitioner or patient)
|
|
8176
|
+
* @param entityId - ID of the entity
|
|
8177
|
+
* @param startDate - Start date for fetching events
|
|
8178
|
+
* @param endDate - End date for fetching events
|
|
8179
|
+
* @returns Number of events imported
|
|
8180
|
+
*/
|
|
8181
|
+
async importEventsFromExternalCalendars(entityType, entityId, startDate, endDate) {
|
|
8182
|
+
if (entityType === "patient") {
|
|
8183
|
+
return 0;
|
|
8184
|
+
}
|
|
8185
|
+
const syncedCalendars = await this.syncedCalendarsService.getPractitionerSyncedCalendars(
|
|
8186
|
+
entityId
|
|
8187
|
+
);
|
|
8188
|
+
const activeCalendars = syncedCalendars.filter((cal) => cal.isActive);
|
|
8189
|
+
if (activeCalendars.length === 0) {
|
|
8190
|
+
return 0;
|
|
8191
|
+
}
|
|
8192
|
+
let importedEventsCount = 0;
|
|
8193
|
+
const currentTime = Timestamp23.now();
|
|
8194
|
+
for (const calendar of activeCalendars) {
|
|
8195
|
+
try {
|
|
8196
|
+
let externalEvents = [];
|
|
8197
|
+
if (calendar.provider === "google" /* GOOGLE */) {
|
|
8198
|
+
externalEvents = await this.syncedCalendarsService.fetchEventsFromPractitionerGoogleCalendar(
|
|
8199
|
+
entityId,
|
|
8200
|
+
calendar.id,
|
|
8201
|
+
startDate,
|
|
8202
|
+
endDate
|
|
8203
|
+
);
|
|
8204
|
+
}
|
|
8205
|
+
for (const externalEvent of externalEvents) {
|
|
8206
|
+
try {
|
|
8207
|
+
const convertedEvent = this.syncedCalendarsService.convertGoogleEventsToPractitionerEvents(
|
|
8208
|
+
entityId,
|
|
8209
|
+
[externalEvent]
|
|
8210
|
+
)[0];
|
|
8211
|
+
if (!convertedEvent.eventTime) {
|
|
8212
|
+
continue;
|
|
8213
|
+
}
|
|
8214
|
+
const eventData = {
|
|
8215
|
+
// Ensure all required fields are set
|
|
8216
|
+
eventName: convertedEvent.eventName || "External Event",
|
|
8217
|
+
eventTime: convertedEvent.eventTime,
|
|
8218
|
+
description: convertedEvent.description || "",
|
|
8219
|
+
status: "confirmed" /* CONFIRMED */,
|
|
8220
|
+
syncStatus: "external" /* EXTERNAL */,
|
|
8221
|
+
eventType: "blocking" /* BLOCKING */,
|
|
8222
|
+
practitionerProfileId: entityId,
|
|
8223
|
+
syncedCalendarEventId: [
|
|
8224
|
+
{
|
|
8225
|
+
eventId: externalEvent.id,
|
|
8226
|
+
syncedCalendarProvider: calendar.provider,
|
|
8227
|
+
syncedAt: currentTime
|
|
8228
|
+
}
|
|
8229
|
+
]
|
|
8230
|
+
};
|
|
8231
|
+
const doctorEvent = await this.createDoctorBlockingEvent(
|
|
8232
|
+
entityId,
|
|
8233
|
+
eventData
|
|
8234
|
+
);
|
|
8235
|
+
if (doctorEvent) {
|
|
8236
|
+
importedEventsCount++;
|
|
8237
|
+
}
|
|
8238
|
+
} catch (eventError) {
|
|
8239
|
+
console.error("Error importing event:", eventError);
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
} catch (calendarError) {
|
|
8243
|
+
console.error(
|
|
8244
|
+
`Error fetching events from calendar ${calendar.id}:`,
|
|
8245
|
+
calendarError
|
|
8246
|
+
);
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
return importedEventsCount;
|
|
8250
|
+
}
|
|
8251
|
+
/**
|
|
8252
|
+
* Creates a blocking event in a doctor's calendar
|
|
8253
|
+
* @param doctorId - ID of the doctor
|
|
8254
|
+
* @param eventData - Calendar event data
|
|
8255
|
+
* @returns Created calendar event
|
|
8256
|
+
*/
|
|
8257
|
+
async createDoctorBlockingEvent(doctorId, eventData) {
|
|
8258
|
+
try {
|
|
8259
|
+
const eventId = this.generateId();
|
|
8260
|
+
const eventRef = doc19(
|
|
8261
|
+
this.db,
|
|
8262
|
+
PRACTITIONERS_COLLECTION,
|
|
8263
|
+
doctorId,
|
|
8264
|
+
CALENDAR_COLLECTION,
|
|
8265
|
+
eventId
|
|
8266
|
+
);
|
|
8267
|
+
const newEvent = {
|
|
8268
|
+
id: eventId,
|
|
8269
|
+
...eventData,
|
|
8270
|
+
createdAt: serverTimestamp18(),
|
|
8271
|
+
updatedAt: serverTimestamp18()
|
|
8272
|
+
};
|
|
8273
|
+
await setDoc19(eventRef, newEvent);
|
|
8274
|
+
return {
|
|
8275
|
+
...newEvent,
|
|
8276
|
+
createdAt: Timestamp23.now(),
|
|
8277
|
+
updatedAt: Timestamp23.now()
|
|
8278
|
+
};
|
|
8279
|
+
} catch (error) {
|
|
8280
|
+
console.error(
|
|
8281
|
+
`Error creating blocking event for doctor ${doctorId}:`,
|
|
8282
|
+
error
|
|
8283
|
+
);
|
|
8284
|
+
return null;
|
|
8285
|
+
}
|
|
8286
|
+
}
|
|
8287
|
+
/**
|
|
8288
|
+
* Periodically syncs events from external calendars for doctors
|
|
8289
|
+
* This would be called via a scheduled Cloud Function
|
|
8290
|
+
* @param lookbackDays - Number of days to look back for events
|
|
8291
|
+
* @param lookforwardDays - Number of days to look forward for events
|
|
8292
|
+
*/
|
|
8293
|
+
async synchronizeExternalCalendars(lookbackDays = 7, lookforwardDays = 30) {
|
|
8294
|
+
try {
|
|
8295
|
+
const practitionersRef = collection17(this.db, PRACTITIONERS_COLLECTION);
|
|
8296
|
+
const practitionersSnapshot = await getDocs16(practitionersRef);
|
|
8297
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
8298
|
+
startDate.setDate(startDate.getDate() - lookbackDays);
|
|
8299
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
8300
|
+
endDate.setDate(endDate.getDate() + lookforwardDays);
|
|
8301
|
+
const syncPromises = [];
|
|
8302
|
+
for (const docSnapshot of practitionersSnapshot.docs) {
|
|
8303
|
+
const practitionerId = docSnapshot.id;
|
|
8304
|
+
syncPromises.push(
|
|
8305
|
+
this.importEventsFromExternalCalendars(
|
|
8306
|
+
"doctor",
|
|
8307
|
+
practitionerId,
|
|
8308
|
+
startDate,
|
|
8309
|
+
endDate
|
|
8310
|
+
).then((count) => {
|
|
8311
|
+
console.log(
|
|
8312
|
+
`Imported ${count} events for doctor ${practitionerId}`
|
|
8313
|
+
);
|
|
8314
|
+
}).catch((error) => {
|
|
8315
|
+
console.error(
|
|
8316
|
+
`Error importing events for doctor ${practitionerId}:`,
|
|
8317
|
+
error
|
|
8318
|
+
);
|
|
8319
|
+
})
|
|
8320
|
+
);
|
|
8321
|
+
syncPromises.push(
|
|
8322
|
+
this.updateExistingEventsFromExternalCalendars(
|
|
8323
|
+
practitionerId,
|
|
8324
|
+
startDate,
|
|
8325
|
+
endDate
|
|
8326
|
+
).then((count) => {
|
|
8327
|
+
console.log(
|
|
8328
|
+
`Updated ${count} events for doctor ${practitionerId}`
|
|
8329
|
+
);
|
|
8330
|
+
}).catch((error) => {
|
|
8331
|
+
console.error(
|
|
8332
|
+
`Error updating events for doctor ${practitionerId}:`,
|
|
8333
|
+
error
|
|
8334
|
+
);
|
|
8335
|
+
})
|
|
8336
|
+
);
|
|
8337
|
+
}
|
|
8338
|
+
await Promise.all(syncPromises);
|
|
8339
|
+
console.log("Completed external calendar synchronization");
|
|
8340
|
+
} catch (error) {
|
|
8341
|
+
console.error("Error synchronizing external calendars:", error);
|
|
8342
|
+
}
|
|
8343
|
+
}
|
|
8344
|
+
/**
|
|
8345
|
+
* Updates existing events that were synced from external calendars
|
|
8346
|
+
* @param doctorId - ID of the doctor
|
|
8347
|
+
* @param startDate - Start date for fetching events
|
|
8348
|
+
* @param endDate - End date for fetching events
|
|
8349
|
+
* @returns Number of events updated
|
|
8350
|
+
*/
|
|
8351
|
+
async updateExistingEventsFromExternalCalendars(doctorId, startDate, endDate) {
|
|
8352
|
+
var _a;
|
|
8353
|
+
try {
|
|
8354
|
+
const eventsRef = collection17(
|
|
8355
|
+
this.db,
|
|
8356
|
+
PRACTITIONERS_COLLECTION,
|
|
8357
|
+
doctorId,
|
|
8358
|
+
CALENDAR_COLLECTION
|
|
8359
|
+
);
|
|
8360
|
+
const q = query16(
|
|
8361
|
+
eventsRef,
|
|
8362
|
+
where16("syncStatus", "==", "external" /* EXTERNAL */),
|
|
8363
|
+
where16("eventTime.start", ">=", Timestamp23.fromDate(startDate)),
|
|
8364
|
+
where16("eventTime.start", "<=", Timestamp23.fromDate(endDate))
|
|
8365
|
+
);
|
|
8366
|
+
const eventsSnapshot = await getDocs16(q);
|
|
8367
|
+
const events = eventsSnapshot.docs.map((doc20) => ({
|
|
8368
|
+
id: doc20.id,
|
|
8369
|
+
...doc20.data()
|
|
8370
|
+
}));
|
|
8371
|
+
const calendars = await this.syncedCalendarsService.getPractitionerSyncedCalendars(
|
|
8372
|
+
doctorId
|
|
8373
|
+
);
|
|
8374
|
+
const activeCalendars = calendars.filter((cal) => cal.isActive);
|
|
8375
|
+
if (activeCalendars.length === 0 || events.length === 0) {
|
|
8376
|
+
return 0;
|
|
8377
|
+
}
|
|
8378
|
+
let updatedCount = 0;
|
|
8379
|
+
for (const event of events) {
|
|
8380
|
+
if (!((_a = event.syncedCalendarEventId) == null ? void 0 : _a.length)) continue;
|
|
8381
|
+
for (const syncId of event.syncedCalendarEventId) {
|
|
8382
|
+
const calendar = activeCalendars.find(
|
|
8383
|
+
(cal) => cal.provider === syncId.syncedCalendarProvider
|
|
8384
|
+
);
|
|
8385
|
+
if (!calendar) continue;
|
|
8386
|
+
if (syncId.syncedCalendarProvider === "google" /* GOOGLE */) {
|
|
8387
|
+
try {
|
|
8388
|
+
const externalEvent = await this.fetchExternalEvent(
|
|
8389
|
+
doctorId,
|
|
8390
|
+
calendar,
|
|
8391
|
+
syncId.eventId
|
|
8392
|
+
);
|
|
8393
|
+
if (externalEvent) {
|
|
8394
|
+
const externalStartTime = new Date(
|
|
8395
|
+
externalEvent.start.dateTime || externalEvent.start.date
|
|
8396
|
+
).getTime();
|
|
8397
|
+
const externalEndTime = new Date(
|
|
8398
|
+
externalEvent.end.dateTime || externalEvent.end.date
|
|
8399
|
+
).getTime();
|
|
8400
|
+
const localStartTime = event.eventTime.start.toDate().getTime();
|
|
8401
|
+
const localEndTime = event.eventTime.end.toDate().getTime();
|
|
8402
|
+
if (externalStartTime !== localStartTime || externalEndTime !== localEndTime || externalEvent.summary !== event.eventName || externalEvent.description !== event.description) {
|
|
8403
|
+
await this.updateLocalEventFromExternal(
|
|
8404
|
+
doctorId,
|
|
8405
|
+
event.id,
|
|
8406
|
+
externalEvent
|
|
8407
|
+
);
|
|
8408
|
+
updatedCount++;
|
|
8409
|
+
}
|
|
8410
|
+
} else {
|
|
8411
|
+
await this.updateEventStatus(
|
|
8412
|
+
doctorId,
|
|
8413
|
+
event.id,
|
|
8414
|
+
"canceled" /* CANCELED */
|
|
8415
|
+
);
|
|
8416
|
+
updatedCount++;
|
|
8417
|
+
}
|
|
8418
|
+
} catch (error) {
|
|
8419
|
+
console.error(
|
|
8420
|
+
`Error updating external event ${event.id}:`,
|
|
8421
|
+
error
|
|
8422
|
+
);
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
return updatedCount;
|
|
8428
|
+
} catch (error) {
|
|
8429
|
+
console.error(
|
|
8430
|
+
"Error updating existing events from external calendars:",
|
|
8431
|
+
error
|
|
8432
|
+
);
|
|
8433
|
+
return 0;
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
/**
|
|
8437
|
+
* Fetches a single external event from Google Calendar
|
|
8438
|
+
* @param doctorId - ID of the doctor
|
|
8439
|
+
* @param calendar - Calendar information
|
|
8440
|
+
* @param externalEventId - ID of the external event
|
|
8441
|
+
* @returns External event data or null if not found
|
|
8442
|
+
*/
|
|
8443
|
+
async fetchExternalEvent(doctorId, calendar, externalEventId) {
|
|
8444
|
+
try {
|
|
8445
|
+
if (calendar.provider === "google" /* GOOGLE */) {
|
|
8446
|
+
const result = await this.syncedCalendarsService.fetchEventFromPractitionerGoogleCalendar(
|
|
8447
|
+
doctorId,
|
|
8448
|
+
calendar.id,
|
|
8449
|
+
externalEventId
|
|
8450
|
+
);
|
|
8451
|
+
return result;
|
|
8452
|
+
}
|
|
8453
|
+
return null;
|
|
8454
|
+
} catch (error) {
|
|
8455
|
+
console.error(`Error fetching external event ${externalEventId}:`, error);
|
|
8456
|
+
return null;
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
/**
|
|
8460
|
+
* Updates a local event with data from an external event
|
|
8461
|
+
* @param doctorId - ID of the doctor
|
|
8462
|
+
* @param eventId - ID of the local event
|
|
8463
|
+
* @param externalEvent - External event data
|
|
8464
|
+
*/
|
|
8465
|
+
async updateLocalEventFromExternal(doctorId, eventId, externalEvent) {
|
|
8466
|
+
try {
|
|
8467
|
+
const startTime = new Date(
|
|
8468
|
+
externalEvent.start.dateTime || externalEvent.start.date
|
|
8469
|
+
);
|
|
8470
|
+
const endTime = new Date(
|
|
8471
|
+
externalEvent.end.dateTime || externalEvent.end.date
|
|
8472
|
+
);
|
|
8473
|
+
const eventRef = doc19(
|
|
8474
|
+
this.db,
|
|
8475
|
+
PRACTITIONERS_COLLECTION,
|
|
8476
|
+
doctorId,
|
|
8477
|
+
CALENDAR_COLLECTION,
|
|
8478
|
+
eventId
|
|
8479
|
+
);
|
|
8480
|
+
await updateDoc20(eventRef, {
|
|
8481
|
+
eventName: externalEvent.summary || "External Event",
|
|
8482
|
+
eventTime: {
|
|
8483
|
+
start: Timestamp23.fromDate(startTime),
|
|
8484
|
+
end: Timestamp23.fromDate(endTime)
|
|
8485
|
+
},
|
|
8486
|
+
description: externalEvent.description || "",
|
|
8487
|
+
updatedAt: serverTimestamp18()
|
|
8488
|
+
});
|
|
8489
|
+
console.log(`Updated local event ${eventId} from external event`);
|
|
8490
|
+
} catch (error) {
|
|
8491
|
+
console.error(
|
|
8492
|
+
`Error updating local event ${eventId} from external:`,
|
|
8493
|
+
error
|
|
8494
|
+
);
|
|
8495
|
+
}
|
|
8496
|
+
}
|
|
8497
|
+
/**
|
|
8498
|
+
* Updates an event's status
|
|
8499
|
+
* @param doctorId - ID of the doctor
|
|
8500
|
+
* @param eventId - ID of the event
|
|
8501
|
+
* @param status - New status
|
|
8502
|
+
*/
|
|
8503
|
+
async updateEventStatus(doctorId, eventId, status) {
|
|
8504
|
+
try {
|
|
8505
|
+
const eventRef = doc19(
|
|
8506
|
+
this.db,
|
|
8507
|
+
PRACTITIONERS_COLLECTION,
|
|
8508
|
+
doctorId,
|
|
8509
|
+
CALENDAR_COLLECTION,
|
|
8510
|
+
eventId
|
|
8511
|
+
);
|
|
8512
|
+
await updateDoc20(eventRef, {
|
|
8513
|
+
status,
|
|
8514
|
+
updatedAt: serverTimestamp18()
|
|
8515
|
+
});
|
|
8516
|
+
console.log(`Updated event ${eventId} status to ${status}`);
|
|
8517
|
+
} catch (error) {
|
|
8518
|
+
console.error(`Error updating event ${eventId} status:`, error);
|
|
8519
|
+
}
|
|
8520
|
+
}
|
|
8521
|
+
/**
|
|
8522
|
+
* Creates a scheduled job to periodically sync external calendars
|
|
8523
|
+
* Note: This would be implemented using Cloud Functions in a real application
|
|
8524
|
+
* This is a sample implementation to show how it could be set up
|
|
8525
|
+
* @param interval - Interval in hours
|
|
8526
|
+
*/
|
|
8527
|
+
createScheduledSyncJob(interval = 3) {
|
|
8528
|
+
console.log(
|
|
8529
|
+
`Setting up scheduled calendar sync job every ${interval} hours`
|
|
8530
|
+
);
|
|
8531
|
+
}
|
|
8532
|
+
// #endregion
|
|
8533
|
+
// #region Private Helper Methods
|
|
8534
|
+
/**
|
|
8535
|
+
* Validates appointment creation parameters
|
|
8536
|
+
* @param params - Appointment parameters to validate
|
|
8537
|
+
* @throws Error if validation fails
|
|
8538
|
+
*/
|
|
8539
|
+
async validateAppointmentParams(params) {
|
|
8540
|
+
await createAppointmentSchema.parseAsync(params);
|
|
8541
|
+
}
|
|
8542
|
+
/**
|
|
8543
|
+
* Validates if the event time falls within clinic working hours
|
|
8544
|
+
* @param clinicId - ID of the clinic
|
|
8545
|
+
* @param eventTime - Event time to validate
|
|
8546
|
+
* @throws Error if validation fails
|
|
8547
|
+
*/
|
|
8548
|
+
async validateClinicWorkingHours(clinicId, eventTime) {
|
|
8549
|
+
const startDate = eventTime.start.toDate();
|
|
8550
|
+
const workingHours = await this.getClinicWorkingHours(clinicId, startDate);
|
|
8551
|
+
if (workingHours.length === 0) {
|
|
8552
|
+
throw new Error("Clinic is not open on this day");
|
|
8553
|
+
}
|
|
8554
|
+
const startTime = startDate;
|
|
8555
|
+
const endTime = eventTime.end.toDate();
|
|
8556
|
+
const isWithinWorkingHours = workingHours.some((slot) => {
|
|
8557
|
+
return slot.start <= startTime && slot.end >= endTime && slot.isAvailable;
|
|
8558
|
+
});
|
|
8559
|
+
if (!isWithinWorkingHours) {
|
|
8560
|
+
throw new Error("Appointment time is outside clinic working hours");
|
|
8561
|
+
}
|
|
8562
|
+
}
|
|
8563
|
+
/**
|
|
8564
|
+
* Validates if the doctor is available during the event time
|
|
8565
|
+
* @param doctorId - ID of the doctor
|
|
8566
|
+
* @param eventTime - Event time to validate
|
|
8567
|
+
* @param clinicId - ID of the clinic where the appointment is being booked
|
|
8568
|
+
* @throws Error if validation fails
|
|
8569
|
+
*/
|
|
8570
|
+
async validateDoctorAvailability(doctorId, eventTime, clinicId) {
|
|
8571
|
+
var _a;
|
|
8572
|
+
const startDate = eventTime.start.toDate();
|
|
8573
|
+
const startTime = startDate;
|
|
8574
|
+
const endTime = eventTime.end.toDate();
|
|
8575
|
+
const practitionerRef = doc19(this.db, PRACTITIONERS_COLLECTION, doctorId);
|
|
8576
|
+
const practitionerDoc = await getDoc22(practitionerRef);
|
|
8577
|
+
if (!practitionerDoc.exists()) {
|
|
8578
|
+
throw new Error(`Doctor with ID ${doctorId} not found`);
|
|
8579
|
+
}
|
|
8580
|
+
const practitioner = practitionerDoc.data();
|
|
8581
|
+
if (!practitioner.clinics.includes(clinicId)) {
|
|
8582
|
+
throw new Error("Doctor does not work at this clinic");
|
|
8583
|
+
}
|
|
8584
|
+
const clinicWorkingHours = (_a = practitioner.clinicWorkingHours) == null ? void 0 : _a.find(
|
|
8585
|
+
(hours) => hours.clinicId === clinicId && hours.isActive
|
|
8586
|
+
);
|
|
8587
|
+
if (!clinicWorkingHours) {
|
|
8588
|
+
throw new Error("Doctor does not have working hours set for this clinic");
|
|
8589
|
+
}
|
|
8590
|
+
const dayOfWeek = startDate.getDay();
|
|
8591
|
+
const dayKey = [
|
|
8592
|
+
"sunday",
|
|
8593
|
+
"monday",
|
|
8594
|
+
"tuesday",
|
|
8595
|
+
"wednesday",
|
|
8596
|
+
"thursday",
|
|
8597
|
+
"friday",
|
|
8598
|
+
"saturday"
|
|
8599
|
+
][dayOfWeek];
|
|
8600
|
+
const daySchedule = clinicWorkingHours.workingHours[dayKey];
|
|
8601
|
+
if (!daySchedule) {
|
|
8602
|
+
throw new Error("Doctor is not working on this day at this clinic");
|
|
8603
|
+
}
|
|
8604
|
+
const [startHour, startMinute] = daySchedule.start.split(":").map(Number);
|
|
8605
|
+
const [endHour, endMinute] = daySchedule.end.split(":").map(Number);
|
|
8606
|
+
const scheduleStart = new Date(startDate);
|
|
8607
|
+
scheduleStart.setHours(startHour, startMinute, 0, 0);
|
|
8608
|
+
const scheduleEnd = new Date(startDate);
|
|
8609
|
+
scheduleEnd.setHours(endHour, endMinute, 0, 0);
|
|
8610
|
+
if (startTime < scheduleStart || endTime > scheduleEnd) {
|
|
8611
|
+
throw new Error(
|
|
8612
|
+
"Appointment time is outside doctor's working hours at this clinic"
|
|
8613
|
+
);
|
|
8614
|
+
}
|
|
8615
|
+
const appointments = await this.getDoctorAppointments(doctorId, startDate);
|
|
8616
|
+
const hasOverlap = appointments.some((appointment) => {
|
|
8617
|
+
const appointmentStart = appointment.eventTime.start.toDate();
|
|
8618
|
+
const appointmentEnd = appointment.eventTime.end.toDate();
|
|
8619
|
+
return startTime >= appointmentStart && startTime < appointmentEnd || endTime > appointmentStart && endTime <= appointmentEnd || startTime <= appointmentStart && endTime >= appointmentEnd;
|
|
8620
|
+
});
|
|
8621
|
+
if (hasOverlap) {
|
|
8622
|
+
throw new Error("Doctor has another appointment during this time");
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
/**
|
|
8626
|
+
* Updates appointment status
|
|
8627
|
+
* @param appointmentId - ID of the appointment
|
|
8628
|
+
* @param clinicId - ID of the clinic
|
|
8629
|
+
* @param status - New status
|
|
8630
|
+
* @returns Updated calendar event
|
|
8631
|
+
*/
|
|
8632
|
+
async updateAppointmentStatus(appointmentId, clinicId, status) {
|
|
8633
|
+
const appointmentRef = doc19(this.db, CALENDAR_COLLECTION, appointmentId);
|
|
8634
|
+
const appointmentDoc = await getDoc22(appointmentRef);
|
|
8635
|
+
if (!appointmentDoc.exists()) {
|
|
8636
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
8637
|
+
}
|
|
8638
|
+
const appointment = appointmentDoc.data();
|
|
8639
|
+
if (appointment.clinicBranchId !== clinicId) {
|
|
8640
|
+
throw new Error("Appointment does not belong to the specified clinic");
|
|
8641
|
+
}
|
|
8642
|
+
this.validateStatusTransition(appointment.status, status);
|
|
8643
|
+
const updateParams = {
|
|
8644
|
+
appointmentId,
|
|
8645
|
+
clinicId,
|
|
8646
|
+
doctorId: appointment.practitionerProfileId || "",
|
|
8647
|
+
patientId: appointment.patientProfileId || "",
|
|
8648
|
+
status
|
|
8649
|
+
};
|
|
8650
|
+
await this.validateUpdatePermissions(updateParams);
|
|
8651
|
+
return this.updateAppointment(updateParams);
|
|
8652
|
+
}
|
|
8653
|
+
/**
|
|
8654
|
+
* Validates status transition
|
|
8655
|
+
* @param currentStatus - Current status
|
|
8656
|
+
* @param newStatus - New status
|
|
8657
|
+
* @throws Error if transition is invalid
|
|
8658
|
+
*/
|
|
8659
|
+
validateStatusTransition(currentStatus, newStatus) {
|
|
8660
|
+
const validTransitions = {
|
|
8661
|
+
["pending" /* PENDING */]: [
|
|
8662
|
+
"confirmed" /* CONFIRMED */,
|
|
8663
|
+
"rejected" /* REJECTED */,
|
|
8664
|
+
"canceled" /* CANCELED */
|
|
8665
|
+
],
|
|
8666
|
+
["confirmed" /* CONFIRMED */]: [
|
|
8667
|
+
"canceled" /* CANCELED */,
|
|
8668
|
+
"completed" /* COMPLETED */,
|
|
8669
|
+
"rescheduled" /* RESCHEDULED */
|
|
8670
|
+
],
|
|
8671
|
+
["rejected" /* REJECTED */]: [],
|
|
8672
|
+
["canceled" /* CANCELED */]: [],
|
|
8673
|
+
["rescheduled" /* RESCHEDULED */]: [
|
|
8674
|
+
"confirmed" /* CONFIRMED */,
|
|
8675
|
+
"canceled" /* CANCELED */
|
|
8676
|
+
],
|
|
8677
|
+
["completed" /* COMPLETED */]: []
|
|
8678
|
+
};
|
|
8679
|
+
if (!validTransitions[currentStatus].includes(newStatus)) {
|
|
8680
|
+
throw new Error(
|
|
8681
|
+
`Invalid status transition from ${currentStatus} to ${newStatus}`
|
|
8682
|
+
);
|
|
8683
|
+
}
|
|
8684
|
+
}
|
|
8685
|
+
/**
|
|
8686
|
+
* Syncs appointment with external calendars based on entity type and status
|
|
8687
|
+
* @param appointment - Calendar event to sync
|
|
8688
|
+
*/
|
|
8689
|
+
async syncAppointmentWithExternalCalendars(appointment) {
|
|
8690
|
+
if (!appointment.practitionerProfileId || !appointment.patientProfileId) {
|
|
8691
|
+
return;
|
|
8692
|
+
}
|
|
8693
|
+
try {
|
|
8694
|
+
const [doctorCalendars, patientCalendars] = await Promise.all([
|
|
8695
|
+
this.syncedCalendarsService.getPractitionerSyncedCalendars(
|
|
8696
|
+
appointment.practitionerProfileId
|
|
8697
|
+
),
|
|
8698
|
+
this.syncedCalendarsService.getPatientSyncedCalendars(
|
|
8699
|
+
appointment.patientProfileId
|
|
8700
|
+
)
|
|
8701
|
+
]);
|
|
8702
|
+
const activeDoctorCalendars = doctorCalendars.filter(
|
|
8703
|
+
(cal) => cal.isActive
|
|
8704
|
+
);
|
|
8705
|
+
const activePatientCalendars = patientCalendars.filter(
|
|
8706
|
+
(cal) => cal.isActive
|
|
8707
|
+
);
|
|
8708
|
+
if (activeDoctorCalendars.length === 0 && activePatientCalendars.length === 0) {
|
|
8709
|
+
return;
|
|
8710
|
+
}
|
|
8711
|
+
if (appointment.syncStatus !== "internal" /* INTERNAL */) {
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
if (appointment.status === "confirmed" /* CONFIRMED */ && activeDoctorCalendars.length > 0) {
|
|
8715
|
+
await Promise.all(
|
|
8716
|
+
activeDoctorCalendars.map(
|
|
8717
|
+
(calendar) => this.syncEventToExternalCalendar(appointment, calendar, "doctor")
|
|
8718
|
+
)
|
|
8719
|
+
);
|
|
8720
|
+
}
|
|
8721
|
+
if (appointment.status !== "canceled" /* CANCELED */ && appointment.status !== "rejected" /* REJECTED */ && activePatientCalendars.length > 0) {
|
|
8722
|
+
await Promise.all(
|
|
8723
|
+
activePatientCalendars.map(
|
|
8724
|
+
(calendar) => this.syncEventToExternalCalendar(appointment, calendar, "patient")
|
|
8725
|
+
)
|
|
8726
|
+
);
|
|
8727
|
+
}
|
|
8728
|
+
} catch (error) {
|
|
8729
|
+
console.error("Error syncing with external calendars:", error);
|
|
8730
|
+
}
|
|
8731
|
+
}
|
|
8732
|
+
/**
|
|
8733
|
+
* Syncs a single event to an external calendar
|
|
8734
|
+
* @param appointment - Calendar event to sync
|
|
8735
|
+
* @param calendar - External calendar to sync with
|
|
8736
|
+
* @param entityType - Type of entity owning the calendar
|
|
8737
|
+
*/
|
|
8738
|
+
async syncEventToExternalCalendar(appointment, calendar, entityType) {
|
|
8739
|
+
var _a, _b, _c, _d, _e;
|
|
8740
|
+
try {
|
|
8741
|
+
const eventToSync = { ...appointment };
|
|
8742
|
+
let eventTitle = appointment.eventName;
|
|
8743
|
+
const clinicName = ((_a = appointment.clinicBranchInfo) == null ? void 0 : _a.name) || "Clinic";
|
|
8744
|
+
if (entityType === "patient") {
|
|
8745
|
+
eventTitle = `[${appointment.status}] ${eventTitle} @ ${clinicName}`;
|
|
8746
|
+
} else {
|
|
8747
|
+
eventTitle = `${eventTitle} - Patient: ${((_b = appointment.patientProfileInfo) == null ? void 0 : _b.fullName) || "Unknown"} @ ${clinicName}`;
|
|
8748
|
+
}
|
|
8749
|
+
eventToSync.eventName = eventTitle;
|
|
8750
|
+
const existingSyncId = (_d = (_c = appointment.syncedCalendarEventId) == null ? void 0 : _c.find(
|
|
8751
|
+
(sync) => sync.syncedCalendarProvider === calendar.provider
|
|
8752
|
+
)) == null ? void 0 : _d.eventId;
|
|
8753
|
+
if (calendar.provider === "google" /* GOOGLE */) {
|
|
8754
|
+
const result = await this.syncedCalendarsService.syncPractitionerEventsToGoogleCalendar(
|
|
8755
|
+
entityType === "doctor" ? appointment.practitionerProfileId : appointment.patientProfileId,
|
|
8756
|
+
calendar.id,
|
|
8757
|
+
[eventToSync],
|
|
8758
|
+
existingSyncId
|
|
8759
|
+
// Pass existing sync ID if we have one
|
|
8760
|
+
);
|
|
8761
|
+
if (result.success && ((_e = result.eventIds) == null ? void 0 : _e.length) && !existingSyncId) {
|
|
8762
|
+
const newSyncEvent = {
|
|
8763
|
+
eventId: result.eventIds[0],
|
|
8764
|
+
syncedCalendarProvider: calendar.provider,
|
|
8765
|
+
syncedAt: Timestamp23.now()
|
|
8766
|
+
};
|
|
8767
|
+
await this.updateEventWithSyncId(
|
|
8768
|
+
entityType === "doctor" ? appointment.practitionerProfileId : appointment.patientProfileId,
|
|
8769
|
+
entityType,
|
|
8770
|
+
appointment.id,
|
|
8771
|
+
newSyncEvent
|
|
8772
|
+
);
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8775
|
+
} catch (error) {
|
|
8776
|
+
console.error(`Error syncing with ${entityType}'s calendar:`, error);
|
|
8777
|
+
}
|
|
8778
|
+
}
|
|
8779
|
+
/**
|
|
8780
|
+
* Updates an event with a new sync ID
|
|
8781
|
+
* @param entityId - ID of the entity (doctor or patient)
|
|
8782
|
+
* @param entityType - Type of entity
|
|
8783
|
+
* @param eventId - ID of the event
|
|
8784
|
+
* @param syncEvent - Sync event information
|
|
8785
|
+
*/
|
|
8786
|
+
async updateEventWithSyncId(entityId, entityType, eventId, syncEvent) {
|
|
8787
|
+
try {
|
|
8788
|
+
const collectionPath = entityType === "doctor" ? `${PRACTITIONERS_COLLECTION}/${entityId}/${CALENDAR_COLLECTION}` : `${PATIENTS_COLLECTION}/${entityId}/${CALENDAR_COLLECTION}`;
|
|
8789
|
+
const eventRef = doc19(this.db, collectionPath, eventId);
|
|
8790
|
+
const eventDoc = await getDoc22(eventRef);
|
|
8791
|
+
if (eventDoc.exists()) {
|
|
8792
|
+
const event = eventDoc.data();
|
|
8793
|
+
const syncIds = [...event.syncedCalendarEventId || []];
|
|
8794
|
+
const existingSyncIndex = syncIds.findIndex(
|
|
8795
|
+
(sync) => sync.syncedCalendarProvider === syncEvent.syncedCalendarProvider
|
|
8796
|
+
);
|
|
8797
|
+
if (existingSyncIndex >= 0) {
|
|
8798
|
+
syncIds[existingSyncIndex] = syncEvent;
|
|
8799
|
+
} else {
|
|
8800
|
+
syncIds.push(syncEvent);
|
|
8801
|
+
}
|
|
8802
|
+
await updateDoc20(eventRef, {
|
|
8803
|
+
syncedCalendarEventId: syncIds,
|
|
8804
|
+
updatedAt: serverTimestamp18()
|
|
8805
|
+
});
|
|
8806
|
+
console.log(
|
|
8807
|
+
`Updated event ${eventId} with sync ID ${syncEvent.eventId}`
|
|
8808
|
+
);
|
|
8809
|
+
}
|
|
8810
|
+
} catch (error) {
|
|
8811
|
+
console.error("Error updating event with sync ID:", error);
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
/**
|
|
8815
|
+
* Validates update permissions and parameters
|
|
8816
|
+
* @param params - Update parameters to validate
|
|
8817
|
+
*/
|
|
8818
|
+
async validateUpdatePermissions(params) {
|
|
8819
|
+
await updateAppointmentSchema.parseAsync(params);
|
|
8820
|
+
}
|
|
8821
|
+
/**
|
|
8822
|
+
* Gets clinic working hours for a specific date
|
|
8823
|
+
* @param clinicId - ID of the clinic
|
|
8824
|
+
* @param date - Date to get working hours for
|
|
8825
|
+
* @returns Working hours for the clinic
|
|
8826
|
+
*/
|
|
8827
|
+
async getClinicWorkingHours(clinicId, date) {
|
|
8828
|
+
const clinicRef = doc19(this.db, CLINICS_COLLECTION, clinicId);
|
|
8829
|
+
const clinicDoc = await getDoc22(clinicRef);
|
|
8830
|
+
if (!clinicDoc.exists()) {
|
|
8831
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
8832
|
+
}
|
|
8833
|
+
const workingHours = [];
|
|
8834
|
+
const dayOfWeek = date.getDay();
|
|
8835
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
8836
|
+
return workingHours;
|
|
8837
|
+
}
|
|
8838
|
+
const workingDate = new Date(date);
|
|
8839
|
+
workingDate.setHours(9, 0, 0, 0);
|
|
8840
|
+
const startTime = new Date(workingDate);
|
|
8841
|
+
workingDate.setHours(17, 0, 0, 0);
|
|
8842
|
+
const endTime = new Date(workingDate);
|
|
8843
|
+
workingHours.push({
|
|
8844
|
+
start: startTime,
|
|
8845
|
+
end: endTime,
|
|
8846
|
+
isAvailable: true
|
|
8847
|
+
});
|
|
8848
|
+
return workingHours;
|
|
8849
|
+
}
|
|
8850
|
+
/**
|
|
8851
|
+
* Gets doctor's schedule for a specific date
|
|
8852
|
+
* @param doctorId - ID of the doctor
|
|
8853
|
+
* @param date - Date to get schedule for
|
|
8854
|
+
* @returns Doctor's schedule
|
|
8855
|
+
*/
|
|
8856
|
+
async getDoctorSchedule(doctorId, date) {
|
|
8857
|
+
const practitionerRef = doc19(this.db, PRACTITIONERS_COLLECTION, doctorId);
|
|
8858
|
+
const practitionerDoc = await getDoc22(practitionerRef);
|
|
8859
|
+
if (!practitionerDoc.exists()) {
|
|
8860
|
+
throw new Error(`Doctor with ID ${doctorId} not found`);
|
|
8861
|
+
}
|
|
8862
|
+
const schedule = [];
|
|
8863
|
+
const dayOfWeek = date.getDay();
|
|
8864
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
8865
|
+
return schedule;
|
|
8866
|
+
}
|
|
8867
|
+
const scheduleDate = new Date(date);
|
|
8868
|
+
scheduleDate.setHours(9, 0, 0, 0);
|
|
8869
|
+
const startTime = new Date(scheduleDate);
|
|
8870
|
+
scheduleDate.setHours(17, 0, 0, 0);
|
|
8871
|
+
const endTime = new Date(scheduleDate);
|
|
8872
|
+
schedule.push({
|
|
8873
|
+
start: startTime,
|
|
8874
|
+
end: endTime,
|
|
8875
|
+
isAvailable: true
|
|
8876
|
+
});
|
|
8877
|
+
return schedule;
|
|
8878
|
+
}
|
|
8879
|
+
/**
|
|
8880
|
+
* Gets doctor's appointments for a specific date
|
|
8881
|
+
* @param doctorId - ID of the doctor
|
|
8882
|
+
* @param date - Date to get appointments for
|
|
8883
|
+
* @returns Array of calendar events
|
|
8884
|
+
*/
|
|
8885
|
+
async getDoctorAppointments(doctorId, date) {
|
|
8886
|
+
const startOfDay = new Date(date);
|
|
8887
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
8888
|
+
const endOfDay = new Date(date);
|
|
8889
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
8890
|
+
const appointmentsRef = collection17(this.db, CALENDAR_COLLECTION);
|
|
8891
|
+
const q = query16(
|
|
8892
|
+
appointmentsRef,
|
|
8893
|
+
where16("practitionerProfileId", "==", doctorId),
|
|
8894
|
+
where16("eventTime.start", ">=", Timestamp23.fromDate(startOfDay)),
|
|
8895
|
+
where16("eventTime.start", "<=", Timestamp23.fromDate(endOfDay)),
|
|
8896
|
+
where16("status", "in", [
|
|
8897
|
+
"confirmed" /* CONFIRMED */,
|
|
8898
|
+
"pending" /* PENDING */
|
|
8899
|
+
])
|
|
8900
|
+
);
|
|
8901
|
+
const querySnapshot = await getDocs16(q);
|
|
8902
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
8903
|
+
}
|
|
8904
|
+
/**
|
|
8905
|
+
* Calculates available time slots based on working hours, schedule and existing appointments
|
|
8906
|
+
* @param workingHours - Clinic working hours
|
|
8907
|
+
* @param doctorSchedule - Doctor's schedule
|
|
8908
|
+
* @param existingAppointments - Existing appointments
|
|
8909
|
+
* @returns Array of available time slots
|
|
8910
|
+
*/
|
|
8911
|
+
calculateAvailableSlots(workingHours, doctorSchedule, existingAppointments) {
|
|
8912
|
+
const availableSlots = [];
|
|
8913
|
+
for (const workingHour of workingHours) {
|
|
8914
|
+
for (const scheduleSlot of doctorSchedule) {
|
|
8915
|
+
const overlapStart = new Date(
|
|
8916
|
+
Math.max(workingHour.start.getTime(), scheduleSlot.start.getTime())
|
|
8917
|
+
);
|
|
8918
|
+
const overlapEnd = new Date(
|
|
8919
|
+
Math.min(workingHour.end.getTime(), scheduleSlot.end.getTime())
|
|
8920
|
+
);
|
|
8921
|
+
if (overlapStart < overlapEnd && workingHour.isAvailable && scheduleSlot.isAvailable) {
|
|
8922
|
+
let slotStart = new Date(overlapStart);
|
|
8923
|
+
while (slotStart < overlapEnd) {
|
|
8924
|
+
const slotEnd = new Date(
|
|
8925
|
+
slotStart.getTime() + MIN_APPOINTMENT_DURATION2 * 60 * 1e3
|
|
8926
|
+
);
|
|
8927
|
+
const hasOverlap = existingAppointments.some((appointment) => {
|
|
8928
|
+
const appointmentStart = appointment.eventTime.start.toDate();
|
|
8929
|
+
const appointmentEnd = appointment.eventTime.end.toDate();
|
|
8930
|
+
return slotStart >= appointmentStart && slotStart < appointmentEnd || slotEnd > appointmentStart && slotEnd <= appointmentEnd;
|
|
8931
|
+
});
|
|
8932
|
+
if (!hasOverlap && slotEnd <= overlapEnd) {
|
|
8933
|
+
availableSlots.push({
|
|
8934
|
+
start: new Date(slotStart),
|
|
8935
|
+
end: new Date(slotEnd),
|
|
8936
|
+
isAvailable: true
|
|
8937
|
+
});
|
|
8938
|
+
}
|
|
8939
|
+
slotStart = new Date(
|
|
8940
|
+
slotStart.getTime() + MIN_APPOINTMENT_DURATION2 * 60 * 1e3
|
|
8941
|
+
);
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
}
|
|
8946
|
+
return availableSlots;
|
|
8947
|
+
}
|
|
8948
|
+
/**
|
|
8949
|
+
* Fetches and creates info cards for clinic, doctor, and patient profiles
|
|
8950
|
+
* @param clinicId - ID of the clinic
|
|
8951
|
+
* @param doctorId - ID of the doctor
|
|
8952
|
+
* @param patientId - ID of the patient
|
|
8953
|
+
* @returns Object containing info cards for all profiles
|
|
8954
|
+
*/
|
|
8955
|
+
async fetchProfileInfoCards(clinicId, doctorId, patientId) {
|
|
8956
|
+
var _a;
|
|
8957
|
+
try {
|
|
8958
|
+
const [clinicDoc, practitionerDoc, patientDoc, patientSensitiveInfoDoc] = await Promise.all([
|
|
8959
|
+
getDoc22(doc19(this.db, CLINICS_COLLECTION, clinicId)),
|
|
8960
|
+
getDoc22(doc19(this.db, PRACTITIONERS_COLLECTION, doctorId)),
|
|
8961
|
+
getDoc22(doc19(this.db, PATIENTS_COLLECTION, patientId)),
|
|
8962
|
+
getDoc22(
|
|
8963
|
+
doc19(
|
|
8964
|
+
this.db,
|
|
8965
|
+
PATIENTS_COLLECTION,
|
|
8966
|
+
patientId,
|
|
8967
|
+
PATIENT_SENSITIVE_INFO_COLLECTION,
|
|
8968
|
+
patientId
|
|
8969
|
+
)
|
|
8970
|
+
)
|
|
8971
|
+
]);
|
|
8972
|
+
const clinicInfo = clinicDoc.exists() ? {
|
|
8973
|
+
id: clinicDoc.id,
|
|
8974
|
+
featuredPhoto: clinicDoc.data().featuredPhoto || "",
|
|
8975
|
+
name: clinicDoc.data().name,
|
|
8976
|
+
description: clinicDoc.data().description || "",
|
|
8977
|
+
location: clinicDoc.data().location,
|
|
8978
|
+
contactInfo: clinicDoc.data().contactInfo
|
|
8979
|
+
} : null;
|
|
8980
|
+
const practitionerInfo = practitionerDoc.exists() ? {
|
|
8981
|
+
id: practitionerDoc.id,
|
|
8982
|
+
practitionerPhoto: practitionerDoc.data().basicInfo.profileImageUrl || null,
|
|
8983
|
+
name: `${practitionerDoc.data().basicInfo.firstName} ${practitionerDoc.data().basicInfo.lastName}`,
|
|
8984
|
+
email: practitionerDoc.data().basicInfo.email,
|
|
8985
|
+
phone: practitionerDoc.data().basicInfo.phoneNumber || null,
|
|
8986
|
+
certification: practitionerDoc.data().certification
|
|
8987
|
+
} : null;
|
|
8988
|
+
let patientInfo = null;
|
|
8989
|
+
if (patientSensitiveInfoDoc.exists()) {
|
|
8990
|
+
const sensitiveData = patientSensitiveInfoDoc.data();
|
|
8991
|
+
patientInfo = {
|
|
8992
|
+
id: patientId,
|
|
8993
|
+
fullName: `${sensitiveData.firstName} ${sensitiveData.lastName}`,
|
|
8994
|
+
email: sensitiveData.email || "",
|
|
8995
|
+
phone: sensitiveData.phoneNumber || null,
|
|
8996
|
+
dateOfBirth: sensitiveData.dateOfBirth || Timestamp23.now(),
|
|
8997
|
+
gender: sensitiveData.gender || "other" /* OTHER */
|
|
8998
|
+
};
|
|
8999
|
+
} else if (patientDoc.exists()) {
|
|
9000
|
+
patientInfo = {
|
|
9001
|
+
id: patientDoc.id,
|
|
9002
|
+
fullName: patientDoc.data().displayName,
|
|
9003
|
+
email: ((_a = patientDoc.data().contactInfo) == null ? void 0 : _a.email) || "",
|
|
9004
|
+
phone: patientDoc.data().phoneNumber || null,
|
|
9005
|
+
dateOfBirth: patientDoc.data().dateOfBirth || Timestamp23.now(),
|
|
9006
|
+
gender: patientDoc.data().gender || "other" /* OTHER */
|
|
9007
|
+
};
|
|
9008
|
+
}
|
|
9009
|
+
return {
|
|
9010
|
+
clinicInfo,
|
|
9011
|
+
practitionerInfo,
|
|
9012
|
+
patientInfo
|
|
9013
|
+
};
|
|
9014
|
+
} catch (error) {
|
|
9015
|
+
console.error("Error fetching profile info cards:", error);
|
|
9016
|
+
return {
|
|
9017
|
+
clinicInfo: null,
|
|
9018
|
+
practitionerInfo: null,
|
|
9019
|
+
patientInfo: null
|
|
9020
|
+
};
|
|
9021
|
+
}
|
|
9022
|
+
}
|
|
9023
|
+
// #endregion
|
|
9024
|
+
};
|
|
9025
|
+
|
|
9026
|
+
// src/validations/notification.schema.ts
|
|
9027
|
+
import { z as z18 } from "zod";
|
|
9028
|
+
var baseNotificationSchema = z18.object({
|
|
9029
|
+
id: z18.string().optional(),
|
|
9030
|
+
userId: z18.string(),
|
|
9031
|
+
notificationTime: z18.any(),
|
|
6394
9032
|
// Timestamp
|
|
6395
|
-
notificationType:
|
|
6396
|
-
notificationTokens:
|
|
6397
|
-
status:
|
|
6398
|
-
createdAt:
|
|
9033
|
+
notificationType: z18.nativeEnum(NotificationType),
|
|
9034
|
+
notificationTokens: z18.array(z18.string()),
|
|
9035
|
+
status: z18.nativeEnum(NotificationStatus),
|
|
9036
|
+
createdAt: z18.any().optional(),
|
|
6399
9037
|
// Timestamp
|
|
6400
|
-
updatedAt:
|
|
9038
|
+
updatedAt: z18.any().optional(),
|
|
6401
9039
|
// Timestamp
|
|
6402
|
-
title:
|
|
6403
|
-
body:
|
|
6404
|
-
isRead:
|
|
6405
|
-
userRole:
|
|
9040
|
+
title: z18.string(),
|
|
9041
|
+
body: z18.string(),
|
|
9042
|
+
isRead: z18.boolean(),
|
|
9043
|
+
userRole: z18.nativeEnum(UserRole)
|
|
6406
9044
|
});
|
|
6407
9045
|
var preRequirementNotificationSchema = baseNotificationSchema.extend({
|
|
6408
|
-
notificationType:
|
|
6409
|
-
treatmentId:
|
|
6410
|
-
requirements:
|
|
6411
|
-
deadline:
|
|
9046
|
+
notificationType: z18.literal("preRequirement" /* PRE_REQUIREMENT */),
|
|
9047
|
+
treatmentId: z18.string(),
|
|
9048
|
+
requirements: z18.array(z18.string()),
|
|
9049
|
+
deadline: z18.any()
|
|
6412
9050
|
// Timestamp
|
|
6413
9051
|
});
|
|
6414
9052
|
var postRequirementNotificationSchema = baseNotificationSchema.extend({
|
|
6415
|
-
notificationType:
|
|
6416
|
-
treatmentId:
|
|
6417
|
-
requirements:
|
|
6418
|
-
deadline:
|
|
9053
|
+
notificationType: z18.literal("postRequirement" /* POST_REQUIREMENT */),
|
|
9054
|
+
treatmentId: z18.string(),
|
|
9055
|
+
requirements: z18.array(z18.string()),
|
|
9056
|
+
deadline: z18.any()
|
|
6419
9057
|
// Timestamp
|
|
6420
9058
|
});
|
|
6421
9059
|
var appointmentReminderNotificationSchema = baseNotificationSchema.extend({
|
|
6422
|
-
notificationType:
|
|
6423
|
-
appointmentId:
|
|
6424
|
-
appointmentTime:
|
|
9060
|
+
notificationType: z18.literal("appointmentReminder" /* APPOINTMENT_REMINDER */),
|
|
9061
|
+
appointmentId: z18.string(),
|
|
9062
|
+
appointmentTime: z18.any(),
|
|
6425
9063
|
// Timestamp
|
|
6426
|
-
treatmentType:
|
|
6427
|
-
doctorName:
|
|
9064
|
+
treatmentType: z18.string(),
|
|
9065
|
+
doctorName: z18.string()
|
|
6428
9066
|
});
|
|
6429
9067
|
var appointmentNotificationSchema = baseNotificationSchema.extend({
|
|
6430
|
-
notificationType:
|
|
6431
|
-
appointmentId:
|
|
6432
|
-
appointmentStatus:
|
|
6433
|
-
previousStatus:
|
|
6434
|
-
reason:
|
|
9068
|
+
notificationType: z18.literal("appointmentNotification" /* APPOINTMENT_NOTIFICATION */),
|
|
9069
|
+
appointmentId: z18.string(),
|
|
9070
|
+
appointmentStatus: z18.string(),
|
|
9071
|
+
previousStatus: z18.string(),
|
|
9072
|
+
reason: z18.string().optional()
|
|
6435
9073
|
});
|
|
6436
|
-
var notificationSchema =
|
|
9074
|
+
var notificationSchema = z18.discriminatedUnion("notificationType", [
|
|
6437
9075
|
preRequirementNotificationSchema,
|
|
6438
9076
|
postRequirementNotificationSchema,
|
|
6439
9077
|
appointmentReminderNotificationSchema,
|
|
@@ -6445,9 +9083,14 @@ export {
|
|
|
6445
9083
|
AllergyType,
|
|
6446
9084
|
AuthService,
|
|
6447
9085
|
BlockingCondition,
|
|
9086
|
+
CALENDAR_COLLECTION,
|
|
6448
9087
|
CLINICS_COLLECTION,
|
|
6449
9088
|
CLINIC_ADMINS_COLLECTION,
|
|
6450
9089
|
CLINIC_GROUPS_COLLECTION,
|
|
9090
|
+
CalendarEventStatus,
|
|
9091
|
+
CalendarEventType,
|
|
9092
|
+
CalendarServiceV2,
|
|
9093
|
+
CalendarSyncStatus,
|
|
6451
9094
|
CertificationLevel,
|
|
6452
9095
|
CertificationSpecialty,
|
|
6453
9096
|
ClinicAdminService,
|
|
@@ -6488,7 +9131,10 @@ export {
|
|
|
6488
9131
|
PractitionerService,
|
|
6489
9132
|
PricingMeasure,
|
|
6490
9133
|
ProcedureFamily,
|
|
9134
|
+
SYNCED_CALENDARS_COLLECTION,
|
|
6491
9135
|
SubscriptionModel,
|
|
9136
|
+
SyncedCalendarProvider,
|
|
9137
|
+
SyncedCalendarsService,
|
|
6492
9138
|
TreatmentBenefit,
|
|
6493
9139
|
USER_ERRORS,
|
|
6494
9140
|
UserService,
|
|
@@ -6505,6 +9151,8 @@ export {
|
|
|
6505
9151
|
appointmentReminderNotificationSchema,
|
|
6506
9152
|
baseNotificationSchema,
|
|
6507
9153
|
blockingConditionSchema,
|
|
9154
|
+
calendarEventSchema,
|
|
9155
|
+
calendarEventTimeSchema,
|
|
6508
9156
|
clinicAdminOptionsSchema,
|
|
6509
9157
|
clinicAdminSchema,
|
|
6510
9158
|
clinicAdminSignupSchema,
|
|
@@ -6520,6 +9168,8 @@ export {
|
|
|
6520
9168
|
contactPersonSchema,
|
|
6521
9169
|
contraindicationSchema,
|
|
6522
9170
|
createAdminTokenSchema,
|
|
9171
|
+
createAppointmentSchema,
|
|
9172
|
+
createCalendarEventSchema,
|
|
6523
9173
|
createClinicAdminSchema,
|
|
6524
9174
|
createClinicGroupSchema,
|
|
6525
9175
|
createClinicSchema,
|
|
@@ -6551,21 +9201,30 @@ export {
|
|
|
6551
9201
|
patientDoctorSchema,
|
|
6552
9202
|
patientLocationInfoSchema,
|
|
6553
9203
|
patientMedicalInfoSchema,
|
|
9204
|
+
patientProfileInfoSchema,
|
|
6554
9205
|
patientProfileSchema,
|
|
6555
9206
|
patientSensitiveInfoSchema,
|
|
6556
9207
|
postRequirementNotificationSchema,
|
|
6557
9208
|
practitionerBasicInfoSchema,
|
|
6558
9209
|
practitionerCertificationSchema,
|
|
6559
9210
|
practitionerClinicProceduresSchema,
|
|
9211
|
+
practitionerClinicWorkingHoursSchema,
|
|
9212
|
+
practitionerProfileInfoSchema,
|
|
6560
9213
|
practitionerReviewSchema,
|
|
6561
9214
|
practitionerSchema,
|
|
6562
9215
|
practitionerWorkingHoursSchema,
|
|
6563
9216
|
preRequirementNotificationSchema,
|
|
9217
|
+
procedureCategorizationSchema,
|
|
9218
|
+
procedureInfoSchema,
|
|
6564
9219
|
reviewInfoSchema,
|
|
6565
9220
|
serviceInfoSchema,
|
|
9221
|
+
syncedCalendarEventSchema,
|
|
9222
|
+
timeSlotSchema2 as timeSlotSchema,
|
|
6566
9223
|
timestampSchema,
|
|
6567
9224
|
updateAllergySchema,
|
|
9225
|
+
updateAppointmentSchema,
|
|
6568
9226
|
updateBlockingConditionSchema,
|
|
9227
|
+
updateCalendarEventSchema,
|
|
6569
9228
|
updateClinicAdminSchema,
|
|
6570
9229
|
updateClinicGroupSchema,
|
|
6571
9230
|
updateClinicSchema,
|