@blackcode_sa/metaestetics-api 1.12.42 → 1.12.43

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.
@@ -305,4 +305,81 @@ export class ConstantsService extends BaseService {
305
305
  contraindications: arrayRemove(toRemove),
306
306
  });
307
307
  }
308
+
309
+ // =================================================================
310
+ // CSV Export Methods
311
+ // =================================================================
312
+
313
+ /**
314
+ * Exports treatment benefits to CSV string, suitable for Excel/Sheets.
315
+ * Includes headers and optional UTF-8 BOM.
316
+ */
317
+ async exportBenefitsToCsv(options?: {
318
+ includeBom?: boolean;
319
+ }): Promise<string> {
320
+ const includeBom = options?.includeBom ?? true;
321
+
322
+ const headers = ["id", "name", "description"];
323
+
324
+ const rows: string[] = [];
325
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
326
+
327
+ const benefits = await this.getAllBenefitsForFilter();
328
+
329
+ for (const benefit of benefits) {
330
+ rows.push(this.benefitToCsvRow(benefit));
331
+ }
332
+
333
+ const csvBody = rows.join("\r\n");
334
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
335
+ }
336
+
337
+ /**
338
+ * Exports contraindications to CSV string, suitable for Excel/Sheets.
339
+ * Includes headers and optional UTF-8 BOM.
340
+ */
341
+ async exportContraindicationsToCsv(options?: {
342
+ includeBom?: boolean;
343
+ }): Promise<string> {
344
+ const includeBom = options?.includeBom ?? true;
345
+
346
+ const headers = ["id", "name", "description"];
347
+
348
+ const rows: string[] = [];
349
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
350
+
351
+ const contraindications = await this.getAllContraindicationsForFilter();
352
+
353
+ for (const contraindication of contraindications) {
354
+ rows.push(this.contraindicationToCsvRow(contraindication));
355
+ }
356
+
357
+ const csvBody = rows.join("\r\n");
358
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
359
+ }
360
+
361
+ private benefitToCsvRow(benefit: TreatmentBenefitDynamic): string {
362
+ const values = [
363
+ benefit.id ?? "",
364
+ benefit.name ?? "",
365
+ benefit.description ?? "",
366
+ ];
367
+ return values.map((v) => this.formatCsvValue(v)).join(",");
368
+ }
369
+
370
+ private contraindicationToCsvRow(contraindication: ContraindicationDynamic): string {
371
+ const values = [
372
+ contraindication.id ?? "",
373
+ contraindication.name ?? "",
374
+ contraindication.description ?? "",
375
+ ];
376
+ return values.map((v) => this.formatCsvValue(v)).join(",");
377
+ }
378
+
379
+ private formatCsvValue(value: any): string {
380
+ const str = value === null || value === undefined ? "" : String(value);
381
+ // Escape double quotes by doubling them and wrap in quotes
382
+ const escaped = str.replace(/"/g, '""');
383
+ return `"${escaped}"`;
384
+ }
308
385
  }
@@ -450,4 +450,104 @@ export class ProductService extends BaseService implements IProductService {
450
450
  } as Product),
451
451
  );
452
452
  }
