@blackcode_sa/metaestetics-api 1.14.50 → 1.14.52
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/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Firestore, collection, doc, addDoc, deleteDoc, getDocs, query, where, serverTimestamp, getDoc } from 'firebase/firestore';
|
|
1
|
+
import { Firestore, collection, doc, addDoc, deleteDoc, getDocs, query, where, serverTimestamp, getDoc, updateDoc } from 'firebase/firestore';
|
|
2
2
|
import {
|
|
3
3
|
DocumentTemplate,
|
|
4
4
|
FilledDocumentStatus,
|
|
5
5
|
DOCTOR_FORMS_SUBCOLLECTION,
|
|
6
6
|
USER_FORMS_SUBCOLLECTION,
|
|
7
7
|
DOCUMENTATION_TEMPLATES_COLLECTION,
|
|
8
|
+
FilledDocument,
|
|
8
9
|
} from '../../../types/documentation-templates';
|
|
9
10
|
import {
|
|
10
11
|
APPOINTMENTS_COLLECTION,
|
|
@@ -18,6 +19,95 @@ export interface InitializeExtendedProcedureFormsResult {
|
|
|
18
19
|
allLinkedFormIds: string[];
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Determines if a form template is procedure-specific or general/shared
|
|
24
|
+
* Uses explicit tags first, then falls back to heuristics
|
|
25
|
+
*
|
|
26
|
+
* Priority order:
|
|
27
|
+
* 1. "procedure-specific" tag → always procedure-specific
|
|
28
|
+
* 2. "shared" tag → always shared
|
|
29
|
+
* 3. Consent-related tags ("consent", "consent-form") → procedure-specific
|
|
30
|
+
* 4. Title contains "consent" → procedure-specific
|
|
31
|
+
* 5. Default → shared (general forms)
|
|
32
|
+
*
|
|
33
|
+
* @param template DocumentTemplate to check
|
|
34
|
+
* @returns true if procedure-specific, false if general/shared
|
|
35
|
+
*/
|
|
36
|
+
function isProcedureSpecificForm(template: DocumentTemplate): boolean {
|
|
37
|
+
const tags = template.tags || [];
|
|
38
|
+
const titleLower = template.title.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// Priority 1: Explicit "procedure-specific" tag → always procedure-specific
|
|
41
|
+
if (tags.includes('procedure-specific')) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Priority 2: Explicit "shared" tag → always shared
|
|
46
|
+
if (tags.includes('shared')) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Priority 3: Consent-related tags → procedure-specific
|
|
51
|
+
if (tags.some(tag => {
|
|
52
|
+
const tagLower = tag.toLowerCase();
|
|
53
|
+
return tagLower.includes('consent') || tagLower === 'consent-form';
|
|
54
|
+
})) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Priority 4: Check title for "consent" → procedure-specific
|
|
59
|
+
if (titleLower.includes('consent')) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Priority 5: Default → shared (general forms)
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if a form with the given templateId already exists in the appointment
|
|
69
|
+
* @param db Firestore instance
|
|
70
|
+
* @param appointmentId Appointment ID
|
|
71
|
+
* @param templateId Template ID to check
|
|
72
|
+
* @param isUserForm Whether to check user-forms or doctor-forms subcollection
|
|
73
|
+
* @returns Existing FilledDocument or null if not found
|
|
74
|
+
*/
|
|
75
|
+
async function findExistingFormByTemplate(
|
|
76
|
+
db: Firestore,
|
|
77
|
+
appointmentId: string,
|
|
78
|
+
templateId: string,
|
|
79
|
+
isUserForm: boolean
|
|
80
|
+
): Promise<FilledDocument | null> {
|
|
81
|
+
const formSubcollection = isUserForm
|
|
82
|
+
? USER_FORMS_SUBCOLLECTION
|
|
83
|
+
: DOCTOR_FORMS_SUBCOLLECTION;
|
|
84
|
+
|
|
85
|
+
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
86
|
+
const formsCollectionRef = collection(appointmentRef, formSubcollection);
|
|
87
|
+
|
|
88
|
+
const q = query(
|
|
89
|
+
formsCollectionRef,
|
|
90
|
+
where('templateId', '==', templateId)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const querySnapshot = await getDocs(q);
|
|
94
|
+
|
|
95
|
+
if (querySnapshot.empty) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Return the first matching form (should only be one for general forms)
|
|
100
|
+
const docSnap = querySnapshot.docs[0];
|
|
101
|
+
const data = docSnap.data() as FilledDocument;
|
|
102
|
+
|
|
103
|
+
// Ensure id is populated from Firestore document ID if not in data
|
|
104
|
+
if (!data.id) {
|
|
105
|
+
data.id = docSnap.id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
|
|
21
111
|
/**
|
|
22
112
|
* Initializes forms for an extended procedure using client-side Firestore
|
|
23
113
|
* Similar to DocumentManagerAdminService but for client-side usage
|
|
@@ -94,7 +184,67 @@ export async function initializeFormsForExtendedProcedure(
|
|
|
94
184
|
? USER_FORMS_SUBCOLLECTION
|
|
95
185
|
: DOCTOR_FORMS_SUBCOLLECTION;
|
|
96
186
|
|
|
97
|
-
//
|
|
187
|
+
// Check if form is procedure-specific or general/shared
|
|
188
|
+
const isProcedureSpecific = isProcedureSpecificForm(template);
|
|
189
|
+
|
|
190
|
+
// For general/shared forms, check if form already exists in appointment
|
|
191
|
+
let existingForm: FilledDocument | null = null;
|
|
192
|
+
if (!isProcedureSpecific) {
|
|
193
|
+
try {
|
|
194
|
+
existingForm = await findExistingFormByTemplate(
|
|
195
|
+
db,
|
|
196
|
+
appointmentId,
|
|
197
|
+
templateRef.templateId,
|
|
198
|
+
isUserForm
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (existingForm) {
|
|
202
|
+
console.log(
|
|
203
|
+
`[FormInit] Found existing shared form ${existingForm.id} (template: ${template.id}) for appointment ${appointmentId}. Reusing instead of creating duplicate.`
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Reuse existing form - add to linkedForms but don't create new document
|
|
207
|
+
// Note: We still add it to allLinkedFormIds to ensure it's tracked,
|
|
208
|
+
// but we don't add it again if it's already in the appointment's linkedForms
|
|
209
|
+
// (This will be handled by the caller merging the arrays)
|
|
210
|
+
|
|
211
|
+
const linkedForm: LinkedFormInfo = {
|
|
212
|
+
formId: existingForm.id,
|
|
213
|
+
templateId: template.id,
|
|
214
|
+
templateVersion: template.version,
|
|
215
|
+
title: template.title,
|
|
216
|
+
isUserForm: isUserForm,
|
|
217
|
+
isRequired: isRequired,
|
|
218
|
+
sortingOrder: templateRef.sortingOrder,
|
|
219
|
+
status: existingForm.status || FilledDocumentStatus.PENDING,
|
|
220
|
+
path: `${APPOINTMENTS_COLLECTION}/${appointmentId}/${formSubcollectionPath}/${existingForm.id}`,
|
|
221
|
+
};
|
|
222
|
+
initializedFormsInfo.push(linkedForm);
|
|
223
|
+
|
|
224
|
+
// Add to allLinkedFormIds if not already present (to avoid duplicates)
|
|
225
|
+
if (!allLinkedFormIds.includes(existingForm.id)) {
|
|
226
|
+
allLinkedFormIds.push(existingForm.id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Add to pendingUserFormsIds if it's a required user form and not already completed
|
|
230
|
+
if (isUserForm && isRequired && existingForm.status === FilledDocumentStatus.PENDING) {
|
|
231
|
+
if (!pendingUserFormsIds.includes(existingForm.id)) {
|
|
232
|
+
pendingUserFormsIds.push(existingForm.id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
continue; // Skip creating new form, reuse existing one
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.warn(
|
|
240
|
+
`[FormInit] Error checking for existing form (template: ${templateRef.templateId}):`,
|
|
241
|
+
error
|
|
242
|
+
);
|
|
243
|
+
// Continue to create new form if check fails
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Create new form document (either procedure-specific or general form not found)
|
|
98
248
|
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
99
249
|
const formsCollectionRef = collection(appointmentRef, formSubcollectionPath);
|
|
100
250
|
|
|
@@ -118,6 +268,9 @@ export async function initializeFormsForExtendedProcedure(
|
|
|
118
268
|
const docRef = await addDoc(formsCollectionRef, filledDocumentData);
|
|
119
269
|
const filledDocumentId = docRef.id;
|
|
120
270
|
|
|
271
|
+
// Update the document to include the id field in the data (for consistency)
|
|
272
|
+
await updateDoc(docRef, { id: filledDocumentId });
|
|
273
|
+
|
|
121
274
|
// Add to pendingUserFormsIds if it's a required user form
|
|
122
275
|
if (isUserForm && isRequired) {
|
|
123
276
|
pendingUserFormsIds.push(filledDocumentId);
|
|
@@ -138,8 +291,9 @@ export async function initializeFormsForExtendedProcedure(
|
|
|
138
291
|
};
|
|
139
292
|
initializedFormsInfo.push(linkedForm);
|
|
140
293
|
|
|
294
|
+
const formType = isProcedureSpecific ? 'procedure-specific' : 'general/shared';
|
|
141
295
|
console.log(
|
|
142
|
-
`[FormInit] Created FilledDocument ${filledDocumentId} (template: ${template.id}, isUserForm: ${isUserForm}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
|
|
296
|
+
`[FormInit] Created ${formType} FilledDocument ${filledDocumentId} (template: ${template.id}, isUserForm: ${isUserForm}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
|
|
143
297
|
);
|
|
144
298
|
} catch (error) {
|
|
145
299
|
console.error(
|
|
@@ -154,6 +308,7 @@ export async function initializeFormsForExtendedProcedure(
|
|
|
154
308
|
|
|
155
309
|
/**
|
|
156
310
|
* Removes all forms associated with a specific procedure from an appointment
|
|
311
|
+
* Only removes procedure-specific forms. Shared forms are kept if still referenced by other procedures.
|
|
157
312
|
* Removes both user forms and doctor forms
|
|
158
313
|
* @param db Firestore instance
|
|
159
314
|
* @param appointmentId Appointment ID
|
|
@@ -167,7 +322,34 @@ export async function removeFormsForExtendedProcedure(
|
|
|
167
322
|
): Promise<string[]> {
|
|
168
323
|
const removedFormIds: string[] = [];
|
|
169
324
|
|
|
325
|
+
// Get appointment to check linkedForms
|
|
170
326
|
const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
327
|
+
const appointmentSnap = await getDoc(appointmentRef);
|
|
328
|
+
|
|
329
|
+
if (!appointmentSnap.exists()) {
|
|
330
|
+
console.warn(
|
|
331
|
+
`[FormInit] Appointment ${appointmentId} not found when removing forms for procedure ${procedureId}.`
|
|
332
|
+
);
|
|
333
|
+
return removedFormIds;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const appointment = appointmentSnap.data() as any;
|
|
337
|
+
const linkedForms = appointment.linkedForms || [];
|
|
338
|
+
|
|
339
|
+
// Get all procedure IDs (main + extended) to check if shared forms are still referenced
|
|
340
|
+
const mainProcedureId = appointment.procedureId;
|
|
341
|
+
const extendedProcedureIds = appointment.metadata?.extendedProcedures?.map(
|
|
342
|
+
(ep: any) => ep.procedureId
|
|
343
|
+
) || [];
|
|
344
|
+
const allProcedureIds = [mainProcedureId, ...extendedProcedureIds].filter(Boolean);
|
|
345
|
+
const remainingProcedureIds = allProcedureIds.filter((id: string) => id !== procedureId);
|
|
346
|
+
|
|
347
|
+
// Helper function to check if form appears multiple times in linkedForms (indicating it's shared)
|
|
348
|
+
const isFormSharedAndReferenced = (formId: string): boolean => {
|
|
349
|
+
const formEntries = linkedForms.filter((form: LinkedFormInfo) => form.formId === formId);
|
|
350
|
+
// If form appears multiple times in linkedForms, it's shared across procedures
|
|
351
|
+
return formEntries.length > 1;
|
|
352
|
+
};
|
|
171
353
|
|
|
172
354
|
// Remove from doctor forms subcollection
|
|
173
355
|
const doctorFormsRef = collection(appointmentRef, DOCTOR_FORMS_SUBCOLLECTION);
|
|
@@ -179,11 +361,49 @@ export async function removeFormsForExtendedProcedure(
|
|
|
179
361
|
|
|
180
362
|
for (const formDoc of doctorFormsSnap.docs) {
|
|
181
363
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
)
|
|
364
|
+
const formData = formDoc.data() as FilledDocument;
|
|
365
|
+
|
|
366
|
+
// Fetch template to check if form is shared
|
|
367
|
+
let isShared = false;
|
|
368
|
+
if (formData.templateId) {
|
|
369
|
+
try {
|
|
370
|
+
const templateDoc = doc(db, DOCUMENTATION_TEMPLATES_COLLECTION, formData.templateId);
|
|
371
|
+
const templateSnap = await getDoc(templateDoc);
|
|
372
|
+
if (templateSnap.exists()) {
|
|
373
|
+
const template = templateSnap.data() as DocumentTemplate;
|
|
374
|
+
isShared = !isProcedureSpecificForm(template);
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.warn(
|
|
378
|
+
`[FormInit] Could not check template for form ${formDoc.id}, assuming procedure-specific:`,
|
|
379
|
+
error
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Only delete procedure-specific forms
|
|
385
|
+
// Shared forms are kept even if procedureId matches, as they may be used by other procedures
|
|
386
|
+
if (!isShared) {
|
|
387
|
+
await deleteDoc(formDoc.ref);
|
|
388
|
+
removedFormIds.push(formDoc.id);
|
|
389
|
+
console.log(
|
|
390
|
+
`[FormInit] Removed procedure-specific doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
|
|
391
|
+
);
|
|
392
|
+
} else {
|
|
393
|
+
// Check if form is still referenced by other procedures (appears multiple times in linkedForms)
|
|
394
|
+
if (isFormSharedAndReferenced(formDoc.id)) {
|
|
395
|
+
console.log(
|
|
396
|
+
`[FormInit] Skipped deletion of shared doctor form ${formDoc.id} - still referenced by other procedures.`
|
|
397
|
+
);
|
|
398
|
+
} else {
|
|
399
|
+
// Shared form but only referenced by this procedure (shouldn't happen, but handle it)
|
|
400
|
+
await deleteDoc(formDoc.ref);
|
|
401
|
+
removedFormIds.push(formDoc.id);
|
|
402
|
+
console.log(
|
|
403
|
+
`[FormInit] Removed shared doctor form ${formDoc.id} - no longer referenced by other procedures.`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
187
407
|
} catch (error) {
|
|
188
408
|
console.error(
|
|
189
409
|
`[FormInit] Error removing doctor form ${formDoc.id}:`,
|
|
@@ -202,11 +422,49 @@ export async function removeFormsForExtendedProcedure(
|
|
|
202
422
|
|
|
203
423
|
for (const formDoc of userFormsSnap.docs) {
|
|
204
424
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)
|
|
425
|
+
const formData = formDoc.data() as FilledDocument;
|
|
426
|
+
|
|
427
|
+
// Fetch template to check if form is shared
|
|
428
|
+
let isShared = false;
|
|
429
|
+
if (formData.templateId) {
|
|
430
|
+
try {
|
|
431
|
+
const templateDoc = doc(db, DOCUMENTATION_TEMPLATES_COLLECTION, formData.templateId);
|
|
432
|
+
const templateSnap = await getDoc(templateDoc);
|
|
433
|
+
if (templateSnap.exists()) {
|
|
434
|
+
const template = templateSnap.data() as DocumentTemplate;
|
|
435
|
+
isShared = !isProcedureSpecificForm(template);
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.warn(
|
|
439
|
+
`[FormInit] Could not check template for form ${formDoc.id}, assuming procedure-specific:`,
|
|
440
|
+
error
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Only delete procedure-specific forms
|
|
446
|
+
// Shared forms are kept even if procedureId matches, as they may be used by other procedures
|
|
447
|
+
if (!isShared) {
|
|
448
|
+
await deleteDoc(formDoc.ref);
|
|
449
|
+
removedFormIds.push(formDoc.id);
|
|
450
|
+
console.log(
|
|
451
|
+
`[FormInit] Removed procedure-specific user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
|
|
452
|
+
);
|
|
453
|
+
} else {
|
|
454
|
+
// Check if form is still referenced by other procedures (appears multiple times in linkedForms)
|
|
455
|
+
if (isFormSharedAndReferenced(formDoc.id)) {
|
|
456
|
+
console.log(
|
|
457
|
+
`[FormInit] Skipped deletion of shared user form ${formDoc.id} - still referenced by other procedures.`
|
|
458
|
+
);
|
|
459
|
+
} else {
|
|
460
|
+
// Shared form but only referenced by this procedure (shouldn't happen, but handle it)
|
|
461
|
+
await deleteDoc(formDoc.ref);
|
|
462
|
+
removedFormIds.push(formDoc.id);
|
|
463
|
+
console.log(
|
|
464
|
+
`[FormInit] Removed shared user form ${formDoc.id} - no longer referenced by other procedures.`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
210
468
|
} catch (error) {
|
|
211
469
|
console.error(
|
|
212
470
|
`[FormInit] Error removing user form ${formDoc.id}:`,
|
|
@@ -144,7 +144,12 @@ export class FilledDocumentService extends BaseService {
|
|
|
144
144
|
if (!docSnap.exists()) {
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
const data = docSnap.data() as FilledDocument;
|
|
148
|
+
// Ensure id is populated from Firestore document ID if not in data
|
|
149
|
+
if (!data.id) {
|
|
150
|
+
data.id = docSnap.id;
|
|
151
|
+
}
|
|
152
|
+
return data;
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
/**
|
|
@@ -287,9 +292,14 @@ export class FilledDocumentService extends BaseService {
|
|
|
287
292
|
const documents: FilledDocument[] = [];
|
|
288
293
|
let lastVisible: QueryDocumentSnapshot<FilledDocument> | null = null;
|
|
289
294
|
|
|
290
|
-
querySnapshot.forEach((
|
|
291
|
-
|
|
292
|
-
|
|
295
|
+
querySnapshot.forEach((docSnapshot) => {
|
|
296
|
+
const data = docSnapshot.data() as FilledDocument;
|
|
297
|
+
// Ensure id is populated from Firestore document ID if not in data
|
|
298
|
+
if (!data.id) {
|
|
299
|
+
data.id = docSnapshot.id;
|
|
300
|
+
}
|
|
301
|
+
documents.push(data);
|
|
302
|
+
lastVisible = docSnapshot as QueryDocumentSnapshot<FilledDocument>;
|
|
293
303
|
});
|
|
294
304
|
|
|
295
305
|
return {
|