@blackcode_sa/metaestetics-api 1.14.51 → 1.14.53

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/index.js CHANGED
@@ -4843,7 +4843,6 @@ function initializeMetadata(appointment) {
4843
4843
  };
4844
4844
  }
4845
4845
  async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4846
- var _a;
4847
4846
  validateZoneKeyFormat(zoneId);
4848
4847
  const appointment = await getAppointmentOrThrow(db, appointmentId);
4849
4848
  const metadata = initializeMetadata(appointment);
@@ -4852,15 +4851,19 @@ async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4852
4851
  zonesData[zoneId] = [];
4853
4852
  }
4854
4853
  const now = (/* @__PURE__ */ new Date()).toISOString();
4854
+ const cleanItem = Object.fromEntries(
4855
+ Object.entries(item).filter(([_, value]) => value !== void 0)
4856
+ );
4857
+ const notesVisibleToPatientValue = cleanItem.notesVisibleToPatient !== void 0 ? cleanItem.notesVisibleToPatient : cleanItem.notes ? false : void 0;
4855
4858
  const itemWithSubtotal = {
4856
- ...item,
4859
+ ...cleanItem,
4857
4860
  parentZone: zoneId,
4858
4861
  // Set parentZone to the zone key
4859
- subtotal: calculateItemSubtotal(item),
4860
- // Set default visibility to false (privacy-first) if notes exist and visibility not explicitly set
4861
- notesVisibleToPatient: (_a = item.notesVisibleToPatient) != null ? _a : item.notes ? false : void 0,
4862
+ subtotal: calculateItemSubtotal(cleanItem),
4862
4863
  createdAt: now,
4863
- updatedAt: now
4864
+ updatedAt: now,
4865
+ // Only include notesVisibleToPatient if it has a defined boolean value
4866
+ ...typeof notesVisibleToPatientValue === "boolean" && { notesVisibleToPatient: notesVisibleToPatientValue }
4864
4867
  };
4865
4868
  zonesData[zoneId].push(itemWithSubtotal);
4866
4869
  const finalbilling = calculateFinalBilling(zonesData, 0.081);
@@ -4957,6 +4960,45 @@ var import_firestore10 = require("firebase/firestore");
4957
4960
 
4958
4961
  // src/services/appointment/utils/form-initialization.utils.ts
4959
4962
  var import_firestore9 = require("firebase/firestore");