453
+
454
+ /**
455
+ * Exports products to CSV string, suitable for Excel/Sheets.
456
+ * Includes headers and optional UTF-8 BOM.
457
+ * By default exports only active products (set includeInactive to true to export all).
458
+ */
459
+ async exportToCsv(options?: {
460
+ includeInactive?: boolean;
461
+ includeBom?: boolean;
462
+ }): Promise<string> {
463
+ const includeInactive = options?.includeInactive ?? false;
464
+ const includeBom = options?.includeBom ?? true;
465
+
466
+ const headers = [
467
+ "id",
468
+ "name",
469
+ "brandId",
470
+ "brandName",
471
+ "assignedTechnologyIds",
472
+ "description",
473
+ "technicalDetails",
474
+ "dosage",
475
+ "composition",
476
+ "indications",
477
+ "contraindications",
478
+ "warnings",
479
+ "isActive",
480
+ ];
481
+
482
+ const rows: string[] = [];
483
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
484
+
485
+ const PAGE_SIZE = 1000;
486
+ let cursor: any | undefined;
487
+
488
+ // Build base constraints
489
+ const constraints: QueryConstraint[] = [];
490
+ if (!includeInactive) {
491
+ constraints.push(where("isActive", "==", true));
492
+ }
493
+ constraints.push(orderBy("name"));
494
+
495
+ // Page through all results from top-level collection
496
+ // eslint-disable-next-line no-constant-condition
497
+ while (true) {
498
+ const queryConstraints: QueryConstraint[] = [...constraints, limit(PAGE_SIZE)];
499
+ if (cursor) queryConstraints.push(startAfter(cursor));
500
+
501
+ const q = query(this.getTopLevelProductsRef(), ...queryConstraints);
502
+ const snapshot = await getDocs(q);
503
+ if (snapshot.empty) break;
504
+
505
+ for (const d of snapshot.docs) {
506
+ const product = ({ id: d.id, ...d.data() } as unknown) as Product;
507
+ rows.push(this.productToCsvRow(product));
508
+ }
509
+
510
+ cursor = snapshot.docs[snapshot.docs.length - 1];
511
+ if (snapshot.size < PAGE_SIZE) break;
512
+ }
513
+
514
+ const csvBody = rows.join("\r\n");
515
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
516
+ }
517
+
518
+ private productToCsvRow(product: Product): string {
519
+ const values = [
520
+ product.id ?? "",
521
+ product.name ?? "",
522
+ product.brandId ?? "",
523
+ product.brandName ?? "",
524
+ product.assignedTechnologyIds?.join(";") ?? "",
525
+ product.description ?? "",
526
+ product.technicalDetails ?? "",
527
+ product.dosage ?? "",
528
+ product.composition ?? "",
529
+ product.indications?.join(";") ?? "",
530
+ product.contraindications?.map(c => c.name).join(";") ?? "",
531
+ product.warnings?.join(";") ?? "",
532
+ String(product.isActive ?? ""),
533
+ ];
534
+ return values.map((v) => this.formatCsvValue(v)).join(",");
535
+ }
536
+
537
+ private formatDateIso(value: any): string {
538
+ // Firestore timestamps may come back as Date or Timestamp; handle both
539
+ if (value instanceof Date) return value.toISOString();
540
+ if (value && typeof value.toDate === "function") {
541
+ const d = value.toDate();
542
+ return d instanceof Date ? d.toISOString() : String(value);
543
+ }
544
+ return String(value ?? "");
545
+ }
546
+
547
+ private formatCsvValue(value: any): string {
548
+ const str = value === null || value === undefined ? "" : String(value);
549
+ // Escape double quotes by doubling them and wrap in quotes
550
+ const escaped = str.replace(/"/g, '""');
551
+ return `"${escaped}"`;
552
+ }
453
553
  }
@@ -7,6 +7,7 @@ import {
7
7
  query,
8
8
  updateDoc,
9
9
  where,
10
+ orderBy,
10
11
  } from "firebase/firestore";
11
12
  import {
12
13
  Requirement,
@@ -156,4 +157,79 @@ export class RequirementService extends BaseService {
156
157
  ...docSnap.data(),
157
158
  } as Requirement;
158
159
  }
160
+
161
+ /**
162
+ * Exports requirements to CSV string, suitable for Excel/Sheets.
163
+ * Includes headers and optional UTF-8 BOM.
164
+ * By default exports only active requirements (set includeInactive to true to export all).
165
+ */
166
+ async exportToCsv(options?: {
167
+ includeInactive?: boolean;
168
+ includeBom?: boolean;
169
+ }): Promise<string> {
170
+ const includeInactive = options?.includeInactive ?? false;
171
+ const includeBom = options?.includeBom ?? true;
172
+
173
+ const headers = [
174
+ "id",
175
+ "type",
176
+ "name",
177
+ "description",
178
+ "timeframe_duration",
179
+ "timeframe_unit",
180
+ "timeframe_notifyAt",
181
+ "importance",
182
+ "isActive",
183
+ ];
184
+
185
+ const rows: string[] = [];
186
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
187
+
188
+ // Get all requirements (they're not paginated in the original service)
189
+ const q = includeInactive
190
+ ? query(this.requirementsRef, orderBy("name"))
191
+ : query(this.requirementsRef, where("isActive", "==", true), orderBy("name"));
192
+
193
+ const snapshot = await getDocs(q);
194
+
195
+ for (const d of snapshot.docs) {
196
+ const requirement = ({ id: d.id, ...d.data() } as unknown) as Requirement;
197
+ rows.push(this.requirementToCsvRow(requirement));
198
+ }
199
+
200
+ const csvBody = rows.join("\r\n");
201
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
202
+ }
203
+
204
+ private requirementToCsvRow(requirement: Requirement): string {
205
+ const values = [
206
+ requirement.id ?? "",
207
+ requirement.type ?? "",
208
+ requirement.name ?? "",
209
+ requirement.description ?? "",
210
+ requirement.timeframe?.duration?.toString() ?? "",
211
+ requirement.timeframe?.unit ?? "",
212
+ requirement.timeframe?.notifyAt?.join(";") ?? "",
213
+ requirement.importance ?? "",
214
+ String(requirement.isActive ?? ""),
215
+ ];
216
+ return values.map((v) => this.formatCsvValue(v)).join(",");
217
+ }
218
+
219
+ private formatDateIso(value: any): string {
220
+ // Firestore timestamps may come back as Date or Timestamp; handle both
221
+ if (value instanceof Date) return value.toISOString();
222
+ if (value && typeof value.toDate === "function") {
223
+ const d = value.toDate();
224
+ return d instanceof Date ? d.toISOString() : String(value);
225
+ }
226
+ return String(value ?? "");
227
+ }
228
+
229
+ private formatCsvValue(value: any): string {
230
+ const str = value === null || value === undefined ? "" : String(value);
231
+ // Escape double quotes by doubling them and wrap in quotes
232
+ const escaped = str.replace(/"/g, '""');
233
+ return `"${escaped}"`;
234
+ }
159
235
  }
