@blackcode_sa/metaestetics-api 1.6.15 → 1.6.17
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.js +160 -76
- package/dist/admin/index.mjs +160 -76
- package/dist/index.d.mts +37 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +145 -4
- package/dist/index.mjs +227 -79
- package/package.json +1 -1
- package/src/admin/notifications/notifications.admin.ts +140 -28
- package/src/services/appointment/appointment.service.ts +238 -0
|
@@ -9,6 +9,7 @@ import { Appointment, PaymentStatus } from "../../types/appointment";
|
|
|
9
9
|
import { UserRole } from "../../types";
|
|
10
10
|
import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
|
|
11
11
|
import { TimestampUtils } from "../../utils/TimestampUtils";
|
|
12
|
+
import { Logger } from "../logger";
|
|
12
13
|
|
|
13
14
|
export class NotificationsAdmin {
|
|
14
15
|
private expo: Expo;
|
|
@@ -46,19 +47,31 @@ export class NotificationsAdmin {
|
|
|
46
47
|
* Priprema Expo poruku za slanje
|
|
47
48
|
*/
|
|
48
49
|
private prepareExpoMessage(notification: Notification): ExpoPushMessage[] {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
const validTokens = notification.notificationTokens.filter((token) =>
|
|
51
|
+
Expo.isExpoPushToken(token)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
Logger.info(
|
|
55
|
+
`[NotificationsAdmin] Preparing Expo messages for notification ${notification.id}`,
|
|
56
|
+
{
|
|
57
|
+
totalTokens: notification.notificationTokens.length,
|
|
58
|
+
validTokens: validTokens.length,
|
|
59
|
+
invalidTokensCount:
|
|
60
|
+
notification.notificationTokens.length - validTokens.length,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return validTokens.map((token) => ({
|
|
65
|
+
to: token,
|
|
66
|
+
sound: "default",
|
|
67
|
+
title: notification.title,
|
|
68
|
+
body: notification.body,
|
|
69
|
+
data: {
|
|
70
|
+
notificationId: notification.id,
|
|
71
|
+
notificationType: notification.notificationType,
|
|
72
|
+
userId: notification.userId,
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
/**
|
|
@@ -93,27 +106,58 @@ export class NotificationsAdmin {
|
|
|
93
106
|
*/
|
|
94
107
|
async sendPushNotification(notification: Notification): Promise<boolean> {
|
|
95
108
|
try {
|
|
109
|
+
Logger.info(
|
|
110
|
+
`[NotificationsAdmin] Processing notification ${notification.id} for sending`,
|
|
111
|
+
{
|
|
112
|
+
userId: notification.userId,
|
|
113
|
+
tokenCount: notification.notificationTokens?.length || 0,
|
|
114
|
+
type: notification.notificationType,
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
96
118
|
const messages = this.prepareExpoMessage(notification);
|
|
97
119
|
|
|
98
120
|
if (messages.length === 0) {
|
|
121
|
+
const errorMsg = "No valid notification tokens found";
|
|
122
|
+
Logger.error(
|
|
123
|
+
`[NotificationsAdmin] ${errorMsg} for notification ${notification.id}`
|
|
124
|
+
);
|
|
99
125
|
await this.updateNotificationStatus(
|
|
100
126
|
notification.id!,
|
|
101
127
|
NotificationStatus.FAILED,
|
|
102
|
-
|
|
128
|
+
errorMsg
|
|
103
129
|
);
|
|
104
130
|
return false;
|
|
105
131
|
}
|
|
106
132
|
|
|
107
133
|
const chunks = this.expo.chunkPushNotifications(messages);
|
|
134
|
+
Logger.info(
|
|
135
|
+
`[NotificationsAdmin] Sending ${messages.length} messages in ${chunks.length} chunks for notification ${notification.id}`
|
|
136
|
+
);
|
|
137
|
+
|
|
108
138
|
const tickets: ExpoPushTicket[][] = [];
|
|
109
139
|
|
|
110
140
|
// Šaljemo sve chunks
|
|
111
|
-
for (
|
|
141
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
142
|
+
const chunk = chunks[i];
|
|
112
143
|
try {
|
|
144
|
+
Logger.info(
|
|
145
|
+
`[NotificationsAdmin] Sending chunk ${i + 1}/${
|
|
146
|
+
chunks.length
|
|
147
|
+
} with ${chunk.length} messages`
|
|
148
|
+
);
|
|
113
149
|
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
150
|
+
Logger.info(
|
|
151
|
+
`[NotificationsAdmin] Received ${
|
|
152
|
+
ticketChunk.length
|
|
153
|
+
} tickets for chunk ${i + 1}`
|
|
154
|
+
);
|
|
114
155
|
tickets.push(ticketChunk);
|
|
115
156
|
} catch (error) {
|
|
116
|
-
|
|
157
|
+
Logger.error(
|
|
158
|
+
`[NotificationsAdmin] Chunk ${i + 1} sending error:`,
|
|
159
|
+
error
|
|
160
|
+
);
|
|
117
161
|
throw error;
|
|
118
162
|
}
|
|
119
163
|
}
|
|
@@ -121,25 +165,62 @@ export class NotificationsAdmin {
|
|
|
121
165
|
// Proveravamo rezultate
|
|
122
166
|
let hasErrors = false;
|
|
123
167
|
const errors: string[] = [];
|
|
168
|
+
const ticketsFlat = tickets.flat();
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
// Log detailed ticket information
|
|
171
|
+
const ticketResults = {
|
|
172
|
+
total: ticketsFlat.length,
|
|
173
|
+
success: 0,
|
|
174
|
+
error: 0,
|
|
175
|
+
errorDetails: {} as Record<string, number>,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
ticketsFlat.forEach((ticket, index) => {
|
|
126
179
|
if (ticket.status === "error") {
|
|
127
180
|
hasErrors = true;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
181
|
+
ticketResults.error++;
|
|
182
|
+
|
|
183
|
+
// Count each error type
|
|
184
|
+
const errorMessage = ticket.message || "Unknown error";
|
|
185
|
+
ticketResults.errorDetails[errorMessage] =
|
|
186
|
+
(ticketResults.errorDetails[errorMessage] || 0) + 1;
|
|
187
|
+
|
|
188
|
+
const tokenInfo =
|
|
189
|
+
index < notification.notificationTokens.length
|
|
190
|
+
? `Token ${notification.notificationTokens[index]}`
|
|
191
|
+
: `Token at index ${index}`;
|
|
192
|
+
|
|
193
|
+
errors.push(`${tokenInfo}: ${errorMessage}`);
|
|
194
|
+
} else {
|
|
195
|
+
ticketResults.success++;
|
|
131
196
|
}
|
|
132
197
|
});
|
|
133
198
|
|
|
199
|
+
Logger.info(
|
|
200
|
+
`[NotificationsAdmin] Ticket results for notification ${notification.id}`,
|
|
201
|
+
ticketResults
|
|
202
|
+
);
|
|
203
|
+
|
|
134
204
|
if (hasErrors) {
|
|
205
|
+
const errorSummary = errors.join("; ");
|
|
206
|
+
Logger.warn(
|
|
207
|
+
`[NotificationsAdmin] Partial success or errors in notification ${notification.id}`,
|
|
208
|
+
{ errorCount: errors.length, errorSummary }
|
|
209
|
+
);
|
|
210
|
+
|
|
135
211
|
await this.updateNotificationStatus(
|
|
136
212
|
notification.id!,
|
|
137
|
-
|
|
138
|
-
|
|
213
|
+
ticketResults.success > 0
|
|
214
|
+
? NotificationStatus.PARTIAL_SUCCESS
|
|
215
|
+
: NotificationStatus.FAILED,
|
|
216
|
+
errorSummary
|
|
139
217
|
);
|
|
140
|
-
return
|
|
218
|
+
return ticketResults.success > 0;
|
|
141
219
|
}
|
|
142
220
|
|
|
221
|
+
Logger.info(
|
|
222
|
+
`[NotificationsAdmin] Successfully sent notification ${notification.id} to all recipients`
|
|
223
|
+
);
|
|
143
224
|
await this.updateNotificationStatus(
|
|
144
225
|
notification.id!,
|
|
145
226
|
NotificationStatus.SENT
|
|
@@ -148,7 +229,10 @@ export class NotificationsAdmin {
|
|
|
148
229
|
} catch (error) {
|
|
149
230
|
const errorMessage =
|
|
150
231
|
error instanceof Error ? error.message : "Unknown error";
|
|
151
|
-
|
|
232
|
+
Logger.error(
|
|
233
|
+
`[NotificationsAdmin] Critical error sending notification ${notification.id}:`,
|
|
234
|
+
error
|
|
235
|
+
);
|
|
152
236
|
await this.updateNotificationStatus(
|
|
153
237
|
notification.id!,
|
|
154
238
|
NotificationStatus.FAILED,
|
|
@@ -164,6 +248,10 @@ export class NotificationsAdmin {
|
|
|
164
248
|
async processPendingNotifications(batchSize: number = 100): Promise<void> {
|
|
165
249
|
const now = admin.firestore.Timestamp.now();
|
|
166
250
|
|
|
251
|
+
Logger.info(
|
|
252
|
+
`[NotificationsAdmin] Starting to process pending notifications with batch size ${batchSize}`
|
|
253
|
+
);
|
|
254
|
+
|
|
167
255
|
const pendingNotifications = await this.db
|
|
168
256
|
.collection("notifications")
|
|
169
257
|
.where("status", "==", NotificationStatus.PENDING)
|
|
@@ -171,12 +259,27 @@ export class NotificationsAdmin {
|
|
|
171
259
|
.limit(batchSize)
|
|
172
260
|
.get();
|
|
173
261
|
|
|
262
|
+
Logger.info(
|
|
263
|
+
`[NotificationsAdmin] Found ${pendingNotifications.size} pending notifications to process`
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (pendingNotifications.empty) {
|
|
267
|
+
Logger.info(
|
|
268
|
+
"[NotificationsAdmin] No pending notifications found to process"
|
|
269
|
+
);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
174
273
|
const results = await Promise.allSettled(
|
|
175
274
|
pendingNotifications.docs.map(async (doc) => {
|
|
176
275
|
const notification = {
|
|
177
276
|
id: doc.id,
|
|
178
277
|
...doc.data(),
|
|
179
278
|
} as Notification;
|
|
279
|
+
|
|
280
|
+
Logger.info(
|
|
281
|
+
`[NotificationsAdmin] Processing notification ${notification.id} of type ${notification.notificationType}`
|
|
282
|
+
);
|
|
180
283
|
return this.sendPushNotification(notification);
|
|
181
284
|
})
|
|
182
285
|
);
|
|
@@ -189,8 +292,8 @@ export class NotificationsAdmin {
|
|
|
189
292
|
(r) => r.status === "rejected" || (r.status === "fulfilled" && !r.value)
|
|
190
293
|
).length;
|
|
191
294
|
|
|
192
|
-
|
|
193
|
-
`Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
|
|
295
|
+
Logger.info(
|
|
296
|
+
`[NotificationsAdmin] Processed ${results.length} notifications: ${successful} successful, ${failed} failed`
|
|
194
297
|
);
|
|
195
298
|
}
|
|
196
299
|
|
|
@@ -204,6 +307,11 @@ export class NotificationsAdmin {
|
|
|
204
307
|
const cutoffDate = new Date();
|
|
205
308
|
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
206
309
|
|
|
310
|
+
Logger.info(
|
|
311
|
+
`[NotificationsAdmin] Starting cleanup of notifications older than ${daysOld} days`
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
let totalDeleted = 0;
|
|
207
315
|
while (true) {
|
|
208
316
|
const oldNotifications = await this.db
|
|
209
317
|
.collection("notifications")
|
|
@@ -216,6 +324,9 @@ export class NotificationsAdmin {
|
|
|
216
324
|
.get();
|
|
217
325
|
|
|
218
326
|
if (oldNotifications.empty) {
|
|
327
|
+
Logger.info(
|
|
328
|
+
`[NotificationsAdmin] No more old notifications to delete. Total deleted: ${totalDeleted}`
|
|
329
|
+
);
|
|
219
330
|
break;
|
|
220
331
|
}
|
|
221
332
|
|
|
@@ -225,8 +336,9 @@ export class NotificationsAdmin {
|
|
|
225
336
|
});
|
|
226
337
|
|
|
227
338
|
await batch.commit();
|
|
228
|
-
|
|
229
|
-
|
|
339
|
+
totalDeleted += oldNotifications.size;
|
|
340
|
+
Logger.info(
|
|
341
|
+
`[NotificationsAdmin] Deleted batch of ${oldNotifications.size} old notifications. Running total: ${totalDeleted}`
|
|
230
342
|
);
|
|
231
343
|
}
|
|
232
344
|
}
|
|
@@ -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
|
}
|