@blackcode_sa/metaestetics-api 1.12.50 → 1.12.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/dist/index.js CHANGED
@@ -16558,7 +16558,7 @@ var ProcedureService = class extends BaseService {
16558
16558
  rating: ((_a = practitioner.reviewInfo) == null ? void 0 : _a.averageRating) || 0,
16559
16559
  services: practitioner.procedures || []
16560
16560
  };
16561
- const { productsMetadata: _, ...validatedDataWithoutProductsMetadata } = validatedData;
16561
+ const { productsMetadata: _, productId: __, ...validatedDataWithoutProductsMetadata } = validatedData;
16562
16562
  const newProcedure = {
16563
16563
  id: procedureId,
16564
16564
  ...validatedDataWithoutProductsMetadata,
@@ -16707,7 +16707,7 @@ var ProcedureService = class extends BaseService {
16707
16707
  const procedureId = this.generateId();
16708
16708
  createdProcedureIds.push(procedureId);
16709
16709
  const procedureRef = (0, import_firestore55.doc)(this.db, PROCEDURES_COLLECTION, procedureId);
16710
- const { productsMetadata: _, ...validatedDataWithoutProductsMetadata } = validatedData;
16710
+ const { productsMetadata: _, productId: __, ...validatedDataWithoutProductsMetadata } = validatedData;
16711
16711
  const newProcedure = {
16712
16712
  id: procedureId,
16713
16713
  ...validatedDataWithoutProductsMetadata,
@@ -18277,6 +18277,73 @@ var BrandService = class extends BaseService {
18277
18277
  ...docSnap.data()
18278
18278
  };
18279
18279
  }
18280
+ /**
18281
+ * Exports brands to CSV string, suitable for Excel/Sheets.
18282
+ * Includes headers and optional UTF-8 BOM.
18283
+ * By default exports only active brands (set includeInactive to true to export all).
18284
+ */
18285
+ async exportToCsv(options) {
18286
+ var _a, _b;
18287
+ const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
18288
+ const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
18289
+ const headers = [
18290
+ "id",
18291
+ "name",
18292
+ "manufacturer",
18293
+ "website",
18294
+ "description",
18295
+ "isActive"
18296
+ ];
18297
+ const rows = [];
18298
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
18299
+ const PAGE_SIZE = 1e3;
18300
+ let cursor;
18301
+ const baseConstraints = [];
18302
+ if (!includeInactive) {
18303
+ baseConstraints.push((0, import_firestore58.where)("isActive", "==", true));
18304
+ }
18305
+ baseConstraints.push((0, import_firestore58.orderBy)("name_lowercase"));
18306
+ while (true) {
18307
+ const constraints = [...baseConstraints, (0, import_firestore58.limit)(PAGE_SIZE)];
18308
+ if (cursor) constraints.push((0, import_firestore58.startAfter)(cursor));
18309
+ const q = (0, import_firestore58.query)(this.getBrandsRef(), ...constraints);
18310
+ const snapshot = await (0, import_firestore58.getDocs)(q);
18311
+ if (snapshot.empty) break;
18312
+ for (const d of snapshot.docs) {
18313
+ const brand = { id: d.id, ...d.data() };
18314
+ rows.push(this.brandToCsvRow(brand));
18315
+ }
18316
+ cursor = snapshot.docs[snapshot.docs.length - 1];
18317
+ if (snapshot.size < PAGE_SIZE) break;
18318
+ }
18319
+ const csvBody = rows.join("\r\n");
18320
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
18321
+ }
18322
+ brandToCsvRow(brand) {
18323
+ var _a, _b, _c, _d, _e, _f;
18324
+ const values = [
18325
+ (_a = brand.id) != null ? _a : "",
18326
+ (_b = brand.name) != null ? _b : "",
18327
+ (_c = brand.manufacturer) != null ? _c : "",
18328
+ (_d = brand.website) != null ? _d : "",
18329
+ (_e = brand.description) != null ? _e : "",
18330
+ String((_f = brand.isActive) != null ? _f : "")
18331
+ ];
18332
+ return values.map((v) => this.formatCsvValue(v)).join(",");
18333
+ }
18334
+ formatDateIso(value) {
18335
+ if (value instanceof Date) return value.toISOString();
18336
+ if (value && typeof value.toDate === "function") {
18337
+ const d = value.toDate();
18338
+ return d instanceof Date ? d.toISOString() : String(value);
18339
+ }
18340
+ return String(value != null ? value : "");
18341
+ }
18342
+ formatCsvValue(value) {
18343
+ const str = value === null || value === void 0 ? "" : String(value);
18344
+ const escaped = str.replace(/"/g, '""');
18345
+ return `"${escaped}"`;
18346
+ }
18280
18347
  };
18281
18348
 
18282
18349
  // src/backoffice/services/category.service.ts
@@ -18455,6 +18522,71 @@ var CategoryService = class extends BaseService {
18455
18522
  ...docSnap.data()
18456
18523
  };
18457
18524
  }
18525
+ /**
18526
+ * Exports categories to CSV string, suitable for Excel/Sheets.
18527
+ * Includes headers and optional UTF-8 BOM.
18528
+ * By default exports only active categories (set includeInactive to true to export all).
18529
+ */
18530
+ async exportToCsv(options) {
18531
+ var _a, _b;
18532
+ const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
18533
+ const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
18534
+ const headers = [
18535
+ "id",
18536
+ "name",
18537
+ "description",
18538
+ "family",
18539
+ "isActive"
18540
+ ];
18541
+ const rows = [];
18542
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
18543
+ const PAGE_SIZE = 1e3;
18544
+ let cursor;
18545
+ const constraints = [];
18546
+ if (!includeInactive) {
18547
+ constraints.push((0, import_firestore59.where)("isActive", "==", true));
18548
+ }
18549
+ constraints.push((0, import_firestore59.orderBy)("name"));
18550
+ while (true) {
18551
+ const queryConstraints = [...constraints, (0, import_firestore59.limit)(PAGE_SIZE)];
18552
+ if (cursor) queryConstraints.push((0, import_firestore59.startAfter)(cursor));
18553
+ const q = (0, import_firestore59.query)(this.categoriesRef, ...queryConstraints);
18554
+ const snapshot = await (0, import_firestore59.getDocs)(q);
18555
+ if (snapshot.empty) break;
18556
+ for (const d of snapshot.docs) {
18557
+ const category = { id: d.id, ...d.data() };
18558
+ rows.push(this.categoryToCsvRow(category));
18559
+ }
18560
+ cursor = snapshot.docs[snapshot.docs.length - 1];
18561
+ if (snapshot.size < PAGE_SIZE) break;
18562
+ }
18563
+ const csvBody = rows.join("\r\n");
18564
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
18565
+ }
18566
+ categoryToCsvRow(category) {
18567
+ var _a, _b, _c, _d, _e;
18568
+ const values = [
18569
+ (_a = category.id) != null ? _a : "",
18570
+ (_b = category.name) != null ? _b : "",
18571
+ (_c = category.description) != null ? _c : "",
18572
+ (_d = category.family) != null ? _d : "",
18573
+ String((_e = category.isActive) != null ? _e : "")
18574
+ ];
18575
+ return values.map((v) => this.formatCsvValue(v)).join(",");
18576
+ }
18577
+ formatDateIso(value) {
18578
+ if (value instanceof Date) return value.toISOString();
18579
+ if (value && typeof value.toDate === "function") {
18580
+ const d = value.toDate();
18581
+ return d instanceof Date ? d.toISOString() : String(value);
18582
+ }
18583
+ return String(value != null ? value : "");
18584
+ }
18585
+ formatCsvValue(value) {
18586
+ const str = value === null || value === void 0 ? "" : String(value);
18587
+ const escaped = str.replace(/"/g, '""');
18588
+ return `"${escaped}"`;
18589
+ }
18458
18590
  };
18459
18591
 
18460
18592
  // src/backoffice/services/subcategory.service.ts
@@ -18682,10 +18814,83 @@ var SubcategoryService = class extends BaseService {
18682
18814
  ...docSnap.data()
18683
18815
  };
18684
18816
  }
