@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
|
@@ -169,6 +169,73 @@ var BrandService = class extends BaseService {
|
|
|
169
169
|
...docSnap.data()
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Exports brands to CSV string, suitable for Excel/Sheets.
|
|
174
|
+
* Includes headers and optional UTF-8 BOM.
|
|
175
|
+
* By default exports only active brands (set includeInactive to true to export all).
|
|
176
|
+
*/
|
|
177
|
+
async exportToCsv(options) {
|
|
178
|
+
var _a, _b;
|
|
179
|
+
const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
|
|
180
|
+
const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
|
|
181
|
+
const headers = [
|
|
182
|
+
"id",
|
|
183
|
+
"name",
|
|
184
|
+
"manufacturer",
|
|
185
|
+
"website",
|
|
186
|
+
"description",
|
|
187
|
+
"isActive"
|
|
188
|
+
];
|
|
189
|
+
const rows = [];
|
|
190
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
191
|
+
const PAGE_SIZE = 1e3;
|
|
192
|
+
let cursor;
|
|
193
|
+
const baseConstraints = [];
|
|
194
|
+
if (!includeInactive) {
|
|
195
|
+
baseConstraints.push(where("isActive", "==", true));
|
|
196
|
+
}
|
|
197
|
+
baseConstraints.push(orderBy("name_lowercase"));
|
|
198
|
+
while (true) {
|
|
199
|
+
const constraints = [...baseConstraints, limit(PAGE_SIZE)];
|
|
200
|
+
if (cursor) constraints.push(startAfter(cursor));
|
|
201
|
+
const q = query(this.getBrandsRef(), ...constraints);
|
|
202
|
+
const snapshot = await getDocs(q);
|
|
203
|
+
if (snapshot.empty) break;
|
|
204
|
+
for (const d of snapshot.docs) {
|
|
205
|
+
const brand = { id: d.id, ...d.data() };
|
|
206
|
+
rows.push(this.brandToCsvRow(brand));
|
|
207
|
+
}
|
|
208
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
209
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
210
|
+
}
|
|
211
|
+
const csvBody = rows.join("\r\n");
|
|
212
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
213
|
+
}
|
|
214
|
+
brandToCsvRow(brand) {
|
|
215
|
+
var _a, _b, _c, _d, _e, _f;
|
|
216
|
+
const values = [
|
|
217
|
+
(_a = brand.id) != null ? _a : "",
|
|
218
|
+
(_b = brand.name) != null ? _b : "",
|
|
219
|
+
(_c = brand.manufacturer) != null ? _c : "",
|
|
220
|
+
(_d = brand.website) != null ? _d : "",
|
|
221
|
+
(_e = brand.description) != null ? _e : "",
|
|
222
|
+
String((_f = brand.isActive) != null ? _f : "")
|
|
223
|
+
];
|
|
224
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
225
|
+
}
|
|
226
|
+
formatDateIso(value) {
|
|
227
|
+
if (value instanceof Date) return value.toISOString();
|
|
228
|
+
if (value && typeof value.toDate === "function") {
|
|
229
|
+
const d = value.toDate();
|
|
230
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
231
|
+
}
|
|
232
|
+
return String(value != null ? value : "");
|
|
233
|
+
}
|
|
234
|
+
formatCsvValue(value) {
|
|
235
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
236
|
+
const escaped = str.replace(/"/g, '""');
|
|
237
|
+
return `"${escaped}"`;
|
|
238
|
+
}
|
|
172
239
|
};
|
|
173
240
|
|
|
174
241
|
// src/backoffice/services/category.service.ts
|
|
@@ -367,6 +434,71 @@ var CategoryService = class extends BaseService {
|
|
|
367
434
|
...docSnap.data()
|
|
368
435
|
};
|
|
369
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Exports categories to CSV string, suitable for Excel/Sheets.
|
|
439
|
+
* Includes headers and optional UTF-8 BOM.
|
|
440
|
+
* By default exports only active categories (set includeInactive to true to export all).
|
|
441
|
+
*/
|
|
442
|
+
async exportToCsv(options) {
|
|
443
|
+
var _a, _b;
|
|
444
|
+
const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
|
|
445
|
+
const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
|
|
446
|
+
const headers = [
|
|
447
|
+
"id",
|
|
448
|
+
"name",
|
|
449
|
+
"description",
|
|
450
|
+
"family",
|
|
451
|
+
"isActive"
|
|
452
|
+
];
|
|
453
|
+
const rows = [];
|
|
454
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
455
|
+
const PAGE_SIZE = 1e3;
|
|
456
|
+
let cursor;
|
|
457
|
+
const constraints = [];
|
|
458
|
+
if (!includeInactive) {
|
|
459
|
+
constraints.push(where2("isActive", "==", true));
|
|
460
|
+
}
|
|
461
|
+
constraints.push(orderBy2("name"));
|
|
462
|
+
while (true) {
|
|
463
|
+
const queryConstraints = [...constraints, limit2(PAGE_SIZE)];
|
|
464
|
+
if (cursor) queryConstraints.push(startAfter2(cursor));
|
|
465
|
+
const q = query2(this.categoriesRef, ...queryConstraints);
|
|
466
|
+
const snapshot = await getDocs2(q);
|
|
467
|
+
if (snapshot.empty) break;
|
|
468
|
+
for (const d of snapshot.docs) {
|
|
469
|
+
const category = { id: d.id, ...d.data() };
|
|
470
|
+
rows.push(this.categoryToCsvRow(category));
|
|
471
|
+
}
|
|
472
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
473
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
474
|
+
}
|
|
475
|
+
const csvBody = rows.join("\r\n");
|
|
476
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
477
|
+
}
|
|
478
|
+
categoryToCsvRow(category) {
|
|
479
|
+
var _a, _b, _c, _d, _e;
|
|
480
|
+
const values = [
|
|
481
|
+
(_a = category.id) != null ? _a : "",
|
|
482
|
+
(_b = category.name) != null ? _b : "",
|
|
483
|
+
(_c = category.description) != null ? _c : "",
|
|
484
|
+
(_d = category.family) != null ? _d : "",
|
|
485
|
+
String((_e = category.isActive) != null ? _e : "")
|
|
486
|
+
];
|
|
487
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
488
|
+
}
|
|
489
|
+
formatDateIso(value) {
|
|
490
|
+
if (value instanceof Date) return value.toISOString();
|
|
491
|
+
if (value && typeof value.toDate === "function") {
|
|
492
|
+
const d = value.toDate();
|
|
493
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
494
|
+
}
|
|
495
|
+
return String(value != null ? value : "");
|
|
496
|
+
}
|
|
497
|
+
formatCsvValue(value) {
|
|
498
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
499
|
+
const escaped = str.replace(/"/g, '""');
|
|
500
|
+
return `"${escaped}"`;
|
|
501
|
+
}
|
|
370
502
|
};
|
|
371
503
|
|
|
372
504
|
// src/services/documentation-templates/documentation-template.service.ts
|
|
@@ -1158,7 +1290,9 @@ import {
|
|
|
1158
1290
|
limit as limit6,
|
|
1159
1291
|
orderBy as orderBy6,
|
|
1160
1292
|
startAfter as startAfter5,
|
|
1161
|
-
getCountFromServer as getCountFromServer4
|
|
1293
|
+
getCountFromServer as getCountFromServer4,
|
|
1294
|
+
arrayUnion,
|
|
1295
|
+
arrayRemove
|
|
1162
1296
|
} from "firebase/firestore";
|
|
1163
1297
|
|
|
1164
1298
|
// src/backoffice/types/product.types.ts
|
|
@@ -1170,7 +1304,14 @@ var TECHNOLOGIES_COLLECTION = "technologies";
|
|
|
1170
1304
|
// src/backoffice/services/product.service.ts
|
|
1171
1305
|
var ProductService = class extends BaseService {
|
|
1172
1306
|
/**
|
|
1173
|
-
* Gets reference to products collection
|
|
1307
|
+
* Gets reference to top-level products collection (source of truth)
|
|
1308
|
+
* @returns Firestore collection reference
|
|
1309
|
+
*/
|
|
1310
|
+
getTopLevelProductsRef() {
|
|
1311
|
+
return collection6(this.db, PRODUCTS_COLLECTION);
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Gets reference to products collection under a technology (backward compatibility)
|
|
1174
1315
|
* @param technologyId - ID of the technology
|
|
1175
1316
|
* @returns Firestore collection reference
|
|
1176
1317
|
*/
|
|
@@ -1186,6 +1327,7 @@ var ProductService = class extends BaseService {
|
|
|
1186
1327
|
...product,
|
|
1187
1328
|
brandId,
|
|
1188
1329
|
technologyId,
|
|
1330
|
+
// Required for old subcollection structure
|
|
1189
1331
|
createdAt: now,
|
|
1190
1332
|
updatedAt: now,
|
|
1191
1333
|
isActive: true
|
|
@@ -1245,30 +1387,26 @@ var ProductService = class extends BaseService {
|
|
|
1245
1387
|
}
|
|
1246
1388
|
/**
|
|
1247
1389
|
* Gets counts of active products grouped by category, subcategory, and technology.
|
|
1248
|
-
*
|
|
1390
|
+
* Queries technology subcollections which have the legacy fields synced by Cloud Functions.
|
|
1249
1391
|
*/
|
|
1250
1392
|
async getProductCounts() {
|
|
1251
|
-
const q = query6(collectionGroup(this.db, PRODUCTS_COLLECTION), where6("isActive", "==", true));
|
|
1252
|
-
const snapshot = await getDocs6(q);
|
|
1253
1393
|
const counts = {
|
|
1254
1394
|
byCategory: {},
|
|
1255
1395
|
bySubcategory: {},
|
|
1256
1396
|
byTechnology: {}
|
|
1257
1397
|
};
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1398
|
+
const q = query6(collectionGroup(this.db, PRODUCTS_COLLECTION), where6("isActive", "==", true));
|
|
1399
|
+
const snapshot = await getDocs6(q);
|
|
1261
1400
|
snapshot.docs.forEach((doc11) => {
|
|
1262
1401
|
const product = doc11.data();
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
counts.byCategory[categoryId] = (counts.byCategory[categoryId] || 0) + 1;
|
|
1402
|
+
if (product.categoryId) {
|
|
1403
|
+
counts.byCategory[product.categoryId] = (counts.byCategory[product.categoryId] || 0) + 1;
|
|
1266
1404
|
}
|
|
1267
|
-
if (subcategoryId) {
|
|
1268
|
-
counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
|
|
1405
|
+
if (product.subcategoryId) {
|
|
1406
|
+
counts.bySubcategory[product.subcategoryId] = (counts.bySubcategory[product.subcategoryId] || 0) + 1;
|
|
1269
1407
|
}
|
|
1270
|
-
if (technologyId) {
|
|
1271
|
-
counts.byTechnology[technologyId] = (counts.byTechnology[technologyId] || 0) + 1;
|
|
1408
|
+
if (product.technologyId) {
|
|
1409
|
+
counts.byTechnology[product.technologyId] = (counts.byTechnology[product.technologyId] || 0) + 1;
|
|
1272
1410
|
}
|
|
1273
1411
|
});
|
|
1274
1412
|
return counts;
|
|
@@ -1347,6 +1485,241 @@ var ProductService = class extends BaseService {
|
|
|
1347
1485
|
...docSnap.data()
|
|
1348
1486
|
};
|
|
1349
1487
|
}
|
|
1488
|
+
// ==========================================
|
|
1489
|
+
// NEW METHODS: Top-level collection (preferred)
|
|
1490
|
+
// ==========================================
|
|
1491
|
+
/**
|
|
1492
|
+
* Creates a new product in the top-level collection
|
|
1493
|
+
*/
|
|
1494
|
+
async createTopLevel(brandId, product, technologyIds = []) {
|
|
1495
|
+
const now = /* @__PURE__ */ new Date();
|
|
1496
|
+
const newProduct = {
|
|
1497
|
+
...product,
|
|
1498
|
+
brandId,
|
|
1499
|
+
assignedTechnologyIds: technologyIds,
|
|
1500
|
+
createdAt: now,
|
|
1501
|
+
updatedAt: now,
|
|
1502
|
+
isActive: true
|
|
1503
|
+
};
|
|
1504
|
+
const productRef = await addDoc3(this.getTopLevelProductsRef(), newProduct);
|
|
1505
|
+
return { id: productRef.id, ...newProduct };
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Gets all products from the top-level collection
|
|
1509
|
+
*/
|
|
1510
|
+
async getAllTopLevel(options) {
|
|
1511
|
+
const { rowsPerPage, lastVisible, brandId } = options;
|
|
1512
|
+
const constraints = [where6("isActive", "==", true), orderBy6("name")];
|
|
1513
|
+
if (brandId) {
|
|
1514
|
+
constraints.push(where6("brandId", "==", brandId));
|
|
1515
|
+
}
|
|
1516
|
+
if (lastVisible) {
|
|
1517
|
+
constraints.push(startAfter5(lastVisible));
|
|
1518
|
+
}
|
|
1519
|
+
constraints.push(limit6(rowsPerPage));
|
|
1520
|
+
const q = query6(this.getTopLevelProductsRef(), ...constraints);
|
|
1521
|
+
const snapshot = await getDocs6(q);
|
|
1522
|
+
const products = snapshot.docs.map(
|
|
1523
|
+
(doc11) => ({
|
|
1524
|
+
id: doc11.id,
|
|
1525
|
+
...doc11.data()
|
|
1526
|
+
})
|
|
1527
|
+
);
|
|
1528
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
1529
|
+
return { products, lastVisible: newLastVisible };
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Gets a product by ID from the top-level collection
|
|
1533
|
+
*/
|
|
1534
|
+
async getByIdTopLevel(productId) {
|
|
1535
|
+
const docRef = doc6(this.getTopLevelProductsRef(), productId);
|
|
1536
|
+
const docSnap = await getDoc6(docRef);
|
|
1537
|
+
if (!docSnap.exists()) return null;
|
|
1538
|
+
return {
|
|
1539
|
+
id: docSnap.id,
|
|
1540
|
+
...docSnap.data()
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Updates a product in the top-level collection
|
|
1545
|
+
*/
|
|
1546
|
+
async updateTopLevel(productId, product) {
|
|
1547
|
+
const updateData = {
|
|
1548
|
+
...product,
|
|
1549
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1550
|
+
};
|
|
1551
|
+
const docRef = doc6(this.getTopLevelProductsRef(), productId);
|
|
1552
|
+
await updateDoc6(docRef, updateData);
|
|
1553
|
+
return this.getByIdTopLevel(productId);
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Deletes a product from the top-level collection (soft delete)
|
|
1557
|
+
*/
|
|
1558
|
+
async deleteTopLevel(productId) {
|
|
1559
|
+
await this.updateTopLevel(productId, {
|
|
1560
|
+
isActive: false
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Assigns a product to a technology
|
|
1565
|
+
*/
|
|
1566
|
+
async assignToTechnology(productId, technologyId) {
|
|
1567
|
+
const docRef = doc6(this.getTopLevelProductsRef(), productId);
|
|
1568
|
+
await updateDoc6(docRef, {
|
|
1569
|
+
assignedTechnologyIds: arrayUnion(technologyId),
|
|
1570
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Unassigns a product from a technology
|
|
1575
|
+
*/
|
|
1576
|
+
async unassignFromTechnology(productId, technologyId) {
|
|
1577
|
+
const docRef = doc6(this.getTopLevelProductsRef(), productId);
|
|
1578
|
+
await updateDoc6(docRef, {
|
|
1579
|
+
assignedTechnologyIds: arrayRemove(technologyId),
|
|
1580
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Gets products assigned to a specific technology
|
|
1585
|
+
*/
|
|
1586
|
+
async getAssignedProducts(technologyId) {
|
|
1587
|
+
const q = query6(
|
|
1588
|
+
this.getTopLevelProductsRef(),
|
|
1589
|
+
where6("assignedTechnologyIds", "array-contains", technologyId),
|
|
1590
|
+
where6("isActive", "==", true),
|
|
1591
|
+
orderBy6("name")
|
|
1592
|
+
);
|
|
1593
|
+
const snapshot = await getDocs6(q);
|
|
1594
|
+
return snapshot.docs.map(
|
|
1595
|
+
(doc11) => ({
|
|
1596
|
+
id: doc11.id,
|
|
1597
|
+
...doc11.data()
|
|
1598
|
+
})
|
|
1599
|
+
);
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Gets products NOT assigned to a specific technology
|
|
1603
|
+
*/
|
|
1604
|
+
async getUnassignedProducts(technologyId) {
|
|
1605
|
+
const q = query6(
|
|
1606
|
+
this.getTopLevelProductsRef(),
|
|
1607
|
+
where6("isActive", "==", true),
|
|
1608
|
+
orderBy6("name")
|
|
1609
|
+
);
|
|
1610
|
+
const snapshot = await getDocs6(q);
|
|
1611
|
+
const allProducts = snapshot.docs.map(
|
|
1612
|
+
(doc11) => ({
|
|
1613
|
+
id: doc11.id,
|
|
1614
|
+
...doc11.data()
|
|
1615
|
+
})
|
|
1616
|
+
);
|
|
1617
|
+
return allProducts.filter(
|
|
1618
|
+
(product) => {
|
|
1619
|
+
var _a;
|
|
1620
|
+
return !((_a = product.assignedTechnologyIds) == null ? void 0 : _a.includes(technologyId));
|
|
1621
|
+
}
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Gets all products for a brand (from top-level collection)
|
|
1626
|
+
*/
|
|
1627
|
+
async getByBrand(brandId) {
|
|
1628
|
+
const q = query6(
|
|
1629
|
+
this.getTopLevelProductsRef(),
|
|
1630
|
+
where6("brandId", "==", brandId),
|
|
1631
|
+
where6("isActive", "==", true),
|
|
1632
|
+
orderBy6("name")
|
|
1633
|
+
);
|
|
1634
|
+
const snapshot = await getDocs6(q);
|
|
1635
|
+
return snapshot.docs.map(
|
|
1636
|
+
(doc11) => ({
|
|
1637
|
+
id: doc11.id,
|
|
1638
|
+
...doc11.data()
|
|
1639
|
+
})
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Exports products to CSV string, suitable for Excel/Sheets.
|
|
1644
|
+
* Includes headers and optional UTF-8 BOM.
|
|
1645
|
+
* By default exports only active products (set includeInactive to true to export all).
|
|
1646
|
+
*/
|
|
1647
|
+
async exportToCsv(options) {
|
|
1648
|
+
var _a, _b;
|
|
1649
|
+
const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
|
|
1650
|
+
const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
|
|
1651
|
+
const headers = [
|
|
1652
|
+
"id",
|
|
1653
|
+
"name",
|
|
1654
|
+
"brandId",
|
|
1655
|
+
"brandName",
|
|
1656
|
+
"assignedTechnologyIds",
|
|
1657
|
+
"description",
|
|
1658
|
+
"technicalDetails",
|
|
1659
|
+
"dosage",
|
|
1660
|
+
"composition",
|
|
1661
|
+
"indications",
|
|
1662
|
+
"contraindications",
|
|
1663
|
+
"warnings",
|
|
1664
|
+
"isActive"
|
|
1665
|
+
];
|
|
1666
|
+
const rows = [];
|
|
1667
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
1668
|
+
const PAGE_SIZE = 1e3;
|
|
1669
|
+
let cursor;
|
|
1670
|
+
const constraints = [];
|
|
1671
|
+
if (!includeInactive) {
|
|
1672
|
+
constraints.push(where6("isActive", "==", true));
|
|
1673
|
+
}
|
|
1674
|
+
constraints.push(orderBy6("name"));
|
|
1675
|
+
while (true) {
|
|
1676
|
+
const queryConstraints = [...constraints, limit6(PAGE_SIZE)];
|
|
1677
|
+
if (cursor) queryConstraints.push(startAfter5(cursor));
|
|
1678
|
+
const q = query6(this.getTopLevelProductsRef(), ...queryConstraints);
|
|
1679
|
+
const snapshot = await getDocs6(q);
|
|
1680
|
+
if (snapshot.empty) break;
|
|
1681
|
+
for (const d of snapshot.docs) {
|
|
1682
|
+
const product = { id: d.id, ...d.data() };
|
|
1683
|
+
rows.push(this.productToCsvRow(product));
|
|
1684
|
+
}
|
|
1685
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
1686
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
1687
|
+
}
|
|
1688
|
+
const csvBody = rows.join("\r\n");
|
|
1689
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
1690
|
+
}
|
|
1691
|
+
productToCsvRow(product) {
|
|
1692
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
|
|
1693
|
+
const values = [
|
|
1694
|
+
(_a = product.id) != null ? _a : "",
|
|
1695
|
+
(_b = product.name) != null ? _b : "",
|
|
1696
|
+
(_c = product.brandId) != null ? _c : "",
|
|
1697
|
+
(_d = product.brandName) != null ? _d : "",
|
|
1698
|
+
(_f = (_e = product.assignedTechnologyIds) == null ? void 0 : _e.join(";")) != null ? _f : "",
|
|
1699
|
+
(_g = product.description) != null ? _g : "",
|
|
1700
|
+
(_h = product.technicalDetails) != null ? _h : "",
|
|
1701
|
+
(_i = product.dosage) != null ? _i : "",
|
|
1702
|
+
(_j = product.composition) != null ? _j : "",
|
|
1703
|
+
(_l = (_k = product.indications) == null ? void 0 : _k.join(";")) != null ? _l : "",
|
|
1704
|
+
(_n = (_m = product.contraindications) == null ? void 0 : _m.map((c) => c.name).join(";")) != null ? _n : "",
|
|
1705
|
+
(_p = (_o = product.warnings) == null ? void 0 : _o.join(";")) != null ? _p : "",
|
|
1706
|
+
String((_q = product.isActive) != null ? _q : "")
|
|
1707
|
+
];
|
|
1708
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
1709
|
+
}
|
|
1710
|
+
formatDateIso(value) {
|
|
1711
|
+
if (value instanceof Date) return value.toISOString();
|
|
1712
|
+
if (value && typeof value.toDate === "function") {
|
|
1713
|
+
const d = value.toDate();
|
|
1714
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
1715
|
+
}
|
|
1716
|
+
return String(value != null ? value : "");
|
|
1717
|
+
}
|
|
1718
|
+
formatCsvValue(value) {
|
|
1719
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
1720
|
+
const escaped = str.replace(/"/g, '""');
|
|
1721
|
+
return `"${escaped}"`;
|
|
1722
|
+
}
|
|
1350
1723
|
};
|
|
1351
1724
|
|
|
1352
1725
|
// src/backoffice/services/requirement.service.ts
|
|
@@ -1358,7 +1731,8 @@ import {
|
|
|
1358
1731
|
getDocs as getDocs7,
|
|
1359
1732
|
query as query7,
|
|
1360
1733
|
updateDoc as updateDoc7,
|
|
1361
|
-
where as where7
|
|
1734
|
+
where as where7,
|
|
1735
|
+
orderBy as orderBy7
|
|
1362
1736
|
} from "firebase/firestore";
|
|
1363
1737
|
|
|
1364
1738
|
// src/backoffice/types/requirement.types.ts
|
|
@@ -1479,6 +1853,65 @@ var RequirementService = class extends BaseService {
|
|
|
1479
1853
|
...docSnap.data()
|
|
1480
1854
|
};
|
|
1481
1855
|
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Exports requirements to CSV string, suitable for Excel/Sheets.
|
|
1858
|
+
* Includes headers and optional UTF-8 BOM.
|
|
1859
|
+
* By default exports only active requirements (set includeInactive to true to export all).
|
|
1860
|
+
*/
|
|
1861
|
+
async exportToCsv(options) {
|
|
1862
|
+
var _a, _b;
|
|
1863
|
+
const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
|
|
1864
|
+
const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
|
|
1865
|
+
const headers = [
|
|
1866
|
+
"id",
|
|
1867
|
+
"type",
|
|
1868
|
+
"name",
|
|
1869
|
+
"description",
|
|
1870
|
+
"timeframe_duration",
|
|
1871
|
+
"timeframe_unit",
|
|
1872
|
+
"timeframe_notifyAt",
|
|
1873
|
+
"importance",
|
|
1874
|
+
"isActive"
|
|
1875
|
+
];
|
|
1876
|
+
const rows = [];
|
|
1877
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
1878
|
+
const q = includeInactive ? query7(this.requirementsRef, orderBy7("name")) : query7(this.requirementsRef, where7("isActive", "==", true), orderBy7("name"));
|
|
1879
|
+
const snapshot = await getDocs7(q);
|
|
1880
|
+
for (const d of snapshot.docs) {
|
|
1881
|
+
const requirement = { id: d.id, ...d.data() };
|
|
1882
|
+
rows.push(this.requirementToCsvRow(requirement));
|
|
1883
|
+
}
|
|
1884
|
+
const csvBody = rows.join("\r\n");
|
|
1885
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
1886
|
+
}
|
|
1887
|
+
requirementToCsvRow(requirement) {
|
|
1888
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
|
|
1889
|
+
const values = [
|
|
1890
|
+
(_a = requirement.id) != null ? _a : "",
|
|
1891
|
+
(_b = requirement.type) != null ? _b : "",
|
|
1892
|
+
(_c = requirement.name) != null ? _c : "",
|
|
1893
|
+
(_d = requirement.description) != null ? _d : "",
|
|
1894
|
+
(_g = (_f = (_e = requirement.timeframe) == null ? void 0 : _e.duration) == null ? void 0 : _f.toString()) != null ? _g : "",
|
|
1895
|
+
(_i = (_h = requirement.timeframe) == null ? void 0 : _h.unit) != null ? _i : "",
|
|
1896
|
+
(_l = (_k = (_j = requirement.timeframe) == null ? void 0 : _j.notifyAt) == null ? void 0 : _k.join(";")) != null ? _l : "",
|
|
1897
|
+
(_m = requirement.importance) != null ? _m : "",
|
|
1898
|
+
String((_n = requirement.isActive) != null ? _n : "")
|
|
1899
|
+
];
|
|
1900
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
1901
|
+
}
|
|
1902
|
+
formatDateIso(value) {
|
|
1903
|
+
if (value instanceof Date) return value.toISOString();
|
|
1904
|
+
if (value && typeof value.toDate === "function") {
|
|
1905
|
+
const d = value.toDate();
|
|
1906
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
1907
|
+
}
|
|
1908
|
+
return String(value != null ? value : "");
|
|
1909
|
+
}
|
|
1910
|
+
formatCsvValue(value) {
|
|
1911
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
1912
|
+
const escaped = str.replace(/"/g, '""');
|
|
1913
|
+
return `"${escaped}"`;
|
|
1914
|
+
}
|
|
1482
1915
|
};
|
|
1483
1916
|
|
|
1484
1917
|
// src/backoffice/services/subcategory.service.ts
|
|
@@ -1492,7 +1925,7 @@ import {
|
|
|
1492
1925
|
getDoc as getDoc8,
|
|
1493
1926
|
getDocs as getDocs8,
|
|
1494
1927
|
limit as limit7,
|
|
1495
|
-
orderBy as
|
|
1928
|
+
orderBy as orderBy8,
|
|
1496
1929
|
query as query8,
|
|
1497
1930
|
setDoc as setDoc4,
|
|
1498
1931
|
startAfter as startAfter6,
|
|
@@ -1566,7 +1999,7 @@ var SubcategoryService = class extends BaseService {
|
|
|
1566
1999
|
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
1567
2000
|
const constraints = [
|
|
1568
2001
|
where8("isActive", "==", active),
|
|
1569
|
-
|
|
2002
|
+
orderBy8("name"),
|
|
1570
2003
|
queryLimit ? limit7(queryLimit) : void 0,
|
|
1571
2004
|
lastVisible ? startAfter6(lastVisible) : void 0
|
|
1572
2005
|
].filter((c) => !!c);
|
|
@@ -1593,7 +2026,7 @@ var SubcategoryService = class extends BaseService {
|
|
|
1593
2026
|
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
1594
2027
|
const constraints = [
|
|
1595
2028
|
where8("isActive", "==", active),
|
|
1596
|
-
|
|
2029
|
+
orderBy8("name"),
|
|
1597
2030
|
queryLimit ? limit7(queryLimit) : void 0,
|
|
1598
2031
|
lastVisible ? startAfter6(lastVisible) : void 0
|
|
1599
2032
|
].filter((c) => !!c);
|
|
@@ -1722,6 +2155,74 @@ var SubcategoryService = class extends BaseService {
|
|
|
1722
2155
|
...docSnap.data()
|
|
1723
2156
|
};
|
|
1724
2157
|
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Exports subcategories to CSV string, suitable for Excel/Sheets.
|
|
2160
|
+
* Includes headers and optional UTF-8 BOM.
|
|
2161
|
+
* By default exports only active subcategories (set includeInactive to true to export all).
|
|
2162
|
+
*/
|
|
2163
|
+
async exportToCsv(options) {
|
|
2164
|
+
var _a, _b;
|
|
2165
|
+
const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
|
|
2166
|
+
const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
|
|
2167
|
+
const headers = [
|
|
2168
|
+
"id",
|
|
2169
|
+
"name",
|
|
2170
|
+
"categoryId",
|
|
2171
|
+
"description",
|
|
2172
|
+
"isActive"
|
|
2173
|
+
];
|
|
2174
|
+
const rows = [];
|
|
2175
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
2176
|
+
const PAGE_SIZE = 1e3;
|
|
2177
|
+
let cursor;
|
|
2178
|
+
const constraints = [];
|
|
2179
|
+
if (!includeInactive) {
|
|
2180
|
+
constraints.push(where8("isActive", "==", true));
|
|
2181
|
+
}
|
|
2182
|
+
constraints.push(orderBy8("name"));
|
|
2183
|
+
while (true) {
|
|
2184
|
+
const queryConstraints = [...constraints, limit7(PAGE_SIZE)];
|
|
2185
|
+
if (cursor) queryConstraints.push(startAfter6(cursor));
|
|
2186
|
+
const q = query8(
|
|
2187
|
+
collectionGroup2(this.db, SUBCATEGORIES_COLLECTION),
|
|
2188
|
+
...queryConstraints
|
|
2189
|
+
);
|
|
2190
|
+
const snapshot = await getDocs8(q);
|
|
2191
|
+
if (snapshot.empty) break;
|
|
2192
|
+
for (const d of snapshot.docs) {
|
|
2193
|
+
const subcategory = { id: d.id, ...d.data() };
|
|
2194
|
+
rows.push(this.subcategoryToCsvRow(subcategory));
|
|
2195
|
+
}
|
|
2196
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
2197
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
2198
|
+
}
|
|
2199
|
+
const csvBody = rows.join("\r\n");
|
|
2200
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
2201
|
+
}
|
|
2202
|
+
subcategoryToCsvRow(subcategory) {
|
|
2203
|
+
var _a, _b, _c, _d, _e;
|
|
2204
|
+
const values = [
|
|
2205
|
+
(_a = subcategory.id) != null ? _a : "",
|
|
2206
|
+
(_b = subcategory.name) != null ? _b : "",
|
|
2207
|
+
(_c = subcategory.categoryId) != null ? _c : "",
|
|
2208
|
+
(_d = subcategory.description) != null ? _d : "",
|
|
2209
|
+
String((_e = subcategory.isActive) != null ? _e : "")
|
|
2210
|
+
];
|
|
2211
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
2212
|
+
}
|
|
2213
|
+
formatDateIso(value) {
|
|
2214
|
+
if (value instanceof Date) return value.toISOString();
|
|
2215
|
+
if (value && typeof value.toDate === "function") {
|
|
2216
|
+
const d = value.toDate();
|
|
2217
|
+
return d instanceof Date ? d.toISOString() : String(value);
|
|
2218
|
+
}
|
|
2219
|
+
return String(value != null ? value : "");
|
|
2220
|
+
}
|
|
2221
|
+
formatCsvValue(value) {
|
|
2222
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
2223
|
+
const escaped = str.replace(/"/g, '""');
|
|
2224
|
+
return `"${escaped}"`;
|
|
2225
|
+
}
|
|
1725
2226
|
};
|
|
1726
2227
|
|
|
1727
2228
|
// src/backoffice/services/technology.service.ts
|
|
@@ -1732,13 +2233,14 @@ import {
|
|
|
1732
2233
|
getDoc as getDoc9,
|
|
1733
2234
|
getDocs as getDocs9,
|
|
1734
2235
|
limit as limit8,
|
|
1735
|
-
orderBy as
|
|
2236
|
+
orderBy as orderBy9,
|
|
1736
2237
|
query as query9,
|
|
1737
2238
|
startAfter as startAfter7,
|
|
1738
2239
|
updateDoc as updateDoc9,
|
|
1739
2240
|
where as where9,
|
|
1740
|
-
arrayUnion,
|
|
1741
|
-
arrayRemove
|
|
2241
|
+
arrayUnion as arrayUnion2,
|
|
2242
|
+
arrayRemove as arrayRemove2,
|
|
2243
|
+
writeBatch
|
|
1742
2244
|
} from "firebase/firestore";
|
|
1743
2245
|
|
|
1744
2246
|
// src/backoffice/types/static/certification.types.ts
|
|
@@ -1845,7 +2347,7 @@ var TechnologyService = class extends BaseService {
|
|
|
1845
2347
|
const { active = true, limit: queryLimit = 10, lastVisible } = options;
|
|
1846
2348
|
const constraints = [
|
|
1847
2349
|
where9("isActive", "==", active),
|
|
1848
|
-
|
|
2350
|
+
orderBy9("name"),
|
|
1849
2351
|
queryLimit ? limit8(queryLimit) : void 0,
|
|
1850
2352
|
lastVisible ? startAfter7(lastVisible) : void 0
|
|
1851
2353
|
].filter((c) => !!c);
|
|
@@ -1871,7 +2373,7 @@ var TechnologyService = class extends BaseService {
|
|
|
1871
2373
|
const constraints = [
|
|
1872
2374
|
where9("categoryId", "==", categoryId),
|
|
1873
2375
|
where9("isActive", "==", active),
|
|
1874
|
-
|
|
2376
|
+
orderBy9("name"),
|
|
1875
2377
|
queryLimit ? limit8(queryLimit) : void 0,
|
|
1876
2378
|
lastVisible ? startAfter7(lastVisible) : void 0
|
|
1877
2379
|
].filter((c) => !!c);
|
|
@@ -1897,7 +2399,7 @@ var TechnologyService = class extends BaseService {
|
|
|
1897
2399
|
const constraints = [
|
|
1898
2400
|
where9("subcategoryId", "==", subcategoryId),
|
|
1899
2401
|
where9("isActive", "==", active),
|
|
1900
|
-
|
|
2402
|
+
orderBy9("name"),
|
|
1901
2403
|
queryLimit ? limit8(queryLimit) : void 0,
|
|
1902
2404
|
lastVisible ? startAfter7(lastVisible) : void 0
|
|
1903
2405
|
].filter((c) => !!c);
|
|
@@ -1927,7 +2429,18 @@ var TechnologyService = class extends BaseService {
|
|
|
1927
2429
|
});
|
|
1928
2430
|
updateData.updatedAt = /* @__PURE__ */ new Date();
|
|
1929
2431
|
const docRef = doc9(this.technologiesRef, id);
|
|
2432
|
+
const beforeTech = await this.getById(id);
|
|
1930
2433
|
await updateDoc9(docRef, updateData);
|
|
2434
|
+
const categoryChanged = beforeTech && updateData.categoryId && beforeTech.categoryId !== updateData.categoryId;
|
|
2435
|
+
const subcategoryChanged = beforeTech && updateData.subcategoryId && beforeTech.subcategoryId !== updateData.subcategoryId;
|
|
2436
|
+
const nameChanged = beforeTech && updateData.name && beforeTech.name !== updateData.name;
|
|
2437
|
+
if (categoryChanged || subcategoryChanged || nameChanged) {
|
|
2438
|
+
await this.updateProductsInSubcollection(id, {
|
|
2439
|
+
categoryId: updateData.categoryId,
|
|
2440
|
+
subcategoryId: updateData.subcategoryId,
|
|
2441
|
+
technologyName: updateData.name
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
1931
2444
|
return this.getById(id);
|
|
1932
2445
|
}
|
|
1933
2446
|
/**
|
|
@@ -1968,7 +2481,7 @@ var TechnologyService = class extends BaseService {
|
|
|
1968
2481
|
const docRef = doc9(this.technologiesRef, technologyId);
|
|
1969
2482
|
const requirementType = requirement.type === "pre" ? "requirements.pre" : "requirements.post";
|
|
1970
2483
|
await updateDoc9(docRef, {
|
|
1971
|
-
[requirementType]:
|
|
2484
|
+
[requirementType]: arrayUnion2(requirement),
|
|
1972
2485
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1973
2486
|
});
|
|
1974
2487
|
return this.getById(technologyId);
|
|
@@ -1983,7 +2496,7 @@ var TechnologyService = class extends BaseService {
|
|
|
1983
2496
|
const docRef = doc9(this.technologiesRef, technologyId);
|
|
1984
2497
|
const requirementType = requirement.type === "pre" ? "requirements.pre" : "requirements.post";
|
|
1985
2498
|
await updateDoc9(docRef, {
|
|
1986
|
-
[requirementType]:
|
|
2499
|
+
[requirementType]: arrayRemove2(requirement),
|
|
1987
2500
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1988
2501
|
});
|
|
1989
2502
|
return this.getById(technologyId);
|
|
@@ -2022,7 +2535,7 @@ var TechnologyService = class extends BaseService {
|
|
|
2022
2535
|
async addBlockingCondition(technologyId, condition) {
|
|
2023
2536
|
const docRef = doc9(this.technologiesRef, technologyId);
|
|
2024
2537
|
await updateDoc9(docRef, {
|
|
2025
|
-
blockingConditions:
|
|
2538
|
+
blockingConditions: arrayUnion2(condition),
|
|
2026
2539
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2027
2540
|
});
|
|
2028
2541
|
return this.getById(technologyId);
|
|
@@ -2036,7 +2549,7 @@ var TechnologyService = class extends BaseService {
|
|
|
2036
2549
|
async removeBlockingCondition(technologyId, condition) {
|
|
2037
2550
|
const docRef = doc9(this.technologiesRef, technologyId);
|
|
2038
2551
|
await updateDoc9(docRef, {
|
|
2039
|
-
blockingConditions:
|
|
2552
|
+
blockingConditions: arrayRemove2(condition),
|
|
2040
2553
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2041
2554
|
});
|
|
2042
2555
|
return this.getById(technologyId);
|
|
@@ -2315,7 +2828,7 @@ var TechnologyService = class extends BaseService {
|
|
|
2315
2828
|
collection9(this.db, TECHNOLOGIES_COLLECTION),
|
|
2316
2829
|
where9("isActive", "==", true),
|
|
2317
2830
|
where9("subcategoryId", "==", subcategoryId),
|
|
2318
|
-
|
|
2831
|
+
orderBy9("name")
|
|
2319
2832
|
);
|
|
2320
2833
|
const snapshot = await getDocs9(q);
|
|
2321
2834
|
return snapshot.docs.map(
|
|
@@ -2336,7 +2849,7 @@ var TechnologyService = class extends BaseService {
|
|
|
2336
2849
|
where9("isActive", "==", true),
|
|
2337
2850
|
where9("categoryId", "==", categoryId),
|
|
2338
2851
|
where9("subcategoryId", "==", subcategoryId),
|
|
2339
|
-
|
|
2852
|
+
orderBy9("name")
|
|
2340
2853
|
);
|
|
2341
2854
|
const snapshot = await getDocs9(q);
|
|
2342
2855
|
return snapshot.docs.map(
|
|
@@ -2353,7 +2866,7 @@ var TechnologyService = class extends BaseService {
|
|
|
2353
2866
|
const q = query9(
|
|
2354
2867
|
collection9(this.db, TECHNOLOGIES_COLLECTION),
|
|
2355
2868
|
where9("isActive", "==", true),
|
|
2356
|
-
|
|
2869
|
+
orderBy9("name")
|
|
2357
2870
|
);
|
|
2358
2871
|
const snapshot = await getDocs9(q);
|
|
2359
2872
|
return snapshot.docs.map(
|
|
@@ -2363,12 +2876,231 @@ var TechnologyService = class extends BaseService {
|
|
|
2363
2876
|
})
|
|
2364
2877
|
);
|
|
2365
2878
|
}
|
|
2879
|
+
// ==========================================
|
|
2880
|
+
// NEW METHODS: Product assignment management
|
|
2881
|
+
// ==========================================
|
|
2882
|
+
/**
|
|
2883
|
+
* Assigns multiple products to a technology
|
|
2884
|
+
* Updates each product's assignedTechnologyIds array
|
|
2885
|
+
*/
|
|
2886
|
+
async assignProducts(technologyId, productIds) {
|
|
2887
|
+
const batch = writeBatch(this.db);
|
|
2888
|
+
for (const productId of productIds) {
|
|
2889
|
+
const productRef = doc9(this.db, PRODUCTS_COLLECTION, productId);
|
|
2890
|
+
batch.update(productRef, {
|
|
2891
|
+
assignedTechnologyIds: arrayUnion2(technologyId),
|
|
2892
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
await batch.commit();
|
|
2896
|
+
}
|
|
2897
|
+
/**
|
|
2898
|
+
* Unassigns multiple products from a technology
|
|
2899
|
+
* Updates each product's assignedTechnologyIds array
|
|
2900
|
+
*/
|
|
2901
|
+
async unassignProducts(technologyId, productIds) {
|
|
2902
|
+
const batch = writeBatch(this.db);
|
|
2903
|
+
for (const productId of productIds) {
|
|
2904
|
+
const productRef = doc9(this.db, PRODUCTS_COLLECTION, productId);
|
|
2905
|
+
batch.update(productRef, {
|
|
2906
|
+
assignedTechnologyIds: arrayRemove2(technologyId),
|
|
2907
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
await batch.commit();
|
|
2911
|
+
}
|
|
2912
|
+
/**
|
|
2913
|
+
* Gets products assigned to a specific technology
|
|
2914
|
+
* Reads from top-level collection for immediate consistency (Cloud Functions may lag)
|
|
2915
|
+
*/
|
|
2916
|
+
async getAssignedProducts(technologyId) {
|
|
2917
|
+
const q = query9(
|
|
2918
|
+
collection9(this.db, PRODUCTS_COLLECTION),
|
|
2919
|
+
where9("assignedTechnologyIds", "array-contains", technologyId),
|
|
2920
|
+
where9("isActive", "==", true),
|
|
2921
|
+
orderBy9("name")
|
|
2922
|
+
);
|
|
2923
|
+
const snapshot = await getDocs9(q);
|
|
2924
|
+
return snapshot.docs.map(
|
|
2925
|
+
(doc11) => ({
|
|
2926
|
+
id: doc11.id,
|
|
2927
|
+
...doc11.data()
|
|
2928
|
+
})
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Gets products NOT assigned to a specific technology
|
|
2933
|
+
*/
|
|
2934
|
+
async getUnassignedProducts(technologyId) {
|
|
2935
|
+
const q = query9(
|
|
2936
|
+
collection9(this.db, PRODUCTS_COLLECTION),
|
|
2937
|
+
where9("isActive", "==", true),
|
|
2938
|
+
orderBy9("name")
|
|
2939
|
+
);
|
|
2940
|
+
const snapshot = await getDocs9(q);
|
|
2941
|
+
const allProducts = snapshot.docs.map(
|
|
2942
|
+
(doc11) => ({
|
|
2943
|
+
id: doc11.id,
|
|
2944
|
+
...doc11.data()
|
|
2945
|
+
})
|
|
2946
|
+
);
|
|
2947
|
+
return allProducts.filter(
|
|
2948
|
+
(product) => {
|
|
2949
|
+
var _a;
|
|
2950
|
+
return !((_a = product.assignedTechnologyIds) == null ? void 0 : _a.includes(technologyId));
|
|
2951
|
+
}
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Gets product assignment statistics for a technology
|
|
2956
|
+
*/
|
|
2957
|
+
async getProductStats(technologyId) {
|
|
2958
|
+
const products = await this.getAssignedProducts(technologyId);
|
|
2959
|
+
const byBrand = {};
|
|
2960
|
+
products.forEach((product) => {
|
|
2961
|
+
byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
|
|
2962
|
+
});
|
|
2963
|
+
return {
|
|
2964
|
+
totalAssigned: products.length,
|
|
2965
|
+
byBrand
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Updates products in technology subcollection when technology metadata changes
|
|
2970
|
+
* @param technologyId - ID of the technology
|
|
2971
|
+
* @param updates - Fields to update (categoryId, subcategoryId, technologyName)
|
|
2972
|
+
*/
|
|
2973
|
+
async updateProductsInSubcollection(technologyId, updates) {
|
|
2974
|
+
const productsRef = collection9(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
2975
|
+
const productsSnapshot = await getDocs9(productsRef);
|
|
2976
|
+
if (productsSnapshot.empty) {
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
const batch = writeBatch(this.db);
|
|
2980
|
+
for (const productDoc of productsSnapshot.docs) {
|
|
2981
|
+
const productRef = productDoc.ref;
|
|
2982
|
+
const updateFields = {};
|
|
2983
|
+
if (updates.categoryId !== void 0) {
|
|
2984
|
+
updateFields.categoryId = updates.categoryId;
|
|
2985
|
+
}
|
|
2986
|
+
if (updates.subcategoryId !== void 0) {
|
|
2987
|
+
updateFields.subcategoryId = updates.subcategoryId;
|
|
2988
|
+
}
|
|
2989
|
+
if (updates.technologyName !== void 0) {
|
|
2990
|
+
updateFields.technologyName = updates.technologyName;
|
|
2991
|
+
}
|
|
2992
|
+
if (Object.keys(updateFields).length > 0) {
|
|
2993
|
+
batch.update(productRef, updateFields);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
await batch.commit();
|
|
2997
|
+
}
|
|
2998
|
+
/**
|
|
2999
|
+
* Exports technologies to CSV string, suitable for Excel/Sheets.
|
|
3000
|
+
* Includes headers and optional UTF-8 BOM.
|
|
3001
|
+
* By default exports only active technologies (set includeInactive to true to export all).
|
|
3002
|
+
* Includes product names from subcollections.
|
|
3003
|
+
*/
|
|
3004
|
+
async exportToCsv(options) {
|
|
3005
|
+
var _a, _b;
|
|
3006
|
+
const includeInactive = (_a = options == null ? void 0 : options.includeInactive) != null ? _a : false;
|
|
3007
|
+
const includeBom = (_b = options == null ? void 0 : options.includeBom) != null ? _b : true;
|
|
3008
|
+
const headers = [
|
|
3009
|
+
"id",
|
|
3010
|
+
"name",
|
|
3011
|
+
"description",
|
|
3012
|
+
"family",
|
|
3013
|
+
"categoryId",
|
|
3014
|
+
"subcategoryId",
|
|
3015
|
+
"technicalDetails",
|
|
3016
|
+
"requirements_pre",
|
|
3017
|
+
"requirements_post",
|
|
3018
|
+
"blockingConditions",
|
|
3019
|
+
"contraindications",
|
|
3020
|
+
"benefits",
|
|
3021
|
+
"certificationMinimumLevel",
|
|
3022
|
+
"certificationRequiredSpecialties",
|
|
3023
|
+
"documentationTemplateIds",
|
|
3024
|
+
"productNames",
|
|
3025
|
+
"isActive"
|
|
3026
|
+
];
|
|
3027
|
+
const rows = [];
|
|
3028
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
3029
|
+
const PAGE_SIZE = 1e3;
|
|
3030
|
+
let cursor;
|
|
3031
|
+
const constraints = [];
|
|
3032
|
+
if (!includeInactive) {
|
|
3033
|
+
constraints.push(where9("isActive", "==", true));
|
|
3034
|
+
}
|
|
3035
|
+
constraints.push(orderBy9("name"));
|
|
3036
|
+
while (true) {
|
|
3037
|
+
const queryConstraints = [...constraints, limit8(PAGE_SIZE)];
|
|
3038
|
+
if (cursor) queryConstraints.push(startAfter7(cursor));
|
|
3039
|
+
const q = query9(this.technologiesRef, ...queryConstraints);
|
|
3040
|
+
const snapshot = await getDocs9(q);
|
|
3041
|
+
if (snapshot.empty) break;
|
|
3042
|
+
for (const d of snapshot.docs) {
|
|
3043
|
+
const technology = { id: d.id, ...d.data() };
|
|
3044
|
+
const productNames = await this.getProductNamesForTechnology(technology.id);
|
|
3045
|
+
rows.push(this.technologyToCsvRow(technology, productNames));
|
|
3046
|
+
}
|
|
3047
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
3048
|
+
if (snapshot.size < PAGE_SIZE) break;
|
|
3049
|
+
}
|
|
3050
|
+
const csvBody = rows.join("\r\n");
|
|
3051
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Gets product names from the technology's product subcollection
|
|
3055
|
+
*/
|
|
3056
|
+
async getProductNamesForTechnology(technologyId) {
|
|
3057
|
+
try {
|
|
3058
|
+
const productsRef = collection9(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
3059
|
+
const q = query9(productsRef, where9("isActive", "==", true));
|
|
3060
|
+
const snapshot = await getDocs9(q);
|
|
3061
|
+
return snapshot.docs.map((doc11) => {
|
|
3062
|
+
const product = doc11.data();
|
|
3063
|
+
return product.name || "";
|
|
3064
|
+
}).filter((name) => name);
|
|
3065
|
+
} catch (error) {
|
|
3066
|
+
console.error(`Error fetching products for technology ${technologyId}:`, error);
|
|
3067
|
+
return [];
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
technologyToCsvRow(technology, productNames = []) {
|
|
3071
|
+
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;
|
|
3072
|
+
const values = [
|
|
3073
|
+
(_a = technology.id) != null ? _a : "",
|
|
3074
|
+
(_b = technology.name) != null ? _b : "",
|
|
3075
|
+
(_c = technology.description) != null ? _c : "",
|
|
3076
|
+
(_d = technology.family) != null ? _d : "",
|
|
3077
|
+
(_e = technology.categoryId) != null ? _e : "",
|
|
3078
|
+
(_f = technology.subcategoryId) != null ? _f : "",
|
|
3079
|
+
(_g = technology.technicalDetails) != null ? _g : "",
|
|
3080
|
+
(_j = (_i = (_h = technology.requirements) == null ? void 0 : _h.pre) == null ? void 0 : _i.map((r) => r.name).join(";")) != null ? _j : "",
|
|
3081
|
+
(_m = (_l = (_k = technology.requirements) == null ? void 0 : _k.post) == null ? void 0 : _l.map((r) => r.name).join(";")) != null ? _m : "",
|
|
3082
|
+
(_o = (_n = technology.blockingConditions) == null ? void 0 : _n.join(";")) != null ? _o : "",
|
|
3083
|
+
(_q = (_p = technology.contraindications) == null ? void 0 : _p.map((c) => c.name).join(";")) != null ? _q : "",
|
|
3084
|
+
(_s = (_r = technology.benefits) == null ? void 0 : _r.map((b) => b.name).join(";")) != null ? _s : "",
|
|
3085
|
+
(_u = (_t = technology.certificationRequirement) == null ? void 0 : _t.minimumLevel) != null ? _u : "",
|
|
3086
|
+
(_x = (_w = (_v = technology.certificationRequirement) == null ? void 0 : _v.requiredSpecialties) == null ? void 0 : _w.join(";")) != null ? _x : "",
|
|
3087
|
+
(_z = (_y = technology.documentationTemplates) == null ? void 0 : _y.map((t) => t.templateId).join(";")) != null ? _z : "",
|
|
3088
|
+
productNames.join(";"),
|
|
3089
|
+
String((_A = technology.isActive) != null ? _A : "")
|
|
3090
|
+
];
|
|
3091
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
3092
|
+
}
|
|
3093
|
+
formatCsvValue(value) {
|
|
3094
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
3095
|
+
const escaped = str.replace(/"/g, '""');
|
|
3096
|
+
return `"${escaped}"`;
|
|
3097
|
+
}
|
|
2366
3098
|
};
|
|
2367
3099
|
|
|
2368
3100
|
// src/backoffice/services/constants.service.ts
|
|
2369
3101
|
import {
|
|
2370
|
-
arrayRemove as
|
|
2371
|
-
arrayUnion as
|
|
3102
|
+
arrayRemove as arrayRemove3,
|
|
3103
|
+
arrayUnion as arrayUnion3,
|
|
2372
3104
|
doc as doc10,
|
|
2373
3105
|
getDoc as getDoc10,
|
|
2374
3106
|
setDoc as setDoc5,
|
|
@@ -2436,7 +3168,7 @@ var ConstantsService = class extends BaseService {
|
|
|
2436
3168
|
await setDoc5(this.treatmentBenefitsDocRef, { benefits: [newBenefit] });
|
|
2437
3169
|
} else {
|
|
2438
3170
|
await updateDoc10(this.treatmentBenefitsDocRef, {
|
|
2439
|
-
benefits:
|
|
3171
|
+
benefits: arrayUnion3(newBenefit)
|
|
2440
3172
|
});
|
|
2441
3173
|
}
|
|
2442
3174
|
return newBenefit;
|
|
@@ -2490,7 +3222,7 @@ var ConstantsService = class extends BaseService {
|
|
|
2490
3222
|
return;
|
|
2491
3223
|
}
|
|
2492
3224
|
await updateDoc10(this.treatmentBenefitsDocRef, {
|
|
2493
|
-
benefits:
|
|
3225
|
+
benefits: arrayRemove3(benefitToRemove)
|
|
2494
3226
|
});
|
|
2495
3227
|
}
|
|
2496
3228
|
// =================================================================
|
|
@@ -2543,7 +3275,7 @@ var ConstantsService = class extends BaseService {
|
|
|
2543
3275
|
});
|
|
2544
3276
|
} else {
|
|
2545
3277
|
await updateDoc10(this.contraindicationsDocRef, {
|
|
2546
|
-
contraindications:
|
|
3278
|
+
contraindications: arrayUnion3(newContraindication)
|
|
2547
3279
|
});
|
|
2548
3280
|
}
|
|
2549
3281
|
return newContraindication;
|
|
@@ -2599,9 +3331,69 @@ var ConstantsService = class extends BaseService {
|
|
|
2599
3331
|
return;
|
|
2600
3332
|
}
|
|
2601
3333
|
await updateDoc10(this.contraindicationsDocRef, {
|
|
2602
|
-
contraindications:
|
|
3334
|
+
contraindications: arrayRemove3(toRemove)
|
|
2603
3335
|
});
|
|
2604
3336
|
}
|
|
3337
|
+
// =================================================================
|
|
3338
|
+
// CSV Export Methods
|
|
3339
|
+
// =================================================================
|
|
3340
|
+
/**
|
|
3341
|
+
* Exports treatment benefits to CSV string, suitable for Excel/Sheets.
|
|
3342
|
+
* Includes headers and optional UTF-8 BOM.
|
|
3343
|
+
*/
|
|
3344
|
+
async exportBenefitsToCsv(options) {
|
|
3345
|
+
var _a;
|
|
3346
|
+
const includeBom = (_a = options == null ? void 0 : options.includeBom) != null ? _a : true;
|
|
3347
|
+
const headers = ["id", "name", "description"];
|
|
3348
|
+
const rows = [];
|
|
3349
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
3350
|
+
const benefits = await this.getAllBenefitsForFilter();
|
|
3351
|
+
for (const benefit of benefits) {
|
|
3352
|
+
rows.push(this.benefitToCsvRow(benefit));
|
|
3353
|
+
}
|
|
3354
|
+
const csvBody = rows.join("\r\n");
|
|
3355
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
3356
|
+
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Exports contraindications to CSV string, suitable for Excel/Sheets.
|
|
3359
|
+
* Includes headers and optional UTF-8 BOM.
|
|
3360
|
+
*/
|
|
3361
|
+
async exportContraindicationsToCsv(options) {
|
|
3362
|
+
var _a;
|
|
3363
|
+
const includeBom = (_a = options == null ? void 0 : options.includeBom) != null ? _a : true;
|
|
3364
|
+
const headers = ["id", "name", "description"];
|
|
3365
|
+
const rows = [];
|
|
3366
|
+
rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
|
|
3367
|
+
const contraindications = await this.getAllContraindicationsForFilter();
|
|
3368
|
+
for (const contraindication of contraindications) {
|
|
3369
|
+
rows.push(this.contraindicationToCsvRow(contraindication));
|
|
3370
|
+
}
|
|
3371
|
+
const csvBody = rows.join("\r\n");
|
|
3372
|
+
return includeBom ? "\uFEFF" + csvBody : csvBody;
|
|
3373
|
+
}
|
|
3374
|
+
benefitToCsvRow(benefit) {
|
|
3375
|
+
var _a, _b, _c;
|
|
3376
|
+
const values = [
|
|
3377
|
+
(_a = benefit.id) != null ? _a : "",
|
|
3378
|
+
(_b = benefit.name) != null ? _b : "",
|
|
3379
|
+
(_c = benefit.description) != null ? _c : ""
|
|
3380
|
+
];
|
|
3381
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
3382
|
+
}
|
|
3383
|
+
contraindicationToCsvRow(contraindication) {
|
|
3384
|
+
var _a, _b, _c;
|
|
3385
|
+
const values = [
|
|
3386
|
+
(_a = contraindication.id) != null ? _a : "",
|
|
3387
|
+
(_b = contraindication.name) != null ? _b : "",
|
|
3388
|
+
(_c = contraindication.description) != null ? _c : ""
|
|
3389
|
+
];
|
|
3390
|
+
return values.map((v) => this.formatCsvValue(v)).join(",");
|
|
3391
|
+
}
|
|
3392
|
+
formatCsvValue(value) {
|
|
3393
|
+
const str = value === null || value === void 0 ? "" : String(value);
|
|
3394
|
+
const escaped = str.replace(/"/g, '""');
|
|
3395
|
+
return `"${escaped}"`;
|
|
3396
|
+
}
|
|
2605
3397
|
};
|
|
2606
3398
|
|
|
2607
3399
|
// src/backoffice/types/static/blocking-condition.types.ts
|