@blackcode_sa/metaestetics-api 1.4.18 → 1.5.1
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 +7900 -4726
- package/dist/index.d.ts +7900 -4726
- package/dist/index.js +3029 -111
- package/dist/index.mjs +3059 -111
- package/package.json +4 -3
- package/src/index.ts +57 -2
- 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/practitioner/practitioner.service.ts +346 -1
- package/src/types/calendar/index.ts +187 -0
- package/src/types/calendar/synced-calendar.types.ts +66 -0
- package/src/types/clinic/index.ts +1 -12
- package/src/types/index.ts +4 -0
- package/src/types/practitioner/index.ts +81 -0
- package/src/types/profile/index.ts +39 -0
- package/src/validations/calendar.schema.ts +223 -0
- package/src/validations/practitioner.schema.ts +66 -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";
|
|
@@ -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);
|
|
@@ -3028,6 +3053,19 @@ import {
|
|
|
3028
3053
|
|
|
3029
3054
|
// src/types/practitioner/index.ts
|
|
3030
3055
|
var PRACTITIONERS_COLLECTION = "practitioners";
|
|
3056
|
+
var REGISTER_TOKENS_COLLECTION = "register_tokens";
|
|
3057
|
+
var PractitionerStatus = /* @__PURE__ */ ((PractitionerStatus2) => {
|
|
3058
|
+
PractitionerStatus2["DRAFT"] = "draft";
|
|
3059
|
+
PractitionerStatus2["ACTIVE"] = "active";
|
|
3060
|
+
return PractitionerStatus2;
|
|
3061
|
+
})(PractitionerStatus || {});
|
|
3062
|
+
var PractitionerTokenStatus = /* @__PURE__ */ ((PractitionerTokenStatus2) => {
|
|
3063
|
+
PractitionerTokenStatus2["ACTIVE"] = "active";
|
|
3064
|
+
PractitionerTokenStatus2["USED"] = "used";
|
|
3065
|
+
PractitionerTokenStatus2["EXPIRED"] = "expired";
|
|
3066
|
+
PractitionerTokenStatus2["REVOKED"] = "revoked";
|
|
3067
|
+
return PractitionerTokenStatus2;
|
|
3068
|
+
})(PractitionerTokenStatus || {});
|
|
3031
3069
|
|
|
3032
3070
|
// src/validations/practitioner.schema.ts
|
|
3033
3071
|
import { z as z10 } from "zod";
|
|
@@ -3096,6 +3134,21 @@ var practitionerWorkingHoursSchema = z10.object({
|
|
|
3096
3134
|
createdAt: z10.instanceof(Timestamp7),
|
|
3097
3135
|
updatedAt: z10.instanceof(Timestamp7)
|
|
3098
3136
|
});
|
|
3137
|
+
var practitionerClinicWorkingHoursSchema = z10.object({
|
|
3138
|
+
clinicId: z10.string().min(1),
|
|
3139
|
+
workingHours: z10.object({
|
|
3140
|
+
monday: timeSlotSchema,
|
|
3141
|
+
tuesday: timeSlotSchema,
|
|
3142
|
+
wednesday: timeSlotSchema,
|
|
3143
|
+
thursday: timeSlotSchema,
|
|
3144
|
+
friday: timeSlotSchema,
|
|
3145
|
+
saturday: timeSlotSchema,
|
|
3146
|
+
sunday: timeSlotSchema
|
|
3147
|
+
}),
|
|
3148
|
+
isActive: z10.boolean(),
|
|
3149
|
+
createdAt: z10.instanceof(Timestamp7),
|
|
3150
|
+
updatedAt: z10.instanceof(Timestamp7)
|
|
3151
|
+
});
|
|
3099
3152
|
var practitionerReviewSchema = z10.object({
|
|
3100
3153
|
id: z10.string().min(1),
|
|
3101
3154
|
practitionerId: z10.string().min(1),
|
|
@@ -3121,8 +3174,10 @@ var practitionerSchema = z10.object({
|
|
|
3121
3174
|
basicInfo: practitionerBasicInfoSchema,
|
|
3122
3175
|
certification: practitionerCertificationSchema,
|
|
3123
3176
|
clinics: z10.array(z10.string()),
|
|
3177
|
+
clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema),
|
|
3124
3178
|
isActive: z10.boolean(),
|
|
3125
3179
|
isVerified: z10.boolean(),
|
|
3180
|
+
status: z10.nativeEnum(PractitionerStatus),
|
|
3126
3181
|
createdAt: z10.instanceof(Timestamp7),
|
|
3127
3182
|
updatedAt: z10.instanceof(Timestamp7)
|
|
3128
3183
|
});
|
|
@@ -3131,8 +3186,37 @@ var createPractitionerSchema = z10.object({
|
|
|
3131
3186
|
basicInfo: practitionerBasicInfoSchema,
|
|
3132
3187
|
certification: practitionerCertificationSchema,
|
|
3133
3188
|
clinics: z10.array(z10.string()).optional(),
|
|
3189
|
+
clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
3134
3190
|
isActive: z10.boolean(),
|
|
3135
|
-
isVerified: z10.boolean()
|
|
3191
|
+
isVerified: z10.boolean(),
|
|
3192
|
+
status: z10.nativeEnum(PractitionerStatus).optional()
|
|
3193
|
+
});
|
|
3194
|
+
var createDraftPractitionerSchema = z10.object({
|
|
3195
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
3196
|
+
certification: practitionerCertificationSchema,
|
|
3197
|
+
clinics: z10.array(z10.string()).optional(),
|
|
3198
|
+
clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
3199
|
+
isActive: z10.boolean().optional().default(false),
|
|
3200
|
+
isVerified: z10.boolean().optional().default(false)
|
|
3201
|
+
});
|
|
3202
|
+
var practitionerTokenSchema = z10.object({
|
|
3203
|
+
id: z10.string().min(1),
|
|
3204
|
+
token: z10.string().min(6),
|
|
3205
|
+
practitionerId: z10.string().min(1),
|
|
3206
|
+
email: z10.string().email(),
|
|
3207
|
+
clinicId: z10.string().min(1),
|
|
3208
|
+
status: z10.nativeEnum(PractitionerTokenStatus),
|
|
3209
|
+
createdBy: z10.string().min(1),
|
|
3210
|
+
createdAt: z10.instanceof(Timestamp7),
|
|
3211
|
+
expiresAt: z10.instanceof(Timestamp7),
|
|
3212
|
+
usedBy: z10.string().optional(),
|
|
3213
|
+
usedAt: z10.instanceof(Timestamp7).optional()
|
|
3214
|
+
});
|
|
3215
|
+
var createPractitionerTokenSchema = z10.object({
|
|
3216
|
+
practitionerId: z10.string().min(1),
|
|
3217
|
+
email: z10.string().email(),
|
|
3218
|
+
clinicId: z10.string().min(1),
|
|
3219
|
+
expiresAt: z10.date().optional()
|
|
3136
3220
|
});
|
|
3137
3221
|
|
|
3138
3222
|
// src/services/practitioner/practitioner.service.ts
|
|
@@ -3180,8 +3264,10 @@ var PractitionerService = class extends BaseService {
|
|
|
3180
3264
|
basicInfo: validatedData.basicInfo,
|
|
3181
3265
|
certification: validatedData.certification,
|
|
3182
3266
|
clinics: validatedData.clinics || [],
|
|
3267
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
3183
3268
|
isActive: validatedData.isActive,
|
|
3184
3269
|
isVerified: validatedData.isVerified,
|
|
3270
|
+
status: validatedData.status || "active" /* ACTIVE */,
|
|
3185
3271
|
createdAt: serverTimestamp9(),
|
|
3186
3272
|
updatedAt: serverTimestamp9()
|
|
3187
3273
|
};
|
|
@@ -3206,6 +3292,200 @@ var PractitionerService = class extends BaseService {
|
|
|
3206
3292
|
throw error;
|
|
3207
3293
|
}
|
|
3208
3294
|
}
|
|
3295
|
+
/**
|
|
3296
|
+
* Kreira novi draft profil zdravstvenog radnika bez povezanog korisnika
|
|
3297
|
+
* Koristi se od strane administratora klinike za kreiranje profila i kasnije pozivanje
|
|
3298
|
+
* @param data Podaci za kreiranje draft profila
|
|
3299
|
+
* @param createdBy ID administratora koji kreira profil
|
|
3300
|
+
* @param clinicId ID klinike za koju se kreira profil
|
|
3301
|
+
* @returns Objekt koji sadrži kreirani draft profil i token za registraciju
|
|
3302
|
+
*/
|
|
3303
|
+
async createDraftPractitioner(data, createdBy, clinicId) {
|
|
3304
|
+
try {
|
|
3305
|
+
const validatedData = createDraftPractitionerSchema.parse(data);
|
|
3306
|
+
const clinic = await this.getClinicService().getClinic(clinicId);
|
|
3307
|
+
if (!clinic) {
|
|
3308
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
3309
|
+
}
|
|
3310
|
+
const clinics = data.clinics || [clinicId];
|
|
3311
|
+
if (data.clinics) {
|
|
3312
|
+
for (const cId of data.clinics) {
|
|
3313
|
+
if (cId !== clinicId) {
|
|
3314
|
+
const otherClinic = await this.getClinicService().getClinic(cId);
|
|
3315
|
+
if (!otherClinic) {
|
|
3316
|
+
throw new Error(`Clinic ${cId} not found`);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
const practitionerId = this.generateId();
|
|
3322
|
+
const practitionerData = {
|
|
3323
|
+
id: practitionerId,
|
|
3324
|
+
userRef: "",
|
|
3325
|
+
// Prazno - biće popunjeno kada korisnik kreira nalog
|
|
3326
|
+
basicInfo: validatedData.basicInfo,
|
|
3327
|
+
certification: validatedData.certification,
|
|
3328
|
+
clinics,
|
|
3329
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
3330
|
+
isActive: validatedData.isActive !== void 0 ? validatedData.isActive : false,
|
|
3331
|
+
isVerified: validatedData.isVerified !== void 0 ? validatedData.isVerified : false,
|
|
3332
|
+
status: "draft" /* DRAFT */,
|
|
3333
|
+
createdAt: serverTimestamp9(),
|
|
3334
|
+
updatedAt: serverTimestamp9()
|
|
3335
|
+
};
|
|
3336
|
+
practitionerSchema.parse({
|
|
3337
|
+
...practitionerData,
|
|
3338
|
+
userRef: "temp-for-validation",
|
|
3339
|
+
createdAt: Timestamp8.now(),
|
|
3340
|
+
updatedAt: Timestamp8.now()
|
|
3341
|
+
});
|
|
3342
|
+
await setDoc7(
|
|
3343
|
+
doc5(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
|
|
3344
|
+
practitionerData
|
|
3345
|
+
);
|
|
3346
|
+
const savedPractitioner = await this.getPractitioner(practitionerData.id);
|
|
3347
|
+
if (!savedPractitioner) {
|
|
3348
|
+
throw new Error("Failed to create draft practitioner profile");
|
|
3349
|
+
}
|
|
3350
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
3351
|
+
const expiration = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
3352
|
+
const token = {
|
|
3353
|
+
id: this.generateId(),
|
|
3354
|
+
token: tokenString,
|
|
3355
|
+
practitionerId,
|
|
3356
|
+
email: practitionerData.basicInfo.email,
|
|
3357
|
+
clinicId,
|
|
3358
|
+
status: "active" /* ACTIVE */,
|
|
3359
|
+
createdBy,
|
|
3360
|
+
createdAt: Timestamp8.now(),
|
|
3361
|
+
expiresAt: Timestamp8.fromDate(expiration)
|
|
3362
|
+
};
|
|
3363
|
+
practitionerTokenSchema.parse(token);
|
|
3364
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
3365
|
+
await setDoc7(doc5(this.db, tokenPath), token);
|
|
3366
|
+
return { practitioner: savedPractitioner, token };
|
|
3367
|
+
} catch (error) {
|
|
3368
|
+
if (error instanceof z11.ZodError) {
|
|
3369
|
+
throw new Error("Invalid practitioner data: " + error.message);
|
|
3370
|
+
}
|
|
3371
|
+
throw error;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
/**
|
|
3375
|
+
* Creates a token for inviting practitioner to claim their profile
|
|
3376
|
+
* @param data Data for creating token
|
|
3377
|
+
* @param createdBy ID of the user creating the token
|
|
3378
|
+
* @returns Created token
|
|
3379
|
+
*/
|
|
3380
|
+
async createPractitionerToken(data, createdBy) {
|
|
3381
|
+
try {
|
|
3382
|
+
const validatedData = createPractitionerTokenSchema.parse(data);
|
|
3383
|
+
const practitioner = await this.getPractitioner(
|
|
3384
|
+
validatedData.practitionerId
|
|
3385
|
+
);
|
|
3386
|
+
if (!practitioner) {
|
|
3387
|
+
throw new Error("Practitioner not found");
|
|
3388
|
+
}
|
|
3389
|
+
if (practitioner.status !== "draft" /* DRAFT */) {
|
|
3390
|
+
throw new Error(
|
|
3391
|
+
"Can only create tokens for practitioners in DRAFT status"
|
|
3392
|
+
);
|
|
3393
|
+
}
|
|
3394
|
+
const clinic = await this.getClinicService().getClinic(
|
|
3395
|
+
validatedData.clinicId
|
|
3396
|
+
);
|
|
3397
|
+
if (!clinic) {
|
|
3398
|
+
throw new Error(`Clinic ${validatedData.clinicId} not found`);
|
|
3399
|
+
}
|
|
3400
|
+
if (!practitioner.clinics.includes(validatedData.clinicId)) {
|
|
3401
|
+
throw new Error("Practitioner is not associated with this clinic");
|
|
3402
|
+
}
|
|
3403
|
+
const expiration = validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
3404
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
3405
|
+
const token = {
|
|
3406
|
+
id: this.generateId(),
|
|
3407
|
+
token: tokenString,
|
|
3408
|
+
practitionerId: validatedData.practitionerId,
|
|
3409
|
+
email: validatedData.email,
|
|
3410
|
+
clinicId: validatedData.clinicId,
|
|
3411
|
+
status: "active" /* ACTIVE */,
|
|
3412
|
+
createdBy,
|
|
3413
|
+
createdAt: Timestamp8.now(),
|
|
3414
|
+
expiresAt: Timestamp8.fromDate(expiration)
|
|
3415
|
+
};
|
|
3416
|
+
practitionerTokenSchema.parse(token);
|
|
3417
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
3418
|
+
await setDoc7(doc5(this.db, tokenPath), token);
|
|
3419
|
+
return token;
|
|
3420
|
+
} catch (error) {
|
|
3421
|
+
if (error instanceof z11.ZodError) {
|
|
3422
|
+
throw new Error("Invalid token data: " + error.message);
|
|
3423
|
+
}
|
|
3424
|
+
throw error;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
/**
|
|
3428
|
+
* Gets active tokens for a practitioner
|
|
3429
|
+
* @param practitionerId ID of the practitioner
|
|
3430
|
+
* @returns Array of active tokens
|
|
3431
|
+
*/
|
|
3432
|
+
async getPractitionerActiveTokens(practitionerId) {
|
|
3433
|
+
const tokensRef = collection3(
|
|
3434
|
+
this.db,
|
|
3435
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
3436
|
+
);
|
|
3437
|
+
const q = query3(
|
|
3438
|
+
tokensRef,
|
|
3439
|
+
where3("status", "==", "active" /* ACTIVE */),
|
|
3440
|
+
where3("expiresAt", ">", Timestamp8.now())
|
|
3441
|
+
);
|
|
3442
|
+
const querySnapshot = await getDocs3(q);
|
|
3443
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Gets a token by its string value and validates it
|
|
3447
|
+
* @param tokenString The token string to find
|
|
3448
|
+
* @returns The token if found and valid, null otherwise
|
|
3449
|
+
*/
|
|
3450
|
+
async validateToken(tokenString) {
|
|
3451
|
+
const practitionersRef = collection3(this.db, PRACTITIONERS_COLLECTION);
|
|
3452
|
+
const practitionersSnapshot = await getDocs3(practitionersRef);
|
|
3453
|
+
for (const practitionerDoc of practitionersSnapshot.docs) {
|
|
3454
|
+
const practitionerId = practitionerDoc.id;
|
|
3455
|
+
const tokensRef = collection3(
|
|
3456
|
+
this.db,
|
|
3457
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
3458
|
+
);
|
|
3459
|
+
const q = query3(
|
|
3460
|
+
tokensRef,
|
|
3461
|
+
where3("token", "==", tokenString),
|
|
3462
|
+
where3("status", "==", "active" /* ACTIVE */),
|
|
3463
|
+
where3("expiresAt", ">", Timestamp8.now())
|
|
3464
|
+
);
|
|
3465
|
+
const tokenSnapshot = await getDocs3(q);
|
|
3466
|
+
if (!tokenSnapshot.empty) {
|
|
3467
|
+
return tokenSnapshot.docs[0].data();
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
return null;
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Marks a token as used
|
|
3474
|
+
* @param tokenId ID of the token
|
|
3475
|
+
* @param practitionerId ID of the practitioner
|
|
3476
|
+
* @param userId ID of the user using the token
|
|
3477
|
+
*/
|
|
3478
|
+
async markTokenAsUsed(tokenId, practitionerId, userId) {
|
|
3479
|
+
const tokenRef = doc5(
|
|
3480
|
+
this.db,
|
|
3481
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
|
|
3482
|
+
);
|
|
3483
|
+
await updateDoc8(tokenRef, {
|
|
3484
|
+
status: "used" /* USED */,
|
|
3485
|
+
usedBy: userId,
|
|
3486
|
+
usedAt: Timestamp8.now()
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3209
3489
|
/**
|
|
3210
3490
|
* Dohvata zdravstvenog radnika po ID-u
|
|
3211
3491
|
*/
|
|
@@ -3239,10 +3519,23 @@ var PractitionerService = class extends BaseService {
|
|
|
3239
3519
|
const q = query3(
|
|
3240
3520
|
collection3(this.db, PRACTITIONERS_COLLECTION),
|
|
3241
3521
|
where3("clinics", "array-contains", clinicId),
|
|
3242
|
-
where3("isActive", "==", true)
|
|
3522
|
+
where3("isActive", "==", true),
|
|
3523
|
+
where3("status", "==", "active" /* ACTIVE */)
|
|
3524
|
+
);
|
|
3525
|
+
const querySnapshot = await getDocs3(q);
|
|
3526
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
3527
|
+
}
|
|
3528
|
+
/**
|
|
3529
|
+
* Dohvata sve draft zdravstvene radnike za određenu kliniku
|
|
3530
|
+
*/
|
|
3531
|
+
async getDraftPractitionersByClinic(clinicId) {
|
|
3532
|
+
const q = query3(
|
|
3533
|
+
collection3(this.db, PRACTITIONERS_COLLECTION),
|
|
3534
|
+
where3("clinics", "array-contains", clinicId),
|
|
3535
|
+
where3("status", "==", "draft" /* DRAFT */)
|
|
3243
3536
|
);
|
|
3244
3537
|
const querySnapshot = await getDocs3(q);
|
|
3245
|
-
return querySnapshot.docs.map((
|
|
3538
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
3246
3539
|
}
|
|
3247
3540
|
/**
|
|
3248
3541
|
* Ažurira profil zdravstvenog radnika
|
|
@@ -3348,6 +3641,35 @@ var PractitionerService = class extends BaseService {
|
|
|
3348
3641
|
}
|
|
3349
3642
|
await deleteDoc2(doc5(this.db, PRACTITIONERS_COLLECTION, practitionerId));
|
|
3350
3643
|
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Validates a registration token and claims the associated draft practitioner profile
|
|
3646
|
+
* @param tokenString The token provided by the practitioner
|
|
3647
|
+
* @param userId The ID of the user claiming the profile
|
|
3648
|
+
* @returns The claimed practitioner profile or null if token is invalid
|
|
3649
|
+
*/
|
|
3650
|
+
async validateTokenAndClaimProfile(tokenString, userId) {
|
|
3651
|
+
const token = await this.validateToken(tokenString);
|
|
3652
|
+
if (!token) {
|
|
3653
|
+
return null;
|
|
3654
|
+
}
|
|
3655
|
+
const practitioner = await this.getPractitioner(token.practitionerId);
|
|
3656
|
+
if (!practitioner) {
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
if (practitioner.status !== "draft" /* DRAFT */) {
|
|
3660
|
+
throw new Error("This practitioner profile has already been claimed");
|
|
3661
|
+
}
|
|
3662
|
+
const existingPractitioner = await this.getPractitionerByUserRef(userId);
|
|
3663
|
+
if (existingPractitioner) {
|
|
3664
|
+
throw new Error("User already has a practitioner profile");
|
|
3665
|
+
}
|
|
3666
|
+
const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
|
|
3667
|
+
userRef: userId,
|
|
3668
|
+
status: "active" /* ACTIVE */
|
|
3669
|
+
});
|
|
3670
|
+
await this.markTokenAsUsed(token.id, token.practitionerId, userId);
|
|
3671
|
+
return updatedPractitioner;
|
|
3672
|
+
}
|
|
3351
3673
|
};
|
|
3352
3674
|
|
|
3353
3675
|
// src/services/user.service.ts
|
|
@@ -3522,7 +3844,7 @@ var UserService = class extends BaseService {
|
|
|
3522
3844
|
];
|
|
3523
3845
|
const q = query4(collection4(this.db, USERS_COLLECTION), ...constraints);
|
|
3524
3846
|
const querySnapshot = await getDocs4(q);
|
|
3525
|
-
const users = querySnapshot.docs.map((
|
|
3847
|
+
const users = querySnapshot.docs.map((doc20) => doc20.data());
|
|
3526
3848
|
return Promise.all(users.map((userData) => userSchema.parse(userData)));
|
|
3527
3849
|
}
|
|
3528
3850
|
/**
|
|
@@ -3921,7 +4243,7 @@ async function getAllActiveGroups(db) {
|
|
|
3921
4243
|
where5("isActive", "==", true)
|
|
3922
4244
|
);
|
|
3923
4245
|
const querySnapshot = await getDocs5(q);
|
|
3924
|
-
return querySnapshot.docs.map((
|
|
4246
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
3925
4247
|
}
|
|
3926
4248
|
async function updateClinicGroup(db, groupId, data, app) {
|
|
3927
4249
|
console.log("[CLINIC_GROUP] Updating clinic group", { groupId });
|
|
@@ -4591,7 +4913,7 @@ async function getClinicsByGroup(db, groupId) {
|
|
|
4591
4913
|
where6("isActive", "==", true)
|
|
4592
4914
|
);
|
|
4593
4915
|
const querySnapshot = await getDocs6(q);
|
|
4594
|
-
return querySnapshot.docs.map((
|
|
4916
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
4595
4917
|
}
|
|
4596
4918
|
async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app) {
|
|
4597
4919
|
console.log("[CLINIC] Starting clinic update", { clinicId, adminId });
|
|
@@ -4803,7 +5125,7 @@ async function getClinicsByAdmin(db, adminId, options = {}, clinicAdminService,
|
|
|
4803
5125
|
}
|
|
4804
5126
|
const q = query6(collection6(db, CLINICS_COLLECTION), ...constraints);
|
|
4805
5127
|
const querySnapshot = await getDocs6(q);
|
|
4806
|
-
return querySnapshot.docs.map((
|
|
5128
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
4807
5129
|
}
|
|
4808
5130
|
async function getActiveClinicsByAdmin(db, adminId, clinicAdminService, clinicGroupService) {
|
|
4809
5131
|
return getClinicsByAdmin(
|
|
@@ -4956,8 +5278,8 @@ async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
|
4956
5278
|
}
|
|
4957
5279
|
const q = query7(collection8(db, CLINICS_COLLECTION), ...constraints);
|
|
4958
5280
|
const querySnapshot = await getDocs7(q);
|
|
4959
|
-
for (const
|
|
4960
|
-
const clinic =
|
|
5281
|
+
for (const doc20 of querySnapshot.docs) {
|
|
5282
|
+
const clinic = doc20.data();
|
|
4961
5283
|
const distance = distanceBetween(
|
|
4962
5284
|
[center.latitude, center.longitude],
|
|
4963
5285
|
[clinic.location.latitude, clinic.location.longitude]
|
|
@@ -5831,9 +6153,9 @@ var NotificationService = class extends BaseService {
|
|
|
5831
6153
|
orderBy("notificationTime", "desc")
|
|
5832
6154
|
);
|
|
5833
6155
|
const querySnapshot = await getDocs9(q);
|
|
5834
|
-
return querySnapshot.docs.map((
|
|
5835
|
-
id:
|
|
5836
|
-
...
|
|
6156
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
6157
|
+
id: doc20.id,
|
|
6158
|
+
...doc20.data()
|
|
5837
6159
|
}));
|
|
5838
6160
|
}
|
|
5839
6161
|
/**
|
|
@@ -5847,9 +6169,9 @@ var NotificationService = class extends BaseService {
|
|
|
5847
6169
|
orderBy("notificationTime", "desc")
|
|
5848
6170
|
);
|
|
5849
6171
|
const querySnapshot = await getDocs9(q);
|
|
5850
|
-
return querySnapshot.docs.map((
|
|
5851
|
-
id:
|
|
5852
|
-
...
|
|
6172
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
6173
|
+
id: doc20.id,
|
|
6174
|
+
...doc20.data()
|
|
5853
6175
|
}));
|
|
5854
6176
|
}
|
|
5855
6177
|
/**
|
|
@@ -5921,9 +6243,9 @@ var NotificationService = class extends BaseService {
|
|
|
5921
6243
|
orderBy("notificationTime", "desc")
|
|
5922
6244
|
);
|
|
5923
6245
|
const querySnapshot = await getDocs9(q);
|
|
5924
|
-
return querySnapshot.docs.map((
|
|
5925
|
-
id:
|
|
5926
|
-
...
|
|
6246
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
6247
|
+
id: doc20.id,
|
|
6248
|
+
...doc20.data()
|
|
5927
6249
|
}));
|
|
5928
6250
|
}
|
|
5929
6251
|
/**
|
|
@@ -5936,9 +6258,9 @@ var NotificationService = class extends BaseService {
|
|
|
5936
6258
|
orderBy("notificationTime", "desc")
|
|
5937
6259
|
);
|
|
5938
6260
|
const querySnapshot = await getDocs9(q);
|
|
5939
|
-
return querySnapshot.docs.map((
|
|
5940
|
-
id:
|
|
5941
|
-
...
|
|
6261
|
+
return querySnapshot.docs.map((doc20) => ({
|
|
6262
|
+
id: doc20.id,
|
|
6263
|
+
...doc20.data()
|
|
5942
6264
|
}));
|
|
5943
6265
|
}
|
|
5944
6266
|
};
|
|
@@ -6068,9 +6390,9 @@ var DocumentationTemplateService = class extends BaseService {
|
|
|
6068
6390
|
const querySnapshot = await getDocs10(q);
|
|
6069
6391
|
const templates = [];
|
|
6070
6392
|
let lastVisible = null;
|
|
6071
|
-
querySnapshot.forEach((
|
|
6072
|
-
templates.push(
|
|
6073
|
-
lastVisible =
|
|
6393
|
+
querySnapshot.forEach((doc20) => {
|
|
6394
|
+
templates.push(doc20.data());
|
|
6395
|
+
lastVisible = doc20;
|
|
6074
6396
|
});
|
|
6075
6397
|
return {
|
|
6076
6398
|
templates,
|
|
@@ -6098,9 +6420,9 @@ var DocumentationTemplateService = class extends BaseService {
|
|
|
6098
6420
|
const querySnapshot = await getDocs10(q);
|
|
6099
6421
|
const templates = [];
|
|
6100
6422
|
let lastVisible = null;
|
|
6101
|
-
querySnapshot.forEach((
|
|
6102
|
-
templates.push(
|
|
6103
|
-
lastVisible =
|
|
6423
|
+
querySnapshot.forEach((doc20) => {
|
|
6424
|
+
templates.push(doc20.data());
|
|
6425
|
+
lastVisible = doc20;
|
|
6104
6426
|
});
|
|
6105
6427
|
return {
|
|
6106
6428
|
templates,
|
|
@@ -6127,9 +6449,9 @@ var DocumentationTemplateService = class extends BaseService {
|
|
|
6127
6449
|
const querySnapshot = await getDocs10(q);
|
|
6128
6450
|
const templates = [];
|
|
6129
6451
|
let lastVisible = null;
|
|
6130
|
-
querySnapshot.forEach((
|
|
6131
|
-
templates.push(
|
|
6132
|
-
lastVisible =
|
|
6452
|
+
querySnapshot.forEach((doc20) => {
|
|
6453
|
+
templates.push(doc20.data());
|
|
6454
|
+
lastVisible = doc20;
|
|
6133
6455
|
});
|
|
6134
6456
|
return {
|
|
6135
6457
|
templates,
|
|
@@ -6254,9 +6576,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6254
6576
|
const querySnapshot = await getDocs11(q);
|
|
6255
6577
|
const documents = [];
|
|
6256
6578
|
let lastVisible = null;
|
|
6257
|
-
querySnapshot.forEach((
|
|
6258
|
-
documents.push(
|
|
6259
|
-
lastVisible =
|
|
6579
|
+
querySnapshot.forEach((doc20) => {
|
|
6580
|
+
documents.push(doc20.data());
|
|
6581
|
+
lastVisible = doc20;
|
|
6260
6582
|
});
|
|
6261
6583
|
return {
|
|
6262
6584
|
documents,
|
|
@@ -6283,9 +6605,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6283
6605
|
const querySnapshot = await getDocs11(q);
|
|
6284
6606
|
const documents = [];
|
|
6285
6607
|
let lastVisible = null;
|
|
6286
|
-
querySnapshot.forEach((
|
|
6287
|
-
documents.push(
|
|
6288
|
-
lastVisible =
|
|
6608
|
+
querySnapshot.forEach((doc20) => {
|
|
6609
|
+
documents.push(doc20.data());
|
|
6610
|
+
lastVisible = doc20;
|
|
6289
6611
|
});
|
|
6290
6612
|
return {
|
|
6291
6613
|
documents,
|
|
@@ -6312,9 +6634,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6312
6634
|
const querySnapshot = await getDocs11(q);
|
|
6313
6635
|
const documents = [];
|
|
6314
6636
|
let lastVisible = null;
|
|
6315
|
-
querySnapshot.forEach((
|
|
6316
|
-
documents.push(
|
|
6317
|
-
lastVisible =
|
|
6637
|
+
querySnapshot.forEach((doc20) => {
|
|
6638
|
+
documents.push(doc20.data());
|
|
6639
|
+
lastVisible = doc20;
|
|
6318
6640
|
});
|
|
6319
6641
|
return {
|
|
6320
6642
|
documents,
|
|
@@ -6341,9 +6663,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6341
6663
|
const querySnapshot = await getDocs11(q);
|
|
6342
6664
|
const documents = [];
|
|
6343
6665
|
let lastVisible = null;
|
|
6344
|
-
querySnapshot.forEach((
|
|
6345
|
-
documents.push(
|
|
6346
|
-
lastVisible =
|
|
6666
|
+
querySnapshot.forEach((doc20) => {
|
|
6667
|
+
documents.push(doc20.data());
|
|
6668
|
+
lastVisible = doc20;
|
|
6347
6669
|
});
|
|
6348
6670
|
return {
|
|
6349
6671
|
documents,
|
|
@@ -6370,9 +6692,9 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6370
6692
|
const querySnapshot = await getDocs11(q);
|
|
6371
6693
|
const documents = [];
|
|
6372
6694
|
let lastVisible = null;
|
|
6373
|
-
querySnapshot.forEach((
|
|
6374
|
-
documents.push(
|
|
6375
|
-
lastVisible =
|
|
6695
|
+
querySnapshot.forEach((doc20) => {
|
|
6696
|
+
documents.push(doc20.data());
|
|
6697
|
+
lastVisible = doc20;
|
|
6376
6698
|
});
|
|
6377
6699
|
return {
|
|
6378
6700
|
documents,
|
|
@@ -6381,55 +6703,2654 @@ var FilledDocumentService = class extends BaseService {
|
|
|
6381
6703
|
}
|
|
6382
6704
|
};
|
|
6383
6705
|
|
|
6384
|
-
// src/
|
|
6706
|
+
// src/services/calendar/calendar-refactored.service.ts
|
|
6707
|
+
import { Timestamp as Timestamp23, serverTimestamp as serverTimestamp18 } from "firebase/firestore";
|
|
6708
|
+
|
|
6709
|
+
// src/types/calendar/synced-calendar.types.ts
|
|
6710
|
+
var SyncedCalendarProvider = /* @__PURE__ */ ((SyncedCalendarProvider3) => {
|
|
6711
|
+
SyncedCalendarProvider3["GOOGLE"] = "google";
|
|
6712
|
+
SyncedCalendarProvider3["OUTLOOK"] = "outlook";
|
|
6713
|
+
SyncedCalendarProvider3["APPLE"] = "apple";
|
|
6714
|
+
return SyncedCalendarProvider3;
|
|
6715
|
+
})(SyncedCalendarProvider || {});
|
|
6716
|
+
var SYNCED_CALENDARS_COLLECTION = "syncedCalendars";
|
|
6717
|
+
|
|
6718
|
+
// src/services/calendar/calendar-refactored.service.ts
|
|
6719
|
+
import {
|
|
6720
|
+
doc as doc19,
|
|
6721
|
+
getDoc as getDoc22,
|
|
6722
|
+
collection as collection17,
|
|
6723
|
+
query as query16,
|
|
6724
|
+
where as where16,
|
|
6725
|
+
getDocs as getDocs16,
|
|
6726
|
+
setDoc as setDoc19,
|
|
6727
|
+
updateDoc as updateDoc20
|
|
6728
|
+
} from "firebase/firestore";
|
|
6729
|
+
|
|
6730
|
+
// src/validations/calendar.schema.ts
|
|
6731
|
+
import { z as z17 } from "zod";
|
|
6732
|
+
import { Timestamp as Timestamp17 } from "firebase/firestore";
|
|
6733
|
+
|
|
6734
|
+
// src/validations/profile-info.schema.ts
|
|
6385
6735
|
import { z as z16 } from "zod";
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6736
|
+
import { Timestamp as Timestamp16 } from "firebase/firestore";
|
|
6737
|
+
var clinicInfoSchema2 = z16.object({
|
|
6738
|
+
id: z16.string(),
|
|
6739
|
+
featuredPhoto: z16.string(),
|
|
6740
|
+
name: z16.string(),
|
|
6741
|
+
description: z16.string(),
|
|
6742
|
+
location: clinicLocationSchema,
|
|
6743
|
+
contactInfo: clinicContactInfoSchema
|
|
6744
|
+
});
|
|
6745
|
+
var practitionerProfileInfoSchema = z16.object({
|
|
6746
|
+
id: z16.string(),
|
|
6747
|
+
practitionerPhoto: z16.string().nullable(),
|
|
6748
|
+
name: z16.string(),
|
|
6749
|
+
email: z16.string().email(),
|
|
6750
|
+
phone: z16.string().nullable(),
|
|
6751
|
+
certification: practitionerCertificationSchema
|
|
6752
|
+
});
|
|
6753
|
+
var patientProfileInfoSchema = z16.object({
|
|
6754
|
+
id: z16.string(),
|
|
6755
|
+
fullName: z16.string(),
|
|
6756
|
+
email: z16.string().email(),
|
|
6757
|
+
phone: z16.string().nullable(),
|
|
6758
|
+
dateOfBirth: z16.instanceof(Timestamp16),
|
|
6759
|
+
gender: z16.nativeEnum(Gender)
|
|
6760
|
+
});
|
|
6761
|
+
|
|
6762
|
+
// src/validations/calendar.schema.ts
|
|
6763
|
+
var MIN_APPOINTMENT_DURATION = 15;
|
|
6764
|
+
var calendarEventTimeSchema = z17.object({
|
|
6765
|
+
start: z17.instanceof(Date).or(z17.instanceof(Timestamp17)),
|
|
6766
|
+
end: z17.instanceof(Date).or(z17.instanceof(Timestamp17))
|
|
6767
|
+
}).refine(
|
|
6768
|
+
(data) => {
|
|
6769
|
+
const startDate = data.start instanceof Timestamp17 ? data.start.toDate() : data.start;
|
|
6770
|
+
const endDate = data.end instanceof Timestamp17 ? data.end.toDate() : data.end;
|
|
6771
|
+
return startDate < endDate;
|
|
6772
|
+
},
|
|
6773
|
+
{
|
|
6774
|
+
message: "End time must be after start time",
|
|
6775
|
+
path: ["end"]
|
|
6776
|
+
}
|
|
6777
|
+
).refine(
|
|
6778
|
+
(data) => {
|
|
6779
|
+
const startDate = data.start instanceof Timestamp17 ? data.start.toDate() : data.start;
|
|
6780
|
+
return startDate > /* @__PURE__ */ new Date();
|
|
6781
|
+
},
|
|
6782
|
+
{
|
|
6783
|
+
message: "Appointment must be scheduled in the future",
|
|
6784
|
+
path: ["start"]
|
|
6785
|
+
}
|
|
6786
|
+
);
|
|
6787
|
+
var timeSlotSchema2 = z17.object({
|
|
6788
|
+
start: z17.date(),
|
|
6789
|
+
end: z17.date(),
|
|
6790
|
+
isAvailable: z17.boolean()
|
|
6791
|
+
}).refine((data) => data.start < data.end, {
|
|
6792
|
+
message: "End time must be after start time",
|
|
6793
|
+
path: ["end"]
|
|
6794
|
+
});
|
|
6795
|
+
var syncedCalendarEventSchema = z17.object({
|
|
6796
|
+
eventId: z17.string(),
|
|
6797
|
+
syncedCalendarProvider: z17.nativeEnum(SyncedCalendarProvider),
|
|
6798
|
+
syncedAt: z17.instanceof(Date).or(z17.instanceof(Timestamp17))
|
|
6799
|
+
});
|
|
6800
|
+
var procedureInfoSchema = z17.object({
|
|
6801
|
+
name: z17.string(),
|
|
6802
|
+
description: z17.string(),
|
|
6803
|
+
duration: z17.number().min(MIN_APPOINTMENT_DURATION),
|
|
6804
|
+
price: z17.number().min(0),
|
|
6805
|
+
currency: z17.nativeEnum(Currency)
|
|
6806
|
+
});
|
|
6807
|
+
var procedureCategorizationSchema = z17.object({
|
|
6808
|
+
procedureFamily: z17.string(),
|
|
6809
|
+
// Replace with proper enum when available
|
|
6810
|
+
procedureCategory: z17.string(),
|
|
6811
|
+
// Replace with proper enum when available
|
|
6812
|
+
procedureSubcategory: z17.string(),
|
|
6813
|
+
// Replace with proper enum when available
|
|
6814
|
+
procedureTechnology: z17.string(),
|
|
6815
|
+
// Replace with proper enum when available
|
|
6816
|
+
procedureProduct: z17.string()
|
|
6817
|
+
// Replace with proper enum when available
|
|
6818
|
+
});
|
|
6819
|
+
var createAppointmentSchema = z17.object({
|
|
6820
|
+
clinicId: z17.string().min(1, "Clinic ID is required"),
|
|
6821
|
+
doctorId: z17.string().min(1, "Doctor ID is required"),
|
|
6822
|
+
patientId: z17.string().min(1, "Patient ID is required"),
|
|
6823
|
+
procedureId: z17.string().min(1, "Procedure ID is required"),
|
|
6824
|
+
eventLocation: clinicLocationSchema,
|
|
6825
|
+
eventTime: calendarEventTimeSchema,
|
|
6826
|
+
description: z17.string().optional()
|
|
6827
|
+
}).refine(
|
|
6828
|
+
(data) => {
|
|
6829
|
+
return true;
|
|
6830
|
+
},
|
|
6831
|
+
{
|
|
6832
|
+
message: "Invalid appointment parameters"
|
|
6833
|
+
}
|
|
6834
|
+
);
|
|
6835
|
+
var updateAppointmentSchema = z17.object({
|
|
6836
|
+
appointmentId: z17.string().min(1, "Appointment ID is required"),
|
|
6837
|
+
clinicId: z17.string().min(1, "Clinic ID is required"),
|
|
6838
|
+
doctorId: z17.string().min(1, "Doctor ID is required"),
|
|
6839
|
+
patientId: z17.string().min(1, "Patient ID is required"),
|
|
6840
|
+
eventTime: calendarEventTimeSchema.optional(),
|
|
6841
|
+
description: z17.string().optional(),
|
|
6842
|
+
status: z17.nativeEnum(CalendarEventStatus).optional()
|
|
6843
|
+
});
|
|
6844
|
+
var createCalendarEventSchema = z17.object({
|
|
6845
|
+
id: z17.string(),
|
|
6846
|
+
clinicBranchId: z17.string().nullable().optional(),
|
|
6847
|
+
clinicBranchInfo: z17.any().nullable().optional(),
|
|
6848
|
+
practitionerProfileId: z17.string().nullable().optional(),
|
|
6849
|
+
practitionerProfileInfo: practitionerProfileInfoSchema.nullable().optional(),
|
|
6850
|
+
patientProfileId: z17.string().nullable().optional(),
|
|
6851
|
+
patientProfileInfo: patientProfileInfoSchema.nullable().optional(),
|
|
6852
|
+
procedureId: z17.string().nullable().optional(),
|
|
6853
|
+
appointmentId: z17.string().nullable().optional(),
|
|
6854
|
+
syncedCalendarEventId: z17.array(syncedCalendarEventSchema).nullable().optional(),
|
|
6855
|
+
eventName: z17.string().min(1, "Event name is required"),
|
|
6856
|
+
eventLocation: clinicLocationSchema.optional(),
|
|
6857
|
+
eventTime: calendarEventTimeSchema,
|
|
6858
|
+
description: z17.string().optional(),
|
|
6859
|
+
status: z17.nativeEnum(CalendarEventStatus),
|
|
6860
|
+
syncStatus: z17.nativeEnum(CalendarSyncStatus),
|
|
6861
|
+
eventType: z17.nativeEnum(CalendarEventType),
|
|
6862
|
+
createdAt: z17.any(),
|
|
6863
|
+
// FieldValue for server timestamp
|
|
6864
|
+
updatedAt: z17.any()
|
|
6865
|
+
// FieldValue for server timestamp
|
|
6866
|
+
});
|
|
6867
|
+
var updateCalendarEventSchema = z17.object({
|
|
6868
|
+
syncedCalendarEventId: z17.array(syncedCalendarEventSchema).nullable().optional(),
|
|
6869
|
+
appointmentId: z17.string().nullable().optional(),
|
|
6870
|
+
eventName: z17.string().optional(),
|
|
6871
|
+
eventTime: calendarEventTimeSchema.optional(),
|
|
6872
|
+
description: z17.string().optional(),
|
|
6873
|
+
status: z17.nativeEnum(CalendarEventStatus).optional(),
|
|
6874
|
+
syncStatus: z17.nativeEnum(CalendarSyncStatus).optional(),
|
|
6875
|
+
eventType: z17.nativeEnum(CalendarEventType).optional(),
|
|
6876
|
+
updatedAt: z17.any()
|
|
6877
|
+
// FieldValue for server timestamp
|
|
6878
|
+
});
|
|
6879
|
+
var calendarEventSchema = z17.object({
|
|
6880
|
+
id: z17.string(),
|
|
6881
|
+
clinicBranchId: z17.string().nullable().optional(),
|
|
6882
|
+
clinicBranchInfo: z17.any().nullable().optional(),
|
|
6883
|
+
// Will be replaced with proper clinic info schema
|
|
6884
|
+
practitionerProfileId: z17.string().nullable().optional(),
|
|
6885
|
+
practitionerProfileInfo: practitionerProfileInfoSchema.nullable().optional(),
|
|
6886
|
+
patientProfileId: z17.string().nullable().optional(),
|
|
6887
|
+
patientProfileInfo: patientProfileInfoSchema.nullable().optional(),
|
|
6888
|
+
procedureId: z17.string().nullable().optional(),
|
|
6889
|
+
procedureInfo: procedureInfoSchema.nullable().optional(),
|
|
6890
|
+
procedureCategorization: procedureCategorizationSchema.nullable().optional(),
|
|
6891
|
+
appointmentId: z17.string().nullable().optional(),
|
|
6892
|
+
syncedCalendarEventId: z17.array(syncedCalendarEventSchema).nullable().optional(),
|
|
6893
|
+
eventName: z17.string(),
|
|
6894
|
+
eventLocation: clinicLocationSchema.optional(),
|
|
6895
|
+
eventTime: calendarEventTimeSchema,
|
|
6896
|
+
description: z17.string().optional(),
|
|
6897
|
+
status: z17.nativeEnum(CalendarEventStatus),
|
|
6898
|
+
syncStatus: z17.nativeEnum(CalendarSyncStatus),
|
|
6899
|
+
eventType: z17.nativeEnum(CalendarEventType),
|
|
6900
|
+
createdAt: z17.instanceof(Date).or(z17.instanceof(Timestamp17)),
|
|
6901
|
+
updatedAt: z17.instanceof(Date).or(z17.instanceof(Timestamp17))
|
|
6902
|
+
});
|
|
6903
|
+
|
|
6904
|
+
// src/services/calendar/utils/clinic.utils.ts
|
|
6905
|
+
import {
|
|
6906
|
+
collection as collection13,
|
|
6907
|
+
doc as doc15,
|
|
6908
|
+
getDoc as getDoc18,
|
|
6909
|
+
getDocs as getDocs12,
|
|
6910
|
+
setDoc as setDoc15,
|
|
6911
|
+
updateDoc as updateDoc16,
|
|
6912
|
+
deleteDoc as deleteDoc8,
|
|
6913
|
+
query as query12,
|
|
6914
|
+
where as where12,
|
|
6915
|
+
orderBy as orderBy4,
|
|
6916
|
+
Timestamp as Timestamp18,
|
|
6917
|
+
serverTimestamp as serverTimestamp14
|
|
6918
|
+
} from "firebase/firestore";
|
|
6919
|
+
|
|
6920
|
+
// src/services/calendar/utils/docs.utils.ts
|
|
6921
|
+
import { doc as doc14 } from "firebase/firestore";
|
|
6922
|
+
function getPractitionerCalendarEventDocRef(db, practitionerId, eventId) {
|
|
6923
|
+
return doc14(
|
|
6924
|
+
db,
|
|
6925
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${CALENDAR_COLLECTION}/${eventId}`
|
|
6926
|
+
);
|
|
6927
|
+
}
|
|
6928
|
+
function getPatientCalendarEventDocRef(db, patientId, eventId) {
|
|
6929
|
+
return doc14(
|
|
6930
|
+
db,
|
|
6931
|
+
`${PATIENTS_COLLECTION}/${patientId}/${CALENDAR_COLLECTION}/${eventId}`
|
|
6932
|
+
);
|
|
6933
|
+
}
|
|
6934
|
+
function getClinicCalendarEventDocRef(db, clinicId, eventId) {
|
|
6935
|
+
return doc14(
|
|
6936
|
+
db,
|
|
6937
|
+
`${CLINICS_COLLECTION}/${clinicId}/${CALENDAR_COLLECTION}/${eventId}`
|
|
6938
|
+
);
|
|
6939
|
+
}
|
|
6940
|
+
function getPractitionerSyncedCalendarDocRef(db, practitionerId, syncedCalendarId) {
|
|
6941
|
+
return doc14(
|
|
6942
|
+
db,
|
|
6943
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/syncedCalendars/${syncedCalendarId}`
|
|
6944
|
+
);
|
|
6945
|
+
}
|
|
6946
|
+
function getPatientSyncedCalendarDocRef(db, patientId, syncedCalendarId) {
|
|
6947
|
+
return doc14(
|
|
6948
|
+
db,
|
|
6949
|
+
`${PATIENTS_COLLECTION}/${patientId}/syncedCalendars/${syncedCalendarId}`
|
|
6950
|
+
);
|
|
6951
|
+
}
|
|
6952
|
+
function getClinicSyncedCalendarDocRef(db, clinicId, syncedCalendarId) {
|
|
6953
|
+
return doc14(
|
|
6954
|
+
db,
|
|
6955
|
+
`${CLINICS_COLLECTION}/${clinicId}/syncedCalendars/${syncedCalendarId}`
|
|
6956
|
+
);
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6959
|
+
// src/services/calendar/utils/clinic.utils.ts
|
|
6960
|
+
async function createClinicCalendarEventUtil(db, clinicId, eventData, generateId2) {
|
|
6961
|
+
const eventId = generateId2();
|
|
6962
|
+
const eventRef = getClinicCalendarEventDocRef(db, clinicId, eventId);
|
|
6963
|
+
const newEvent = {
|
|
6964
|
+
id: eventId,
|
|
6965
|
+
...eventData,
|
|
6966
|
+
createdAt: serverTimestamp14(),
|
|
6967
|
+
updatedAt: serverTimestamp14()
|
|
6968
|
+
};
|
|
6969
|
+
await setDoc15(eventRef, newEvent);
|
|
6970
|
+
return {
|
|
6971
|
+
...newEvent,
|
|
6972
|
+
createdAt: Timestamp18.now(),
|
|
6973
|
+
updatedAt: Timestamp18.now()
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
async function updateClinicCalendarEventUtil(db, clinicId, eventId, updateData) {
|
|
6977
|
+
const eventRef = getClinicCalendarEventDocRef(db, clinicId, eventId);
|
|
6978
|
+
const updates = {
|
|
6979
|
+
...updateData,
|
|
6980
|
+
updatedAt: serverTimestamp14()
|
|
6981
|
+
};
|
|
6982
|
+
await updateDoc16(eventRef, updates);
|
|
6983
|
+
const updatedDoc = await getDoc18(eventRef);
|
|
6984
|
+
if (!updatedDoc.exists()) {
|
|
6985
|
+
throw new Error("Event not found after update");
|
|
6986
|
+
}
|
|
6987
|
+
return updatedDoc.data();
|
|
6988
|
+
}
|
|
6989
|
+
async function checkAutoConfirmAppointmentsUtil(db, clinicId) {
|
|
6990
|
+
const clinicDoc = await getDoc18(doc15(db, `clinics/${clinicId}`));
|
|
6991
|
+
if (!clinicDoc.exists()) {
|
|
6992
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
6993
|
+
}
|
|
6994
|
+
const clinicData = clinicDoc.data();
|
|
6995
|
+
const clinicGroupId = clinicData.clinicGroupId;
|
|
6996
|
+
if (!clinicGroupId) {
|
|
6997
|
+
return false;
|
|
6998
|
+
}
|
|
6999
|
+
const clinicGroupDoc = await getDoc18(
|
|
7000
|
+
doc15(db, `${CLINIC_GROUPS_COLLECTION}/${clinicGroupId}`)
|
|
7001
|
+
);
|
|
7002
|
+
if (!clinicGroupDoc.exists()) {
|
|
7003
|
+
return false;
|
|
7004
|
+
}
|
|
7005
|
+
const clinicGroupData = clinicGroupDoc.data();
|
|
7006
|
+
return !!clinicGroupData.autoConfirmAppointments;
|
|
7007
|
+
}
|
|
7008
|
+
|
|
7009
|
+
// src/services/calendar/utils/patient.utils.ts
|
|
7010
|
+
import {
|
|
7011
|
+
collection as collection14,
|
|
7012
|
+
getDoc as getDoc19,
|
|
7013
|
+
getDocs as getDocs13,
|
|
7014
|
+
setDoc as setDoc16,
|
|
7015
|
+
updateDoc as updateDoc17,
|
|
7016
|
+
deleteDoc as deleteDoc9,
|
|
7017
|
+
query as query13,
|
|
7018
|
+
where as where13,
|
|
7019
|
+
orderBy as orderBy5,
|
|
7020
|
+
Timestamp as Timestamp19,
|
|
7021
|
+
serverTimestamp as serverTimestamp15
|
|
7022
|
+
} from "firebase/firestore";
|
|
7023
|
+
async function createPatientCalendarEventUtil(db, patientId, eventData, generateId2) {
|
|
7024
|
+
const eventId = generateId2();
|
|
7025
|
+
const eventRef = getPatientCalendarEventDocRef(db, patientId, eventId);
|
|
7026
|
+
const newEvent = {
|
|
7027
|
+
id: eventId,
|
|
7028
|
+
...eventData,
|
|
7029
|
+
createdAt: serverTimestamp15(),
|
|
7030
|
+
updatedAt: serverTimestamp15()
|
|
7031
|
+
};
|
|
7032
|
+
await setDoc16(eventRef, newEvent);
|
|
7033
|
+
return {
|
|
7034
|
+
...newEvent,
|
|
7035
|
+
createdAt: Timestamp19.now(),
|
|
7036
|
+
updatedAt: Timestamp19.now()
|
|
7037
|
+
};
|
|
7038
|
+
}
|
|
7039
|
+
async function updatePatientCalendarEventUtil(db, patientId, eventId, updateData) {
|
|
7040
|
+
const eventRef = getPatientCalendarEventDocRef(db, patientId, eventId);
|
|
7041
|
+
const updates = {
|
|
7042
|
+
...updateData,
|
|
7043
|
+
updatedAt: serverTimestamp15()
|
|
7044
|
+
};
|
|
7045
|
+
await updateDoc17(eventRef, updates);
|
|
7046
|
+
const updatedDoc = await getDoc19(eventRef);
|
|
7047
|
+
if (!updatedDoc.exists()) {
|
|
7048
|
+
throw new Error("Event not found after update");
|
|
7049
|
+
}
|
|
7050
|
+
return updatedDoc.data();
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
// src/services/calendar/utils/practitioner.utils.ts
|
|
7054
|
+
import {
|
|
7055
|
+
collection as collection15,
|
|
7056
|
+
getDoc as getDoc20,
|
|
7057
|
+
getDocs as getDocs14,
|
|
7058
|
+
setDoc as setDoc17,
|
|
7059
|
+
updateDoc as updateDoc18,
|
|
7060
|
+
deleteDoc as deleteDoc10,
|
|
7061
|
+
query as query14,
|
|
7062
|
+
where as where14,
|
|
7063
|
+
orderBy as orderBy6,
|
|
7064
|
+
Timestamp as Timestamp20,
|
|
7065
|
+
serverTimestamp as serverTimestamp16
|
|
7066
|
+
} from "firebase/firestore";
|
|
7067
|
+
async function createPractitionerCalendarEventUtil(db, practitionerId, eventData, generateId2) {
|
|
7068
|
+
const eventId = generateId2();
|
|
7069
|
+
const eventRef = getPractitionerCalendarEventDocRef(
|
|
7070
|
+
db,
|
|
7071
|
+
practitionerId,
|
|
7072
|
+
eventId
|
|
7073
|
+
);
|
|
7074
|
+
const newEvent = {
|
|
7075
|
+
id: eventId,
|
|
7076
|
+
...eventData,
|
|
7077
|
+
createdAt: serverTimestamp16(),
|
|
7078
|
+
updatedAt: serverTimestamp16()
|
|
7079
|
+
};
|
|
7080
|
+
await setDoc17(eventRef, newEvent);
|
|
7081
|
+
return {
|
|
7082
|
+
...newEvent,
|
|
7083
|
+
createdAt: Timestamp20.now(),
|
|
7084
|
+
updatedAt: Timestamp20.now()
|
|
7085
|
+
};
|
|
7086
|
+
}
|
|
7087
|
+
async function updatePractitionerCalendarEventUtil(db, practitionerId, eventId, updateData) {
|
|
7088
|
+
const eventRef = getPractitionerCalendarEventDocRef(
|
|
7089
|
+
db,
|
|
7090
|
+
practitionerId,
|
|
7091
|
+
eventId
|
|
7092
|
+
);
|
|
7093
|
+
const updates = {
|
|
7094
|
+
...updateData,
|
|
7095
|
+
updatedAt: serverTimestamp16()
|
|
7096
|
+
};
|
|
7097
|
+
await updateDoc18(eventRef, updates);
|
|
7098
|
+
const updatedDoc = await getDoc20(eventRef);
|
|
7099
|
+
if (!updatedDoc.exists()) {
|
|
7100
|
+
throw new Error("Event not found after update");
|
|
7101
|
+
}
|
|
7102
|
+
return updatedDoc.data();
|
|
7103
|
+
}
|
|
7104
|
+
|
|
7105
|
+
// src/services/calendar/utils/appointment.utils.ts
|
|
7106
|
+
async function createAppointmentUtil(db, clinicId, practitionerId, patientId, eventData, generateId2) {
|
|
7107
|
+
const eventId = generateId2();
|
|
7108
|
+
const autoConfirm = await checkAutoConfirmAppointmentsUtil(db, clinicId);
|
|
7109
|
+
const initialStatus = autoConfirm ? "confirmed" /* CONFIRMED */ : "pending" /* PENDING */;
|
|
7110
|
+
const appointmentData = {
|
|
7111
|
+
...eventData,
|
|
7112
|
+
clinicBranchId: clinicId,
|
|
7113
|
+
practitionerProfileId: practitionerId,
|
|
7114
|
+
patientProfileId: patientId,
|
|
7115
|
+
eventType: "appointment" /* APPOINTMENT */,
|
|
7116
|
+
status: eventData.status || initialStatus
|
|
7117
|
+
};
|
|
7118
|
+
const clinicPromise = createClinicCalendarEventUtil(
|
|
7119
|
+
db,
|
|
7120
|
+
clinicId,
|
|
7121
|
+
appointmentData,
|
|
7122
|
+
() => eventId
|
|
7123
|
+
// Use the same ID for all calendars
|
|
7124
|
+
);
|
|
7125
|
+
const practitionerPromise = createPractitionerCalendarEventUtil(
|
|
7126
|
+
db,
|
|
7127
|
+
practitionerId,
|
|
7128
|
+
appointmentData,
|
|
7129
|
+
() => eventId
|
|
7130
|
+
// Use the same ID for all calendars
|
|
7131
|
+
);
|
|
7132
|
+
const patientPromise = createPatientCalendarEventUtil(
|
|
7133
|
+
db,
|
|
7134
|
+
patientId,
|
|
7135
|
+
appointmentData,
|
|
7136
|
+
() => eventId
|
|
7137
|
+
// Use the same ID for all calendars
|
|
7138
|
+
);
|
|
7139
|
+
const [clinicEvent] = await Promise.all([
|
|
7140
|
+
clinicPromise,
|
|
7141
|
+
practitionerPromise,
|
|
7142
|
+
patientPromise
|
|
7143
|
+
]);
|
|
7144
|
+
return clinicEvent;
|
|
7145
|
+
}
|
|
7146
|
+
async function updateAppointmentUtil(db, clinicId, practitionerId, patientId, eventId, updateData) {
|
|
7147
|
+
const clinicPromise = updateClinicCalendarEventUtil(
|
|
7148
|
+
db,
|
|
7149
|
+
clinicId,
|
|
7150
|
+
eventId,
|
|
7151
|
+
updateData
|
|
7152
|
+
);
|
|
7153
|
+
const practitionerPromise = updatePractitionerCalendarEventUtil(
|
|
7154
|
+
db,
|
|
7155
|
+
practitionerId,
|
|
7156
|
+
eventId,
|
|
7157
|
+
updateData
|
|
7158
|
+
);
|
|
7159
|
+
const patientPromise = updatePatientCalendarEventUtil(
|
|
7160
|
+
db,
|
|
7161
|
+
patientId,
|
|
7162
|
+
eventId,
|
|
7163
|
+
updateData
|
|
7164
|
+
);
|
|
7165
|
+
const [clinicEvent] = await Promise.all([
|
|
7166
|
+
clinicPromise,
|
|
7167
|
+
practitionerPromise,
|
|
7168
|
+
patientPromise
|
|
7169
|
+
]);
|
|
7170
|
+
return clinicEvent;
|
|
7171
|
+
}
|
|
7172
|
+
|
|
7173
|
+
// src/services/calendar/utils/synced-calendar.utils.ts
|
|
7174
|
+
import {
|
|
7175
|
+
collection as collection16,
|
|
7176
|
+
getDoc as getDoc21,
|
|
7177
|
+
getDocs as getDocs15,
|
|
7178
|
+
setDoc as setDoc18,
|
|
7179
|
+
updateDoc as updateDoc19,
|
|
7180
|
+
deleteDoc as deleteDoc11,
|
|
7181
|
+
query as query15,
|
|
7182
|
+
orderBy as orderBy7,
|
|
7183
|
+
Timestamp as Timestamp21,
|
|
7184
|
+
serverTimestamp as serverTimestamp17
|
|
7185
|
+
} from "firebase/firestore";
|
|
7186
|
+
async function createPractitionerSyncedCalendarUtil(db, practitionerId, calendarData, generateId2) {
|
|
7187
|
+
const calendarId = generateId2();
|
|
7188
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
7189
|
+
db,
|
|
7190
|
+
practitionerId,
|
|
7191
|
+
calendarId
|
|
7192
|
+
);
|
|
7193
|
+
const newCalendar = {
|
|
7194
|
+
id: calendarId,
|
|
7195
|
+
...calendarData,
|
|
7196
|
+
createdAt: serverTimestamp17(),
|
|
7197
|
+
updatedAt: serverTimestamp17()
|
|
7198
|
+
};
|
|
7199
|
+
await setDoc18(calendarRef, newCalendar);
|
|
7200
|
+
return {
|
|
7201
|
+
...newCalendar,
|
|
7202
|
+
createdAt: Timestamp21.now(),
|
|
7203
|
+
updatedAt: Timestamp21.now()
|
|
7204
|
+
};
|
|
7205
|
+
}
|
|
7206
|
+
async function createPatientSyncedCalendarUtil(db, patientId, calendarData, generateId2) {
|
|
7207
|
+
const calendarId = generateId2();
|
|
7208
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
7209
|
+
const newCalendar = {
|
|
7210
|
+
id: calendarId,
|
|
7211
|
+
...calendarData,
|
|
7212
|
+
createdAt: serverTimestamp17(),
|
|
7213
|
+
updatedAt: serverTimestamp17()
|
|
7214
|
+
};
|
|
7215
|
+
await setDoc18(calendarRef, newCalendar);
|
|
7216
|
+
return {
|
|
7217
|
+
...newCalendar,
|
|
7218
|
+
createdAt: Timestamp21.now(),
|
|
7219
|
+
updatedAt: Timestamp21.now()
|
|
7220
|
+
};
|
|
7221
|
+
}
|
|
7222
|
+
async function createClinicSyncedCalendarUtil(db, clinicId, calendarData, generateId2) {
|
|
7223
|
+
const calendarId = generateId2();
|
|
7224
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
7225
|
+
const newCalendar = {
|
|
7226
|
+
id: calendarId,
|
|
7227
|
+
...calendarData,
|
|
7228
|
+
createdAt: serverTimestamp17(),
|
|
7229
|
+
updatedAt: serverTimestamp17()
|
|
7230
|
+
};
|
|
7231
|
+
await setDoc18(calendarRef, newCalendar);
|
|
7232
|
+
return {
|
|
7233
|
+
...newCalendar,
|
|
7234
|
+
createdAt: Timestamp21.now(),
|
|
7235
|
+
updatedAt: Timestamp21.now()
|
|
7236
|
+
};
|
|
7237
|
+
}
|
|
7238
|
+
async function getPractitionerSyncedCalendarUtil(db, practitionerId, calendarId) {
|
|
7239
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
7240
|
+
db,
|
|
7241
|
+
practitionerId,
|
|
7242
|
+
calendarId
|
|
7243
|
+
);
|
|
7244
|
+
const calendarDoc = await getDoc21(calendarRef);
|
|
7245
|
+
if (!calendarDoc.exists()) {
|
|
7246
|
+
return null;
|
|
7247
|
+
}
|
|
7248
|
+
return calendarDoc.data();
|
|
7249
|
+
}
|
|
7250
|
+
async function getPractitionerSyncedCalendarsUtil(db, practitionerId) {
|
|
7251
|
+
const calendarsRef = collection16(
|
|
7252
|
+
db,
|
|
7253
|
+
`practitioners/${practitionerId}/${SYNCED_CALENDARS_COLLECTION}`
|
|
7254
|
+
);
|
|
7255
|
+
const q = query15(calendarsRef, orderBy7("createdAt", "desc"));
|
|
7256
|
+
const querySnapshot = await getDocs15(q);
|
|
7257
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
7258
|
+
}
|
|
7259
|
+
async function getPatientSyncedCalendarUtil(db, patientId, calendarId) {
|
|
7260
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
7261
|
+
const calendarDoc = await getDoc21(calendarRef);
|
|
7262
|
+
if (!calendarDoc.exists()) {
|
|
7263
|
+
return null;
|
|
7264
|
+
}
|
|
7265
|
+
return calendarDoc.data();
|
|
7266
|
+
}
|
|
7267
|
+
async function getPatientSyncedCalendarsUtil(db, patientId) {
|
|
7268
|
+
const calendarsRef = collection16(
|
|
7269
|
+
db,
|
|
7270
|
+
`patients/${patientId}/${SYNCED_CALENDARS_COLLECTION}`
|
|
7271
|
+
);
|
|
7272
|
+
const q = query15(calendarsRef, orderBy7("createdAt", "desc"));
|
|
7273
|
+
const querySnapshot = await getDocs15(q);
|
|
7274
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
7275
|
+
}
|
|
7276
|
+
async function getClinicSyncedCalendarUtil(db, clinicId, calendarId) {
|
|
7277
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
7278
|
+
const calendarDoc = await getDoc21(calendarRef);
|
|
7279
|
+
if (!calendarDoc.exists()) {
|
|
7280
|
+
return null;
|
|
7281
|
+
}
|
|
7282
|
+
return calendarDoc.data();
|
|
7283
|
+
}
|
|
7284
|
+
async function getClinicSyncedCalendarsUtil(db, clinicId) {
|
|
7285
|
+
const calendarsRef = collection16(
|
|
7286
|
+
db,
|
|
7287
|
+
`clinics/${clinicId}/${SYNCED_CALENDARS_COLLECTION}`
|
|
7288
|
+
);
|
|
7289
|
+
const q = query15(calendarsRef, orderBy7("createdAt", "desc"));
|
|
7290
|
+
const querySnapshot = await getDocs15(q);
|
|
7291
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
7292
|
+
}
|
|
7293
|
+
async function updatePractitionerSyncedCalendarUtil(db, practitionerId, calendarId, updateData) {
|
|
7294
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
7295
|
+
db,
|
|
7296
|
+
practitionerId,
|
|
7297
|
+
calendarId
|
|
7298
|
+
);
|
|
7299
|
+
const updates = {
|
|
7300
|
+
...updateData,
|
|
7301
|
+
updatedAt: serverTimestamp17()
|
|
7302
|
+
};
|
|
7303
|
+
await updateDoc19(calendarRef, updates);
|
|
7304
|
+
const updatedDoc = await getDoc21(calendarRef);
|
|
7305
|
+
if (!updatedDoc.exists()) {
|
|
7306
|
+
throw new Error("Synced calendar not found after update");
|
|
7307
|
+
}
|
|
7308
|
+
return updatedDoc.data();
|
|
7309
|
+
}
|
|
7310
|
+
async function updatePatientSyncedCalendarUtil(db, patientId, calendarId, updateData) {
|
|
7311
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
7312
|
+
const updates = {
|
|
7313
|
+
...updateData,
|
|
7314
|
+
updatedAt: serverTimestamp17()
|
|
7315
|
+
};
|
|
7316
|
+
await updateDoc19(calendarRef, updates);
|
|
7317
|
+
const updatedDoc = await getDoc21(calendarRef);
|
|
7318
|
+
if (!updatedDoc.exists()) {
|
|
7319
|
+
throw new Error("Synced calendar not found after update");
|
|
7320
|
+
}
|
|
7321
|
+
return updatedDoc.data();
|
|
7322
|
+
}
|
|
7323
|
+
async function updateClinicSyncedCalendarUtil(db, clinicId, calendarId, updateData) {
|
|
7324
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
7325
|
+
const updates = {
|
|
7326
|
+
...updateData,
|
|
7327
|
+
updatedAt: serverTimestamp17()
|
|
7328
|
+
};
|
|
7329
|
+
await updateDoc19(calendarRef, updates);
|
|
7330
|
+
const updatedDoc = await getDoc21(calendarRef);
|
|
7331
|
+
if (!updatedDoc.exists()) {
|
|
7332
|
+
throw new Error("Synced calendar not found after update");
|
|
7333
|
+
}
|
|
7334
|
+
return updatedDoc.data();
|
|
7335
|
+
}
|
|
7336
|
+
async function deletePractitionerSyncedCalendarUtil(db, practitionerId, calendarId) {
|
|
7337
|
+
const calendarRef = getPractitionerSyncedCalendarDocRef(
|
|
7338
|
+
db,
|
|
7339
|
+
practitionerId,
|
|
7340
|
+
calendarId
|
|
7341
|
+
);
|
|
7342
|
+
await deleteDoc11(calendarRef);
|
|
7343
|
+
}
|
|
7344
|
+
async function deletePatientSyncedCalendarUtil(db, patientId, calendarId) {
|
|
7345
|
+
const calendarRef = getPatientSyncedCalendarDocRef(db, patientId, calendarId);
|
|
7346
|
+
await deleteDoc11(calendarRef);
|
|
7347
|
+
}
|
|
7348
|
+
async function deleteClinicSyncedCalendarUtil(db, clinicId, calendarId) {
|
|
7349
|
+
const calendarRef = getClinicSyncedCalendarDocRef(db, clinicId, calendarId);
|
|
7350
|
+
await deleteDoc11(calendarRef);
|
|
7351
|
+
}
|
|
7352
|
+
async function updateLastSyncedTimestampUtil(db, entityType, entityId, calendarId) {
|
|
7353
|
+
const updateData = {
|
|
7354
|
+
lastSyncedAt: Timestamp21.now()
|
|
7355
|
+
};
|
|
7356
|
+
switch (entityType) {
|
|
7357
|
+
case "practitioner":
|
|
7358
|
+
return updatePractitionerSyncedCalendarUtil(
|
|
7359
|
+
db,
|
|
7360
|
+
entityId,
|
|
7361
|
+
calendarId,
|
|
7362
|
+
updateData
|
|
7363
|
+
);
|
|
7364
|
+
case "patient":
|
|
7365
|
+
return updatePatientSyncedCalendarUtil(
|
|
7366
|
+
db,
|
|
7367
|
+
entityId,
|
|
7368
|
+
calendarId,
|
|
7369
|
+
updateData
|
|
7370
|
+
);
|
|
7371
|
+
case "clinic":
|
|
7372
|
+
return updateClinicSyncedCalendarUtil(
|
|
7373
|
+
db,
|
|
7374
|
+
entityId,
|
|
7375
|
+
calendarId,
|
|
7376
|
+
updateData
|
|
7377
|
+
);
|
|
7378
|
+
default:
|
|
7379
|
+
throw new Error(`Invalid entity type: ${entityType}`);
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
|
|
7383
|
+
// src/services/calendar/utils/google-calendar.utils.ts
|
|
7384
|
+
import { Timestamp as Timestamp22 } from "firebase/firestore";
|
|
7385
|
+
var GOOGLE_CALENDAR_API_URL = "https://www.googleapis.com/calendar/v3";
|
|
7386
|
+
var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
7387
|
+
var CLIENT_ID = "your-client-id";
|
|
7388
|
+
var CLIENT_SECRET = "your-client-secret";
|
|
7389
|
+
var REDIRECT_URI = "your-redirect-uri";
|
|
7390
|
+
async function makeRequest(method, url, headers, data, params) {
|
|
7391
|
+
const queryParams = params ? "?" + Object.entries(params).map(
|
|
7392
|
+
([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
7393
|
+
).join("&") : "";
|
|
7394
|
+
const finalUrl = url + queryParams;
|
|
7395
|
+
const options = {
|
|
7396
|
+
method,
|
|
7397
|
+
headers,
|
|
7398
|
+
body: data ? JSON.stringify(data) : void 0
|
|
7399
|
+
};
|
|
7400
|
+
const response = await fetch(finalUrl, options);
|
|
7401
|
+
if (!response.ok) {
|
|
7402
|
+
const error = new Error(
|
|
7403
|
+
`Request failed with status ${response.status}`
|
|
7404
|
+
);
|
|
7405
|
+
error.response = response;
|
|
7406
|
+
throw error;
|
|
7407
|
+
}
|
|
7408
|
+
return response.json();
|
|
7409
|
+
}
|
|
7410
|
+
async function authenticateWithGoogleCalendarUtil(authCode) {
|
|
7411
|
+
try {
|
|
7412
|
+
const data = {
|
|
7413
|
+
code: authCode,
|
|
7414
|
+
client_id: CLIENT_ID,
|
|
7415
|
+
client_secret: CLIENT_SECRET,
|
|
7416
|
+
redirect_uri: REDIRECT_URI,
|
|
7417
|
+
grant_type: "authorization_code"
|
|
7418
|
+
};
|
|
7419
|
+
const response = await makeRequest(
|
|
7420
|
+
"post",
|
|
7421
|
+
GOOGLE_OAUTH_URL,
|
|
7422
|
+
{ "Content-Type": "application/json" },
|
|
7423
|
+
data
|
|
7424
|
+
);
|
|
7425
|
+
return {
|
|
7426
|
+
accessToken: response.access_token,
|
|
7427
|
+
refreshToken: response.refresh_token,
|
|
7428
|
+
expiresIn: response.expires_in
|
|
7429
|
+
};
|
|
7430
|
+
} catch (error) {
|
|
7431
|
+
const apiError = error;
|
|
7432
|
+
console.error(
|
|
7433
|
+
"Error authenticating with Google Calendar:",
|
|
7434
|
+
apiError.message || "Unknown error"
|
|
7435
|
+
);
|
|
7436
|
+
throw new Error(
|
|
7437
|
+
`Failed to authenticate with Google Calendar: ${apiError.message || "Unknown error"}`
|
|
7438
|
+
);
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
async function refreshGoogleCalendarTokenUtil(refreshToken) {
|
|
7442
|
+
try {
|
|
7443
|
+
const data = {
|
|
7444
|
+
refresh_token: refreshToken,
|
|
7445
|
+
client_id: CLIENT_ID,
|
|
7446
|
+
client_secret: CLIENT_SECRET,
|
|
7447
|
+
grant_type: "refresh_token"
|
|
7448
|
+
};
|
|
7449
|
+
const response = await makeRequest(
|
|
7450
|
+
"post",
|
|
7451
|
+
GOOGLE_OAUTH_URL,
|
|
7452
|
+
{ "Content-Type": "application/json" },
|
|
7453
|
+
data
|
|
7454
|
+
);
|
|
7455
|
+
return {
|
|
7456
|
+
accessToken: response.access_token,
|
|
7457
|
+
expiresIn: response.expires_in
|
|
7458
|
+
};
|
|
7459
|
+
} catch (error) {
|
|
7460
|
+
const apiError = error;
|
|
7461
|
+
console.error(
|
|
7462
|
+
"Error refreshing Google Calendar token:",
|
|
7463
|
+
apiError.message || "Unknown error"
|
|
7464
|
+
);
|
|
7465
|
+
throw new Error(
|
|
7466
|
+
`Failed to refresh Google Calendar token: ${apiError.message || "Unknown error"}`
|
|
7467
|
+
);
|
|
7468
|
+
}
|
|
7469
|
+
}
|
|
7470
|
+
async function listGoogleCalendarsUtil(accessToken) {
|
|
7471
|
+
try {
|
|
7472
|
+
const response = await makeRequest(
|
|
7473
|
+
"get",
|
|
7474
|
+
`${GOOGLE_CALENDAR_API_URL}/users/me/calendarList`,
|
|
7475
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
7476
|
+
);
|
|
7477
|
+
return response.items.map((calendar) => ({
|
|
7478
|
+
id: calendar.id,
|
|
7479
|
+
name: calendar.summary
|
|
7480
|
+
}));
|
|
7481
|
+
} catch (error) {
|
|
7482
|
+
const apiError = error;
|
|
7483
|
+
console.error(
|
|
7484
|
+
"Error listing Google Calendars:",
|
|
7485
|
+
apiError.message || "Unknown error"
|
|
7486
|
+
);
|
|
7487
|
+
throw new Error(
|
|
7488
|
+
`Failed to list Google Calendars: ${apiError.message || "Unknown error"}`
|
|
7489
|
+
);
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
async function ensureValidToken(db, entityType, entityId, syncedCalendar) {
|
|
7493
|
+
const expiryTime = syncedCalendar.tokenExpiry.toDate();
|
|
7494
|
+
const now = /* @__PURE__ */ new Date();
|
|
7495
|
+
const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1e3);
|
|
7496
|
+
if (expiryTime < fiveMinutesFromNow) {
|
|
7497
|
+
const { accessToken, expiresIn } = await refreshGoogleCalendarTokenUtil(
|
|
7498
|
+
syncedCalendar.refreshToken
|
|
7499
|
+
);
|
|
7500
|
+
const tokenExpiry = /* @__PURE__ */ new Date();
|
|
7501
|
+
tokenExpiry.setSeconds(tokenExpiry.getSeconds() + expiresIn);
|
|
7502
|
+
const updateData = {
|
|
7503
|
+
accessToken,
|
|
7504
|
+
tokenExpiry: Timestamp22.fromDate(tokenExpiry)
|
|
7505
|
+
};
|
|
7506
|
+
switch (entityType) {
|
|
7507
|
+
case "practitioner":
|
|
7508
|
+
await updatePractitionerSyncedCalendarUtil(
|
|
7509
|
+
db,
|
|
7510
|
+
entityId,
|
|
7511
|
+
syncedCalendar.id,
|
|
7512
|
+
updateData
|
|
7513
|
+
);
|
|
7514
|
+
break;
|
|
7515
|
+
case "patient":
|
|
7516
|
+
await updatePatientSyncedCalendarUtil(
|
|
7517
|
+
db,
|
|
7518
|
+
entityId,
|
|
7519
|
+
syncedCalendar.id,
|
|
7520
|
+
updateData
|
|
7521
|
+
);
|
|
7522
|
+
break;
|
|
7523
|
+
case "clinic":
|
|
7524
|
+
await updateClinicSyncedCalendarUtil(
|
|
7525
|
+
db,
|
|
7526
|
+
entityId,
|
|
7527
|
+
syncedCalendar.id,
|
|
7528
|
+
updateData
|
|
7529
|
+
);
|
|
7530
|
+
break;
|
|
7531
|
+
}
|
|
7532
|
+
return accessToken;
|
|
7533
|
+
}
|
|
7534
|
+
return syncedCalendar.accessToken;
|
|
7535
|
+
}
|
|
7536
|
+
async function syncEventsToGoogleCalendarUtil(db, entityType, entityId, syncedCalendar, events, existingSyncId) {
|
|
7537
|
+
var _a, _b;
|
|
7538
|
+
try {
|
|
7539
|
+
const { accessToken } = await refreshGoogleCalendarTokenUtil(
|
|
7540
|
+
syncedCalendar.refreshToken
|
|
7541
|
+
);
|
|
7542
|
+
let syncedCount = 0;
|
|
7543
|
+
const errors = [];
|
|
7544
|
+
const eventIds = [];
|
|
7545
|
+
for (const event of events) {
|
|
7546
|
+
try {
|
|
7547
|
+
if (event.syncStatus === "external" /* EXTERNAL */) {
|
|
7548
|
+
continue;
|
|
7549
|
+
}
|
|
7550
|
+
if (entityType === "practitioner" && event.status !== "confirmed" /* CONFIRMED */) {
|
|
7551
|
+
continue;
|
|
7552
|
+
}
|
|
7553
|
+
if (entityType === "patient" && (event.status === "canceled" /* CANCELED */ || event.status === "rejected" /* REJECTED */)) {
|
|
7554
|
+
continue;
|
|
7555
|
+
}
|
|
7556
|
+
if (entityType === "clinic") {
|
|
7557
|
+
continue;
|
|
7558
|
+
}
|
|
7559
|
+
const googleEvent = convertCalendarEventToGoogleEventUtil(event);
|
|
7560
|
+
const headers = {
|
|
7561
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7562
|
+
"Content-Type": "application/json"
|
|
7563
|
+
};
|
|
7564
|
+
let responseId = "";
|
|
7565
|
+
if (existingSyncId) {
|
|
7566
|
+
const response = await makeRequest(
|
|
7567
|
+
"put",
|
|
7568
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSyncId}`,
|
|
7569
|
+
headers,
|
|
7570
|
+
googleEvent
|
|
7571
|
+
);
|
|
7572
|
+
responseId = response.id;
|
|
7573
|
+
} else {
|
|
7574
|
+
const existingSync = (_a = event.syncedCalendarEventId) == null ? void 0 : _a.find(
|
|
7575
|
+
(sync) => sync.syncedCalendarProvider === "google" /* GOOGLE */ && // We should check if this is the same calendar we're syncing with, but that information isn't stored
|
|
7576
|
+
// For now, we'll just use the first Google Calendar sync ID
|
|
7577
|
+
sync.syncedCalendarProvider === syncedCalendar.provider
|
|
7578
|
+
);
|
|
7579
|
+
if (existingSync) {
|
|
7580
|
+
const response = await makeRequest(
|
|
7581
|
+
"put",
|
|
7582
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSync.eventId}`,
|
|
7583
|
+
headers,
|
|
7584
|
+
googleEvent
|
|
7585
|
+
);
|
|
7586
|
+
responseId = response.id;
|
|
7587
|
+
} else {
|
|
7588
|
+
const response = await makeRequest(
|
|
7589
|
+
"post",
|
|
7590
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
7591
|
+
headers,
|
|
7592
|
+
googleEvent
|
|
7593
|
+
);
|
|
7594
|
+
responseId = response.id;
|
|
7595
|
+
}
|
|
7596
|
+
}
|
|
7597
|
+
if (responseId) {
|
|
7598
|
+
eventIds.push(responseId);
|
|
7599
|
+
syncedCount++;
|
|
7600
|
+
}
|
|
7601
|
+
} catch (error) {
|
|
7602
|
+
const apiError = error;
|
|
7603
|
+
errors.push({
|
|
7604
|
+
eventId: event.id,
|
|
7605
|
+
error: apiError.message || "Unknown error",
|
|
7606
|
+
status: (_b = apiError.response) == null ? void 0 : _b.status
|
|
7607
|
+
});
|
|
7608
|
+
}
|
|
7609
|
+
}
|
|
7610
|
+
await updateLastSyncedTimestampUtil(
|
|
7611
|
+
db,
|
|
7612
|
+
entityType,
|
|
7613
|
+
entityId,
|
|
7614
|
+
syncedCalendar.id
|
|
7615
|
+
);
|
|
7616
|
+
return {
|
|
7617
|
+
success: errors.length === 0,
|
|
7618
|
+
syncedEvents: syncedCount,
|
|
7619
|
+
errors,
|
|
7620
|
+
eventIds
|
|
7621
|
+
};
|
|
7622
|
+
} catch (error) {
|
|
7623
|
+
console.error("Error syncing with Google Calendar:", error);
|
|
7624
|
+
return {
|
|
7625
|
+
success: false,
|
|
7626
|
+
syncedEvents: 0,
|
|
7627
|
+
errors: [{ error: error.message || "Unknown error" }],
|
|
7628
|
+
eventIds: []
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
async function fetchEventsFromGoogleCalendarUtil(db, entityType, entityId, syncedCalendar, startDate, endDate) {
|
|
7633
|
+
try {
|
|
7634
|
+
const accessToken = await ensureValidToken(
|
|
7635
|
+
db,
|
|
7636
|
+
entityType,
|
|
7637
|
+
entityId,
|
|
7638
|
+
syncedCalendar
|
|
7639
|
+
);
|
|
7640
|
+
const timeMin = startDate.toISOString();
|
|
7641
|
+
const timeMax = endDate.toISOString();
|
|
7642
|
+
const response = await makeRequest(
|
|
7643
|
+
"get",
|
|
7644
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
|
|
7645
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
7646
|
+
void 0,
|
|
7647
|
+
{
|
|
7648
|
+
timeMin,
|
|
7649
|
+
timeMax,
|
|
7650
|
+
singleEvents: "true",
|
|
7651
|
+
orderBy: "startTime"
|
|
7652
|
+
}
|
|
7653
|
+
);
|
|
7654
|
+
await updateLastSyncedTimestampUtil(
|
|
7655
|
+
db,
|
|
7656
|
+
entityType,
|
|
7657
|
+
entityId,
|
|
7658
|
+
syncedCalendar.id
|
|
7659
|
+
);
|
|
7660
|
+
return response.items;
|
|
7661
|
+
} catch (error) {
|
|
7662
|
+
const apiError = error;
|
|
7663
|
+
console.error(
|
|
7664
|
+
"Error fetching events from Google Calendar:",
|
|
7665
|
+
apiError.message || "Unknown error"
|
|
7666
|
+
);
|
|
7667
|
+
throw new Error(
|
|
7668
|
+
`Failed to fetch events from Google Calendar: ${apiError.message || "Unknown error"}`
|
|
7669
|
+
);
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
function convertGoogleEventToCalendarEventUtil(googleEvent, entityId, entityType) {
|
|
7673
|
+
const start = googleEvent.start.dateTime ? new Date(googleEvent.start.dateTime) : new Date(googleEvent.start.date);
|
|
7674
|
+
const end = googleEvent.end.dateTime ? new Date(googleEvent.end.dateTime) : new Date(googleEvent.end.date);
|
|
7675
|
+
const calendarEvent = {
|
|
7676
|
+
eventName: googleEvent.summary || "External Event",
|
|
7677
|
+
eventLocation: googleEvent.location,
|
|
7678
|
+
eventTime: {
|
|
7679
|
+
start: Timestamp22.fromDate(start),
|
|
7680
|
+
end: Timestamp22.fromDate(end)
|
|
7681
|
+
},
|
|
7682
|
+
description: googleEvent.description || "",
|
|
7683
|
+
// External events are always set as CONFIRMED - status updates will happen externally
|
|
7684
|
+
status: "confirmed" /* CONFIRMED */,
|
|
7685
|
+
// All external events are marked as EXTERNAL to indicate they originated outside our system
|
|
7686
|
+
syncStatus: "external" /* EXTERNAL */,
|
|
7687
|
+
// All external events are treated as BLOCKING events
|
|
7688
|
+
eventType: "blocking" /* BLOCKING */,
|
|
7689
|
+
// Store the original Google Calendar event ID
|
|
7690
|
+
syncedCalendarEventId: [
|
|
7691
|
+
{
|
|
7692
|
+
eventId: googleEvent.id,
|
|
7693
|
+
syncedCalendarProvider: "google" /* GOOGLE */,
|
|
7694
|
+
syncedAt: Timestamp22.now()
|
|
7695
|
+
}
|
|
7696
|
+
]
|
|
7697
|
+
};
|
|
7698
|
+
switch (entityType) {
|
|
7699
|
+
case "practitioner":
|
|
7700
|
+
calendarEvent.practitionerProfileId = entityId;
|
|
7701
|
+
break;
|
|
7702
|
+
case "patient":
|
|
7703
|
+
calendarEvent.patientProfileId = entityId;
|
|
7704
|
+
break;
|
|
7705
|
+
case "clinic":
|
|
7706
|
+
calendarEvent.clinicBranchId = entityId;
|
|
7707
|
+
break;
|
|
7708
|
+
}
|
|
7709
|
+
return calendarEvent;
|
|
7710
|
+
}
|
|
7711
|
+
function convertCalendarEventToGoogleEventUtil(calendarEvent) {
|
|
7712
|
+
const googleEvent = {
|
|
7713
|
+
summary: calendarEvent.eventName,
|
|
7714
|
+
location: calendarEvent.eventLocation,
|
|
7715
|
+
description: calendarEvent.description,
|
|
7716
|
+
start: {
|
|
7717
|
+
dateTime: calendarEvent.eventTime.start.toDate().toISOString(),
|
|
7718
|
+
timeZone: "UTC"
|
|
7719
|
+
},
|
|
7720
|
+
end: {
|
|
7721
|
+
dateTime: calendarEvent.eventTime.end.toDate().toISOString(),
|
|
7722
|
+
timeZone: "UTC"
|
|
7723
|
+
},
|
|
7724
|
+
// Add reminders
|
|
7725
|
+
reminders: {
|
|
7726
|
+
useDefault: false,
|
|
7727
|
+
overrides: [
|
|
7728
|
+
{ method: "email", minutes: 24 * 60 },
|
|
7729
|
+
// 1 day before
|
|
7730
|
+
{ method: "popup", minutes: 30 }
|
|
7731
|
+
// 30 minutes before
|
|
7732
|
+
]
|
|
7733
|
+
}
|
|
7734
|
+
};
|
|
7735
|
+
switch (calendarEvent.status) {
|
|
7736
|
+
case "confirmed" /* CONFIRMED */:
|
|
7737
|
+
googleEvent.status = "confirmed";
|
|
7738
|
+
break;
|
|
7739
|
+
case "canceled" /* CANCELED */:
|
|
7740
|
+
googleEvent.status = "cancelled";
|
|
7741
|
+
break;
|
|
7742
|
+
case "pending" /* PENDING */:
|
|
7743
|
+
googleEvent.status = "tentative";
|
|
7744
|
+
break;
|
|
7745
|
+
default:
|
|
7746
|
+
googleEvent.status = "confirmed";
|
|
7747
|
+
}
|
|
7748
|
+
if (calendarEvent.eventType === "appointment" /* APPOINTMENT */) {
|
|
7749
|
+
googleEvent.attendees = [];
|
|
7750
|
+
if (calendarEvent.practitionerProfileId) {
|
|
7751
|
+
googleEvent.attendees.push({
|
|
7752
|
+
email: "practitioner@example.com",
|
|
7753
|
+
// This would be fetched from the practitioner profile
|
|
7754
|
+
displayName: "Dr. Practitioner",
|
|
7755
|
+
// This would be fetched from the practitioner profile
|
|
7756
|
+
responseStatus: "accepted"
|
|
7757
|
+
});
|
|
7758
|
+
}
|
|
7759
|
+
if (calendarEvent.patientProfileId) {
|
|
7760
|
+
googleEvent.attendees.push({
|
|
7761
|
+
email: "patient@example.com",
|
|
7762
|
+
// This would be fetched from the patient profile
|
|
7763
|
+
displayName: "Patient",
|
|
7764
|
+
// This would be fetched from the patient profile
|
|
7765
|
+
responseStatus: "needsAction"
|
|
7766
|
+
});
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
return googleEvent;
|
|
7770
|
+
}
|
|
7771
|
+
async function deleteGoogleCalendarEventUtil(db, entityType, entityId, syncedCalendar, eventId) {
|
|
7772
|
+
try {
|
|
7773
|
+
const accessToken = await ensureValidToken(
|
|
7774
|
+
db,
|
|
7775
|
+
entityType,
|
|
7776
|
+
entityId,
|
|
7777
|
+
syncedCalendar
|
|
7778
|
+
);
|
|
7779
|
+
await makeRequest(
|
|
7780
|
+
"delete",
|
|
7781
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
|
|
7782
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
7783
|
+
);
|
|
7784
|
+
return true;
|
|
7785
|
+
} catch (error) {
|
|
7786
|
+
const apiError = error;
|
|
7787
|
+
console.error(
|
|
7788
|
+
"Error deleting event from Google Calendar:",
|
|
7789
|
+
apiError.message || "Unknown error"
|
|
7790
|
+
);
|
|
7791
|
+
throw new Error(
|
|
7792
|
+
`Failed to delete event from Google Calendar: ${apiError.message || "Unknown error"}`
|
|
7793
|
+
);
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
function getGoogleCalendarOAuthUrlUtil(scopes = ["https://www.googleapis.com/auth/calendar"]) {
|
|
7797
|
+
const scopeString = encodeURIComponent(scopes.join(" "));
|
|
7798
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
|
|
7799
|
+
REDIRECT_URI
|
|
7800
|
+
)}&response_type=code&scope=${scopeString}&access_type=offline&prompt=consent`;
|
|
7801
|
+
}
|
|
7802
|
+
|
|
7803
|
+
// src/services/calendar/synced-calendars.service.ts
|
|
7804
|
+
var SyncedCalendarsService = class extends BaseService {
|
|
7805
|
+
/**
|
|
7806
|
+
* Creates a new SyncedCalendarsService instance
|
|
7807
|
+
* @param db - Firestore instance
|
|
7808
|
+
* @param auth - Firebase Auth instance
|
|
7809
|
+
* @param app - Firebase App instance
|
|
7810
|
+
*/
|
|
7811
|
+
constructor(db, auth, app) {
|
|
7812
|
+
super(db, auth, app);
|
|
7813
|
+
}
|
|
7814
|
+
// ===== Practitioner Synced Calendars =====
|
|
7815
|
+
/**
|
|
7816
|
+
* Creates a synced calendar for a practitioner
|
|
7817
|
+
* @param practitionerId - ID of the practitioner
|
|
7818
|
+
* @param calendarData - Synced calendar data
|
|
7819
|
+
* @returns Created synced calendar
|
|
7820
|
+
*/
|
|
7821
|
+
async createPractitionerSyncedCalendar(practitionerId, calendarData) {
|
|
7822
|
+
return createPractitionerSyncedCalendarUtil(
|
|
7823
|
+
this.db,
|
|
7824
|
+
practitionerId,
|
|
7825
|
+
calendarData,
|
|
7826
|
+
this.generateId.bind(this)
|
|
7827
|
+
);
|
|
7828
|
+
}
|
|
7829
|
+
/**
|
|
7830
|
+
* Gets a synced calendar for a practitioner
|
|
7831
|
+
* @param practitionerId - ID of the practitioner
|
|
7832
|
+
* @param calendarId - ID of the synced calendar
|
|
7833
|
+
* @returns Synced calendar or null if not found
|
|
7834
|
+
*/
|
|
7835
|
+
async getPractitionerSyncedCalendar(practitionerId, calendarId) {
|
|
7836
|
+
return getPractitionerSyncedCalendarUtil(
|
|
7837
|
+
this.db,
|
|
7838
|
+
practitionerId,
|
|
7839
|
+
calendarId
|
|
7840
|
+
);
|
|
7841
|
+
}
|
|
7842
|
+
/**
|
|
7843
|
+
* Gets all synced calendars for a practitioner
|
|
7844
|
+
* @param practitionerId - ID of the practitioner
|
|
7845
|
+
* @returns Array of synced calendars
|
|
7846
|
+
*/
|
|
7847
|
+
async getPractitionerSyncedCalendars(practitionerId) {
|
|
7848
|
+
return getPractitionerSyncedCalendarsUtil(this.db, practitionerId);
|
|
7849
|
+
}
|
|
7850
|
+
/**
|
|
7851
|
+
* Updates a synced calendar for a practitioner
|
|
7852
|
+
* @param practitionerId - ID of the practitioner
|
|
7853
|
+
* @param calendarId - ID of the synced calendar
|
|
7854
|
+
* @param updateData - Data to update
|
|
7855
|
+
* @returns Updated synced calendar
|
|
7856
|
+
*/
|
|
7857
|
+
async updatePractitionerSyncedCalendar(practitionerId, calendarId, updateData) {
|
|
7858
|
+
return updatePractitionerSyncedCalendarUtil(
|
|
7859
|
+
this.db,
|
|
7860
|
+
practitionerId,
|
|
7861
|
+
calendarId,
|
|
7862
|
+
updateData
|
|
7863
|
+
);
|
|
7864
|
+
}
|
|
7865
|
+
/**
|
|
7866
|
+
* Deletes a synced calendar for a practitioner
|
|
7867
|
+
* @param practitionerId - ID of the practitioner
|
|
7868
|
+
* @param calendarId - ID of the synced calendar
|
|
7869
|
+
*/
|
|
7870
|
+
async deletePractitionerSyncedCalendar(practitionerId, calendarId) {
|
|
7871
|
+
return deletePractitionerSyncedCalendarUtil(
|
|
7872
|
+
this.db,
|
|
7873
|
+
practitionerId,
|
|
7874
|
+
calendarId
|
|
7875
|
+
);
|
|
7876
|
+
}
|
|
7877
|
+
// ===== Patient Synced Calendars =====
|
|
7878
|
+
/**
|
|
7879
|
+
* Creates a synced calendar for a patient
|
|
7880
|
+
* @param patientId - ID of the patient
|
|
7881
|
+
* @param calendarData - Synced calendar data
|
|
7882
|
+
* @returns Created synced calendar
|
|
7883
|
+
*/
|
|
7884
|
+
async createPatientSyncedCalendar(patientId, calendarData) {
|
|
7885
|
+
return createPatientSyncedCalendarUtil(
|
|
7886
|
+
this.db,
|
|
7887
|
+
patientId,
|
|
7888
|
+
calendarData,
|
|
7889
|
+
this.generateId.bind(this)
|
|
7890
|
+
);
|
|
7891
|
+
}
|
|
7892
|
+
/**
|
|
7893
|
+
* Gets a synced calendar for a patient
|
|
7894
|
+
* @param patientId - ID of the patient
|
|
7895
|
+
* @param calendarId - ID of the synced calendar
|
|
7896
|
+
* @returns Synced calendar or null if not found
|
|
7897
|
+
*/
|
|
7898
|
+
async getPatientSyncedCalendar(patientId, calendarId) {
|
|
7899
|
+
return getPatientSyncedCalendarUtil(this.db, patientId, calendarId);
|
|
7900
|
+
}
|
|
7901
|
+
/**
|
|
7902
|
+
* Gets all synced calendars for a patient
|
|
7903
|
+
* @param patientId - ID of the patient
|
|
7904
|
+
* @returns Array of synced calendars
|
|
7905
|
+
*/
|
|
7906
|
+
async getPatientSyncedCalendars(patientId) {
|
|
7907
|
+
return getPatientSyncedCalendarsUtil(this.db, patientId);
|
|
7908
|
+
}
|
|
7909
|
+
/**
|
|
7910
|
+
* Updates a synced calendar for a patient
|
|
7911
|
+
* @param patientId - ID of the patient
|
|
7912
|
+
* @param calendarId - ID of the synced calendar
|
|
7913
|
+
* @param updateData - Data to update
|
|
7914
|
+
* @returns Updated synced calendar
|
|
7915
|
+
*/
|
|
7916
|
+
async updatePatientSyncedCalendar(patientId, calendarId, updateData) {
|
|
7917
|
+
return updatePatientSyncedCalendarUtil(
|
|
7918
|
+
this.db,
|
|
7919
|
+
patientId,
|
|
7920
|
+
calendarId,
|
|
7921
|
+
updateData
|
|
7922
|
+
);
|
|
7923
|
+
}
|
|
7924
|
+
/**
|
|
7925
|
+
* Deletes a synced calendar for a patient
|
|
7926
|
+
* @param patientId - ID of the patient
|
|
7927
|
+
* @param calendarId - ID of the synced calendar
|
|
7928
|
+
*/
|
|
7929
|
+
async deletePatientSyncedCalendar(patientId, calendarId) {
|
|
7930
|
+
return deletePatientSyncedCalendarUtil(this.db, patientId, calendarId);
|
|
7931
|
+
}
|
|
7932
|
+
// ===== Clinic Synced Calendars =====
|
|
7933
|
+
/**
|
|
7934
|
+
* Creates a synced calendar for a clinic
|
|
7935
|
+
* @param clinicId - ID of the clinic
|
|
7936
|
+
* @param calendarData - Synced calendar data
|
|
7937
|
+
* @returns Created synced calendar
|
|
7938
|
+
*/
|
|
7939
|
+
async createClinicSyncedCalendar(clinicId, calendarData) {
|
|
7940
|
+
return createClinicSyncedCalendarUtil(
|
|
7941
|
+
this.db,
|
|
7942
|
+
clinicId,
|
|
7943
|
+
calendarData,
|
|
7944
|
+
this.generateId.bind(this)
|
|
7945
|
+
);
|
|
7946
|
+
}
|
|
7947
|
+
/**
|
|
7948
|
+
* Gets a synced calendar for a clinic
|
|
7949
|
+
* @param clinicId - ID of the clinic
|
|
7950
|
+
* @param calendarId - ID of the synced calendar
|
|
7951
|
+
* @returns Synced calendar or null if not found
|
|
7952
|
+
*/
|
|
7953
|
+
async getClinicSyncedCalendar(clinicId, calendarId) {
|
|
7954
|
+
return getClinicSyncedCalendarUtil(this.db, clinicId, calendarId);
|
|
7955
|
+
}
|
|
7956
|
+
/**
|
|
7957
|
+
* Gets all synced calendars for a clinic
|
|
7958
|
+
* @param clinicId - ID of the clinic
|
|
7959
|
+
* @returns Array of synced calendars
|
|
7960
|
+
*/
|
|
7961
|
+
async getClinicSyncedCalendars(clinicId) {
|
|
7962
|
+
return getClinicSyncedCalendarsUtil(this.db, clinicId);
|
|
7963
|
+
}
|
|
7964
|
+
/**
|
|
7965
|
+
* Updates a synced calendar for a clinic
|
|
7966
|
+
* @param clinicId - ID of the clinic
|
|
7967
|
+
* @param calendarId - ID of the synced calendar
|
|
7968
|
+
* @param updateData - Data to update
|
|
7969
|
+
* @returns Updated synced calendar
|
|
7970
|
+
*/
|
|
7971
|
+
async updateClinicSyncedCalendar(clinicId, calendarId, updateData) {
|
|
7972
|
+
return updateClinicSyncedCalendarUtil(
|
|
7973
|
+
this.db,
|
|
7974
|
+
clinicId,
|
|
7975
|
+
calendarId,
|
|
7976
|
+
updateData
|
|
7977
|
+
);
|
|
7978
|
+
}
|
|
7979
|
+
/**
|
|
7980
|
+
* Deletes a synced calendar for a clinic
|
|
7981
|
+
* @param clinicId - ID of the clinic
|
|
7982
|
+
* @param calendarId - ID of the synced calendar
|
|
7983
|
+
*/
|
|
7984
|
+
async deleteClinicSyncedCalendar(clinicId, calendarId) {
|
|
7985
|
+
return deleteClinicSyncedCalendarUtil(this.db, clinicId, calendarId);
|
|
7986
|
+
}
|
|
7987
|
+
// ===== Google Calendar Integration =====
|
|
7988
|
+
/**
|
|
7989
|
+
* Gets the OAuth URL for Google Calendar
|
|
7990
|
+
* @param scopes - OAuth scopes to request
|
|
7991
|
+
* @returns OAuth URL
|
|
7992
|
+
*/
|
|
7993
|
+
getGoogleCalendarOAuthUrl(scopes = ["https://www.googleapis.com/auth/calendar"]) {
|
|
7994
|
+
return getGoogleCalendarOAuthUrlUtil(scopes);
|
|
7995
|
+
}
|
|
7996
|
+
/**
|
|
7997
|
+
* Authenticates with Google Calendar using an authorization code
|
|
7998
|
+
* @param authCode - Authorization code from Google OAuth
|
|
7999
|
+
* @returns Access token, refresh token, and expiration time
|
|
8000
|
+
*/
|
|
8001
|
+
async authenticateWithGoogleCalendar(authCode) {
|
|
8002
|
+
return authenticateWithGoogleCalendarUtil(authCode);
|
|
8003
|
+
}
|
|
8004
|
+
/**
|
|
8005
|
+
* Lists available Google Calendars for a user
|
|
8006
|
+
* @param accessToken - Google API access token
|
|
8007
|
+
* @returns List of available calendars
|
|
8008
|
+
*/
|
|
8009
|
+
async listGoogleCalendars(accessToken) {
|
|
8010
|
+
return listGoogleCalendarsUtil(accessToken);
|
|
8011
|
+
}
|
|
8012
|
+
/**
|
|
8013
|
+
* Syncs events from our system to Google Calendar for a practitioner
|
|
8014
|
+
* @param practitionerId - ID of the practitioner
|
|
8015
|
+
* @param calendarId - ID of the synced calendar
|
|
8016
|
+
* @param events - Events to sync
|
|
8017
|
+
* @param existingSyncId - Optional existing sync ID for updating an event
|
|
8018
|
+
* @returns Result of the sync operation
|
|
8019
|
+
*/
|
|
8020
|
+
async syncPractitionerEventsToGoogleCalendar(practitionerId, calendarId, events, existingSyncId) {
|
|
8021
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
8022
|
+
practitionerId,
|
|
8023
|
+
calendarId
|
|
8024
|
+
);
|
|
8025
|
+
if (!syncedCalendar) {
|
|
8026
|
+
throw new Error("Synced calendar not found");
|
|
8027
|
+
}
|
|
8028
|
+
return syncEventsToGoogleCalendarUtil(
|
|
8029
|
+
this.db,
|
|
8030
|
+
"practitioner",
|
|
8031
|
+
practitionerId,
|
|
8032
|
+
syncedCalendar,
|
|
8033
|
+
events,
|
|
8034
|
+
existingSyncId
|
|
8035
|
+
);
|
|
8036
|
+
}
|
|
8037
|
+
/**
|
|
8038
|
+
* Syncs events from our system to Google Calendar for a patient
|
|
8039
|
+
* @param patientId - ID of the patient
|
|
8040
|
+
* @param calendarId - ID of the synced calendar
|
|
8041
|
+
* @param events - Events to sync
|
|
8042
|
+
* @param existingSyncId - Optional existing sync ID for updating an event
|
|
8043
|
+
* @returns Result of the sync operation
|
|
8044
|
+
*/
|
|
8045
|
+
async syncPatientEventsToGoogleCalendar(patientId, calendarId, events, existingSyncId) {
|
|
8046
|
+
const syncedCalendar = await this.getPatientSyncedCalendar(
|
|
8047
|
+
patientId,
|
|
8048
|
+
calendarId
|
|
8049
|
+
);
|
|
8050
|
+
if (!syncedCalendar) {
|
|
8051
|
+
throw new Error("Synced calendar not found");
|
|
8052
|
+
}
|
|
8053
|
+
return syncEventsToGoogleCalendarUtil(
|
|
8054
|
+
this.db,
|
|
8055
|
+
"patient",
|
|
8056
|
+
patientId,
|
|
8057
|
+
syncedCalendar,
|
|
8058
|
+
events,
|
|
8059
|
+
existingSyncId
|
|
8060
|
+
);
|
|
8061
|
+
}
|
|
8062
|
+
/**
|
|
8063
|
+
* Syncs events from our system to Google Calendar for a clinic
|
|
8064
|
+
* @param clinicId - ID of the clinic
|
|
8065
|
+
* @param calendarId - ID of the synced calendar
|
|
8066
|
+
* @param events - Events to sync
|
|
8067
|
+
* @returns Result of the sync operation
|
|
8068
|
+
*/
|
|
8069
|
+
async syncClinicEventsToGoogleCalendar(clinicId, calendarId, events) {
|
|
8070
|
+
const syncedCalendar = await this.getClinicSyncedCalendar(
|
|
8071
|
+
clinicId,
|
|
8072
|
+
calendarId
|
|
8073
|
+
);
|
|
8074
|
+
if (!syncedCalendar) {
|
|
8075
|
+
throw new Error("Synced calendar not found");
|
|
8076
|
+
}
|
|
8077
|
+
return syncEventsToGoogleCalendarUtil(
|
|
8078
|
+
this.db,
|
|
8079
|
+
"clinic",
|
|
8080
|
+
clinicId,
|
|
8081
|
+
syncedCalendar,
|
|
8082
|
+
events
|
|
8083
|
+
);
|
|
8084
|
+
}
|
|
8085
|
+
/**
|
|
8086
|
+
* Fetches events from Google Calendar for a practitioner
|
|
8087
|
+
* @param practitionerId - ID of the practitioner
|
|
8088
|
+
* @param calendarId - ID of the synced calendar
|
|
8089
|
+
* @param startDate - Start date for fetching events
|
|
8090
|
+
* @param endDate - End date for fetching events
|
|
8091
|
+
* @returns Events fetched from Google Calendar
|
|
8092
|
+
*/
|
|
8093
|
+
async fetchEventsFromPractitionerGoogleCalendar(practitionerId, calendarId, startDate, endDate) {
|
|
8094
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
8095
|
+
practitionerId,
|
|
8096
|
+
calendarId
|
|
8097
|
+
);
|
|
8098
|
+
if (!syncedCalendar) {
|
|
8099
|
+
throw new Error("Synced calendar not found");
|
|
8100
|
+
}
|
|
8101
|
+
return fetchEventsFromGoogleCalendarUtil(
|
|
8102
|
+
this.db,
|
|
8103
|
+
"practitioner",
|
|
8104
|
+
practitionerId,
|
|
8105
|
+
syncedCalendar,
|
|
8106
|
+
startDate,
|
|
8107
|
+
endDate
|
|
8108
|
+
);
|
|
8109
|
+
}
|
|
8110
|
+
/**
|
|
8111
|
+
* Fetches events from Google Calendar for a patient
|
|
8112
|
+
* @param patientId - ID of the patient
|
|
8113
|
+
* @param calendarId - ID of the synced calendar
|
|
8114
|
+
* @param startDate - Start date for fetching events
|
|
8115
|
+
* @param endDate - End date for fetching events
|
|
8116
|
+
* @returns Events fetched from Google Calendar
|
|
8117
|
+
*/
|
|
8118
|
+
async fetchEventsFromPatientGoogleCalendar(patientId, calendarId, startDate, endDate) {
|
|
8119
|
+
const syncedCalendar = await this.getPatientSyncedCalendar(
|
|
8120
|
+
patientId,
|
|
8121
|
+
calendarId
|
|
8122
|
+
);
|
|
8123
|
+
if (!syncedCalendar) {
|
|
8124
|
+
throw new Error("Synced calendar not found");
|
|
8125
|
+
}
|
|
8126
|
+
return fetchEventsFromGoogleCalendarUtil(
|
|
8127
|
+
this.db,
|
|
8128
|
+
"patient",
|
|
8129
|
+
patientId,
|
|
8130
|
+
syncedCalendar,
|
|
8131
|
+
startDate,
|
|
8132
|
+
endDate
|
|
8133
|
+
);
|
|
8134
|
+
}
|
|
8135
|
+
/**
|
|
8136
|
+
* Fetches events from Google Calendar for a clinic
|
|
8137
|
+
* @param clinicId - ID of the clinic
|
|
8138
|
+
* @param calendarId - ID of the synced calendar
|
|
8139
|
+
* @param startDate - Start date for fetching events
|
|
8140
|
+
* @param endDate - End date for fetching events
|
|
8141
|
+
* @returns Events fetched from Google Calendar
|
|
8142
|
+
*/
|
|
8143
|
+
async fetchEventsFromClinicGoogleCalendar(clinicId, calendarId, startDate, endDate) {
|
|
8144
|
+
const syncedCalendar = await this.getClinicSyncedCalendar(
|
|
8145
|
+
clinicId,
|
|
8146
|
+
calendarId
|
|
8147
|
+
);
|
|
8148
|
+
if (!syncedCalendar) {
|
|
8149
|
+
throw new Error("Synced calendar not found");
|
|
8150
|
+
}
|
|
8151
|
+
return fetchEventsFromGoogleCalendarUtil(
|
|
8152
|
+
this.db,
|
|
8153
|
+
"clinic",
|
|
8154
|
+
clinicId,
|
|
8155
|
+
syncedCalendar,
|
|
8156
|
+
startDate,
|
|
8157
|
+
endDate
|
|
8158
|
+
);
|
|
8159
|
+
}
|
|
8160
|
+
/**
|
|
8161
|
+
* Deletes an event from Google Calendar for a practitioner
|
|
8162
|
+
* @param practitionerId - ID of the practitioner
|
|
8163
|
+
* @param calendarId - ID of the synced calendar
|
|
8164
|
+
* @param eventId - ID of the event in Google Calendar
|
|
8165
|
+
* @returns Success status
|
|
8166
|
+
*/
|
|
8167
|
+
async deletePractitionerGoogleCalendarEvent(practitionerId, calendarId, eventId) {
|
|
8168
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
8169
|
+
practitionerId,
|
|
8170
|
+
calendarId
|
|
8171
|
+
);
|
|
8172
|
+
if (!syncedCalendar) {
|
|
8173
|
+
throw new Error("Synced calendar not found");
|
|
8174
|
+
}
|
|
8175
|
+
return deleteGoogleCalendarEventUtil(
|
|
8176
|
+
this.db,
|
|
8177
|
+
"practitioner",
|
|
8178
|
+
practitionerId,
|
|
8179
|
+
syncedCalendar,
|
|
8180
|
+
eventId
|
|
8181
|
+
);
|
|
8182
|
+
}
|
|
8183
|
+
/**
|
|
8184
|
+
* Deletes an event from Google Calendar for a patient
|
|
8185
|
+
* @param patientId - ID of the patient
|
|
8186
|
+
* @param calendarId - ID of the synced calendar
|
|
8187
|
+
* @param eventId - ID of the event in Google Calendar
|
|
8188
|
+
* @returns Success status
|
|
8189
|
+
*/
|
|
8190
|
+
async deletePatientGoogleCalendarEvent(patientId, calendarId, eventId) {
|
|
8191
|
+
const syncedCalendar = await this.getPatientSyncedCalendar(
|
|
8192
|
+
patientId,
|
|
8193
|
+
calendarId
|
|
8194
|
+
);
|
|
8195
|
+
if (!syncedCalendar) {
|
|
8196
|
+
throw new Error("Synced calendar not found");
|
|
8197
|
+
}
|
|
8198
|
+
return deleteGoogleCalendarEventUtil(
|
|
8199
|
+
this.db,
|
|
8200
|
+
"patient",
|
|
8201
|
+
patientId,
|
|
8202
|
+
syncedCalendar,
|
|
8203
|
+
eventId
|
|
8204
|
+
);
|
|
8205
|
+
}
|
|
8206
|
+
/**
|
|
8207
|
+
* Deletes an event from Google Calendar for a clinic
|
|
8208
|
+
* @param clinicId - ID of the clinic
|
|
8209
|
+
* @param calendarId - ID of the synced calendar
|
|
8210
|
+
* @param eventId - ID of the event in Google Calendar
|
|
8211
|
+
* @returns Success status
|
|
8212
|
+
*/
|
|
8213
|
+
async deleteClinicGoogleCalendarEvent(clinicId, calendarId, eventId) {
|
|
8214
|
+
const syncedCalendar = await this.getClinicSyncedCalendar(
|
|
8215
|
+
clinicId,
|
|
8216
|
+
calendarId
|
|
8217
|
+
);
|
|
8218
|
+
if (!syncedCalendar) {
|
|
8219
|
+
throw new Error("Synced calendar not found");
|
|
8220
|
+
}
|
|
8221
|
+
return deleteGoogleCalendarEventUtil(
|
|
8222
|
+
this.db,
|
|
8223
|
+
"clinic",
|
|
8224
|
+
clinicId,
|
|
8225
|
+
syncedCalendar,
|
|
8226
|
+
eventId
|
|
8227
|
+
);
|
|
8228
|
+
}
|
|
8229
|
+
/**
|
|
8230
|
+
* Converts Google Calendar events to our system's format for a practitioner
|
|
8231
|
+
* @param practitionerId - ID of the practitioner
|
|
8232
|
+
* @param googleEvents - Google Calendar events
|
|
8233
|
+
* @returns Converted calendar events
|
|
8234
|
+
*/
|
|
8235
|
+
convertGoogleEventsToPractitionerEvents(practitionerId, googleEvents) {
|
|
8236
|
+
return googleEvents.map(
|
|
8237
|
+
(event) => convertGoogleEventToCalendarEventUtil(
|
|
8238
|
+
event,
|
|
8239
|
+
practitionerId,
|
|
8240
|
+
"practitioner"
|
|
8241
|
+
)
|
|
8242
|
+
);
|
|
8243
|
+
}
|
|
8244
|
+
/**
|
|
8245
|
+
* Converts Google Calendar events to our system's format for a patient
|
|
8246
|
+
* @param patientId - ID of the patient
|
|
8247
|
+
* @param googleEvents - Google Calendar events
|
|
8248
|
+
* @returns Converted calendar events
|
|
8249
|
+
*/
|
|
8250
|
+
convertGoogleEventsToPatientEvents(patientId, googleEvents) {
|
|
8251
|
+
return googleEvents.map(
|
|
8252
|
+
(event) => convertGoogleEventToCalendarEventUtil(event, patientId, "patient")
|
|
8253
|
+
);
|
|
8254
|
+
}
|
|
8255
|
+
/**
|
|
8256
|
+
* Converts Google Calendar events to our system's format for a clinic
|
|
8257
|
+
* @param clinicId - ID of the clinic
|
|
8258
|
+
* @param googleEvents - Google Calendar events
|
|
8259
|
+
* @returns Converted calendar events
|
|
8260
|
+
*/
|
|
8261
|
+
convertGoogleEventsToClinicEvents(clinicId, googleEvents) {
|
|
8262
|
+
return googleEvents.map(
|
|
8263
|
+
(event) => convertGoogleEventToCalendarEventUtil(event, clinicId, "clinic")
|
|
8264
|
+
);
|
|
8265
|
+
}
|
|
8266
|
+
/**
|
|
8267
|
+
* Fetches a single event from Google Calendar for a practitioner
|
|
8268
|
+
* @param practitionerId - ID of the practitioner
|
|
8269
|
+
* @param calendarId - ID of the synced calendar
|
|
8270
|
+
* @param eventId - ID of the event in Google Calendar
|
|
8271
|
+
* @returns The event data or null if not found
|
|
8272
|
+
*/
|
|
8273
|
+
async fetchEventFromPractitionerGoogleCalendar(practitionerId, calendarId, eventId) {
|
|
8274
|
+
var _a;
|
|
8275
|
+
const syncedCalendar = await this.getPractitionerSyncedCalendar(
|
|
8276
|
+
practitionerId,
|
|
8277
|
+
calendarId
|
|
8278
|
+
);
|
|
8279
|
+
if (!syncedCalendar) {
|
|
8280
|
+
throw new Error("Synced calendar not found");
|
|
8281
|
+
}
|
|
8282
|
+
try {
|
|
8283
|
+
const { accessToken } = await refreshGoogleCalendarTokenUtil(
|
|
8284
|
+
syncedCalendar.refreshToken
|
|
8285
|
+
);
|
|
8286
|
+
const response = await makeRequest(
|
|
8287
|
+
"get",
|
|
8288
|
+
`${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
|
|
8289
|
+
{ Authorization: `Bearer ${accessToken}` }
|
|
8290
|
+
);
|
|
8291
|
+
await updateLastSyncedTimestampUtil(
|
|
8292
|
+
this.db,
|
|
8293
|
+
"practitioner",
|
|
8294
|
+
practitionerId,
|
|
8295
|
+
syncedCalendar.id
|
|
8296
|
+
);
|
|
8297
|
+
return response;
|
|
8298
|
+
} catch (error) {
|
|
8299
|
+
if (((_a = error.response) == null ? void 0 : _a.status) === 404) {
|
|
8300
|
+
return null;
|
|
8301
|
+
}
|
|
8302
|
+
console.error(
|
|
8303
|
+
`Error fetching event from Google Calendar: ${error.message}`
|
|
8304
|
+
);
|
|
8305
|
+
throw error;
|
|
8306
|
+
}
|
|
8307
|
+
}
|
|
8308
|
+
};
|
|
8309
|
+
|
|
8310
|
+
// src/services/calendar/calendar-refactored.service.ts
|
|
8311
|
+
var MIN_APPOINTMENT_DURATION2 = 15;
|
|
8312
|
+
var CalendarServiceV2 = class extends BaseService {
|
|
8313
|
+
/**
|
|
8314
|
+
* Creates a new CalendarService instance
|
|
8315
|
+
* @param db - Firestore instance
|
|
8316
|
+
* @param auth - Firebase Auth instance
|
|
8317
|
+
* @param app - Firebase App instance
|
|
8318
|
+
*/
|
|
8319
|
+
constructor(db, auth, app) {
|
|
8320
|
+
super(db, auth, app);
|
|
8321
|
+
this.syncedCalendarsService = new SyncedCalendarsService(db, auth, app);
|
|
8322
|
+
}
|
|
8323
|
+
// #region Public API Methods
|
|
8324
|
+
/**
|
|
8325
|
+
* Creates a new appointment with proper validation and scheduling rules
|
|
8326
|
+
* @param params - Appointment creation parameters
|
|
8327
|
+
* @returns Created calendar event
|
|
8328
|
+
*/
|
|
8329
|
+
async createAppointment(params) {
|
|
8330
|
+
await this.validateAppointmentParams(params);
|
|
8331
|
+
await this.validateClinicWorkingHours(params.clinicId, params.eventTime);
|
|
8332
|
+
await this.validateDoctorAvailability(
|
|
8333
|
+
params.doctorId,
|
|
8334
|
+
params.eventTime,
|
|
8335
|
+
params.clinicId
|
|
8336
|
+
);
|
|
8337
|
+
const { clinicInfo, practitionerInfo, patientInfo } = await this.fetchProfileInfoCards(
|
|
8338
|
+
params.clinicId,
|
|
8339
|
+
params.doctorId,
|
|
8340
|
+
params.patientId
|
|
8341
|
+
);
|
|
8342
|
+
const appointmentData = {
|
|
8343
|
+
clinicBranchId: params.clinicId,
|
|
8344
|
+
clinicBranchInfo: clinicInfo,
|
|
8345
|
+
practitionerProfileId: params.doctorId,
|
|
8346
|
+
practitionerProfileInfo: practitionerInfo,
|
|
8347
|
+
patientProfileId: params.patientId,
|
|
8348
|
+
patientProfileInfo: patientInfo,
|
|
8349
|
+
procedureId: params.procedureId,
|
|
8350
|
+
eventLocation: params.eventLocation,
|
|
8351
|
+
eventName: "Appointment",
|
|
8352
|
+
// TODO: Add procedure name when procedure model is available
|
|
8353
|
+
eventTime: params.eventTime,
|
|
8354
|
+
description: params.description || "",
|
|
8355
|
+
status: "pending" /* PENDING */,
|
|
8356
|
+
syncStatus: "internal" /* INTERNAL */,
|
|
8357
|
+
eventType: "appointment" /* APPOINTMENT */
|
|
8358
|
+
};
|
|
8359
|
+
const appointment = await createAppointmentUtil(
|
|
8360
|
+
this.db,
|
|
8361
|
+
params.clinicId,
|
|
8362
|
+
params.doctorId,
|
|
8363
|
+
params.patientId,
|
|
8364
|
+
appointmentData,
|
|
8365
|
+
this.generateId.bind(this)
|
|
8366
|
+
);
|
|
8367
|
+
await this.syncAppointmentWithExternalCalendars(appointment);
|
|
8368
|
+
return appointment;
|
|
8369
|
+
}
|
|
8370
|
+
/**
|
|
8371
|
+
* Updates an existing appointment
|
|
8372
|
+
* @param params - Appointment update parameters
|
|
8373
|
+
* @returns Updated calendar event
|
|
8374
|
+
*/
|
|
8375
|
+
async updateAppointment(params) {
|
|
8376
|
+
await this.validateUpdatePermissions(params);
|
|
8377
|
+
const updateData = {
|
|
8378
|
+
eventTime: params.eventTime,
|
|
8379
|
+
description: params.description,
|
|
8380
|
+
status: params.status
|
|
8381
|
+
};
|
|
8382
|
+
const appointment = await updateAppointmentUtil(
|
|
8383
|
+
this.db,
|
|
8384
|
+
params.clinicId,
|
|
8385
|
+
params.doctorId,
|
|
8386
|
+
params.patientId,
|
|
8387
|
+
params.appointmentId,
|
|
8388
|
+
updateData
|
|
8389
|
+
);
|
|
8390
|
+
await this.syncAppointmentWithExternalCalendars(appointment);
|
|
8391
|
+
return appointment;
|
|
8392
|
+
}
|
|
8393
|
+
/**
|
|
8394
|
+
* Gets available appointment slots for a doctor at a clinic
|
|
8395
|
+
* @param clinicId - ID of the clinic
|
|
8396
|
+
* @param doctorId - ID of the doctor
|
|
8397
|
+
* @param date - Date to check availability for
|
|
8398
|
+
* @returns Array of available time slots
|
|
8399
|
+
*/
|
|
8400
|
+
async getAvailableSlots(clinicId, doctorId, date) {
|
|
8401
|
+
const workingHours = await this.getClinicWorkingHours(clinicId, date);
|
|
8402
|
+
const doctorSchedule = await this.getDoctorSchedule(doctorId, date);
|
|
8403
|
+
const existingAppointments = await this.getDoctorAppointments(
|
|
8404
|
+
doctorId,
|
|
8405
|
+
date
|
|
8406
|
+
);
|
|
8407
|
+
return this.calculateAvailableSlots(
|
|
8408
|
+
workingHours,
|
|
8409
|
+
doctorSchedule,
|
|
8410
|
+
existingAppointments
|
|
8411
|
+
);
|
|
8412
|
+
}
|
|
8413
|
+
/**
|
|
8414
|
+
* Confirms an appointment
|
|
8415
|
+
* @param appointmentId - ID of the appointment
|
|
8416
|
+
* @param clinicId - ID of the clinic
|
|
8417
|
+
* @returns Confirmed calendar event
|
|
8418
|
+
*/
|
|
8419
|
+
async confirmAppointment(appointmentId, clinicId) {
|
|
8420
|
+
return this.updateAppointmentStatus(
|
|
8421
|
+
appointmentId,
|
|
8422
|
+
clinicId,
|
|
8423
|
+
"confirmed" /* CONFIRMED */
|
|
8424
|
+
);
|
|
8425
|
+
}
|
|
8426
|
+
/**
|
|
8427
|
+
* Rejects an appointment
|
|
8428
|
+
* @param appointmentId - ID of the appointment
|
|
8429
|
+
* @param clinicId - ID of the clinic
|
|
8430
|
+
* @returns Rejected calendar event
|
|
8431
|
+
*/
|
|
8432
|
+
async rejectAppointment(appointmentId, clinicId) {
|
|
8433
|
+
return this.updateAppointmentStatus(
|
|
8434
|
+
appointmentId,
|
|
8435
|
+
clinicId,
|
|
8436
|
+
"rejected" /* REJECTED */
|
|
8437
|
+
);
|
|
8438
|
+
}
|
|
8439
|
+
/**
|
|
8440
|
+
* Cancels an appointment
|
|
8441
|
+
* @param appointmentId - ID of the appointment
|
|
8442
|
+
* @param clinicId - ID of the clinic
|
|
8443
|
+
* @returns Canceled calendar event
|
|
8444
|
+
*/
|
|
8445
|
+
async cancelAppointment(appointmentId, clinicId) {
|
|
8446
|
+
return this.updateAppointmentStatus(
|
|
8447
|
+
appointmentId,
|
|
8448
|
+
clinicId,
|
|
8449
|
+
"canceled" /* CANCELED */
|
|
8450
|
+
);
|
|
8451
|
+
}
|
|
8452
|
+
/**
|
|
8453
|
+
* Imports events from external calendars
|
|
8454
|
+
* @param entityType - Type of entity (practitioner or patient)
|
|
8455
|
+
* @param entityId - ID of the entity
|
|
8456
|
+
* @param startDate - Start date for fetching events
|
|
8457
|
+
* @param endDate - End date for fetching events
|
|
8458
|
+
* @returns Number of events imported
|
|
8459
|
+
*/
|
|
8460
|
+
async importEventsFromExternalCalendars(entityType, entityId, startDate, endDate) {
|
|
8461
|
+
if (entityType === "patient") {
|
|
8462
|
+
return 0;
|
|
8463
|
+
}
|
|
8464
|
+
const syncedCalendars = await this.syncedCalendarsService.getPractitionerSyncedCalendars(
|
|
8465
|
+
entityId
|
|
8466
|
+
);
|
|
8467
|
+
const activeCalendars = syncedCalendars.filter((cal) => cal.isActive);
|
|
8468
|
+
if (activeCalendars.length === 0) {
|
|
8469
|
+
return 0;
|
|
8470
|
+
}
|
|
8471
|
+
let importedEventsCount = 0;
|
|
8472
|
+
const currentTime = Timestamp23.now();
|
|
8473
|
+
for (const calendar of activeCalendars) {
|
|
8474
|
+
try {
|
|
8475
|
+
let externalEvents = [];
|
|
8476
|
+
if (calendar.provider === "google" /* GOOGLE */) {
|
|
8477
|
+
externalEvents = await this.syncedCalendarsService.fetchEventsFromPractitionerGoogleCalendar(
|
|
8478
|
+
entityId,
|
|
8479
|
+
calendar.id,
|
|
8480
|
+
startDate,
|
|
8481
|
+
endDate
|
|
8482
|
+
);
|
|
8483
|
+
}
|
|
8484
|
+
for (const externalEvent of externalEvents) {
|
|
8485
|
+
try {
|
|
8486
|
+
const convertedEvent = this.syncedCalendarsService.convertGoogleEventsToPractitionerEvents(
|
|
8487
|
+
entityId,
|
|
8488
|
+
[externalEvent]
|
|
8489
|
+
)[0];
|
|
8490
|
+
if (!convertedEvent.eventTime) {
|
|
8491
|
+
continue;
|
|
8492
|
+
}
|
|
8493
|
+
const eventData = {
|
|
8494
|
+
// Ensure all required fields are set
|
|
8495
|
+
eventName: convertedEvent.eventName || "External Event",
|
|
8496
|
+
eventTime: convertedEvent.eventTime,
|
|
8497
|
+
description: convertedEvent.description || "",
|
|
8498
|
+
status: "confirmed" /* CONFIRMED */,
|
|
8499
|
+
syncStatus: "external" /* EXTERNAL */,
|
|
8500
|
+
eventType: "blocking" /* BLOCKING */,
|
|
8501
|
+
practitionerProfileId: entityId,
|
|
8502
|
+
syncedCalendarEventId: [
|
|
8503
|
+
{
|
|
8504
|
+
eventId: externalEvent.id,
|
|
8505
|
+
syncedCalendarProvider: calendar.provider,
|
|
8506
|
+
syncedAt: currentTime
|
|
8507
|
+
}
|
|
8508
|
+
]
|
|
8509
|
+
};
|
|
8510
|
+
const doctorEvent = await this.createDoctorBlockingEvent(
|
|
8511
|
+
entityId,
|
|
8512
|
+
eventData
|
|
8513
|
+
);
|
|
8514
|
+
if (doctorEvent) {
|
|
8515
|
+
importedEventsCount++;
|
|
8516
|
+
}
|
|
8517
|
+
} catch (eventError) {
|
|
8518
|
+
console.error("Error importing event:", eventError);
|
|
8519
|
+
}
|
|
8520
|
+
}
|
|
8521
|
+
} catch (calendarError) {
|
|
8522
|
+
console.error(
|
|
8523
|
+
`Error fetching events from calendar ${calendar.id}:`,
|
|
8524
|
+
calendarError
|
|
8525
|
+
);
|
|
8526
|
+
}
|
|
8527
|
+
}
|
|
8528
|
+
return importedEventsCount;
|
|
8529
|
+
}
|
|
8530
|
+
/**
|
|
8531
|
+
* Creates a blocking event in a doctor's calendar
|
|
8532
|
+
* @param doctorId - ID of the doctor
|
|
8533
|
+
* @param eventData - Calendar event data
|
|
8534
|
+
* @returns Created calendar event
|
|
8535
|
+
*/
|
|
8536
|
+
async createDoctorBlockingEvent(doctorId, eventData) {
|
|
8537
|
+
try {
|
|
8538
|
+
const eventId = this.generateId();
|
|
8539
|
+
const eventRef = doc19(
|
|
8540
|
+
this.db,
|
|
8541
|
+
PRACTITIONERS_COLLECTION,
|
|
8542
|
+
doctorId,
|
|
8543
|
+
CALENDAR_COLLECTION,
|
|
8544
|
+
eventId
|
|
8545
|
+
);
|
|
8546
|
+
const newEvent = {
|
|
8547
|
+
id: eventId,
|
|
8548
|
+
...eventData,
|
|
8549
|
+
createdAt: serverTimestamp18(),
|
|
8550
|
+
updatedAt: serverTimestamp18()
|
|
8551
|
+
};
|
|
8552
|
+
await setDoc19(eventRef, newEvent);
|
|
8553
|
+
return {
|
|
8554
|
+
...newEvent,
|
|
8555
|
+
createdAt: Timestamp23.now(),
|
|
8556
|
+
updatedAt: Timestamp23.now()
|
|
8557
|
+
};
|
|
8558
|
+
} catch (error) {
|
|
8559
|
+
console.error(
|
|
8560
|
+
`Error creating blocking event for doctor ${doctorId}:`,
|
|
8561
|
+
error
|
|
8562
|
+
);
|
|
8563
|
+
return null;
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
8566
|
+
/**
|
|
8567
|
+
* Periodically syncs events from external calendars for doctors
|
|
8568
|
+
* This would be called via a scheduled Cloud Function
|
|
8569
|
+
* @param lookbackDays - Number of days to look back for events
|
|
8570
|
+
* @param lookforwardDays - Number of days to look forward for events
|
|
8571
|
+
*/
|
|
8572
|
+
async synchronizeExternalCalendars(lookbackDays = 7, lookforwardDays = 30) {
|
|
8573
|
+
try {
|
|
8574
|
+
const practitionersRef = collection17(this.db, PRACTITIONERS_COLLECTION);
|
|
8575
|
+
const practitionersSnapshot = await getDocs16(practitionersRef);
|
|
8576
|
+
const startDate = /* @__PURE__ */ new Date();
|
|
8577
|
+
startDate.setDate(startDate.getDate() - lookbackDays);
|
|
8578
|
+
const endDate = /* @__PURE__ */ new Date();
|
|
8579
|
+
endDate.setDate(endDate.getDate() + lookforwardDays);
|
|
8580
|
+
const syncPromises = [];
|
|
8581
|
+
for (const docSnapshot of practitionersSnapshot.docs) {
|
|
8582
|
+
const practitionerId = docSnapshot.id;
|
|
8583
|
+
syncPromises.push(
|
|
8584
|
+
this.importEventsFromExternalCalendars(
|
|
8585
|
+
"doctor",
|
|
8586
|
+
practitionerId,
|
|
8587
|
+
startDate,
|
|
8588
|
+
endDate
|
|
8589
|
+
).then((count) => {
|
|
8590
|
+
console.log(
|
|
8591
|
+
`Imported ${count} events for doctor ${practitionerId}`
|
|
8592
|
+
);
|
|
8593
|
+
}).catch((error) => {
|
|
8594
|
+
console.error(
|
|
8595
|
+
`Error importing events for doctor ${practitionerId}:`,
|
|
8596
|
+
error
|
|
8597
|
+
);
|
|
8598
|
+
})
|
|
8599
|
+
);
|
|
8600
|
+
syncPromises.push(
|
|
8601
|
+
this.updateExistingEventsFromExternalCalendars(
|
|
8602
|
+
practitionerId,
|
|
8603
|
+
startDate,
|
|
8604
|
+
endDate
|
|
8605
|
+
).then((count) => {
|
|
8606
|
+
console.log(
|
|
8607
|
+
`Updated ${count} events for doctor ${practitionerId}`
|
|
8608
|
+
);
|
|
8609
|
+
}).catch((error) => {
|
|
8610
|
+
console.error(
|
|
8611
|
+
`Error updating events for doctor ${practitionerId}:`,
|
|
8612
|
+
error
|
|
8613
|
+
);
|
|
8614
|
+
})
|
|
8615
|
+
);
|
|
8616
|
+
}
|
|
8617
|
+
await Promise.all(syncPromises);
|
|
8618
|
+
console.log("Completed external calendar synchronization");
|
|
8619
|
+
} catch (error) {
|
|
8620
|
+
console.error("Error synchronizing external calendars:", error);
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
/**
|
|
8624
|
+
* Updates existing events that were synced from external calendars
|
|
8625
|
+
* @param doctorId - ID of the doctor
|
|
8626
|
+
* @param startDate - Start date for fetching events
|
|
8627
|
+
* @param endDate - End date for fetching events
|
|
8628
|
+
* @returns Number of events updated
|
|
8629
|
+
*/
|
|
8630
|
+
async updateExistingEventsFromExternalCalendars(doctorId, startDate, endDate) {
|
|
8631
|
+
var _a;
|
|
8632
|
+
try {
|
|
8633
|
+
const eventsRef = collection17(
|
|
8634
|
+
this.db,
|
|
8635
|
+
PRACTITIONERS_COLLECTION,
|
|
8636
|
+
doctorId,
|
|
8637
|
+
CALENDAR_COLLECTION
|
|
8638
|
+
);
|
|
8639
|
+
const q = query16(
|
|
8640
|
+
eventsRef,
|
|
8641
|
+
where16("syncStatus", "==", "external" /* EXTERNAL */),
|
|
8642
|
+
where16("eventTime.start", ">=", Timestamp23.fromDate(startDate)),
|
|
8643
|
+
where16("eventTime.start", "<=", Timestamp23.fromDate(endDate))
|
|
8644
|
+
);
|
|
8645
|
+
const eventsSnapshot = await getDocs16(q);
|
|
8646
|
+
const events = eventsSnapshot.docs.map((doc20) => ({
|
|
8647
|
+
id: doc20.id,
|
|
8648
|
+
...doc20.data()
|
|
8649
|
+
}));
|
|
8650
|
+
const calendars = await this.syncedCalendarsService.getPractitionerSyncedCalendars(
|
|
8651
|
+
doctorId
|
|
8652
|
+
);
|
|
8653
|
+
const activeCalendars = calendars.filter((cal) => cal.isActive);
|
|
8654
|
+
if (activeCalendars.length === 0 || events.length === 0) {
|
|
8655
|
+
return 0;
|
|
8656
|
+
}
|
|
8657
|
+
let updatedCount = 0;
|
|
8658
|
+
for (const event of events) {
|
|
8659
|
+
if (!((_a = event.syncedCalendarEventId) == null ? void 0 : _a.length)) continue;
|
|
8660
|
+
for (const syncId of event.syncedCalendarEventId) {
|
|
8661
|
+
const calendar = activeCalendars.find(
|
|
8662
|
+
(cal) => cal.provider === syncId.syncedCalendarProvider
|
|
8663
|
+
);
|
|
8664
|
+
if (!calendar) continue;
|
|
8665
|
+
if (syncId.syncedCalendarProvider === "google" /* GOOGLE */) {
|
|
8666
|
+
try {
|
|
8667
|
+
const externalEvent = await this.fetchExternalEvent(
|
|
8668
|
+
doctorId,
|
|
8669
|
+
calendar,
|
|
8670
|
+
syncId.eventId
|
|
8671
|
+
);
|
|
8672
|
+
if (externalEvent) {
|
|
8673
|
+
const externalStartTime = new Date(
|
|
8674
|
+
externalEvent.start.dateTime || externalEvent.start.date
|
|
8675
|
+
).getTime();
|
|
8676
|
+
const externalEndTime = new Date(
|
|
8677
|
+
externalEvent.end.dateTime || externalEvent.end.date
|
|
8678
|
+
).getTime();
|
|
8679
|
+
const localStartTime = event.eventTime.start.toDate().getTime();
|
|
8680
|
+
const localEndTime = event.eventTime.end.toDate().getTime();
|
|
8681
|
+
if (externalStartTime !== localStartTime || externalEndTime !== localEndTime || externalEvent.summary !== event.eventName || externalEvent.description !== event.description) {
|
|
8682
|
+
await this.updateLocalEventFromExternal(
|
|
8683
|
+
doctorId,
|
|
8684
|
+
event.id,
|
|
8685
|
+
externalEvent
|
|
8686
|
+
);
|
|
8687
|
+
updatedCount++;
|
|
8688
|
+
}
|
|
8689
|
+
} else {
|
|
8690
|
+
await this.updateEventStatus(
|
|
8691
|
+
doctorId,
|
|
8692
|
+
event.id,
|
|
8693
|
+
"canceled" /* CANCELED */
|
|
8694
|
+
);
|
|
8695
|
+
updatedCount++;
|
|
8696
|
+
}
|
|
8697
|
+
} catch (error) {
|
|
8698
|
+
console.error(
|
|
8699
|
+
`Error updating external event ${event.id}:`,
|
|
8700
|
+
error
|
|
8701
|
+
);
|
|
8702
|
+
}
|
|
8703
|
+
}
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8706
|
+
return updatedCount;
|
|
8707
|
+
} catch (error) {
|
|
8708
|
+
console.error(
|
|
8709
|
+
"Error updating existing events from external calendars:",
|
|
8710
|
+
error
|
|
8711
|
+
);
|
|
8712
|
+
return 0;
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
/**
|
|
8716
|
+
* Fetches a single external event from Google Calendar
|
|
8717
|
+
* @param doctorId - ID of the doctor
|
|
8718
|
+
* @param calendar - Calendar information
|
|
8719
|
+
* @param externalEventId - ID of the external event
|
|
8720
|
+
* @returns External event data or null if not found
|
|
8721
|
+
*/
|
|
8722
|
+
async fetchExternalEvent(doctorId, calendar, externalEventId) {
|
|
8723
|
+
try {
|
|
8724
|
+
if (calendar.provider === "google" /* GOOGLE */) {
|
|
8725
|
+
const result = await this.syncedCalendarsService.fetchEventFromPractitionerGoogleCalendar(
|
|
8726
|
+
doctorId,
|
|
8727
|
+
calendar.id,
|
|
8728
|
+
externalEventId
|
|
8729
|
+
);
|
|
8730
|
+
return result;
|
|
8731
|
+
}
|
|
8732
|
+
return null;
|
|
8733
|
+
} catch (error) {
|
|
8734
|
+
console.error(`Error fetching external event ${externalEventId}:`, error);
|
|
8735
|
+
return null;
|
|
8736
|
+
}
|
|
8737
|
+
}
|
|
8738
|
+
/**
|
|
8739
|
+
* Updates a local event with data from an external event
|
|
8740
|
+
* @param doctorId - ID of the doctor
|
|
8741
|
+
* @param eventId - ID of the local event
|
|
8742
|
+
* @param externalEvent - External event data
|
|
8743
|
+
*/
|
|
8744
|
+
async updateLocalEventFromExternal(doctorId, eventId, externalEvent) {
|
|
8745
|
+
try {
|
|
8746
|
+
const startTime = new Date(
|
|
8747
|
+
externalEvent.start.dateTime || externalEvent.start.date
|
|
8748
|
+
);
|
|
8749
|
+
const endTime = new Date(
|
|
8750
|
+
externalEvent.end.dateTime || externalEvent.end.date
|
|
8751
|
+
);
|
|
8752
|
+
const eventRef = doc19(
|
|
8753
|
+
this.db,
|
|
8754
|
+
PRACTITIONERS_COLLECTION,
|
|
8755
|
+
doctorId,
|
|
8756
|
+
CALENDAR_COLLECTION,
|
|
8757
|
+
eventId
|
|
8758
|
+
);
|
|
8759
|
+
await updateDoc20(eventRef, {
|
|
8760
|
+
eventName: externalEvent.summary || "External Event",
|
|
8761
|
+
eventTime: {
|
|
8762
|
+
start: Timestamp23.fromDate(startTime),
|
|
8763
|
+
end: Timestamp23.fromDate(endTime)
|
|
8764
|
+
},
|
|
8765
|
+
description: externalEvent.description || "",
|
|
8766
|
+
updatedAt: serverTimestamp18()
|
|
8767
|
+
});
|
|
8768
|
+
console.log(`Updated local event ${eventId} from external event`);
|
|
8769
|
+
} catch (error) {
|
|
8770
|
+
console.error(
|
|
8771
|
+
`Error updating local event ${eventId} from external:`,
|
|
8772
|
+
error
|
|
8773
|
+
);
|
|
8774
|
+
}
|
|
8775
|
+
}
|
|
8776
|
+
/**
|
|
8777
|
+
* Updates an event's status
|
|
8778
|
+
* @param doctorId - ID of the doctor
|
|
8779
|
+
* @param eventId - ID of the event
|
|
8780
|
+
* @param status - New status
|
|
8781
|
+
*/
|
|
8782
|
+
async updateEventStatus(doctorId, eventId, status) {
|
|
8783
|
+
try {
|
|
8784
|
+
const eventRef = doc19(
|
|
8785
|
+
this.db,
|
|
8786
|
+
PRACTITIONERS_COLLECTION,
|
|
8787
|
+
doctorId,
|
|
8788
|
+
CALENDAR_COLLECTION,
|
|
8789
|
+
eventId
|
|
8790
|
+
);
|
|
8791
|
+
await updateDoc20(eventRef, {
|
|
8792
|
+
status,
|
|
8793
|
+
updatedAt: serverTimestamp18()
|
|
8794
|
+
});
|
|
8795
|
+
console.log(`Updated event ${eventId} status to ${status}`);
|
|
8796
|
+
} catch (error) {
|
|
8797
|
+
console.error(`Error updating event ${eventId} status:`, error);
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
/**
|
|
8801
|
+
* Creates a scheduled job to periodically sync external calendars
|
|
8802
|
+
* Note: This would be implemented using Cloud Functions in a real application
|
|
8803
|
+
* This is a sample implementation to show how it could be set up
|
|
8804
|
+
* @param interval - Interval in hours
|
|
8805
|
+
*/
|
|
8806
|
+
createScheduledSyncJob(interval = 3) {
|
|
8807
|
+
console.log(
|
|
8808
|
+
`Setting up scheduled calendar sync job every ${interval} hours`
|
|
8809
|
+
);
|
|
8810
|
+
}
|
|
8811
|
+
// #endregion
|
|
8812
|
+
// #region Private Helper Methods
|
|
8813
|
+
/**
|
|
8814
|
+
* Validates appointment creation parameters
|
|
8815
|
+
* @param params - Appointment parameters to validate
|
|
8816
|
+
* @throws Error if validation fails
|
|
8817
|
+
*/
|
|
8818
|
+
async validateAppointmentParams(params) {
|
|
8819
|
+
await createAppointmentSchema.parseAsync(params);
|
|
8820
|
+
}
|
|
8821
|
+
/**
|
|
8822
|
+
* Validates if the event time falls within clinic working hours
|
|
8823
|
+
* @param clinicId - ID of the clinic
|
|
8824
|
+
* @param eventTime - Event time to validate
|
|
8825
|
+
* @throws Error if validation fails
|
|
8826
|
+
*/
|
|
8827
|
+
async validateClinicWorkingHours(clinicId, eventTime) {
|
|
8828
|
+
const startDate = eventTime.start.toDate();
|
|
8829
|
+
const workingHours = await this.getClinicWorkingHours(clinicId, startDate);
|
|
8830
|
+
if (workingHours.length === 0) {
|
|
8831
|
+
throw new Error("Clinic is not open on this day");
|
|
8832
|
+
}
|
|
8833
|
+
const startTime = startDate;
|
|
8834
|
+
const endTime = eventTime.end.toDate();
|
|
8835
|
+
const isWithinWorkingHours = workingHours.some((slot) => {
|
|
8836
|
+
return slot.start <= startTime && slot.end >= endTime && slot.isAvailable;
|
|
8837
|
+
});
|
|
8838
|
+
if (!isWithinWorkingHours) {
|
|
8839
|
+
throw new Error("Appointment time is outside clinic working hours");
|
|
8840
|
+
}
|
|
8841
|
+
}
|
|
8842
|
+
/**
|
|
8843
|
+
* Validates if the doctor is available during the event time
|
|
8844
|
+
* @param doctorId - ID of the doctor
|
|
8845
|
+
* @param eventTime - Event time to validate
|
|
8846
|
+
* @param clinicId - ID of the clinic where the appointment is being booked
|
|
8847
|
+
* @throws Error if validation fails
|
|
8848
|
+
*/
|
|
8849
|
+
async validateDoctorAvailability(doctorId, eventTime, clinicId) {
|
|
8850
|
+
var _a;
|
|
8851
|
+
const startDate = eventTime.start.toDate();
|
|
8852
|
+
const startTime = startDate;
|
|
8853
|
+
const endTime = eventTime.end.toDate();
|
|
8854
|
+
const practitionerRef = doc19(this.db, PRACTITIONERS_COLLECTION, doctorId);
|
|
8855
|
+
const practitionerDoc = await getDoc22(practitionerRef);
|
|
8856
|
+
if (!practitionerDoc.exists()) {
|
|
8857
|
+
throw new Error(`Doctor with ID ${doctorId} not found`);
|
|
8858
|
+
}
|
|
8859
|
+
const practitioner = practitionerDoc.data();
|
|
8860
|
+
if (!practitioner.clinics.includes(clinicId)) {
|
|
8861
|
+
throw new Error("Doctor does not work at this clinic");
|
|
8862
|
+
}
|
|
8863
|
+
const clinicWorkingHours = (_a = practitioner.clinicWorkingHours) == null ? void 0 : _a.find(
|
|
8864
|
+
(hours) => hours.clinicId === clinicId && hours.isActive
|
|
8865
|
+
);
|
|
8866
|
+
if (!clinicWorkingHours) {
|
|
8867
|
+
throw new Error("Doctor does not have working hours set for this clinic");
|
|
8868
|
+
}
|
|
8869
|
+
const dayOfWeek = startDate.getDay();
|
|
8870
|
+
const dayKey = [
|
|
8871
|
+
"sunday",
|
|
8872
|
+
"monday",
|
|
8873
|
+
"tuesday",
|
|
8874
|
+
"wednesday",
|
|
8875
|
+
"thursday",
|
|
8876
|
+
"friday",
|
|
8877
|
+
"saturday"
|
|
8878
|
+
][dayOfWeek];
|
|
8879
|
+
const daySchedule = clinicWorkingHours.workingHours[dayKey];
|
|
8880
|
+
if (!daySchedule) {
|
|
8881
|
+
throw new Error("Doctor is not working on this day at this clinic");
|
|
8882
|
+
}
|
|
8883
|
+
const [startHour, startMinute] = daySchedule.start.split(":").map(Number);
|
|
8884
|
+
const [endHour, endMinute] = daySchedule.end.split(":").map(Number);
|
|
8885
|
+
const scheduleStart = new Date(startDate);
|
|
8886
|
+
scheduleStart.setHours(startHour, startMinute, 0, 0);
|
|
8887
|
+
const scheduleEnd = new Date(startDate);
|
|
8888
|
+
scheduleEnd.setHours(endHour, endMinute, 0, 0);
|
|
8889
|
+
if (startTime < scheduleStart || endTime > scheduleEnd) {
|
|
8890
|
+
throw new Error(
|
|
8891
|
+
"Appointment time is outside doctor's working hours at this clinic"
|
|
8892
|
+
);
|
|
8893
|
+
}
|
|
8894
|
+
const appointments = await this.getDoctorAppointments(doctorId, startDate);
|
|
8895
|
+
const hasOverlap = appointments.some((appointment) => {
|
|
8896
|
+
const appointmentStart = appointment.eventTime.start.toDate();
|
|
8897
|
+
const appointmentEnd = appointment.eventTime.end.toDate();
|
|
8898
|
+
return startTime >= appointmentStart && startTime < appointmentEnd || endTime > appointmentStart && endTime <= appointmentEnd || startTime <= appointmentStart && endTime >= appointmentEnd;
|
|
8899
|
+
});
|
|
8900
|
+
if (hasOverlap) {
|
|
8901
|
+
throw new Error("Doctor has another appointment during this time");
|
|
8902
|
+
}
|
|
8903
|
+
}
|
|
8904
|
+
/**
|
|
8905
|
+
* Updates appointment status
|
|
8906
|
+
* @param appointmentId - ID of the appointment
|
|
8907
|
+
* @param clinicId - ID of the clinic
|
|
8908
|
+
* @param status - New status
|
|
8909
|
+
* @returns Updated calendar event
|
|
8910
|
+
*/
|
|
8911
|
+
async updateAppointmentStatus(appointmentId, clinicId, status) {
|
|
8912
|
+
const appointmentRef = doc19(this.db, CALENDAR_COLLECTION, appointmentId);
|
|
8913
|
+
const appointmentDoc = await getDoc22(appointmentRef);
|
|
8914
|
+
if (!appointmentDoc.exists()) {
|
|
8915
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
8916
|
+
}
|
|
8917
|
+
const appointment = appointmentDoc.data();
|
|
8918
|
+
if (appointment.clinicBranchId !== clinicId) {
|
|
8919
|
+
throw new Error("Appointment does not belong to the specified clinic");
|
|
8920
|
+
}
|
|
8921
|
+
this.validateStatusTransition(appointment.status, status);
|
|
8922
|
+
const updateParams = {
|
|
8923
|
+
appointmentId,
|
|
8924
|
+
clinicId,
|
|
8925
|
+
doctorId: appointment.practitionerProfileId || "",
|
|
8926
|
+
patientId: appointment.patientProfileId || "",
|
|
8927
|
+
status
|
|
8928
|
+
};
|
|
8929
|
+
await this.validateUpdatePermissions(updateParams);
|
|
8930
|
+
return this.updateAppointment(updateParams);
|
|
8931
|
+
}
|
|
8932
|
+
/**
|
|
8933
|
+
* Validates status transition
|
|
8934
|
+
* @param currentStatus - Current status
|
|
8935
|
+
* @param newStatus - New status
|
|
8936
|
+
* @throws Error if transition is invalid
|
|
8937
|
+
*/
|
|
8938
|
+
validateStatusTransition(currentStatus, newStatus) {
|
|
8939
|
+
const validTransitions = {
|
|
8940
|
+
["pending" /* PENDING */]: [
|
|
8941
|
+
"confirmed" /* CONFIRMED */,
|
|
8942
|
+
"rejected" /* REJECTED */,
|
|
8943
|
+
"canceled" /* CANCELED */
|
|
8944
|
+
],
|
|
8945
|
+
["confirmed" /* CONFIRMED */]: [
|
|
8946
|
+
"canceled" /* CANCELED */,
|
|
8947
|
+
"completed" /* COMPLETED */,
|
|
8948
|
+
"rescheduled" /* RESCHEDULED */
|
|
8949
|
+
],
|
|
8950
|
+
["rejected" /* REJECTED */]: [],
|
|
8951
|
+
["canceled" /* CANCELED */]: [],
|
|
8952
|
+
["rescheduled" /* RESCHEDULED */]: [
|
|
8953
|
+
"confirmed" /* CONFIRMED */,
|
|
8954
|
+
"canceled" /* CANCELED */
|
|
8955
|
+
],
|
|
8956
|
+
["completed" /* COMPLETED */]: []
|
|
8957
|
+
};
|
|
8958
|
+
if (!validTransitions[currentStatus].includes(newStatus)) {
|
|
8959
|
+
throw new Error(
|
|
8960
|
+
`Invalid status transition from ${currentStatus} to ${newStatus}`
|
|
8961
|
+
);
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8964
|
+
/**
|
|
8965
|
+
* Syncs appointment with external calendars based on entity type and status
|
|
8966
|
+
* @param appointment - Calendar event to sync
|
|
8967
|
+
*/
|
|
8968
|
+
async syncAppointmentWithExternalCalendars(appointment) {
|
|
8969
|
+
if (!appointment.practitionerProfileId || !appointment.patientProfileId) {
|
|
8970
|
+
return;
|
|
8971
|
+
}
|
|
8972
|
+
try {
|
|
8973
|
+
const [doctorCalendars, patientCalendars] = await Promise.all([
|
|
8974
|
+
this.syncedCalendarsService.getPractitionerSyncedCalendars(
|
|
8975
|
+
appointment.practitionerProfileId
|
|
8976
|
+
),
|
|
8977
|
+
this.syncedCalendarsService.getPatientSyncedCalendars(
|
|
8978
|
+
appointment.patientProfileId
|
|
8979
|
+
)
|
|
8980
|
+
]);
|
|
8981
|
+
const activeDoctorCalendars = doctorCalendars.filter(
|
|
8982
|
+
(cal) => cal.isActive
|
|
8983
|
+
);
|
|
8984
|
+
const activePatientCalendars = patientCalendars.filter(
|
|
8985
|
+
(cal) => cal.isActive
|
|
8986
|
+
);
|
|
8987
|
+
if (activeDoctorCalendars.length === 0 && activePatientCalendars.length === 0) {
|
|
8988
|
+
return;
|
|
8989
|
+
}
|
|
8990
|
+
if (appointment.syncStatus !== "internal" /* INTERNAL */) {
|
|
8991
|
+
return;
|
|
8992
|
+
}
|
|
8993
|
+
if (appointment.status === "confirmed" /* CONFIRMED */ && activeDoctorCalendars.length > 0) {
|
|
8994
|
+
await Promise.all(
|
|
8995
|
+
activeDoctorCalendars.map(
|
|
8996
|
+
(calendar) => this.syncEventToExternalCalendar(appointment, calendar, "doctor")
|
|
8997
|
+
)
|
|
8998
|
+
);
|
|
8999
|
+
}
|
|
9000
|
+
if (appointment.status !== "canceled" /* CANCELED */ && appointment.status !== "rejected" /* REJECTED */ && activePatientCalendars.length > 0) {
|
|
9001
|
+
await Promise.all(
|
|
9002
|
+
activePatientCalendars.map(
|
|
9003
|
+
(calendar) => this.syncEventToExternalCalendar(appointment, calendar, "patient")
|
|
9004
|
+
)
|
|
9005
|
+
);
|
|
9006
|
+
}
|
|
9007
|
+
} catch (error) {
|
|
9008
|
+
console.error("Error syncing with external calendars:", error);
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
/**
|
|
9012
|
+
* Syncs a single event to an external calendar
|
|
9013
|
+
* @param appointment - Calendar event to sync
|
|
9014
|
+
* @param calendar - External calendar to sync with
|
|
9015
|
+
* @param entityType - Type of entity owning the calendar
|
|
9016
|
+
*/
|
|
9017
|
+
async syncEventToExternalCalendar(appointment, calendar, entityType) {
|
|
9018
|
+
var _a, _b, _c, _d, _e;
|
|
9019
|
+
try {
|
|
9020
|
+
const eventToSync = { ...appointment };
|
|
9021
|
+
let eventTitle = appointment.eventName;
|
|
9022
|
+
const clinicName = ((_a = appointment.clinicBranchInfo) == null ? void 0 : _a.name) || "Clinic";
|
|
9023
|
+
if (entityType === "patient") {
|
|
9024
|
+
eventTitle = `[${appointment.status}] ${eventTitle} @ ${clinicName}`;
|
|
9025
|
+
} else {
|
|
9026
|
+
eventTitle = `${eventTitle} - Patient: ${((_b = appointment.patientProfileInfo) == null ? void 0 : _b.fullName) || "Unknown"} @ ${clinicName}`;
|
|
9027
|
+
}
|
|
9028
|
+
eventToSync.eventName = eventTitle;
|
|
9029
|
+
const existingSyncId = (_d = (_c = appointment.syncedCalendarEventId) == null ? void 0 : _c.find(
|
|
9030
|
+
(sync) => sync.syncedCalendarProvider === calendar.provider
|
|
9031
|
+
)) == null ? void 0 : _d.eventId;
|
|
9032
|
+
if (calendar.provider === "google" /* GOOGLE */) {
|
|
9033
|
+
const result = await this.syncedCalendarsService.syncPractitionerEventsToGoogleCalendar(
|
|
9034
|
+
entityType === "doctor" ? appointment.practitionerProfileId : appointment.patientProfileId,
|
|
9035
|
+
calendar.id,
|
|
9036
|
+
[eventToSync],
|
|
9037
|
+
existingSyncId
|
|
9038
|
+
// Pass existing sync ID if we have one
|
|
9039
|
+
);
|
|
9040
|
+
if (result.success && ((_e = result.eventIds) == null ? void 0 : _e.length) && !existingSyncId) {
|
|
9041
|
+
const newSyncEvent = {
|
|
9042
|
+
eventId: result.eventIds[0],
|
|
9043
|
+
syncedCalendarProvider: calendar.provider,
|
|
9044
|
+
syncedAt: Timestamp23.now()
|
|
9045
|
+
};
|
|
9046
|
+
await this.updateEventWithSyncId(
|
|
9047
|
+
entityType === "doctor" ? appointment.practitionerProfileId : appointment.patientProfileId,
|
|
9048
|
+
entityType,
|
|
9049
|
+
appointment.id,
|
|
9050
|
+
newSyncEvent
|
|
9051
|
+
);
|
|
9052
|
+
}
|
|
9053
|
+
}
|
|
9054
|
+
} catch (error) {
|
|
9055
|
+
console.error(`Error syncing with ${entityType}'s calendar:`, error);
|
|
9056
|
+
}
|
|
9057
|
+
}
|
|
9058
|
+
/**
|
|
9059
|
+
* Updates an event with a new sync ID
|
|
9060
|
+
* @param entityId - ID of the entity (doctor or patient)
|
|
9061
|
+
* @param entityType - Type of entity
|
|
9062
|
+
* @param eventId - ID of the event
|
|
9063
|
+
* @param syncEvent - Sync event information
|
|
9064
|
+
*/
|
|
9065
|
+
async updateEventWithSyncId(entityId, entityType, eventId, syncEvent) {
|
|
9066
|
+
try {
|
|
9067
|
+
const collectionPath = entityType === "doctor" ? `${PRACTITIONERS_COLLECTION}/${entityId}/${CALENDAR_COLLECTION}` : `${PATIENTS_COLLECTION}/${entityId}/${CALENDAR_COLLECTION}`;
|
|
9068
|
+
const eventRef = doc19(this.db, collectionPath, eventId);
|
|
9069
|
+
const eventDoc = await getDoc22(eventRef);
|
|
9070
|
+
if (eventDoc.exists()) {
|
|
9071
|
+
const event = eventDoc.data();
|
|
9072
|
+
const syncIds = [...event.syncedCalendarEventId || []];
|
|
9073
|
+
const existingSyncIndex = syncIds.findIndex(
|
|
9074
|
+
(sync) => sync.syncedCalendarProvider === syncEvent.syncedCalendarProvider
|
|
9075
|
+
);
|
|
9076
|
+
if (existingSyncIndex >= 0) {
|
|
9077
|
+
syncIds[existingSyncIndex] = syncEvent;
|
|
9078
|
+
} else {
|
|
9079
|
+
syncIds.push(syncEvent);
|
|
9080
|
+
}
|
|
9081
|
+
await updateDoc20(eventRef, {
|
|
9082
|
+
syncedCalendarEventId: syncIds,
|
|
9083
|
+
updatedAt: serverTimestamp18()
|
|
9084
|
+
});
|
|
9085
|
+
console.log(
|
|
9086
|
+
`Updated event ${eventId} with sync ID ${syncEvent.eventId}`
|
|
9087
|
+
);
|
|
9088
|
+
}
|
|
9089
|
+
} catch (error) {
|
|
9090
|
+
console.error("Error updating event with sync ID:", error);
|
|
9091
|
+
}
|
|
9092
|
+
}
|
|
9093
|
+
/**
|
|
9094
|
+
* Validates update permissions and parameters
|
|
9095
|
+
* @param params - Update parameters to validate
|
|
9096
|
+
*/
|
|
9097
|
+
async validateUpdatePermissions(params) {
|
|
9098
|
+
await updateAppointmentSchema.parseAsync(params);
|
|
9099
|
+
}
|
|
9100
|
+
/**
|
|
9101
|
+
* Gets clinic working hours for a specific date
|
|
9102
|
+
* @param clinicId - ID of the clinic
|
|
9103
|
+
* @param date - Date to get working hours for
|
|
9104
|
+
* @returns Working hours for the clinic
|
|
9105
|
+
*/
|
|
9106
|
+
async getClinicWorkingHours(clinicId, date) {
|
|
9107
|
+
const clinicRef = doc19(this.db, CLINICS_COLLECTION, clinicId);
|
|
9108
|
+
const clinicDoc = await getDoc22(clinicRef);
|
|
9109
|
+
if (!clinicDoc.exists()) {
|
|
9110
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
9111
|
+
}
|
|
9112
|
+
const workingHours = [];
|
|
9113
|
+
const dayOfWeek = date.getDay();
|
|
9114
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
9115
|
+
return workingHours;
|
|
9116
|
+
}
|
|
9117
|
+
const workingDate = new Date(date);
|
|
9118
|
+
workingDate.setHours(9, 0, 0, 0);
|
|
9119
|
+
const startTime = new Date(workingDate);
|
|
9120
|
+
workingDate.setHours(17, 0, 0, 0);
|
|
9121
|
+
const endTime = new Date(workingDate);
|
|
9122
|
+
workingHours.push({
|
|
9123
|
+
start: startTime,
|
|
9124
|
+
end: endTime,
|
|
9125
|
+
isAvailable: true
|
|
9126
|
+
});
|
|
9127
|
+
return workingHours;
|
|
9128
|
+
}
|
|
9129
|
+
/**
|
|
9130
|
+
* Gets doctor's schedule for a specific date
|
|
9131
|
+
* @param doctorId - ID of the doctor
|
|
9132
|
+
* @param date - Date to get schedule for
|
|
9133
|
+
* @returns Doctor's schedule
|
|
9134
|
+
*/
|
|
9135
|
+
async getDoctorSchedule(doctorId, date) {
|
|
9136
|
+
const practitionerRef = doc19(this.db, PRACTITIONERS_COLLECTION, doctorId);
|
|
9137
|
+
const practitionerDoc = await getDoc22(practitionerRef);
|
|
9138
|
+
if (!practitionerDoc.exists()) {
|
|
9139
|
+
throw new Error(`Doctor with ID ${doctorId} not found`);
|
|
9140
|
+
}
|
|
9141
|
+
const schedule = [];
|
|
9142
|
+
const dayOfWeek = date.getDay();
|
|
9143
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) {
|
|
9144
|
+
return schedule;
|
|
9145
|
+
}
|
|
9146
|
+
const scheduleDate = new Date(date);
|
|
9147
|
+
scheduleDate.setHours(9, 0, 0, 0);
|
|
9148
|
+
const startTime = new Date(scheduleDate);
|
|
9149
|
+
scheduleDate.setHours(17, 0, 0, 0);
|
|
9150
|
+
const endTime = new Date(scheduleDate);
|
|
9151
|
+
schedule.push({
|
|
9152
|
+
start: startTime,
|
|
9153
|
+
end: endTime,
|
|
9154
|
+
isAvailable: true
|
|
9155
|
+
});
|
|
9156
|
+
return schedule;
|
|
9157
|
+
}
|
|
9158
|
+
/**
|
|
9159
|
+
* Gets doctor's appointments for a specific date
|
|
9160
|
+
* @param doctorId - ID of the doctor
|
|
9161
|
+
* @param date - Date to get appointments for
|
|
9162
|
+
* @returns Array of calendar events
|
|
9163
|
+
*/
|
|
9164
|
+
async getDoctorAppointments(doctorId, date) {
|
|
9165
|
+
const startOfDay = new Date(date);
|
|
9166
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
9167
|
+
const endOfDay = new Date(date);
|
|
9168
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
9169
|
+
const appointmentsRef = collection17(this.db, CALENDAR_COLLECTION);
|
|
9170
|
+
const q = query16(
|
|
9171
|
+
appointmentsRef,
|
|
9172
|
+
where16("practitionerProfileId", "==", doctorId),
|
|
9173
|
+
where16("eventTime.start", ">=", Timestamp23.fromDate(startOfDay)),
|
|
9174
|
+
where16("eventTime.start", "<=", Timestamp23.fromDate(endOfDay)),
|
|
9175
|
+
where16("status", "in", [
|
|
9176
|
+
"confirmed" /* CONFIRMED */,
|
|
9177
|
+
"pending" /* PENDING */
|
|
9178
|
+
])
|
|
9179
|
+
);
|
|
9180
|
+
const querySnapshot = await getDocs16(q);
|
|
9181
|
+
return querySnapshot.docs.map((doc20) => doc20.data());
|
|
9182
|
+
}
|
|
9183
|
+
/**
|
|
9184
|
+
* Calculates available time slots based on working hours, schedule and existing appointments
|
|
9185
|
+
* @param workingHours - Clinic working hours
|
|
9186
|
+
* @param doctorSchedule - Doctor's schedule
|
|
9187
|
+
* @param existingAppointments - Existing appointments
|
|
9188
|
+
* @returns Array of available time slots
|
|
9189
|
+
*/
|
|
9190
|
+
calculateAvailableSlots(workingHours, doctorSchedule, existingAppointments) {
|
|
9191
|
+
const availableSlots = [];
|
|
9192
|
+
for (const workingHour of workingHours) {
|
|
9193
|
+
for (const scheduleSlot of doctorSchedule) {
|
|
9194
|
+
const overlapStart = new Date(
|
|
9195
|
+
Math.max(workingHour.start.getTime(), scheduleSlot.start.getTime())
|
|
9196
|
+
);
|
|
9197
|
+
const overlapEnd = new Date(
|
|
9198
|
+
Math.min(workingHour.end.getTime(), scheduleSlot.end.getTime())
|
|
9199
|
+
);
|
|
9200
|
+
if (overlapStart < overlapEnd && workingHour.isAvailable && scheduleSlot.isAvailable) {
|
|
9201
|
+
let slotStart = new Date(overlapStart);
|
|
9202
|
+
while (slotStart < overlapEnd) {
|
|
9203
|
+
const slotEnd = new Date(
|
|
9204
|
+
slotStart.getTime() + MIN_APPOINTMENT_DURATION2 * 60 * 1e3
|
|
9205
|
+
);
|
|
9206
|
+
const hasOverlap = existingAppointments.some((appointment) => {
|
|
9207
|
+
const appointmentStart = appointment.eventTime.start.toDate();
|
|
9208
|
+
const appointmentEnd = appointment.eventTime.end.toDate();
|
|
9209
|
+
return slotStart >= appointmentStart && slotStart < appointmentEnd || slotEnd > appointmentStart && slotEnd <= appointmentEnd;
|
|
9210
|
+
});
|
|
9211
|
+
if (!hasOverlap && slotEnd <= overlapEnd) {
|
|
9212
|
+
availableSlots.push({
|
|
9213
|
+
start: new Date(slotStart),
|
|
9214
|
+
end: new Date(slotEnd),
|
|
9215
|
+
isAvailable: true
|
|
9216
|
+
});
|
|
9217
|
+
}
|
|
9218
|
+
slotStart = new Date(
|
|
9219
|
+
slotStart.getTime() + MIN_APPOINTMENT_DURATION2 * 60 * 1e3
|
|
9220
|
+
);
|
|
9221
|
+
}
|
|
9222
|
+
}
|
|
9223
|
+
}
|
|
9224
|
+
}
|
|
9225
|
+
return availableSlots;
|
|
9226
|
+
}
|
|
9227
|
+
/**
|
|
9228
|
+
* Fetches and creates info cards for clinic, doctor, and patient profiles
|
|
9229
|
+
* @param clinicId - ID of the clinic
|
|
9230
|
+
* @param doctorId - ID of the doctor
|
|
9231
|
+
* @param patientId - ID of the patient
|
|
9232
|
+
* @returns Object containing info cards for all profiles
|
|
9233
|
+
*/
|
|
9234
|
+
async fetchProfileInfoCards(clinicId, doctorId, patientId) {
|
|
9235
|
+
var _a;
|
|
9236
|
+
try {
|
|
9237
|
+
const [clinicDoc, practitionerDoc, patientDoc, patientSensitiveInfoDoc] = await Promise.all([
|
|
9238
|
+
getDoc22(doc19(this.db, CLINICS_COLLECTION, clinicId)),
|
|
9239
|
+
getDoc22(doc19(this.db, PRACTITIONERS_COLLECTION, doctorId)),
|
|
9240
|
+
getDoc22(doc19(this.db, PATIENTS_COLLECTION, patientId)),
|
|
9241
|
+
getDoc22(
|
|
9242
|
+
doc19(
|
|
9243
|
+
this.db,
|
|
9244
|
+
PATIENTS_COLLECTION,
|
|
9245
|
+
patientId,
|
|
9246
|
+
PATIENT_SENSITIVE_INFO_COLLECTION,
|
|
9247
|
+
patientId
|
|
9248
|
+
)
|
|
9249
|
+
)
|
|
9250
|
+
]);
|
|
9251
|
+
const clinicInfo = clinicDoc.exists() ? {
|
|
9252
|
+
id: clinicDoc.id,
|
|
9253
|
+
featuredPhoto: clinicDoc.data().featuredPhoto || "",
|
|
9254
|
+
name: clinicDoc.data().name,
|
|
9255
|
+
description: clinicDoc.data().description || "",
|
|
9256
|
+
location: clinicDoc.data().location,
|
|
9257
|
+
contactInfo: clinicDoc.data().contactInfo
|
|
9258
|
+
} : null;
|
|
9259
|
+
const practitionerInfo = practitionerDoc.exists() ? {
|
|
9260
|
+
id: practitionerDoc.id,
|
|
9261
|
+
practitionerPhoto: practitionerDoc.data().basicInfo.profileImageUrl || null,
|
|
9262
|
+
name: `${practitionerDoc.data().basicInfo.firstName} ${practitionerDoc.data().basicInfo.lastName}`,
|
|
9263
|
+
email: practitionerDoc.data().basicInfo.email,
|
|
9264
|
+
phone: practitionerDoc.data().basicInfo.phoneNumber || null,
|
|
9265
|
+
certification: practitionerDoc.data().certification
|
|
9266
|
+
} : null;
|
|
9267
|
+
let patientInfo = null;
|
|
9268
|
+
if (patientSensitiveInfoDoc.exists()) {
|
|
9269
|
+
const sensitiveData = patientSensitiveInfoDoc.data();
|
|
9270
|
+
patientInfo = {
|
|
9271
|
+
id: patientId,
|
|
9272
|
+
fullName: `${sensitiveData.firstName} ${sensitiveData.lastName}`,
|
|
9273
|
+
email: sensitiveData.email || "",
|
|
9274
|
+
phone: sensitiveData.phoneNumber || null,
|
|
9275
|
+
dateOfBirth: sensitiveData.dateOfBirth || Timestamp23.now(),
|
|
9276
|
+
gender: sensitiveData.gender || "other" /* OTHER */
|
|
9277
|
+
};
|
|
9278
|
+
} else if (patientDoc.exists()) {
|
|
9279
|
+
patientInfo = {
|
|
9280
|
+
id: patientDoc.id,
|
|
9281
|
+
fullName: patientDoc.data().displayName,
|
|
9282
|
+
email: ((_a = patientDoc.data().contactInfo) == null ? void 0 : _a.email) || "",
|
|
9283
|
+
phone: patientDoc.data().phoneNumber || null,
|
|
9284
|
+
dateOfBirth: patientDoc.data().dateOfBirth || Timestamp23.now(),
|
|
9285
|
+
gender: patientDoc.data().gender || "other" /* OTHER */
|
|
9286
|
+
};
|
|
9287
|
+
}
|
|
9288
|
+
return {
|
|
9289
|
+
clinicInfo,
|
|
9290
|
+
practitionerInfo,
|
|
9291
|
+
patientInfo
|
|
9292
|
+
};
|
|
9293
|
+
} catch (error) {
|
|
9294
|
+
console.error("Error fetching profile info cards:", error);
|
|
9295
|
+
return {
|
|
9296
|
+
clinicInfo: null,
|
|
9297
|
+
practitionerInfo: null,
|
|
9298
|
+
patientInfo: null
|
|
9299
|
+
};
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
// #endregion
|
|
9303
|
+
};
|
|
9304
|
+
|
|
9305
|
+
// src/validations/notification.schema.ts
|
|
9306
|
+
import { z as z18 } from "zod";
|
|
9307
|
+
var baseNotificationSchema = z18.object({
|
|
9308
|
+
id: z18.string().optional(),
|
|
9309
|
+
userId: z18.string(),
|
|
9310
|
+
notificationTime: z18.any(),
|
|
6390
9311
|
// Timestamp
|
|
6391
|
-
notificationType:
|
|
6392
|
-
notificationTokens:
|
|
6393
|
-
status:
|
|
6394
|
-
createdAt:
|
|
9312
|
+
notificationType: z18.nativeEnum(NotificationType),
|
|
9313
|
+
notificationTokens: z18.array(z18.string()),
|
|
9314
|
+
status: z18.nativeEnum(NotificationStatus),
|
|
9315
|
+
createdAt: z18.any().optional(),
|
|
6395
9316
|
// Timestamp
|
|
6396
|
-
updatedAt:
|
|
9317
|
+
updatedAt: z18.any().optional(),
|
|
6397
9318
|
// Timestamp
|
|
6398
|
-
title:
|
|
6399
|
-
body:
|
|
6400
|
-
isRead:
|
|
6401
|
-
userRole:
|
|
9319
|
+
title: z18.string(),
|
|
9320
|
+
body: z18.string(),
|
|
9321
|
+
isRead: z18.boolean(),
|
|
9322
|
+
userRole: z18.nativeEnum(UserRole)
|
|
6402
9323
|
});
|
|
6403
9324
|
var preRequirementNotificationSchema = baseNotificationSchema.extend({
|
|
6404
|
-
notificationType:
|
|
6405
|
-
treatmentId:
|
|
6406
|
-
requirements:
|
|
6407
|
-
deadline:
|
|
9325
|
+
notificationType: z18.literal("preRequirement" /* PRE_REQUIREMENT */),
|
|
9326
|
+
treatmentId: z18.string(),
|
|
9327
|
+
requirements: z18.array(z18.string()),
|
|
9328
|
+
deadline: z18.any()
|
|
6408
9329
|
// Timestamp
|
|
6409
9330
|
});
|
|
6410
9331
|
var postRequirementNotificationSchema = baseNotificationSchema.extend({
|
|
6411
|
-
notificationType:
|
|
6412
|
-
treatmentId:
|
|
6413
|
-
requirements:
|
|
6414
|
-
deadline:
|
|
9332
|
+
notificationType: z18.literal("postRequirement" /* POST_REQUIREMENT */),
|
|
9333
|
+
treatmentId: z18.string(),
|
|
9334
|
+
requirements: z18.array(z18.string()),
|
|
9335
|
+
deadline: z18.any()
|
|
6415
9336
|
// Timestamp
|
|
6416
9337
|
});
|
|
6417
9338
|
var appointmentReminderNotificationSchema = baseNotificationSchema.extend({
|
|
6418
|
-
notificationType:
|
|
6419
|
-
appointmentId:
|
|
6420
|
-
appointmentTime:
|
|
9339
|
+
notificationType: z18.literal("appointmentReminder" /* APPOINTMENT_REMINDER */),
|
|
9340
|
+
appointmentId: z18.string(),
|
|
9341
|
+
appointmentTime: z18.any(),
|
|
6421
9342
|
// Timestamp
|
|
6422
|
-
treatmentType:
|
|
6423
|
-
doctorName:
|
|
9343
|
+
treatmentType: z18.string(),
|
|
9344
|
+
doctorName: z18.string()
|
|
6424
9345
|
});
|
|
6425
9346
|
var appointmentNotificationSchema = baseNotificationSchema.extend({
|
|
6426
|
-
notificationType:
|
|
6427
|
-
appointmentId:
|
|
6428
|
-
appointmentStatus:
|
|
6429
|
-
previousStatus:
|
|
6430
|
-
reason:
|
|
9347
|
+
notificationType: z18.literal("appointmentNotification" /* APPOINTMENT_NOTIFICATION */),
|
|
9348
|
+
appointmentId: z18.string(),
|
|
9349
|
+
appointmentStatus: z18.string(),
|
|
9350
|
+
previousStatus: z18.string(),
|
|
9351
|
+
reason: z18.string().optional()
|
|
6431
9352
|
});
|
|
6432
|
-
var notificationSchema =
|
|
9353
|
+
var notificationSchema = z18.discriminatedUnion("notificationType", [
|
|
6433
9354
|
preRequirementNotificationSchema,
|
|
6434
9355
|
postRequirementNotificationSchema,
|
|
6435
9356
|
appointmentReminderNotificationSchema,
|
|
@@ -6441,9 +9362,14 @@ export {
|
|
|
6441
9362
|
AllergyType,
|
|
6442
9363
|
AuthService,
|
|
6443
9364
|
BlockingCondition,
|
|
9365
|
+
CALENDAR_COLLECTION,
|
|
6444
9366
|
CLINICS_COLLECTION,
|
|
6445
9367
|
CLINIC_ADMINS_COLLECTION,
|
|
6446
9368
|
CLINIC_GROUPS_COLLECTION,
|
|
9369
|
+
CalendarEventStatus,
|
|
9370
|
+
CalendarEventType,
|
|
9371
|
+
CalendarServiceV2,
|
|
9372
|
+
CalendarSyncStatus,
|
|
6447
9373
|
CertificationLevel,
|
|
6448
9374
|
CertificationSpecialty,
|
|
6449
9375
|
ClinicAdminService,
|
|
@@ -6482,9 +9408,15 @@ export {
|
|
|
6482
9408
|
PatientService,
|
|
6483
9409
|
PracticeType,
|
|
6484
9410
|
PractitionerService,
|
|
9411
|
+
PractitionerStatus,
|
|
9412
|
+
PractitionerTokenStatus,
|
|
6485
9413
|
PricingMeasure,
|
|
6486
9414
|
ProcedureFamily,
|
|
9415
|
+
REGISTER_TOKENS_COLLECTION,
|
|
9416
|
+
SYNCED_CALENDARS_COLLECTION,
|
|
6487
9417
|
SubscriptionModel,
|
|
9418
|
+
SyncedCalendarProvider,
|
|
9419
|
+
SyncedCalendarsService,
|
|
6488
9420
|
TreatmentBenefit,
|
|
6489
9421
|
USER_ERRORS,
|
|
6490
9422
|
UserService,
|
|
@@ -6501,6 +9433,8 @@ export {
|
|
|
6501
9433
|
appointmentReminderNotificationSchema,
|
|
6502
9434
|
baseNotificationSchema,
|
|
6503
9435
|
blockingConditionSchema,
|
|
9436
|
+
calendarEventSchema,
|
|
9437
|
+
calendarEventTimeSchema,
|
|
6504
9438
|
clinicAdminOptionsSchema,
|
|
6505
9439
|
clinicAdminSchema,
|
|
6506
9440
|
clinicAdminSignupSchema,
|
|
@@ -6516,16 +9450,20 @@ export {
|
|
|
6516
9450
|
contactPersonSchema,
|
|
6517
9451
|
contraindicationSchema,
|
|
6518
9452
|
createAdminTokenSchema,
|
|
9453
|
+
createAppointmentSchema,
|
|
9454
|
+
createCalendarEventSchema,
|
|
6519
9455
|
createClinicAdminSchema,
|
|
6520
9456
|
createClinicGroupSchema,
|
|
6521
9457
|
createClinicSchema,
|
|
6522
9458
|
createDefaultClinicGroupSchema,
|
|
6523
9459
|
createDocumentTemplateSchema,
|
|
9460
|
+
createDraftPractitionerSchema,
|
|
6524
9461
|
createPatientLocationInfoSchema,
|
|
6525
9462
|
createPatientMedicalInfoSchema,
|
|
6526
9463
|
createPatientProfileSchema,
|
|
6527
9464
|
createPatientSensitiveInfoSchema,
|
|
6528
9465
|
createPractitionerSchema,
|
|
9466
|
+
createPractitionerTokenSchema,
|
|
6529
9467
|
createUserOptionsSchema,
|
|
6530
9468
|
doctorInfoSchema,
|
|
6531
9469
|
documentElementSchema,
|
|
@@ -6547,21 +9485,31 @@ export {
|
|
|
6547
9485
|
patientDoctorSchema,
|
|
6548
9486
|
patientLocationInfoSchema,
|
|
6549
9487
|
patientMedicalInfoSchema,
|
|
9488
|
+
patientProfileInfoSchema,
|
|
6550
9489
|
patientProfileSchema,
|
|
6551
9490
|
patientSensitiveInfoSchema,
|
|
6552
9491
|
postRequirementNotificationSchema,
|
|
6553
9492
|
practitionerBasicInfoSchema,
|
|
6554
9493
|
practitionerCertificationSchema,
|
|
6555
9494
|
practitionerClinicProceduresSchema,
|
|
9495
|
+
practitionerClinicWorkingHoursSchema,
|
|
9496
|
+
practitionerProfileInfoSchema,
|
|
6556
9497
|
practitionerReviewSchema,
|
|
6557
9498
|
practitionerSchema,
|
|
9499
|
+
practitionerTokenSchema,
|
|
6558
9500
|
practitionerWorkingHoursSchema,
|
|
6559
9501
|
preRequirementNotificationSchema,
|
|
9502
|
+
procedureCategorizationSchema,
|
|
9503
|
+
procedureInfoSchema,
|
|
6560
9504
|
reviewInfoSchema,
|
|
6561
9505
|
serviceInfoSchema,
|
|
9506
|
+
syncedCalendarEventSchema,
|
|
9507
|
+
timeSlotSchema2 as timeSlotSchema,
|
|
6562
9508
|
timestampSchema,
|
|
6563
9509
|
updateAllergySchema,
|
|
9510
|
+
updateAppointmentSchema,
|
|
6564
9511
|
updateBlockingConditionSchema,
|
|
9512
|
+
updateCalendarEventSchema,
|
|
6565
9513
|
updateClinicAdminSchema,
|
|
6566
9514
|
updateClinicGroupSchema,
|
|
6567
9515
|
updateClinicSchema,
|