@blackcode_sa/metaestetics-api 1.12.40 โ 1.12.41
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 +178 -19
- package/dist/backoffice/index.d.ts +178 -19
- package/dist/backoffice/index.js +261 -17
- package/dist/backoffice/index.mjs +277 -30
- package/dist/index.d.mts +170 -11
- package/dist/index.d.ts +170 -11
- package/dist/index.js +288 -28
- package/dist/index.mjs +299 -36
- package/package.json +1 -1
- package/src/backoffice/services/migrate-products.ts +116 -0
- package/src/backoffice/services/product.service.ts +216 -21
- package/src/backoffice/services/technology.service.ts +111 -0
- package/src/backoffice/types/product.types.ts +115 -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,
|
|
@@ -124,39 +134,33 @@ export class ProductService extends BaseService implements IProductService {
|
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
/**
|
|
127
|
-
* Gets counts of active products grouped by
|
|
128
|
-
*
|
|
137
|
+
* Gets counts of active products grouped by technology.
|
|
138
|
+
* NOTE: Only counts top-level collection to avoid duplication during migration.
|
|
139
|
+
* Categories/subcategories not available in top-level structure.
|
|
129
140
|
*/
|
|
130
141
|
async getProductCounts(): Promise<{
|
|
131
142
|
byCategory: Record<string, number>;
|
|
132
143
|
bySubcategory: Record<string, number>;
|
|
133
144
|
byTechnology: Record<string, number>;
|
|
134
145
|
}> {
|
|
135
|
-
const q = query(collectionGroup(this.db, PRODUCTS_COLLECTION), where('isActive', '==', true));
|
|
136
|
-
const snapshot = await getDocs(q);
|
|
137
|
-
|
|
138
146
|
const counts = {
|
|
139
147
|
byCategory: {} as Record<string, number>,
|
|
140
148
|
bySubcategory: {} as Record<string, number>,
|
|
141
149
|
byTechnology: {} as Record<string, number>,
|
|
142
150
|
};
|
|
143
151
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
// Query top-level collection only (to avoid duplication during migration)
|
|
153
|
+
const q = query(this.getTopLevelProductsRef(), where('isActive', '==', true));
|
|
154
|
+
const snapshot = await getDocs(q);
|
|
147
155
|
|
|
148
156
|
snapshot.docs.forEach(doc => {
|
|
149
157
|
const product = doc.data() as Product;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
|
|
157
|
-
}
|
|
158
|
-
if (technologyId) {
|
|
159
|
-
counts.byTechnology[technologyId] = (counts.byTechnology[technologyId] || 0) + 1;
|
|
158
|
+
|
|
159
|
+
// Count by technology using assignedTechnologyIds
|
|
160
|
+
if (product.assignedTechnologyIds && Array.isArray(product.assignedTechnologyIds)) {
|
|
161
|
+
product.assignedTechnologyIds.forEach(techId => {
|
|
162
|
+
counts.byTechnology[techId] = (counts.byTechnology[techId] || 0) + 1;
|
|
163
|
+
});
|
|
160
164
|
}
|
|
161
165
|
});
|
|
162
166
|
|
|
@@ -252,4 +256,195 @@ export class ProductService extends BaseService implements IProductService {
|
|
|
252
256
|
...docSnap.data(),
|
|
253
257
|
} as Product;
|
|
254
258
|
}
|
|
259
|
+
|
|
260
|
+
// ==========================================
|
|
261
|
+
// NEW METHODS: Top-level collection (preferred)
|
|
262
|
+
// ==========================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Creates a new product in the top-level collection
|
|
266
|
+
*/
|
|
267
|
+
async createTopLevel(
|
|
268
|
+
brandId: string,
|
|
269
|
+
product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'assignedTechnologyIds'>,
|
|
270
|
+
technologyIds: string[] = [],
|
|
271
|
+
): Promise<Product> {
|
|
272
|
+
const now = new Date();
|
|
273
|
+
const newProduct: Omit<Product, 'id'> = {
|
|
274
|
+
...product,
|
|
275
|
+
brandId,
|
|
276
|
+
assignedTechnologyIds: technologyIds,
|
|
277
|
+
createdAt: now,
|
|
278
|
+
updatedAt: now,
|
|
279
|
+
isActive: true,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const productRef = await addDoc(this.getTopLevelProductsRef(), newProduct);
|
|
283
|
+
return { id: productRef.id, ...newProduct };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Gets all products from the top-level collection
|
|
288
|
+
*/
|
|
289
|
+
async getAllTopLevel(options: {
|
|
290
|
+
rowsPerPage: number;
|
|
291
|
+
lastVisible?: any;
|
|
292
|
+
brandId?: string;
|
|
293
|
+
}): Promise<{ products: Product[]; lastVisible: any }> {
|
|
294
|
+
const { rowsPerPage, lastVisible, brandId } = options;
|
|
295
|
+
|
|
296
|
+
const constraints: QueryConstraint[] = [where('isActive', '==', true), orderBy('name')];
|
|
297
|
+
|
|
298
|
+
if (brandId) {
|
|
299
|
+
constraints.push(where('brandId', '==', brandId));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (lastVisible) {
|
|
303
|
+
constraints.push(startAfter(lastVisible));
|
|
304
|
+
}
|
|
305
|
+
constraints.push(limit(rowsPerPage));
|
|
306
|
+
|
|
307
|
+
const q = query(this.getTopLevelProductsRef(), ...constraints);
|
|
308
|
+
const snapshot = await getDocs(q);
|
|
309
|
+
|
|
310
|
+
const products = snapshot.docs.map(
|
|
311
|
+
doc =>
|
|
312
|
+
({
|
|
313
|
+
id: doc.id,
|
|
314
|
+
...doc.data(),
|
|
315
|
+
} as Product),
|
|
316
|
+
);
|
|
317
|
+
const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
|
|
318
|
+
|
|
319
|
+
return { products, lastVisible: newLastVisible };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Gets a product by ID from the top-level collection
|
|
324
|
+
*/
|
|
325
|
+
async getByIdTopLevel(productId: string): Promise<Product | null> {
|
|
326
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
327
|
+
const docSnap = await getDoc(docRef);
|
|
328
|
+
if (!docSnap.exists()) return null;
|
|
329
|
+
return {
|
|
330
|
+
id: docSnap.id,
|
|
331
|
+
...docSnap.data(),
|
|
332
|
+
} as Product;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Updates a product in the top-level collection
|
|
337
|
+
*/
|
|
338
|
+
async updateTopLevel(
|
|
339
|
+
productId: string,
|
|
340
|
+
product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId'>>,
|
|
341
|
+
): Promise<Product | null> {
|
|
342
|
+
const updateData = {
|
|
343
|
+
...product,
|
|
344
|
+
updatedAt: new Date(),
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
348
|
+
await updateDoc(docRef, updateData);
|
|
349
|
+
|
|
350
|
+
return this.getByIdTopLevel(productId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Deletes a product from the top-level collection (soft delete)
|
|
355
|
+
*/
|
|
356
|
+
async deleteTopLevel(productId: string): Promise<void> {
|
|
357
|
+
await this.updateTopLevel(productId, {
|
|
358
|
+
isActive: false,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Assigns a product to a technology
|
|
364
|
+
*/
|
|
365
|
+
async assignToTechnology(productId: string, technologyId: string): Promise<void> {
|
|
366
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
367
|
+
await updateDoc(docRef, {
|
|
368
|
+
assignedTechnologyIds: arrayUnion(technologyId),
|
|
369
|
+
updatedAt: new Date(),
|
|
370
|
+
});
|
|
371
|
+
// Cloud Function will handle syncing to subcollection
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Unassigns a product from a technology
|
|
376
|
+
*/
|
|
377
|
+
async unassignFromTechnology(productId: string, technologyId: string): Promise<void> {
|
|
378
|
+
const docRef = doc(this.getTopLevelProductsRef(), productId);
|
|
379
|
+
await updateDoc(docRef, {
|
|
380
|
+
assignedTechnologyIds: arrayRemove(technologyId),
|
|
381
|
+
updatedAt: new Date(),
|
|
382
|
+
});
|
|
383
|
+
// Cloud Function will handle removing from subcollection
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Gets products assigned to a specific technology
|
|
388
|
+
*/
|
|
389
|
+
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
390
|
+
const q = query(
|
|
391
|
+
this.getTopLevelProductsRef(),
|
|
392
|
+
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
393
|
+
where('isActive', '==', true),
|
|
394
|
+
orderBy('name'),
|
|
395
|
+
);
|
|
396
|
+
const snapshot = await getDocs(q);
|
|
397
|
+
return snapshot.docs.map(
|
|
398
|
+
doc =>
|
|
399
|
+
({
|
|
400
|
+
id: doc.id,
|
|
401
|
+
...doc.data(),
|
|
402
|
+
} as Product),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Gets products NOT assigned to a specific technology
|
|
408
|
+
*/
|
|
409
|
+
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
410
|
+
const q = query(
|
|
411
|
+
this.getTopLevelProductsRef(),
|
|
412
|
+
where('isActive', '==', true),
|
|
413
|
+
orderBy('name'),
|
|
414
|
+
);
|
|
415
|
+
const snapshot = await getDocs(q);
|
|
416
|
+
|
|
417
|
+
const allProducts = snapshot.docs.map(
|
|
418
|
+
doc =>
|
|
419
|
+
({
|
|
420
|
+
id: doc.id,
|
|
421
|
+
...doc.data(),
|
|
422
|
+
} as Product),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// Filter out products already assigned to this technology
|
|
426
|
+
return allProducts.filter(product =>
|
|
427
|
+
!product.assignedTechnologyIds?.includes(technologyId)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Gets all products for a brand (from top-level collection)
|
|
433
|
+
*/
|
|
434
|
+
async getByBrand(brandId: string): Promise<Product[]> {
|
|
435
|
+
const q = query(
|
|
436
|
+
this.getTopLevelProductsRef(),
|
|
437
|
+
where('brandId', '==', brandId),
|
|
438
|
+
where('isActive', '==', true),
|
|
439
|
+
orderBy('name'),
|
|
440
|
+
);
|
|
441
|
+
const snapshot = await getDocs(q);
|
|
442
|
+
return snapshot.docs.map(
|
|
443
|
+
doc =>
|
|
444
|
+
({
|
|
445
|
+
id: doc.id,
|
|
446
|
+
...doc.data(),
|
|
447
|
+
} as Product),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
255
450
|
}
|
|
@@ -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
|
|
@@ -778,4 +780,113 @@ export class TechnologyService extends BaseService implements ITechnologyService
|
|
|
778
780
|
} as Technology),
|
|
779
781
|
);
|
|
780
782
|
}
|
|
783
|
+
|
|
784
|
+
// ==========================================
|
|
785
|
+
// NEW METHODS: Product assignment management
|
|
786
|
+
// ==========================================
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Assigns multiple products to a technology
|
|
790
|
+
* Updates each product's assignedTechnologyIds array
|
|
791
|
+
*/
|
|
792
|
+
async assignProducts(technologyId: string, productIds: string[]): Promise<void> {
|
|
793
|
+
const batch = writeBatch(this.db);
|
|
794
|
+
|
|
795
|
+
for (const productId of productIds) {
|
|
796
|
+
const productRef = doc(this.db, PRODUCTS_COLLECTION, productId);
|
|
797
|
+
batch.update(productRef, {
|
|
798
|
+
assignedTechnologyIds: arrayUnion(technologyId),
|
|
799
|
+
updatedAt: new Date(),
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
await batch.commit();
|
|
804
|
+
// Cloud Function will handle syncing to subcollections
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Unassigns multiple products from a technology
|
|
809
|
+
* Updates each product's assignedTechnologyIds array
|
|
810
|
+
*/
|
|
811
|
+
async unassignProducts(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: arrayRemove(technologyId),
|
|
818
|
+
updatedAt: new Date(),
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
await batch.commit();
|
|
823
|
+
// Cloud Function will handle removing from subcollections
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Gets products assigned to a specific technology
|
|
828
|
+
* Reads from top-level collection for immediate consistency (Cloud Functions may lag)
|
|
829
|
+
*/
|
|
830
|
+
async getAssignedProducts(technologyId: string): Promise<Product[]> {
|
|
831
|
+
const q = query(
|
|
832
|
+
collection(this.db, PRODUCTS_COLLECTION),
|
|
833
|
+
where('assignedTechnologyIds', 'array-contains', technologyId),
|
|
834
|
+
where('isActive', '==', true),
|
|
835
|
+
orderBy('name'),
|
|
836
|
+
);
|
|
837
|
+
const snapshot = await getDocs(q);
|
|
838
|
+
|
|
839
|
+
return snapshot.docs.map(
|
|
840
|
+
doc =>
|
|
841
|
+
({
|
|
842
|
+
id: doc.id,
|
|
843
|
+
...doc.data(),
|
|
844
|
+
} as Product),
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Gets products NOT assigned to a specific technology
|
|
850
|
+
*/
|
|
851
|
+
async getUnassignedProducts(technologyId: string): Promise<Product[]> {
|
|
852
|
+
const q = query(
|
|
853
|
+
collection(this.db, PRODUCTS_COLLECTION),
|
|
854
|
+
where('isActive', '==', true),
|
|
855
|
+
orderBy('name'),
|
|
856
|
+
);
|
|
857
|
+
const snapshot = await getDocs(q);
|
|
858
|
+
|
|
859
|
+
const allProducts = snapshot.docs.map(
|
|
860
|
+
doc =>
|
|
861
|
+
({
|
|
862
|
+
id: doc.id,
|
|
863
|
+
...doc.data(),
|
|
864
|
+
} as Product),
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
// Filter out products already assigned to this technology
|
|
868
|
+
return allProducts.filter(product =>
|
|
869
|
+
!product.assignedTechnologyIds?.includes(technologyId)
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Gets product assignment statistics for a technology
|
|
875
|
+
*/
|
|
876
|
+
async getProductStats(technologyId: string): Promise<{
|
|
877
|
+
totalAssigned: number;
|
|
878
|
+
byBrand: Record<string, number>;
|
|
879
|
+
}> {
|
|
880
|
+
const products = await this.getAssignedProducts(technologyId);
|
|
881
|
+
|
|
882
|
+
const byBrand: Record<string, number> = {};
|
|
883
|
+
products.forEach(product => {
|
|
884
|
+
byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
totalAssigned: products.length,
|
|
889
|
+
byBrand,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
781
892
|
}
|
|
@@ -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,17 @@ export interface Product {
|
|
|
38
40
|
composition?: string;
|
|
39
41
|
indications?: string[];
|
|
40
42
|
contraindications?: ContraindicationDynamic[];
|
|
43
|
+
|
|
44
|
+
// DEPRECATED: Kept for backward compatibility with subcollection structure
|
|
45
|
+
// These fields exist in old products in /technologies/{id}/products/
|
|
46
|
+
/** @deprecated Use assignedTechnologyIds instead */
|
|
47
|
+
technologyId?: string;
|
|
48
|
+
/** @deprecated Will be removed in future version */
|
|
49
|
+
technologyName?: string;
|
|
50
|
+
/** @deprecated Not needed in top-level collection */
|
|
51
|
+
categoryId?: string;
|
|
52
|
+
/** @deprecated Not needed in top-level collection */
|
|
53
|
+
subcategoryId?: string;
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
/**
|
|
@@ -47,9 +60,97 @@ export const PRODUCTS_COLLECTION = 'products';
|
|
|
47
60
|
|
|
48
61
|
/**
|
|
49
62
|
* Interface for the ProductService class
|
|
63
|
+
*
|
|
64
|
+
* NOTE: This interface maintains backward compatibility while adding new top-level collection methods.
|
|
65
|
+
* Old methods using technologyId are kept for existing code, new methods work with top-level collection.
|
|
50
66
|
*/
|
|
51
67
|
export interface IProductService {
|
|
68
|
+
// ==========================================
|
|
69
|
+
// NEW METHODS: Top-level collection (preferred)
|
|
70
|
+
// ==========================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new product in the top-level collection
|
|
74
|
+
* @param brandId - ID of the brand that manufactures this product
|
|
75
|
+
* @param product - Product data
|
|
76
|
+
* @param technologyIds - Optional array of technology IDs to assign this product to
|
|
77
|
+
*/
|
|
78
|
+
createTopLevel(
|
|
79
|
+
brandId: string,
|
|
80
|
+
product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'assignedTechnologyIds'>,
|
|
81
|
+
technologyIds?: string[],
|
|
82
|
+
): Promise<Product>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets all products from the top-level collection
|
|
86
|
+
* @param options - Query options
|
|
87
|
+
*/
|
|
88
|
+
getAllTopLevel(options: {
|
|
89
|
+
rowsPerPage: number;
|
|
90
|
+
lastVisible?: any;
|
|
91
|
+
brandId?: string;
|
|
92
|
+
}): Promise<{ products: Product[]; lastVisible: any }>;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets a product by ID from the top-level collection
|
|
96
|
+
* @param productId - ID of the product
|
|
97
|
+
*/
|
|
98
|
+
getByIdTopLevel(productId: string): Promise<Product | null>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Updates a product in the top-level collection
|
|
102
|
+
* @param productId - ID of the product to update
|
|
103
|
+
* @param product - Updated product data
|
|
104
|
+
*/
|
|
105
|
+
updateTopLevel(
|
|
106
|
+
productId: string,
|
|
107
|
+
product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId'>>,
|
|
108
|
+
): Promise<Product | null>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Deletes a product from the top-level collection (soft delete)
|
|
112
|
+
* @param productId - ID of the product to delete
|
|
113
|
+
*/
|
|
114
|
+
deleteTopLevel(productId: string): Promise<void>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Assigns a product to a technology
|
|
118
|
+
* @param productId - ID of the product
|
|
119
|
+
* @param technologyId - ID of the technology
|
|
120
|
+
*/
|
|
121
|
+
assignToTechnology(productId: string, technologyId: string): Promise<void>;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Unassigns a product from a technology
|
|
125
|
+
* @param productId - ID of the product
|
|
126
|
+
* @param technologyId - ID of the technology
|
|
127
|
+
*/
|
|
128
|
+
unassignFromTechnology(productId: string, technologyId: string): Promise<void>;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Gets products assigned to a specific technology
|
|
132
|
+
* @param technologyId - ID of the technology
|
|
133
|
+
*/
|
|
134
|
+
getAssignedProducts(technologyId: string): Promise<Product[]>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Gets products NOT assigned to a specific technology
|
|
138
|
+
* @param technologyId - ID of the technology
|
|
139
|
+
*/
|
|
140
|
+
getUnassignedProducts(technologyId: string): Promise<Product[]>;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Gets all products for a brand
|
|
144
|
+
* @param brandId - ID of the brand
|
|
145
|
+
*/
|
|
146
|
+
getByBrand(brandId: string): Promise<Product[]>;
|
|
147
|
+
|
|
148
|
+
// ==========================================
|
|
149
|
+
// DEPRECATED METHODS: Kept for backward compatibility
|
|
150
|
+
// ==========================================
|
|
151
|
+
|
|
52
152
|
/**
|
|
153
|
+
* @deprecated Use createTopLevel instead
|
|
53
154
|
* Creates a new product
|
|
54
155
|
* @param technologyId - ID of the technology this product is used with
|
|
55
156
|
* @param brandId - ID of the brand that manufactures this product
|
|
@@ -62,6 +163,7 @@ export interface IProductService {
|
|
|
62
163
|
): Promise<Product>;
|
|
63
164
|
|
|
64
165
|
/**
|
|
166
|
+
* @deprecated Use getAllTopLevel instead
|
|
65
167
|
* Gets a paginated list of all products, with optional filters.
|
|
66
168
|
*/
|
|
67
169
|
getAll(options: {
|
|
@@ -73,6 +175,7 @@ export interface IProductService {
|
|
|
73
175
|
}): Promise<{ products: Product[]; lastVisible: any }>;
|
|
74
176
|
|
|
75
177
|
/**
|
|
178
|
+
* @deprecated Use alternative counting methods
|
|
76
179
|
* Gets the total count of active products, with optional filters.
|
|
77
180
|
*/
|
|
78
181
|
getProductsCount(options: {
|
|
@@ -82,6 +185,7 @@ export interface IProductService {
|
|
|
82
185
|
}): Promise<number>;
|
|
83
186
|
|
|
84
187
|
/**
|
|
188
|
+
* @deprecated Use alternative counting methods
|
|
85
189
|
* Gets counts of active products grouped by category, subcategory, and technology.
|
|
86
190
|
*/
|
|
87
191
|
getProductCounts(): Promise<{
|
|
@@ -91,18 +195,21 @@ export interface IProductService {
|
|
|
91
195
|
}>;
|
|
92
196
|
|
|
93
197
|
/**
|
|
198
|
+
* @deprecated Use getAssignedProducts instead
|
|
94
199
|
* Gets all products for a specific technology (non-paginated, for filters/dropdowns)
|
|
95
200
|
* @param technologyId - ID of the technology
|
|
96
201
|
*/
|
|
97
202
|
getAllByTechnology(technologyId: string): Promise<Product[]>;
|
|
98
203
|
|
|
99
204
|
/**
|
|
205
|
+
* @deprecated Use getByBrand instead
|
|
100
206
|
* Gets all products for a brand
|
|
101
207
|
* @param brandId - ID of the brand
|
|
102
208
|
*/
|
|
103
209
|
getAllByBrand(brandId: string): Promise<Product[]>;
|
|
104
210
|
|
|
105
211
|
/**
|
|
212
|
+
* @deprecated Use updateTopLevel instead
|
|
106
213
|
* Updates a product
|
|
107
214
|
* @param technologyId - ID of the technology
|
|
108
215
|
* @param productId - ID of the product to update
|
|
@@ -115,6 +222,7 @@ export interface IProductService {
|
|
|
115
222
|
): Promise<Product | null>;
|
|
116
223
|
|
|
117
224
|
/**
|
|
225
|
+
* @deprecated Use deleteTopLevel instead
|
|
118
226
|
* Deletes a product (soft delete)
|
|
119
227
|
* @param technologyId - ID of the technology
|
|
120
228
|
* @param productId - ID of the product to delete
|
|
@@ -122,6 +230,7 @@ export interface IProductService {
|
|
|
122
230
|
delete(technologyId: string, productId: string): Promise<void>;
|
|
123
231
|
|
|
124
232
|
/**
|
|
233
|
+
* @deprecated Use getByIdTopLevel instead
|
|
125
234
|
* Gets a product by ID
|
|
126
235
|
* @param technologyId - ID of the technology
|
|
127
236
|
* @param productId - ID of the product
|
|
@@ -412,20 +412,44 @@ export class AppointmentService extends BaseService {
|
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
// AUTO-CLEANUP: Remove invalid recommendedProcedures with empty notes BEFORE validation
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
415
|
+
console.log(
|
|
416
|
+
'[APPOINTMENT_SERVICE] ๐ BEFORE CLEANUP - recommendedProcedures:',
|
|
417
|
+
JSON.stringify(data.metadata?.recommendedProcedures, null, 2),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
if (
|
|
421
|
+
data.metadata?.recommendedProcedures &&
|
|
422
|
+
Array.isArray(data.metadata.recommendedProcedures)
|
|
423
|
+
) {
|
|
424
|
+
const validRecommendations = data.metadata.recommendedProcedures.filter((rec: any) => {
|
|
425
|
+
const isValid = rec.note && typeof rec.note === 'string' && rec.note.trim().length > 0;
|
|
426
|
+
if (!isValid) {
|
|
427
|
+
console.log('[APPOINTMENT_SERVICE] โ INVALID recommendation found:', rec);
|
|
428
|
+
}
|
|
429
|
+
return isValid;
|
|
430
|
+
});
|
|
431
|
+
|
|
419
432
|
if (validRecommendations.length !== data.metadata.recommendedProcedures.length) {
|
|
420
433
|
console.log(
|
|
421
|
-
`[APPOINTMENT_SERVICE] Removing ${
|
|
434
|
+
`[APPOINTMENT_SERVICE] ๐งน Removing ${
|
|
435
|
+
data.metadata.recommendedProcedures.length - validRecommendations.length
|
|
436
|
+
} invalid recommended procedures with empty notes`,
|
|
422
437
|
);
|
|
423
438
|
data.metadata.recommendedProcedures = validRecommendations;
|
|
439
|
+
} else {
|
|
440
|
+
console.log('[APPOINTMENT_SERVICE] โ
All recommendedProcedures are valid');
|
|
424
441
|
}
|
|
425
442
|
}
|
|
426
443
|
|
|
444
|
+
console.log(
|
|
445
|
+
'[APPOINTMENT_SERVICE] ๐ AFTER CLEANUP - recommendedProcedures:',
|
|
446
|
+
JSON.stringify(data.metadata?.recommendedProcedures, null, 2),
|
|
447
|
+
);
|
|
448
|
+
|
|
427
449
|
// Validate input data
|
|
450
|
+
console.log('[APPOINTMENT_SERVICE] ๐ Starting Zod validation...');
|
|
428
451
|
const validatedData = await updateAppointmentSchema.parseAsync(data);
|
|
452
|
+
console.log('[APPOINTMENT_SERVICE] โ
Zod validation passed!');
|
|
429
453
|
|
|
430
454
|
// Update the appointment using the utility function
|
|
431
455
|
const updatedAppointment = await updateAppointmentUtil(this.db, appointmentId, validatedData);
|