@blackcode_sa/metaestetics-api 1.12.43 → 1.12.45
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 +6 -12
- package/dist/admin/index.d.ts +6 -12
- package/dist/backoffice/index.d.mts +18 -275
- package/dist/backoffice/index.d.ts +18 -275
- package/dist/backoffice/index.js +14 -802
- package/dist/backoffice/index.mjs +38 -830
- package/dist/index.d.mts +10 -255
- package/dist/index.d.ts +10 -255
- package/dist/index.js +32 -750
- package/dist/index.mjs +40 -761
- package/package.json +1 -1
- package/src/backoffice/services/brand.service.ts +0 -86
- package/src/backoffice/services/category.service.ts +0 -84
- package/src/backoffice/services/constants.service.ts +0 -77
- package/src/backoffice/services/product.service.ts +18 -316
- package/src/backoffice/services/requirement.service.ts +0 -76
- package/src/backoffice/services/subcategory.service.ts +0 -87
- package/src/backoffice/services/technology.service.ts +0 -289
- package/src/backoffice/types/product.types.ts +6 -116
- package/src/services/appointment/utils/zone-management.utils.ts +17 -3
- package/src/backoffice/services/migrate-products.ts +0 -116
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
query,
|
|
8
8
|
updateDoc,
|
|
9
9
|
where,
|
|
10
|
-
orderBy,
|
|
11
10
|
} from "firebase/firestore";
|
|
12
11
|
import {
|
|
13
12
|
Requirement,
|
|
@@ -157,79 +156,4 @@ export class RequirementService extends BaseService {
|
|
|
157
156
|
...docSnap.data(),
|
|
158
157
|
} as Requirement;
|
|
159
158
|
}
|
|
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
|
-
}
|
|
235
159
|
}
|
|
@@ -305,91 +305,4 @@ 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
|
-
}
|
|
395
308
|
}
|
|
@@ -15,8 +15,6 @@ import {
|
|
|
15
15
|
arrayUnion,
|
|
16
16
|
arrayRemove,
|
|
17
17
|
Firestore,
|
|
18
|
-
writeBatch,
|
|
19
|
-
QueryConstraint,
|
|
20
18
|
} from 'firebase/firestore';
|
|
21
19
|
import { Technology, TECHNOLOGIES_COLLECTION, ITechnologyService } from '../types/technology.types';
|
|
22
20
|
import { Requirement, RequirementType } from '../types/requirement.types';
|
|
@@ -31,7 +29,6 @@ import {
|
|
|
31
29
|
import { BaseService } from '../../services/base.service';
|
|
32
30
|
import { ProcedureFamily } from '../types/static/procedure-family.types';
|
|
33
31
|
import { Practitioner, PractitionerCertification } from '../../types/practitioner';
|
|
34
|
-
import { Product, PRODUCTS_COLLECTION } from '../types/product.types';
|
|
35
32
|
|
|
36
33
|
/**
|
|
37
34
|
* Default vrednosti za sertifikaciju
|
|
@@ -243,25 +240,7 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
243
240
|
updateData.updatedAt = new Date();
|
|
244
241
|
|
|
245
242
|
const docRef = doc(this.technologiesRef, id);
|
|
246
|
-
|
|
247
|
-
// Get the technology before update to check what changed
|
|
248
|
-
const beforeTech = await this.getById(id);
|
|
249
|
-
|
|
250
243
|
await updateDoc(docRef, updateData);
|
|
251
|
-
|
|
252
|
-
// If categoryId, subcategoryId, or name changed, update all products in subcollection
|
|
253
|
-
const categoryChanged = beforeTech && updateData.categoryId && beforeTech.categoryId !== updateData.categoryId;
|
|
254
|
-
const subcategoryChanged = beforeTech && updateData.subcategoryId && beforeTech.subcategoryId !== updateData.subcategoryId;
|
|
255
|
-
const nameChanged = beforeTech && updateData.name && beforeTech.name !== updateData.name;
|
|
256
|
-
|
|
257
|
-
if (categoryChanged || subcategoryChanged || nameChanged) {
|
|
258
|
-
await this.updateProductsInSubcollection(id, {
|
|
259
|
-
categoryId: updateData.categoryId,
|
|
260
|
-
subcategoryId: updateData.subcategoryId,
|
|
261
|
-
technologyName: updateData.name,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
244
|
return this.getById(id);
|
|
266
245
|
}
|
|
267
246
|
|
|
@@ -799,272 +778,4 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
799
778
|
} as Technology),
|
|
800
779
|
);
|
|
801
780
|
}
|
|
802
|
-
|
|
803
|
-
// ==========================================
|
|
804
|
-
// NEW METHODS: Product assignment management
|
|
805
|
-
// ==========================================
|
|
806
|
-
|
|
807
|
-
/**
|
|
808
|
-
* Assigns multiple products to a technology
|
|
809
|
-
* Updates each product's assignedTechnologyIds array
|
|
810
|
-
*/
|
|
811
|
-
async assignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
812
|
-
const batch = writeBatch(this.db);
|
|
813
|
-
|
|
814
|
-
for (const productId of productIds) {
|
|
815
|
-
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
816
|
-
batch.update(productRef, {
|
|
817
|
-
assignedTechnologyIds: arrayUnion(technologyId),
|
|
818
|
-
updatedAt: new Date(),
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
await batch.commit();
|
|
823
|
-
// Cloud Function will handle syncing to subcollections
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
* Unassigns multiple products from a technology
|
|
828
|
-
* Updates each product's assignedTechnologyIds array
|
|
829
|
-
*/
|
|
830
|
-
async unassignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
831
|
-
const batch = writeBatch(this.db);
|
|
832
|
-
|
|
833
|
-
for (const productId of productIds) {
|
|
834
|
-
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
835
|
-
batch.update(productRef, {
|
|
836
|
-
assignedTechnologyIds: arrayRemove(technologyId),
|
|
837
|
-
updatedAt: new Date(),
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
await batch.commit();
|
|
842
|
-
// Cloud Function will handle removing from subcollections
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* Gets products assigned to a specific technology
|
|
847
|
-
* Reads from top-level collection for immediate consistency (Cloud Functions may lag)
|
|
848
|
-
*/
|
|
849
|
-
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
850
|
-
const q = query(
|
|
851
|
-
collection(this.db, PRODUCTS_COLLECTION),
|
|
852
|
-
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
853
|
-
where('isActive', '==', true),
|
|
854
|
-
orderBy('name'),
|
|
855
|
-
);
|
|
856
|
-
const snapshot = await getDocs(q);
|
|
857
|
-
|
|
858
|
-
return snapshot.docs.map(
|
|
859
|
-
doc =>
|
|
860
|
-
({
|
|
861
|
-
id: doc.id,
|
|
862
|
-
...doc.data(),
|
|
863
|
-
} as Product),
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* Gets products NOT assigned to a specific technology
|
|
869
|
-
*/
|
|
870
|
-
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
871
|
-
const q = query(
|
|
872
|
-
collection(this.db, PRODUCTS_COLLECTION),
|
|
873
|
-
where('isActive', '==', true),
|
|
874
|
-
orderBy('name'),
|
|
875
|
-
);
|
|
876
|
-
const snapshot = await getDocs(q);
|
|
877
|
-
|
|
878
|
-
const allProducts = snapshot.docs.map(
|
|
879
|
-
doc =>
|
|
880
|
-
({
|
|
881
|
-
id: doc.id,
|
|
882
|
-
...doc.data(),
|
|
883
|
-
} as Product),
|
|
884
|
-
);
|
|
885
|
-
|
|
886
|
-
// Filter out products already assigned to this technology
|
|
887
|
-
return allProducts.filter(product =>
|
|
888
|
-
!product.assignedTechnologyIds?.includes(technologyId)
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* Gets product assignment statistics for a technology
|
|
894
|
-
*/
|
|
895
|
-
async getProductStats(technologyId: string): Promise<{
|
|
896
|
-
totalAssigned: number;
|
|
897
|
-
byBrand: Record<string, number>;
|
|
898
|
-
}> {
|
|
899
|
-
const products = await this.getAssignedProducts(technologyId);
|
|
900
|
-
|
|
901
|
-
const byBrand: Record<string, number> = {};
|
|
902
|
-
products.forEach(product => {
|
|
903
|
-
byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
|
|
904
|
-
});
|
|
905
|
-
|
|
906
|
-
return {
|
|
907
|
-
totalAssigned: products.length,
|
|
908
|
-
byBrand,
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Updates products in technology subcollection when technology metadata changes
|
|
914
|
-
* @param technologyId - ID of the technology
|
|
915
|
-
* @param updates - Fields to update (categoryId, subcategoryId, technologyName)
|
|
916
|
-
*/
|
|
917
|
-
private async updateProductsInSubcollection(
|
|
918
|
-
technologyId: string,
|
|
919
|
-
updates: { categoryId?: string; subcategoryId?: string; technologyName?: string }
|
|
920
|
-
): Promise<void> {
|
|
921
|
-
const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
922
|
-
const productsSnapshot = await getDocs(productsRef);
|
|
923
|
-
|
|
924
|
-
if (productsSnapshot.empty) {
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const batch = writeBatch(this.db);
|
|
929
|
-
|
|
930
|
-
for (const productDoc of productsSnapshot.docs) {
|
|
931
|
-
const productRef = productDoc.ref;
|
|
932
|
-
const updateFields: any = {};
|
|
933
|
-
|
|
934
|
-
if (updates.categoryId !== undefined) {
|
|
935
|
-
updateFields.categoryId = updates.categoryId;
|
|
936
|
-
}
|
|
937
|
-
if (updates.subcategoryId !== undefined) {
|
|
938
|
-
updateFields.subcategoryId = updates.subcategoryId;
|
|
939
|
-
}
|
|
940
|
-
if (updates.technologyName !== undefined) {
|
|
941
|
-
updateFields.technologyName = updates.technologyName;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (Object.keys(updateFields).length > 0) {
|
|
945
|
-
batch.update(productRef, updateFields);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
await batch.commit();
|
|
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
|
-
}
|
|
1070
781
|
}
|
|
@@ -6,10 +6,9 @@ import type { ContraindicationDynamic } from './admin-constants.types';
|
|
|
6
6
|
*
|
|
7
7
|
* @property id - Unique identifier of the product
|
|
8
8
|
* @property name - Name of the product
|
|
9
|
-
* @property brandId - ID of the brand that manufactures this product
|
|
10
|
-
* @property brandName - Name of the brand (denormalized for display)
|
|
11
|
-
* @property assignedTechnologyIds - Array of technology IDs this product is assigned to
|
|
12
9
|
* @property description - Detailed description of the product and its purpose
|
|
10
|
+
* @property brandId - ID of the brand that manufactures this product
|
|
11
|
+
* @property technologyId - ID of the technology this product is used with
|
|
13
12
|
* @property technicalDetails - Technical details and specifications
|
|
14
13
|
* @property warnings - List of warnings related to product use
|
|
15
14
|
* @property dosage - Dosage information (if applicable)
|
|
@@ -25,11 +24,10 @@ export interface Product {
|
|
|
25
24
|
name: string;
|
|
26
25
|
brandId: string;
|
|
27
26
|
brandName: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Product details
|
|
27
|
+
technologyId: string;
|
|
28
|
+
technologyName: string;
|
|
29
|
+
categoryId: string;
|
|
30
|
+
subcategoryId: string;
|
|
33
31
|
createdAt: Date;
|
|
34
32
|
updatedAt: Date;
|
|
35
33
|
isActive: boolean;
|
|
@@ -40,18 +38,6 @@ export interface Product {
|
|
|
40
38
|
composition?: string;
|
|
41
39
|
indications?: string[];
|
|
42
40
|
contraindications?: ContraindicationDynamic[];
|
|
43
|
-
|
|
44
|
-
// LEGACY FIELDS: Only present in technology subcollections (/technologies/{id}/products/)
|
|
45
|
-
// These fields are synced by Cloud Functions for backward compatibility
|
|
46
|
-
// NOT stored in top-level /products collection
|
|
47
|
-
/** Present only in subcollections - synced from technology metadata */
|
|
48
|
-
technologyId?: string;
|
|
49
|
-
/** Present only in subcollections - synced from technology name */
|
|
50
|
-
technologyName?: string;
|
|
51
|
-
/** Present only in subcollections - synced from technology categoryId */
|
|
52
|
-
categoryId?: string;
|
|
53
|
-
/** Present only in subcollections - synced from technology subcategoryId */
|
|
54
|
-
subcategoryId?: string;
|
|
55
41
|
}
|
|
56
42
|
|
|
57
43
|
/**
|
|
@@ -61,97 +47,9 @@ export const PRODUCTS_COLLECTION = 'products';
|
|
|
61
47
|
|
|
62
48
|
/**
|
|
63
49
|
* Interface for the ProductService class
|
|
64
|
-
*
|
|
65
|
-
* NOTE: This interface maintains backward compatibility while adding new top-level collection methods.
|
|
66
|
-
* Old methods using technologyId are kept for existing code, new methods work with top-level collection.
|
|
67
50
|
*/
|
|
68
51
|
export interface IProductService {
|
|
69
|
-
// ==========================================
|
|
70
|
-
// NEW METHODS: Top-level collection (preferred)
|
|
71
|
-
// ==========================================
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Creates a new product in the top-level collection
|
|
75
|
-
* @param brandId - ID of the brand that manufactures this product
|
|
76
|
-
* @param product - Product data
|
|
77
|
-
* @param technologyIds - Optional array of technology IDs to assign this product to
|
|
78
|
-
*/
|
|
79
|
-
createTopLevel(
|
|
80
|
-
brandId: string,
|
|
81
|
-
product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'assignedTechnologyIds'>,
|
|
82
|
-
technologyIds?: string[],
|
|
83
|
-
): Promise<Product>;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Gets all products from the top-level collection
|
|
87
|
-
* @param options - Query options
|
|
88
|
-
*/
|
|
89
|
-
getAllTopLevel(options: {
|
|
90
|
-
rowsPerPage: number;
|
|
91
|
-
lastVisible?: any;
|
|
92
|
-
brandId?: string;
|
|
93
|
-
}): Promise<{ products: Product[]; lastVisible: any }>;
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Gets a product by ID from the top-level collection
|
|
97
|
-
* @param productId - ID of the product
|
|
98
|
-
*/
|
|
99
|
-
getByIdTopLevel(productId: string): Promise<Product | null>;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Updates a product in the top-level collection
|
|
103
|
-
* @param productId - ID of the product to update
|
|
104
|
-
* @param product - Updated product data
|
|
105
|
-
*/
|
|
106
|
-
updateTopLevel(
|
|
107
|
-
productId: string,
|
|
108
|
-
product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId'>>,
|
|
109
|
-
): Promise<Product | null>;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Deletes a product from the top-level collection (soft delete)
|
|
113
|
-
* @param productId - ID of the product to delete
|
|
114
|
-
*/
|
|
115
|
-
deleteTopLevel(productId: string): Promise<void>;
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Assigns a product to a technology
|
|
119
|
-
* @param productId - ID of the product
|
|
120
|
-
* @param technologyId - ID of the technology
|
|
121
|
-
*/
|
|
122
|
-
assignToTechnology(productId: string, technologyId: string): Promise<void>;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Unassigns a product from a technology
|
|
126
|
-
* @param productId - ID of the product
|
|
127
|
-
* @param technologyId - ID of the technology
|
|
128
|
-
*/
|
|
129
|
-
unassignFromTechnology(productId: string, technologyId: string): Promise<void>;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Gets products assigned to a specific technology
|
|
133
|
-
* @param technologyId - ID of the technology
|
|
134
|
-
*/
|
|
135
|
-
getAssignedProducts(technologyId: string): Promise<Product[]>;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Gets products NOT assigned to a specific technology
|
|
139
|
-
* @param technologyId - ID of the technology
|
|
140
|
-
*/
|
|
141
|
-
getUnassignedProducts(technologyId: string): Promise<Product[]>;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Gets all products for a brand
|
|
145
|
-
* @param brandId - ID of the brand
|
|
146
|
-
*/
|
|
147
|
-
getByBrand(brandId: string): Promise<Product[]>;
|
|
148
|
-
|
|
149
|
-
// ==========================================
|
|
150
|
-
// DEPRECATED METHODS: Kept for backward compatibility
|
|
151
|
-
// ==========================================
|
|
152
|
-
|
|
153
52
|
/**
|
|
154
|
-
* @deprecated Use createTopLevel instead
|
|
155
53
|
* Creates a new product
|
|
156
54
|
* @param technologyId - ID of the technology this product is used with
|
|
157
55
|
* @param brandId - ID of the brand that manufactures this product
|
|
@@ -164,7 +62,6 @@ export interface IProductService {
|
|
|
164
62
|
): Promise<Product>;
|
|
165
63
|
|
|
166
64
|
/**
|
|
167
|
-
* @deprecated Use getAllTopLevel instead
|
|
168
65
|
* Gets a paginated list of all products, with optional filters.
|
|
169
66
|
*/
|
|
170
67
|
getAll(options: {
|
|
@@ -176,7 +73,6 @@ export interface IProductService {
|
|
|
176
73
|
}): Promise<{ products: Product[]; lastVisible: any }>;
|
|
177
74
|
|
|
178
75
|
/**
|
|
179
|
-
* @deprecated Use alternative counting methods
|
|
180
76
|
* Gets the total count of active products, with optional filters.
|
|
181
77
|
*/
|
|
182
78
|
getProductsCount(options: {
|
|
@@ -186,7 +82,6 @@ export interface IProductService {
|
|
|
186
82
|
}): Promise<number>;
|
|
187
83
|
|
|
188
84
|
/**
|
|
189
|
-
* @deprecated Use alternative counting methods
|
|
190
85
|
* Gets counts of active products grouped by category, subcategory, and technology.
|
|
191
86
|
*/
|
|
192
87
|
getProductCounts(): Promise<{
|
|
@@ -196,21 +91,18 @@ export interface IProductService {
|
|
|
196
91
|
}>;
|
|
197
92
|
|
|
198
93
|
/**
|
|
199
|
-
* @deprecated Use getAssignedProducts instead
|
|
200
94
|
* Gets all products for a specific technology (non-paginated, for filters/dropdowns)
|
|
201
95
|
* @param technologyId - ID of the technology
|
|
202
96
|
*/
|
|
203
97
|
getAllByTechnology(technologyId: string): Promise<Product[]>;
|
|
204
98
|
|
|
205
99
|
/**
|
|
206
|
-
* @deprecated Use getByBrand instead
|
|
207
100
|
* Gets all products for a brand
|
|
208
101
|
* @param brandId - ID of the brand
|
|
209
102
|
*/
|
|
210
103
|
getAllByBrand(brandId: string): Promise<Product[]>;
|
|
211
104
|
|
|
212
105
|
/**
|
|
213
|
-
* @deprecated Use updateTopLevel instead
|
|
214
106
|
* Updates a product
|
|
215
107
|
* @param technologyId - ID of the technology
|
|
216
108
|
* @param productId - ID of the product to update
|
|
@@ -223,7 +115,6 @@ export interface IProductService {
|
|
|
223
115
|
): Promise<Product | null>;
|
|
224
116
|
|
|
225
117
|
/**
|
|
226
|
-
* @deprecated Use deleteTopLevel instead
|
|
227
118
|
* Deletes a product (soft delete)
|
|
228
119
|
* @param technologyId - ID of the technology
|
|
229
120
|
* @param productId - ID of the product to delete
|
|
@@ -231,7 +122,6 @@ export interface IProductService {
|
|
|
231
122
|
delete(technologyId: string, productId: string): Promise<void>;
|
|
232
123
|
|
|
233
124
|
/**
|
|
234
|
-
* @deprecated Use getByIdTopLevel instead
|
|
235
125
|
* Gets a product by ID
|
|
236
126
|
* @param technologyId - ID of the technology
|
|
237
127
|
* @param productId - ID of the product
|