@blackcode_sa/metaestetics-api 1.12.50 → 1.12.51
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/admin/index.d.mts +12 -6
- package/dist/admin/index.d.ts +12 -6
- package/dist/backoffice/index.d.mts +275 -18
- package/dist/backoffice/index.d.ts +275 -18
- package/dist/backoffice/index.js +802 -14
- package/dist/backoffice/index.mjs +830 -38
- package/dist/index.d.mts +255 -10
- package/dist/index.d.ts +255 -10
- package/dist/index.js +748 -19
- package/dist/index.mjs +759 -27
- package/package.json +1 -1
- package/src/backoffice/services/brand.service.ts +86 -0
- package/src/backoffice/services/category.service.ts +84 -0
- package/src/backoffice/services/constants.service.ts +77 -0
- package/src/backoffice/services/migrate-products.ts +116 -0
- package/src/backoffice/services/product.service.ts +316 -18
- package/src/backoffice/services/requirement.service.ts +76 -0
- package/src/backoffice/services/subcategory.service.ts +87 -0
- package/src/backoffice/services/technology.service.ts +289 -0
- package/src/backoffice/types/product.types.ts +116 -6
- package/src/services/procedure/procedure.service.ts +2 -2
package/package.json
CHANGED
|
@@ -167,4 +167,90 @@ export class BrandService extends BaseService {
|
|
|
167
167
|
...docSnap.data(),
|
|
168
168
|
} as Brand;
|
|
169
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Exports brands to CSV string, suitable for Excel/Sheets.
|
|
173
|
+
* Includes headers and optional UTF-8 BOM.
|
|
174
|
+
* By default exports only active brands (set includeInactive to true to export all).
|
|
175
|
+
*/
|
|
176
|
+
async exportToCsv(options?: {
|
|
177
|
+
includeInactive?: boolean;
|
|
178
|
+
includeBom?: boolean;
|
|
179
|
+
}): Promise<string> {
|
|
180
|
+
const includeInactive = options?.includeInactive ?? false;
|
|
181
|
+
const includeBom = options?.includeBom ?? true;
|
|
182
|
+
|
|
183
|
+
const headers = [
|
|
184
|
+
"id",
|
|
185
|
+
"name",
|
|
186
|
+
"manufacturer",
|
|
187
|
+
"website",
|
|
188
|
+
"description",
|
|
189
|
+
"isActive",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const rows: string[] = [];
|
|
193
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
194
|
+
|
|
195
|
+
const PAGE_SIZE = 1000;
|
|
196
|
+
let cursor: any | undefined;
|
|
197
|
+
|
|
198
|
+
// Build base constraints
|
|
199
|
+
const baseConstraints: QueryConstraint[] = [];
|
|
200
|
+
if (!includeInactive) {
|
|
201
|
+
baseConstraints.push(where("isActive", "==", true));
|
|
202
|
+
}
|
|
203
|
+
baseConstraints.push(orderBy("name_lowercase"));
|
|
204
|
+
|
|
205
|
+
// Page through all results
|
|
206
|
+
// eslint-disable-next-line no-constant-condition
|
|
207
|
+
while (true) {
|
|
208
|
+
const constraints: QueryConstraint[] = [...baseConstraints, limit(PAGE_SIZE)];
|
|
209
|
+
if (cursor) constraints.push(startAfter(cursor));
|
|
210
|
+
|
|
211
|
+
const q = query(this.getBrandsRef(), ...constraints);
|
|
212
|
+
const snapshot = await getDocs(q);
|
|
213
|
+
if (snapshot.empty) break;
|
|
214
|
+
|
|
215
|
+
for (const d of snapshot.docs) {
|
|
216
|
+
const brand = ({ id: d.id, ...d.data() } as unknown) as Brand;
|
|
217
|
+
rows.push(this.brandToCsvRow(brand));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
221
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const csvBody = rows.join("\r\n");
|
|
225
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private brandToCsvRow(brand: Brand): string {
|
|
229
|
+
const values = [
|
|
230
|
+
brand.id ?? "",
|
|
231
|
+
brand.name ?? "",
|
|
232
|
+
brand.manufacturer ?? "",
|
|
233
|
+
brand.website ?? "",
|
|
234
|
+
brand.description ?? "",
|
|
235
|
+
String(brand.isActive ?? ""),
|
|
236
|
+
];
|
|
237
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private formatDateIso(value: any): string {
|
|
241
|
+
// Firestore timestamps may come back as Date or Timestamp; handle both
|
|
242
|
+
if (value instanceof Date) return value.toISOString();
|
|
243
|
+
if (value && typeof value.toDate === "function") {
|
|
244
|
+
const d = value.toDate();
|
|
245
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
246
|
+
}
|
|
247
|
+
return String(value ?? "");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private formatCsvValue(value: any): string {
|
|
251
|
+
const str = value === null || value === undefined ? "" : String(value);
|
|
252
|
+
// Escape double quotes by doubling them and wrap in quotes
|
|
253
|
+
const escaped = str.replace(/"/g, '""');
|
|
254
|
+
return `"${escaped}"`;
|
|
255
|
+
}
|
|
170
256
|
}
|
|
@@ -231,4 +231,88 @@ export class CategoryService extends BaseService implements ICategoryService {
|
|
|
231
231
|
...docSnap.data(),
|
|
232
232
|
} as Category;
|
|
233
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Exports categories to CSV string, suitable for Excel/Sheets.
|
|
237
|
+
* Includes headers and optional UTF-8 BOM.
|
|
238
|
+
* By default exports only active categories (set includeInactive to true to export all).
|
|
239
|
+
*/
|
|
240
|
+
async exportToCsv(options?: {
|
|
241
|
+
includeInactive?: boolean;
|
|
242
|
+
includeBom?: boolean;
|
|
243
|
+
}): Promise<string> {
|
|
244
|
+
const includeInactive = options?.includeInactive ?? false;
|
|
245
|
+
const includeBom = options?.includeBom ?? true;
|
|
246
|
+
|
|
247
|
+
const headers = [
|
|
248
|
+
"id",
|
|
249
|
+
"name",
|
|
250
|
+
"description",
|
|
251
|
+
"family",
|
|
252
|
+
"isActive",
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
const rows: string[] = [];
|
|
256
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
257
|
+
|
|
258
|
+
const PAGE_SIZE = 1000;
|
|
259
|
+
let cursor: any | undefined;
|
|
260
|
+
|
|
261
|
+
// Build base constraints
|
|
262
|
+
const constraints: any[] = [];
|
|
263
|
+
if (!includeInactive) {
|
|
264
|
+
constraints.push(where("isActive", "==", true));
|
|
265
|
+
}
|
|
266
|
+
constraints.push(orderBy("name"));
|
|
267
|
+
|
|
268
|
+
// Page through all results
|
|
269
|
+
// eslint-disable-next-line no-constant-condition
|
|
270
|
+
while (true) {
|
|
271
|
+
const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
|
|
272
|
+
if (cursor) queryConstraints.push(startAfter(cursor));
|
|
273
|
+
|
|
274
|
+
const q = query(this.categoriesRef, ...queryConstraints);
|
|
275
|
+
const snapshot = await getDocs(q);
|
|
276
|
+
if (snapshot.empty) break;
|
|
277
|
+
|
|
278
|
+
for (const d of snapshot.docs) {
|
|
279
|
+
const category = ({ id: d.id, ...d.data() } as unknown) as Category;
|
|
280
|
+
rows.push(this.categoryToCsvRow(category));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
284
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const csvBody = rows.join("\r\n");
|
|
288
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private categoryToCsvRow(category: Category): string {
|
|
292
|
+
const values = [
|
|
293
|
+
category.id ?? "",
|
|
294
|
+
category.name ?? "",
|
|
295
|
+
category.description ?? "",
|
|
296
|
+
category.family ?? "",
|
|
297
|
+
String(category.isActive ?? ""),
|
|
298
|
+
];
|
|
299
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private formatDateIso(value: any): string {
|
|
303
|
+
// Firestore timestamps may come back as Date or Timestamp; handle both
|
|
304
|
+
if (value instanceof Date) return value.toISOString();
|
|
305
|
+
if (value && typeof value.toDate === "function") {
|
|
306
|
+
const d = value.toDate();
|
|
307
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
308
|
+
}
|
|
309
|
+
return String(value ?? "");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private formatCsvValue(value: any): string {
|
|
313
|
+
const str = value === null || value === undefined ? "" : String(value);
|
|
314
|
+
// Escape double quotes by doubling them and wrap in quotes
|
|
315
|
+
const escaped = str.replace(/"/g, '""');
|
|
316
|
+
return `"${escaped}"`;
|
|
317
|
+
}
|
|
234
318
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as admin from 'firebase-admin';
|
|
2
|
+
import { Product } from '../types/product.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Migration script to copy existing products from technology subcollections
|
|
6
|
+
* to the new top-level products collection
|
|
7
|
+
*
|
|
8
|
+
* Usage: Run this once to migrate existing data
|
|
9
|
+
*/
|
|
10
|
+
export async function migrateProductsToTopLevel(db: admin.firestore.Firestore) {
|
|
11
|
+
console.log('🚀 Starting product migration...');
|
|
12
|
+
|
|
13
|
+
// Get all technologies
|
|
14
|
+
const technologiesSnapshot = await db.collection('technologies').get();
|
|
15
|
+
|
|
16
|
+
const productMap = new Map<string, {
|
|
17
|
+
product: any;
|
|
18
|
+
technologyIds: string[];
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
let totalProcessed = 0;
|
|
22
|
+
|
|
23
|
+
// Step 1: Collect all products from all technology subcollections
|
|
24
|
+
for (const techDoc of technologiesSnapshot.docs) {
|
|
25
|
+
const technologyId = techDoc.id;
|
|
26
|
+
console.log(`📦 Processing technology: ${technologyId}`);
|
|
27
|
+
|
|
28
|
+
const productsSnapshot = await db
|
|
29
|
+
.collection('technologies')
|
|
30
|
+
.doc(technologyId)
|
|
31
|
+
.collection('products')
|
|
32
|
+
.get();
|
|
33
|
+
|
|
34
|
+
for (const productDoc of productsSnapshot.docs) {
|
|
35
|
+
const productId = productDoc.id;
|
|
36
|
+
const productData = productDoc.data();
|
|
37
|
+
|
|
38
|
+
totalProcessed++;
|
|
39
|
+
|
|
40
|
+
// Deduplicate by name + brandId
|
|
41
|
+
const key = `${productData.name}_${productData.brandId}`;
|
|
42
|
+
|
|
43
|
+
if (productMap.has(key)) {
|
|
44
|
+
// Product already exists, just add this technology
|
|
45
|
+
productMap.get(key)!.technologyIds.push(technologyId);
|
|
46
|
+
} else {
|
|
47
|
+
// New product
|
|
48
|
+
productMap.set(key, {
|
|
49
|
+
product: productData,
|
|
50
|
+
technologyIds: [technologyId],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`✅ Found ${productMap.size} unique products from ${totalProcessed} total entries`);
|
|
57
|
+
|
|
58
|
+
// Step 2: Create products in top-level collection
|
|
59
|
+
const batch = db.batch();
|
|
60
|
+
let batchCount = 0;
|
|
61
|
+
const MAX_BATCH_SIZE = 500;
|
|
62
|
+
let createdCount = 0;
|
|
63
|
+
|
|
64
|
+
for (const [key, { product, technologyIds }] of productMap.entries()) {
|
|
65
|
+
const productRef = db.collection('products').doc();
|
|
66
|
+
|
|
67
|
+
const migratedProduct: any = {
|
|
68
|
+
...product,
|
|
69
|
+
assignedTechnologyIds: technologyIds, // Track all assigned technologies
|
|
70
|
+
migratedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
71
|
+
migrationKey: key, // For debugging
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
batch.set(productRef, migratedProduct);
|
|
75
|
+
createdCount++;
|
|
76
|
+
batchCount++;
|
|
77
|
+
|
|
78
|
+
if (batchCount >= MAX_BATCH_SIZE) {
|
|
79
|
+
await batch.commit();
|
|
80
|
+
console.log(`💾 Committed batch of ${batchCount} products (${createdCount}/${productMap.size})`);
|
|
81
|
+
batchCount = 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (batchCount > 0) {
|
|
86
|
+
await batch.commit();
|
|
87
|
+
console.log(`💾 Committed final batch of ${batchCount} products`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('✅ Migration complete!');
|
|
91
|
+
console.log(`📊 Summary:`);
|
|
92
|
+
console.log(` - Products processed: ${totalProcessed}`);
|
|
93
|
+
console.log(` - Unique products created: ${createdCount}`);
|
|
94
|
+
console.log(` - Average technologies per product: ${(createdCount > 0 ? totalProcessed / createdCount : 0).toFixed(2)}`);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
totalProcessed,
|
|
98
|
+
uniqueCreated: createdCount,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Run migration (for testing)
|
|
104
|
+
*/
|
|
105
|
+
if (require.main === module) {
|
|
106
|
+
(async () => {
|
|
107
|
+
try {
|
|
108
|
+
await migrateProductsToTopLevel(admin.firestore());
|
|
109
|
+
process.exit(0);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('❌ Migration failed:', error);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
}
|
|
116
|
+
|