@blackcode_sa/metaestetics-api 1.11.3 → 1.12.0

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.
Files changed (48) hide show
  1. package/dist/admin/index.d.mts +329 -318
  2. package/dist/admin/index.d.ts +329 -318
  3. package/dist/backoffice/index.d.mts +1166 -430
  4. package/dist/backoffice/index.d.ts +1166 -430
  5. package/dist/backoffice/index.js +1128 -245
  6. package/dist/backoffice/index.mjs +1119 -209
  7. package/dist/index.d.mts +4428 -4035
  8. package/dist/index.d.ts +4428 -4035
  9. package/dist/index.js +1642 -665
  10. package/dist/index.mjs +1406 -401
  11. package/package.json +1 -1
  12. package/src/backoffice/expo-safe/index.ts +3 -0
  13. package/src/backoffice/services/README.md +40 -0
  14. package/src/backoffice/services/brand.service.ts +85 -6
  15. package/src/backoffice/services/category.service.ts +92 -10
  16. package/src/backoffice/services/constants.service.ts +308 -0
  17. package/src/backoffice/services/documentation-template.service.ts +56 -2
  18. package/src/backoffice/services/index.ts +1 -0
  19. package/src/backoffice/services/product.service.ts +126 -5
  20. package/src/backoffice/services/requirement.service.ts +13 -0
  21. package/src/backoffice/services/subcategory.service.ts +184 -13
  22. package/src/backoffice/services/technology.service.ts +344 -129
  23. package/src/backoffice/types/admin-constants.types.ts +69 -0
  24. package/src/backoffice/types/brand.types.ts +1 -0
  25. package/src/backoffice/types/index.ts +1 -0
  26. package/src/backoffice/types/product.types.ts +31 -4
  27. package/src/backoffice/types/static/contraindication.types.ts +1 -0
  28. package/src/backoffice/types/static/treatment-benefit.types.ts +1 -0
  29. package/src/backoffice/types/technology.types.ts +113 -4
  30. package/src/backoffice/validations/schemas.ts +35 -9
  31. package/src/services/appointment/appointment.service.ts +0 -5
  32. package/src/services/appointment/utils/appointment.utils.ts +124 -113
  33. package/src/services/base.service.ts +10 -3
  34. package/src/services/documentation-templates/documentation-template.service.ts +116 -0
  35. package/src/services/media/media.service.ts +2 -2
  36. package/src/services/procedure/procedure.service.ts +436 -234
  37. package/src/types/appointment/index.ts +2 -3
  38. package/src/types/clinic/index.ts +1 -6
  39. package/src/types/patient/medical-info.types.ts +3 -3
  40. package/src/types/procedure/index.ts +20 -17
  41. package/src/validations/clinic.schema.ts +1 -6
  42. package/src/validations/patient/medical-info.schema.ts +7 -2
  43. package/src/backoffice/services/__tests__/brand.service.test.ts +0 -196
  44. package/src/backoffice/services/__tests__/category.service.test.ts +0 -201
  45. package/src/backoffice/services/__tests__/product.service.test.ts +0 -358
  46. package/src/backoffice/services/__tests__/requirement.service.test.ts +0 -226
  47. package/src/backoffice/services/__tests__/subcategory.service.test.ts +0 -181
  48. package/src/backoffice/services/__tests__/technology.service.test.ts +0 -1097
