@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.
- package/dist/admin/index.js +1 -3
- package/dist/admin/index.mjs +1 -3
- package/dist/index.d.mts +40 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +167 -22
- package/dist/index.mjs +238 -86
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +0 -2
- package/src/admin/users/user-profile.admin.ts +404 -0
- package/src/config/firebase.ts +17 -1
- package/src/services/appointment/appointment.service.ts +238 -0
- package/src/services/auth.v2.service.ts +959 -0
- package/src/services/user.v2.service.ts +466 -0
|
@@ -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
|
+
}
|
package/src/config/firebase.ts
CHANGED
|
@@ -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
|
}
|