18817
+ /**
18818
+ * Exports subcategories to CSV string, suitable for Excel/Sheets.
18819
+ * Includes headers and optional UTF-8 BOM.
18820
+ * By default exports only active subcategories (set includeInactive to true to export all).
18821
+ */
18822
+ async exportToCsv(options) {
18823
+ var _a, _b;
18824
+ const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
18825
+ const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
18826
+ const headers = [
18827
+ "id",
18828
+ "name",
18829
+ "categoryId",
18830
+ "description",
18831
+ "isActive"
18832
+ ];
18833
+ const rows = [];
18834
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
18835
+ const PAGE_SIZE = 1e3;
18836
+ let cursor;
18837
+ const constraints = [];
18838
+ if (!includeInactive) {
18839
+ constraints.push((0, import_firestore60.where)("isActive", "==", true));
18840
+ }
18841
+ constraints.push((0, import_firestore60.orderBy)("name"));
18842
+ while (true) {
18843
+ const queryConstraints = [...constraints, (0, import_firestore60.limit)(PAGE_SIZE)];
18844
+ if (cursor) queryConstraints.push((0, import_firestore60.startAfter)(cursor));
18845
+ const q = (0, import_firestore60.query)(
18846
+ (0, import_firestore60.collectionGroup)(this.db, SUBCATEGORIES_COLLECTION),
18847
+ ...queryConstraints
18848
+ );
18849
+ const snapshot = await (0, import_firestore60.getDocs)(q);
18850
+ if (snapshot.empty) break;
18851
+ for (const d of snapshot.docs) {
18852
+ const subcategory = { id: d.id, ...d.data() };
18853
+ rows.push(this.subcategoryToCsvRow(subcategory));
18854
+ }
18855
+ cursor = snapshot.docs[snapshot.docs.length - 1];
18856
+ if (snapshot.size < PAGE_SIZE) break;
18857
+ }
18858
+ const csvBody = rows.join("\r\n");
18859
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
18860
+ }
18861
+ subcategoryToCsvRow(subcategory) {
18862
+ var _a, _b, _c, _d, _e;
18863
+ const values = [
18864
+ (_a = subcategory.id) != null ? _a : "",
18865
+ (_b = subcategory.name) != null ? _b : "",
18866
+ (_c = subcategory.categoryId) != null ? _c : "",
18867
+ (_d = subcategory.description) != null ? _d : "",
18868
+ String((_e = subcategory.isActive) != null ? _e : "")
18869
+ ];
18870
+ return values.map((v) => this.formatCsvValue(v)).join(",");
18871
+ }
18872
+ formatDateIso(value) {
18873
+ if (value instanceof Date) return value.toISOString();
18874
+ if (value && typeof value.toDate === "function") {
18875
+ const d = value.toDate();
18876
+ return d instanceof Date ? d.toISOString() : String(value);
18877
+ }
18878
+ return String(value != null ? value : "");
18879
+ }
18880
+ formatCsvValue(value) {
18881
+ const str = value === null || value === void 0 ? "" : String(value);
18882
+ const escaped = str.replace(/"/g, '""');
18883
+ return `"${escaped}"`;
18884
+ }
18685
18885
  };
