@blackcode_sa/metaestetics-api 1.6.3 → 1.6.5
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 +439 -25
- package/dist/admin/index.d.ts +439 -25
- package/dist/admin/index.js +36107 -2493
- package/dist/admin/index.mjs +36093 -2461
- package/dist/backoffice/index.d.mts +254 -1
- package/dist/backoffice/index.d.ts +254 -1
- package/dist/backoffice/index.js +86 -12
- package/dist/backoffice/index.mjs +86 -13
- package/dist/index.d.mts +1434 -621
- package/dist/index.d.ts +1434 -621
- package/dist/index.js +1381 -970
- package/dist/index.mjs +1433 -1016
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +321 -0
- package/src/admin/booking/booking.admin.ts +376 -3
- package/src/admin/index.ts +15 -1
- package/src/admin/notifications/notifications.admin.ts +1 -1
- package/src/admin/requirements/README.md +128 -0
- package/src/admin/requirements/patient-requirements.admin.service.ts +482 -0
- package/src/backoffice/types/product.types.ts +2 -0
- package/src/index.ts +16 -1
- package/src/services/appointment/appointment.service.ts +386 -250
- package/src/services/clinic/clinic-admin.service.ts +3 -0
- package/src/services/clinic/clinic-group.service.ts +8 -0
- package/src/services/documentation-templates/documentation-template.service.ts +24 -16
- package/src/services/documentation-templates/filled-document.service.ts +253 -136
- package/src/services/patient/patientRequirements.service.ts +285 -0
- package/src/services/procedure/procedure.service.ts +1 -0
- package/src/types/appointment/index.ts +136 -11
- package/src/types/documentation-templates/index.ts +34 -2
- package/src/types/notifications/README.md +77 -0
- package/src/types/notifications/index.ts +154 -27
- package/src/types/patient/patient-requirements.ts +81 -0
- package/src/types/procedure/index.ts +7 -0
- package/src/validations/appointment.schema.ts +298 -62
- package/src/validations/documentation-templates/template.schema.ts +55 -0
- package/src/validations/documentation-templates.schema.ts +9 -14
- package/src/validations/notification.schema.ts +3 -3
- package/src/validations/patient/patient-requirements.schema.ts +75 -0
- package/src/validations/procedure.schema.ts +3 -0
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
Timestamp,
|
|
4
4
|
DocumentSnapshot,
|
|
5
5
|
serverTimestamp,
|
|
6
|
+
arrayUnion,
|
|
7
|
+
arrayRemove,
|
|
6
8
|
} from "firebase/firestore";
|
|
7
9
|
import { Auth } from "firebase/auth";
|
|
8
10
|
import { FirebaseApp } from "firebase/app";
|
|
@@ -15,6 +17,9 @@ import {
|
|
|
15
17
|
UpdateAppointmentData,
|
|
16
18
|
SearchAppointmentsParams,
|
|
17
19
|
PaymentStatus,
|
|
20
|
+
AppointmentMediaItem,
|
|
21
|
+
PatientReviewInfo,
|
|
22
|
+
LinkedFormInfo,
|
|
18
23
|
} from "../../types/appointment";
|
|
19
24
|
import {
|
|
20
25
|
createAppointmentSchema,
|
|
@@ -27,6 +32,7 @@ import { CalendarServiceV2 } from "../calendar/calendar-refactored.service";
|
|
|
27
32
|
import { PatientService } from "../patient/patient.service";
|
|
28
33
|
import { PractitionerService } from "../practitioner/practitioner.service";
|
|
29
34
|
import { ClinicService } from "../clinic/clinic.service";
|
|
35
|
+
import { FilledDocumentService } from "../documentation-templates/filled-document.service";
|
|
30
36
|
|
|
31
37
|
// Import utility functions
|
|
32
38
|
import {
|
|
@@ -54,6 +60,7 @@ export class AppointmentService extends BaseService {
|
|
|
54
60
|
private patientService: PatientService;
|
|
55
61
|
private practitionerService: PractitionerService;
|
|
56
62
|
private clinicService: ClinicService;
|
|
63
|
+
private filledDocumentService: FilledDocumentService;
|
|
57
64
|
private functions: Functions;
|
|
58
65
|
|
|
59
66
|
/**
|
|
@@ -74,110 +81,18 @@ export class AppointmentService extends BaseService {
|
|
|
74
81
|
calendarService: CalendarServiceV2,
|
|
75
82
|
patientService: PatientService,
|
|
76
83
|
practitionerService: PractitionerService,
|
|
77
|
-
clinicService: ClinicService
|
|
84
|
+
clinicService: ClinicService,
|
|
85
|
+
filledDocumentService: FilledDocumentService
|
|
78
86
|
) {
|
|
79
87
|
super(db, auth, app);
|
|
80
88
|
this.calendarService = calendarService;
|
|
81
89
|
this.patientService = patientService;
|
|
82
90
|
this.practitionerService = practitionerService;
|
|
83
91
|
this.clinicService = clinicService;
|
|
92
|
+
this.filledDocumentService = filledDocumentService;
|
|
84
93
|
this.functions = getFunctions(app, "europe-west6"); // Initialize Firebase Functions with the correct region
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
/**
|
|
88
|
-
* Test method using the callable function version of getAvailableBookingSlots
|
|
89
|
-
* For development and testing purposes only - not for production use
|
|
90
|
-
*
|
|
91
|
-
* @param clinicId ID of the clinic
|
|
92
|
-
* @param practitionerId ID of the practitioner
|
|
93
|
-
* @param procedureId ID of the procedure
|
|
94
|
-
* @param startDate Start date of the time range to check
|
|
95
|
-
* @param endDate End date of the time range to check
|
|
96
|
-
* @returns Test result from the callable function
|
|
97
|
-
*/
|
|
98
|
-
async testGetAvailableBookingSlots(
|
|
99
|
-
clinicId: string,
|
|
100
|
-
practitionerId: string,
|
|
101
|
-
procedureId: string,
|
|
102
|
-
startDate: Date,
|
|
103
|
-
endDate: Date
|
|
104
|
-
): Promise<any> {
|
|
105
|
-
try {
|
|
106
|
-
console.log(
|
|
107
|
-
`[APPOINTMENT_SERVICE] Testing callable function for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// Get the callable function
|
|
111
|
-
const getAvailableBookingSlotsCallable = httpsCallable(
|
|
112
|
-
this.functions,
|
|
113
|
-
"getAvailableBookingSlots"
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
// Call the function with the required parameters
|
|
117
|
-
const result = await getAvailableBookingSlotsCallable({
|
|
118
|
-
clinicId,
|
|
119
|
-
practitionerId,
|
|
120
|
-
procedureId,
|
|
121
|
-
timeframe: {
|
|
122
|
-
start: startDate.getTime(),
|
|
123
|
-
end: endDate.getTime(),
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
console.log(
|
|
128
|
-
"[APPOINTMENT_SERVICE] Callable function test result:",
|
|
129
|
-
result.data
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
return result.data;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
console.error(
|
|
135
|
-
"[APPOINTMENT_SERVICE] Error testing callable function:",
|
|
136
|
-
error
|
|
137
|
-
);
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Gets available booking slots for a specific clinic, practitioner, and procedure.
|
|
144
|
-
*
|
|
145
|
-
* @param clinicId ID of the clinic
|
|
146
|
-
* @param practitionerId ID of the practitioner
|
|
147
|
-
* @param procedureId ID of the procedure
|
|
148
|
-
* @param startDate Start date of the time range to check
|
|
149
|
-
* @param endDate End date of the time range to check
|
|
150
|
-
* @returns Array of available booking slots
|
|
151
|
-
*/
|
|
152
|
-
async getAvailableBookingSlots(
|
|
153
|
-
clinicId: string,
|
|
154
|
-
practitionerId: string,
|
|
155
|
-
procedureId: string,
|
|
156
|
-
startDate: Date,
|
|
157
|
-
endDate: Date
|
|
158
|
-
): Promise<AvailableSlot[]> {
|
|
159
|
-
try {
|
|
160
|
-
console.log(
|
|
161
|
-
`[APPOINTMENT_SERVICE] Getting available booking slots for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
// Just call our HTTP implementation since the callable function isn't working in the browser
|
|
165
|
-
return this.getAvailableBookingSlotsHttp(
|
|
166
|
-
clinicId,
|
|
167
|
-
practitionerId,
|
|
168
|
-
procedureId,
|
|
169
|
-
startDate,
|
|
170
|
-
endDate
|
|
171
|
-
);
|
|
172
|
-
} catch (error) {
|
|
173
|
-
console.error(
|
|
174
|
-
"[APPOINTMENT_SERVICE] Error getting available booking slots:",
|
|
175
|
-
error
|
|
176
|
-
);
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
96
|
/**
|
|
182
97
|
* Gets available booking slots for a specific clinic, practitioner, and procedure using HTTP request.
|
|
183
98
|
* This is an alternative implementation using direct HTTP request instead of callable function.
|
|
@@ -319,42 +234,108 @@ export class AppointmentService extends BaseService {
|
|
|
319
234
|
}
|
|
320
235
|
|
|
321
236
|
/**
|
|
322
|
-
* Creates
|
|
237
|
+
* Creates an appointment via the Cloud Function orchestrateAppointmentCreation
|
|
323
238
|
*
|
|
324
|
-
* @param data
|
|
239
|
+
* @param data - CreateAppointmentData object
|
|
325
240
|
* @returns The created appointment
|
|
326
241
|
*/
|
|
327
|
-
async
|
|
242
|
+
async createAppointmentHttp(
|
|
243
|
+
data: CreateAppointmentData
|
|
244
|
+
): Promise<Appointment> {
|
|
328
245
|
try {
|
|
329
|
-
console.log(
|
|
246
|
+
console.log(
|
|
247
|
+
"[APPOINTMENT_SERVICE] Creating appointment via cloud function"
|
|
248
|
+
);
|
|
330
249
|
|
|
331
|
-
//
|
|
332
|
-
const
|
|
250
|
+
// Get the authenticated user's ID token
|
|
251
|
+
const currentUser = this.auth.currentUser;
|
|
252
|
+
if (!currentUser) {
|
|
253
|
+
throw new Error("User must be authenticated to create an appointment");
|
|
254
|
+
}
|
|
255
|
+
const idToken = await currentUser.getIdToken();
|
|
333
256
|
|
|
334
|
-
//
|
|
335
|
-
const
|
|
336
|
-
this.db,
|
|
337
|
-
validatedData.clinicBranchId,
|
|
338
|
-
validatedData.practitionerId,
|
|
339
|
-
validatedData.patientId,
|
|
340
|
-
validatedData.procedureId
|
|
341
|
-
);
|
|
257
|
+
// Construct the function URL for the Express app endpoint
|
|
258
|
+
const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/orchestrateAppointmentCreation`;
|
|
342
259
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
260
|
+
// Prepare request data for the Cloud Function
|
|
261
|
+
// Map CreateAppointmentData to OrchestrateAppointmentCreationData format
|
|
262
|
+
const requestData = {
|
|
263
|
+
patientId: data.patientId,
|
|
264
|
+
procedureId: data.procedureId,
|
|
265
|
+
appointmentStartTime: data.appointmentStartTime.toMillis
|
|
266
|
+
? data.appointmentStartTime.toMillis()
|
|
267
|
+
: new Date(data.appointmentStartTime as any).getTime(),
|
|
268
|
+
appointmentEndTime: data.appointmentEndTime.toMillis
|
|
269
|
+
? data.appointmentEndTime.toMillis()
|
|
270
|
+
: new Date(data.appointmentEndTime as any).getTime(),
|
|
271
|
+
patientNotes: data.patientNotes || null,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
console.log(
|
|
275
|
+
`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`
|
|
349
276
|
);
|
|
350
277
|
|
|
278
|
+
// Make the HTTP request with expanded CORS options for browser
|
|
279
|
+
const response = await fetch(functionUrl, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
mode: "cors",
|
|
282
|
+
cache: "no-cache",
|
|
283
|
+
credentials: "omit",
|
|
284
|
+
headers: {
|
|
285
|
+
"Content-Type": "application/json",
|
|
286
|
+
Authorization: `Bearer ${idToken}`,
|
|
287
|
+
},
|
|
288
|
+
redirect: "follow",
|
|
289
|
+
referrerPolicy: "no-referrer",
|
|
290
|
+
body: JSON.stringify(requestData),
|
|
291
|
+
});
|
|
292
|
+
|
|
351
293
|
console.log(
|
|
352
|
-
`[APPOINTMENT_SERVICE]
|
|
294
|
+
`[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
|
|
353
295
|
);
|
|
354
296
|
|
|
355
|
-
|
|
297
|
+
// Check if the request was successful
|
|
298
|
+
if (!response.ok) {
|
|
299
|
+
const errorText = await response.text();
|
|
300
|
+
console.error(
|
|
301
|
+
`[APPOINTMENT_SERVICE] Error response details: ${errorText}`
|
|
302
|
+
);
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Parse the response
|
|
309
|
+
const result = await response.json();
|
|
310
|
+
|
|
311
|
+
if (!result.success) {
|
|
312
|
+
throw new Error(result.error || "Failed to create appointment");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// If the backend returns the full appointment data
|
|
316
|
+
if (result.appointmentData) {
|
|
317
|
+
console.log(
|
|
318
|
+
`[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`
|
|
319
|
+
);
|
|
320
|
+
return result.appointmentData;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// If only the ID is returned, fetch the complete appointment
|
|
324
|
+
const createdAppointment = await this.getAppointmentById(
|
|
325
|
+
result.appointmentId
|
|
326
|
+
);
|
|
327
|
+
if (!createdAppointment) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Failed to retrieve created appointment with ID: ${result.appointmentId}`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return createdAppointment;
|
|
356
334
|
} catch (error) {
|
|
357
|
-
console.error(
|
|
335
|
+
console.error(
|
|
336
|
+
"[APPOINTMENT_SERVICE] Error creating appointment via cloud function:",
|
|
337
|
+
error
|
|
338
|
+
);
|
|
358
339
|
throw error;
|
|
359
340
|
}
|
|
360
341
|
}
|
|
@@ -580,61 +561,67 @@ export class AppointmentService extends BaseService {
|
|
|
580
561
|
*
|
|
581
562
|
* @param appointmentId ID of the appointment
|
|
582
563
|
* @param newStatus New status to set
|
|
583
|
-
* @param
|
|
584
|
-
* @param canceledBy Required if canceling the appointment
|
|
564
|
+
* @param details Optional details for the status change
|
|
585
565
|
* @returns The updated appointment
|
|
586
566
|
*/
|
|
587
567
|
async updateAppointmentStatus(
|
|
588
568
|
appointmentId: string,
|
|
589
569
|
newStatus: AppointmentStatus,
|
|
590
|
-
|
|
591
|
-
|
|
570
|
+
details?: {
|
|
571
|
+
cancellationReason?: string;
|
|
572
|
+
canceledBy?: "patient" | "clinic" | "practitioner" | "system";
|
|
573
|
+
}
|
|
592
574
|
): Promise<Appointment> {
|
|
593
575
|
console.log(
|
|
594
576
|
`[APPOINTMENT_SERVICE] Updating status of appointment ${appointmentId} to ${newStatus}`
|
|
595
577
|
);
|
|
578
|
+
const updateData: UpdateAppointmentData = {
|
|
579
|
+
status: newStatus,
|
|
580
|
+
updatedAt: serverTimestamp(),
|
|
581
|
+
};
|
|
596
582
|
|
|
597
|
-
// Create update data based on the new status
|
|
598
|
-
const updateData: UpdateAppointmentData = { status: newStatus };
|
|
599
|
-
|
|
600
|
-
// Add cancellation details if applicable
|
|
601
583
|
if (
|
|
602
584
|
newStatus === AppointmentStatus.CANCELED_CLINIC ||
|
|
603
|
-
newStatus === AppointmentStatus.CANCELED_PATIENT
|
|
585
|
+
newStatus === AppointmentStatus.CANCELED_PATIENT ||
|
|
586
|
+
newStatus === AppointmentStatus.CANCELED_PATIENT_RESCHEDULED
|
|
604
587
|
) {
|
|
605
|
-
if (!cancellationReason) {
|
|
606
|
-
throw new Error(
|
|
607
|
-
"Cancellation reason is required when canceling an appointment"
|
|
608
|
-
);
|
|
588
|
+
if (!details?.cancellationReason) {
|
|
589
|
+
throw new Error("Cancellation reason is required when canceling.");
|
|
609
590
|
}
|
|
610
|
-
if (!canceledBy) {
|
|
611
|
-
throw new Error(
|
|
612
|
-
"Canceled by is required when canceling an appointment"
|
|
613
|
-
);
|
|
591
|
+
if (!details?.canceledBy) {
|
|
592
|
+
throw new Error("Canceled by is required when canceling.");
|
|
614
593
|
}
|
|
615
|
-
|
|
616
|
-
updateData.
|
|
617
|
-
updateData.
|
|
594
|
+
updateData.cancellationReason = details.cancellationReason;
|
|
595
|
+
updateData.canceledBy = details.canceledBy;
|
|
596
|
+
updateData.cancellationTime = Timestamp.now();
|
|
618
597
|
}
|
|
619
598
|
|
|
620
|
-
// Add confirmation time if confirming
|
|
621
599
|
if (newStatus === AppointmentStatus.CONFIRMED) {
|
|
622
600
|
updateData.confirmationTime = Timestamp.now();
|
|
623
601
|
}
|
|
624
602
|
|
|
603
|
+
if (newStatus === AppointmentStatus.RESCHEDULED_BY_CLINIC) {
|
|
604
|
+
updateData.rescheduleTime = Timestamp.now();
|
|
605
|
+
}
|
|
606
|
+
|
|
625
607
|
return this.updateAppointment(appointmentId, updateData);
|
|
626
608
|
}
|
|
627
609
|
|
|
628
610
|
/**
|
|
629
|
-
* Confirms an
|
|
630
|
-
*
|
|
631
|
-
* @param appointmentId ID of the appointment to confirm
|
|
632
|
-
* @returns The confirmed appointment
|
|
611
|
+
* Confirms a PENDING appointment by an Admin/Clinic.
|
|
633
612
|
*/
|
|
634
|
-
async
|
|
613
|
+
async confirmAppointmentAdmin(appointmentId: string): Promise<Appointment> {
|
|
635
614
|
console.log(
|
|
636
|
-
`[APPOINTMENT_SERVICE]
|
|
615
|
+
`[APPOINTMENT_SERVICE] Admin confirming appointment: ${appointmentId}`
|
|
637
616
|
);
|
|
617
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
618
|
+
if (!appointment)
|
|
619
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
620
|
+
if (appointment.status !== AppointmentStatus.PENDING) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Appointment ${appointmentId} is not in PENDING state to be confirmed.`
|
|
623
|
+
);
|
|
624
|
+
}
|
|
638
625
|
return this.updateAppointmentStatus(
|
|
639
626
|
appointmentId,
|
|
640
627
|
AppointmentStatus.CONFIRMED
|
|
@@ -642,184 +629,354 @@ export class AppointmentService extends BaseService {
|
|
|
642
629
|
}
|
|
643
630
|
|
|
644
631
|
/**
|
|
645
|
-
* Cancels an appointment
|
|
646
|
-
*
|
|
647
|
-
* @param appointmentId ID of the appointment to cancel
|
|
648
|
-
* @param reason Reason for cancellation
|
|
649
|
-
* @returns The canceled appointment
|
|
632
|
+
* Cancels an appointment by the User (Patient).
|
|
650
633
|
*/
|
|
651
|
-
async
|
|
634
|
+
async cancelAppointmentUser(
|
|
652
635
|
appointmentId: string,
|
|
653
636
|
reason: string
|
|
654
637
|
): Promise<Appointment> {
|
|
655
638
|
console.log(
|
|
656
|
-
`[APPOINTMENT_SERVICE]
|
|
639
|
+
`[APPOINTMENT_SERVICE] User canceling appointment: ${appointmentId}`
|
|
657
640
|
);
|
|
658
641
|
return this.updateAppointmentStatus(
|
|
659
642
|
appointmentId,
|
|
660
|
-
AppointmentStatus.
|
|
661
|
-
|
|
662
|
-
|
|
643
|
+
AppointmentStatus.CANCELED_PATIENT,
|
|
644
|
+
{
|
|
645
|
+
cancellationReason: reason,
|
|
646
|
+
canceledBy: "patient",
|
|
647
|
+
}
|
|
663
648
|
);
|
|
664
649
|
}
|
|
665
650
|
|
|
666
651
|
/**
|
|
667
|
-
* Cancels an appointment
|
|
668
|
-
*
|
|
669
|
-
* @param appointmentId ID of the appointment to cancel
|
|
670
|
-
* @param reason Reason for cancellation
|
|
671
|
-
* @returns The canceled appointment
|
|
652
|
+
* Cancels an appointment by an Admin/Clinic.
|
|
672
653
|
*/
|
|
673
|
-
async
|
|
654
|
+
async cancelAppointmentAdmin(
|
|
674
655
|
appointmentId: string,
|
|
675
656
|
reason: string
|
|
676
657
|
): Promise<Appointment> {
|
|
677
658
|
console.log(
|
|
678
|
-
`[APPOINTMENT_SERVICE]
|
|
659
|
+
`[APPOINTMENT_SERVICE] Admin canceling appointment: ${appointmentId}`
|
|
679
660
|
);
|
|
680
661
|
return this.updateAppointmentStatus(
|
|
681
662
|
appointmentId,
|
|
682
|
-
AppointmentStatus.
|
|
683
|
-
|
|
684
|
-
|
|
663
|
+
AppointmentStatus.CANCELED_CLINIC,
|
|
664
|
+
{
|
|
665
|
+
cancellationReason: reason,
|
|
666
|
+
canceledBy: "clinic",
|
|
667
|
+
}
|
|
685
668
|
);
|
|
686
669
|
}
|
|
687
670
|
|
|
688
671
|
/**
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
|
|
692
|
-
|
|
672
|
+
* Admin proposes to reschedule an appointment.
|
|
673
|
+
* Sets status to RESCHEDULED_BY_CLINIC and updates times.
|
|
674
|
+
*/
|
|
675
|
+
async rescheduleAppointmentAdmin(
|
|
676
|
+
appointmentId: string,
|
|
677
|
+
newStartTime: Timestamp,
|
|
678
|
+
newEndTime: Timestamp
|
|
679
|
+
): Promise<Appointment> {
|
|
680
|
+
console.log(
|
|
681
|
+
`[APPOINTMENT_SERVICE] Admin rescheduling appointment: ${appointmentId}`
|
|
682
|
+
);
|
|
683
|
+
if (newEndTime.toMillis() <= newStartTime.toMillis()) {
|
|
684
|
+
throw new Error("New end time must be after new start time.");
|
|
685
|
+
}
|
|
686
|
+
const updateData: UpdateAppointmentData = {
|
|
687
|
+
status: AppointmentStatus.RESCHEDULED_BY_CLINIC,
|
|
688
|
+
appointmentStartTime: newStartTime,
|
|
689
|
+
appointmentEndTime: newEndTime,
|
|
690
|
+
rescheduleTime: Timestamp.now(),
|
|
691
|
+
confirmationTime: null,
|
|
692
|
+
updatedAt: serverTimestamp(),
|
|
693
|
+
};
|
|
694
|
+
return this.updateAppointment(appointmentId, updateData);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* User confirms a reschedule proposed by the clinic.
|
|
699
|
+
* Status changes from RESCHEDULED_BY_CLINIC to CONFIRMED.
|
|
693
700
|
*/
|
|
694
|
-
async
|
|
701
|
+
async rescheduleAppointmentConfirmUser(
|
|
702
|
+
appointmentId: string
|
|
703
|
+
): Promise<Appointment> {
|
|
695
704
|
console.log(
|
|
696
|
-
`[APPOINTMENT_SERVICE]
|
|
705
|
+
`[APPOINTMENT_SERVICE] User confirming reschedule for: ${appointmentId}`
|
|
697
706
|
);
|
|
707
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
708
|
+
if (!appointment)
|
|
709
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
710
|
+
if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
|
|
713
|
+
);
|
|
714
|
+
}
|
|
698
715
|
return this.updateAppointmentStatus(
|
|
699
716
|
appointmentId,
|
|
700
|
-
AppointmentStatus.
|
|
717
|
+
AppointmentStatus.CONFIRMED
|
|
701
718
|
);
|
|
702
719
|
}
|
|
703
720
|
|
|
704
721
|
/**
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* @param appointmentId ID of the appointment
|
|
708
|
-
* @returns The updated appointment
|
|
722
|
+
* User rejects a reschedule proposed by the clinic.
|
|
723
|
+
* Status changes from RESCHEDULED_BY_CLINIC to CANCELED_PATIENT_RESCHEDULED.
|
|
709
724
|
*/
|
|
710
|
-
async
|
|
711
|
-
|
|
725
|
+
async rescheduleAppointmentRejectUser(
|
|
726
|
+
appointmentId: string,
|
|
727
|
+
reason: string
|
|
728
|
+
): Promise<Appointment> {
|
|
729
|
+
console.log(
|
|
730
|
+
`[APPOINTMENT_SERVICE] User rejecting reschedule for: ${appointmentId}`
|
|
731
|
+
);
|
|
732
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
733
|
+
if (!appointment)
|
|
734
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
735
|
+
if (appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC) {
|
|
736
|
+
throw new Error(
|
|
737
|
+
`Appointment ${appointmentId} is not in RESCHEDULED_BY_CLINIC state.`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
712
740
|
return this.updateAppointmentStatus(
|
|
713
741
|
appointmentId,
|
|
714
|
-
AppointmentStatus.
|
|
742
|
+
AppointmentStatus.CANCELED_PATIENT_RESCHEDULED,
|
|
743
|
+
{
|
|
744
|
+
cancellationReason: reason,
|
|
745
|
+
canceledBy: "patient",
|
|
746
|
+
}
|
|
715
747
|
);
|
|
716
748
|
}
|
|
717
749
|
|
|
718
750
|
/**
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
* @param appointmentId ID of the appointment
|
|
722
|
-
* @param actualDurationMinutes Actual duration of the appointment in minutes
|
|
723
|
-
* @returns The updated appointment
|
|
751
|
+
* Admin checks in a patient for their appointment.
|
|
752
|
+
* Requires all pending user forms to be completed.
|
|
724
753
|
*/
|
|
725
|
-
async
|
|
754
|
+
async checkInPatientAdmin(appointmentId: string): Promise<Appointment> {
|
|
755
|
+
console.log(
|
|
756
|
+
`[APPOINTMENT_SERVICE] Admin checking in patient for: ${appointmentId}`
|
|
757
|
+
);
|
|
758
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
759
|
+
if (!appointment)
|
|
760
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
761
|
+
|
|
762
|
+
if (
|
|
763
|
+
appointment.pendingUserFormsIds &&
|
|
764
|
+
appointment.pendingUserFormsIds.length > 0
|
|
765
|
+
) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`Cannot check in: Patient has ${
|
|
768
|
+
appointment.pendingUserFormsIds.length
|
|
769
|
+
} pending required form(s). IDs: ${appointment.pendingUserFormsIds.join(
|
|
770
|
+
", "
|
|
771
|
+
)}`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (
|
|
775
|
+
appointment.status !== AppointmentStatus.CONFIRMED &&
|
|
776
|
+
appointment.status !== AppointmentStatus.RESCHEDULED_BY_CLINIC
|
|
777
|
+
) {
|
|
778
|
+
console.warn(
|
|
779
|
+
`Checking in appointment ${appointmentId} with status ${appointment.status}. Ensure this is intended.`
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return this.updateAppointmentStatus(
|
|
784
|
+
appointmentId,
|
|
785
|
+
AppointmentStatus.CHECKED_IN
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Doctor starts the appointment procedure.
|
|
791
|
+
*/
|
|
792
|
+
async startAppointmentDoctor(appointmentId: string): Promise<Appointment> {
|
|
793
|
+
console.log(
|
|
794
|
+
`[APPOINTMENT_SERVICE] Doctor starting appointment: ${appointmentId}`
|
|
795
|
+
);
|
|
796
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
797
|
+
if (!appointment)
|
|
798
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
799
|
+
if (appointment.status !== AppointmentStatus.CHECKED_IN) {
|
|
800
|
+
throw new Error(
|
|
801
|
+
`Appointment ${appointmentId} must be CHECKED_IN to start.`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
// Update status and set procedureActualStartTime
|
|
805
|
+
const updateData: UpdateAppointmentData = {
|
|
806
|
+
status: AppointmentStatus.IN_PROGRESS,
|
|
807
|
+
procedureActualStartTime: Timestamp.now(), // Set actual start time
|
|
808
|
+
updatedAt: serverTimestamp(),
|
|
809
|
+
};
|
|
810
|
+
return this.updateAppointment(appointmentId, updateData);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Doctor completes and finalizes the appointment.
|
|
815
|
+
*/
|
|
816
|
+
async completeAppointmentDoctor(
|
|
726
817
|
appointmentId: string,
|
|
727
|
-
|
|
818
|
+
finalizationNotes: string,
|
|
819
|
+
actualDurationMinutesInput?: number // Renamed to avoid conflict if we calculate
|
|
728
820
|
): Promise<Appointment> {
|
|
729
821
|
console.log(
|
|
730
|
-
`[APPOINTMENT_SERVICE]
|
|
822
|
+
`[APPOINTMENT_SERVICE] Doctor completing appointment: ${appointmentId}`
|
|
731
823
|
);
|
|
824
|
+
const currentUser = this.auth.currentUser;
|
|
825
|
+
if (!currentUser)
|
|
826
|
+
throw new Error("Authentication required to complete appointment.");
|
|
827
|
+
|
|
828
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
829
|
+
if (!appointment)
|
|
830
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
831
|
+
|
|
832
|
+
let calculatedDurationMinutes = actualDurationMinutesInput;
|
|
833
|
+
const procedureCompletionTime = Timestamp.now();
|
|
834
|
+
|
|
835
|
+
// Calculate duration if not provided and actual start time is available
|
|
836
|
+
if (
|
|
837
|
+
calculatedDurationMinutes === undefined &&
|
|
838
|
+
appointment.procedureActualStartTime
|
|
839
|
+
) {
|
|
840
|
+
const startTimeMillis = appointment.procedureActualStartTime.toMillis();
|
|
841
|
+
const endTimeMillis = procedureCompletionTime.toMillis();
|
|
842
|
+
if (endTimeMillis > startTimeMillis) {
|
|
843
|
+
calculatedDurationMinutes = Math.round(
|
|
844
|
+
(endTimeMillis - startTimeMillis) / 60000
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
732
848
|
|
|
733
849
|
const updateData: UpdateAppointmentData = {
|
|
734
850
|
status: AppointmentStatus.COMPLETED,
|
|
735
|
-
actualDurationMinutes,
|
|
851
|
+
actualDurationMinutes: calculatedDurationMinutes, // Use calculated or provided duration
|
|
852
|
+
finalizedDetails: {
|
|
853
|
+
by: currentUser.uid, // This is used ID, not practitioner's profile ID (just so we know who completed the appointment)
|
|
854
|
+
at: procedureCompletionTime, // Use consistent completion timestamp
|
|
855
|
+
notes: finalizationNotes,
|
|
856
|
+
},
|
|
857
|
+
// Optionally update appointmentEndTime to the actual completion time
|
|
858
|
+
// appointmentEndTime: procedureCompletionTime,
|
|
859
|
+
updatedAt: serverTimestamp(),
|
|
736
860
|
};
|
|
737
|
-
|
|
738
861
|
return this.updateAppointment(appointmentId, updateData);
|
|
739
862
|
}
|
|
740
863
|
|
|
741
864
|
/**
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
* @param appointmentId ID of the appointment
|
|
745
|
-
* @returns The updated appointment
|
|
865
|
+
* Admin marks an appointment as No-Show.
|
|
746
866
|
*/
|
|
747
|
-
async
|
|
867
|
+
async markNoShowAdmin(appointmentId: string): Promise<Appointment> {
|
|
748
868
|
console.log(
|
|
749
|
-
`[APPOINTMENT_SERVICE]
|
|
869
|
+
`[APPOINTMENT_SERVICE] Admin marking no-show for: ${appointmentId}`
|
|
750
870
|
);
|
|
871
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
872
|
+
if (!appointment)
|
|
873
|
+
throw new Error(`Appointment ${appointmentId} not found.`);
|
|
874
|
+
if (
|
|
875
|
+
Timestamp.now().toMillis() < appointment.appointmentStartTime.toMillis()
|
|
876
|
+
) {
|
|
877
|
+
throw new Error("Cannot mark no-show before appointment start time.");
|
|
878
|
+
}
|
|
751
879
|
return this.updateAppointmentStatus(
|
|
752
880
|
appointmentId,
|
|
753
|
-
AppointmentStatus.NO_SHOW
|
|
881
|
+
AppointmentStatus.NO_SHOW,
|
|
882
|
+
{
|
|
883
|
+
cancellationReason: "Patient did not show up for the appointment.",
|
|
884
|
+
canceledBy: "clinic",
|
|
885
|
+
}
|
|
754
886
|
);
|
|
755
887
|
}
|
|
756
888
|
|
|
757
889
|
/**
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
* @param appointmentId ID of the appointment
|
|
761
|
-
* @param paymentStatus New payment status
|
|
762
|
-
* @param paymentTransactionId Optional transaction ID for the payment
|
|
763
|
-
* @returns The updated appointment
|
|
890
|
+
* Adds a media item to an appointment.
|
|
764
891
|
*/
|
|
765
|
-
async
|
|
892
|
+
async addMediaToAppointment(
|
|
766
893
|
appointmentId: string,
|
|
767
|
-
|
|
768
|
-
paymentTransactionId?: string
|
|
894
|
+
mediaItemData: Omit<AppointmentMediaItem, "id" | "uploadedAt">
|
|
769
895
|
): Promise<Appointment> {
|
|
770
896
|
console.log(
|
|
771
|
-
`[APPOINTMENT_SERVICE]
|
|
897
|
+
`[APPOINTMENT_SERVICE] Adding media to appointment ${appointmentId}`
|
|
772
898
|
);
|
|
899
|
+
const currentUser = this.auth.currentUser;
|
|
900
|
+
if (!currentUser) throw new Error("Authentication required.");
|
|
901
|
+
|
|
902
|
+
const newMediaItem: AppointmentMediaItem = {
|
|
903
|
+
...mediaItemData,
|
|
904
|
+
id: this.generateId(),
|
|
905
|
+
uploadedAt: Timestamp.now(),
|
|
906
|
+
uploadedBy: currentUser.uid,
|
|
907
|
+
};
|
|
773
908
|
|
|
774
909
|
const updateData: UpdateAppointmentData = {
|
|
775
|
-
|
|
776
|
-
|
|
910
|
+
media: arrayUnion(newMediaItem) as any,
|
|
911
|
+
updatedAt: serverTimestamp(),
|
|
777
912
|
};
|
|
778
|
-
|
|
779
913
|
return this.updateAppointment(appointmentId, updateData);
|
|
780
914
|
}
|
|
781
915
|
|
|
782
916
|
/**
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
* @param appointmentId ID of the appointment
|
|
786
|
-
* @param requirementIds IDs of the requirements to mark as completed
|
|
787
|
-
* @returns The updated appointment
|
|
917
|
+
* Removes a media item from an appointment.
|
|
788
918
|
*/
|
|
789
|
-
async
|
|
919
|
+
async removeMediaFromAppointment(
|
|
790
920
|
appointmentId: string,
|
|
791
|
-
|
|
921
|
+
mediaItemId: string
|
|
792
922
|
): Promise<Appointment> {
|
|
793
923
|
console.log(
|
|
794
|
-
`[APPOINTMENT_SERVICE]
|
|
924
|
+
`[APPOINTMENT_SERVICE] Removing media ${mediaItemId} from appointment ${appointmentId}`
|
|
795
925
|
);
|
|
926
|
+
const appointment = await this.getAppointmentById(appointmentId);
|
|
927
|
+
if (!appointment || !appointment.media) {
|
|
928
|
+
throw new Error("Appointment or media list not found.");
|
|
929
|
+
}
|
|
930
|
+
const mediaToRemove = appointment.media.find((m) => m.id === mediaItemId);
|
|
931
|
+
if (!mediaToRemove) {
|
|
932
|
+
throw new Error(`Media item ${mediaItemId} not found in appointment.`);
|
|
933
|
+
}
|
|
796
934
|
|
|
797
935
|
const updateData: UpdateAppointmentData = {
|
|
798
|
-
|
|
936
|
+
media: arrayRemove(mediaToRemove) as any,
|
|
937
|
+
updatedAt: serverTimestamp(),
|
|
799
938
|
};
|
|
800
|
-
|
|
801
939
|
return this.updateAppointment(appointmentId, updateData);
|
|
802
940
|
}
|
|
803
941
|
|
|
804
942
|
/**
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
* @param appointmentId ID of the appointment
|
|
808
|
-
* @param requirementIds IDs of the requirements to mark as completed
|
|
809
|
-
* @returns The updated appointment
|
|
943
|
+
* Adds or updates review information for an appointment.
|
|
810
944
|
*/
|
|
811
|
-
async
|
|
945
|
+
async addReviewToAppointment(
|
|
812
946
|
appointmentId: string,
|
|
813
|
-
|
|
947
|
+
reviewData: Omit<PatientReviewInfo, "reviewedAt" | "reviewId">
|
|
814
948
|
): Promise<Appointment> {
|
|
815
949
|
console.log(
|
|
816
|
-
`[APPOINTMENT_SERVICE]
|
|
950
|
+
`[APPOINTMENT_SERVICE] Adding review to appointment ${appointmentId}`
|
|
817
951
|
);
|
|
818
|
-
|
|
952
|
+
const newReviewInfo: PatientReviewInfo = {
|
|
953
|
+
...reviewData,
|
|
954
|
+
reviewId: this.generateId(),
|
|
955
|
+
reviewedAt: Timestamp.now(),
|
|
956
|
+
};
|
|
819
957
|
const updateData: UpdateAppointmentData = {
|
|
820
|
-
|
|
958
|
+
reviewInfo: newReviewInfo,
|
|
959
|
+
updatedAt: serverTimestamp(),
|
|
821
960
|
};
|
|
961
|
+
return this.updateAppointment(appointmentId, updateData);
|
|
962
|
+
}
|
|
822
963
|
|
|
964
|
+
/**
|
|
965
|
+
* Updates the payment status of an appointment.
|
|
966
|
+
*/
|
|
967
|
+
async updatePaymentStatus(
|
|
968
|
+
appointmentId: string,
|
|
969
|
+
paymentStatus: PaymentStatus,
|
|
970
|
+
paymentTransactionId?: string
|
|
971
|
+
): Promise<Appointment> {
|
|
972
|
+
console.log(
|
|
973
|
+
`[APPOINTMENT_SERVICE] Updating payment status of appointment ${appointmentId} to ${paymentStatus}`
|
|
974
|
+
);
|
|
975
|
+
const updateData: UpdateAppointmentData = {
|
|
976
|
+
paymentStatus,
|
|
977
|
+
paymentTransactionId: paymentTransactionId || null,
|
|
978
|
+
updatedAt: serverTimestamp(),
|
|
979
|
+
};
|
|
823
980
|
return this.updateAppointment(appointmentId, updateData);
|
|
824
981
|
}
|
|
825
982
|
|
|
@@ -844,25 +1001,4 @@ export class AppointmentService extends BaseService {
|
|
|
844
1001
|
|
|
845
1002
|
return this.updateAppointment(appointmentId, updateData);
|
|
846
1003
|
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Debug helper: Get the current user's ID token for testing purposes
|
|
850
|
-
* Use this token in Postman with Authorization: Bearer TOKEN
|
|
851
|
-
*/
|
|
852
|
-
async getDebugToken(): Promise<string | null> {
|
|
853
|
-
try {
|
|
854
|
-
const currentUser = this.auth.currentUser;
|
|
855
|
-
if (!currentUser) {
|
|
856
|
-
console.log("[APPOINTMENT_SERVICE] No user is signed in");
|
|
857
|
-
return null;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const idToken = await currentUser.getIdToken();
|
|
861
|
-
console.log("[APPOINTMENT_SERVICE] Debug token:", idToken);
|
|
862
|
-
return idToken;
|
|
863
|
-
} catch (error) {
|
|
864
|
-
console.error("[APPOINTMENT_SERVICE] Error getting debug token:", error);
|
|
865
|
-
return null;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
1004
|
}
|