@@ -305,4 +305,91 @@ export class SubcategoryService extends BaseService {
305
305
  ...docSnap.data(),
306
306
  } as Subcategory;
307
307
  }
308
+
309
+ /**
310
+ * Exports subcategories to CSV string, suitable for Excel/Sheets.
311
+ * Includes headers and optional UTF-8 BOM.
312
+ * By default exports only active subcategories (set includeInactive to true to export all).
313
+ */
314
+ async exportToCsv(options?: {
315
+ includeInactive?: boolean;
316
+ includeBom?: boolean;
317
+ }): Promise<string> {
318
+ const includeInactive = options?.includeInactive ?? false;
319
+ const includeBom = options?.includeBom ?? true;
320
+
321
+ const headers = [
322
+ "id",
323
+ "name",
324
+ "categoryId",
325
+ "description",
326
+ "isActive",
327
+ ];
328
+
329
+ const rows: string[] = [];
330
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
331
+
332
+ const PAGE_SIZE = 1000;
333
+ let cursor: any | undefined;
334
+
335
+ // Build base constraints
336
+ const constraints: any[] = [];
337
+ if (!includeInactive) {
338
+ constraints.push(where("isActive", "==", true));
339
+ }
340
+ constraints.push(orderBy("name"));
341
+
342
+ // Page through all results using collectionGroup
343
+ // eslint-disable-next-line no-constant-condition
344
+ while (true) {
345
+ const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
346
+ if (cursor) queryConstraints.push(startAfter(cursor));
347
+
348
+ const q = query(
349
+ collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
350
+ ...queryConstraints
351
+ );
352
+ const snapshot = await getDocs(q);
353
+ if (snapshot.empty) break;
354
+
355
+ for (const d of snapshot.docs) {
356
+ const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
357
+ rows.push(this.subcategoryToCsvRow(subcategory));
358
+ }
359
+
360
+ cursor = snapshot.docs[snapshot.docs.length - 1];
361
+ if (snapshot.size < PAGE_SIZE) break;
362
+ }
363
+
364
+ const csvBody = rows.join("\r\n");
365
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
366
+ }
367
+
368
+ private subcategoryToCsvRow(subcategory: Subcategory): string {
369
+ const values = [
370
+ subcategory.id ?? "",
371
+ subcategory.name ?? "",
372
+ subcategory.categoryId ?? "",
373
+ subcategory.description ?? "",
374
+ String(subcategory.isActive ?? ""),
375
+ ];
376
+ return values.map((v) => this.formatCsvValue(v)).join(",");
377
+ }
378
+
379
+ private formatDateIso(value: any): string {
380
+ // Firestore timestamps may come back as Date or Timestamp; handle both
381
+ if (value instanceof Date) return value.toISOString();
382
+ if (value && typeof value.toDate === "function") {
383
+ const d = value.toDate();
384
+ return d instanceof Date ? d.toISOString() : String(value);
385
+ }
386
+ return String(value ?? "");
387
+ }
388
+
389
+ private formatCsvValue(value: any): string {
390
+ const str = value === null || value === undefined ? "" : String(value);
391
+ // Escape double quotes by doubling them and wrap in quotes
392
+ const escaped = str.replace(/"/g, '""');
393
+ return `"${escaped}"`;
394
+ }
308
395
  }
@@ -16,6 +16,7 @@ import {
16
16
  arrayRemove,
17
17
  Firestore,
18
18
  writeBatch,
19
+ QueryConstraint,
19
20
  } from 'firebase/firestore';
20
21
  import { Technology, TECHNOLOGIES_COLLECTION, ITechnologyService } from '../types/technology.types';
21
22
  import { Requirement, RequirementType } from '../types/requirement.types';
@@ -947,4 +948,123 @@ export class TechnologyService extends BaseService implements ITechnologyService
947
948
 
948
949
  await batch.commit();
949
950
  }
