@blackcode_sa/metaestetics-api 1.6.16 → 1.6.18

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.
@@ -0,0 +1,404 @@
1
+ import * as admin from "firebase-admin";
2
+ import { User, UserRole, USERS_COLLECTION } from "../../types";
3
+ import {
4
+ PatientProfile,
5
+ PATIENTS_COLLECTION,
6
+ CreatePatientProfileData,
7
+ PatientSensitiveInfo,
8
+ PATIENT_SENSITIVE_INFO_COLLECTION,
9
+ CreatePatientSensitiveInfoData,
10
+ Gender,
11
+ PatientMedicalInfo,
12
+ PATIENT_MEDICAL_INFO_COLLECTION,
13
+ } from "../../types/patient";
14
+
15
+ /**
16
+ * @class UserProfileAdminService
17
+ * @description Handles user profile management operations for admin tasks
18
+ */
19
+ export class UserProfileAdminService {
20
+ private db: admin.firestore.Firestore;
21
+
22
+ /**
23
+ * Constructor for UserProfileAdminService
24
+ * @param firestore Optional Firestore instance. If not provided, uses the default admin SDK instance.
25
+ */
26
+ constructor(firestore?: admin.firestore.Firestore) {
27
+ this.db = firestore || admin.firestore();
28
+ }
29
+
30
+ /**
31
+ * Creates a blank user profile with minimal information
32
+ * @param authUserData Basic user data from Firebase Auth
33
+ * @returns The created user document
34
+ */
35
+ async createBlankUserProfile(authUserData: {
36
+ uid: string;
37
+ email: string | null;
38
+ isAnonymous: boolean;
39
+ }): Promise<User> {
40
+ console.log(
41
+ `[UserProfileAdminService] Creating blank user profile for user ${authUserData.uid}`
42
+ );
43
+
44
+ const userData: User = {
45
+ uid: authUserData.uid,
46
+ email: authUserData.email,
47
+ roles: [], // Empty roles array as requested
48
+ isAnonymous: authUserData.isAnonymous,
49
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
50
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
51
+ lastLoginAt: admin.firestore.FieldValue.serverTimestamp(),
52
+ };
53
+
54
+ try {
55
+ const userRef = this.db
56
+ .collection(USERS_COLLECTION)
57
+ .doc(authUserData.uid);
58
+ await userRef.set(userData);
59
+
60
+ // Fetch the created document to return with server timestamps
61
+ const userDoc = await userRef.get();
62
+ return userDoc.data() as User;
63
+ } catch (error) {
64
+ console.error(
65
+ `[UserProfileAdminService] Error creating blank user profile for ${authUserData.uid}:`,
66
+ error
67
+ );
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Initializes patient role for a user and creates all required patient documents
74
+ * Creates patient profile, sensitive info, and medical info in one operation
75
+ *
76
+ * @param userId The user ID to initialize with patient role
77
+ * @param options Optional data for different aspects of patient initialization
78
+ * @returns Object containing the updated user and all created patient documents
79
+ */
80
+ async initializePatientRole(
81
+ userId: string,
82
+ options?: {
83
+ profileData?: Partial<CreatePatientProfileData>;
84
+ sensitiveData?: Partial<CreatePatientSensitiveInfoData>;
85
+ }
86
+ ): Promise<{
87
+ user: User;
88
+ patientProfile: PatientProfile;
89
+ patientSensitiveInfo: PatientSensitiveInfo;
90
+ patientMedicalInfo: PatientMedicalInfo;
91
+ }> {
92
+ console.log(
93
+ `[UserProfileAdminService] Initializing complete patient role for user ${userId}`
94
+ );
95
+
96
+ try {
97
+ // Get user document
98
+ const userRef = this.db.collection(USERS_COLLECTION).doc(userId);
99
+ const userDoc = await userRef.get();
100
+
101
+ if (!userDoc.exists) {
102
+ throw new Error(`User ${userId} not found`);
103
+ }
104
+
105
+ const userData = userDoc.data() as User;
106
+ let patientProfileId: string | undefined = userData.patientProfile;
107
+
108
+ // Create patient profile if it doesn't exist
109
+ let patientProfile: PatientProfile;
110
+ if (!patientProfileId) {
111
+ // Create patient profile
112
+ const patientProfileRef = this.db.collection(PATIENTS_COLLECTION).doc();
113
+ patientProfileId = patientProfileRef.id;
114
+
115
+ // Set default profile data
116
+ const defaultProfileData: CreatePatientProfileData = {
117
+ userRef: userId,
118
+ displayName: "Patient", // Default display name
119
+ expoTokens: [],
120
+ gamification: {
121
+ level: 1,
122
+ points: 0,
123
+ },
124
+ isActive: true,
125
+ isVerified: false,
126
+ doctors: [],
127
+ clinics: [],
128
+ doctorIds: [],
129
+ clinicIds: [],
130
+ };
131
+
132
+ // Merge with any provided profile data
133
+ const mergedProfileData = {
134
+ ...defaultProfileData,
135
+ ...options?.profileData,
136
+ };
137
+
138
+ // Create the patient profile document with explicit properties
139
+ const patientProfileData = {
140
+ id: patientProfileId,
141
+ userRef: mergedProfileData.userRef,
142
+ displayName: mergedProfileData.displayName,
143
+ profilePhoto: null,
144
+ gamification: mergedProfileData.gamification || {
145
+ level: 1,
146
+ points: 0,
147
+ },
148
+ expoTokens: mergedProfileData.expoTokens || [],
149
+ isActive: mergedProfileData.isActive,
150
+ isVerified: mergedProfileData.isVerified,
151
+ doctors: mergedProfileData.doctors || [],
152
+ clinics: mergedProfileData.clinics || [],
153
+ doctorIds: mergedProfileData.doctorIds || [],
154
+ clinicIds: mergedProfileData.clinicIds || [],
155
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
156
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
157
+ };
158
+
159
+ // Store the document
160
+ await patientProfileRef.set(patientProfileData);
161
+
162
+ // For returning to the client, use type assertions
163
+ patientProfile = {
164
+ ...patientProfileData,
165
+ createdAt: null as any,
166
+ updatedAt: null as any,
167
+ } as unknown as PatientProfile;
168
+
169
+ console.log(
170
+ `[UserProfileAdminService] Creating patient profile with ID ${patientProfileId}`
171
+ );
172
+ } else {
173
+ // Get existing patient profile
174
+ const patientProfileRef = this.db
175
+ .collection(PATIENTS_COLLECTION)
176
+ .doc(patientProfileId);
177
+ const patientProfileDoc = await patientProfileRef.get();
178
+
179
+ if (!patientProfileDoc.exists) {
180
+ throw new Error(
181
+ `Patient profile ${patientProfileId} exists in user but not in patients collection`
182
+ );
183
+ }
184
+
185
+ patientProfile = patientProfileDoc.data() as PatientProfile;
186
+ }
187
+
188
+ // Create sensitive information document
189
+ const sensitiveInfoRef = this.db
190
+ .collection(PATIENTS_COLLECTION)
191
+ .doc(patientProfileId)
192
+ .collection(PATIENT_SENSITIVE_INFO_COLLECTION)
193
+ .doc("info"); // Use 'info' as standard document ID for subcollection
194
+
195
+ // Check if sensitive info document already exists
196
+ const sensitiveInfoDoc = await sensitiveInfoRef.get();
197
+ let patientSensitiveInfo: PatientSensitiveInfo;
198
+
199
+ if (!sensitiveInfoDoc.exists) {
200
+ // Create default sensitive info data
201
+ const defaultSensitiveData: CreatePatientSensitiveInfoData = {
202
+ patientId: patientProfileId,
203
+ userRef: userId,
204
+ firstName: "",
205
+ lastName: "",
206
+ dateOfBirth: null,
207
+ gender: Gender.PREFER_NOT_TO_SAY,
208
+ };
209
+
210
+ // Merge with provided data
211
+ const mergedSensitiveData = {
212
+ ...defaultSensitiveData,
213
+ ...options?.sensitiveData,
214
+ };
215
+
216
+ // Create sensitive info
217
+ const sensitiveInfoData = {
218
+ ...mergedSensitiveData,
219
+ createdAt: admin.firestore.FieldValue.serverTimestamp(),
220
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
221
+ // Leave dateOfBirth as is
222
+ };
223
+
224
+ // Store the document
225
+ await sensitiveInfoRef.set(sensitiveInfoData);
226
+
227
+ // Convert for client return with type assertions
228
+ patientSensitiveInfo = {
229
+ ...sensitiveInfoData,
230
+ createdAt: null as any,
231
+ updatedAt: null as any,
232
+ } as unknown as PatientSensitiveInfo;
233
+
234
+ console.log(
235
+ `[UserProfileAdminService] Creating sensitive info in subcollection for patient ${patientProfileId}`
236
+ );
237
+ } else {
238
+ patientSensitiveInfo = sensitiveInfoDoc.data() as PatientSensitiveInfo;
239
+ }
240
+
241
+ // Create medical info document as a subcollection
242
+ const medicalInfoRef = this.db
243
+ .collection(PATIENTS_COLLECTION)
244
+ .doc(patientProfileId)
245
+ .collection(PATIENT_MEDICAL_INFO_COLLECTION)
246
+ .doc("info"); // Use 'info' as standard document ID for subcollection
247
+
248
+ // Check if medical info document already exists
249
+ const medicalInfoDoc = await medicalInfoRef.get();
250
+ let patientMedicalInfo: PatientMedicalInfo;
251
+
252
+ if (!medicalInfoDoc.exists) {
253
+ // Create medical info
254
+ const medicalInfoData = {
255
+ patientId: patientProfileId,
256
+ vitalStats: {},
257
+ blockingConditions: [],
258
+ contraindications: [],
259
+ allergies: [],
260
+ currentMedications: [],
261
+ lastUpdated: admin.firestore.FieldValue.serverTimestamp(),
262
+ updatedBy: userId,
263
+ };
264
+
265
+ // Store the document
266
+ await medicalInfoRef.set(medicalInfoData);
267
+
268
+ // Convert for client return with type assertions
269
+ patientMedicalInfo = {
270
+ ...medicalInfoData,
271
+ lastUpdated: null as any,
272
+ } as unknown as PatientMedicalInfo;
273
+
274
+ console.log(
275
+ `[UserProfileAdminService] Creating medical info in subcollection for patient ${patientProfileId}`
276
+ );
277
+ } else {
278
+ patientMedicalInfo = medicalInfoDoc.data() as PatientMedicalInfo;
279
+ }
280
+
281
+ // Update user document with patient role and profile reference
282
+ const batch = this.db.batch();
283
+
284
+ // Add patient role if not already present
285
+ if (!userData.roles.includes(UserRole.PATIENT)) {
286
+ batch.update(userRef, {
287
+ roles: admin.firestore.FieldValue.arrayUnion(UserRole.PATIENT),
288
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
289
+ });
290
+ }
291
+
292
+ // Add patient profile reference if not already set
293
+ if (!userData.patientProfile) {
294
+ batch.update(userRef, {
295
+ patientProfile: patientProfileId,
296
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
297
+ });
298
+ }
299
+
300
+ await batch.commit();
301
+
302
+ // Get updated user document
303
+ const updatedUserDoc = await userRef.get();
304
+ const updatedUser = updatedUserDoc.data() as User;
305
+
306
+ console.log(
307
+ `[UserProfileAdminService] Successfully initialized patient role for user ${userId}`
308
+ );
309
+
310
+ return {
311
+ user: updatedUser,
312
+ patientProfile,
313
+ patientSensitiveInfo,
314
+ patientMedicalInfo,
315
+ };
316
+ } catch (error) {
317
+ console.error(
318
+ `[UserProfileAdminService] Error initializing patient role for user ${userId}:`,
319
+ error
320
+ );
321
+ throw error;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Initializes clinic admin role for a user
327
+ * @param userId The user ID to initialize with clinic admin role
328
+ * @returns The updated user document
329
+ */
330
+ async initializeClinicAdminRole(userId: string): Promise<User> {
331
+ console.log(
332
+ `[UserProfileAdminService] Initializing clinic admin role for user ${userId}`
333
+ );
334
+
335
+ try {
336
+ const userRef = this.db.collection(USERS_COLLECTION).doc(userId);
337
+ const userDoc = await userRef.get();
338
+
339
+ if (!userDoc.exists) {
340
+ throw new Error(`User ${userId} not found`);
341
+ }
342
+
343
+ const userData = userDoc.data() as User;
344
+
345
+ // Only add the role if it doesn't already exist
346
+ if (!userData.roles.includes(UserRole.CLINIC_ADMIN)) {
347
+ await userRef.update({
348
+ roles: admin.firestore.FieldValue.arrayUnion(UserRole.CLINIC_ADMIN),
349
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
350
+ });
351
+ }
352
+
353
+ // Return the updated user document
354
+ const updatedUserDoc = await userRef.get();
355
+ return updatedUserDoc.data() as User;
356
+ } catch (error) {
357
+ console.error(
358
+ `[UserProfileAdminService] Error initializing clinic admin role for user ${userId}:`,
359
+ error
360
+ );
361
+ throw error;
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Initializes practitioner role for a user
367
+ * @param userId The user ID to initialize with practitioner role
368
+ * @returns The updated user document
369
+ */
370
+ async initializePractitionerRole(userId: string): Promise<User> {
371
+ console.log(
372
+ `[UserProfileAdminService] Initializing practitioner role for user ${userId}`
373
+ );
374
+
375
+ try {
376
+ const userRef = this.db.collection(USERS_COLLECTION).doc(userId);
377
+ const userDoc = await userRef.get();
378
+
379
+ if (!userDoc.exists) {
380
+ throw new Error(`User ${userId} not found`);
381
+ }
382
+
383
+ const userData = userDoc.data() as User;
384
+
385
+ // Only add the role if it doesn't already exist
386
+ if (!userData.roles.includes(UserRole.PRACTITIONER)) {
387
+ await userRef.update({
388
+ roles: admin.firestore.FieldValue.arrayUnion(UserRole.PRACTITIONER),
389
+ updatedAt: admin.firestore.FieldValue.serverTimestamp(),
390
+ });
391
+ }
392
+
393
+ // Return the updated user document
394
+ const updatedUserDoc = await userRef.get();
395
+ return updatedUserDoc.data() as User;
396
+ } catch (error) {
397
+ console.error(
398
+ `[UserProfileAdminService] Error initializing practitioner role for user ${userId}:`,
399
+ error
400
+ );
401
+ throw error;
402
+ }
403
+ }
404
+ }
@@ -3,12 +3,16 @@ import { Firestore, getFirestore } from "firebase/firestore";
3
3
  import { Auth, getAuth } from "firebase/auth";
4
4
  import { Analytics, getAnalytics } from "firebase/analytics";
5
5
  import { Platform } from "react-native";
6
+ import { FirebaseStorage, getStorage } from "firebase/storage";
7
+ import { Functions, getFunctions } from "firebase/functions";
6
8
 
7
9
  interface FirebaseInstance {
8
10
  app: FirebaseApp;
9
11
  db: Firestore;
10
12
  auth: Auth;
11
13
  analytics: Analytics | null;
14
+ storage: FirebaseStorage;
15
+ functions: Functions;
12
16
  }
13
17
 
14
18
  let firebaseInstance: FirebaseInstance | null = null;
@@ -26,13 +30,15 @@ export const initializeFirebase = (config: {
26
30
  const app = initializeApp(config);
27
31
  const db = getFirestore(app);
28
32
  const auth = getAuth(app);
33
+ const storage = getStorage(app);
34
+ const functions = getFunctions(app);
29
35
 
30
36
  let analytics = null;
31
37
  if (typeof window !== "undefined" && Platform.OS === "web") {
32
38
  analytics = getAnalytics(app);
33
39
  }
34
40
 
35
- firebaseInstance = { app, db, auth, analytics };
41
+ firebaseInstance = { app, db, auth, analytics, storage, functions };
36
42
  }
37
43
  return firebaseInstance;
38
44
  };
@@ -60,3 +66,13 @@ export const getFirebaseApp = async (): Promise<FirebaseApp> => {
60
66
  const instance = await getFirebaseInstance();
61
67
  return instance.app;
62
68
  };
69
+
70
+ export const getFirebaseStorage = async (): Promise<FirebaseStorage> => {
71
+ const instance = await getFirebaseInstance();
72
+ return instance.storage;
73
+ };
74
+
75
+ export const getFirebaseFunctions = async (): Promise<Functions> => {
76
+ const instance = await getFirebaseInstance();
77
+ return instance.functions;
78
+ };
@@ -5,6 +5,14 @@ import {
5
5
  serverTimestamp,
6
6
  arrayUnion,
7
7
  arrayRemove,
8
+ QueryConstraint,
9
+ where,
10
+ orderBy,
11
+ collection,
12
+ query,
13
+ limit,
14
+ startAfter,
15
+ getDocs,
8
16
  } from "firebase/firestore";
9
17
  import { Auth } from "firebase/auth";
10
18
  import { FirebaseApp } from "firebase/app";
@@ -21,6 +29,7 @@ import {
21
29
  PatientReviewInfo,
22
30
  LinkedFormInfo,
23
31
  type CreateAppointmentHttpData,
32
+ APPOINTMENTS_COLLECTION,
24
33
  } from "../../types/appointment";
25
34
  import {
26
35
  createAppointmentSchema,
@@ -1002,4 +1011,233 @@ export class AppointmentService extends BaseService {
1002
1011
 
1003
1012
  return this.updateAppointment(appointmentId, updateData);
1004
1013
  }
1014
+
1015
+ /**
1016
+ * Gets upcoming appointments for a specific patient.
1017
+ * These include appointments with statuses: PENDING, CONFIRMED, CHECKED_IN, IN_PROGRESS
1018
+ *
1019
+ * @param patientId ID of the patient
1020
+ * @param options Optional parameters for filtering and pagination
1021
+ * @returns Found appointments and the last document for pagination
1022
+ */
1023
+ async getUpcomingPatientAppointments(
1024
+ patientId: string,
1025
+ options?: {
1026
+ startDate?: Date; // Optional starting date (defaults to now)
1027
+ endDate?: Date;
1028
+ limit?: number;
1029
+ startAfter?: DocumentSnapshot;
1030
+ }
1031
+ ): Promise<{
1032
+ appointments: Appointment[];
1033
+ lastDoc: DocumentSnapshot | null;
1034
+ }> {
1035
+ try {
1036
+ console.log(
1037
+ `[APPOINTMENT_SERVICE] Getting upcoming appointments for patient: ${patientId}`
1038
+ );
1039
+
1040
+ // Default to current date/time if no startDate provided
1041
+ const effectiveStartDate = options?.startDate || new Date();
1042
+
1043
+ // Define the statuses considered as "upcoming"
1044
+ const upcomingStatuses = [
1045
+ AppointmentStatus.PENDING,
1046
+ AppointmentStatus.CONFIRMED,
1047
+ AppointmentStatus.CHECKED_IN,
1048
+ AppointmentStatus.IN_PROGRESS,
1049
+ AppointmentStatus.RESCHEDULED_BY_CLINIC,
1050
+ ];
1051
+
1052
+ // Build query constraints
1053
+ const constraints: QueryConstraint[] = [];
1054
+
1055
+ // Patient ID filter
1056
+ constraints.push(where("patientId", "==", patientId));
1057
+
1058
+ // Status filter - multiple statuses
1059
+ constraints.push(where("status", "in", upcomingStatuses));
1060
+
1061
+ // Date range filters
1062
+ constraints.push(
1063
+ where(
1064
+ "appointmentStartTime",
1065
+ ">=",
1066
+ Timestamp.fromDate(effectiveStartDate)
1067
+ )
1068
+ );
1069
+
1070
+ if (options?.endDate) {
1071
+ constraints.push(
1072
+ where(
1073
+ "appointmentStartTime",
1074
+ "<=",
1075
+ Timestamp.fromDate(options.endDate)
1076
+ )
1077
+ );
1078
+ }
1079
+
1080
+ // Order by appointment start time (ascending for upcoming - closest first)
1081
+ constraints.push(orderBy("appointmentStartTime", "asc"));
1082
+
1083
+ // Add pagination if specified
1084
+ if (options?.limit) {
1085
+ constraints.push(limit(options.limit));
1086
+ }
1087
+
1088
+ if (options?.startAfter) {
1089
+ constraints.push(startAfter(options.startAfter));
1090
+ }
1091
+
1092
+ // Execute query
1093
+ const q = query(
1094
+ collection(this.db, APPOINTMENTS_COLLECTION),
1095
+ ...constraints
1096
+ );
1097
+ const querySnapshot = await getDocs(q);
1098
+
1099
+ // Extract results
1100
+ const appointments = querySnapshot.docs.map(
1101
+ (doc) => doc.data() as Appointment
1102
+ );
1103
+
1104
+ // Get last document for pagination
1105
+ const lastDoc =
1106
+ querySnapshot.docs.length > 0
1107
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
1108
+ : null;
1109
+
1110
+ console.log(
1111
+ `[APPOINTMENT_SERVICE] Found ${appointments.length} upcoming appointments for patient ${patientId}`
1112
+ );
1113
+
1114
+ return { appointments, lastDoc };
1115
+ } catch (error) {
1116
+ console.error(
1117
+ `[APPOINTMENT_SERVICE] Error getting upcoming appointments for patient ${patientId}:`,
1118
+ error
1119
+ );
1120
+ throw error;
1121
+ }
1122
+ }
1123
+
1124
+ /**
1125
+ * Gets past appointments for a specific patient.
1126
+ * These include appointments with statuses: COMPLETED, CANCELED_PATIENT,
1127
+ * CANCELED_PATIENT_RESCHEDULED, CANCELED_CLINIC, NO_SHOW
1128
+ *
1129
+ * @param patientId ID of the patient
1130
+ * @param options Optional parameters for filtering and pagination
1131
+ * @returns Found appointments and the last document for pagination
1132
+ */
1133
+ async getPastPatientAppointments(
1134
+ patientId: string,
1135
+ options?: {
1136
+ startDate?: Date;
1137
+ endDate?: Date; // Optional end date (defaults to now)
1138
+ showCanceled?: boolean; // Whether to include canceled appointments
1139
+ showNoShow?: boolean; // Whether to include no-show appointments
1140
+ limit?: number;
1141
+ startAfter?: DocumentSnapshot;
1142
+ }
1143
+ ): Promise<{
1144
+ appointments: Appointment[];
1145
+ lastDoc: DocumentSnapshot | null;
1146
+ }> {
1147
+ try {
1148
+ console.log(
1149
+ `[APPOINTMENT_SERVICE] Getting past appointments for patient: ${patientId}`
1150
+ );
1151
+
1152
+ // Default to current date/time if no endDate provided
1153
+ const effectiveEndDate = options?.endDate || new Date();
1154
+
1155
+ // Define the base status for past appointments
1156
+ const pastStatuses: AppointmentStatus[] = [AppointmentStatus.COMPLETED];
1157
+
1158
+ // Add canceled statuses if requested
1159
+ if (options?.showCanceled) {
1160
+ pastStatuses.push(
1161
+ AppointmentStatus.CANCELED_PATIENT,
1162
+ AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
1163
+ AppointmentStatus.CANCELED_CLINIC
1164
+ );
1165
+ }
1166
+
1167
+ // Add no-show status if requested
1168
+ if (options?.showNoShow) {
1169
+ pastStatuses.push(AppointmentStatus.NO_SHOW);
1170
+ }
1171
+
1172
+ // Build query constraints
1173
+ const constraints: QueryConstraint[] = [];
1174
+
1175
+ // Patient ID filter
1176
+ constraints.push(where("patientId", "==", patientId));
1177
+
1178
+ // Status filter - multiple statuses
1179
+ constraints.push(where("status", "in", pastStatuses));
1180
+
1181
+ // Date range filters
1182
+ if (options?.startDate) {
1183
+ constraints.push(
1184
+ where(
1185
+ "appointmentStartTime",
1186
+ ">=",
1187
+ Timestamp.fromDate(options.startDate)
1188
+ )
1189
+ );
1190
+ }
1191
+
1192
+ constraints.push(
1193
+ where(
1194
+ "appointmentStartTime",
1195
+ "<=",
1196
+ Timestamp.fromDate(effectiveEndDate)
1197
+ )
1198
+ );
1199
+
1200
+ // Order by appointment start time (descending for past - most recent first)
1201
+ constraints.push(orderBy("appointmentStartTime", "desc"));
1202
+
1203
+ // Add pagination if specified
1204
+ if (options?.limit) {
1205
+ constraints.push(limit(options.limit));
1206
+ }
1207
+
1208
+ if (options?.startAfter) {
1209
+ constraints.push(startAfter(options.startAfter));
1210
+ }
1211
+
1212
+ // Execute query
1213
+ const q = query(
1214
+ collection(this.db, APPOINTMENTS_COLLECTION),
1215
+ ...constraints
1216
+ );
1217
+ const querySnapshot = await getDocs(q);
1218
+
1219
+ // Extract results
1220
+ const appointments = querySnapshot.docs.map(
1221
+ (doc) => doc.data() as Appointment
1222
+ );
1223
+
1224
+ // Get last document for pagination
1225
+ const lastDoc =
1226
+ querySnapshot.docs.length > 0
1227
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
1228
+ : null;
1229
+
1230
+ console.log(
1231
+ `[APPOINTMENT_SERVICE] Found ${appointments.length} past appointments for patient ${patientId}`
1232
+ );
1233
+
1234
+ return { appointments, lastDoc };
1235
+ } catch (error) {
1236
+ console.error(
1237
+ `[APPOINTMENT_SERVICE] Error getting past appointments for patient ${patientId}:`,
1238
+ error
1239
+ );
1240
+ throw error;
1241
+ }
1242
+ }
1005
1243
  }