4963
+ function isProcedureSpecificForm(template) {
4964
+ const tags = template.tags || [];
4965
+ const titleLower = template.title.toLowerCase();
4966
+ if (tags.includes("procedure-specific")) {
4967
+ return true;
4968
+ }
4969
+ if (tags.includes("shared")) {
4970
+ return false;
4971
+ }
4972
+ if (tags.some((tag) => {
4973
+ const tagLower = tag.toLowerCase();
4974
+ return tagLower.includes("consent") || tagLower === "consent-form";
4975
+ })) {
4976
+ return true;
4977
+ }
4978
+ if (titleLower.includes("consent")) {
4979
+ return true;
4980
+ }
4981
+ return false;
4982
+ }
4983
+ async function findExistingFormByTemplate(db, appointmentId, templateId, isUserForm) {
4984
+ const formSubcollection = isUserForm ? USER_FORMS_SUBCOLLECTION : DOCTOR_FORMS_SUBCOLLECTION;
4985
+ const appointmentRef = (0, import_firestore9.doc)(db, APPOINTMENTS_COLLECTION, appointmentId);
4986
+ const formsCollectionRef = (0, import_firestore9.collection)(appointmentRef, formSubcollection);
4987
+ const q = (0, import_firestore9.query)(
4988
+ formsCollectionRef,
4989
+ (0, import_firestore9.where)("templateId", "==", templateId)
4990
+ );
4991
+ const querySnapshot = await (0, import_firestore9.getDocs)(q);
4992
+ if (querySnapshot.empty) {
4993
+ return null;
4994
+ }
4995
+ const docSnap = querySnapshot.docs[0];
4996
+ const data = docSnap.data();
4997
+ if (!data.id) {
4998
+ data.id = docSnap.id;
4999
+ }
5000
+ return data;
5001
+ }
4960
5002
  async function initializeFormsForExtendedProcedure(db, appointmentId, procedureId, technologyTemplates, patientId, practitionerId, clinicId) {
4961
5003
  const initializedFormsInfo = [];
4962
5004
  const pendingUserFormsIds = [];
@@ -4999,6 +5041,49 @@ async function initializeFormsForExtendedProcedure(db, appointmentId, procedureI
4999
5041
  const isRequired = templateRef.isRequired;
5000
5042
  const isUserForm = templateRef.isUserForm || false;
5001
5043
  const formSubcollectionPath = isUserForm ? USER_FORMS_SUBCOLLECTION : DOCTOR_FORMS_SUBCOLLECTION;
5044
+ const isProcedureSpecific = isProcedureSpecificForm(template);
5045
+ let existingForm = null;
5046
+ if (!isProcedureSpecific) {
5047
+ try {
5048
+ existingForm = await findExistingFormByTemplate(
5049
+ db,
5050
+ appointmentId,
5051
+ templateRef.templateId,
5052
+ isUserForm
5053
+ );
5054
+ if (existingForm) {
5055
+ console.log(
5056
+ `[FormInit] Found existing shared form ${existingForm.id} (template: ${template.id}) for appointment ${appointmentId}. Reusing instead of creating duplicate.`
5057
+ );
5058
+ const linkedForm = {
5059
+ formId: existingForm.id,
5060
+ templateId: template.id,
5061
+ templateVersion: template.version,
5062
+ title: template.title,
5063
+ isUserForm,
5064
+ isRequired,
5065
+ sortingOrder: templateRef.sortingOrder,
5066
+ status: existingForm.status || "pending" /* PENDING */,
5067
+ path: `${APPOINTMENTS_COLLECTION}/${appointmentId}/${formSubcollectionPath}/${existingForm.id}`
5068
+ };
5069
+ initializedFormsInfo.push(linkedForm);
5070
+ if (!allLinkedFormIds.includes(existingForm.id)) {
5071
+ allLinkedFormIds.push(existingForm.id);
5072
+ }
5073
+ if (isUserForm && isRequired && existingForm.status === "pending" /* PENDING */) {
5074
+ if (!pendingUserFormsIds.includes(existingForm.id)) {
5075
+ pendingUserFormsIds.push(existingForm.id);
5076
+ }
5077
+ }
5078
+ continue;
5079
+ }
5080
+ } catch (error) {
5081
+ console.warn(
5082
+ `[FormInit] Error checking for existing form (template: ${templateRef.templateId}):`,
5083
+ error
5084
+ );
5085
+ }
5086
+ }
5002
5087
  const appointmentRef = (0, import_firestore9.doc)(db, APPOINTMENTS_COLLECTION, appointmentId);
5003
5088
  const formsCollectionRef = (0, import_firestore9.collection)(appointmentRef, formSubcollectionPath);
5004
5089
  const filledDocumentData = {
@@ -5036,8 +5121,9 @@ async function initializeFormsForExtendedProcedure(db, appointmentId, procedureI
5036
5121
  path: docRef.path
5037
5122
  };
5038
5123
  initializedFormsInfo.push(linkedForm);
5124
+ const formType = isProcedureSpecific ? "procedure-specific" : "general/shared";
5039
5125
  console.log(
5040
- `[FormInit] Created FilledDocument ${filledDocumentId} (template: ${template.id}, isUserForm: ${isUserForm}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
5126
+ `[FormInit] Created ${formType} FilledDocument ${filledDocumentId} (template: ${template.id}, isUserForm: ${isUserForm}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
5041
5127
  );
5042
5128
  } catch (error) {
5043
5129
  console.error(
@@ -5049,8 +5135,28 @@ async function initializeFormsForExtendedProcedure(db, appointmentId, procedureI
5049
5135
  return { initializedFormsInfo, pendingUserFormsIds, allLinkedFormIds };
5050
5136
  }
5051
5137
  async function removeFormsForExtendedProcedure(db, appointmentId, procedureId) {
5138
+ var _a, _b;
5052
5139
  const removedFormIds = [];
5053
5140
  const appointmentRef = (0, import_firestore9.doc)(db, APPOINTMENTS_COLLECTION, appointmentId);
5141
+ const appointmentSnap = await (0, import_firestore9.getDoc)(appointmentRef);
5142
+ if (!appointmentSnap.exists()) {
5143
+ console.warn(
5144
+ `[FormInit] Appointment ${appointmentId} not found when removing forms for procedure ${procedureId}.`
5145
+ );
5146
+ return removedFormIds;
5147
+ }
5148
+ const appointment = appointmentSnap.data();
5149
+ const linkedForms = appointment.linkedForms || [];
5150
+ const mainProcedureId = appointment.procedureId;
5151
+ const extendedProcedureIds = ((_b = (_a = appointment.metadata) == null ? void 0 : _a.extendedProcedures) == null ? void 0 : _b.map(
5152
+ (ep) => ep.procedureId
5153
+ )) || [];
5154
+ const allProcedureIds = [mainProcedureId, ...extendedProcedureIds].filter(Boolean);
5155
+ const remainingProcedureIds = allProcedureIds.filter((id) => id !== procedureId);
5156
+ const isFormSharedAndReferenced = (formId) => {
5157
+ const formEntries = linkedForms.filter((form) => form.formId === formId);
5158
+ return formEntries.length > 1;
5159
+ };
5054
5160
  const doctorFormsRef = (0, import_firestore9.collection)(appointmentRef, DOCTOR_FORMS_SUBCOLLECTION);
5055
5161
  const doctorFormsQuery = (0, import_firestore9.query)(
5056
5162
  doctorFormsRef,
@@ -5059,11 +5165,42 @@ async function removeFormsForExtendedProcedure(db, appointmentId, procedureId) {
5059
5165
  const doctorFormsSnap = await (0, import_firestore9.getDocs)(doctorFormsQuery);
5060
5166
  for (const formDoc of doctorFormsSnap.docs) {
5061
5167
  try {
5062
- await (0, import_firestore9.deleteDoc)(formDoc.ref);
5063
- removedFormIds.push(formDoc.id);
5064
- console.log(
5065
- `[FormInit] Removed doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
5066
- );
5168
+ const formData = formDoc.data();
5169
+ let isShared = false;
5170
+ if (formData.templateId) {
5171
+ try {
5172
+ const templateDoc = (0, import_firestore9.doc)(db, DOCUMENTATION_TEMPLATES_COLLECTION, formData.templateId);
5173
+ const templateSnap = await (0, import_firestore9.getDoc)(templateDoc);
5174
+ if (templateSnap.exists()) {
5175
+ const template = templateSnap.data();
5176
+ isShared = !isProcedureSpecificForm(template);
5177
+ }
5178
+ } catch (error) {
5179
+ console.warn(
5180
+ `[FormInit] Could not check template for form ${formDoc.id}, assuming procedure-specific:`,
5181
+ error
5182
+ );
5183
+ }
5184
+ }
5185
+ if (!isShared) {
5186
+ await (0, import_firestore9.deleteDoc)(formDoc.ref);
5187
+ removedFormIds.push(formDoc.id);
5188
+ console.log(
5189
+ `[FormInit] Removed procedure-specific doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
5190
+ );
5191
+ } else {
5192
+ if (isFormSharedAndReferenced(formDoc.id)) {
5193
+ console.log(
5194
+ `[FormInit] Skipped deletion of shared doctor form ${formDoc.id} - still referenced by other procedures.`
5195
+ );
5196
+ } else {
5197
+ await (0, import_firestore9.deleteDoc)(formDoc.ref);
5198
+ removedFormIds.push(formDoc.id);
5199
+ console.log(
5200
+ `[FormInit] Removed shared doctor form ${formDoc.id} - no longer referenced by other procedures.`
5201
+ );
5202
+ }
5203
+ }
5067
5204
  } catch (error) {
5068
5205
  console.error(
5069
5206
  `[FormInit] Error removing doctor form ${formDoc.id}:`,
@@ -5079,11 +5216,42 @@ async function removeFormsForExtendedProcedure(db, appointmentId, procedureId) {
5079
5216
  const userFormsSnap = await (0, import_firestore9.getDocs)(userFormsQuery);
5080
5217
  for (const formDoc of userFormsSnap.docs) {
5081
5218
  try {
5082
- await (0, import_firestore9.deleteDoc)(formDoc.ref);
5083
- removedFormIds.push(formDoc.id);
5084
- console.log(
5085
- `[FormInit] Removed user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
5086
- );
5219
+ const formData = formDoc.data();
5220
+ let isShared = false;
5221
+ if (formData.templateId) {
5222
+ try {
5223
+ const templateDoc = (0, import_firestore9.doc)(db, DOCUMENTATION_TEMPLATES_COLLECTION, formData.templateId);
5224
+ const templateSnap = await (0, import_firestore9.getDoc)(templateDoc);
5225
+ if (templateSnap.exists()) {
5226
+ const template = templateSnap.data();
5227
+ isShared = !isProcedureSpecificForm(template);
5228
+ }
5229
+ } catch (error) {
5230
+ console.warn(
5231
+ `[FormInit] Could not check template for form ${formDoc.id}, assuming procedure-specific:`,
5232
+ error
5233
+ );
5234
+ }
5235
+ }
5236
+ if (!isShared) {
5237
+ await (0, import_firestore9.deleteDoc)(formDoc.ref);
5238
+ removedFormIds.push(formDoc.id);
5239
+ console.log(
5240
+ `[FormInit] Removed procedure-specific user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
5241
+ );
5242
+ } else {
5243
+ if (isFormSharedAndReferenced(formDoc.id)) {
5244
+ console.log(
5245
+ `[FormInit] Skipped deletion of shared user form ${formDoc.id} - still referenced by other procedures.`
5246
+ );
5247
+ } else {
5248
+ await (0, import_firestore9.deleteDoc)(formDoc.ref);
5249
+ removedFormIds.push(formDoc.id);
5250
+ console.log(
5251
+ `[FormInit] Removed shared user form ${formDoc.id} - no longer referenced by other procedures.`
5252
+ );
5253
+ }
5254
+ }
5087
5255
  } catch (error) {
5088
5256
  console.error(
5089
5257
  `[FormInit] Error removing user form ${formDoc.id}:`,
package/dist/index.mjs CHANGED
@@ -4729,7 +4729,6 @@ function initializeMetadata(appointment) {
4729
4729
  };
4730
4730
  }
4731
4731
  async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4732
- var _a;
4733
4732
  validateZoneKeyFormat(zoneId);
4734
4733
  const appointment = await getAppointmentOrThrow(db, appointmentId);
4735
4734
  const metadata = initializeMetadata(appointment);
@@ -4738,15 +4737,19 @@ async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4738
4737
  zonesData[zoneId] = [];
4739
4738
  }
4740
4739
  const now = (/* @__PURE__ */ new Date()).toISOString();
4740
+ const cleanItem = Object.fromEntries(
4741
+ Object.entries(item).filter(([_, value]) => value !== void 0)
4742
+ );
4743
+ const notesVisibleToPatientValue = cleanItem.notesVisibleToPatient !== void 0 ? cleanItem.notesVisibleToPatient : cleanItem.notes ? false : void 0;
4741
4744
  const itemWithSubtotal = {
4742
- ...item,
4745
+ ...cleanItem,
4743
4746
  parentZone: zoneId,
4744
4747
  // Set parentZone to the zone key
4745
- subtotal: calculateItemSubtotal(item),
4746
- // Set default visibility to false (privacy-first) if notes exist and visibility not explicitly set
4747
- notesVisibleToPatient: (_a = item.notesVisibleToPatient) != null ? _a : item.notes ? false : void 0,
4748
+ subtotal: calculateItemSubtotal(cleanItem),
4748
4749
  createdAt: now,
4749
- updatedAt: now
4750
+ updatedAt: now,
4751
+ // Only include notesVisibleToPatient if it has a defined boolean value
4752
+ ...typeof notesVisibleToPatientValue === "boolean" && { notesVisibleToPatient: notesVisibleToPatientValue }
4750
4753
  };
4751
4754
  zonesData[zoneId].push(itemWithSubtotal);
4752
4755
  const finalbilling = calculateFinalBilling(zonesData, 0.081);
@@ -4843,6 +4846,45 @@ import { updateDoc as updateDoc5, serverTimestamp as serverTimestamp4, doc as do
4843
4846
 
4844
4847
  // src/services/appointment/utils/form-initialization.utils.ts
4845
4848
  import { collection as collection5, doc as doc6, addDoc, deleteDoc as deleteDoc2, getDocs as getDocs5, query as query5, where as where5, serverTimestamp as serverTimestamp3, getDoc as getDoc6, updateDoc as updateDoc4 } from "firebase/firestore";
4849
+ function isProcedureSpecificForm(template) {
4850
+ const tags = template.tags || [];
4851
+ const titleLower = template.title.toLowerCase();
4852
+ if (tags.includes("procedure-specific")) {
4853
+ return true;
4854
+ }
4855
+ if (tags.includes("shared")) {
4856
+ return false;
4857
+ }
4858
+ if (tags.some((tag) => {
4859
+ const tagLower = tag.toLowerCase();
4860
+ return tagLower.includes("consent") || tagLower === "consent-form";
4861
+ })) {
4862
+ return true;
4863
+ }
4864
+ if (titleLower.includes("consent")) {
4865
+ return true;
4866
+ }
4867
+ return false;
4868
+ }
4869
+ async function findExistingFormByTemplate(db, appointmentId, templateId, isUserForm) {
4870
+ const formSubcollection = isUserForm ? USER_FORMS_SUBCOLLECTION : DOCTOR_FORMS_SUBCOLLECTION;
4871
+ const appointmentRef = doc6(db, APPOINTMENTS_COLLECTION, appointmentId);
4872
+ const formsCollectionRef = collection5(appointmentRef, formSubcollection);
4873
+ const q = query5(
4874
+ formsCollectionRef,
4875
+ where5("templateId", "==", templateId)
4876
+ );
4877
+ const querySnapshot = await getDocs5(q);
4878
+ if (querySnapshot.empty) {
4879
+ return null;
4880
+ }
4881
+ const docSnap = querySnapshot.docs[0];
4882
+ const data = docSnap.data();
4883
+ if (!data.id) {
4884
+ data.id = docSnap.id;
4885
+ }
4886
+ return data;
4887
+ }
4846
4888
  async function initializeFormsForExtendedProcedure(db, appointmentId, procedureId, technologyTemplates, patientId, practitionerId, clinicId) {
4847
4889
  const initializedFormsInfo = [];
4848
4890
  const pendingUserFormsIds = [];
@@ -4885,6 +4927,49 @@ async function initializeFormsForExtendedProcedure(db, appointmentId, procedureI
4885
4927
  const isRequired = templateRef.isRequired;
4886
4928
  const isUserForm = templateRef.isUserForm || false;
4887
4929
  const formSubcollectionPath = isUserForm ? USER_FORMS_SUBCOLLECTION : DOCTOR_FORMS_SUBCOLLECTION;
4930
+ const isProcedureSpecific = isProcedureSpecificForm(template);
4931
+ let existingForm = null;
4932
+ if (!isProcedureSpecific) {
4933
+ try {
4934
+ existingForm = await findExistingFormByTemplate(
4935
+ db,
4936
+ appointmentId,
4937
+ templateRef.templateId,
4938
+ isUserForm
4939
+ );
4940
+ if (existingForm) {
4941
+ console.log(
4942
+ `[FormInit] Found existing shared form ${existingForm.id} (template: ${template.id}) for appointment ${appointmentId}. Reusing instead of creating duplicate.`
4943
+ );
4944
+ const linkedForm = {
4945
+ formId: existingForm.id,
4946
+ templateId: template.id,
4947
+ templateVersion: template.version,
4948
+ title: template.title,
4949
+ isUserForm,
4950
+ isRequired,
4951
+ sortingOrder: templateRef.sortingOrder,
4952
+ status: existingForm.status || "pending" /* PENDING */,
4953
+ path: `${APPOINTMENTS_COLLECTION}/${appointmentId}/${formSubcollectionPath}/${existingForm.id}`
4954
+ };
4955
+ initializedFormsInfo.push(linkedForm);
4956
+ if (!allLinkedFormIds.includes(existingForm.id)) {
4957
+ allLinkedFormIds.push(existingForm.id);
4958
+ }
4959
+ if (isUserForm && isRequired && existingForm.status === "pending" /* PENDING */) {
4960
+ if (!pendingUserFormsIds.includes(existingForm.id)) {
4961
+ pendingUserFormsIds.push(existingForm.id);
4962
+ }
4963
+ }
4964
+ continue;
4965
+ }
4966
+ } catch (error) {
4967
+ console.warn(
4968
+ `[FormInit] Error checking for existing form (template: ${templateRef.templateId}):`,
4969
+ error
4970
+ );
4971
+ }
4972
+ }
4888
4973
  const appointmentRef = doc6(db, APPOINTMENTS_COLLECTION, appointmentId);
4889
4974
  const formsCollectionRef = collection5(appointmentRef, formSubcollectionPath);
4890
4975
  const filledDocumentData = {
@@ -4922,8 +5007,9 @@ async function initializeFormsForExtendedProcedure(db, appointmentId, procedureI
4922
5007
  path: docRef.path
4923
5008
  };
4924
5009
  initializedFormsInfo.push(linkedForm);
5010
+ const formType = isProcedureSpecific ? "procedure-specific" : "general/shared";
4925
5011
  console.log(
4926
- `[FormInit] Created FilledDocument ${filledDocumentId} (template: ${template.id}, isUserForm: ${isUserForm}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
5012
+ `[FormInit] Created ${formType} FilledDocument ${filledDocumentId} (template: ${template.id}, isUserForm: ${isUserForm}) for extended procedure ${procedureId} in appointment ${appointmentId}.`
4927
5013
  );
4928
5014
  } catch (error) {
4929
5015
  console.error(
@@ -4935,8 +5021,28 @@ async function initializeFormsForExtendedProcedure(db, appointmentId, procedureI
4935
5021
  return { initializedFormsInfo, pendingUserFormsIds, allLinkedFormIds };
4936
5022
  }
4937
5023
  async function removeFormsForExtendedProcedure(db, appointmentId, procedureId) {
5024
+ var _a, _b;
4938
5025
  const removedFormIds = [];
4939
5026
  const appointmentRef = doc6(db, APPOINTMENTS_COLLECTION, appointmentId);
5027
+ const appointmentSnap = await getDoc6(appointmentRef);
5028
+ if (!appointmentSnap.exists()) {
5029
+ console.warn(
5030
+ `[FormInit] Appointment ${appointmentId} not found when removing forms for procedure ${procedureId}.`
5031
+ );
5032
+ return removedFormIds;
5033
+ }
5034
+ const appointment = appointmentSnap.data();
5035
+ const linkedForms = appointment.linkedForms || [];
5036
+ const mainProcedureId = appointment.procedureId;
5037
+ const extendedProcedureIds = ((_b = (_a = appointment.metadata) == null ? void 0 : _a.extendedProcedures) == null ? void 0 : _b.map(
5038
+ (ep) => ep.procedureId
5039
+ )) || [];
5040
+ const allProcedureIds = [mainProcedureId, ...extendedProcedureIds].filter(Boolean);
5041
+ const remainingProcedureIds = allProcedureIds.filter((id) => id !== procedureId);
5042
+ const isFormSharedAndReferenced = (formId) => {
5043
+ const formEntries = linkedForms.filter((form) => form.formId === formId);
5044
+ return formEntries.length > 1;
5045
+ };
4940
5046
  const doctorFormsRef = collection5(appointmentRef, DOCTOR_FORMS_SUBCOLLECTION);
4941
5047
  const doctorFormsQuery = query5(
4942
5048
  doctorFormsRef,
@@ -4945,11 +5051,42 @@ async function removeFormsForExtendedProcedure(db, appointmentId, procedureId) {
4945
5051
  const doctorFormsSnap = await getDocs5(doctorFormsQuery);
4946
5052
  for (const formDoc of doctorFormsSnap.docs) {
4947
5053
  try {
4948
- await deleteDoc2(formDoc.ref);
4949
- removedFormIds.push(formDoc.id);
4950
- console.log(
4951
- `[FormInit] Removed doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
4952
- );
5054
+ const formData = formDoc.data();
5055
+ let isShared = false;
5056
+ if (formData.templateId) {
5057
+ try {
5058
+ const templateDoc = doc6(db, DOCUMENTATION_TEMPLATES_COLLECTION, formData.templateId);
5059
+ const templateSnap = await getDoc6(templateDoc);
5060
+ if (templateSnap.exists()) {
5061
+ const template = templateSnap.data();
5062
+ isShared = !isProcedureSpecificForm(template);
5063
+ }
5064
+ } catch (error) {
5065
+ console.warn(
5066
+ `[FormInit] Could not check template for form ${formDoc.id}, assuming procedure-specific:`,
5067
+ error
5068
+ );
5069
+ }
5070
+ }
5071
+ if (!isShared) {
5072
+ await deleteDoc2(formDoc.ref);
5073
+ removedFormIds.push(formDoc.id);
5074
+ console.log(
5075
+ `[FormInit] Removed procedure-specific doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
5076
+ );
5077
+ } else {
5078
+ if (isFormSharedAndReferenced(formDoc.id)) {
5079
+ console.log(
5080
+ `[FormInit] Skipped deletion of shared doctor form ${formDoc.id} - still referenced by other procedures.`
5081
+ );
5082
+ } else {
5083
+ await deleteDoc2(formDoc.ref);
5084
+ removedFormIds.push(formDoc.id);
5085
+ console.log(
5086
+ `[FormInit] Removed shared doctor form ${formDoc.id} - no longer referenced by other procedures.`
5087
+ );
5088
+ }
5089
+ }
4953
5090
  } catch (error) {
4954
5091
  console.error(
4955
5092
  `[FormInit] Error removing doctor form ${formDoc.id}:`,
@@ -4965,11 +5102,42 @@ async function removeFormsForExtendedProcedure(db, appointmentId, procedureId) {
4965
5102
  const userFormsSnap = await getDocs5(userFormsQuery);
4966
5103
  for (const formDoc of userFormsSnap.docs) {
4967
5104
  try {
4968
- await deleteDoc2(formDoc.ref);
4969
- removedFormIds.push(formDoc.id);
4970
- console.log(
4971
- `[FormInit] Removed user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
4972
- );
5105
+ const formData = formDoc.data();
5106
+ let isShared = false;
5107
+ if (formData.templateId) {
5108
+ try {
5109
+ const templateDoc = doc6(db, DOCUMENTATION_TEMPLATES_COLLECTION, formData.templateId);
5110
+ const templateSnap = await getDoc6(templateDoc);
5111
+ if (templateSnap.exists()) {
5112
+ const template = templateSnap.data();
5113
+ isShared = !isProcedureSpecificForm(template);
5114
+ }
5115
+ } catch (error) {
5116
+ console.warn(
5117
+ `[FormInit] Could not check template for form ${formDoc.id}, assuming procedure-specific:`,
5118
+ error
5119
+ );
5120
+ }
5121
+ }
5122
+ if (!isShared) {
5123
+ await deleteDoc2(formDoc.ref);
5124
+ removedFormIds.push(formDoc.id);
5125
+ console.log(
5126
+ `[FormInit] Removed procedure-specific user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
5127
+ );
5128
+ } else {
5129
+ if (isFormSharedAndReferenced(formDoc.id)) {
5130
+ console.log(
5131
+ `[FormInit] Skipped deletion of shared user form ${formDoc.id} - still referenced by other procedures.`
5132
+ );
5133
+ } else {
5134
+ await deleteDoc2(formDoc.ref);
5135
+ removedFormIds.push(formDoc.id);
5136
+ console.log(
5137
+ `[FormInit] Removed shared user form ${formDoc.id} - no longer referenced by other procedures.`
5138
+ );
5139
+ }
5140
+ }
4973
5141
  } catch (error) {
4974
5142
  console.error(
4975
5143
  `[FormInit] Error removing user form ${formDoc.id}:`,
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.51",
4
+ "version": "1.14.53",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -5,6 +5,7 @@ import {
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
 
@@ -141,8 +291,9 @@ export async function initializeFormsForExtendedProcedure(
141
291
  };
142
292
  initializedFormsInfo.push(linkedForm);
143
293
 
294
+ const formType = isProcedureSpecific ? 'procedure-specific' : 'general/shared';
144
295
  console.log(
145
- `[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}.`
146
297
  );
147
298
  } catch (error) {
148
299
  console.error(
@@ -157,6 +308,7 @@ export async function initializeFormsForExtendedProcedure(
157
308
 
158
309
  /**
159
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.
160
312
  * Removes both user forms and doctor forms
161
313
  * @param db Firestore instance
162
314
  * @param appointmentId Appointment ID
@@ -170,7 +322,34 @@ export async function removeFormsForExtendedProcedure(
170
322
  ): Promise<string[]> {
171
323
  const removedFormIds: string[] = [];
172
324
 
325
+ // Get appointment to check linkedForms
173
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
+ };
174
353
 
175
354
  // Remove from doctor forms subcollection
176
355
  const doctorFormsRef = collection(appointmentRef, DOCTOR_FORMS_SUBCOLLECTION);
@@ -182,11 +361,49 @@ export async function removeFormsForExtendedProcedure(
182
361
 
183
362
  for (const formDoc of doctorFormsSnap.docs) {
184
363
  try {
185
- await deleteDoc(formDoc.ref);
186
- removedFormIds.push(formDoc.id);
187
- console.log(
188
- `[FormInit] Removed doctor form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
189
- );
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
+ }
190
407
  } catch (error) {
191
408
  console.error(
192
409
  `[FormInit] Error removing doctor form ${formDoc.id}:`,
@@ -205,11 +422,49 @@ export async function removeFormsForExtendedProcedure(
205
422
 
206
423
  for (const formDoc of userFormsSnap.docs) {
207
424
  try {
208
- await deleteDoc(formDoc.ref);
209
- removedFormIds.push(formDoc.id);
210
- console.log(
211
- `[FormInit] Removed user form ${formDoc.id} for extended procedure ${procedureId} from appointment ${appointmentId}.`
212
- );
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
+ }
213
468
  } catch (error) {
214
469
  console.error(
215
470
  `[FormInit] Error removing user form ${formDoc.id}:`,
@@ -159,14 +159,25 @@ export async function addItemToZoneUtil(
159
159
 
160
160
  // Calculate subtotal for the item
161
161
  const now = new Date().toISOString();
162
+
163
+ // Filter out undefined values from item (Firestore doesn't allow undefined)
164
+ const cleanItem = Object.fromEntries(
165
+ Object.entries(item).filter(([_, value]) => value !== undefined)
166
+ ) as Omit<ZoneItemData, 'subtotal' | 'parentZone'>;
167
+
168
+ // Determine notesVisibleToPatient value (privacy-first: default to false if notes exist, otherwise omit)
169
+ const notesVisibleToPatientValue = cleanItem.notesVisibleToPatient !== undefined
170
+ ? cleanItem.notesVisibleToPatient
171
+ : (cleanItem.notes ? false : undefined);
172
+
162
173
  const itemWithSubtotal: ZoneItemData = {
163
- ...item,
174
+ ...cleanItem,
164
175
  parentZone: zoneId, // Set parentZone to the zone key
165
- subtotal: calculateItemSubtotal(item),
166
- // Set default visibility to false (privacy-first) if notes exist and visibility not explicitly set
167
- notesVisibleToPatient: item.notesVisibleToPatient ?? (item.notes ? false : undefined),
176
+ subtotal: calculateItemSubtotal(cleanItem),
168
177
  createdAt: now,
169
178
  updatedAt: now,
179
+ // Only include notesVisibleToPatient if it has a defined boolean value
180
+ ...(typeof notesVisibleToPatientValue === 'boolean' && { notesVisibleToPatient: notesVisibleToPatientValue }),
170
181
  };
171
182
 
172
183
  // Add item to zone