@blackcode_sa/metaestetics-api 1.12.1 → 1.12.3

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.1",
4
+ "version": "1.12.3",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,102 @@
1
+ # Service Method Fixes for Procedure Creation
2
+
3
+ ## Problem
4
+ The procedure creation components (`CreateProcedureBulk.tsx`, `CreateProcedure.tsx`, `UpdateProcedure.tsx`) were not loading categories and subcategories because the service methods were returning paginated objects instead of arrays.
5
+
6
+ ## Root Cause
7
+ The procedure service was calling paginated methods that return objects like `{ categories, lastVisible }` instead of filter methods that return arrays like `Category[]`.
8
+
9
+ ## Changes Made
10
+
11
+ ### 1. Added Missing Methods
12
+
13
+ #### CategoryService
14
+ - **Added**: `getAllForFilterByFamily(family: ProcedureFamily): Promise<Category[]>`
15
+ - **Added**: Interface `ICategoryService` in `category.types.ts`
16
+ - **Modified**: `CategoryService` now implements `ICategoryService`
17
+
18
+ #### TechnologyService
19
+ - **Added**: `getAllForFilterBySubcategory(subcategoryId: string): Promise<Technology[]>`
20
+ - **Modified**: Added method to `ITechnologyService` interface
21
+ - **Modified**: `TechnologyService` implements `ITechnologyService`
22
+
23
+ #### ProductService
24
+ - **Added**: `getAllByTechnology(technologyId: string): Promise<Product[]>`
25
+ - **Modified**: Added method to `IProductService` interface
26
+
27
+ ### 2. Updated Procedure Service Calls
28
+
29
+ Changed from paginated methods to filter methods:
30
+
31
+ ```typescript
32
+ // Before (returned { categories, lastVisible })
33
+ await getCategoryService().getAllByFamily(family)
34
+
35
+ // After (returns Category[])
36
+ await getCategoryService().getAllForFilterByFamily(family)
37
+ ```
38
+
39
+ ```typescript
40
+ // Before (returned { subcategories, lastVisible })
41
+ await getSubcategoryService().getAllByCategoryId(id)
42
+
43
+ // After (returns Subcategory[])
44
+ await getSubcategoryService().getAllForFilterByCategoryId(id)
45
+ ```
46
+
47
+ ```typescript
48
+ // Before (returned { technologies, lastVisible })
49
+ await getTechnologyService().getAllBySubcategoryId(id)
50
+
51
+ // After (returns Technology[])
52
+ await getTechnologyService().getAllForFilterBySubcategory(id)
53
+ ```
54
+
55
+ ```typescript
56
+ // Before (method didn't exist)
57
+ await getProductService().getAllByTechnology(technologyId)
58
+
59
+ // After (returns Product[])
60
+ await getProductService().getAllByTechnology(technologyId)
61
+ ```
62
+
63
+ ### 3. Service Method Patterns
64
+
65
+ #### Paginated Methods (for admin UI with pagination)
66
+ - Return: `{ items: T[], lastVisible: DocumentData }`
67
+ - Example: `getAllByFamily()`, `getAllByCategoryId()`, `getAllBySubcategoryId()`
68
+
69
+ #### Filter Methods (for dropdowns/filters)
70
+ - Return: `T[]`
71
+ - Example: `getAllForFilter()`, `getAllForFilterByFamily()`, `getAllForFilterByCategoryId()`
72
+
73
+ ## Files Modified
74
+
75
+ ### API (Backend)
76
+ - `Api/src/backoffice/services/category.service.ts`
77
+ - `Api/src/backoffice/services/technology.service.ts`
78
+ - `Api/src/backoffice/services/product.service.ts`
79
+ - `Api/src/backoffice/types/category.types.ts`
80
+ - `Api/src/backoffice/types/technology.types.ts`
81
+ - `Api/src/backoffice/types/product.types.ts`
82
+
83
+ ### Client App
84
+ - `ClinicApp/src/store/procedure/procedure.service.ts`
85
+
86
+ ## Expected Result
87
+ Categories, subcategories, technologies, and products should now load correctly in:
88
+ - Create Procedure Bulk form
89
+ - Create Procedure form
90
+ - Update Procedure form
91
+
92
+ The dropdowns should populate with the correct options when selecting families, categories, subcategories, and technologies.
93
+
94
+ ## Testing
95
+ 1. Navigate to Create Procedure Bulk
96
+ 2. Select a family (Aesthetics/Surgery) - categories should load
97
+ 3. Select a category - subcategories should load
98
+ 4. Select a subcategory - technologies should load
99
+ 5. Select a technology - products should load and qualified practitioners should appear
100
+
101
+ ## Note
102
+ If TypeScript errors persist about missing methods, the API package may need to be rebuilt (`npm run build` in the Api directory) for the type definitions to be updated in the consuming client application.
@@ -12,10 +12,10 @@ import {
12
12
  startAfter,
13
13
  updateDoc,
14
14
  where,
15
- } from "firebase/firestore";
16
- import { Category, CATEGORIES_COLLECTION } from "../types/category.types";
17
- import { BaseService } from "../../services/base.service";
18
- import { ProcedureFamily } from "../types/static/procedure-family.types";
15
+ } from 'firebase/firestore';
16
+ import { Category, CATEGORIES_COLLECTION, ICategoryService } from '../types/category.types';
17
+ import { BaseService } from '../../services/base.service';
18
+ import { ProcedureFamily } from '../types/static/procedure-family.types';
19
19
 
