@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
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collection,
|
|
3
|
+
getDocs,
|
|
4
|
+
query,
|
|
5
|
+
where,
|
|
6
|
+
doc,
|
|
7
|
+
updateDoc,
|
|
8
|
+
Timestamp,
|
|
9
|
+
DocumentReference,
|
|
10
|
+
serverTimestamp,
|
|
11
|
+
orderBy,
|
|
12
|
+
limit,
|
|
13
|
+
startAfter,
|
|
14
|
+
getDoc,
|
|
15
|
+
FieldPath,
|
|
16
|
+
WhereFilterOp,
|
|
17
|
+
Firestore,
|
|
18
|
+
DocumentSnapshot,
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { FirebaseApp } from "firebase/app";
|
|
21
|
+
import { Auth } from "firebase/auth";
|
|
22
|
+
import { BaseService } from "../base.service";
|
|
23
|
+
import {
|
|
24
|
+
PatientRequirementInstance,
|
|
25
|
+
PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME,
|
|
26
|
+
PatientInstructionStatus,
|
|
27
|
+
PatientRequirementOverallStatus,
|
|
28
|
+
PatientRequirementInstruction,
|
|
29
|
+
} from "../../types/patient/patient-requirements";
|
|
30
|
+
import { UserRole } from "../../types"; // Assuming UserRole is in the root types
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Interface for filtering active patient requirements.
|
|
34
|
+
*/
|
|
35
|
+
export interface PatientRequirementsFilters {
|
|
36
|
+
appointmentId?: string | "all"; // Specific appointment, or 'all' for any appointment
|
|
37
|
+
statuses?: PatientRequirementOverallStatus[];
|
|
38
|
+
instructionStatuses?: PatientInstructionStatus[]; // Filter by status of individual instructions
|
|
39
|
+
dueBefore?: Timestamp; // Filter for instructions due before this time
|
|
40
|
+
dueAfter?: Timestamp; // Filter for instructions due after this time
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class PatientRequirementsService extends BaseService {
|
|
44
|
+
constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
|
|
45
|
+
super(db, auth, app);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private getPatientRequirementsCollectionRef(patientId: string) {
|
|
49
|
+
return collection(
|
|
50
|
+
this.db,
|
|
51
|
+
`patients/${patientId}/${PATIENT_REQUIREMENTS_SUBCOLLECTION_NAME}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private getPatientRequirementDocRef(
|
|
56
|
+
patientId: string,
|
|
57
|
+
instanceId: string
|
|
58
|
+
): DocumentReference<PatientRequirementInstance> {
|
|
59
|
+
return doc(
|
|
60
|
+
this.getPatientRequirementsCollectionRef(patientId),
|
|
61
|
+
instanceId
|
|
62
|
+
) as DocumentReference<PatientRequirementInstance>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gets a specific patient requirement instance by its ID.
|
|
67
|
+
* @param patientId - The ID of the patient.
|
|
68
|
+
* @param instanceId - The ID of the requirement instance.
|
|
69
|
+
* @returns The patient requirement instance or null if not found.
|
|
70
|
+
*/
|
|
71
|
+
async getPatientRequirementInstance(
|
|
72
|
+
patientId: string,
|
|
73
|
+
instanceId: string
|
|
74
|
+
): Promise<PatientRequirementInstance | null> {
|
|
75
|
+
const docRef = this.getPatientRequirementDocRef(patientId, instanceId);
|
|
76
|
+
const docSnap = await getDoc(docRef);
|
|
77
|
+
if (!docSnap.exists()) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Explicitly type data to exclude 'id' before spreading
|
|
81
|
+
const data = docSnap.data() as Omit<PatientRequirementInstance, "id">;
|
|
82
|
+
return { id: docSnap.id, ...data } as PatientRequirementInstance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Retrieves patient requirement instances based on specified filters.
|
|
87
|
+
* This is a flexible query method.
|
|
88
|
+
*
|
|
89
|
+
* @param patientId - The ID of the patient.
|
|
90
|
+
* @param filters - Optional filters for appointmentId, overall statuses, instruction statuses, and due timeframes.
|
|
91
|
+
* @param pageLimit - Optional limit for pagination.
|
|
92
|
+
* @param lastVisible - Optional last document snapshot for pagination.
|
|
93
|
+
* @returns A promise resolving to an array of matching patient requirement instances and the last document snapshot.
|
|
94
|
+
*/
|
|
95
|
+
async getAllPatientRequirementInstances(
|
|
96
|
+
patientId: string,
|
|
97
|
+
filters?: PatientRequirementsFilters,
|
|
98
|
+
pageLimit: number = 20,
|
|
99
|
+
lastVisible?: DocumentSnapshot
|
|
100
|
+
): Promise<{
|
|
101
|
+
requirements: PatientRequirementInstance[];
|
|
102
|
+
lastDoc: DocumentSnapshot | null;
|
|
103
|
+
}> {
|
|
104
|
+
const collRef = this.getPatientRequirementsCollectionRef(patientId);
|
|
105
|
+
let q = query(collRef, orderBy("createdAt", "desc")); // Default sort
|
|
106
|
+
|
|
107
|
+
const queryConstraints = [];
|
|
108
|
+
|
|
109
|
+
if (filters?.appointmentId && filters.appointmentId !== "all") {
|
|
110
|
+
queryConstraints.push(
|
|
111
|
+
where("appointmentId", "==", filters.appointmentId)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (filters?.statuses && filters.statuses.length > 0) {
|
|
116
|
+
queryConstraints.push(where("overallStatus", "in", filters.statuses));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Filtering by instruction statuses or due times requires iterating post-fetch
|
|
120
|
+
// or more complex data modeling if direct querying is essential at scale.
|
|
121
|
+
// For a "light" service, post-fetch filtering for these is acceptable for now.
|
|
122
|
+
|
|
123
|
+
if (lastVisible) {
|
|
124
|
+
queryConstraints.push(startAfter(lastVisible));
|
|
125
|
+
}
|
|
126
|
+
queryConstraints.push(limit(pageLimit));
|
|
127
|
+
|
|
128
|
+
q = query(collRef, ...queryConstraints);
|
|
129
|
+
|
|
130
|
+
const snapshot = await getDocs(q);
|
|
131
|
+
let requirements = snapshot.docs.map((docSnap: DocumentSnapshot) => {
|
|
132
|
+
// Explicitly cast data after ensuring it's not undefined (though .data() on QueryDocumentSnapshot is not undefined)
|
|
133
|
+
const data = docSnap.data() as Omit<PatientRequirementInstance, "id">;
|
|
134
|
+
return { id: docSnap.id, ...data } as PatientRequirementInstance;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Post-fetch filtering for instruction statuses and due times
|
|
138
|
+
if (
|
|
139
|
+
filters?.instructionStatuses &&
|
|
140
|
+
filters.instructionStatuses.length > 0
|
|
141
|
+
) {
|
|
142
|
+
requirements = requirements.filter((req) =>
|
|
143
|
+
req.instructions.some((instr) =>
|
|
144
|
+
filters.instructionStatuses!.includes(instr.status)
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (filters?.dueBefore) {
|
|
150
|
+
const dueBeforeMillis = filters.dueBefore.toMillis();
|
|
151
|
+
requirements = requirements.filter((req) =>
|
|
152
|
+
req.instructions.some(
|
|
153
|
+
(instr) => instr.dueTime.toMillis() < dueBeforeMillis
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (filters?.dueAfter) {
|
|
159
|
+
const dueAfterMillis = filters.dueAfter.toMillis();
|
|
160
|
+
requirements = requirements.filter((req) =>
|
|
161
|
+
req.instructions.some(
|
|
162
|
+
(instr) => instr.dueTime.toMillis() > dueAfterMillis
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1] || null;
|
|
168
|
+
|
|
169
|
+
return { requirements, lastDoc: newLastVisible };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Marks a specific instruction within a PatientRequirementInstance as ACTION_TAKEN.
|
|
174
|
+
* If all instructions are actioned, updates the overallStatus of the instance.
|
|
175
|
+
*
|
|
176
|
+
* @param patientId - The ID of the patient.
|
|
177
|
+
* @param instanceId - The ID of the PatientRequirementInstance.
|
|
178
|
+
* @param instructionId - The ID of the instruction to complete.
|
|
179
|
+
* @returns The updated PatientRequirementInstance.
|
|
180
|
+
* @throws Error if the instance or instruction is not found, or if the instruction is not in a completable state.
|
|
181
|
+
*/
|
|
182
|
+
async completeInstruction(
|
|
183
|
+
patientId: string,
|
|
184
|
+
instanceId: string,
|
|
185
|
+
instructionId: string
|
|
186
|
+
): Promise<PatientRequirementInstance> {
|
|
187
|
+
const instanceRef = this.getPatientRequirementDocRef(patientId, instanceId);
|
|
188
|
+
const instanceSnap = await getDoc(instanceRef); // Simplified: getDoc without explicit generic here
|
|
189
|
+
|
|
190
|
+
if (!instanceSnap.exists()) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`PatientRequirementInstance ${instanceId} not found for patient ${patientId}.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Explicitly cast data after exists check
|
|
197
|
+
const instanceData = instanceSnap.data() as Omit<
|
|
198
|
+
PatientRequirementInstance,
|
|
199
|
+
"id"
|
|
200
|
+
>;
|
|
201
|
+
const instance = { id: instanceSnap.id, ...instanceData };
|
|
202
|
+
|
|
203
|
+
const instructionIndex = instance.instructions.findIndex(
|
|
204
|
+
(instr) => instr.instructionId === instructionId
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (instructionIndex === -1) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Instruction ${instructionId} not found in instance ${instanceId}.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const instructionToUpdate = instance.instructions[instructionIndex];
|
|
214
|
+
|
|
215
|
+
// Allow completion if it's PENDING_NOTIFICATION, ACTION_DUE, or even MISSED (if policy allows late completion)
|
|
216
|
+
if (
|
|
217
|
+
instructionToUpdate.status !==
|
|
218
|
+
PatientInstructionStatus.PENDING_NOTIFICATION &&
|
|
219
|
+
instructionToUpdate.status !== PatientInstructionStatus.ACTION_DUE &&
|
|
220
|
+
instructionToUpdate.status !== PatientInstructionStatus.MISSED
|
|
221
|
+
) {
|
|
222
|
+
// If already ACTION_TAKEN or CANCELLED, do nothing or throw specific error
|
|
223
|
+
if (
|
|
224
|
+
instructionToUpdate.status === PatientInstructionStatus.ACTION_TAKEN
|
|
225
|
+
) {
|
|
226
|
+
console.warn(
|
|
227
|
+
`Instruction ${instructionId} is already marked as ACTION_TAKEN.`
|
|
228
|
+
);
|
|
229
|
+
return instance as PatientRequirementInstance; // Ensure return type matches
|
|
230
|
+
}
|
|
231
|
+
throw new Error(
|
|
232
|
+
`Instruction ${instructionId} is in status ${instructionToUpdate.status} and cannot be marked as completed.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const now = Timestamp.now();
|
|
237
|
+
const updatedInstructions = [...instance.instructions];
|
|
238
|
+
updatedInstructions[instructionIndex] = {
|
|
239
|
+
...instructionToUpdate,
|
|
240
|
+
status: PatientInstructionStatus.ACTION_TAKEN,
|
|
241
|
+
actionTakenAt: now,
|
|
242
|
+
updatedAt: now,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Check if all instructions are now ACTION_TAKEN
|
|
246
|
+
const allActionTaken = updatedInstructions.every(
|
|
247
|
+
(instr) => instr.status === PatientInstructionStatus.ACTION_TAKEN
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
let newOverallStatus = instance.overallStatus;
|
|
251
|
+
if (allActionTaken) {
|
|
252
|
+
newOverallStatus = PatientRequirementOverallStatus.ALL_INSTRUCTIONS_MET;
|
|
253
|
+
} else if (
|
|
254
|
+
updatedInstructions.some(
|
|
255
|
+
(instr) => instr.status === PatientInstructionStatus.ACTION_TAKEN
|
|
256
|
+
)
|
|
257
|
+
) {
|
|
258
|
+
// If some are taken, but not all, and it was previously just ACTIVE
|
|
259
|
+
newOverallStatus = PatientRequirementOverallStatus.PARTIALLY_COMPLETED;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const updatePayload: { [key: string]: any } = {
|
|
263
|
+
// Using a general type for updateDoc payload
|
|
264
|
+
instructions: updatedInstructions,
|
|
265
|
+
updatedAt: now,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (newOverallStatus !== instance.overallStatus) {
|
|
269
|
+
updatePayload.overallStatus = newOverallStatus;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await updateDoc(instanceRef, updatePayload);
|
|
273
|
+
|
|
274
|
+
// Construct the returned object accurately
|
|
275
|
+
return {
|
|
276
|
+
...instance,
|
|
277
|
+
instructions: updatedInstructions,
|
|
278
|
+
updatedAt: now,
|
|
279
|
+
overallStatus: newOverallStatus,
|
|
280
|
+
} as PatientRequirementInstance;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Note: As per the request, full CRUD (create, direct update of instance, delete) is not part of this light service,
|
|
284
|
+
// as those will be handled by Cloud Functions reacting to appointment lifecycle events.
|
|
285
|
+
}
|
|
@@ -179,6 +179,7 @@ export class ProcedureService extends BaseService {
|
|
|
179
179
|
technology,
|
|
180
180
|
product,
|
|
181
181
|
blockingConditions: technology.blockingConditions,
|
|
182
|
+
contraindications: technology.contraindications || [],
|
|
182
183
|
treatmentBenefits: technology.benefits,
|
|
183
184
|
preRequirements: technology.requirements.pre,
|
|
184
185
|
postRequirements: technology.requirements.post,
|
|
@@ -6,24 +6,26 @@ import {
|
|
|
6
6
|
} from "../profile";
|
|
7
7
|
import { ProcedureSummaryInfo } from "../procedure";
|
|
8
8
|
import { Currency } from "../../backoffice/types/static/pricing.types";
|
|
9
|
-
import { CalendarEventStatus } from "../calendar";
|
|
10
9
|
import { BlockingCondition } from "../../backoffice/types/static/blocking-condition.types";
|
|
11
10
|
import { Contraindication } from "../../backoffice/types/static/contraindication.types";
|
|
12
11
|
import { Requirement } from "../../backoffice/types/requirement.types";
|
|
12
|
+
import { FilledDocumentStatus } from "../documentation-templates";
|
|
13
|
+
import type { ProcedureFamily } from "../../backoffice";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Enum defining the possible statuses of an appointment.
|
|
16
17
|
*/
|
|
17
18
|
export enum AppointmentStatus {
|
|
18
|
-
|
|
19
|
+
PENDING = "pending", // Initial state after booking, before confirmation (if applicable)
|
|
19
20
|
CONFIRMED = "confirmed", // Confirmed by clinic/practitioner
|
|
20
21
|
CHECKED_IN = "checked_in", // Patient has arrived
|
|
21
22
|
IN_PROGRESS = "in_progress", // Procedure has started
|
|
22
23
|
COMPLETED = "completed", // Procedure finished successfully
|
|
23
24
|
CANCELED_PATIENT = "canceled_patient", // Canceled by the patient
|
|
25
|
+
CANCELED_PATIENT_RESCHEDULED = "canceled_patient_rescheduled", // Canceled by the patient and rescheduled by the clinic
|
|
24
26
|
CANCELED_CLINIC = "canceled_clinic", // Canceled by the clinic/practitioner
|
|
25
27
|
NO_SHOW = "no_show", // Patient did not attend
|
|
26
|
-
|
|
28
|
+
RESCHEDULED_BY_CLINIC = "rescheduled_by_clinic", // When appointment is rescheduled by the clinic, waiting for patient confirmation or cancellation (when reschedule is accepted, status goes to confirmed, if not accepted, then status goes to canceled_patient_rescheduled)
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
@@ -37,6 +39,76 @@ export enum PaymentStatus {
|
|
|
37
39
|
NOT_APPLICABLE = "not_applicable", // For free services or other scenarios
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Enum for different types of media that can be attached to an appointment.
|
|
44
|
+
*/
|
|
45
|
+
export enum MediaType {
|
|
46
|
+
BEFORE_PHOTO = "before_photo",
|
|
47
|
+
AFTER_PHOTO = "after_photo",
|
|
48
|
+
CONSENT_SCAN = "consent_scan",
|
|
49
|
+
OTHER_DOCUMENT = "other_document",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Interface to describe a media file linked to an appointment.
|
|
54
|
+
*/
|
|
55
|
+
export interface AppointmentMediaItem {
|
|
56
|
+
id: string; // Auto-generated unique ID for the media item
|
|
57
|
+
type: MediaType;
|
|
58
|
+
url: string; // Cloud Storage URL
|
|
59
|
+
fileName?: string;
|
|
60
|
+
uploadedAt: Timestamp;
|
|
61
|
+
uploadedBy: string; // User ID (patient, practitioner, or clinic_admin)
|
|
62
|
+
description?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Interface for procedure-specific information
|
|
67
|
+
*/
|
|
68
|
+
export interface ProcedureExtendedInfo {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
description: string;
|
|
72
|
+
cost: number;
|
|
73
|
+
duration: number;
|
|
74
|
+
procedureFamily: ProcedureFamily;
|
|
75
|
+
procedureCategoryId: string;
|
|
76
|
+
procedureCategoryName: string;
|
|
77
|
+
procedureSubCategoryId: string;
|
|
78
|
+
procedureSubCategoryName: string;
|
|
79
|
+
procedureTechnologyId: string;
|
|
80
|
+
procedureTechnologyName: string;
|
|
81
|
+
procedureProductBrandId: string;
|
|
82
|
+
procedureProductBrandName: string;
|
|
83
|
+
procedureProductId: string;
|
|
84
|
+
procedureProductName: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Interface to describe a filled form linked to an appointment.
|
|
89
|
+
*/
|
|
90
|
+
export interface LinkedFormInfo {
|
|
91
|
+
formId: string; // ID of the FilledDocument
|
|
92
|
+
templateId: string;
|
|
93
|
+
templateVersion: number;
|
|
94
|
+
title: string; // For display, usually from DocumentTemplate.title
|
|
95
|
+
isUserForm: boolean;
|
|
96
|
+
status: FilledDocumentStatus; // Status of the filled form (e.g., draft, completed, signed)
|
|
97
|
+
path: string; // Full Firestore path to the filled document (e.g., appointments/{aid}/user-forms/{fid})
|
|
98
|
+
submittedAt?: Timestamp;
|
|
99
|
+
completedAt?: Timestamp; // When the form reached a final state like 'completed' or 'signed'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Interface for summarized patient review information linked to an appointment.
|
|
104
|
+
*/
|
|
105
|
+
export interface PatientReviewInfo {
|
|
106
|
+
reviewId: string; // ID of the full review document/record if stored elsewhere
|
|
107
|
+
rating: number; // e.g., 1-5 stars
|
|
108
|
+
comment?: string; // A short snippet or the full comment
|
|
109
|
+
reviewedAt: Timestamp;
|
|
110
|
+
}
|
|
111
|
+
|
|
40
112
|
/**
|
|
41
113
|
* Represents a booked appointment, aggregating key information and relevant procedure rules.
|
|
42
114
|
*/
|
|
@@ -64,7 +136,9 @@ export interface Appointment {
|
|
|
64
136
|
/** ID of the procedure */
|
|
65
137
|
procedureId: string;
|
|
66
138
|
/** Aggregated procedure information including product/brand (snapshot) */
|
|
67
|
-
procedureInfo: ProcedureSummaryInfo;
|
|
139
|
+
procedureInfo: ProcedureSummaryInfo; // Aggregated procedure information
|
|
140
|
+
/** Extended procedure information */
|
|
141
|
+
procedureExtendedInfo: ProcedureExtendedInfo; // Aggregated extended procedure information
|
|
68
142
|
|
|
69
143
|
/** Status of the appointment */
|
|
70
144
|
status: AppointmentStatus;
|
|
@@ -72,8 +146,11 @@ export interface Appointment {
|
|
|
72
146
|
/** Timestamps */
|
|
73
147
|
bookingTime: Timestamp;
|
|
74
148
|
confirmationTime?: Timestamp | null;
|
|
149
|
+
cancellationTime?: Timestamp | null;
|
|
150
|
+
rescheduleTime?: Timestamp | null;
|
|
75
151
|
appointmentStartTime: Timestamp;
|
|
76
152
|
appointmentEndTime: Timestamp;
|
|
153
|
+
procedureActualStartTime?: Timestamp | null; // NEW: Actual start time of the procedure
|
|
77
154
|
actualDurationMinutes?: number;
|
|
78
155
|
|
|
79
156
|
/** Cancellation Details */
|
|
@@ -100,20 +177,40 @@ export interface Appointment {
|
|
|
100
177
|
completedPreRequirements?: string[]; // IDs of completed pre-requirements
|
|
101
178
|
completedPostRequirements?: string[]; // IDs of completed post-requirements
|
|
102
179
|
|
|
103
|
-
/**
|
|
180
|
+
/** NEW: Linked forms (consent, procedure-specific forms, etc.) */
|
|
181
|
+
linkedFormIds?: string[];
|
|
182
|
+
linkedForms?: LinkedFormInfo[];
|
|
183
|
+
pendingUserFormsIds?: string[]; // Determines if there are any user forms that are pending for this appointment, blocks the appointment from being checked in (only for user forms with isRequired = true)
|
|
184
|
+
|
|
185
|
+
/** NEW: Media items (before/after photos, scanned documents, etc.) */
|
|
186
|
+
media?: AppointmentMediaItem[];
|
|
187
|
+
|
|
188
|
+
/** NEW: Information about the patient's review for this appointment */
|
|
189
|
+
reviewInfo?: PatientReviewInfo | null;
|
|
190
|
+
|
|
191
|
+
/** NEW: Details about the finalization of the appointment by the practitioner */
|
|
192
|
+
finalizedDetails?: {
|
|
193
|
+
by: string; // Practitioner User ID
|
|
194
|
+
at: Timestamp;
|
|
195
|
+
notes?: string;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Timestamps for record creation and updates */
|
|
104
199
|
createdAt: Timestamp;
|
|
105
200
|
updatedAt: Timestamp;
|
|
106
201
|
|
|
107
202
|
/** Recurring appointment information */
|
|
108
203
|
isRecurring?: boolean;
|
|
109
204
|
recurringAppointmentId?: string | null;
|
|
205
|
+
|
|
206
|
+
/** NEW: Flag for soft deletion or archiving */
|
|
207
|
+
isArchived?: boolean;
|
|
110
208
|
}
|
|
111
209
|
|
|
112
210
|
/**
|
|
113
211
|
* Data needed to create a new Appointment
|
|
114
212
|
*/
|
|
115
213
|
export interface CreateAppointmentData {
|
|
116
|
-
calendarEventId: string;
|
|
117
214
|
clinicBranchId: string;
|
|
118
215
|
practitionerId: string;
|
|
119
216
|
patientId: string;
|
|
@@ -124,7 +221,7 @@ export interface CreateAppointmentData {
|
|
|
124
221
|
currency: Currency;
|
|
125
222
|
patientNotes?: string | null;
|
|
126
223
|
initialStatus: AppointmentStatus;
|
|
127
|
-
initialPaymentStatus?: PaymentStatus;
|
|
224
|
+
initialPaymentStatus?: PaymentStatus; // Defaults to UNPAID if not provided
|
|
128
225
|
}
|
|
129
226
|
|
|
130
227
|
/**
|
|
@@ -132,15 +229,43 @@ export interface CreateAppointmentData {
|
|
|
132
229
|
*/
|
|
133
230
|
export interface UpdateAppointmentData {
|
|
134
231
|
status?: AppointmentStatus;
|
|
135
|
-
confirmationTime?: Timestamp | null;
|
|
232
|
+
confirmationTime?: Timestamp | FieldValue | null;
|
|
233
|
+
cancellationTime?: Timestamp | FieldValue | null;
|
|
234
|
+
rescheduleTime?: Timestamp | FieldValue | null;
|
|
235
|
+
procedureActualStartTime?: Timestamp | FieldValue | null; // NEW
|
|
136
236
|
actualDurationMinutes?: number;
|
|
137
237
|
cancellationReason?: string | null;
|
|
138
238
|
canceledBy?: "patient" | "clinic" | "practitioner" | "system";
|
|
139
239
|
internalNotes?: string | null;
|
|
240
|
+
patientNotes?: string | FieldValue | null; // Allow FieldValue for deleting
|
|
140
241
|
paymentStatus?: PaymentStatus;
|
|
141
|
-
paymentTransactionId?: string | null;
|
|
142
|
-
completedPreRequirements?: string[]; //
|
|
143
|
-
completedPostRequirements?: string[]
|
|
242
|
+
paymentTransactionId?: string | FieldValue | null;
|
|
243
|
+
completedPreRequirements?: string[] | FieldValue; // Allow FieldValue for arrayUnion/arrayRemove
|
|
244
|
+
completedPostRequirements?: string[] | FieldValue;
|
|
245
|
+
appointmentStartTime?: Timestamp; // For rescheduling
|
|
246
|
+
appointmentEndTime?: Timestamp; // For rescheduling
|
|
247
|
+
calendarEventId?: string; // If calendar event needs to be re-linked
|
|
248
|
+
cost?: number; // If cost is adjusted
|
|
249
|
+
clinicBranchId?: string; // If appointment is moved to another branch (complex scenario)
|
|
250
|
+
practitionerId?: string; // If practitioner is changed
|
|
251
|
+
|
|
252
|
+
/** NEW: For updating linked forms - typically managed by dedicated methods */
|
|
253
|
+
linkedFormIds?: string[] | FieldValue;
|
|
254
|
+
linkedForms?: LinkedFormInfo[] | FieldValue;
|
|
255
|
+
|
|
256
|
+
/** NEW: For updating media items - typically managed by dedicated methods */
|
|
257
|
+
media?: AppointmentMediaItem[] | FieldValue;
|
|
258
|
+
|
|
259
|
+
/** NEW: For adding/updating review information */
|
|
260
|
+
reviewInfo?: PatientReviewInfo | FieldValue | null;
|
|
261
|
+
|
|
262
|
+
/** NEW: For setting practitioner finalization details */
|
|
263
|
+
finalizedDetails?: { by: string; at: Timestamp; notes?: string } | FieldValue;
|
|
264
|
+
|
|
265
|
+
/** NEW: For archiving/unarchiving */
|
|
266
|
+
isArchived?: boolean;
|
|
267
|
+
|
|
268
|
+
updatedAt?: FieldValue; // To set server timestamp
|
|
144
269
|
}
|
|
145
270
|
|
|
146
271
|
/**
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export const DOCUMENTATION_TEMPLATES_COLLECTION = "documentation-templates";
|
|
9
9
|
export const FILLED_DOCUMENTS_COLLECTION = "filled-documents";
|
|
10
|
+
export const USER_FORMS_SUBCOLLECTION = "user-forms";
|
|
11
|
+
export const DOCTOR_FORMS_SUBCOLLECTION = "doctor-forms";
|
|
10
12
|
/**
|
|
11
13
|
* Enum for element types in documentation templates
|
|
12
14
|
*/
|
|
@@ -27,6 +29,7 @@ export enum DocumentElementType {
|
|
|
27
29
|
TEXT_INPUT = "text_input",
|
|
28
30
|
DATE_PICKER = "date_picker",
|
|
29
31
|
SIGNATURE = "signature",
|
|
32
|
+
DITIGAL_SIGNATURE = "digital_signature",
|
|
30
33
|
FILE_UPLOAD = "file_upload",
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -60,6 +63,11 @@ export enum DynamicVariable {
|
|
|
60
63
|
PATIENT_BIRTHDAY = "[PATIENT_BIRTHDAY]",
|
|
61
64
|
APPOINTMENT_DATE = "[APPOINTMENT_DATE]",
|
|
62
65
|
CURRENT_DATE = "[CURRENT_DATE]",
|
|
66
|
+
PROCEDURE_NAME = "[PROCEDURE_NAME]",
|
|
67
|
+
PROCEDURE_DESCRIPTION = "[PROCEDURE_DESCRIPTION]",
|
|
68
|
+
PROCEDURE_COST = "[PROCEDURE_COST]",
|
|
69
|
+
PROCEDURE_DURATION = "[PROCEDURE_DURATION]",
|
|
70
|
+
PROCEDURE_RISK = "[PROCEDURE_RISK]",
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
/**
|
|
@@ -174,6 +182,14 @@ export interface SignatureElement extends BaseDocumentElement {
|
|
|
174
182
|
label: string;
|
|
175
183
|
}
|
|
176
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Interface for digital signature element
|
|
187
|
+
*/
|
|
188
|
+
export interface DigitalSignatureElement extends BaseDocumentElement {
|
|
189
|
+
type: DocumentElementType.DITIGAL_SIGNATURE;
|
|
190
|
+
label: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
/**
|
|
178
194
|
* Interface for file upload element
|
|
179
195
|
*/
|
|
@@ -213,6 +229,9 @@ export interface DocumentTemplate {
|
|
|
213
229
|
createdBy: string; // User ID
|
|
214
230
|
elements: DocumentElement[];
|
|
215
231
|
tags?: string[]; // For categorization
|
|
232
|
+
isUserForm?: boolean; // Default is false, and that means it's a doctor form
|
|
233
|
+
isRequired?: boolean; // Default is false, and that means it's an optional form, this is mostly used for user forms
|
|
234
|
+
sortingOrder?: number; // For sorting the forms in the UI, default is 0
|
|
216
235
|
version: number; // For versioning
|
|
217
236
|
isActive: boolean; // Whether the template is active
|
|
218
237
|
}
|
|
@@ -225,6 +244,9 @@ export interface CreateDocumentTemplateData {
|
|
|
225
244
|
description?: string;
|
|
226
245
|
elements: Omit<DocumentElement, "id">[];
|
|
227
246
|
tags?: string[];
|
|
247
|
+
isUserForm?: boolean; // Default is false, and that means it's a doctor form
|
|
248
|
+
isRequired?: boolean; // Default is false, and that means it's an optional form, this is mostly used for user forms
|
|
249
|
+
sortingOrder?: number; // For sorting the forms in the UI, default is 0
|
|
228
250
|
}
|
|
229
251
|
|
|
230
252
|
/**
|
|
@@ -236,6 +258,9 @@ export interface UpdateDocumentTemplateData {
|
|
|
236
258
|
elements?: Omit<DocumentElement, "id">[];
|
|
237
259
|
tags?: string[];
|
|
238
260
|
isActive?: boolean;
|
|
261
|
+
isUserForm?: boolean; // Default is false, and that means it's a doctor form
|
|
262
|
+
isRequired?: boolean; // Default is false, and that means it's an optional form, this is mostly used for user forms
|
|
263
|
+
sortingOrder?: number; // For sorting the forms in the UI, default is 0
|
|
239
264
|
}
|
|
240
265
|
|
|
241
266
|
/**
|
|
@@ -245,6 +270,10 @@ export interface FilledDocument {
|
|
|
245
270
|
id: string;
|
|
246
271
|
templateId: string;
|
|
247
272
|
templateVersion: number;
|
|
273
|
+
isUserForm: boolean;
|
|
274
|
+
isRequired: boolean;
|
|
275
|
+
procedureId: string;
|
|
276
|
+
appointmentId: string;
|
|
248
277
|
patientId: string;
|
|
249
278
|
practitionerId: string;
|
|
250
279
|
clinicId: string;
|
|
@@ -259,6 +288,9 @@ export interface FilledDocument {
|
|
|
259
288
|
*/
|
|
260
289
|
export enum FilledDocumentStatus {
|
|
261
290
|
DRAFT = "draft",
|
|
262
|
-
|
|
263
|
-
|
|
291
|
+
SKIPPED = "skipped",
|
|
292
|
+
PENDING = "pending",
|
|
293
|
+
COMPLETED = "completed", // When doctor or patient completes the form
|
|
294
|
+
SIGNED = "signed", // Only used for user forms
|
|
295
|
+
REJECTED = "rejected", // Only used for user forms
|
|
264
296
|
}
|