@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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.11.3",
4
+ "version": "1.12.0",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -10,6 +10,7 @@ export { CategoryService } from "../services/category.service";
10
10
  export { SubcategoryService } from "../services/subcategory.service";
11
11
  export { TechnologyService } from "../services/technology.service";
12
12
  export { ProductService } from "../services/product.service";
13
+ export { ConstantsService } from "../services/constants.service";
13
14
 
14
15
  // Backoffice types
15
16
  export type { Brand } from "../types/brand.types";
@@ -34,4 +35,6 @@ export {
34
35
  export { Contraindication } from "../types/static/contraindication.types";
35
36
  export { ProcedureFamily } from "../types/static/procedure-family.types";
36
37
  export { TreatmentBenefit } from "../types/static/treatment-benefit.types";
38
+ export { TreatmentBenefitDynamic } from "../types/";
39
+ export { ContraindicationDynamic } from "../types/";
37
40
  export { RequirementType, TimeUnit } from "../types/requirement.types";
@@ -0,0 +1,40 @@
1
+ # Backoffice Services
2
+
3
+ This directory contains services used by the backoffice application.
4
+
5
+ ## Services
6
+
7
+ ### `CategoryService`
8
+
9
+ Manages procedure categories. Categories are the first level of organization after procedure family (aesthetics/surgery).
10
+
11
+ - **`create(category)`**: Creates a new category.
12
+ - **`getAll()`**: Retrieves all active categories.
13
+ - **`getAllByFamily(family)`**: Retrieves all active categories for a specific procedure family.
14
+ - **`update(id, category)`**: Updates an existing category.
15
+ - **`delete(id)`**: Soft deletes a category.
16
+ - **`getById(id)`**: Retrieves a category by its ID.
17
+
18
+ ### `ProductService`
19
+
20
+ Manages products, which are sub-items of a `Technology`.
21
+
22
+ - **`create(technologyId, brandId, product)`**: Creates a new product under a technology.
23
+ - **`getAllByTechnology(technologyId)`**: Retrieves all products for a technology.
24
+ - **`getAllByBrand(brandId)`**: Retrieves all products for a brand.
25
+ - **`update(technologyId, productId, product)`**: Updates a product.
26
+ - **`delete(technologyId, productId)`**: Soft deletes a product.
27
+ - **`getById(technologyId, productId)`**: Retrieves a product by its ID.
28
+
29
+ ### `ConstantsService`
30
+
31
+ Manages administrative constants like treatment benefits and contraindications.
32
+
33
+ - **`getTreatmentBenefits()`**: Retrieves all treatment benefits.
34
+ - **`addTreatmentBenefit(benefit)`**: Adds a new treatment benefit.
35
+ - **`updateTreatmentBenefit(benefit)`**: Updates an existing treatment benefit.
36
+ - **`deleteTreatmentBenefit(benefitId)`**: Deletes a treatment benefit.
37
+ - **`getContraindications()`**: Retrieves all contraindications.
38
+ - **`addContraindication(contraindication)`**: Adds a new contraindication.
39
+ - **`updateContraindication(contraindication)`**: Updates an existing contraindication.
40
+ - **`deleteContraindication(contraindicationId)`**: Deletes a contraindication.
@@ -7,6 +7,13 @@ import {
7
7
  query,
8
8
  updateDoc,
9
9
  where,
10
+ limit,
11
+ orderBy,
12
+ startAfter,
13
+ getCountFromServer,
14
+ Query,
15
+ DocumentData,
16
+ QueryConstraint,
10
17
  } from "firebase/firestore";
11
18
  import { Brand, BRANDS_COLLECTION } from "../types/brand.types";
12
19
  import { BaseService } from "../../services/base.service";
@@ -22,10 +29,13 @@ export class BrandService extends BaseService {
22
29
  /**
23
30
  * Creates a new brand
24
31
  */
25
- async create(brand: Omit<Brand, "id" | "createdAt" | "updatedAt">) {
32
+ async create(
33
+ brand: Omit<Brand, "id" | "createdAt" | "updatedAt" | "name_lowercase">
34
+ ) {
26
35
  const now = new Date();
27
36
  const newBrand: Omit<Brand, "id"> = {
28
37
  ...brand,
38
+ name_lowercase: brand.name.toLowerCase(),
29
39
  createdAt: now,
30
40
  updatedAt: now,
31
41
  isActive: true,
@@ -36,10 +46,75 @@ export class BrandService extends BaseService {
36
46
  }
37
47
 
38
48
  /**
39
- * Gets all active brands
49
+ * Gets a paginated list of active brands, optionally filtered by name.
50
+ * @param rowsPerPage - The number of brands to fetch.
51
+ * @param searchTerm - An optional string to filter brand names by (starts-with search).
52
+ * @param lastVisible - An optional document snapshot to use as a cursor for pagination.
53
+ */
54
+ async getAll(rowsPerPage: number, searchTerm?: string, lastVisible?: any) {
55
+ const constraints: QueryConstraint[] = [
56
+ where("isActive", "==", true),
57
+ orderBy("name_lowercase"),
58
+ ];
59
+
60
+ if (searchTerm) {
61
+ const lowercasedSearchTerm = searchTerm.toLowerCase();
62
+ constraints.push(where("name_lowercase", ">=", lowercasedSearchTerm));
63
+ constraints.push(
64
+ where("name_lowercase", "<=", lowercasedSearchTerm + "\uf8ff")
65
+ );
66
+ }
67
+
68
+ if (lastVisible) {
69
+ constraints.push(startAfter(lastVisible));
70
+ }
71
+
72
+ constraints.push(limit(rowsPerPage));
73
+
74
+ const q = query(this.getBrandsRef(), ...constraints);
75
+ const snapshot = await getDocs(q);
76
+
77
+ const brands = snapshot.docs.map(
78
+ (doc) =>
79
+ ({
80
+ id: doc.id,
81
+ ...doc.data(),
82
+ } as Brand)
83
+ );
84
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
85
+
86
+ return { brands, lastVisible: newLastVisible };
87
+ }
88
+
89
+ /**
90
+ * Gets the total count of active brands, optionally filtered by name.
91
+ * @param searchTerm - An optional string to filter brand names by (starts-with search).
40
92
  */
41
- async getAll() {
42
- const q = query(this.getBrandsRef(), where("isActive", "==", true));
93
+ async getBrandsCount(searchTerm?: string) {
94
+ const constraints: QueryConstraint[] = [where("isActive", "==", true)];
95
+
96
+ if (searchTerm) {
97
+ const lowercasedSearchTerm = searchTerm.toLowerCase();
98
+ constraints.push(where("name_lowercase", ">=", lowercasedSearchTerm));
99
+ constraints.push(
100
+ where("name_lowercase", "<=", lowercasedSearchTerm + "\uf8ff")
101
+ );
102
+ }
103
+
104
+ const q = query(this.getBrandsRef(), ...constraints);
105
+ const snapshot = await getCountFromServer(q);
106
+ return snapshot.data().count;
107
+ }
108
+
109
+ /**
110
+ * Gets all active brands for filter dropdowns (not paginated).
111
+ */
112
+ async getAllForFilter(): Promise<Brand[]> {
113
+ const q = query(
114
+ this.getBrandsRef(),
115
+ where("isActive", "==", true),
116
+ orderBy("name")
117
+ );
43
118
  const snapshot = await getDocs(q);
44
119
  return snapshot.docs.map(
45
120
  (doc) =>
@@ -55,13 +130,17 @@ export class BrandService extends BaseService {
55
130
  */
56
131
  async update(
57
132
  brandId: string,
58
- brand: Partial<Omit<Brand, "id" | "createdAt">>
133
+ brand: Partial<Omit<Brand, "id" | "createdAt" | "name_lowercase">>
59
134
  ) {
60
- const updateData = {
135
+ const updateData: { [key: string]: any } = {
61
136
  ...brand,
62
137
  updatedAt: new Date(),
63
138
  };
64
139
 
140
+ if (brand.name) {
141
+ updateData.name_lowercase = brand.name.toLowerCase();
142
+ }
143
+
65
144
  const docRef = doc(this.getBrandsRef(), brandId);
66
145
  await updateDoc(docRef, updateData);
67
146
  return this.getById(brandId);
@@ -2,9 +2,14 @@ import {
2
2
  addDoc,
3
3
  collection,
4
4
  doc,
5
+ DocumentData,
6
+ getCountFromServer,
5
7
  getDoc,
6
8
  getDocs,
9
+ limit,
10
+ orderBy,
7
11
  query,
12
+ startAfter,
8
13
  updateDoc,
9
14
  where,
10
15
  } from "firebase/firestore";
@@ -52,10 +57,31 @@ export class CategoryService extends BaseService {
52
57
  }
53
58
 
54
59
  /**
55
- * Vraća sve aktivne kategorije
56
- * @returns Lista aktivnih kategorija
60
+ * Returns counts of categories for each family.
61
+ * @param active - Whether to count active or inactive categories.
62
+ * @returns A record mapping family to category count.
57
63
  */
58
- async getAll() {
64
+ async getCategoryCounts(active = true) {
65
+ const counts: Record<string, number> = {};
66
+ const families = Object.values(ProcedureFamily);
67
+
68
+ for (const family of families) {
69
+ const q = query(
70
+ this.categoriesRef,
71
+ where("family", "==", family),
72
+ where("isActive", "==", active)
73
+ );
74
+ const snapshot = await getCountFromServer(q);
75
+ counts[family] = snapshot.data().count;
76
+ }
77
+ return counts;
78
+ }
79
+
80
+ /**
81
+ * Vraća sve kategorije za potrebe filtera (bez paginacije)
82
+ * @returns Lista svih aktivnih kategorija
83
+ */
84
+ async getAllForFilter() {
59
85
  const q = query(this.categoriesRef, where("isActive", "==", true));
60
86
  const snapshot = await getDocs(q);
61
87
  return snapshot.docs.map(
@@ -68,24 +94,72 @@ export class CategoryService extends BaseService {
68
94
  }
69
95
 
70
96
  /**
71
- * Vraća sve aktivne kategorije za određenu familiju procedura
97
+ * Vraća sve kategorije sa paginacijom
98
+ * @param options - Pagination and filter options
99
+ * @returns Lista kategorija i poslednji vidljiv dokument
100
+ */
101
+ async getAll(
102
+ options: {
103
+ active?: boolean;
104
+ limit?: number;
105
+ lastVisible?: DocumentData;
106
+ } = {}
107
+ ) {
108
+ const { active = true, limit: queryLimit = 10, lastVisible } = options;
109
+ const constraints = [
110
+ where("isActive", "==", active),
111
+ orderBy("name"),
112
+ queryLimit ? limit(queryLimit) : undefined,
113
+ lastVisible ? startAfter(lastVisible) : undefined,
114
+ ].filter((c): c is NonNullable<typeof c> => !!c);
115
+
116
+ const q = query(this.categoriesRef, ...constraints);
117
+ const snapshot = await getDocs(q);
118
+ const categories = snapshot.docs.map(
119
+ (doc) =>
120
+ ({
121
+ id: doc.id,
122
+ ...doc.data(),
123
+ } as Category)
124
+ );
125
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
126
+ return { categories, lastVisible: newLastVisible };
127
+ }
128
+
129
+ /**
130
+ * Vraća sve aktivne kategorije za određenu familiju procedura sa paginacijom
72
131
  * @param family - Familija procedura (aesthetics/surgery)
132
+ * @param options - Pagination options
73
133
  * @returns Lista kategorija koje pripadaju traženoj familiji
74
134
  */
75
- async getAllByFamily(family: ProcedureFamily) {
76
- const q = query(
77
- this.categoriesRef,
135
+ async getAllByFamily(
136
+ family: ProcedureFamily,
137
+ options: {
138
+ active?: boolean;
139
+ limit?: number;
140
+ lastVisible?: DocumentData;
141
+ } = {}
142
+ ) {
143
+ const { active = true, limit: queryLimit = 10, lastVisible } = options;
144
+ const constraints = [
78
145
  where("family", "==", family),
79
- where("isActive", "==", true)
80
- );
146
+ where("isActive", "==", active),
147
+ orderBy("name"),
148
+ queryLimit ? limit(queryLimit) : undefined,
149
+ lastVisible ? startAfter(lastVisible) : undefined,
150
+ ].filter((c): c is NonNullable<typeof c> => !!c);
151
+
152
+ const q = query(this.categoriesRef, ...constraints);
81
153
  const snapshot = await getDocs(q);
82
- return snapshot.docs.map(
154
+ const categories = snapshot.docs.map(
83
155
  (doc) =>
84
156
  ({
85
157
  id: doc.id,
86
158
  ...doc.data(),
87
159
  } as Category)
88
160
  );
161
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
162
+ return { categories, lastVisible: newLastVisible };
89
163
  }
90
164
 
91
165
  /**
@@ -116,6 +190,14 @@ export class CategoryService extends BaseService {
116
190
  await this.update(id, { isActive: false });
117
191
  }
118
192
 
193
+ /**
194
+ * Reactivates a category by setting its isActive flag to true.
195
+ * @param id - The ID of the category to reactivate.
196
+ */
197
+ async reactivate(id: string) {
198
+ await this.update(id, { isActive: true });
199
+ }
200
+
119
201
  /**
120
202
  * Vraća kategoriju po ID-u
121
203
  * @param id - ID tražene kategorije
@@ -0,0 +1,308 @@
1
+ import {
2
+ arrayRemove,
3
+ arrayUnion,
4
+ collection,
5
+ doc,
6
+ getDoc,
7
+ setDoc,
8
+ updateDoc,
9
+ } from "firebase/firestore";
10
+ import { BaseService } from "../../services/base.service";
11
+ import {
12
+ ContraindicationDynamic,
13
+ ContraindicationsDocument,
14
+ TreatmentBenefitDynamic,
15
+ TreatmentBenefitsDocument,
16
+ } from "../types/admin-constants.types";
17
+
18
+ const ADMIN_CONSTANTS_COLLECTION = "admin-constants";
19
+ const TREATMENT_BENEFITS_DOC = "treatment-benefits";
20
+ const CONTRAINDICATIONS_DOC = "contraindications";
21
+
22
+ /**
23
+ * @class ConstantsService
24
+ * @description Service for managing administrative constants, such as treatment benefits and contraindications.
25
+ * These constants are stored in a single Firestore collection 'admin-constants',
26
+ * with each type of constant in its own document ('treatment-benefits', 'contraindications').
27
+ * The constants themselves are stored in an array within these documents.
28
+ * @extends {BaseService}
29
+ */
30
+ export class ConstantsService extends BaseService {
31
+ /**
32
+ * @description Gets the reference to the document holding treatment benefits.
33
+ * @private
34
+ * @type {DocumentReference}
35
+ */
36
+ private get treatmentBenefitsDocRef() {
37
+ return doc(this.db, ADMIN_CONSTANTS_COLLECTION, TREATMENT_BENEFITS_DOC);
38
+ }
39
+
40
+ /**
41
+ * @description Gets the reference to the document holding contraindications.
42
+ * @private
43
+ * @type {DocumentReference}
44
+ */
45
+ private get contraindicationsDocRef() {
46
+ return doc(this.db, ADMIN_CONSTANTS_COLLECTION, CONTRAINDICATIONS_DOC);
47
+ }
48
+
49
+ // =================================================================
50
+ // Treatment Benefits
51
+ // =================================================================
52
+
53
+ /**
54
+ * @description Retrieves all treatment benefits without pagination.
55
+ * @returns {Promise<TreatmentBenefitDynamic[]>} An array of all treatment benefits.
56
+ */
57
+ async getAllBenefitsForFilter(): Promise<TreatmentBenefitDynamic[]> {
58
+ const docSnap = await getDoc(this.treatmentBenefitsDocRef);
59
+ if (!docSnap.exists()) {
60
+ return [];
61
+ }
62
+ return (docSnap.data() as TreatmentBenefitsDocument).benefits;
63
+ }
64
+
65
+ /**
66
+ * @description Retrieves a paginated list of treatment benefits.
67
+ * @param {{ page: number; limit: number }} options - Pagination options.
68
+ * @returns {Promise<{ benefits: TreatmentBenefitDynamic[]; total: number }>} A paginated list of benefits and the total count.
69
+ */
70
+ async getAllBenefits(options: {
71
+ page: number;
72
+ limit: number;
73
+ }): Promise<{ benefits: TreatmentBenefitDynamic[]; total: number }> {
74
+ const allBenefits = await this.getAllBenefitsForFilter();
75
+ const { page, limit } = options;
76
+ const startIndex = page * limit;
77
+ const endIndex = startIndex + limit;
78
+ const paginatedBenefits = allBenefits.slice(startIndex, endIndex);
79
+ return { benefits: paginatedBenefits, total: allBenefits.length };
80
+ }
81
+
82
+ /**
83
+ * @description Adds a new treatment benefit.
84
+ * @param {Omit<TreatmentBenefitDynamic, "id">} benefit - The treatment benefit to add, without an ID.
85
+ * @returns {Promise<TreatmentBenefitDynamic>} The newly created treatment benefit with its generated ID.
86
+ */
87
+ async addTreatmentBenefit(
88
+ benefit: Omit<TreatmentBenefitDynamic, "id">
89
+ ): Promise<TreatmentBenefitDynamic> {
90
+ const newBenefit: TreatmentBenefitDynamic = {
91
+ id: this.generateId(),
92
+ ...benefit,
93
+ };
94
+
95
+ const docSnap = await getDoc(this.treatmentBenefitsDocRef);
96
+ if (!docSnap.exists()) {
97
+ await setDoc(this.treatmentBenefitsDocRef, { benefits: [newBenefit] });
98
+ } else {
99
+ await updateDoc(this.treatmentBenefitsDocRef, {
100
+ benefits: arrayUnion(newBenefit),
101
+ });
102
+ }
103
+
104
+ return newBenefit;
105
+ }
106
+
107
+ /**
108
+ * @description Retrieves a single treatment benefit by its ID.
109
+ * @param {string} benefitId - The ID of the treatment benefit to retrieve.
110
+ * @returns {Promise<TreatmentBenefitDynamic | undefined>} The found treatment benefit or undefined.
111
+ */
112
+ async getBenefitById(
113
+ benefitId: string
114
+ ): Promise<TreatmentBenefitDynamic | undefined> {
115
+ const benefits = await this.getAllBenefitsForFilter();
116
+ return benefits.find((b) => b.id === benefitId);
117
+ }
118
+
119
+ /**
120
+ * @description Searches for treatment benefits by name (case-insensitive).
121
+ * @param {string} searchTerm - The term to search for in the benefit names.
122
+ * @returns {Promise<TreatmentBenefitDynamic[]>} An array of matching treatment benefits.
123
+ */
124
+ async searchBenefitsByName(
125
+ searchTerm: string
126
+ ): Promise<TreatmentBenefitDynamic[]> {
127
+ const benefits = await this.getAllBenefitsForFilter();
128
+ const normalizedSearchTerm = searchTerm.toLowerCase();
129
+ return benefits.filter((b) =>
130
+ b.name.toLowerCase().includes(normalizedSearchTerm)
131
+ );
132
+ }
133
+
134
+ /**
135
+ * @description Updates an existing treatment benefit.
136
+ * @param {TreatmentBenefitDynamic} benefit - The treatment benefit with updated data. Its ID must match an existing benefit.
137
+ * @returns {Promise<TreatmentBenefitDynamic>} The updated treatment benefit.
138
+ * @throws {Error} If the treatment benefit is not found.
139
+ */
140
+ async updateTreatmentBenefit(
141
+ benefit: TreatmentBenefitDynamic
142
+ ): Promise<TreatmentBenefitDynamic> {
143
+ const benefits = await this.getAllBenefitsForFilter();
144
+ const benefitIndex = benefits.findIndex((b) => b.id === benefit.id);
145
+
146
+ if (benefitIndex === -1) {
147
+ throw new Error("Treatment benefit not found.");
148
+ }
149
+
150
+ benefits[benefitIndex] = benefit;
151
+
152
+ await updateDoc(this.treatmentBenefitsDocRef, { benefits });
153
+ return benefit;
154
+ }
155
+
156
+ /**
157
+ * @description Deletes a treatment benefit by its ID.
158
+ * @param {string} benefitId - The ID of the treatment benefit to delete.
159
+ * @returns {Promise<void>}
160
+ */
161
+ async deleteTreatmentBenefit(benefitId: string): Promise<void> {
162
+ const benefits = await this.getAllBenefitsForFilter();
163
+ const benefitToRemove = benefits.find((b) => b.id === benefitId);
164
+
165
+ if (!benefitToRemove) {
166
+ return;
167
+ }
168
+
169
+ await updateDoc(this.treatmentBenefitsDocRef, {
170
+ benefits: arrayRemove(benefitToRemove),
171
+ });
172
+ }
173
+
174
+ // =================================================================
175
+ // Contraindications
176
+ // =================================================================
177
+
178
+ /**
179
+ * @description Retrieves all contraindications without pagination.
180
+ * @returns {Promise<ContraindicationDynamic[]>} An array of all contraindications.
181
+ */
182
+ async getAllContraindicationsForFilter(): Promise<ContraindicationDynamic[]> {
183
+ const docSnap = await getDoc(this.contraindicationsDocRef);
184
+ if (!docSnap.exists()) {
185
+ return [];
186
+ }
187
+ return (docSnap.data() as ContraindicationsDocument).contraindications;
188
+ }
189
+
190
+ /**
191
+ * @description Retrieves a paginated list of contraindications.
192
+ * @param {{ page: number; limit: number }} options - Pagination options.
193
+ * @returns {Promise<{ contraindications: ContraindicationDynamic[]; total: number }>} A paginated list and the total count.
194
+ */
195
+ async getAllContraindications(options: {
196
+ page: number;
197
+ limit: number;
198
+ }): Promise<{ contraindications: ContraindicationDynamic[]; total: number }> {
199
+ const allContraindications = await this.getAllContraindicationsForFilter();
200
+ const { page, limit } = options;
201
+ const startIndex = page * limit;
202
+ const endIndex = startIndex + limit;
203
+ const paginatedContraindications = allContraindications.slice(
204
+ startIndex,
205
+ endIndex
206
+ );
207
+ return {
208
+ contraindications: paginatedContraindications,
209
+ total: allContraindications.length,
210
+ };
211
+ }
212
+
213
+ /**
214
+ * @description Adds a new contraindication.
215
+ * @param {Omit<ContraindicationDynamic, "id">} contraindication - The contraindication to add, without an ID.
216
+ * @returns {Promise<ContraindicationDynamic>} The newly created contraindication with its generated ID.
217
+ */
218
+ async addContraindication(
219
+ contraindication: Omit<ContraindicationDynamic, "id">
220
+ ): Promise<ContraindicationDynamic> {
221
+ const newContraindication: ContraindicationDynamic = {
222
+ id: this.generateId(),
223
+ ...contraindication,
224
+ };
225
+
226
+ const docSnap = await getDoc(this.contraindicationsDocRef);
227
+ if (!docSnap.exists()) {
228
+ await setDoc(this.contraindicationsDocRef, {
229
+ contraindications: [newContraindication],
230
+ });
231
+ } else {
232
+ await updateDoc(this.contraindicationsDocRef, {
233
+ contraindications: arrayUnion(newContraindication),
234
+ });
235
+ }
236
+
237
+ return newContraindication;
238
+ }
239
+
240
+ /**
241
+ * @description Retrieves a single contraindication by its ID.
242
+ * @param {string} contraindicationId - The ID of the contraindication to retrieve.
243
+ * @returns {Promise<ContraindicationDynamic | undefined>} The found contraindication or undefined.
244
+ */
245
+ async getContraindicationById(
246
+ contraindicationId: string
247
+ ): Promise<ContraindicationDynamic | undefined> {
248
+ const contraindications = await this.getAllContraindicationsForFilter();
249
+ return contraindications.find((c) => c.id === contraindicationId);
250
+ }
251
+
252
+ /**
253
+ * @description Searches for contraindications by name (case-insensitive).
254
+ * @param {string} searchTerm - The term to search for in the contraindication names.
255
+ * @returns {Promise<ContraindicationDynamic[]>} An array of matching contraindications.
256
+ */
257
+ async searchContraindicationsByName(
258
+ searchTerm: string
259
+ ): Promise<ContraindicationDynamic[]> {
260
+ const contraindications = await this.getAllContraindicationsForFilter();
261
+ const normalizedSearchTerm = searchTerm.toLowerCase();
262
+ return contraindications.filter((c) =>
263
+ c.name.toLowerCase().includes(normalizedSearchTerm)
264
+ );
265
+ }
266
+
267
+ /**
268
+ * @description Updates an existing contraindication.
269
+ * @param {ContraindicationDynamic} contraindication - The contraindication with updated data. Its ID must match an existing one.
270
+ * @returns {Promise<ContraindicationDynamic>} The updated contraindication.
271
+ * @throws {Error} If the contraindication is not found.
272
+ */
273
+ async updateContraindication(
274
+ contraindication: ContraindicationDynamic
275
+ ): Promise<ContraindicationDynamic> {
276
+ const contraindications = await this.getAllContraindicationsForFilter();
277
+ const index = contraindications.findIndex(
278
+ (c) => c.id === contraindication.id
279
+ );
280
+
281
+ if (index === -1) {
282
+ throw new Error("Contraindication not found.");
283
+ }
284
+
285
+ contraindications[index] = contraindication;
286
+
287
+ await updateDoc(this.contraindicationsDocRef, { contraindications });
288
+ return contraindication;
289
+ }
290
+
291
+ /**
292
+ * @description Deletes a contraindication by its ID.
293
+ * @param {string} contraindicationId - The ID of the contraindication to delete.
294
+ * @returns {Promise<void>}
295
+ */
296
+ async deleteContraindication(contraindicationId: string): Promise<void> {
297
+ const contraindications = await this.getAllContraindicationsForFilter();
298
+ const toRemove = contraindications.find((c) => c.id === contraindicationId);
299
+
300
+ if (!toRemove) {
301
+ return;
302
+ }
303
+
304
+ await updateDoc(this.contraindicationsDocRef, {
305
+ contraindications: arrayRemove(toRemove),
306
+ });
307
+ }
308
+ }