20
20
  /**
21
21
  * Servis za upravljanje kategorijama procedura.
@@ -30,7 +30,7 @@ import { ProcedureFamily } from "../types/static/procedure-family.types";
30
30
  * family: ProcedureFamily.AESTHETICS
31
31
  * });
32
32
  */
33
- export class CategoryService extends BaseService {
33
+ export class CategoryService extends BaseService implements ICategoryService {
34
34
  /**
35
35
  * Referenca na Firestore kolekciju kategorija
36
36
  */
@@ -43,9 +43,9 @@ export class CategoryService extends BaseService {
43
43
  * @param category - Podaci za novu kategoriju
44
44
  * @returns Kreirana kategorija sa generisanim ID-em
45
45
  */
46
- async create(category: Omit<Category, "id" | "createdAt" | "updatedAt">) {
46
+ async create(category: Omit<Category, 'id' | 'createdAt' | 'updatedAt'>) {
47
47
  const now = new Date();
48
- const newCategory: Omit<Category, "id"> = {
48
+ const newCategory: Omit<Category, 'id'> = {
49
49
  ...category,
50
50
  createdAt: now,
51
51
  updatedAt: now,
@@ -68,8 +68,8 @@ export class CategoryService extends BaseService {
68
68
  for (const family of families) {
69
69
  const q = query(
70
70
  this.categoriesRef,
71
- where("family", "==", family),
72
- where("isActive", "==", active)
71
+ where('family', '==', family),
72
+ where('isActive', '==', active),
73
73
  );
74
74
  const snapshot = await getCountFromServer(q);
75
75
  counts[family] = snapshot.data().count;
@@ -82,14 +82,36 @@ export class CategoryService extends BaseService {
82
82
  * @returns Lista svih aktivnih kategorija
83
83
  */
84
84
  async getAllForFilter() {
85
- const q = query(this.categoriesRef, where("isActive", "==", true));
85
+ const q = query(this.categoriesRef, where('isActive', '==', true));
86
86
  const snapshot = await getDocs(q);
87
87
  return snapshot.docs.map(
88
- (doc) =>
88
+ doc =>
89
89
  ({
90
90
  id: doc.id,
91
91
  ...doc.data(),
92
- } as Category)
92
+ } as Category),
93
+ );
94
+ }
95
+
96
+ /**
97
+ * Vraća sve kategorije za određenu familiju za potrebe filtera (bez paginacije)
98
+ * @param family - Familija procedura (aesthetics/surgery)
99
+ * @returns Lista aktivnih kategorija koje pripadaju traženoj familiji
100
+ */
101
+ async getAllForFilterByFamily(family: ProcedureFamily) {
102
+ const q = query(
103
+ this.categoriesRef,
104
+ where('family', '==', family),
105
+ where('isActive', '==', true),
106
+ orderBy('name'),
107
+ );
108
+ const snapshot = await getDocs(q);
109
+ return snapshot.docs.map(
110
+ doc =>
111
+ ({
112
+ id: doc.id,
113
+ ...doc.data(),
114
+ } as Category),
93
115
  );
94
116
  }
95
117
 
@@ -103,12 +125,12 @@ export class CategoryService extends BaseService {
103
125
  active?: boolean;
104
126
  limit?: number;
105
127
  lastVisible?: DocumentData;
106
- } = {}
128
+ } = {},
107
129
  ) {
108
130
  const { active = true, limit: queryLimit = 10, lastVisible } = options;
109
131
  const constraints = [
110
- where("isActive", "==", active),
111
- orderBy("name"),
132
+ where('isActive', '==', active),
133
+ orderBy('name'),
112
134
  queryLimit ? limit(queryLimit) : undefined,
113
135
  lastVisible ? startAfter(lastVisible) : undefined,
114
136
  ].filter((c): c is NonNullable<typeof c> => !!c);
@@ -116,11 +138,11 @@ export class CategoryService extends BaseService {
116
138
  const q = query(this.categoriesRef, ...constraints);
117
139
  const snapshot = await getDocs(q);
118
140
  const categories = snapshot.docs.map(
119
- (doc) =>
141
+ doc =>
120
142
  ({
121
143
  id: doc.id,
122
144
  ...doc.data(),
123
- } as Category)
145
+ } as Category),
124
146
  );
125
147
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
126
148
  return { categories, lastVisible: newLastVisible };
@@ -138,13 +160,13 @@ export class CategoryService extends BaseService {
138
160
  active?: boolean;
139
161
  limit?: number;
140
162
  lastVisible?: DocumentData;
141
- } = {}
163
+ } = {},
142
164
  ) {
143
165
  const { active = true, limit: queryLimit = 10, lastVisible } = options;
144
166
  const constraints = [
145
- where("family", "==", family),
146
- where("isActive", "==", active),
147
- orderBy("name"),
167
+ where('family', '==', family),
168
+ where('isActive', '==', active),
169
+ orderBy('name'),
148
170
  queryLimit ? limit(queryLimit) : undefined,
149
171
  lastVisible ? startAfter(lastVisible) : undefined,
150
172
  ].filter((c): c is NonNullable<typeof c> => !!c);
@@ -152,11 +174,11 @@ export class CategoryService extends BaseService {
152
174
  const q = query(this.categoriesRef, ...constraints);
153
175
  const snapshot = await getDocs(q);
154
176
  const categories = snapshot.docs.map(
155
- (doc) =>
177
+ doc =>
156
178
  ({
157
179
  id: doc.id,
158
180
  ...doc.data(),
159
- } as Category)
181
+ } as Category),
160
182
  );
161
183
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
162
184
  return { categories, lastVisible: newLastVisible };
@@ -168,10 +190,7 @@ export class CategoryService extends BaseService {
168
190
  * @param category - Novi podaci za kategoriju
169
191
  * @returns Ažurirana kategorija
170
192
  */
171
- async update(
172
- id: string,
173
- category: Partial<Omit<Category, "id" | "createdAt">>
174
- ) {
193
+ async update(id: string, category: Partial<Omit<Category, 'id' | 'createdAt'>>) {
175
194
  const updateData = {
176
195
  ...category,
177
196
  updatedAt: new Date(),
@@ -13,14 +13,10 @@ import {
13
13
  startAfter,
14
14
  getCountFromServer,
15
15
  QueryConstraint,
16
- } from "firebase/firestore";
17
- import {
18
- Product,
19
- PRODUCTS_COLLECTION,
20
- IProductService,
21
- } from "../types/product.types";
22
- import { BaseService } from "../../services/base.service";
23
- import { TECHNOLOGIES_COLLECTION } from "../types/technology.types";
16
+ } from 'firebase/firestore';
17
+ import { Product, PRODUCTS_COLLECTION, IProductService } from '../types/product.types';
18
+ import { BaseService } from '../../services/base.service';
19
+ import { TECHNOLOGIES_COLLECTION } from '../types/technology.types';
24
20
 
25
21
  export class ProductService extends BaseService implements IProductService {
26
22
  /**
@@ -29,12 +25,7 @@ export class ProductService extends BaseService implements IProductService {
29
25
  * @returns Firestore collection reference
30
26
  */
31
27
  private getProductsRef(technologyId: string) {
32
- return collection(
33
- this.db,
34
- TECHNOLOGIES_COLLECTION,
35
- technologyId,
36
- PRODUCTS_COLLECTION
37
- );
28
+ return collection(this.db, TECHNOLOGIES_COLLECTION, technologyId, PRODUCTS_COLLECTION);
38
29
  }
39
30
 
40
31
  /**
@@ -43,14 +34,11 @@ export class ProductService extends BaseService implements IProductService {
43
34
  async create(
44
35
  technologyId: string,
45
36
  brandId: string,
46
- product: Omit<
47
- Product,
48
- "id" | "createdAt" | "updatedAt" | "brandId" | "technologyId"
49
- >
37
+ product: Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'brandId' | 'technologyId'>,
50
38
  ): Promise<Product> {
51
39
  const now = new Date();
52
40
  // categoryId and subcategoryId are now expected to be part of the product object
53
- const newProduct: Omit<Product, "id"> = {
41
+ const newProduct: Omit<Product, 'id'> = {
54
42
  ...product,
55
43
  brandId,
56
44
  technologyId,
@@ -59,10 +47,7 @@ export class ProductService extends BaseService implements IProductService {
59
47
  isActive: true,
60
48
  };
61
49
 
62
- const productRef = await addDoc(
63
- this.getProductsRef(technologyId),
64
- newProduct
65
- );
50
+ const productRef = await addDoc(this.getProductsRef(technologyId), newProduct);
66
51
 
67
52
  return { id: productRef.id, ...newProduct };
68
53
  }
@@ -78,27 +63,18 @@ export class ProductService extends BaseService implements IProductService {
78
63
  subcategoryId?: string;
79
64
  technologyId?: string;
80
65
  }): Promise<{ products: Product[]; lastVisible: any }> {
81
- const {
82
- rowsPerPage,
83
- lastVisible,
84
- categoryId,
85
- subcategoryId,
86
- technologyId,
87
- } = options;
66
+ const { rowsPerPage, lastVisible, categoryId, subcategoryId, technologyId } = options;
88
67
 
89
- const constraints: QueryConstraint[] = [
90
- where("isActive", "==", true),
91
- orderBy("name"),
92
- ];
68
+ const constraints: QueryConstraint[] = [where('isActive', '==', true), orderBy('name')];
93
69
 
94
70
  if (categoryId) {
95
- constraints.push(where("categoryId", "==", categoryId));
71
+ constraints.push(where('categoryId', '==', categoryId));
96
72
  }
97
73
  if (subcategoryId) {
98
- constraints.push(where("subcategoryId", "==", subcategoryId));
74
+ constraints.push(where('subcategoryId', '==', subcategoryId));
99
75
  }
100
76
  if (technologyId) {
101
- constraints.push(where("technologyId", "==", technologyId));
77
+ constraints.push(where('technologyId', '==', technologyId));
102
78
  }
103
79
 
104
80
  if (lastVisible) {
@@ -106,18 +82,15 @@ export class ProductService extends BaseService implements IProductService {
106
82
  }
107
83
  constraints.push(limit(rowsPerPage));
108
84
 
109
- const q = query(
110
- collectionGroup(this.db, PRODUCTS_COLLECTION),
111
- ...constraints
112
- );
85
+ const q = query(collectionGroup(this.db, PRODUCTS_COLLECTION), ...constraints);
113
86
  const snapshot = await getDocs(q);
114
87
 
115
88
  const products = snapshot.docs.map(
116
- (doc) =>
89
+ doc =>
117
90
  ({
118
91
  id: doc.id,
119
92
  ...doc.data(),
120
- } as Product)
93
+ } as Product),
121
94
  );
122
95
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
123
96
 
@@ -133,22 +106,19 @@ export class ProductService extends BaseService implements IProductService {
133
106
  technologyId?: string;
134
107
  }): Promise<number> {
135
108
  const { categoryId, subcategoryId, technologyId } = options;
136
- const constraints: QueryConstraint[] = [where("isActive", "==", true)];
109
+ const constraints: QueryConstraint[] = [where('isActive', '==', true)];
137
110
 
138
111
  if (categoryId) {
139
- constraints.push(where("categoryId", "==", categoryId));
112
+ constraints.push(where('categoryId', '==', categoryId));
140
113
  }
141
114
  if (subcategoryId) {
142
- constraints.push(where("subcategoryId", "==", subcategoryId));
115
+ constraints.push(where('subcategoryId', '==', subcategoryId));
143
116
  }
144
117
  if (technologyId) {
145
- constraints.push(where("technologyId", "==", technologyId));
118
+ constraints.push(where('technologyId', '==', technologyId));
146
119
  }
147
120
 
148
- const q = query(
149
- collectionGroup(this.db, PRODUCTS_COLLECTION),
150
- ...constraints
151
- );
121
+ const q = query(collectionGroup(this.db, PRODUCTS_COLLECTION), ...constraints);
152
122
  const snapshot = await getCountFromServer(q);
153
123
  return snapshot.data().count;
154
124
  }
@@ -162,10 +132,7 @@ export class ProductService extends BaseService implements IProductService {
162
132
  bySubcategory: Record<string, number>;
163
133
  byTechnology: Record<string, number>;
164
134
  }> {
165
- const q = query(
166
- collectionGroup(this.db, PRODUCTS_COLLECTION),
167
- where("isActive", "==", true)
168
- );
135
+ const q = query(collectionGroup(this.db, PRODUCTS_COLLECTION), where('isActive', '==', true));
169
136
  const snapshot = await getDocs(q);
170
137
 
171
138
  const counts = {
@@ -178,27 +145,43 @@ export class ProductService extends BaseService implements IProductService {
178
145
  return counts;
179
146
  }
180
147
 
181
- snapshot.docs.forEach((doc) => {
148
+ snapshot.docs.forEach(doc => {
182
149
  const product = doc.data() as Product;
183
150
  const { categoryId, subcategoryId, technologyId } = product;
184
151
 
185
152
  if (categoryId) {
186
- counts.byCategory[categoryId] =
187
- (counts.byCategory[categoryId] || 0) + 1;
153
+ counts.byCategory[categoryId] = (counts.byCategory[categoryId] || 0) + 1;
188
154
  }
189
155
  if (subcategoryId) {
190
- counts.bySubcategory[subcategoryId] =
191
- (counts.bySubcategory[subcategoryId] || 0) + 1;
156
+ counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
192
157
  }
193
158
  if (technologyId) {
194
- counts.byTechnology[technologyId] =
195
- (counts.byTechnology[technologyId] || 0) + 1;
159
+ counts.byTechnology[technologyId] = (counts.byTechnology[technologyId] || 0) + 1;
196
160
  }
197
161
  });
198
162
 
199
163
  return counts;
200
164
  }
201
165
 
166
+ /**
167
+ * Gets all products for a specific technology (non-paginated, for filters/dropdowns)
168
+ */
169
+ async getAllByTechnology(technologyId: string): Promise<Product[]> {
170
+ const q = query(
171
+ this.getProductsRef(technologyId),
172
+ where('isActive', '==', true),
173
+ orderBy('name'),
174
+ );
175
+ const snapshot = await getDocs(q);
176
+ return snapshot.docs.map(
177
+ doc =>
178
+ ({
179
+ id: doc.id,
180
+ ...doc.data(),
181
+ } as Product),
182
+ );
183
+ }
184
+
202
185
  /**
203
186
  * Gets all products for a brand by filtering through all technologies
204
187
  */
@@ -211,18 +194,18 @@ export class ProductService extends BaseService implements IProductService {
211
194
  for (const techDoc of technologiesSnapshot.docs) {
212
195
  const q = query(
213
196
  this.getProductsRef(techDoc.id),
214
- where("brandId", "==", brandId),
215
- where("isActive", "==", true)
197
+ where('brandId', '==', brandId),
198
+ where('isActive', '==', true),
216
199
  );
217
200
  const snapshot = await getDocs(q);
218
201
  products.push(
219
202
  ...snapshot.docs.map(
220
- (doc) =>
203
+ doc =>
221
204
  ({
222
205
  id: doc.id,
223
206
  ...doc.data(),
224
- } as Product)
225
- )
207
+ } as Product),
208
+ ),
226
209
  );
227
210
  }
228
211
 
@@ -235,9 +218,7 @@ export class ProductService extends BaseService implements IProductService {
235
218
  async update(
236
219
  technologyId: string,
237
220
  productId: string,
238
- product: Partial<
239
- Omit<Product, "id" | "createdAt" | "brandId" | "technologyId">
240
- >
221
+ product: Partial<Omit<Product, 'id' | 'createdAt' | 'brandId' | 'technologyId'>>,
241
222
  ): Promise<Product | null> {
242
223
  const updateData = {
243
224
  ...product,
@@ -262,10 +243,7 @@ export class ProductService extends BaseService implements IProductService {
262
243
  /**
263
244
  * Gets a product by ID
264
245
  */
265
- async getById(
266
- technologyId: string,
267
- productId: string
268
- ): Promise<Product | null> {
246
+ async getById(technologyId: string, productId: string): Promise<Product | null> {
269
247
  const docRef = doc(this.getProductsRef(technologyId), productId);
270
248
  const docSnap = await getDoc(docRef);
271
249
  if (!docSnap.exists()) return null;