@blackcode_sa/metaestetics-api 1.12.40 → 1.12.42
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 +182 -18
- package/dist/backoffice/index.d.ts +182 -18
- package/dist/backoffice/index.js +302 -14
- package/dist/backoffice/index.mjs +318 -27
- package/dist/index.d.mts +174 -10
- package/dist/index.d.ts +174 -10
- package/dist/index.js +329 -25
- package/dist/index.mjs +340 -33
- package/package.json +1 -1
- package/src/backoffice/services/migrate-products.ts +116 -0
- package/src/backoffice/services/product.service.ts +216 -18
- package/src/backoffice/services/technology.service.ts +169 -0
- package/src/backoffice/types/product.types.ts +116 -6
- package/src/services/appointment/appointment.service.ts +29 -5
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
startAfter,
|
|
14
14
|
getCountFromServer,
|
|
15
15
|
QueryConstraint,
|
|
16
|
+
arrayUnion,
|
|
17
|
+
arrayRemove,
|
|
16
18
|
} from 'firebase/firestore';
|
|
17
19
|
import { Product, PRODUCTS_COLLECTION, IProductService } from '../types/product.types';
|
|
18
20
|
import { BaseService } from '../../services/base.service';
|
|
@@ -20,7 +22,15 @@ import { TECHNOLOGIES_COLLECTION } from '../types/technology.types';
|
|
|
20
22
|
|
|
21
23
|
export class ProductService extends BaseService implements IProductService {
|
|
22
24
|
/**
|
|
23
|
-
* Gets reference to products collection
|
|
25
|
+
* Gets reference to top-level products collection (source of truth)
|
|
26
|
+
* @returns Firestore collection reference
|
|
27
|
+
*/
|
|
28
|
+
private getTopLevelProductsRef() {
|
|
29
|
+
return collection(this.db, PRODUCTS_COLLECTION);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets reference to products collection under a technology (backward compatibility)
|
|
24
34
|
* @param technologyId - ID of the technology
|
|
25
35
|
* @returns Firestore collection reference
|
|
26
36
|
*/
|
|
@@ -37,11 +47,11 @@ export class ProductService extends BaseService implements IProductService {
|
|
|
37
47
|
product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'technologyId'>,
|
|
38
48
|
): Promise<Product> {
|
|
39
49
|
const now = new Date();
|
|
40
|
-
//
|
|
50
|
+
// Create product with legacy structure for subcollection compatibility
|
|
41
51
|
const newProduct: Omit<Product, 'id'> = {
|
|
42
52
|
...product,
|
|
43
53
|
brandId,
|
|
44
|
-
technologyId,
|
|
54
|
+
technologyId, // Required for old subcollection structure
|
|
45
55
|
createdAt: now,
|
|
46
56
|
updatedAt: now,
|
|
47
57
|
isActive: true,
|
|
@@ -125,38 +135,35 @@ export class ProductService extends BaseService implements IProductService {
|
|
|
125
135
|
|
|
126
136
|
/**
|
|
127
137
|
* Gets counts of active products grouped by category, subcategory, and technology.
|
|
128
|
-
*
|
|
138
|
+
* Queries technology subcollections which have the legacy fields synced by Cloud Functions.
|
|
129
139
|
*/
|
|
130
140
|
async getProductCounts(): Promise<{
|
|
131
141
|
byCategory: Record<string, number>;
|
|
132
142
|
bySubcategory: Record<string, number>;
|
|
133
143
|
byTechnology: Record<string, number>;
|
|
134
144
|
}> {
|
|
135
|
-
const q = query(collectionGroup(this.db, PRODUCTS_COLLECTION), where('isActive', '==', true));
|
|
136
|
-
const snapshot = await getDocs(q);
|
|
137
|
-
|
|
138
145
|
const counts = {
|
|
139
146
|
byCategory: {} as Record<string, number>,
|
|
140
147
|
bySubcategory: {} as Record<string, number>,
|
|
141
148
|
byTechnology: {} as Record<string, number>,
|
|
142
149
|
};
|
|
143
150
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
// Query technology subcollections (which have synced legacy fields)
|
|
152
|
+
const q = query(collectionGroup(this.db, PRODUCTS_COLLECTION), where('isActive', '==', true));
|
|
153
|
+
const snapshot = await getDocs(q);
|
|
147
154
|
|
|
148
155
|
snapshot.docs.forEach(doc => {
|
|
149
156
|
const product = doc.data() as Product;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (categoryId) {
|
|
153
|
-
counts.byCategory[categoryId] = (counts.byCategory[categoryId] || 0) + 1;
|
|
157
|
+
|
|
158
|
+
// Use legacy fields from subcollections
|
|
159
|
+
if (product.categoryId) {
|
|
160
|
+
counts.byCategory[product.categoryId] = (counts.byCategory[product.categoryId] || 0) + 1;
|
|
154
161
|
}
|
|
155
|
-
if (subcategoryId) {
|
|
156
|
-
counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
|
|
162
|
+
if (product.subcategoryId) {
|
|
163
|
+
counts.bySubcategory[product.subcategoryId] = (counts.bySubcategory[product.subcategoryId] || 0) + 1;
|
|
157
164
|
}
|
|
158
|
-
if (technologyId) {
|
|
159
|
-
counts.byTechnology[technologyId] = (counts.byTechnology[technologyId] || 0) + 1;
|
|
165
|
+
if (product.technologyId) {
|
|
166
|
+
counts.byTechnology[product.technologyId] = (counts.byTechnology[product.technologyId] || 0) + 1;
|
|
160
167
|
}
|
|
161
168
|
});
|
|
162
169
|
|
|
@@ -252,4 +259,195 @@ export class ProductService extends BaseService implements IProductService {
|
|
|
252
259
|
...docSnap.data(),
|
|
253
260
|
} as Product;
|
|
254
261
|
}
|
|
262
|
+
|
|
263
|
+
// ==========================================
|
|
264
|
+
// NEW METHODS: Top-level collection (preferred)
|
|
265
|
+
// ==========================================
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Creates a new product in the top-level collection
|
|
269
|
+
*/
|
|
270
|
+
async createTopLevel(
|
|
271
|
+
brandId: string,
|
|
272
|
+
product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'assignedTechnologyIds'>,
|
|
273
|
+
technologyIds: string[] = [],
|
|
274
|
+
): Promise<Product> {
|
|
275
|
+
const now = new Date();
|
|
276
|
+
const newProduct: Omit<Product, 'id'> = {
|
|
277
|
+
...product,
|
|
278
|
+
brandId,
|
|
279
|
+
assignedTechnologyIds: technologyIds,
|
|
280
|
+
createdAt: now,
|
|
281
|
+
updatedAt: now,
|
|
282
|
+
isActive: true,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const productRef = await addDoc(this.getTopLevelProductsRef(), newProduct);
|
|
286
|
+
return { id: productRef.id, ...newProduct };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Gets all products from the top-level collection
|
|
291
|
+
*/
|
|
292
|
+
async getAllTopLevel(options: {
|
|
293
|
+
rowsPerPage: number;
|
|
294
|
+
lastVisible?: any;
|
|
295
|
+
brandId?: string;
|
|
296
|
+
}): Promise<{ products: Product[]; lastVisible: any }> {
|
|
297
|
+
const { rowsPerPage, lastVisible, brandId } = options;
|
|
298
|
+
|
|
299
|
+
const constraints: QueryConstraint[] = [where('isActive', '==', true), orderBy('name')];
|
|
300
|
+
|
|
301
|
+
if (brandId) {
|
|
302
|
+
constraints.push(where('brandId', '==', brandId));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (lastVisible) {
|
|
306
|
+
constraints.push(startAfter(lastVisible));
|
|
307
|
+
}
|
|
308
|
+
constraints.push(limit(rowsPerPage));
|
|
309
|
+
|
|
310
|
+
const q = query(this.getTopLevelProductsRef(), ...constraints);
|
|
311
|
+
const snapshot = await getDocs(q);
|
|
312
|
+
|
|
313
|
+
const products = snapshot.docs.map(
|
|
314
|
+
doc =>
|
|
315
|
+
({
|
|
316
|
+
id: doc.id,
|
|
317
|
+
...doc.data(),
|
|
318
|
+
} as Product),
|
|
319
|
+
);
|
|
320
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
321
|
+
|
|
322
|
+
return { products, lastVisible: newLastVisible };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Gets a product by ID from the top-level collection
|
|
327
|
+
*/
|
|
328
|
+
async getByIdTopLevel(productId: string): Promise<Product | null> {
|
|
329
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
330
|
+
const docSnap = await getDoc(docRef);
|
|
331
|
+
if (!docSnap.exists()) return null;
|
|
332
|
+
return {
|
|
333
|
+
id: docSnap.id,
|
|
334
|
+
...docSnap.data(),
|
|
335
|
+
} as Product;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Updates a product in the top-level collection
|
|
340
|
+
*/
|
|
341
|
+
async updateTopLevel(
|
|
342
|
+
productId: string,
|
|
343
|
+
product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId'>>,
|
|
344
|
+
): Promise<Product | null> {
|
|
345
|
+
const updateData = {
|
|
346
|
+
...product,
|
|
347
|
+
updatedAt: new Date(),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
351
|
+
await updateDoc(docRef, updateData);
|
|
352
|
+
|
|
353
|
+
return this.getByIdTopLevel(productId);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Deletes a product from the top-level collection (soft delete)
|
|
358
|
+
*/
|
|
359
|
+
async deleteTopLevel(productId: string): Promise<void> {
|
|
360
|
+
await this.updateTopLevel(productId, {
|
|
361
|
+
isActive: false,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Assigns a product to a technology
|
|
367
|
+
*/
|
|
368
|
+
async assignToTechnology(productId: string, technologyId: string): Promise<void> {
|
|
369
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
370
|
+
await updateDoc(docRef, {
|
|
371
|
+
assignedTechnologyIds: arrayUnion(technologyId),
|
|
372
|
+
updatedAt: new Date(),
|
|
373
|
+
});
|
|
374
|
+
// Cloud Function will handle syncing to subcollection
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Unassigns a product from a technology
|
|
379
|
+
*/
|
|
380
|
+
async unassignFromTechnology(productId: string, technologyId: string): Promise<void> {
|
|
381
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
382
|
+
await updateDoc(docRef, {
|
|
383
|
+
assignedTechnologyIds: arrayRemove(technologyId),
|
|
384
|
+
updatedAt: new Date(),
|
|
385
|
+
});
|
|
386
|
+
// Cloud Function will handle removing from subcollection
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Gets products assigned to a specific technology
|
|
391
|
+
*/
|
|
392
|
+
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
393
|
+
const q = query(
|
|
394
|
+
this.getTopLevelProductsRef(),
|
|
395
|
+
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
396
|
+
where('isActive', '==', true),
|
|
397
|
+
orderBy('name'),
|
|
398
|
+
);
|
|
399
|
+
const snapshot = await getDocs(q);
|
|
400
|
+
return snapshot.docs.map(
|
|
401
|
+
doc =>
|
|
402
|
+
({
|
|
403
|
+
id: doc.id,
|
|
404
|
+
...doc.data(),
|
|
405
|
+
} as Product),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Gets products NOT assigned to a specific technology
|
|
411
|
+
*/
|
|
412
|
+
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
413
|
+
const q = query(
|
|
414
|
+
this.getTopLevelProductsRef(),
|
|
415
|
+
where('isActive', '==', true),
|
|
416
|
+
orderBy('name'),
|
|
417
|
+
);
|
|
418
|
+
const snapshot = await getDocs(q);
|
|
419
|
+
|
|
420
|
+
const allProducts = snapshot.docs.map(
|
|
421
|
+
doc =>
|
|
422
|
+
({
|
|
423
|
+
id: doc.id,
|
|
424
|
+
...doc.data(),
|
|
425
|
+
} as Product),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// Filter out products already assigned to this technology
|
|
429
|
+
return allProducts.filter(product =>
|
|
430
|
+
!product.assignedTechnologyIds?.includes(technologyId)
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Gets all products for a brand (from top-level collection)
|
|
436
|
+
*/
|
|
437
|
+
async getByBrand(brandId: string): Promise<Product[]> {
|
|
438
|
+
const q = query(
|
|
439
|
+
this.getTopLevelProductsRef(),
|
|
440
|
+
where('brandId', '==', brandId),
|
|
441
|
+
where('isActive', '==', true),
|
|
442
|
+
orderBy('name'),
|
|
443
|
+
);
|
|
444
|
+
const snapshot = await getDocs(q);
|
|
445
|
+
return snapshot.docs.map(
|
|
446
|
+
doc =>
|
|
447
|
+
({
|
|
448
|
+
id: doc.id,
|
|
449
|
+
...doc.data(),
|
|
450
|
+
} as Product),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
255
453
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
arrayUnion,
|
|
16
16
|
arrayRemove,
|
|
17
17
|
Firestore,
|
|
18
|
+
writeBatch,
|
|
18
19
|
} from 'firebase/firestore';
|
|
19
20
|
import { Technology, TECHNOLOGIES_COLLECTION, ITechnologyService } from '../types/technology.types';
|
|
20
21
|
import { Requirement, RequirementType } from '../types/requirement.types';
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
import { BaseService } from '../../services/base.service';
|
|
30
31
|
import { ProcedureFamily } from '../types/static/procedure-family.types';
|
|
31
32
|
import { Practitioner, PractitionerCertification } from '../../types/practitioner';
|
|
33
|
+
import { Product, PRODUCTS_COLLECTION } from '../types/product.types';
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Default vrednosti za sertifikaciju
|
|
@@ -240,7 +242,25 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
240
242
|
updateData.updatedAt = new Date();
|
|
241
243
|
|
|
242
244
|
const docRef = doc(this.technologiesRef, id);
|
|
245
|
+
|
|
246
|
+
// Get the technology before update to check what changed
|
|
247
|
+
const beforeTech = await this.getById(id);
|
|
248
|
+
|
|
243
249
|
await updateDoc(docRef, updateData);
|
|
250
|
+
|
|
251
|
+
// If categoryId, subcategoryId, or name changed, update all products in subcollection
|
|
252
|
+
const categoryChanged = beforeTech && updateData.categoryId && beforeTech.categoryId !== updateData.categoryId;
|
|
253
|
+
const subcategoryChanged = beforeTech && updateData.subcategoryId && beforeTech.subcategoryId !== updateData.subcategoryId;
|
|
254
|
+
const nameChanged = beforeTech && updateData.name && beforeTech.name !== updateData.name;
|
|
255
|
+
|
|
256
|
+
if (categoryChanged || subcategoryChanged || nameChanged) {
|
|
257
|
+
await this.updateProductsInSubcollection(id, {
|
|
258
|
+
categoryId: updateData.categoryId,
|
|
259
|
+
subcategoryId: updateData.subcategoryId,
|
|
260
|
+
technologyName: updateData.name,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
244
264
|
return this.getById(id);
|
|
245
265
|
}
|
|
246
266
|
|
|
@@ -778,4 +798,153 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
778
798
|
} as Technology),
|
|
779
799
|
);
|
|
780
800
|
}
|
|
801
|
+
|
|
802
|
+
// ==========================================
|
|
803
|
+
// NEW METHODS: Product assignment management
|
|
804
|
+
// ==========================================
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Assigns multiple products to a technology
|
|
808
|
+
* Updates each product's assignedTechnologyIds array
|
|
809
|
+
*/
|
|
810
|
+
async assignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
811
|
+
const batch = writeBatch(this.db);
|
|
812
|
+
|
|
813
|
+
for (const productId of productIds) {
|
|
814
|
+
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
815
|
+
batch.update(productRef, {
|
|
816
|
+
assignedTechnologyIds: arrayUnion(technologyId),
|
|
817
|
+
updatedAt: new Date(),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
await batch.commit();
|
|
822
|
+
// Cloud Function will handle syncing to subcollections
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Unassigns multiple products from a technology
|
|
827
|
+
* Updates each product's assignedTechnologyIds array
|
|
828
|
+
*/
|
|
829
|
+
async unassignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
830
|
+
const batch = writeBatch(this.db);
|
|
831
|
+
|
|
832
|
+
for (const productId of productIds) {
|
|
833
|
+
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
834
|
+
batch.update(productRef, {
|
|
835
|
+
assignedTechnologyIds: arrayRemove(technologyId),
|
|
836
|
+
updatedAt: new Date(),
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
await batch.commit();
|
|
841
|
+
// Cloud Function will handle removing from subcollections
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Gets products assigned to a specific technology
|
|
846
|
+
* Reads from top-level collection for immediate consistency (Cloud Functions may lag)
|
|
847
|
+
*/
|
|
848
|
+
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
849
|
+
const q = query(
|
|
850
|
+
collection(this.db, PRODUCTS_COLLECTION),
|
|
851
|
+
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
852
|
+
where('isActive', '==', true),
|
|
853
|
+
orderBy('name'),
|
|
854
|
+
);
|
|
855
|
+
const snapshot = await getDocs(q);
|
|
856
|
+
|
|
857
|
+
return snapshot.docs.map(
|
|
858
|
+
doc =>
|
|
859
|
+
({
|
|
860
|
+
id: doc.id,
|
|
861
|
+
...doc.data(),
|
|
862
|
+
} as Product),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Gets products NOT assigned to a specific technology
|
|
868
|
+
*/
|
|
869
|
+
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
870
|
+
const q = query(
|
|
871
|
+
collection(this.db, PRODUCTS_COLLECTION),
|
|
872
|
+
where('isActive', '==', true),
|
|
873
|
+
orderBy('name'),
|
|
874
|
+
);
|
|
875
|
+
const snapshot = await getDocs(q);
|
|
876
|
+
|
|
877
|
+
const allProducts = snapshot.docs.map(
|
|
878
|
+
doc =>
|
|
879
|
+
({
|
|
880
|
+
id: doc.id,
|
|
881
|
+
...doc.data(),
|
|
882
|
+
} as Product),
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
// Filter out products already assigned to this technology
|
|
886
|
+
return allProducts.filter(product =>
|
|
887
|
+
!product.assignedTechnologyIds?.includes(technologyId)
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Gets product assignment statistics for a technology
|
|
893
|
+
*/
|
|
894
|
+
async getProductStats(technologyId: string): Promise<{
|
|
895
|
+
totalAssigned: number;
|
|
896
|
+
byBrand: Record<string, number>;
|
|
897
|
+
}> {
|
|
898
|
+
const products = await this.getAssignedProducts(technologyId);
|
|
899
|
+
|
|
900
|
+
const byBrand: Record<string, number> = {};
|
|
901
|
+
products.forEach(product => {
|
|
902
|
+
byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
totalAssigned: products.length,
|
|
907
|
+
byBrand,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Updates products in technology subcollection when technology metadata changes
|
|
913
|
+
* @param technologyId - ID of the technology
|
|
914
|
+
* @param updates - Fields to update (categoryId, subcategoryId, technologyName)
|
|
915
|
+
*/
|
|
916
|
+
private async updateProductsInSubcollection(
|
|
917
|
+
technologyId: string,
|
|
918
|
+
updates: { categoryId?: string; subcategoryId?: string; technologyName?: string }
|
|
919
|
+
): Promise<void> {
|
|
920
|
+
const productsRef = collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
|
|
921
|
+
const productsSnapshot = await getDocs(productsRef);
|
|
922
|
+
|
|
923
|
+
if (productsSnapshot.empty) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const batch = writeBatch(this.db);
|
|
928
|
+
|
|
929
|
+
for (const productDoc of productsSnapshot.docs) {
|
|
930
|
+
const productRef = productDoc.ref;
|
|
931
|
+
const updateFields: any = {};
|
|
932
|
+
|
|
933
|
+
if (updates.categoryId !== undefined) {
|
|
934
|
+
updateFields.categoryId = updates.categoryId;
|
|
935
|
+
}
|
|
936
|
+
if (updates.subcategoryId !== undefined) {
|
|
937
|
+
updateFields.subcategoryId = updates.subcategoryId;
|
|
938
|
+
}
|
|
939
|
+
if (updates.technologyName !== undefined) {
|
|
940
|
+
updateFields.technologyName = updates.technologyName;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (Object.keys(updateFields).length > 0) {
|
|
944
|
+
batch.update(productRef, updateFields);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
await batch.commit();
|
|
949
|
+
}
|
|
781
950
|
}
|
|
@@ -6,9 +6,10 @@ 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 description - Detailed description of the product and its purpose
|
|
10
9
|
* @property brandId - ID of the brand that manufactures this product
|
|
11
|
-
* @property
|
|
10
|
+
* @property brandName - Name of the brand (denormalized for display)
|
|
11
|
+
* @property assignedTechnologyIds - Array of technology IDs this product is assigned to
|
|
12
|
+
* @property description - Detailed description of the product and its purpose
|
|
12
13
|
* @property technicalDetails - Technical details and specifications
|
|
13
14
|
* @property warnings - List of warnings related to product use
|
|
14
15
|
* @property dosage - Dosage information (if applicable)
|
|
@@ -24,10 +25,11 @@ export interface Product {
|
|
|
24
25
|
name: string;
|
|
25
26
|
brandId: string;
|
|
26
27
|
brandName: string;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
|
|
29
|
+
// NEW: Technology assignment tracking
|
|
30
|
+
assignedTechnologyIds?: string[];
|
|
31
|
+
|
|
32
|
+
// Product details
|
|
31
33
|
createdAt: Date;
|
|
32
34
|
updatedAt: Date;
|
|
33
35
|
isActive: boolean;
|
|
@@ -38,6 +40,18 @@ export interface Product {
|
|
|
38
40
|
composition?: string;
|
|
39
41
|
indications?: string[];
|
|
40
42
|
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;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
/**
|
|
@@ -47,9 +61,97 @@ export const PRODUCTS_COLLECTION = 'products';
|
|
|
47
61
|
|
|
48
62
|
/**
|
|
49
63
|
* 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.
|
|
50
67
|
*/
|
|
51
68
|
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
|
+
|
|
52
153
|
/**
|
|
154
|
+
* @deprecated Use createTopLevel instead
|
|
53
155
|
* Creates a new product
|
|
54
156
|
* @param technologyId - ID of the technology this product is used with
|
|
55
157
|
* @param brandId - ID of the brand that manufactures this product
|
|
@@ -62,6 +164,7 @@ export interface IProductService {
|
|
|
62
164
|
): Promise<Product>;
|
|
63
165
|
|
|
64
166
|
/**
|
|
167
|
+
* @deprecated Use getAllTopLevel instead
|
|
65
168
|
* Gets a paginated list of all products, with optional filters.
|
|
66
169
|
*/
|
|
67
170
|
getAll(options: {
|
|
@@ -73,6 +176,7 @@ export interface IProductService {
|
|
|
73
176
|
}): Promise<{ products: Product[]; lastVisible: any }>;
|
|
74
177
|
|
|
75
178
|
/**
|
|
179
|
+
* @deprecated Use alternative counting methods
|
|
76
180
|
* Gets the total count of active products, with optional filters.
|
|
77
181
|
*/
|
|
78
182
|
getProductsCount(options: {
|
|
@@ -82,6 +186,7 @@ export interface IProductService {
|
|
|
82
186
|
}): Promise<number>;
|
|
83
187
|
|
|
84
188
|
/**
|
|
189
|
+
* @deprecated Use alternative counting methods
|
|
85
190
|
* Gets counts of active products grouped by category, subcategory, and technology.
|
|
86
191
|
*/
|
|
87
192
|
getProductCounts(): Promise<{
|
|
@@ -91,18 +196,21 @@ export interface IProductService {
|
|
|
91
196
|
}>;
|
|
92
197
|
|
|
93
198
|
/**
|
|
199
|
+
* @deprecated Use getAssignedProducts instead
|
|
94
200
|
* Gets all products for a specific technology (non-paginated, for filters/dropdowns)
|
|
95
201
|
* @param technologyId - ID of the technology
|
|
96
202
|
*/
|
|
97
203
|
getAllByTechnology(technologyId: string): Promise<Product[]>;
|
|
98
204
|
|
|
99
205
|
/**
|
|
206
|
+
* @deprecated Use getByBrand instead
|
|
100
207
|
* Gets all products for a brand
|
|
101
208
|
* @param brandId - ID of the brand
|
|
102
209
|
*/
|
|
103
210
|
getAllByBrand(brandId: string): Promise<Product[]>;
|
|
104
211
|
|
|
105
212
|
/**
|
|
213
|
+
* @deprecated Use updateTopLevel instead
|
|
106
214
|
* Updates a product
|
|
107
215
|
* @param technologyId - ID of the technology
|
|
108
216
|
* @param productId - ID of the product to update
|
|
@@ -115,6 +223,7 @@ export interface IProductService {
|
|
|
115
223
|
): Promise<Product | null>;
|
|
116
224
|
|
|
117
225
|
/**
|
|
226
|
+
* @deprecated Use deleteTopLevel instead
|
|
118
227
|
* Deletes a product (soft delete)
|
|
119
228
|
* @param technologyId - ID of the technology
|
|
120
229
|
* @param productId - ID of the product to delete
|
|
@@ -122,6 +231,7 @@ export interface IProductService {
|
|
|
122
231
|
delete(technologyId: string, productId: string): Promise<void>;
|
|
123
232
|
|
|
124
233
|
/**
|
|
234
|
+
* @deprecated Use getByIdTopLevel instead
|
|
125
235
|
* Gets a product by ID
|
|
126
236
|
* @param technologyId - ID of the technology
|
|
127
237
|
* @param productId - ID of the product
|