@blackcode_sa/metaestetics-api 1.5.31 → 1.5.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.mts +337 -1
- package/dist/admin/index.d.ts +337 -1
- package/dist/admin/index.js +597 -14
- package/dist/admin/index.mjs +595 -14
- package/dist/backoffice/index.d.mts +2 -0
- package/dist/backoffice/index.d.ts +2 -0
- package/dist/index.d.mts +102 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.js +945 -267
- package/dist/index.mjs +968 -283
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +234 -0
- package/src/admin/booking/booking.calculator.ts +686 -0
- package/src/admin/booking/booking.types.ts +56 -0
- package/src/admin/booking/index.ts +3 -0
- package/src/admin/index.ts +3 -0
- package/src/index.ts +5 -0
- package/src/services/appointment/appointment.service.ts +603 -0
- package/src/services/appointment/index.ts +2 -0
- package/src/services/appointment/utils/appointment.utils.ts +590 -0
- package/src/services/clinic/clinic.service.ts +6 -0
- package/src/services/clinic/utils/filter.utils.ts +27 -1
- package/src/services/procedure/procedure.service.ts +43 -5
- package/src/types/appointment/index.ts +161 -0
- package/src/types/procedure/index.ts +2 -0
- package/src/validations/appointment.schema.ts +125 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Firestore,
|
|
3
|
+
collection,
|
|
4
|
+
doc,
|
|
5
|
+
getDoc,
|
|
6
|
+
getDocs,
|
|
7
|
+
query,
|
|
8
|
+
where,
|
|
9
|
+
setDoc,
|
|
10
|
+
updateDoc,
|
|
11
|
+
serverTimestamp,
|
|
12
|
+
Timestamp,
|
|
13
|
+
orderBy,
|
|
14
|
+
limit,
|
|
15
|
+
startAfter,
|
|
16
|
+
QueryConstraint,
|
|
17
|
+
DocumentSnapshot,
|
|
18
|
+
} from "firebase/firestore";
|
|
19
|
+
import {
|
|
20
|
+
Appointment,
|
|
21
|
+
AppointmentStatus,
|
|
22
|
+
CreateAppointmentData,
|
|
23
|
+
UpdateAppointmentData,
|
|
24
|
+
APPOINTMENTS_COLLECTION,
|
|
25
|
+
SearchAppointmentsParams,
|
|
26
|
+
PaymentStatus,
|
|
27
|
+
} from "../../../types/appointment";
|
|
28
|
+
import { CalendarEvent, CALENDAR_COLLECTION } from "../../../types/calendar";
|
|
29
|
+
import { ProcedureSummaryInfo } from "../../../types/procedure";
|
|
30
|
+
import {
|
|
31
|
+
ClinicInfo,
|
|
32
|
+
PatientProfileInfo,
|
|
33
|
+
PractitionerProfileInfo,
|
|
34
|
+
} from "../../../types/profile";
|
|
35
|
+
import { BlockingCondition } from "../../../backoffice/types/static/blocking-condition.types";
|
|
36
|
+
import { Contraindication } from "../../../backoffice/types/static/contraindication.types";
|
|
37
|
+
import { Requirement } from "../../../backoffice/types/requirement.types";
|
|
38
|
+
import { PRACTITIONERS_COLLECTION } from "../../../types/practitioner";
|
|
39
|
+
import { CLINICS_COLLECTION } from "../../../types/clinic";
|
|
40
|
+
import { PATIENTS_COLLECTION } from "../../../types/patient";
|
|
41
|
+
import { PROCEDURES_COLLECTION } from "../../../types/procedure";
|
|
42
|
+
import {
|
|
43
|
+
Technology,
|
|
44
|
+
TECHNOLOGIES_COLLECTION,
|
|
45
|
+
} from "../../../backoffice/types/technology.types";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fetches all the necessary information for an appointment by IDs.
|
|
49
|
+
*
|
|
50
|
+
* @param db Firestore instance
|
|
51
|
+
* @param clinicId Clinic ID
|
|
52
|
+
* @param practitionerId Practitioner ID
|
|
53
|
+
* @param patientId Patient ID
|
|
54
|
+
* @param procedureId Procedure ID
|
|
55
|
+
* @returns Object containing the aggregated information
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchAggregatedInfoUtil(
|
|
58
|
+
db: Firestore,
|
|
59
|
+
clinicId: string,
|
|
60
|
+
practitionerId: string,
|
|
61
|
+
patientId: string,
|
|
62
|
+
procedureId: string
|
|
63
|
+
): Promise<{
|
|
64
|
+
clinicInfo: ClinicInfo;
|
|
65
|
+
practitionerInfo: PractitionerProfileInfo;
|
|
66
|
+
patientInfo: PatientProfileInfo;
|
|
67
|
+
procedureInfo: ProcedureSummaryInfo;
|
|
68
|
+
blockingConditions: BlockingCondition[];
|
|
69
|
+
contraindications: Contraindication[];
|
|
70
|
+
preProcedureRequirements: Requirement[];
|
|
71
|
+
postProcedureRequirements: Requirement[];
|
|
72
|
+
}> {
|
|
73
|
+
try {
|
|
74
|
+
// Fetch all data in parallel for efficiency
|
|
75
|
+
const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] =
|
|
76
|
+
await Promise.all([
|
|
77
|
+
getDoc(doc(db, CLINICS_COLLECTION, clinicId)),
|
|
78
|
+
getDoc(doc(db, PRACTITIONERS_COLLECTION, practitionerId)),
|
|
79
|
+
getDoc(doc(db, PATIENTS_COLLECTION, patientId)),
|
|
80
|
+
getDoc(doc(db, PROCEDURES_COLLECTION, procedureId)),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
// Check if all required entities exist
|
|
84
|
+
if (!clinicDoc.exists()) {
|
|
85
|
+
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
86
|
+
}
|
|
87
|
+
if (!practitionerDoc.exists()) {
|
|
88
|
+
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
89
|
+
}
|
|
90
|
+
if (!patientDoc.exists()) {
|
|
91
|
+
throw new Error(`Patient with ID ${patientId} not found`);
|
|
92
|
+
}
|
|
93
|
+
if (!procedureDoc.exists()) {
|
|
94
|
+
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const clinicData = clinicDoc.data();
|
|
98
|
+
const practitionerData = practitionerDoc.data();
|
|
99
|
+
const patientData = patientDoc.data();
|
|
100
|
+
const procedureData = procedureDoc.data();
|
|
101
|
+
|
|
102
|
+
// Extract relevant info for ClinicInfo
|
|
103
|
+
const clinicInfo: ClinicInfo = {
|
|
104
|
+
id: clinicId,
|
|
105
|
+
featuredPhoto: clinicData.featuredPhotos?.[0] || "",
|
|
106
|
+
name: clinicData.name,
|
|
107
|
+
description: clinicData.description || null,
|
|
108
|
+
location: clinicData.location,
|
|
109
|
+
contactInfo: clinicData.contactInfo,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Extract relevant info for PractitionerProfileInfo
|
|
113
|
+
const practitionerInfo: PractitionerProfileInfo = {
|
|
114
|
+
id: practitionerId,
|
|
115
|
+
practitionerPhoto: practitionerData.basicInfo?.profileImageUrl || null,
|
|
116
|
+
name: `${practitionerData.basicInfo?.firstName || ""} ${
|
|
117
|
+
practitionerData.basicInfo?.lastName || ""
|
|
118
|
+
}`.trim(),
|
|
119
|
+
email: practitionerData.basicInfo?.email || "",
|
|
120
|
+
phone: practitionerData.basicInfo?.phoneNumber || null,
|
|
121
|
+
certification: practitionerData.certification,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Extract relevant info for PatientProfileInfo
|
|
125
|
+
// Note: This may need adjustment depending on how patient data is structured
|
|
126
|
+
const patientInfo: PatientProfileInfo = {
|
|
127
|
+
id: patientId,
|
|
128
|
+
fullName: patientData.displayName || "",
|
|
129
|
+
email: patientData.email || "",
|
|
130
|
+
phone: patientData.phoneNumber || null,
|
|
131
|
+
dateOfBirth: patientData.dateOfBirth || Timestamp.now(),
|
|
132
|
+
gender: patientData.gender || "other",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Extract procedureInfo from the procedure document
|
|
136
|
+
// Assuming procedureData already has a procedureInfo property or similar structure
|
|
137
|
+
const procedureInfo: ProcedureSummaryInfo = {
|
|
138
|
+
id: procedureId,
|
|
139
|
+
name: procedureData.name,
|
|
140
|
+
description: procedureData.description,
|
|
141
|
+
photo: procedureData.photo || "",
|
|
142
|
+
family: procedureData.family,
|
|
143
|
+
categoryName: procedureData.category?.name || "",
|
|
144
|
+
subcategoryName: procedureData.subcategory?.name || "",
|
|
145
|
+
technologyName: procedureData.technology?.name || "",
|
|
146
|
+
brandName: procedureData.product?.brand || "",
|
|
147
|
+
productName: procedureData.product?.name || "",
|
|
148
|
+
price: procedureData.price || 0,
|
|
149
|
+
pricingMeasure: procedureData.pricingMeasure,
|
|
150
|
+
currency: procedureData.currency,
|
|
151
|
+
duration: procedureData.duration || 0,
|
|
152
|
+
clinicId: clinicId,
|
|
153
|
+
clinicName: clinicInfo.name,
|
|
154
|
+
practitionerId: practitionerId,
|
|
155
|
+
practitionerName: practitionerInfo.name,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Fetch the technology document to get procedure requirements
|
|
159
|
+
let technologyId = "";
|
|
160
|
+
if (procedureData.technology?.id) {
|
|
161
|
+
technologyId = procedureData.technology.id;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let blockingConditions: BlockingCondition[] = [];
|
|
165
|
+
let contraindications: Contraindication[] = [];
|
|
166
|
+
let preProcedureRequirements: Requirement[] = [];
|
|
167
|
+
let postProcedureRequirements: Requirement[] = [];
|
|
168
|
+
|
|
169
|
+
// If we have a technology ID, fetch its details
|
|
170
|
+
if (technologyId) {
|
|
171
|
+
const technologyDoc = await getDoc(
|
|
172
|
+
doc(db, TECHNOLOGIES_COLLECTION, technologyId)
|
|
173
|
+
);
|
|
174
|
+
if (technologyDoc.exists()) {
|
|
175
|
+
const technologyData = technologyDoc.data() as Technology;
|
|
176
|
+
|
|
177
|
+
// Extract technology-related info
|
|
178
|
+
blockingConditions = technologyData.blockingConditions || [];
|
|
179
|
+
contraindications = technologyData.contraindications || [];
|
|
180
|
+
preProcedureRequirements = technologyData.requirements?.pre || [];
|
|
181
|
+
postProcedureRequirements = technologyData.requirements?.post || [];
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Fallback to procedure-level data if technology not available
|
|
185
|
+
blockingConditions = procedureData.blockingConditions || [];
|
|
186
|
+
contraindications = procedureData.contraindications || [];
|
|
187
|
+
preProcedureRequirements = procedureData.preRequirements || [];
|
|
188
|
+
postProcedureRequirements = procedureData.postRequirements || [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
clinicInfo,
|
|
193
|
+
practitionerInfo,
|
|
194
|
+
patientInfo,
|
|
195
|
+
procedureInfo,
|
|
196
|
+
blockingConditions,
|
|
197
|
+
contraindications,
|
|
198
|
+
preProcedureRequirements,
|
|
199
|
+
postProcedureRequirements,
|
|
200
|
+
};
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error("Error fetching aggregated info:", error);
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Creates a new appointment in Firestore.
|
|
209
|
+
*
|
|
210
|
+
* @param db Firestore instance
|
|
211
|
+
* @param data Data needed to create the appointment
|
|
212
|
+
* @param aggregatedInfo Already fetched and aggregated info
|
|
213
|
+
* @param generateId Function to generate a unique ID
|
|
214
|
+
* @returns The created Appointment
|
|
215
|
+
*/
|
|
216
|
+
export async function createAppointmentUtil(
|
|
217
|
+
db: Firestore,
|
|
218
|
+
data: CreateAppointmentData,
|
|
219
|
+
aggregatedInfo: {
|
|
220
|
+
clinicInfo: ClinicInfo;
|
|
221
|
+
practitionerInfo: PractitionerProfileInfo;
|
|
222
|
+
patientInfo: PatientProfileInfo;
|
|
223
|
+
procedureInfo: ProcedureSummaryInfo;
|
|
224
|
+
blockingConditions: BlockingCondition[];
|
|
225
|
+
contraindications: Contraindication[];
|
|
226
|
+
preProcedureRequirements: Requirement[];
|
|
227
|
+
postProcedureRequirements: Requirement[];
|
|
228
|
+
},
|
|
229
|
+
generateId: () => string
|
|
230
|
+
): Promise<Appointment> {
|
|
231
|
+
try {
|
|
232
|
+
const appointmentId = generateId();
|
|
233
|
+
|
|
234
|
+
// Create appointment object
|
|
235
|
+
const appointment: Omit<Appointment, "createdAt" | "updatedAt"> & {
|
|
236
|
+
createdAt: any;
|
|
237
|
+
updatedAt: any;
|
|
238
|
+
} = {
|
|
239
|
+
id: appointmentId,
|
|
240
|
+
calendarEventId: data.calendarEventId,
|
|
241
|
+
clinicBranchId: data.clinicBranchId,
|
|
242
|
+
clinicInfo: aggregatedInfo.clinicInfo,
|
|
243
|
+
practitionerId: data.practitionerId,
|
|
244
|
+
practitionerInfo: aggregatedInfo.practitionerInfo,
|
|
245
|
+
patientId: data.patientId,
|
|
246
|
+
patientInfo: aggregatedInfo.patientInfo,
|
|
247
|
+
procedureId: data.procedureId,
|
|
248
|
+
procedureInfo: aggregatedInfo.procedureInfo,
|
|
249
|
+
status: data.initialStatus,
|
|
250
|
+
bookingTime: Timestamp.now(),
|
|
251
|
+
appointmentStartTime: data.appointmentStartTime,
|
|
252
|
+
appointmentEndTime: data.appointmentEndTime,
|
|
253
|
+
patientNotes: data.patientNotes || null,
|
|
254
|
+
cost: data.cost,
|
|
255
|
+
currency: data.currency,
|
|
256
|
+
paymentStatus: data.initialPaymentStatus || PaymentStatus.UNPAID,
|
|
257
|
+
blockingConditions: aggregatedInfo.blockingConditions,
|
|
258
|
+
contraindications: aggregatedInfo.contraindications,
|
|
259
|
+
preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
|
|
260
|
+
postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
|
|
261
|
+
completedPreRequirements: [],
|
|
262
|
+
completedPostRequirements: [],
|
|
263
|
+
createdAt: serverTimestamp(),
|
|
264
|
+
updatedAt: serverTimestamp(),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Add additional fields for confirmation if appointment is already confirmed
|
|
268
|
+
if (data.initialStatus === AppointmentStatus.CONFIRMED) {
|
|
269
|
+
appointment.confirmationTime = Timestamp.now();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Save to Firestore
|
|
273
|
+
await setDoc(doc(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
|
|
274
|
+
|
|
275
|
+
// Update the calendar event with the appointment ID
|
|
276
|
+
const calendarEventRef = doc(db, CALENDAR_COLLECTION, data.calendarEventId);
|
|
277
|
+
await updateDoc(calendarEventRef, {
|
|
278
|
+
appointmentId: appointmentId,
|
|
279
|
+
updatedAt: serverTimestamp(),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Return the created appointment
|
|
283
|
+
// Convert serverTimestamp to regular Timestamp for immediate use
|
|
284
|
+
const now = Timestamp.now();
|
|
285
|
+
return {
|
|
286
|
+
...appointment,
|
|
287
|
+
createdAt: now,
|
|
288
|
+
updatedAt: now,
|
|
289
|
+
} as Appointment;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error("Error creating appointment:", error);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Updates an existing appointment in Firestore.
|
|
298
|
+
*
|
|
299
|
+
* @param db Firestore instance
|
|
300
|
+
* @param appointmentId ID of the appointment to update
|
|
301
|
+
* @param data Update data for the appointment
|
|
302
|
+
* @returns The updated Appointment
|
|
303
|
+
*/
|
|
304
|
+
export async function updateAppointmentUtil(
|
|
305
|
+
db: Firestore,
|
|
306
|
+
appointmentId: string,
|
|
307
|
+
data: UpdateAppointmentData
|
|
308
|
+
): Promise<Appointment> {
|
|
309
|
+
try {
|
|
310
|
+
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
311
|
+
const appointmentDoc = await getDoc(appointmentRef);
|
|
312
|
+
|
|
313
|
+
if (!appointmentDoc.exists()) {
|
|
314
|
+
throw new Error(`Appointment with ID ${appointmentId} not found`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const currentAppointment = appointmentDoc.data() as Appointment;
|
|
318
|
+
|
|
319
|
+
// Handle requirement completion tracking
|
|
320
|
+
let completedPreRequirements =
|
|
321
|
+
currentAppointment.completedPreRequirements || [];
|
|
322
|
+
let completedPostRequirements =
|
|
323
|
+
currentAppointment.completedPostRequirements || [];
|
|
324
|
+
|
|
325
|
+
if (data.completedPreRequirements) {
|
|
326
|
+
// Validate that all IDs exist in the pre-requirements
|
|
327
|
+
const validPreReqIds = currentAppointment.preProcedureRequirements.map(
|
|
328
|
+
(req) => req.id
|
|
329
|
+
);
|
|
330
|
+
const invalidPreReqIds = data.completedPreRequirements.filter(
|
|
331
|
+
(id) => !validPreReqIds.includes(id)
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (invalidPreReqIds.length > 0) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Invalid pre-requirement IDs: ${invalidPreReqIds.join(", ")}`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Update the completed pre-requirements
|
|
341
|
+
completedPreRequirements = [
|
|
342
|
+
...new Set([
|
|
343
|
+
...completedPreRequirements,
|
|
344
|
+
...data.completedPreRequirements,
|
|
345
|
+
]),
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (data.completedPostRequirements) {
|
|
350
|
+
// Validate that all IDs exist in the post-requirements
|
|
351
|
+
const validPostReqIds = currentAppointment.postProcedureRequirements.map(
|
|
352
|
+
(req) => req.id
|
|
353
|
+
);
|
|
354
|
+
const invalidPostReqIds = data.completedPostRequirements.filter(
|
|
355
|
+
(id) => !validPostReqIds.includes(id)
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (invalidPostReqIds.length > 0) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Invalid post-requirement IDs: ${invalidPostReqIds.join(", ")}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Update the completed post-requirements
|
|
365
|
+
completedPostRequirements = [
|
|
366
|
+
...new Set([
|
|
367
|
+
...completedPostRequirements,
|
|
368
|
+
...data.completedPostRequirements,
|
|
369
|
+
]),
|
|
370
|
+
];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Prepare update data
|
|
374
|
+
const updateData: any = {
|
|
375
|
+
...data,
|
|
376
|
+
completedPreRequirements,
|
|
377
|
+
completedPostRequirements,
|
|
378
|
+
updatedAt: serverTimestamp(),
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Remove undefined fields
|
|
382
|
+
Object.keys(updateData).forEach((key) => {
|
|
383
|
+
if (updateData[key] === undefined) {
|
|
384
|
+
delete updateData[key];
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Handle status changes
|
|
389
|
+
if (data.status && data.status !== currentAppointment.status) {
|
|
390
|
+
// Handle confirmation
|
|
391
|
+
if (
|
|
392
|
+
data.status === AppointmentStatus.CONFIRMED &&
|
|
393
|
+
!updateData.confirmationTime
|
|
394
|
+
) {
|
|
395
|
+
updateData.confirmationTime = Timestamp.now();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Update the related calendar event status if needed
|
|
399
|
+
if (currentAppointment.calendarEventId) {
|
|
400
|
+
await updateCalendarEventStatus(
|
|
401
|
+
db,
|
|
402
|
+
currentAppointment.calendarEventId,
|
|
403
|
+
data.status
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Update the appointment
|
|
409
|
+
await updateDoc(appointmentRef, updateData);
|
|
410
|
+
|
|
411
|
+
// Fetch the updated appointment
|
|
412
|
+
const updatedAppointmentDoc = await getDoc(appointmentRef);
|
|
413
|
+
if (!updatedAppointmentDoc.exists()) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Failed to retrieve updated appointment ${appointmentId}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return updatedAppointmentDoc.data() as Appointment;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error(`Error updating appointment ${appointmentId}:`, error);
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Updates the status of a calendar event based on appointment status changes.
|
|
428
|
+
*
|
|
429
|
+
* @param db Firestore instance
|
|
430
|
+
* @param calendarEventId ID of the calendar event
|
|
431
|
+
* @param appointmentStatus New appointment status
|
|
432
|
+
*/
|
|
433
|
+
async function updateCalendarEventStatus(
|
|
434
|
+
db: Firestore,
|
|
435
|
+
calendarEventId: string,
|
|
436
|
+
appointmentStatus: AppointmentStatus
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
try {
|
|
439
|
+
const calendarEventRef = doc(db, CALENDAR_COLLECTION, calendarEventId);
|
|
440
|
+
const calendarEventDoc = await getDoc(calendarEventRef);
|
|
441
|
+
|
|
442
|
+
if (!calendarEventDoc.exists()) {
|
|
443
|
+
console.warn(`Calendar event with ID ${calendarEventId} not found`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Map appointment status to calendar event status
|
|
448
|
+
let calendarStatus;
|
|
449
|
+
switch (appointmentStatus) {
|
|
450
|
+
case AppointmentStatus.CONFIRMED:
|
|
451
|
+
calendarStatus = "confirmed";
|
|
452
|
+
break;
|
|
453
|
+
case AppointmentStatus.CANCELED_PATIENT:
|
|
454
|
+
case AppointmentStatus.CANCELED_CLINIC:
|
|
455
|
+
calendarStatus = "canceled";
|
|
456
|
+
break;
|
|
457
|
+
case AppointmentStatus.RESCHEDULED:
|
|
458
|
+
calendarStatus = "rescheduled";
|
|
459
|
+
break;
|
|
460
|
+
case AppointmentStatus.COMPLETED:
|
|
461
|
+
calendarStatus = "completed";
|
|
462
|
+
break;
|
|
463
|
+
default:
|
|
464
|
+
// For other states, don't update the calendar status
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
await updateDoc(calendarEventRef, {
|
|
469
|
+
status: calendarStatus,
|
|
470
|
+
updatedAt: serverTimestamp(),
|
|
471
|
+
});
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error(`Error updating calendar event ${calendarEventId}:`, error);
|
|
474
|
+
// Don't throw error to avoid failing the appointment update
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Gets an appointment by its ID.
|
|
480
|
+
*
|
|
481
|
+
* @param db Firestore instance
|
|
482
|
+
* @param appointmentId Appointment ID
|
|
483
|
+
* @returns The appointment or null if not found
|
|
484
|
+
*/
|
|
485
|
+
export async function getAppointmentByIdUtil(
|
|
486
|
+
db: Firestore,
|
|
487
|
+
appointmentId: string
|
|
488
|
+
): Promise<Appointment | null> {
|
|
489
|
+
try {
|
|
490
|
+
const appointmentDoc = await getDoc(
|
|
491
|
+
doc(db, APPOINTMENTS_COLLECTION, appointmentId)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
if (!appointmentDoc.exists()) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return appointmentDoc.data() as Appointment;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error(`Error getting appointment ${appointmentId}:`, error);
|
|
501
|
+
throw error;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Searches for appointments based on various criteria.
|
|
507
|
+
*
|
|
508
|
+
* @param db Firestore instance
|
|
509
|
+
* @param params Search parameters
|
|
510
|
+
* @returns Found appointments and the last document for pagination
|
|
511
|
+
*/
|
|
512
|
+
export async function searchAppointmentsUtil(
|
|
513
|
+
db: Firestore,
|
|
514
|
+
params: SearchAppointmentsParams
|
|
515
|
+
): Promise<{ appointments: Appointment[]; lastDoc: DocumentSnapshot | null }> {
|
|
516
|
+
try {
|
|
517
|
+
const constraints: QueryConstraint[] = [];
|
|
518
|
+
|
|
519
|
+
// Add filters based on provided params
|
|
520
|
+
if (params.patientId) {
|
|
521
|
+
constraints.push(where("patientId", "==", params.patientId));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (params.practitionerId) {
|
|
525
|
+
constraints.push(where("practitionerId", "==", params.practitionerId));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (params.clinicBranchId) {
|
|
529
|
+
constraints.push(where("clinicBranchId", "==", params.clinicBranchId));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (params.startDate) {
|
|
533
|
+
constraints.push(
|
|
534
|
+
where(
|
|
535
|
+
"appointmentStartTime",
|
|
536
|
+
">=",
|
|
537
|
+
Timestamp.fromDate(params.startDate)
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (params.endDate) {
|
|
543
|
+
constraints.push(
|
|
544
|
+
where("appointmentStartTime", "<=", Timestamp.fromDate(params.endDate))
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (params.status) {
|
|
549
|
+
if (Array.isArray(params.status)) {
|
|
550
|
+
// If multiple statuses, use in operator
|
|
551
|
+
constraints.push(where("status", "in", params.status));
|
|
552
|
+
} else {
|
|
553
|
+
// Single status
|
|
554
|
+
constraints.push(where("status", "==", params.status));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Add ordering
|
|
559
|
+
constraints.push(orderBy("appointmentStartTime", "asc"));
|
|
560
|
+
|
|
561
|
+
// Add pagination if specified
|
|
562
|
+
if (params.limit) {
|
|
563
|
+
constraints.push(limit(params.limit));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (params.startAfter) {
|
|
567
|
+
constraints.push(startAfter(params.startAfter));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Execute query
|
|
571
|
+
const q = query(collection(db, APPOINTMENTS_COLLECTION), ...constraints);
|
|
572
|
+
const querySnapshot = await getDocs(q);
|
|
573
|
+
|
|
574
|
+
// Extract results
|
|
575
|
+
const appointments = querySnapshot.docs.map(
|
|
576
|
+
(doc) => doc.data() as Appointment
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Get last document for pagination
|
|
580
|
+
const lastDoc =
|
|
581
|
+
querySnapshot.docs.length > 0
|
|
582
|
+
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
583
|
+
: null;
|
|
584
|
+
|
|
585
|
+
return { appointments, lastDoc };
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.error("Error searching appointments:", error);
|
|
588
|
+
throw error;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
@@ -426,6 +426,12 @@ export class ClinicService extends BaseService {
|
|
|
426
426
|
);
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Get clinics based on multiple filtering criteria
|
|
431
|
+
*
|
|
432
|
+
* @param filters - Various filters to apply
|
|
433
|
+
* @returns Filtered clinics and the last document for pagination
|
|
434
|
+
*/
|
|
429
435
|
async getClinicsByFilters(filters: {
|
|
430
436
|
center?: { latitude: number; longitude: number };
|
|
431
437
|
radiusInKm?: number;
|
|
@@ -95,7 +95,7 @@ export async function getClinicsByFilters(
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// Add ordering to make pagination consistent
|
|
98
|
-
constraints.push(orderBy(
|
|
98
|
+
constraints.push(orderBy("location.geohash"));
|
|
99
99
|
|
|
100
100
|
let clinicsResult: (Clinic & { distance?: number })[] = [];
|
|
101
101
|
let lastVisibleDoc = null;
|
|
@@ -227,6 +227,32 @@ export async function getClinicsByFilters(
|
|
|
227
227
|
// Apply filters that couldn't be applied in the query
|
|
228
228
|
let filteredClinics = clinics;
|
|
229
229
|
|
|
230
|
+
// Calculate distance for each clinic if center coordinates are provided
|
|
231
|
+
if (filters.center) {
|
|
232
|
+
const center = filters.center;
|
|
233
|
+
const clinicsWithDistance: (Clinic & { distance: number })[] = [];
|
|
234
|
+
|
|
235
|
+
filteredClinics.forEach((clinic) => {
|
|
236
|
+
const distance = distanceBetween(
|
|
237
|
+
[center.latitude, center.longitude],
|
|
238
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
clinicsWithDistance.push({
|
|
242
|
+
...clinic,
|
|
243
|
+
distance: distance / 1000, // Convert to kilometers
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Replace filtered clinics with the version that includes distances
|
|
248
|
+
filteredClinics = clinicsWithDistance;
|
|
249
|
+
|
|
250
|
+
// Sort by distance - use type assertion to fix type error
|
|
251
|
+
(filteredClinics as (Clinic & { distance: number })[]).sort(
|
|
252
|
+
(a, b) => a.distance - b.distance
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
230
256
|
// Filter by multiple tags if more than one tag was specified
|
|
231
257
|
if (filters.tags && filters.tags.length > 1) {
|
|
232
258
|
filteredClinics = filteredClinics.filter((clinic) => {
|
|
@@ -612,7 +612,7 @@ export class ProcedureService extends BaseService {
|
|
|
612
612
|
}
|
|
613
613
|
|
|
614
614
|
// Add ordering to make pagination consistent
|
|
615
|
-
constraints.push(orderBy(
|
|
615
|
+
constraints.push(orderBy("clinicInfo.location.geohash"));
|
|
616
616
|
|
|
617
617
|
// Add pagination if specified
|
|
618
618
|
if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
|
|
@@ -743,16 +743,54 @@ export class ProcedureService extends BaseService {
|
|
|
743
743
|
return { ...doc.data(), id: doc.id } as Procedure;
|
|
744
744
|
});
|
|
745
745
|
|
|
746
|
-
//
|
|
747
|
-
|
|
746
|
+
// Calculate distance for each procedure if location is provided
|
|
747
|
+
if (filters.location) {
|
|
748
|
+
const center = filters.location;
|
|
749
|
+
const proceduresWithDistance: (Procedure & { distance: number })[] =
|
|
750
|
+
[];
|
|
751
|
+
|
|
752
|
+
procedures.forEach((procedure) => {
|
|
753
|
+
const distance = distanceBetween(
|
|
754
|
+
[center.latitude, center.longitude],
|
|
755
|
+
[
|
|
756
|
+
procedure.clinicInfo.location.latitude,
|
|
757
|
+
procedure.clinicInfo.location.longitude,
|
|
758
|
+
]
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
proceduresWithDistance.push({
|
|
762
|
+
...procedure,
|
|
763
|
+
distance: distance / 1000, // Convert to kilometers
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Replace procedures with version that includes distances
|
|
768
|
+
let filteredProcedures = proceduresWithDistance;
|
|
769
|
+
|
|
770
|
+
// Apply in-memory filters
|
|
771
|
+
filteredProcedures = this.applyInMemoryFilters(
|
|
772
|
+
filteredProcedures,
|
|
773
|
+
filters
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
// Sort by distance
|
|
777
|
+
filteredProcedures.sort((a, b) => a.distance - b.distance);
|
|
778
|
+
|
|
779
|
+
proceduresResult = filteredProcedures;
|
|
780
|
+
} else {
|
|
781
|
+
// Apply filters that couldn't be applied in the query
|
|
782
|
+
let filteredProcedures = this.applyInMemoryFilters(
|
|
783
|
+
procedures,
|
|
784
|
+
filters
|
|
785
|
+
);
|
|
786
|
+
proceduresResult = filteredProcedures;
|
|
787
|
+
}
|
|
748
788
|
|
|
749
789
|
// Set last document for pagination
|
|
750
790
|
lastVisibleDoc =
|
|
751
791
|
querySnapshot.docs.length > 0
|
|
752
792
|
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
753
793
|
: null;
|
|
754
|
-
|
|
755
|
-
proceduresResult = filteredProcedures;
|
|
756
794
|
}
|
|
757
795
|
|
|
758
796
|
return {
|