@blackcode_sa/metaestetics-api 1.12.7 → 1.12.10
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 +51 -33
- package/dist/admin/index.d.ts +51 -33
- package/dist/admin/index.js +30 -9
- package/dist/admin/index.mjs +30 -9
- package/dist/index.d.mts +79 -65
- package/dist/index.d.ts +79 -65
- package/dist/index.js +301 -616
- package/dist/index.mjs +315 -631
- package/package.json +1 -1
- package/src/admin/aggregation/forms/README.md +13 -0
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +71 -51
- package/src/services/appointment/README.md +17 -0
- package/src/services/appointment/appointment.service.ts +233 -386
- package/src/services/auth/auth.service.ts +243 -466
- package/src/services/clinic/billing-transactions.service.ts +4 -1
- package/src/services/clinic/index.ts +5 -5
- package/src/types/clinic/preferences.types.ts +115 -97
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
limit,
|
|
14
14
|
startAfter,
|
|
15
15
|
getDocs,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
16
|
+
getCountFromServer,
|
|
17
|
+
} from 'firebase/firestore';
|
|
18
|
+
import { Auth } from 'firebase/auth';
|
|
19
|
+
import { FirebaseApp } from 'firebase/app';
|
|
20
|
+
import { Functions, getFunctions, httpsCallable } from 'firebase/functions';
|
|
21
|
+
import { BaseService } from '../base.service';
|
|
21
22
|
import {
|
|
22
23
|
Appointment,
|
|
23
24
|
AppointmentStatus,
|
|
@@ -28,26 +29,26 @@ import {
|
|
|
28
29
|
PatientReviewInfo,
|
|
29
30
|
type CreateAppointmentHttpData,
|
|
30
31
|
APPOINTMENTS_COLLECTION,
|
|
31
|
-
} from
|
|
32
|
+
} from '../../types/appointment';
|
|
32
33
|
import {
|
|
33
34
|
updateAppointmentSchema,
|
|
34
35
|
searchAppointmentsSchema,
|
|
35
36
|
rescheduleAppointmentSchema,
|
|
36
|
-
} from
|
|
37
|
+
} from '../../validations/appointment.schema';
|
|
37
38
|
|
|
38
39
|
// Import other services needed (dependency injection pattern)
|
|
39
|
-
import { CalendarServiceV2 } from
|
|
40
|
-
import { PatientService } from
|
|
41
|
-
import { PractitionerService } from
|
|
42
|
-
import { ClinicService } from
|
|
43
|
-
import { FilledDocumentService } from
|
|
40
|
+
import { CalendarServiceV2 } from '../calendar/calendar.v2.service';
|
|
41
|
+
import { PatientService } from '../patient/patient.service';
|
|
42
|
+
import { PractitionerService } from '../practitioner/practitioner.service';
|
|
43
|
+
import { ClinicService } from '../clinic/clinic.service';
|
|
44
|
+
import { FilledDocumentService } from '../documentation-templates/filled-document.service';
|
|
44
45
|
|
|
45
46
|
// Import utility functions
|
|
46
47
|
import {
|
|
47
48
|
updateAppointmentUtil,
|
|
48
49
|
getAppointmentByIdUtil,
|
|
49
50
|
searchAppointmentsUtil,
|
|
50
|
-
} from
|
|
51
|
+
} from './utils/appointment.utils';
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Interface for available booking slot
|
|
@@ -88,7 +89,7 @@ export class AppointmentService extends BaseService {
|
|
|
88
89
|
patientService: PatientService,
|
|
89
90
|
practitionerService: PractitionerService,
|
|
90
91
|
clinicService: ClinicService,
|
|
91
|
-
filledDocumentService: FilledDocumentService
|
|
92
|
+
filledDocumentService: FilledDocumentService,
|
|
92
93
|
) {
|
|
93
94
|
super(db, auth, app);
|
|
94
95
|
this.calendarService = calendarService;
|
|
@@ -96,7 +97,7 @@ export class AppointmentService extends BaseService {
|
|
|
96
97
|
this.practitionerService = practitionerService;
|
|
97
98
|
this.clinicService = clinicService;
|
|
98
99
|
this.filledDocumentService = filledDocumentService;
|
|
99
|
-
this.functions = getFunctions(app,
|
|
100
|
+
this.functions = getFunctions(app, 'europe-west6'); // Initialize Firebase Functions with the correct region
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
/**
|
|
@@ -115,36 +116,26 @@ export class AppointmentService extends BaseService {
|
|
|
115
116
|
practitionerId: string,
|
|
116
117
|
procedureId: string,
|
|
117
118
|
startDate: Date,
|
|
118
|
-
endDate: Date
|
|
119
|
+
endDate: Date,
|
|
119
120
|
): Promise<AvailableSlot[]> {
|
|
120
121
|
try {
|
|
121
122
|
console.log(
|
|
122
|
-
`[APPOINTMENT_SERVICE] Getting available booking slots via HTTP for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}
|
|
123
|
+
`[APPOINTMENT_SERVICE] Getting available booking slots via HTTP for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`,
|
|
123
124
|
);
|
|
124
125
|
|
|
125
126
|
// Validate input parameters
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
!practitionerId ||
|
|
129
|
-
!procedureId ||
|
|
130
|
-
!startDate ||
|
|
131
|
-
!endDate
|
|
132
|
-
) {
|
|
133
|
-
throw new Error(
|
|
134
|
-
"Missing required parameters for booking slots calculation"
|
|
135
|
-
);
|
|
127
|
+
if (!clinicId || !practitionerId || !procedureId || !startDate || !endDate) {
|
|
128
|
+
throw new Error('Missing required parameters for booking slots calculation');
|
|
136
129
|
}
|
|
137
130
|
|
|
138
131
|
if (endDate <= startDate) {
|
|
139
|
-
throw new Error(
|
|
132
|
+
throw new Error('End date must be after start date');
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
// Check if user is authenticated
|
|
143
136
|
const currentUser = this.auth.currentUser;
|
|
144
137
|
if (!currentUser) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
"User must be authenticated to get available booking slots"
|
|
147
|
-
);
|
|
138
|
+
throw new Error('User must be authenticated to get available booking slots');
|
|
148
139
|
}
|
|
149
140
|
|
|
150
141
|
// Construct the function URL for the Express app endpoint
|
|
@@ -156,9 +147,7 @@ export class AppointmentService extends BaseService {
|
|
|
156
147
|
const idToken = await currentUser.getIdToken();
|
|
157
148
|
|
|
158
149
|
// Log that we're getting a token
|
|
159
|
-
console.log(
|
|
160
|
-
`[APPOINTMENT_SERVICE] Got user token, user ID: ${currentUser.uid}`
|
|
161
|
-
);
|
|
150
|
+
console.log(`[APPOINTMENT_SERVICE] Got user token, user ID: ${currentUser.uid}`);
|
|
162
151
|
|
|
163
152
|
// Alternate direct URL (if needed):
|
|
164
153
|
// const functionUrl = `https://getavailablebookingslotshttp-grqala5m6a-oa.a.run.app`;
|
|
@@ -174,37 +163,33 @@ export class AppointmentService extends BaseService {
|
|
|
174
163
|
},
|
|
175
164
|
};
|
|
176
165
|
|
|
177
|
-
console.log(
|
|
178
|
-
`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`
|
|
179
|
-
);
|
|
166
|
+
console.log(`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`);
|
|
180
167
|
|
|
181
168
|
// Make the HTTP request with expanded CORS options for browser
|
|
182
169
|
const response = await fetch(functionUrl, {
|
|
183
|
-
method:
|
|
184
|
-
mode:
|
|
185
|
-
cache:
|
|
186
|
-
credentials:
|
|
170
|
+
method: 'POST',
|
|
171
|
+
mode: 'cors', // Important for cross-origin requests
|
|
172
|
+
cache: 'no-cache', // Don't cache this request
|
|
173
|
+
credentials: 'omit', // Don't send cookies since we're using token auth
|
|
187
174
|
headers: {
|
|
188
|
-
|
|
175
|
+
'Content-Type': 'application/json',
|
|
189
176
|
Authorization: `Bearer ${idToken}`,
|
|
190
177
|
},
|
|
191
|
-
redirect:
|
|
192
|
-
referrerPolicy:
|
|
178
|
+
redirect: 'follow',
|
|
179
|
+
referrerPolicy: 'no-referrer',
|
|
193
180
|
body: JSON.stringify(requestData),
|
|
194
181
|
});
|
|
195
182
|
|
|
196
183
|
console.log(
|
|
197
|
-
`[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}
|
|
184
|
+
`[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`,
|
|
198
185
|
);
|
|
199
186
|
|
|
200
187
|
// Check if the request was successful
|
|
201
188
|
if (!response.ok) {
|
|
202
189
|
const errorText = await response.text();
|
|
203
|
-
console.error(
|
|
204
|
-
`[APPOINTMENT_SERVICE] Error response details: ${errorText}`
|
|
205
|
-
);
|
|
190
|
+
console.error(`[APPOINTMENT_SERVICE] Error response details: ${errorText}`);
|
|
206
191
|
throw new Error(
|
|
207
|
-
`Failed to get available booking slots: ${response.status} ${response.statusText} - ${errorText}
|
|
192
|
+
`Failed to get available booking slots: ${response.status} ${response.statusText} - ${errorText}`,
|
|
208
193
|
);
|
|
209
194
|
}
|
|
210
195
|
|
|
@@ -213,28 +198,19 @@ export class AppointmentService extends BaseService {
|
|
|
213
198
|
console.log(`[APPOINTMENT_SERVICE] Response parsed successfully`, result);
|
|
214
199
|
|
|
215
200
|
if (!result.success) {
|
|
216
|
-
throw new Error(
|
|
217
|
-
result.error || "Failed to get available booking slots"
|
|
218
|
-
);
|
|
201
|
+
throw new Error(result.error || 'Failed to get available booking slots');
|
|
219
202
|
}
|
|
220
203
|
|
|
221
204
|
// Convert timestamp numbers to Date objects
|
|
222
|
-
const slots: AvailableSlot[] = result.availableSlots.map(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
})
|
|
226
|
-
);
|
|
205
|
+
const slots: AvailableSlot[] = result.availableSlots.map((slot: { start: number }) => ({
|
|
206
|
+
start: new Date(slot.start),
|
|
207
|
+
}));
|
|
227
208
|
|
|
228
|
-
console.log(
|
|
229
|
-
`[APPOINTMENT_SERVICE] Found ${slots.length} available booking slots via HTTP`
|
|
230
|
-
);
|
|
209
|
+
console.log(`[APPOINTMENT_SERVICE] Found ${slots.length} available booking slots via HTTP`);
|
|
231
210
|
|
|
232
211
|
return slots;
|
|
233
212
|
} catch (error) {
|
|
234
|
-
console.error(
|
|
235
|
-
"[APPOINTMENT_SERVICE] Error getting available booking slots via HTTP:",
|
|
236
|
-
error
|
|
237
|
-
);
|
|
213
|
+
console.error('[APPOINTMENT_SERVICE] Error getting available booking slots via HTTP:', error);
|
|
238
214
|
throw error;
|
|
239
215
|
}
|
|
240
216
|
}
|
|
@@ -245,18 +221,14 @@ export class AppointmentService extends BaseService {
|
|
|
245
221
|
* @param data - CreateAppointmentData object
|
|
246
222
|
* @returns The created appointment
|
|
247
223
|
*/
|
|
248
|
-
async createAppointmentHttp(
|
|
249
|
-
data: CreateAppointmentHttpData
|
|
250
|
-
): Promise<Appointment> {
|
|
224
|
+
async createAppointmentHttp(data: CreateAppointmentHttpData): Promise<Appointment> {
|
|
251
225
|
try {
|
|
252
|
-
console.log(
|
|
253
|
-
"[APPOINTMENT_SERVICE] Creating appointment via cloud function"
|
|
254
|
-
);
|
|
226
|
+
console.log('[APPOINTMENT_SERVICE] Creating appointment via cloud function');
|
|
255
227
|
|
|
256
228
|
// Get the authenticated user's ID token
|
|
257
229
|
const currentUser = this.auth.currentUser;
|
|
258
230
|
if (!currentUser) {
|
|
259
|
-
throw new Error(
|
|
231
|
+
throw new Error('User must be authenticated to create an appointment');
|
|
260
232
|
}
|
|
261
233
|
const idToken = await currentUser.getIdToken();
|
|
262
234
|
|
|
@@ -277,37 +249,33 @@ export class AppointmentService extends BaseService {
|
|
|
277
249
|
patientNotes: data?.patientNotes || null,
|
|
278
250
|
};
|
|
279
251
|
|
|
280
|
-
console.log(
|
|
281
|
-
`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`
|
|
282
|
-
);
|
|
252
|
+
console.log(`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`);
|
|
283
253
|
|
|
284
254
|
// Make the HTTP request with expanded CORS options for browser
|
|
285
255
|
const response = await fetch(functionUrl, {
|
|
286
|
-
method:
|
|
287
|
-
mode:
|
|
288
|
-
cache:
|
|
289
|
-
credentials:
|
|
256
|
+
method: 'POST',
|
|
257
|
+
mode: 'cors',
|
|
258
|
+
cache: 'no-cache',
|
|
259
|
+
credentials: 'omit',
|
|
290
260
|
headers: {
|
|
291
|
-
|
|
261
|
+
'Content-Type': 'application/json',
|
|
292
262
|
Authorization: `Bearer ${idToken}`,
|
|
293
263
|
},
|
|
294
|
-
redirect:
|
|
295
|
-
referrerPolicy:
|
|
264
|
+
redirect: 'follow',
|
|
265
|
+
referrerPolicy: 'no-referrer',
|
|
296
266
|
body: JSON.stringify(requestData),
|
|
297
267
|
});
|
|
298
268
|
|
|
299
269
|
console.log(
|
|
300
|
-
`[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}
|
|
270
|
+
`[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`,
|
|
301
271
|
);
|
|
302
272
|
|
|
303
273
|
// Check if the request was successful
|
|
304
274
|
if (!response.ok) {
|
|
305
275
|
const errorText = await response.text();
|
|
306
|
-
console.error(
|
|
307
|
-
`[APPOINTMENT_SERVICE] Error response details: ${errorText}`
|
|
308
|
-
);
|
|
276
|
+
console.error(`[APPOINTMENT_SERVICE] Error response details: ${errorText}`);
|
|
309
277
|
throw new Error(
|
|
310
|
-
`Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}
|
|
278
|
+
`Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`,
|
|
311
279
|
);
|
|
312
280
|
}
|
|
313
281
|
|
|
@@ -315,33 +283,24 @@ export class AppointmentService extends BaseService {
|
|
|
315
283
|
const result = await response.json();
|
|
316
284
|
|
|
317
285
|
if (!result.success) {
|
|
318
|
-
throw new Error(result.error ||
|
|
286
|
+
throw new Error(result.error || 'Failed to create appointment');
|
|
319
287
|
}
|
|
320
288
|
|
|
321
289
|
// If the backend returns the full appointment data
|
|
322
290
|
if (result.appointmentData) {
|
|
323
|
-
console.log(
|
|
324
|
-
`[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`
|
|
325
|
-
);
|
|
291
|
+
console.log(`[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`);
|
|
326
292
|
return result.appointmentData;
|
|
327
293
|
}
|
|
328
294
|
|
|
329
295
|
// If only the ID is returned, fetch the complete appointment
|
|
330
|
-
const createdAppointment = await this.getAppointmentById(
|
|
331
|
-
result.appointmentId
|
|
332
|
-
);
|
|
296
|
+
const createdAppointment = await this.getAppointmentById(result.appointmentId);
|
|
333
297
|
if (!createdAppointment) {
|
|
334
|
-
throw new Error(
|
|
335
|
-
`Failed to retrieve created appointment with ID: ${result.appointmentId}`
|
|
336
|
-
);
|
|
298
|
+
throw new Error(`Failed to retrieve created appointment with ID: ${result.appointmentId}`);
|
|
337
299
|
}
|
|
338
300
|
|
|
339
301
|
return createdAppointment;
|
|
340
302
|
} catch (error) {
|
|
341
|
-
console.error(
|
|
342
|
-
"[APPOINTMENT_SERVICE] Error creating appointment via cloud function:",
|
|
343
|
-
error
|
|
344
|
-
);
|
|
303
|
+
console.error('[APPOINTMENT_SERVICE] Error creating appointment via cloud function:', error);
|
|
345
304
|
throw error;
|
|
346
305
|
}
|
|
347
306
|
}
|
|
@@ -354,24 +313,17 @@ export class AppointmentService extends BaseService {
|
|
|
354
313
|
*/
|
|
355
314
|
async getAppointmentById(appointmentId: string): Promise<Appointment | null> {
|
|
356
315
|
try {
|
|
357
|
-
console.log(
|
|
358
|
-
`[APPOINTMENT_SERVICE] Getting appointment with ID: ${appointmentId}`
|
|
359
|
-
);
|
|
316
|
+
console.log(`[APPOINTMENT_SERVICE] Getting appointment with ID: ${appointmentId}`);
|
|
360
317
|
|
|
361
318
|
const appointment = await getAppointmentByIdUtil(this.db, appointmentId);
|
|
362
319
|
|
|
363
320
|
console.log(
|
|
364
|
-
`[APPOINTMENT_SERVICE] Appointment ${appointmentId} ${
|
|
365
|
-
appointment ? "found" : "not found"
|
|
366
|
-
}`
|
|
321
|
+
`[APPOINTMENT_SERVICE] Appointment ${appointmentId} ${appointment ? 'found' : 'not found'}`,
|
|
367
322
|
);
|
|
368
323
|
|
|
369
324
|
return appointment;
|
|
370
325
|
} catch (error) {
|
|
371
|
-
console.error(
|
|
372
|
-
`[APPOINTMENT_SERVICE] Error getting appointment ${appointmentId}:`,
|
|
373
|
-
error
|
|
374
|
-
);
|
|
326
|
+
console.error(`[APPOINTMENT_SERVICE] Error getting appointment ${appointmentId}:`, error);
|
|
375
327
|
throw error;
|
|
376
328
|
}
|
|
377
329
|
}
|
|
@@ -385,33 +337,22 @@ export class AppointmentService extends BaseService {
|
|
|
385
337
|
*/
|
|
386
338
|
async updateAppointment(
|
|
387
339
|
appointmentId: string,
|
|
388
|
-
data: UpdateAppointmentData
|
|
340
|
+
data: UpdateAppointmentData,
|
|
389
341
|
): Promise<Appointment> {
|
|
390
342
|
try {
|
|
391
|
-
console.log(
|
|
392
|
-
`[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`
|
|
393
|
-
);
|
|
343
|
+
console.log(`[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`);
|
|
394
344
|
|
|
395
345
|
// Validate input data
|
|
396
346
|
const validatedData = await updateAppointmentSchema.parseAsync(data);
|
|
397
347
|
|
|
398
348
|
// Update the appointment using the utility function
|
|
399
|
-
const updatedAppointment = await updateAppointmentUtil(
|
|
400
|
-
this.db,
|
|
401
|
-
appointmentId,
|
|
402
|
-
validatedData
|
|
403
|
-
);
|
|
349
|
+
const updatedAppointment = await updateAppointmentUtil(this.db, appointmentId, validatedData);
|
|
404
350
|
|
|
405
|
-
console.log(
|
|
406
|
-
`[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`
|
|
407
|
-
);
|
|
351
|
+
console.log(`[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`);
|
|
408
352
|
|
|
409
353
|
return updatedAppointment;
|
|
410
354
|
} catch (error) {
|
|
411
|
-
console.error(
|
|
412
|
-
`[APPOINTMENT_SERVICE] Error updating appointment ${appointmentId}:`,
|
|
413
|
-
error
|
|
414
|
-
);
|
|
355
|
+
console.error(`[APPOINTMENT_SERVICE] Error updating appointment ${appointmentId}:`, error);
|
|
415
356
|
throw error;
|
|
416
357
|
}
|
|
417
358
|
}
|
|
@@ -427,10 +368,7 @@ export class AppointmentService extends BaseService {
|
|
|
427
368
|
lastDoc: DocumentSnapshot | null;
|
|
428
369
|
}> {
|
|
429
370
|
try {
|
|
430
|
-
console.log(
|
|
431
|
-
"[APPOINTMENT_SERVICE] Searching appointments with params:",
|
|
432
|
-
params
|
|
433
|
-
);
|
|
371
|
+
console.log('[APPOINTMENT_SERVICE] Searching appointments with params:', params);
|
|
434
372
|
|
|
435
373
|
// Validate search parameters
|
|
436
374
|
await searchAppointmentsSchema.parseAsync(params);
|
|
@@ -438,16 +376,11 @@ export class AppointmentService extends BaseService {
|
|
|
438
376
|
// Search for appointments using the utility function
|
|
439
377
|
const result = await searchAppointmentsUtil(this.db, params);
|
|
440
378
|
|
|
441
|
-
console.log(
|
|
442
|
-
`[APPOINTMENT_SERVICE] Found ${result.appointments.length} appointments`
|
|
443
|
-
);
|
|
379
|
+
console.log(`[APPOINTMENT_SERVICE] Found ${result.appointments.length} appointments`);
|
|
444
380
|
|
|
445
381
|
return result;
|
|
446
382
|
} catch (error) {
|
|
447
|
-
console.error(
|
|
448
|
-
"[APPOINTMENT_SERVICE] Error searching appointments:",
|
|
449
|
-
error
|
|
450
|
-
);
|
|
383
|
+
console.error('[APPOINTMENT_SERVICE] Error searching appointments:', error);
|
|
451
384
|
throw error;
|
|
452
385
|
}
|
|
453
386
|
}
|
|
@@ -467,14 +400,12 @@ export class AppointmentService extends BaseService {
|
|
|
467
400
|
status?: AppointmentStatus | AppointmentStatus[];
|
|
468
401
|
limit?: number;
|
|
469
402
|
startAfter?: DocumentSnapshot;
|
|
470
|
-
}
|
|
403
|
+
},
|
|
471
404
|
): Promise<{
|
|
472
405
|
appointments: Appointment[];
|
|
473
406
|
lastDoc: DocumentSnapshot | null;
|
|
474
407
|
}> {
|
|
475
|
-
console.log(
|
|
476
|
-
`[APPOINTMENT_SERVICE] Getting appointments for patient: ${patientId}`
|
|
477
|
-
);
|
|
408
|
+
console.log(`[APPOINTMENT_SERVICE] Getting appointments for patient: ${patientId}`);
|
|
478
409
|
|
|
479
410
|
const searchParams: SearchAppointmentsParams = {
|
|
480
411
|
patientId,
|
|
@@ -503,14 +434,12 @@ export class AppointmentService extends BaseService {
|
|
|
503
434
|
status?: AppointmentStatus | AppointmentStatus[];
|
|
504
435
|
limit?: number;
|
|
505
436
|
startAfter?: DocumentSnapshot;
|
|
506
|
-
}
|
|
437
|
+
},
|
|
507
438
|
): Promise<{
|
|
508
439
|
appointments: Appointment[];
|
|
509
440
|
lastDoc: DocumentSnapshot | null;
|
|
510
441
|
}> {
|
|
511
|
-
console.log(
|
|
512
|
-
`[APPOINTMENT_SERVICE] Getting appointments for practitioner: ${practitionerId}`
|
|
513
|
-
);
|
|
442
|
+
console.log(`[APPOINTMENT_SERVICE] Getting appointments for practitioner: ${practitionerId}`);
|
|
514
443
|
|
|
515
444
|
const searchParams: SearchAppointmentsParams = {
|
|
516
445
|
practitionerId,
|
|
@@ -540,14 +469,12 @@ export class AppointmentService extends BaseService {
|
|
|
540
469
|
status?: AppointmentStatus | AppointmentStatus[];
|
|
541
470
|
limit?: number;
|
|
542
471
|
startAfter?: DocumentSnapshot;
|
|
543
|
-
}
|
|
472
|
+
},
|
|
544
473
|
): Promise<{
|
|
545
474
|
appointments: Appointment[];
|
|
546
475
|
lastDoc: DocumentSnapshot | null;
|
|
547
476
|
}> {
|
|
548
|
-
console.log(
|
|
549
|
-
`[APPOINTMENT_SERVICE] Getting appointments for clinic: ${clinicBranchId}`
|
|
550
|
-
);
|
|
477
|
+
console.log(`[APPOINTMENT_SERVICE] Getting appointments for clinic: ${clinicBranchId}`);
|
|
551
478
|
|
|
552
479
|
const searchParams: SearchAppointmentsParams = {
|
|
553
480
|
clinicBranchId,
|
|
@@ -575,11 +502,11 @@ export class AppointmentService extends BaseService {
|
|
|
575
502
|
newStatus: AppointmentStatus,
|
|
576
503
|
details?: {
|
|
577
504
|
cancellationReason?: string;
|
|
578
|
-
canceledBy?:
|
|
579
|
-
}
|
|
505
|
+
canceledBy?: 'patient' | 'clinic' | 'practitioner' | 'system';
|
|
506
|
+
},
|
|
580
507
|
): Promise<Appointment> {
|
|
581
508
|
console.log(
|
|
582
|
-
`[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}
|
|
509
|
+
`[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`,
|
|
583
510
|
);
|
|
584
511
|
const updateData: UpdateAppointmentData = {
|
|
585
512
|
status: newStatus,
|
|
@@ -592,10 +519,10 @@ export class AppointmentService extends BaseService {
|
|
|
592
519
|
newStatus === AppointmentStatus.CANCELED_PATIENT_RESCHEDULED
|
|
593
520
|
) {
|
|
594
521
|
if (!details?.cancellationReason) {
|
|
595
|
-
throw new Error(
|
|
522
|
+
throw new Error('Cancellation reason is required when canceling.');
|
|
596
523
|
}
|
|
597
524
|
if (!details?.canceledBy) {
|
|
598
|
-
throw new Error(
|
|
525
|
+
throw new Error('Canceled by is required when canceling.');
|
|
599
526
|
}
|
|
600
527
|
updateData.cancellationReason = details.cancellationReason;
|
|
601
528
|
updateData.canceledBy = details.canceledBy;
|
|
@@ -617,61 +544,35 @@ export class AppointmentService extends BaseService {
|
|
|
617
544
|
* Confirms a PENDING appointment by an Admin/Clinic.
|
|
618
545
|
*/
|
|
619
546
|
async confirmAppointmentAdmin(appointmentId: string): Promise<Appointment> {
|
|
620
|
-
console.log(
|
|
621
|
-
`[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`
|
|
622
|
-
);
|
|
547
|
+
console.log(`[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`);
|
|
623
548
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
624
|
-
if (!appointment)
|
|
625
|
-
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
549
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
626
550
|
if (appointment.status !== AppointmentStatus.PENDING) {
|
|
627
|
-
throw new Error(
|
|
628
|
-
`Appointment ${appointmentId} is not in PENDING state to be confirmed.`
|
|
629
|
-
);
|
|
551
|
+
throw new Error(`Appointment ${appointmentId} is not in PENDING state to be confirmed.`);
|
|
630
552
|
}
|
|
631
|
-
return this.updateAppointmentStatus(
|
|
632
|
-
appointmentId,
|
|
633
|
-
AppointmentStatus.CONFIRMED
|
|
634
|
-
);
|
|
553
|
+
return this.updateAppointmentStatus(appointmentId, AppointmentStatus.CONFIRMED);
|
|
635
554
|
}
|
|
636
555
|
|
|
637
556
|
/**
|
|
638
557
|
* Cancels an appointment by the User (Patient).
|
|
639
558
|
*/
|
|
640
|
-
async cancelAppointmentUser(
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
);
|
|
647
|
-
return this.updateAppointmentStatus(
|
|
648
|
-
appointmentId,
|
|
649
|
-
AppointmentStatus.CANCELED_PATIENT,
|
|
650
|
-
{
|
|
651
|
-
cancellationReason: reason,
|
|
652
|
-
canceledBy: "patient",
|
|
653
|
-
}
|
|
654
|
-
);
|
|
559
|
+
async cancelAppointmentUser(appointmentId: string, reason: string): Promise<Appointment> {
|
|
560
|
+
console.log(`[APPOINTMENT_SERVICE] User canceling appointment: ${appointmentId}`);
|
|
561
|
+
return this.updateAppointmentStatus(appointmentId, AppointmentStatus.CANCELED_PATIENT, {
|
|
562
|
+
cancellationReason: reason,
|
|
563
|
+
canceledBy: 'patient',
|
|
564
|
+
});
|
|
655
565
|
}
|
|
656
566
|
|
|
657
567
|
/**
|
|
658
568
|
* Cancels an appointment by an Admin/Clinic.
|
|
659
569
|
*/
|
|
660
|
-
async cancelAppointmentAdmin(
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
);
|
|
667
|
-
return this.updateAppointmentStatus(
|
|
668
|
-
appointmentId,
|
|
669
|
-
AppointmentStatus.CANCELED_CLINIC,
|
|
670
|
-
{
|
|
671
|
-
cancellationReason: reason,
|
|
672
|
-
canceledBy: "clinic",
|
|
673
|
-
}
|
|
674
|
-
);
|
|
570
|
+
async cancelAppointmentAdmin(appointmentId: string, reason: string): Promise<Appointment> {
|
|
571
|
+
console.log(`[APPOINTMENT_SERVICE] Admin canceling appointment: ${appointmentId}`);
|
|
572
|
+
return this.updateAppointmentStatus(appointmentId, AppointmentStatus.CANCELED_CLINIC, {
|
|
573
|
+
cancellationReason: reason,
|
|
574
|
+
canceledBy: 'clinic',
|
|
575
|
+
});
|
|
675
576
|
}
|
|
676
577
|
|
|
677
578
|
/**
|
|
@@ -683,23 +584,17 @@ export class AppointmentService extends BaseService {
|
|
|
683
584
|
newStartTime: any; // Accept any type (number, string, Timestamp, etc.)
|
|
684
585
|
newEndTime: any; // Accept any type (number, string, Timestamp, etc.)
|
|
685
586
|
}): Promise<Appointment> {
|
|
686
|
-
console.log(
|
|
687
|
-
`[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${params.appointmentId}`
|
|
688
|
-
);
|
|
587
|
+
console.log(`[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${params.appointmentId}`);
|
|
689
588
|
|
|
690
589
|
// Validate input data
|
|
691
|
-
const validatedParams = await rescheduleAppointmentSchema.parseAsync(
|
|
692
|
-
params
|
|
693
|
-
);
|
|
590
|
+
const validatedParams = await rescheduleAppointmentSchema.parseAsync(params);
|
|
694
591
|
|
|
695
592
|
// Convert input to Timestamp objects
|
|
696
|
-
const startTimestamp = this.convertToTimestamp(
|
|
697
|
-
validatedParams.newStartTime
|
|
698
|
-
);
|
|
593
|
+
const startTimestamp = this.convertToTimestamp(validatedParams.newStartTime);
|
|
699
594
|
const endTimestamp = this.convertToTimestamp(validatedParams.newEndTime);
|
|
700
595
|
|
|
701
596
|
if (endTimestamp.toMillis() <= startTimestamp.toMillis()) {
|
|
702
|
-
throw new Error(
|
|
597
|
+
throw new Error('New end time must be after new start time.');
|
|
703
598
|
}
|
|
704
599
|
|
|
705
600
|
const updateData: UpdateAppointmentData = {
|
|
@@ -725,17 +620,17 @@ export class AppointmentService extends BaseService {
|
|
|
725
620
|
});
|
|
726
621
|
|
|
727
622
|
// If it's already a Timestamp object with methods
|
|
728
|
-
if (value && typeof value.toMillis ===
|
|
623
|
+
if (value && typeof value.toMillis === 'function') {
|
|
729
624
|
return value;
|
|
730
625
|
}
|
|
731
626
|
|
|
732
627
|
// If it's a number (milliseconds since epoch)
|
|
733
|
-
if (typeof value ===
|
|
628
|
+
if (typeof value === 'number') {
|
|
734
629
|
return Timestamp.fromMillis(value);
|
|
735
630
|
}
|
|
736
631
|
|
|
737
632
|
// If it's a string (ISO date string)
|
|
738
|
-
if (typeof value ===
|
|
633
|
+
if (typeof value === 'string') {
|
|
739
634
|
return Timestamp.fromDate(new Date(value));
|
|
740
635
|
}
|
|
741
636
|
|
|
@@ -745,44 +640,30 @@ export class AppointmentService extends BaseService {
|
|
|
745
640
|
}
|
|
746
641
|
|
|
747
642
|
// If it has _seconds property (serialized Timestamp) - THIS IS WHAT FRONTEND SENDS
|
|
748
|
-
if (value && typeof value._seconds ===
|
|
643
|
+
if (value && typeof value._seconds === 'number') {
|
|
749
644
|
return new Timestamp(value._seconds, value._nanoseconds || 0);
|
|
750
645
|
}
|
|
751
646
|
|
|
752
647
|
// If it has seconds property (serialized Timestamp)
|
|
753
|
-
if (value && typeof value.seconds ===
|
|
648
|
+
if (value && typeof value.seconds === 'number') {
|
|
754
649
|
return new Timestamp(value.seconds, value.nanoseconds || 0);
|
|
755
650
|
}
|
|
756
651
|
|
|
757
|
-
throw new Error(
|
|
758
|
-
`Invalid timestamp format: ${typeof value}, value: ${JSON.stringify(
|
|
759
|
-
value
|
|
760
|
-
)}`
|
|
761
|
-
);
|
|
652
|
+
throw new Error(`Invalid timestamp format: ${typeof value}, value: ${JSON.stringify(value)}`);
|
|
762
653
|
}
|
|
763
654
|
|
|
764
655
|
/**
|
|
765
656
|
* User confirms a reschedule proposed by the clinic.
|
|
766
657
|
* Status changes from RESCHEDULED_BY_CLINIC to CONFIRMED.
|
|
767
658
|
*/
|
|
768
|
-
async rescheduleAppointmentConfirmUser(
|
|
769
|
-
|
|
770
|
-
): Promise<Appointment> {
|
|
771
|
-
console.log(
|
|
772
|
-
`[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`
|
|
773
|
-
);
|
|
659
|
+
async rescheduleAppointmentConfirmUser(appointmentId: string): Promise<Appointment> {
|
|
660
|
+
console.log(`[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`);
|
|
774
661
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
775
|
-
if (!appointment)
|
|
776
|
-
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
662
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
777
663
|
if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
|
|
778
|
-
throw new Error(
|
|
779
|
-
`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
|
|
780
|
-
);
|
|
664
|
+
throw new Error(`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`);
|
|
781
665
|
}
|
|
782
|
-
return this.updateAppointmentStatus(
|
|
783
|
-
appointmentId,
|
|
784
|
-
AppointmentStatus.CONFIRMED
|
|
785
|
-
);
|
|
666
|
+
return this.updateAppointmentStatus(appointmentId, AppointmentStatus.CONFIRMED);
|
|
786
667
|
}
|
|
787
668
|
|
|
788
669
|
/**
|
|
@@ -791,26 +672,21 @@ export class AppointmentService extends BaseService {
|
|
|
791
672
|
*/
|
|
792
673
|
async rescheduleAppointmentRejectUser(
|
|
793
674
|
appointmentId: string,
|
|
794
|
-
reason: string
|
|
675
|
+
reason: string,
|
|
795
676
|
): Promise<Appointment> {
|
|
796
|
-
console.log(
|
|
797
|
-
`[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`
|
|
798
|
-
);
|
|
677
|
+
console.log(`[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`);
|
|
799
678
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
800
|
-
if (!appointment)
|
|
801
|
-
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
679
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
802
680
|
if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
|
|
803
|
-
throw new Error(
|
|
804
|
-
`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
|
|
805
|
-
);
|
|
681
|
+
throw new Error(`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`);
|
|
806
682
|
}
|
|
807
683
|
return this.updateAppointmentStatus(
|
|
808
684
|
appointmentId,
|
|
809
685
|
AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
|
|
810
686
|
{
|
|
811
687
|
cancellationReason: reason,
|
|
812
|
-
canceledBy:
|
|
813
|
-
}
|
|
688
|
+
canceledBy: 'patient',
|
|
689
|
+
},
|
|
814
690
|
);
|
|
815
691
|
}
|
|
816
692
|
|
|
@@ -819,23 +695,15 @@ export class AppointmentService extends BaseService {
|
|
|
819
695
|
* Requires all pending user forms to be completed.
|
|
820
696
|
*/
|
|
821
697
|
async checkInPatientAdmin(appointmentId: string): Promise<Appointment> {
|
|
822
|
-
console.log(
|
|
823
|
-
`[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`
|
|
824
|
-
);
|
|
698
|
+
console.log(`[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`);
|
|
825
699
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
826
|
-
if (!appointment)
|
|
827
|
-
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
700
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
828
701
|
|
|
829
|
-
if (
|
|
830
|
-
appointment.pendingUserFormsIds &&
|
|
831
|
-
appointment.pendingUserFormsIds.length > 0
|
|
832
|
-
) {
|
|
702
|
+
if (appointment.pendingUserFormsIds && appointment.pendingUserFormsIds.length > 0) {
|
|
833
703
|
throw new Error(
|
|
834
704
|
`Cannot check in: Patient has ${
|
|
835
705
|
appointment.pendingUserFormsIds.length
|
|
836
|
-
} pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(
|
|
837
|
-
", "
|
|
838
|
-
)}`
|
|
706
|
+
} pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(', ')}`,
|
|
839
707
|
);
|
|
840
708
|
}
|
|
841
709
|
if (
|
|
@@ -843,30 +711,22 @@ export class AppointmentService extends BaseService {
|
|
|
843
711
|
appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC
|
|
844
712
|
) {
|
|
845
713
|
console.warn(
|
|
846
|
-
`Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended
|
|
714
|
+
`Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended.`,
|
|
847
715
|
);
|
|
848
716
|
}
|
|
849
717
|
|
|
850
|
-
return this.updateAppointmentStatus(
|
|
851
|
-
appointmentId,
|
|
852
|
-
AppointmentStatus.CHECKED_IN
|
|
853
|
-
);
|
|
718
|
+
return this.updateAppointmentStatus(appointmentId, AppointmentStatus.CHECKED_IN);
|
|
854
719
|
}
|
|
855
720
|
|
|
856
721
|
/**
|
|
857
722
|
* Doctor starts the appointment procedure.
|
|
858
723
|
*/
|
|
859
724
|
async startAppointmentDoctor(appointmentId: string): Promise<Appointment> {
|
|
860
|
-
console.log(
|
|
861
|
-
`[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`
|
|
862
|
-
);
|
|
725
|
+
console.log(`[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`);
|
|
863
726
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
864
|
-
if (!appointment)
|
|
865
|
-
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
727
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
866
728
|
if (appointment.status !== AppointmentStatus.CHECKED_IN) {
|
|
867
|
-
throw new Error(
|
|
868
|
-
`Appointment ${appointmentId} must be CHECKED_IN to start.`
|
|
869
|
-
);
|
|
729
|
+
throw new Error(`Appointment ${appointmentId} must be CHECKED_IN to start.`);
|
|
870
730
|
}
|
|
871
731
|
// Update status and set procedureActualStartTime
|
|
872
732
|
const updateData: UpdateAppointmentData = {
|
|
@@ -883,33 +743,24 @@ export class AppointmentService extends BaseService {
|
|
|
883
743
|
async completeAppointmentDoctor(
|
|
884
744
|
appointmentId: string,
|
|
885
745
|
finalizationNotes: string,
|
|
886
|
-
actualDurationMinutesInput?: number // Renamed to avoid conflict if we calculate
|
|
746
|
+
actualDurationMinutesInput?: number, // Renamed to avoid conflict if we calculate
|
|
887
747
|
): Promise<Appointment> {
|
|
888
|
-
console.log(
|
|
889
|
-
`[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`
|
|
890
|
-
);
|
|
748
|
+
console.log(`[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`);
|
|
891
749
|
const currentUser = this.auth.currentUser;
|
|
892
|
-
if (!currentUser)
|
|
893
|
-
throw new Error("Authentication required to complete appointment.");
|
|
750
|
+
if (!currentUser) throw new Error('Authentication required to complete appointment.');
|
|
894
751
|
|
|
895
752
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
896
|
-
if (!appointment)
|
|
897
|
-
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
753
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
898
754
|
|
|
899
755
|
let calculatedDurationMinutes = actualDurationMinutesInput;
|
|
900
756
|
const procedureCompletionTime = Timestamp.now();
|
|
901
757
|
|
|
902
758
|
// Calculate duration if not provided and actual start time is available
|
|
903
|
-
if (
|
|
904
|
-
calculatedDurationMinutes === undefined &&
|
|
905
|
-
appointment.procedureActualStartTime
|
|
906
|
-
) {
|
|
759
|
+
if (calculatedDurationMinutes === undefined && appointment.procedureActualStartTime) {
|
|
907
760
|
const startTimeMillis = appointment.procedureActualStartTime.toMillis();
|
|
908
761
|
const endTimeMillis = procedureCompletionTime.toMillis();
|
|
909
762
|
if (endTimeMillis > startTimeMillis) {
|
|
910
|
-
calculatedDurationMinutes = Math.round(
|
|
911
|
-
(endTimeMillis - startTimeMillis) / 60000
|
|
912
|
-
);
|
|
763
|
+
calculatedDurationMinutes = Math.round((endTimeMillis - startTimeMillis) / 60000);
|
|
913
764
|
}
|
|
914
765
|
}
|
|
915
766
|
|
|
@@ -932,25 +783,16 @@ export class AppointmentService extends BaseService {
|
|
|
932
783
|
* Admin marks an appointment as No-Show.
|
|
933
784
|
*/
|
|
934
785
|
async markNoShowAdmin(appointmentId: string): Promise<Appointment> {
|
|
935
|
-
console.log(
|
|
936
|
-
`[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`
|
|
937
|
-
);
|
|
786
|
+
console.log(`[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`);
|
|
938
787
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
939
|
-
if (!appointment)
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()
|
|
943
|
-
) {
|
|
944
|
-
throw new Error("Cannot mark no-show before appointment start time.");
|
|
788
|
+
if (!appointment) throw new Error(`Appointment ${appointmentId} not found.`);
|
|
789
|
+
if (Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()) {
|
|
790
|
+
throw new Error('Cannot mark no-show before appointment start time.');
|
|
945
791
|
}
|
|
946
|
-
return this.updateAppointmentStatus(
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
cancellationReason: "Patient did not show up for the appointment.",
|
|
951
|
-
canceledBy: "clinic",
|
|
952
|
-
}
|
|
953
|
-
);
|
|
792
|
+
return this.updateAppointmentStatus(appointmentId, AppointmentStatus.NO_SHOW, {
|
|
793
|
+
cancellationReason: 'Patient did not show up for the appointment.',
|
|
794
|
+
canceledBy: 'clinic',
|
|
795
|
+
});
|
|
954
796
|
}
|
|
955
797
|
|
|
956
798
|
/**
|
|
@@ -958,13 +800,11 @@ export class AppointmentService extends BaseService {
|
|
|
958
800
|
*/
|
|
959
801
|
async addMediaToAppointment(
|
|
960
802
|
appointmentId: string,
|
|
961
|
-
mediaItemData: Omit<AppointmentMediaItem,
|
|
803
|
+
mediaItemData: Omit<AppointmentMediaItem, 'id' | 'uploadedAt'>,
|
|
962
804
|
): Promise<Appointment> {
|
|
963
|
-
console.log(
|
|
964
|
-
`[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`
|
|
965
|
-
);
|
|
805
|
+
console.log(`[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`);
|
|
966
806
|
const currentUser = this.auth.currentUser;
|
|
967
|
-
if (!currentUser) throw new Error(
|
|
807
|
+
if (!currentUser) throw new Error('Authentication required.');
|
|
968
808
|
|
|
969
809
|
const newMediaItem: AppointmentMediaItem = {
|
|
970
810
|
...mediaItemData,
|
|
@@ -985,16 +825,16 @@ export class AppointmentService extends BaseService {
|
|
|
985
825
|
*/
|
|
986
826
|
async removeMediaFromAppointment(
|
|
987
827
|
appointmentId: string,
|
|
988
|
-
mediaItemId: string
|
|
828
|
+
mediaItemId: string,
|
|
989
829
|
): Promise<Appointment> {
|
|
990
830
|
console.log(
|
|
991
|
-
`[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}
|
|
831
|
+
`[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}`,
|
|
992
832
|
);
|
|
993
833
|
const appointment = await this.getAppointmentById(appointmentId);
|
|
994
834
|
if (!appointment || !appointment.media) {
|
|
995
|
-
throw new Error(
|
|
835
|
+
throw new Error('Appointment or media list not found.');
|
|
996
836
|
}
|
|
997
|
-
const mediaToRemove = appointment.media.find(
|
|
837
|
+
const mediaToRemove = appointment.media.find(m => m.id === mediaItemId);
|
|
998
838
|
if (!mediaToRemove) {
|
|
999
839
|
throw new Error(`Media item ${mediaItemId} not found in appointment.`);
|
|
1000
840
|
}
|
|
@@ -1011,11 +851,9 @@ export class AppointmentService extends BaseService {
|
|
|
1011
851
|
*/
|
|
1012
852
|
async addReviewToAppointment(
|
|
1013
853
|
appointmentId: string,
|
|
1014
|
-
reviewData: Omit<PatientReviewInfo,
|
|
854
|
+
reviewData: Omit<PatientReviewInfo, 'reviewedAt' | 'reviewId'>,
|
|
1015
855
|
): Promise<Appointment> {
|
|
1016
|
-
console.log(
|
|
1017
|
-
`[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`
|
|
1018
|
-
);
|
|
856
|
+
console.log(`[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`);
|
|
1019
857
|
const newReviewInfo: PatientReviewInfo = {
|
|
1020
858
|
...reviewData,
|
|
1021
859
|
reviewId: this.generateId(),
|
|
@@ -1034,10 +872,10 @@ export class AppointmentService extends BaseService {
|
|
|
1034
872
|
async updatePaymentStatus(
|
|
1035
873
|
appointmentId: string,
|
|
1036
874
|
paymentStatus: PaymentStatus,
|
|
1037
|
-
paymentTransactionId?: string
|
|
875
|
+
paymentTransactionId?: string,
|
|
1038
876
|
): Promise<Appointment> {
|
|
1039
877
|
console.log(
|
|
1040
|
-
`[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}
|
|
878
|
+
`[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`,
|
|
1041
879
|
);
|
|
1042
880
|
const updateData: UpdateAppointmentData = {
|
|
1043
881
|
paymentStatus,
|
|
@@ -1054,13 +892,8 @@ export class AppointmentService extends BaseService {
|
|
|
1054
892
|
* @param notes Updated internal notes
|
|
1055
893
|
* @returns The updated appointment
|
|
1056
894
|
*/
|
|
1057
|
-
async updateInternalNotes(
|
|
1058
|
-
|
|
1059
|
-
notes: string | null
|
|
1060
|
-
): Promise<Appointment> {
|
|
1061
|
-
console.log(
|
|
1062
|
-
`[APPOINTMENT_SERVICE] Updating internal notes for appointment: ${appointmentId}`
|
|
1063
|
-
);
|
|
895
|
+
async updateInternalNotes(appointmentId: string, notes: string | null): Promise<Appointment> {
|
|
896
|
+
console.log(`[APPOINTMENT_SERVICE] Updating internal notes for appointment: ${appointmentId}`);
|
|
1064
897
|
|
|
1065
898
|
const updateData: UpdateAppointmentData = {
|
|
1066
899
|
internalNotes: notes,
|
|
@@ -1084,15 +917,13 @@ export class AppointmentService extends BaseService {
|
|
|
1084
917
|
endDate?: Date;
|
|
1085
918
|
limit?: number;
|
|
1086
919
|
startAfter?: DocumentSnapshot;
|
|
1087
|
-
}
|
|
920
|
+
},
|
|
1088
921
|
): Promise<{
|
|
1089
922
|
appointments: Appointment[];
|
|
1090
923
|
lastDoc: DocumentSnapshot | null;
|
|
1091
924
|
}> {
|
|
1092
925
|
try {
|
|
1093
|
-
console.log(
|
|
1094
|
-
`[APPOINTMENT_SERVICE] Getting upcoming appointments for patient: ${patientId}`
|
|
1095
|
-
);
|
|
926
|
+
console.log(`[APPOINTMENT_SERVICE] Getting upcoming appointments for patient: ${patientId}`);
|
|
1096
927
|
|
|
1097
928
|
// Default to current date/time if no startDate provided
|
|
1098
929
|
const effectiveStartDate = options?.startDate || new Date();
|
|
@@ -1110,32 +941,20 @@ export class AppointmentService extends BaseService {
|
|
|
1110
941
|
const constraints: QueryConstraint[] = [];
|
|
1111
942
|
|
|
1112
943
|
// Patient ID filter
|
|
1113
|
-
constraints.push(where(
|
|
944
|
+
constraints.push(where('patientId', '==', patientId));
|
|
1114
945
|
|
|
1115
946
|
// Status filter - multiple statuses
|
|
1116
|
-
constraints.push(where(
|
|
947
|
+
constraints.push(where('status', 'in', upcomingStatuses));
|
|
1117
948
|
|
|
1118
949
|
// Date range filters
|
|
1119
|
-
constraints.push(
|
|
1120
|
-
where(
|
|
1121
|
-
"appointmentStartTime",
|
|
1122
|
-
">=",
|
|
1123
|
-
Timestamp.fromDate(effectiveStartDate)
|
|
1124
|
-
)
|
|
1125
|
-
);
|
|
950
|
+
constraints.push(where('appointmentStartTime', '>=', Timestamp.fromDate(effectiveStartDate)));
|
|
1126
951
|
|
|
1127
952
|
if (options?.endDate) {
|
|
1128
|
-
constraints.push(
|
|
1129
|
-
where(
|
|
1130
|
-
"appointmentStartTime",
|
|
1131
|
-
"<=",
|
|
1132
|
-
Timestamp.fromDate(options.endDate)
|
|
1133
|
-
)
|
|
1134
|
-
);
|
|
953
|
+
constraints.push(where('appointmentStartTime', '<=', Timestamp.fromDate(options.endDate)));
|
|
1135
954
|
}
|
|
1136
955
|
|
|
1137
956
|
// Order by appointment start time (ascending for upcoming - closest first)
|
|
1138
|
-
constraints.push(orderBy(
|
|
957
|
+
constraints.push(orderBy('appointmentStartTime', 'asc'));
|
|
1139
958
|
|
|
1140
959
|
// Add pagination if specified
|
|
1141
960
|
if (options?.limit) {
|
|
@@ -1147,32 +966,25 @@ export class AppointmentService extends BaseService {
|
|
|
1147
966
|
}
|
|
1148
967
|
|
|
1149
968
|
// Execute query
|
|
1150
|
-
const q = query(
|
|
1151
|
-
collection(this.db, APPOINTMENTS_COLLECTION),
|
|
1152
|
-
...constraints
|
|
1153
|
-
);
|
|
969
|
+
const q = query(collection(this.db, APPOINTMENTS_COLLECTION), ...constraints);
|
|
1154
970
|
const querySnapshot = await getDocs(q);
|
|
1155
971
|
|
|
1156
972
|
// Extract results
|
|
1157
|
-
const appointments = querySnapshot.docs.map(
|
|
1158
|
-
(doc) => doc.data() as Appointment
|
|
1159
|
-
);
|
|
973
|
+
const appointments = querySnapshot.docs.map(doc => doc.data() as Appointment);
|
|
1160
974
|
|
|
1161
975
|
// Get last document for pagination
|
|
1162
976
|
const lastDoc =
|
|
1163
|
-
querySnapshot.docs.length > 0
|
|
1164
|
-
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1165
|
-
: null;
|
|
977
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1166
978
|
|
|
1167
979
|
console.log(
|
|
1168
|
-
`[APPOINTMENT_SERVICE] Found ${appointments.length} upcoming appointments for patient ${patientId}
|
|
980
|
+
`[APPOINTMENT_SERVICE] Found ${appointments.length} upcoming appointments for patient ${patientId}`,
|
|
1169
981
|
);
|
|
1170
982
|
|
|
1171
983
|
return { appointments, lastDoc };
|
|
1172
984
|
} catch (error) {
|
|
1173
985
|
console.error(
|
|
1174
986
|
`[APPOINTMENT_SERVICE] Error getting upcoming appointments for patient ${patientId}:`,
|
|
1175
|
-
error
|
|
987
|
+
error,
|
|
1176
988
|
);
|
|
1177
989
|
throw error;
|
|
1178
990
|
}
|
|
@@ -1196,15 +1008,13 @@ export class AppointmentService extends BaseService {
|
|
|
1196
1008
|
showNoShow?: boolean; // Whether to include no-show appointments
|
|
1197
1009
|
limit?: number;
|
|
1198
1010
|
startAfter?: DocumentSnapshot;
|
|
1199
|
-
}
|
|
1011
|
+
},
|
|
1200
1012
|
): Promise<{
|
|
1201
1013
|
appointments: Appointment[];
|
|
1202
1014
|
lastDoc: DocumentSnapshot | null;
|
|
1203
1015
|
}> {
|
|
1204
1016
|
try {
|
|
1205
|
-
console.log(
|
|
1206
|
-
`[APPOINTMENT_SERVICE] Getting past appointments for patient: ${patientId}`
|
|
1207
|
-
);
|
|
1017
|
+
console.log(`[APPOINTMENT_SERVICE] Getting past appointments for patient: ${patientId}`);
|
|
1208
1018
|
|
|
1209
1019
|
// Default to current date/time if no endDate provided
|
|
1210
1020
|
const effectiveEndDate = options?.endDate || new Date();
|
|
@@ -1217,7 +1027,7 @@ export class AppointmentService extends BaseService {
|
|
|
1217
1027
|
pastStatuses.push(
|
|
1218
1028
|
AppointmentStatus.CANCELED_PATIENT,
|
|
1219
1029
|
AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
|
|
1220
|
-
AppointmentStatus.CANCELED_CLINIC
|
|
1030
|
+
AppointmentStatus.CANCELED_CLINIC,
|
|
1221
1031
|
);
|
|
1222
1032
|
}
|
|
1223
1033
|
|
|
@@ -1230,32 +1040,22 @@ export class AppointmentService extends BaseService {
|
|
|
1230
1040
|
const constraints: QueryConstraint[] = [];
|
|
1231
1041
|
|
|
1232
1042
|
// Patient ID filter
|
|
1233
|
-
constraints.push(where(
|
|
1043
|
+
constraints.push(where('patientId', '==', patientId));
|
|
1234
1044
|
|
|
1235
1045
|
// Status filter - multiple statuses
|
|
1236
|
-
constraints.push(where(
|
|
1046
|
+
constraints.push(where('status', 'in', pastStatuses));
|
|
1237
1047
|
|
|
1238
1048
|
// Date range filters
|
|
1239
1049
|
if (options?.startDate) {
|
|
1240
1050
|
constraints.push(
|
|
1241
|
-
where(
|
|
1242
|
-
"appointmentStartTime",
|
|
1243
|
-
">=",
|
|
1244
|
-
Timestamp.fromDate(options.startDate)
|
|
1245
|
-
)
|
|
1051
|
+
where('appointmentStartTime', '>=', Timestamp.fromDate(options.startDate)),
|
|
1246
1052
|
);
|
|
1247
1053
|
}
|
|
1248
1054
|
|
|
1249
|
-
constraints.push(
|
|
1250
|
-
where(
|
|
1251
|
-
"appointmentStartTime",
|
|
1252
|
-
"<=",
|
|
1253
|
-
Timestamp.fromDate(effectiveEndDate)
|
|
1254
|
-
)
|
|
1255
|
-
);
|
|
1055
|
+
constraints.push(where('appointmentStartTime', '<=', Timestamp.fromDate(effectiveEndDate)));
|
|
1256
1056
|
|
|
1257
1057
|
// Order by appointment start time (descending for past - most recent first)
|
|
1258
|
-
constraints.push(orderBy(
|
|
1058
|
+
constraints.push(orderBy('appointmentStartTime', 'desc'));
|
|
1259
1059
|
|
|
1260
1060
|
// Add pagination if specified
|
|
1261
1061
|
if (options?.limit) {
|
|
@@ -1267,32 +1067,79 @@ export class AppointmentService extends BaseService {
|
|
|
1267
1067
|
}
|
|
1268
1068
|
|
|
1269
1069
|
// Execute query
|
|
1270
|
-
const q = query(
|
|
1271
|
-
collection(this.db, APPOINTMENTS_COLLECTION),
|
|
1272
|
-
...constraints
|
|
1273
|
-
);
|
|
1070
|
+
const q = query(collection(this.db, APPOINTMENTS_COLLECTION), ...constraints);
|
|
1274
1071
|
const querySnapshot = await getDocs(q);
|
|
1275
1072
|
|
|
1276
1073
|
// Extract results
|
|
1277
|
-
const appointments = querySnapshot.docs.map(
|
|
1278
|
-
(doc) => doc.data() as Appointment
|
|
1279
|
-
);
|
|
1074
|
+
const appointments = querySnapshot.docs.map(doc => doc.data() as Appointment);
|
|
1280
1075
|
|
|
1281
1076
|
// Get last document for pagination
|
|
1282
1077
|
const lastDoc =
|
|
1283
|
-
querySnapshot.docs.length > 0
|
|
1284
|
-
? querySnapshot.docs[querySnapshot.docs.length - 1]
|
|
1285
|
-
: null;
|
|
1078
|
+
querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
1286
1079
|
|
|
1287
1080
|
console.log(
|
|
1288
|
-
`[APPOINTMENT_SERVICE] Found ${appointments.length} past appointments for patient ${patientId}
|
|
1081
|
+
`[APPOINTMENT_SERVICE] Found ${appointments.length} past appointments for patient ${patientId}`,
|
|
1289
1082
|
);
|
|
1290
1083
|
|
|
1291
1084
|
return { appointments, lastDoc };
|
|
1292
1085
|
} catch (error) {
|
|
1293
1086
|
console.error(
|
|
1294
1087
|
`[APPOINTMENT_SERVICE] Error getting past appointments for patient ${patientId}:`,
|
|
1295
|
-
error
|
|
1088
|
+
error,
|
|
1089
|
+
);
|
|
1090
|
+
throw error;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Counts completed appointments for a patient with optional clinic filtering.
|
|
1096
|
+
*
|
|
1097
|
+
* @param patientId ID of the patient.
|
|
1098
|
+
* @param clinicBranchId Optional ID of the clinic branch to either include or exclude.
|
|
1099
|
+
* @param excludeClinic Optional boolean. If true (default), excludes the specified clinic. If false, includes only that clinic.
|
|
1100
|
+
* @returns The count of completed appointments.
|
|
1101
|
+
*/
|
|
1102
|
+
async countCompletedAppointments(
|
|
1103
|
+
patientId: string,
|
|
1104
|
+
clinicBranchId?: string,
|
|
1105
|
+
excludeClinic = true,
|
|
1106
|
+
): Promise<number> {
|
|
1107
|
+
try {
|
|
1108
|
+
console.log(
|
|
1109
|
+
`[APPOINTMENT_SERVICE] Counting completed appointments for patient: ${patientId}`,
|
|
1110
|
+
{ clinicBranchId, excludeClinic },
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
// Build query constraints
|
|
1114
|
+
const constraints: QueryConstraint[] = [
|
|
1115
|
+
where('patientId', '==', patientId),
|
|
1116
|
+
where('status', '==', AppointmentStatus.COMPLETED),
|
|
1117
|
+
];
|
|
1118
|
+
|
|
1119
|
+
if (clinicBranchId) {
|
|
1120
|
+
if (excludeClinic) {
|
|
1121
|
+
// Exclude appointments from the specified clinic
|
|
1122
|
+
constraints.push(where('clinicBranchId', '!=', clinicBranchId));
|
|
1123
|
+
} else {
|
|
1124
|
+
// Include only appointments from the specified clinic
|
|
1125
|
+
constraints.push(where('clinicBranchId', '==', clinicBranchId));
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Execute query to get only the count
|
|
1130
|
+
const q = query(collection(this.db, APPOINTMENTS_COLLECTION), ...constraints);
|
|
1131
|
+
const snapshot = await getCountFromServer(q);
|
|
1132
|
+
const count = snapshot.data().count;
|
|
1133
|
+
|
|
1134
|
+
console.log(
|
|
1135
|
+
`[APPOINTMENT_SERVICE] Found ${count} completed appointments for patient ${patientId}`,
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
return count;
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
console.error(
|
|
1141
|
+
`[APPOINTMENT_SERVICE] Error counting completed appointments for patient ${patientId}:`,
|
|
1142
|
+
error,
|
|
1296
1143
|
);
|
|
1297
1144
|
throw error;
|
|
1298
1145
|
}
|