951
+
952
+ /**
953
+ * Exports technologies to CSV string, suitable for Excel/Sheets.
954
+ * Includes headers and optional UTF-8 BOM.
955
+ * By default exports only active technologies (set includeInactive to true to export all).
956
+ * Includes product names from subcollections.
957
+ */
958
+ async exportToCsv(options?: {
959
+ includeInactive?: boolean;
960
+ includeBom?: boolean;
961
+ }): Promise<string> {
962
+ const includeInactive = options?.includeInactive ?? false;
963
+ const includeBom = options?.includeBom ?? true;
964
+
965
+ const headers = [
966
+ "id",
967
+ "name",
968
+ "description",
969
+ "family",
970
+ "categoryId",
971
+ "subcategoryId",
972
+ "technicalDetails",
973
+ "requirements_pre",
974
+ "requirements_post",
975
+ "blockingConditions",
976
+ "contraindications",
977
+ "benefits",
978
+ "certificationMinimumLevel",
979
+ "certificationRequiredSpecialties",
980
+ "documentationTemplateIds",
981
+ "productNames",
982
+ "isActive",
983
+ ];
984
+
985
+ const rows: string[] = [];
986
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
987
+
988
+ const PAGE_SIZE = 1000;
989
+ let cursor: any | undefined;
990
+
991
+ // Build base constraints
992
+ const constraints: QueryConstraint[] = [];
993
+ if (!includeInactive) {
994
+ constraints.push(where("isActive", "==", true));
995
+ }
996
+ constraints.push(orderBy("name"));
997
+
998
+ // Page through all results
999
+ // eslint-disable-next-line no-constant-condition
1000
+ while (true) {
1001
+ const queryConstraints: QueryConstraint[] = [...constraints, limit(PAGE_SIZE)];
1002
+ if (cursor) queryConstraints.push(startAfter(cursor));
1003
+
1004
+ const q = query(this.technologiesRef, ...queryConstraints);
1005
+ const snapshot = await getDocs(q);
1006
+ if (snapshot.empty) break;
1007
+
1008
+ for (const d of snapshot.docs) {
1009
+ const technology = ({ id: d.id, ...d.data() } as unknown) as Technology;
1010
+ // Fetch products for this technology
1011
+ const productNames = await this.getProductNamesForTechnology(technology.id!);
1012
+ rows.push(this.technologyToCsvRow(technology, productNames));
1013
+ }
1014
+
1015
+ cursor = snapshot.docs[snapshot.docs.length - 1];
1016
+ if (snapshot.size < PAGE_SIZE) break;
1017
+ }
1018
+
1019
+ const csvBody = rows.join("\r\n");
1020
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
1021
+ }
1022
+
1023
+ /**
1024
+ * Gets product names from the technology's product subcollection
1025
+ */
1026
+ private async getProductNamesForTechnology(technologyId: string): Promise<string[]> {
1027
+ try {
1028
+ const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
1029
+ const q = query(productsRef, where("isActive", "==", true));
1030
+ const snapshot = await getDocs(q);
1031
+ return snapshot.docs.map(doc => {
1032
+ const product = doc.data() as Product;
1033
+ return product.name || "";
1034
+ }).filter(name => name); // Filter out empty names
1035
+ } catch (error) {
1036
+ console.error(`Error fetching products for technology ${technologyId}:`, error);
1037
+ return [];
1038
+ }
1039
+ }
1040
+
1041
+ private technologyToCsvRow(technology: Technology, productNames: string[] = []): string {
1042
+ const values = [
1043
+ technology.id ?? "",
1044
+ technology.name ?? "",
1045
+ technology.description ?? "",
1046
+ technology.family ?? "",
1047
+ technology.categoryId ?? "",
1048
+ technology.subcategoryId ?? "",
1049
+ technology.technicalDetails ?? "",
1050
+ technology.requirements?.pre?.map(r => r.name).join(";") ?? "",
1051
+ technology.requirements?.post?.map(r => r.name).join(";") ?? "",
1052
+ technology.blockingConditions?.join(";") ?? "",
1053
+ technology.contraindications?.map(c => c.name).join(";") ?? "",
1054
+ technology.benefits?.map(b => b.name).join(";") ?? "",
1055
+ technology.certificationRequirement?.minimumLevel ?? "",
1056
+ technology.certificationRequirement?.requiredSpecialties?.join(";") ?? "",
1057
+ technology.documentationTemplates?.map(t => t.templateId).join(";") ?? "",
1058
+ productNames.join(";"),
1059
+ String(technology.isActive ?? ""),
1060
+ ];
1061
+ return values.map((v) => this.formatCsvValue(v)).join(",");
1062
+ }
1063
+
1064
+ private formatCsvValue(value: any): string {
1065
+ const str = value === null || value === undefined ? "" : String(value);
1066
+ // Escape double quotes by doubling them and wrap in quotes
1067
+ const escaped = str.replace(/"/g, '""');
1068
+ return `"${escaped}"`;
1069
+ }
950
1070
  }