18686
18886
 
18687
18887
  // src/backoffice/services/technology.service.ts
18688
18888
  var import_firestore61 = require("firebase/firestore");
18889
+
18890
+ // src/backoffice/types/product.types.ts
18891
+ var PRODUCTS_COLLECTION = "products";
18892
+
18893
+ // src/backoffice/services/technology.service.ts
18689
18894
  var DEFAULT_CERTIFICATION_REQUIREMENT = {
18690
18895
  minimumLevel: "aesthetician" /* AESTHETICIAN */,
18691
18896
  requiredSpecialties: []
@@ -18847,7 +19052,18 @@ var TechnologyService = class extends BaseService {
18847
19052
  });
18848
19053
  updateData.updatedAt = /* @__PURE__ */ new Date();
18849
19054
  const docRef = (0, import_firestore61.doc)(this.technologiesRef, id);
19055
+ const beforeTech = await this.getById(id);
18850
19056
  await (0, import_firestore61.updateDoc)(docRef, updateData);
19057
+ const categoryChanged = beforeTech && updateData.categoryId && beforeTech.categoryId !== updateData.categoryId;
19058
+ const subcategoryChanged = beforeTech && updateData.subcategoryId && beforeTech.subcategoryId !== updateData.subcategoryId;
19059
+ const nameChanged = beforeTech && updateData.name && beforeTech.name !== updateData.name;
19060
+ if (categoryChanged || subcategoryChanged || nameChanged) {
19061
+ await this.updateProductsInSubcollection(id, {
19062
+ categoryId: updateData.categoryId,
19063
+ subcategoryId: updateData.subcategoryId,
19064
+ technologyName: updateData.name
19065
+ });
19066
+ }
18851
19067
  return this.getById(id);
18852
19068
  }
