@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,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.14.50",
4
+ "version": "1.14.52",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -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
- // Create form document in subcollection
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
- await deleteDoc(formDoc.ref);
183
- removedFormIds.push(formDoc.id);
184
- console.log(
185
- `[FormInit] Removed doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
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
- await deleteDoc(formDoc.ref);
206
- removedFormIds.push(formDoc.id);
207
- console.log(
208
- `[FormInit] Removed user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
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
- return docSnap.data() as FilledDocument;
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((doc) => {
291
- documents.push(doc.data() as FilledDocument);
292
- lastVisible = doc as QueryDocumentSnapshot<FilledDocument>;
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 {