@@ -21,8 +21,10 @@ export class DocumentationTemplateServiceBackoffice {
21
21
  * @param auth - Firebase Auth instance
22
22
  * @param app - Firebase App instance
23
23
  */
24
- constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
25
- this.apiService = new ApiDocumentationTemplateService(db, auth, app);
24
+ constructor(
25
+ ...args: ConstructorParameters<typeof ApiDocumentationTemplateService>
26
+ ) {
27
+ this.apiService = new ApiDocumentationTemplateService(...args);
26
28
  }
27
29
 
28
30
  /**
@@ -88,6 +90,58 @@ export class DocumentationTemplateServiceBackoffice {
88
90
  return this.apiService.getActiveTemplates(pageSize, lastDoc);
89
91
  }
90
92
 
93
+ /**
94
+ * Get all active templates with optional filters and pagination.
95
+ * @param options - Options for filtering and pagination.
96
+ * @returns A promise that resolves to the templates and the last visible document.
97
+ */
98
+ async getTemplates(options: {
99
+ pageSize?: number;
100
+ lastDoc?: QueryDocumentSnapshot<DocumentTemplate>;
101
+ isUserForm?: boolean;
102
+ isRequired?: boolean;
103
+ sortingOrder?: number;
104
+ }): Promise<{
105
+ templates: DocumentTemplate[];
106
+ lastDoc: QueryDocumentSnapshot<DocumentTemplate> | null;
107
+ }> {
108
+ return this.apiService.getTemplates(options);
109
+ }
110
+
111
+ /**
112
+ * Get the total count of active templates with optional filters.
113
+ * @param options - Options for filtering.
114
+ * @returns A promise that resolves to the total count of templates.
115
+ */
116
+ async getTemplatesCount(options: {
117
+ isUserForm?: boolean;
118
+ isRequired?: boolean;
119
+ sortingOrder?: number;
120
+ search?: string;
121
+ }): Promise<number> {
122
+ return this.apiService.getTemplatesCount(options);
123
+ }
124
+
125
+ /**
126
+ * Get all active templates without pagination for filtering purposes.
127
+ * @returns A promise that resolves to an array of all active templates.
128
+ */
129
+ async getAllActiveTemplates(): Promise<DocumentTemplate[]> {
130
+ return this.apiService.getAllActiveTemplates();
131
+ }
132
+
133
+ /**
134
+ * Searches for active templates by title.
135
+ * @param title - The title to search for.
136
+ * @returns A list of templates that match the search criteria.
137
+ */
138
+ async search(title: string): Promise<DocumentTemplate[]> {
139
+ const { templates } = await this.apiService.getActiveTemplates(1000); // A large number to get all templates
140
+ return templates.filter((t) =>
141
+ t.title.toLowerCase().includes(title.toLowerCase())
142
+ );
143
+ }
144
+
91
145
  /**
92
146
  * Get templates by tags
93
147
  * @param tags - Tags to filter by
@@ -5,3 +5,4 @@ export * from "./product.service";
5
5
  export * from "./requirement.service";
6
6
  export * from "./subcategory.service";
7
7
  export * from "./technology.service";
8
+ export * from "./constants.service";
@@ -1,12 +1,18 @@
1
1
  import {
2
2
  addDoc,
3
3
  collection,
4
+ collectionGroup,
4
5
  doc,
5
6
  getDoc,
6
7
  getDocs,
7
8
  query,
8
9
  updateDoc,
9
10
  where,
11
+ limit,
12
+ orderBy,
13
+ startAfter,
14
+ getCountFromServer,
15
+ QueryConstraint,
10
16
  } from "firebase/firestore";
11
17
  import {
12
18
  Product,
@@ -43,6 +49,7 @@ export class ProductService extends BaseService implements IProductService {
43
49
  >
44
50
  ): Promise<Product> {
45
51
  const now = new Date();
52
+ // categoryId and subcategoryId are now expected to be part of the product object
46
53
  const newProduct: Omit<Product, "id"> = {
47
54
  ...product,
48
55
  brandId,
@@ -61,21 +68,135 @@ export class ProductService extends BaseService implements IProductService {
61
68
  }
62
69
 
63
70
  /**
64
- * Gets all products for a technology
71
+ * Gets a paginated list of all products, with optional filters.
72
+ * This uses a collectionGroup query to search across all technologies.
65
73
  */
66
- async getAllByTechnology(technologyId: string): Promise<Product[]> {
74
+ async getAll(options: {
75
+ rowsPerPage: number;
76
+ lastVisible?: any;
77
+ categoryId?: string;
78
+ subcategoryId?: string;
79
+ technologyId?: string;
80
+ }): Promise<{ products: Product[]; lastVisible: any }> {
81
+ const {
82
+ rowsPerPage,
83
+ lastVisible,
84
+ categoryId,
85
+ subcategoryId,
86
+ technologyId,
87
+ } = options;
88
+
89
+ const constraints: QueryConstraint[] = [
90
+ where("isActive", "==", true),
91
+ orderBy("name"),
92
+ ];
93
+
94
+ if (categoryId) {
95
+ constraints.push(where("categoryId", "==", categoryId));
96
+ }
97
+ if (subcategoryId) {
98
+ constraints.push(where("subcategoryId", "==", subcategoryId));
99
+ }
100
+ if (technologyId) {
101
+ constraints.push(where("technologyId", "==", technologyId));
102
+ }
103
+
104
+ if (lastVisible) {
105
+ constraints.push(startAfter(lastVisible));
106
+ }
107
+ constraints.push(limit(rowsPerPage));
108
+
67
109
  const q = query(
68
- this.getProductsRef(technologyId),
69
- where("isActive", "==", true)
110
+ collectionGroup(this.db, PRODUCTS_COLLECTION),
111
+ ...constraints
70
112
  );
71
113
  const snapshot = await getDocs(q);
72
- return snapshot.docs.map(
114
+
115
+ const products = snapshot.docs.map(
73
116
  (doc) =>
74
117
  ({
75
118
  id: doc.id,
76
119
  ...doc.data(),
77
120
  } as Product)
78
121
  );
122
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
123
+
124
+ return { products, lastVisible: newLastVisible };
125
+ }
126
+
127
+ /**
128
+ * Gets the total count of active products, with optional filters.
129
+ */
130
+ async getProductsCount(options: {
131
+ categoryId?: string;
132
+ subcategoryId?: string;
133
+ technologyId?: string;
134
+ }): Promise<number> {
135
+ const { categoryId, subcategoryId, technologyId } = options;
136
+ const constraints: QueryConstraint[] = [where("isActive", "==", true)];
137
+
138
+ if (categoryId) {
139
+ constraints.push(where("categoryId", "==", categoryId));
140
+ }
141
+ if (subcategoryId) {
142
+ constraints.push(where("subcategoryId", "==", subcategoryId));
143
+ }
144
+ if (technologyId) {
145
+ constraints.push(where("technologyId", "==", technologyId));
146
+ }
147
+
148
+ const q = query(
149
+ collectionGroup(this.db, PRODUCTS_COLLECTION),
150
+ ...constraints
151
+ );
152
+ const snapshot = await getCountFromServer(q);
153
+ return snapshot.data().count;
154
+ }
155
+
156
+ /**
157
+ * Gets counts of active products grouped by category, subcategory, and technology.
158
+ * This uses a single collectionGroup query for efficiency.
159
+ */
160
+ async getProductCounts(): Promise<{
161
+ byCategory: Record<string, number>;
162
+ bySubcategory: Record<string, number>;
163
+ byTechnology: Record<string, number>;
164
+ }> {
165
+ const q = query(
166
+ collectionGroup(this.db, PRODUCTS_COLLECTION),
167
+ where("isActive", "==", true)
168
+ );
169
+ const snapshot = await getDocs(q);
170
+
171
+ const counts = {
172
+ byCategory: {} as Record<string, number>,
173
+ bySubcategory: {} as Record<string, number>,
174
+ byTechnology: {} as Record<string, number>,
175
+ };
176
+
177
+ if (snapshot.empty) {
178
+ return counts;
179
+ }
180
+
181
+ snapshot.docs.forEach((doc) => {
182
+ const product = doc.data() as Product;
183
+ const { categoryId, subcategoryId, technologyId } = product;
184
+
185
+ if (categoryId) {
186
+ counts.byCategory[categoryId] =
187
+ (counts.byCategory[categoryId] || 0) + 1;
188
+ }
189
+ if (subcategoryId) {
190
+ counts.bySubcategory[subcategoryId] =
191
+ (counts.bySubcategory[subcategoryId] || 0) + 1;
192
+ }
193
+ if (technologyId) {
194
+ counts.byTechnology[technologyId] =
195
+ (counts.byTechnology[technologyId] || 0) + 1;
196
+ }
197
+ });
198
+
199
+ return counts;
79
200
  }
80
201
 
81
202
  /**
@@ -101,6 +101,19 @@ export class RequirementService extends BaseService {
101
101
  );
102
102
  }
103
103
 
104
+ /**
105
+ * Searches for requirements by name.
106
+ * @param name - The name to search for.
107
+ * @param type - The type of requirement (pre/post).
108
+ * @returns A list of requirements that match the search criteria.
109
+ */
110
+ async search(name: string, type: RequirementType) {
111
+ const requirements = await this.getAllByType(type);
112
+ return requirements.filter((r) =>
113
+ r.name.toLowerCase().includes(name.toLowerCase())
114
+ );
115
+ }
116
+
104
117
  /**
105
118
  * Ažurira postojeći zahtev
106
119
  * @param id - ID zahteva koji se ažurira
@@ -1,10 +1,18 @@
1
1
  import {
2
2
  addDoc,
3
3
  collection,
4
+ collectionGroup,
5
+ deleteDoc,
4
6
  doc,
7
+ DocumentData,
8
+ getCountFromServer,
5
9
  getDoc,
6
10
  getDocs,
11
+ limit,
12
+ orderBy,
7
13
  query,
14
+ setDoc,
15
+ startAfter,
8
16
  updateDoc,
9
17
  where,
10
18
  } from "firebase/firestore";
@@ -69,17 +77,133 @@ export class SubcategoryService extends BaseService {
69
77
  }
70
78
 
71
79
  /**
72
- * Vraća sve aktivne podkategorije za određenu kategoriju
80
+ * Returns counts of subcategories for all categories.
81
+ * @param active - Whether to count active or inactive subcategories.
82
+ * @returns A record mapping category ID to subcategory count.
83
+ */
84
+ async getSubcategoryCounts(active = true) {
85
+ const categoriesRef = collection(this.db, CATEGORIES_COLLECTION);
86
+ const categoriesSnapshot = await getDocs(categoriesRef);
87
+ const counts: Record<string, number> = {};
88
+
89
+ for (const categoryDoc of categoriesSnapshot.docs) {
90
+ const categoryId = categoryDoc.id;
91
+ const subcategoriesRef = this.getSubcategoriesRef(categoryId);
92
+ const q = query(subcategoriesRef, where("isActive", "==", active));
93
+ const snapshot = await getCountFromServer(q);
94
+ counts[categoryId] = snapshot.data().count;
95
+ }
96
+
97
+ return counts;
98
+ }
99
+
100
+ /**
101
+ * Vraća sve aktivne podkategorije za određenu kategoriju sa paginacijom
73
102
  * @param categoryId - ID kategorije čije podkategorije tražimo
74
- * @returns Lista aktivnih podkategorija
103
+ * @param options - Pagination options
104
+ * @returns Lista aktivnih podkategorija i poslednji vidljiv dokument
75
105
  */
76
- async getAllByCategoryId(categoryId: string) {
106
+ async getAllByCategoryId(
107
+ categoryId: string,
108
+ options: {
109
+ active?: boolean;
110
+ limit?: number;
111
+ lastVisible?: DocumentData;
112
+ } = {}
113
+ ) {
114
+ const { active = true, limit: queryLimit = 10, lastVisible } = options;
115
+ const constraints = [
116
+ where("isActive", "==", active),
117
+ orderBy("name"),
118
+ queryLimit ? limit(queryLimit) : undefined,
119
+ lastVisible ? startAfter(lastVisible) : undefined,
120
+ ].filter((c): c is NonNullable<typeof c> => !!c);
121
+
122
+ const q = query(this.getSubcategoriesRef(categoryId), ...constraints);
123
+
124
+ const querySnapshot = await getDocs(q);
125
+ const subcategories = querySnapshot.docs.map(
126
+ (doc) =>
127
+ ({
128
+ id: doc.id,
129
+ ...doc.data(),
130
+ } as Subcategory)
131
+ );
132
+ const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
133
+ return { subcategories, lastVisible: newLastVisible };
134
+ }
135
+
136
+ /**
137
+ * Vraća sve podkategorije sa paginacijom koristeći collection group query.
138
+ * NOTE: This query requires a composite index in Firestore on the 'subcategories' collection group.
139
+ * The index should be on 'isActive' (ascending) and 'name' (ascending).
140
+ * Firestore will provide a link to create this index in the console error if it's missing.
141
+ * @param options - Pagination options
142
+ * @returns Lista podkategorija i poslednji vidljiv dokument
143
+ */
144
+ async getAll(
145
+ options: {
146
+ active?: boolean;
147
+ limit?: number;
148
+ lastVisible?: DocumentData;
149
+ } = {}
150
+ ) {
151
+ const { active = true, limit: queryLimit = 10, lastVisible } = options;
152
+ const constraints = [
153
+ where("isActive", "==", active),
154
+ orderBy("name"),
155
+ queryLimit ? limit(queryLimit) : undefined,
156
+ lastVisible ? startAfter(lastVisible) : undefined,
157
+ ].filter((c): c is NonNullable<typeof c> => !!c);
158
+
159
+ const q = query(
160
+ collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
161
+ ...constraints
162
+ );
163
+
164
+ const querySnapshot = await getDocs(q);
165
+ const subcategories = querySnapshot.docs.map(
166
+ (doc) =>
167
+ ({
168
+ id: doc.id,
169
+ ...doc.data(),
170
+ } as Subcategory)
171
+ );
172
+ const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
173
+ return { subcategories, lastVisible: newLastVisible };
174
+ }
175
+
176
+ /**
177
+ * Vraća sve subkategorije za određenu kategoriju za potrebe filtera (bez paginacije)
178
+ * @param categoryId - ID kategorije čije subkategorije tražimo
179
+ * @returns Lista svih aktivnih subkategorija
180
+ */
181
+ async getAllForFilterByCategoryId(categoryId: string) {
77
182
  const q = query(
78
183
  this.getSubcategoriesRef(categoryId),
79
184
  where("isActive", "==", true)
80
185
  );
81
- const snapshot = await getDocs(q);
82
- return snapshot.docs.map(
186
+ const querySnapshot = await getDocs(q);
187
+ return querySnapshot.docs.map(
188
+ (doc) =>
189
+ ({
190
+ id: doc.id,
191
+ ...doc.data(),
192
+ } as Subcategory)
193
+ );
194
+ }
195
+
196
+ /**
197
+ * Vraća sve subkategorije za potrebe filtera (bez paginacije)
198
+ * @returns Lista svih aktivnih subkategorija
199
+ */
200
+ async getAllForFilter() {
201
+ const q = query(
202
+ collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
203
+ where("isActive", "==", true)
204
+ );
205
+ const querySnapshot = await getDocs(q);
206
+ return querySnapshot.docs.map(
83
207
  (doc) =>
84
208
  ({
85
209
  id: doc.id,
@@ -98,16 +222,54 @@ export class SubcategoryService extends BaseService {
98
222
  async update(
99
223
  categoryId: string,
100
224
  subcategoryId: string,
101
- subcategory: Partial<Omit<Subcategory, "id" | "createdAt" | "categoryId">>
225
+ subcategory: Partial<Omit<Subcategory, "id" | "createdAt">>
102
226
  ) {
103
- const updateData = {
104
- ...subcategory,
105
- updatedAt: new Date(),
106
- };
227
+ const newCategoryId = subcategory.categoryId;
107
228
 
108
- const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
109
- await updateDoc(docRef, updateData);
110
- return this.getById(categoryId, subcategoryId);
229
+ if (newCategoryId && newCategoryId !== categoryId) {
230
+ // Category has changed, move the document
231
+ const oldDocRef = doc(
232
+ this.getSubcategoriesRef(categoryId),
233
+ subcategoryId
234
+ );
235
+ const docSnap = await getDoc(oldDocRef);
236
+
237
+ if (!docSnap.exists()) {
238
+ throw new Error("Subcategory to update does not exist.");
239
+ }
240
+
241
+ const existingData = docSnap.data();
242
+ const newData: Omit<Subcategory, "id"> = {
243
+ ...(existingData as Omit<
244
+ Subcategory,
245
+ "id" | "createdAt" | "updatedAt"
246
+ >),
247
+ ...subcategory,
248
+ categoryId: newCategoryId, // Ensure categoryId is updated
249
+ createdAt: existingData.createdAt, // Preserve original creation date
250
+ updatedAt: new Date(),
251
+ };
252
+
253
+ const newDocRef = doc(
254
+ this.getSubcategoriesRef(newCategoryId),
255
+ subcategoryId
256
+ );
257
+
258
+ await setDoc(newDocRef, newData);
259
+ await deleteDoc(oldDocRef);
260
+
261
+ return { id: subcategoryId, ...newData };
262
+ } else {
263
+ // Category has not changed, just update the document
264
+ const updateData = {
265
+ ...subcategory,
266
+ updatedAt: new Date(),
267
+ };
268
+
269
+ const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
270
+ await updateDoc(docRef, updateData);
271
+ return this.getById(categoryId, subcategoryId);
272
+ }
111
273
  }
112
274
 
113
275
  /**
@@ -119,6 +281,15 @@ export class SubcategoryService extends BaseService {
119
281
  await this.update(categoryId, subcategoryId, { isActive: false });
120
282
  }
121
283
 
284
+ /**
285
+ * Reactivates a subcategory by setting its isActive flag to true.
286
+ * @param categoryId - The ID of the category to which the subcategory belongs.
287
+ * @param subcategoryId - The ID of the subcategory to reactivate.
288
+ */
289
+ async reactivate(categoryId: string, subcategoryId: string) {
290
+ await this.update(categoryId, subcategoryId, { isActive: true });
291
+ }
292
+
122
293
  /**
123
294
  * Vraća podkategoriju po ID-u
124
295
  * @param categoryId - ID kategorije kojoj pripada podkategorija