18853
19069
  /**
@@ -19283,18 +19499,239 @@ var TechnologyService = class extends BaseService {
19283
19499
  })
19284
19500
  );
19285
19501
  }
19502
+ // ==========================================
19503
+ // NEW METHODS: Product assignment management
19504
+ // ==========================================
19505
+ /**
19506
+ * Assigns multiple products to a technology
19507
+ * Updates each product's assignedTechnologyIds array
19508
+ */
19509
+ async assignProducts(technologyId, productIds) {
19510
+ const batch = (0, import_firestore61.writeBatch)(this.db);
19511
+ for (const productId of productIds) {
19512
+ const productRef = (0, import_firestore61.doc)(this.db, PRODUCTS_COLLECTION, productId);
19513
+ batch.update(productRef, {
19514
+ assignedTechnologyIds: (0, import_firestore61.arrayUnion)(technologyId),
19515
+ updatedAt: /* @__PURE__ */ new Date()
19516
+ });
19517
+ }
19518
+ await batch.commit();
19519
+ }
19520
+ /**
19521
+ * Unassigns multiple products from a technology
19522
+ * Updates each product's assignedTechnologyIds array
19523
+ */
19524
+ async unassignProducts(technologyId, productIds) {
19525
+ const batch = (0, import_firestore61.writeBatch)(this.db);
19526
+ for (const productId of productIds) {
19527
+ const productRef = (0, import_firestore61.doc)(this.db, PRODUCTS_COLLECTION, productId);
19528
+ batch.update(productRef, {
19529
+ assignedTechnologyIds: (0, import_firestore61.arrayRemove)(technologyId),
19530
+ updatedAt: /* @__PURE__ */ new Date()
19531
+ });
19532
+ }
19533
+ await batch.commit();
19534
+ }
19535
+ /**
19536
+ * Gets products assigned to a specific technology
19537
+ * Reads from top-level collection for immediate consistency (Cloud Functions may lag)
19538
+ */
19539
+ async getAssignedProducts(technologyId) {
19540
+ const q = (0, import_firestore61.query)(
19541
+ (0, import_firestore61.collection)(this.db, PRODUCTS_COLLECTION),
19542
+ (0, import_firestore61.where)("assignedTechnologyIds", "array-contains", technologyId),
19543
+ (0, import_firestore61.where)("isActive", "==", true),
19544
+ (0, import_firestore61.orderBy)("name")
19545
+ );
19546
+ const snapshot = await (0, import_firestore61.getDocs)(q);
19547
+ return snapshot.docs.map(
19548
+ (doc44) => ({
19549
+ id: doc44.id,
19550
+ ...doc44.data()
19551
+ })
19552
+ );
19553
+ }
19554
+ /**
19555
+ * Gets products NOT assigned to a specific technology
19556
+ */
19557
+ async getUnassignedProducts(technologyId) {
19558
+ const q = (0, import_firestore61.query)(
19559
+ (0, import_firestore61.collection)(this.db, PRODUCTS_COLLECTION),
19560
+ (0, import_firestore61.where)("isActive", "==", true),
19561
+ (0, import_firestore61.orderBy)("name")
19562
+ );
19563
+ const snapshot = await (0, import_firestore61.getDocs)(q);
19564
+ const allProducts = snapshot.docs.map(
19565
+ (doc44) => ({
19566
+ id: doc44.id,
19567
+ ...doc44.data()
19568
+ })
19569
+ );
19570
+ return allProducts.filter(
19571
+ (product) => {
19572
+ var _a;
19573
+ return !((_a = product.assignedTechnologyIds) == null ? void 0 : _a.includes(technologyId));
19574
+ }
19575
+ );
19576
+ }
19577
+ /**
19578
+ * Gets product assignment statistics for a technology
19579
+ */
19580
+ async getProductStats(technologyId) {
19581
+ const products = await this.getAssignedProducts(technologyId);
19582
+ const byBrand = {};
19583
+ products.forEach((product) => {
19584
+ byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
19585
+ });
19586
+ return {
19587
+ totalAssigned: products.length,
19588
+ byBrand
19589
+ };
19590
+ }
19591
+ /**
19592
+ * Updates products in technology subcollection when technology metadata changes
19593
+ * @param technologyId - ID of the technology
19594
+ * @param updates - Fields to update (categoryId, subcategoryId, technologyName)
19595
+ */
19596
+ async updateProductsInSubcollection(technologyId, updates) {
19597
+ const productsRef = (0, import_firestore61.collection)(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
19598
+ const productsSnapshot = await (0, import_firestore61.getDocs)(productsRef);
19599
+ if (productsSnapshot.empty) {
19600
+ return;
19601
+ }
19602
+ const batch = (0, import_firestore61.writeBatch)(this.db);
19603
+ for (const productDoc of productsSnapshot.docs) {
19604
+ const productRef = productDoc.ref;
19605
+ const updateFields = {};
19606
+ if (updates.categoryId !== void 0) {
19607
+ updateFields.categoryId = updates.categoryId;
19608
+ }
19609
+ if (updates.subcategoryId !== void 0) {
19610
+ updateFields.subcategoryId = updates.subcategoryId;
19611
+ }
19612
+ if (updates.technologyName !== void 0) {
19613
+ updateFields.technologyName = updates.technologyName;
19614
+ }
19615
+ if (Object.keys(updateFields).length > 0) {
19616
+ batch.update(productRef, updateFields);
19617
+ }
19618
+ }
19619
+ await batch.commit();
19620
+ }
19621
+ /**
19622
+ * Exports technologies to CSV string, suitable for Excel/Sheets.
19623
+ * Includes headers and optional UTF-8 BOM.
19624
+ * By default exports only active technologies (set includeInactive to true to export all).
19625
+ * Includes product names from subcollections.
19626
+ */
19627
+ async exportToCsv(options) {
19628
+ var _a, _b;
19629
+ const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
19630
+ const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
19631
+ const headers = [
19632
+ "id",
19633
+ "name",
19634
+ "description",
19635
+ "family",
19636
+ "categoryId",
19637
+ "subcategoryId",
19638
+ "technicalDetails",
19639
+ "requirements_pre",
19640
+ "requirements_post",
19641
+ "blockingConditions",
19642
+ "contraindications",
19643
+ "benefits",
19644
+ "certificationMinimumLevel",
19645
+ "certificationRequiredSpecialties",
19646
+ "documentationTemplateIds",
19647
+ "productNames",
19648
+ "isActive"
19649
+ ];
19650
+ const rows = [];
19651
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
19652
+ const PAGE_SIZE = 1e3;
19653
+ let cursor;
19654
+ const constraints = [];
19655
+ if (!includeInactive) {
19656
+ constraints.push((0, import_firestore61.where)("isActive", "==", true));
19657
+ }
19658
+ constraints.push((0, import_firestore61.orderBy)("name"));
19659
+ while (true) {
19660
+ const queryConstraints = [...constraints, (0, import_firestore61.limit)(PAGE_SIZE)];
19661
+ if (cursor) queryConstraints.push((0, import_firestore61.startAfter)(cursor));
19662
+ const q = (0, import_firestore61.query)(this.technologiesRef, ...queryConstraints);
19663
+ const snapshot = await (0, import_firestore61.getDocs)(q);
19664
+ if (snapshot.empty) break;
19665
+ for (const d of snapshot.docs) {
19666
+ const technology = { id: d.id, ...d.data() };
19667
+ const productNames = await this.getProductNamesForTechnology(technology.id);
19668
+ rows.push(this.technologyToCsvRow(technology, productNames));
19669
+ }
19670
+ cursor = snapshot.docs[snapshot.docs.length - 1];
19671
+ if (snapshot.size < PAGE_SIZE) break;
19672
+ }
19673
+ const csvBody = rows.join("\r\n");
19674
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
19675
+ }
19676
+ /**
19677
+ * Gets product names from the technology's product subcollection
19678
+ */
19679
+ async getProductNamesForTechnology(technologyId) {
19680
+ try {
19681
+ const productsRef = (0, import_firestore61.collection)(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
19682
+ const q = (0, import_firestore61.query)(productsRef, (0, import_firestore61.where)("isActive", "==", true));
19683
+ const snapshot = await (0, import_firestore61.getDocs)(q);
19684
+ return snapshot.docs.map((doc44) => {
19685
+ const product = doc44.data();
19686
+ return product.name || "";
19687
+ }).filter((name) => name);
19688
+ } catch (error) {
19689
+ console.error(`Error fetching products for technology ${technologyId}:`, error);
19690
+ return [];
19691
+ }
19692
+ }
19693
+ technologyToCsvRow(technology, productNames = []) {
19694
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A;
19695
+ const values = [
19696
+ (_a = technology.id) != null ? _a : "",
19697
+ (_b = technology.name) != null ? _b : "",
19698
+ (_c = technology.description) != null ? _c : "",
19699
+ (_d = technology.family) != null ? _d : "",
19700
+ (_e = technology.categoryId) != null ? _e : "",
19701
+ (_f = technology.subcategoryId) != null ? _f : "",
19702
+ (_g = technology.technicalDetails) != null ? _g : "",
19703
+ (_j = (_i = (_h = technology.requirements) == null ? void 0 : _h.pre) == null ? void 0 : _i.map((r) => r.name).join(";")) != null ? _j : "",
19704
+ (_m = (_l = (_k = technology.requirements) == null ? void 0 : _k.post) == null ? void 0 : _l.map((r) => r.name).join(";")) != null ? _m : "",
19705
+ (_o = (_n = technology.blockingConditions) == null ? void 0 : _n.join(";")) != null ? _o : "",
19706
+ (_q = (_p = technology.contraindications) == null ? void 0 : _p.map((c) => c.name).join(";")) != null ? _q : "",
19707
+ (_s = (_r = technology.benefits) == null ? void 0 : _r.map((b) => b.name).join(";")) != null ? _s : "",
19708
+ (_u = (_t = technology.certificationRequirement) == null ? void 0 : _t.minimumLevel) != null ? _u : "",
19709
+ (_x = (_w = (_v = technology.certificationRequirement) == null ? void 0 : _v.requiredSpecialties) == null ? void 0 : _w.join(";")) != null ? _x : "",
19710
+ (_z = (_y = technology.documentationTemplates) == null ? void 0 : _y.map((t) => t.templateId).join(";")) != null ? _z : "",
19711
+ productNames.join(";"),
19712
+ String((_A = technology.isActive) != null ? _A : "")
19713
+ ];
19714
+ return values.map((v) => this.formatCsvValue(v)).join(",");
19715
+ }
19716
+ formatCsvValue(value) {
19717
+ const str = value === null || value === void 0 ? "" : String(value);
19718
+ const escaped = str.replace(/"/g, '""');
19719
+ return `"${escaped}"`;
19720
+ }
19286
19721
  };
19287
19722
 
19288
19723
  // src/backoffice/services/product.service.ts
19289
19724
  var import_firestore62 = require("firebase/firestore");
19290
-
19291
- // src/backoffice/types/product.types.ts
19292
- var PRODUCTS_COLLECTION = "products";
19293
-
19294
- // src/backoffice/services/product.service.ts
19295
19725
  var ProductService = class extends BaseService {
19296
19726
  /**
19297
- * Gets reference to products collection under a technology
19727
+ * Gets reference to top-level products collection (source of truth)
19728
+ * @returns Firestore collection reference
19729
+ */
19730
+ getTopLevelProductsRef() {
19731
+ return (0, import_firestore62.collection)(this.db, PRODUCTS_COLLECTION);
19732
+ }
19733
+ /**
19734
+ * Gets reference to products collection under a technology (backward compatibility)
19298
19735
  * @param technologyId - ID of the technology
19299
19736
  * @returns Firestore collection reference
19300
19737
  */
@@ -19310,6 +19747,7 @@ var ProductService = class extends BaseService {
19310
19747
  ...product,
19311
19748
  brandId,
19312
19749
  technologyId,
19750
+ // Required for old subcollection structure
19313
19751
  createdAt: now,
19314
19752
  updatedAt: now,
19315
19753
  isActive: true
@@ -19369,30 +19807,26 @@ var ProductService = class extends BaseService {
19369
19807
  }
19370
19808
  /**
19371
19809
  * Gets counts of active products grouped by category, subcategory, and technology.
19372
- * This uses a single collectionGroup query for efficiency.
19810
+ * Queries technology subcollections which have the legacy fields synced by Cloud Functions.
19373
19811
  */
19374
19812
  async getProductCounts() {
19375
- const q = (0, import_firestore62.query)((0, import_firestore62.collectionGroup)(this.db, PRODUCTS_COLLECTION), (0, import_firestore62.where)("isActive", "==", true));
19376
- const snapshot = await (0, import_firestore62.getDocs)(q);
19377
19813
  const counts = {
19378
19814
  byCategory: {},
19379
19815
  bySubcategory: {},
19380
19816
  byTechnology: {}
19381
19817
  };
19382
- if (snapshot.empty) {
19383
- return counts;
19384
- }
19818
+ const q = (0, import_firestore62.query)((0, import_firestore62.collectionGroup)(this.db, PRODUCTS_COLLECTION), (0, import_firestore62.where)("isActive", "==", true));
19819
+ const snapshot = await (0, import_firestore62.getDocs)(q);
19385
19820
  snapshot.docs.forEach((doc44) => {
19386
19821
  const product = doc44.data();
19387
- const { categoryId, subcategoryId, technologyId } = product;
19388
- if (categoryId) {
19389
- counts.byCategory[categoryId] = (counts.byCategory[categoryId] || 0) + 1;
19822
+ if (product.categoryId) {
19823
+ counts.byCategory[product.categoryId] = (counts.byCategory[product.categoryId] || 0) + 1;
19390
19824
  }
19391
- if (subcategoryId) {
19392
- counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
19825
+ if (product.subcategoryId) {
19826
+ counts.bySubcategory[product.subcategoryId] = (counts.bySubcategory[product.subcategoryId] || 0) + 1;
19393
19827
  }
19394
- if (technologyId) {
19395
- counts.byTechnology[technologyId] = (counts.byTechnology[technologyId] || 0) + 1;
19828
+ if (product.technologyId) {
19829
+ counts.byTechnology[product.technologyId] = (counts.byTechnology[product.technologyId] || 0) + 1;
19396
19830
  }
19397
19831
  });
19398
19832
  return counts;
@@ -19471,6 +19905,241 @@ var ProductService = class extends BaseService {
19471
19905
  ...docSnap.data()
19472
19906
  };
19473
19907
  }
19908
+ // ==========================================
19909
+ // NEW METHODS: Top-level collection (preferred)
19910
+ // ==========================================
19911
+ /**
19912
+ * Creates a new product in the top-level collection
19913
+ */
19914
+ async createTopLevel(brandId, product, technologyIds = []) {
19915
+ const now = /* @__PURE__ */ new Date();
19916
+ const newProduct = {
19917
+ ...product,
19918
+ brandId,
19919
+ assignedTechnologyIds: technologyIds,
19920
+ createdAt: now,
19921
+ updatedAt: now,
19922
+ isActive: true
19923
+ };
19924
+ const productRef = await (0, import_firestore62.addDoc)(this.getTopLevelProductsRef(), newProduct);
19925
+ return { id: productRef.id, ...newProduct };
19926
+ }
19927
+ /**
19928
+ * Gets all products from the top-level collection
19929
+ */
19930
+ async getAllTopLevel(options) {
19931
+ const { rowsPerPage, lastVisible, brandId } = options;
19932
+ const constraints = [(0, import_firestore62.where)("isActive", "==", true), (0, import_firestore62.orderBy)("name")];
19933
+ if (brandId) {
19934
+ constraints.push((0, import_firestore62.where)("brandId", "==", brandId));
19935
+ }
19936
+ if (lastVisible) {
19937
+ constraints.push((0, import_firestore62.startAfter)(lastVisible));
19938
+ }
19939
+ constraints.push((0, import_firestore62.limit)(rowsPerPage));
19940
+ const q = (0, import_firestore62.query)(this.getTopLevelProductsRef(), ...constraints);
19941
+ const snapshot = await (0, import_firestore62.getDocs)(q);
19942
+ const products = snapshot.docs.map(
19943
+ (doc44) => ({
19944
+ id: doc44.id,
19945
+ ...doc44.data()
19946
+ })
19947
+ );
19948
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
19949
+ return { products, lastVisible: newLastVisible };
19950
+ }
19951
+ /**
19952
+ * Gets a product by ID from the top-level collection
19953
+ */
19954
+ async getByIdTopLevel(productId) {
19955
+ const docRef = (0, import_firestore62.doc)(this.getTopLevelProductsRef(), productId);
19956
+ const docSnap = await (0, import_firestore62.getDoc)(docRef);
19957
+ if (!docSnap.exists()) return null;
19958
+ return {
19959
+ id: docSnap.id,
19960
+ ...docSnap.data()
19961
+ };
19962
+ }
19963
+ /**
19964
+ * Updates a product in the top-level collection
19965
+ */
19966
+ async updateTopLevel(productId, product) {
19967
+ const updateData = {
19968
+ ...product,
19969
+ updatedAt: /* @__PURE__ */ new Date()
19970
+ };
19971
+ const docRef = (0, import_firestore62.doc)(this.getTopLevelProductsRef(), productId);
19972
+ await (0, import_firestore62.updateDoc)(docRef, updateData);
19973
+ return this.getByIdTopLevel(productId);
19974
+ }
19975
+ /**
19976
+ * Deletes a product from the top-level collection (soft delete)
19977
+ */
19978
+ async deleteTopLevel(productId) {
19979
+ await this.updateTopLevel(productId, {
19980
+ isActive: false
19981
+ });
19982
+ }
19983
+ /**
19984
+ * Assigns a product to a technology
19985
+ */
19986
+ async assignToTechnology(productId, technologyId) {
19987
+ const docRef = (0, import_firestore62.doc)(this.getTopLevelProductsRef(), productId);
19988
+ await (0, import_firestore62.updateDoc)(docRef, {
19989
+ assignedTechnologyIds: (0, import_firestore62.arrayUnion)(technologyId),
19990
+ updatedAt: /* @__PURE__ */ new Date()
19991
+ });
19992
+ }
19993
+ /**
19994
+ * Unassigns a product from a technology
19995
+ */
19996
+ async unassignFromTechnology(productId, technologyId) {
19997
+ const docRef = (0, import_firestore62.doc)(this.getTopLevelProductsRef(), productId);
19998
+ await (0, import_firestore62.updateDoc)(docRef, {
19999
+ assignedTechnologyIds: (0, import_firestore62.arrayRemove)(technologyId),
20000
+ updatedAt: /* @__PURE__ */ new Date()
20001
+ });
20002
+ }
20003
+ /**
20004
+ * Gets products assigned to a specific technology
20005
+ */
20006
+ async getAssignedProducts(technologyId) {
20007
+ const q = (0, import_firestore62.query)(
20008
+ this.getTopLevelProductsRef(),
20009
+ (0, import_firestore62.where)("assignedTechnologyIds", "array-contains", technologyId),
20010
+ (0, import_firestore62.where)("isActive", "==", true),
20011
+ (0, import_firestore62.orderBy)("name")
20012
+ );
20013
+ const snapshot = await (0, import_firestore62.getDocs)(q);
20014
+ return snapshot.docs.map(
20015
+ (doc44) => ({
20016
+ id: doc44.id,
20017
+ ...doc44.data()
20018
+ })
20019
+ );
20020
+ }
20021
+ /**
20022
+ * Gets products NOT assigned to a specific technology
20023
+ */
20024
+ async getUnassignedProducts(technologyId) {
20025
+ const q = (0, import_firestore62.query)(
20026
+ this.getTopLevelProductsRef(),
20027
+ (0, import_firestore62.where)("isActive", "==", true),
20028
+ (0, import_firestore62.orderBy)("name")
20029
+ );
20030
+ const snapshot = await (0, import_firestore62.getDocs)(q);
20031
+ const allProducts = snapshot.docs.map(
20032
+ (doc44) => ({
20033
+ id: doc44.id,
20034
+ ...doc44.data()
20035
+ })
20036
+ );
20037
+ return allProducts.filter(
20038
+ (product) => {
20039
+ var _a;
20040
+ return !((_a = product.assignedTechnologyIds) == null ? void 0 : _a.includes(technologyId));
20041
+ }
20042
+ );
20043
+ }
20044
+ /**
20045
+ * Gets all products for a brand (from top-level collection)
20046
+ */
20047
+ async getByBrand(brandId) {
20048
+ const q = (0, import_firestore62.query)(
20049
+ this.getTopLevelProductsRef(),
20050
+ (0, import_firestore62.where)("brandId", "==", brandId),
20051
+ (0, import_firestore62.where)("isActive", "==", true),
20052
+ (0, import_firestore62.orderBy)("name")
20053
+ );
20054
+ const snapshot = await (0, import_firestore62.getDocs)(q);
20055
+ return snapshot.docs.map(
20056
+ (doc44) => ({
20057
+ id: doc44.id,
20058
+ ...doc44.data()
20059
+ })
20060
+ );
20061
+ }
20062
+ /**
20063
+ * Exports products to CSV string, suitable for Excel/Sheets.
20064
+ * Includes headers and optional UTF-8 BOM.
20065
+ * By default exports only active products (set includeInactive to true to export all).
20066
+ */
20067
+ async exportToCsv(options) {
20068
+ var _a, _b;
20069
+ const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
20070
+ const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
20071
+ const headers = [
20072
+ "id",
20073
+ "name",
20074
+ "brandId",
20075
+ "brandName",
20076
+ "assignedTechnologyIds",
20077
+ "description",
20078
+ "technicalDetails",
20079
+ "dosage",
20080
+ "composition",
20081
+ "indications",
20082
+ "contraindications",
20083
+ "warnings",
20084
+ "isActive"
20085
+ ];
20086
+ const rows = [];
20087
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
20088
+ const PAGE_SIZE = 1e3;
20089
+ let cursor;
20090
+ const constraints = [];
20091
+ if (!includeInactive) {
20092
+ constraints.push((0, import_firestore62.where)("isActive", "==", true));
20093
+ }
20094
+ constraints.push((0, import_firestore62.orderBy)("name"));
20095
+ while (true) {
20096
+ const queryConstraints = [...constraints, (0, import_firestore62.limit)(PAGE_SIZE)];
20097
+ if (cursor) queryConstraints.push((0, import_firestore62.startAfter)(cursor));
20098
+ const q = (0, import_firestore62.query)(this.getTopLevelProductsRef(), ...queryConstraints);
20099
+ const snapshot = await (0, import_firestore62.getDocs)(q);
20100
+ if (snapshot.empty) break;
20101
+ for (const d of snapshot.docs) {
20102
+ const product = { id: d.id, ...d.data() };
20103
+ rows.push(this.productToCsvRow(product));
20104
+ }
20105
+ cursor = snapshot.docs[snapshot.docs.length - 1];
20106
+ if (snapshot.size < PAGE_SIZE) break;
20107
+ }
20108
+ const csvBody = rows.join("\r\n");
20109
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
20110
+ }
20111
+ productToCsvRow(product) {
20112
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
20113
+ const values = [
20114
+ (_a = product.id) != null ? _a : "",
20115
+ (_b = product.name) != null ? _b : "",
20116
+ (_c = product.brandId) != null ? _c : "",
20117
+ (_d = product.brandName) != null ? _d : "",
20118
+ (_f = (_e = product.assignedTechnologyIds) == null ? void 0 : _e.join(";")) != null ? _f : "",
20119
+ (_g = product.description) != null ? _g : "",
20120
+ (_h = product.technicalDetails) != null ? _h : "",
20121
+ (_i = product.dosage) != null ? _i : "",
20122
+ (_j = product.composition) != null ? _j : "",
20123
+ (_l = (_k = product.indications) == null ? void 0 : _k.join(";")) != null ? _l : "",
20124
+ (_n = (_m = product.contraindications) == null ? void 0 : _m.map((c) => c.name).join(";")) != null ? _n : "",
20125
+ (_p = (_o = product.warnings) == null ? void 0 : _o.join(";")) != null ? _p : "",
20126
+ String((_q = product.isActive) != null ? _q : "")
20127
+ ];
20128
+ return values.map((v) => this.formatCsvValue(v)).join(",");
20129
+ }
20130
+ formatDateIso(value) {
20131
+ if (value instanceof Date) return value.toISOString();
20132
+ if (value && typeof value.toDate === "function") {
20133
+ const d = value.toDate();
20134
+ return d instanceof Date ? d.toISOString() : String(value);
20135
+ }
20136
+ return String(value != null ? value : "");
20137
+ }
20138
+ formatCsvValue(value) {
20139
+ const str = value === null || value === void 0 ? "" : String(value);
20140
+ const escaped = str.replace(/"/g, '""');
20141
+ return `"${escaped}"`;
20142
+ }
19474
20143
  };
19475
20144
 
19476
20145
  // src/backoffice/services/constants.service.ts
@@ -19703,6 +20372,66 @@ var ConstantsService = class extends BaseService {
19703
20372
  contraindications: (0, import_firestore63.arrayRemove)(toRemove)
19704
20373
  });
19705
20374
  }
20375
+ // =================================================================
20376
+ // CSV Export Methods
20377
+ // =================================================================
20378
+ /**
20379
+ * Exports treatment benefits to CSV string, suitable for Excel/Sheets.
20380
+ * Includes headers and optional UTF-8 BOM.
20381
+ */
20382
+ async exportBenefitsToCsv(options) {
20383
+ var _a;
20384
+ const includeBom = (_a = options == null ? void 0 : options.includeBom) != null ? _a : true;
20385
+ const headers = ["id", "name", "description"];
20386
+ const rows = [];
20387
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
20388
+ const benefits = await this.getAllBenefitsForFilter();
20389
+ for (const benefit of benefits) {
20390
+ rows.push(this.benefitToCsvRow(benefit));
20391
+ }
20392
+ const csvBody = rows.join("\r\n");
20393
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
20394
+ }
20395
+ /**
20396
+ * Exports contraindications to CSV string, suitable for Excel/Sheets.
20397
+ * Includes headers and optional UTF-8 BOM.
20398
+ */
20399
+ async exportContraindicationsToCsv(options) {
20400
+ var _a;
20401
+ const includeBom = (_a = options == null ? void 0 : options.includeBom) != null ? _a : true;
20402
+ const headers = ["id", "name", "description"];
20403
+ const rows = [];
20404
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
20405
+ const contraindications = await this.getAllContraindicationsForFilter();
20406
+ for (const contraindication of contraindications) {
20407
+ rows.push(this.contraindicationToCsvRow(contraindication));
20408
+ }
20409
+ const csvBody = rows.join("\r\n");
20410
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
20411
+ }
20412
+ benefitToCsvRow(benefit) {
20413
+ var _a, _b, _c;
20414
+ const values = [
20415
+ (_a = benefit.id) != null ? _a : "",
20416
+ (_b = benefit.name) != null ? _b : "",
20417
+ (_c = benefit.description) != null ? _c : ""
20418
+ ];
20419
+ return values.map((v) => this.formatCsvValue(v)).join(",");
20420
+ }
20421
+ contraindicationToCsvRow(contraindication) {
20422
+ var _a, _b, _c;
20423
+ const values = [
20424
+ (_a = contraindication.id) != null ? _a : "",
20425
+ (_b = contraindication.name) != null ? _b : "",
20426
+ (_c = contraindication.description) != null ? _c : ""
20427
+ ];
20428
+ return values.map((v) => this.formatCsvValue(v)).join(",");
20429
+ }
20430
+ formatCsvValue(value) {
20431
+ const str = value === null || value === void 0 ? "" : String(value);
20432
+ const escaped = str.replace(/"/g, '""');
20433
+ return `"${escaped}"`;
20434
+ }
19706
20435
  };
19707
20436
 
19708
20437
  // src/backoffice/types/static/contraindication.types.ts