@blackcode_sa/metaestetics-api 1.12.62 → 1.12.63

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 (273) hide show
  1. package/dist/admin/index.d.mts +4 -2
  2. package/dist/admin/index.d.ts +4 -2
  3. package/dist/admin/index.js +4 -45
  4. package/dist/admin/index.mjs +4 -45
  5. package/dist/backoffice/index.d.mts +9 -0
  6. package/dist/backoffice/index.d.ts +9 -0
  7. package/dist/backoffice/index.js +11 -0
  8. package/dist/backoffice/index.mjs +11 -0
  9. package/dist/index.d.mts +99 -3
  10. package/dist/index.d.ts +99 -3
  11. package/dist/index.js +545 -281
  12. package/dist/index.mjs +867 -603
  13. package/package.json +119 -119
  14. package/src/__mocks__/firstore.ts +10 -10
  15. package/src/admin/aggregation/README.md +79 -79
  16. package/src/admin/aggregation/appointment/README.md +128 -128
  17. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
  18. package/src/admin/aggregation/appointment/index.ts +1 -1
  19. package/src/admin/aggregation/clinic/README.md +52 -52
  20. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  21. package/src/admin/aggregation/clinic/index.ts +1 -1
  22. package/src/admin/aggregation/forms/README.md +13 -13
  23. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  24. package/src/admin/aggregation/forms/index.ts +1 -1
  25. package/src/admin/aggregation/index.ts +8 -8
  26. package/src/admin/aggregation/patient/README.md +27 -27
  27. package/src/admin/aggregation/patient/index.ts +1 -1
  28. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  29. package/src/admin/aggregation/practitioner/README.md +42 -42
  30. package/src/admin/aggregation/practitioner/index.ts +1 -1
  31. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  32. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  33. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  34. package/src/admin/aggregation/procedure/README.md +43 -43
  35. package/src/admin/aggregation/procedure/index.ts +1 -1
  36. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  37. package/src/admin/aggregation/reviews/index.ts +1 -1
  38. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +641 -689
  39. package/src/admin/booking/README.md +125 -125
  40. package/src/admin/booking/booking.admin.ts +1037 -1037
  41. package/src/admin/booking/booking.calculator.ts +712 -712
  42. package/src/admin/booking/booking.types.ts +59 -59
  43. package/src/admin/booking/index.ts +3 -3
  44. package/src/admin/booking/timezones-problem.md +185 -185
  45. package/src/admin/calendar/README.md +7 -7
  46. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  47. package/src/admin/calendar/index.ts +1 -1
  48. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  49. package/src/admin/documentation-templates/index.ts +1 -1
  50. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  51. package/src/admin/free-consultation/index.ts +1 -1
  52. package/src/admin/index.ts +75 -75
  53. package/src/admin/logger/index.ts +78 -78
  54. package/src/admin/mailing/README.md +95 -95
  55. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  56. package/src/admin/mailing/appointment/index.ts +1 -1
  57. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  58. package/src/admin/mailing/base.mailing.service.ts +208 -208
  59. package/src/admin/mailing/index.ts +3 -3
  60. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  61. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  62. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  63. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  64. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  65. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  66. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  67. package/src/admin/notifications/index.ts +1 -1
  68. package/src/admin/notifications/notifications.admin.ts +710 -710
  69. package/src/admin/requirements/README.md +128 -128
  70. package/src/admin/requirements/index.ts +1 -1
  71. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  72. package/src/admin/users/index.ts +1 -1
  73. package/src/admin/users/user-profile.admin.ts +405 -405
  74. package/src/backoffice/constants/certification.constants.ts +13 -13
  75. package/src/backoffice/constants/index.ts +1 -1
  76. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  77. package/src/backoffice/errors/index.ts +1 -1
  78. package/src/backoffice/expo-safe/README.md +26 -26
  79. package/src/backoffice/expo-safe/index.ts +41 -41
  80. package/src/backoffice/index.ts +5 -5
  81. package/src/backoffice/services/FIXES_README.md +102 -102
  82. package/src/backoffice/services/README.md +40 -40
  83. package/src/backoffice/services/brand.service.ts +256 -256
  84. package/src/backoffice/services/category.service.ts +318 -318
  85. package/src/backoffice/services/constants.service.ts +385 -385
  86. package/src/backoffice/services/documentation-template.service.ts +202 -202
  87. package/src/backoffice/services/index.ts +8 -8
  88. package/src/backoffice/services/migrate-products.ts +116 -116
  89. package/src/backoffice/services/product.service.ts +553 -553
  90. package/src/backoffice/services/requirement.service.ts +235 -235
  91. package/src/backoffice/services/subcategory.service.ts +395 -395
  92. package/src/backoffice/services/technology.service.ts +1083 -1070
  93. package/src/backoffice/types/README.md +12 -12
  94. package/src/backoffice/types/admin-constants.types.ts +69 -69
  95. package/src/backoffice/types/brand.types.ts +29 -29
  96. package/src/backoffice/types/category.types.ts +62 -62
  97. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  98. package/src/backoffice/types/index.ts +10 -10
  99. package/src/backoffice/types/procedure-product.types.ts +38 -38
  100. package/src/backoffice/types/product.types.ts +240 -240
  101. package/src/backoffice/types/requirement.types.ts +63 -63
  102. package/src/backoffice/types/static/README.md +18 -18
  103. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  104. package/src/backoffice/types/static/certification.types.ts +37 -37
  105. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  106. package/src/backoffice/types/static/index.ts +6 -6
  107. package/src/backoffice/types/static/pricing.types.ts +16 -16
  108. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  109. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  110. package/src/backoffice/types/subcategory.types.ts +34 -34
  111. package/src/backoffice/types/technology.types.ts +163 -161
  112. package/src/backoffice/validations/index.ts +1 -1
  113. package/src/backoffice/validations/schemas.ts +164 -163
  114. package/src/config/__mocks__/firebase.ts +99 -99
  115. package/src/config/firebase.ts +78 -78
  116. package/src/config/index.ts +9 -9
  117. package/src/errors/auth.error.ts +6 -6
  118. package/src/errors/auth.errors.ts +200 -200
  119. package/src/errors/clinic.errors.ts +32 -32
  120. package/src/errors/firebase.errors.ts +47 -47
  121. package/src/errors/user.errors.ts +99 -99
  122. package/src/index.backup.ts +407 -407
  123. package/src/index.ts +6 -6
  124. package/src/locales/en.ts +31 -31
  125. package/src/recommender/admin/index.ts +1 -1
  126. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  127. package/src/recommender/front/index.ts +1 -1
  128. package/src/recommender/front/services/onboarding.service.ts +5 -5
  129. package/src/recommender/front/services/recommender.service.ts +3 -3
  130. package/src/recommender/index.ts +1 -1
  131. package/src/services/PATIENTAUTH.MD +197 -197
  132. package/src/services/README.md +106 -106
  133. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  134. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  135. package/src/services/__tests__/auth.service.test.ts +346 -346
  136. package/src/services/__tests__/base.service.test.ts +77 -77
  137. package/src/services/__tests__/user.service.test.ts +528 -528
  138. package/src/services/appointment/README.md +17 -17
  139. package/src/services/appointment/appointment.service.ts +2505 -2082
  140. package/src/services/appointment/index.ts +1 -1
  141. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  142. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  143. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  144. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  145. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  146. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  147. package/src/services/auth/auth.service.ts +989 -989
  148. package/src/services/auth/auth.v2.service.ts +961 -961
  149. package/src/services/auth/index.ts +7 -7
  150. package/src/services/auth/utils/error.utils.ts +90 -90
  151. package/src/services/auth/utils/firebase.utils.ts +49 -49
  152. package/src/services/auth/utils/index.ts +21 -21
  153. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  154. package/src/services/base.service.ts +41 -41
  155. package/src/services/calendar/calendar.service.ts +1077 -1077
  156. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  157. package/src/services/calendar/calendar.v3.service.ts +313 -313
  158. package/src/services/calendar/externalCalendar.service.ts +178 -178
  159. package/src/services/calendar/index.ts +5 -5
  160. package/src/services/calendar/synced-calendars.service.ts +743 -743
  161. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  162. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  163. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  164. package/src/services/calendar/utils/docs.utils.ts +157 -157
  165. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  166. package/src/services/calendar/utils/index.ts +8 -8
  167. package/src/services/calendar/utils/patient.utils.ts +198 -198
  168. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  169. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  170. package/src/services/clinic/README.md +204 -204
  171. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  172. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  173. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  174. package/src/services/clinic/billing-transactions.service.ts +217 -217
  175. package/src/services/clinic/clinic-admin.service.ts +202 -202
  176. package/src/services/clinic/clinic-group.service.ts +310 -310
  177. package/src/services/clinic/clinic.service.ts +708 -708
  178. package/src/services/clinic/index.ts +5 -5
  179. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  180. package/src/services/clinic/utils/admin.utils.ts +551 -551
  181. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  182. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  183. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  184. package/src/services/clinic/utils/filter.utils.ts +446 -446
  185. package/src/services/clinic/utils/index.ts +11 -11
  186. package/src/services/clinic/utils/photos.utils.ts +188 -188
  187. package/src/services/clinic/utils/search.utils.ts +84 -84
  188. package/src/services/clinic/utils/tag.utils.ts +124 -124
  189. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  190. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  191. package/src/services/documentation-templates/index.ts +2 -2
  192. package/src/services/index.ts +13 -13
  193. package/src/services/media/index.ts +1 -1
  194. package/src/services/media/media.service.ts +418 -418
  195. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  196. package/src/services/notifications/index.ts +1 -1
  197. package/src/services/notifications/notification.service.ts +215 -215
  198. package/src/services/patient/README.md +48 -48
  199. package/src/services/patient/To-Do.md +43 -43
  200. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  201. package/src/services/patient/index.ts +2 -2
  202. package/src/services/patient/patient.service.ts +883 -883
  203. package/src/services/patient/patientRequirements.service.ts +285 -285
  204. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  205. package/src/services/patient/utils/clinic.utils.ts +80 -80
  206. package/src/services/patient/utils/docs.utils.ts +142 -142
  207. package/src/services/patient/utils/index.ts +9 -9
  208. package/src/services/patient/utils/location.utils.ts +126 -126
  209. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  210. package/src/services/patient/utils/medical.utils.ts +458 -458
  211. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  212. package/src/services/patient/utils/profile.utils.ts +510 -510
  213. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  214. package/src/services/patient/utils/token.utils.ts +211 -211
  215. package/src/services/practitioner/README.md +145 -145
  216. package/src/services/practitioner/index.ts +1 -1
  217. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  218. package/src/services/procedure/README.md +163 -163
  219. package/src/services/procedure/index.ts +1 -1
  220. package/src/services/procedure/procedure.service.ts +1682 -1682
  221. package/src/services/reviews/index.ts +1 -1
  222. package/src/services/reviews/reviews.service.ts +636 -683
  223. package/src/services/user/index.ts +1 -1
  224. package/src/services/user/user.service.ts +489 -489
  225. package/src/services/user/user.v2.service.ts +466 -466
  226. package/src/types/appointment/index.ts +481 -453
  227. package/src/types/calendar/index.ts +258 -258
  228. package/src/types/calendar/synced-calendar.types.ts +66 -66
  229. package/src/types/clinic/index.ts +489 -489
  230. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  231. package/src/types/clinic/preferences.types.ts +159 -159
  232. package/src/types/clinic/to-do +3 -3
  233. package/src/types/documentation-templates/index.ts +308 -308
  234. package/src/types/index.ts +44 -44
  235. package/src/types/notifications/README.md +77 -77
  236. package/src/types/notifications/index.ts +265 -265
  237. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  238. package/src/types/patient/allergies.ts +58 -58
  239. package/src/types/patient/index.ts +275 -273
  240. package/src/types/patient/medical-info.types.ts +152 -152
  241. package/src/types/patient/patient-requirements.ts +92 -92
  242. package/src/types/patient/token.types.ts +61 -61
  243. package/src/types/practitioner/index.ts +206 -206
  244. package/src/types/procedure/index.ts +181 -181
  245. package/src/types/profile/index.ts +39 -39
  246. package/src/types/reviews/index.ts +130 -132
  247. package/src/types/tz-lookup.d.ts +4 -4
  248. package/src/types/user/index.ts +38 -38
  249. package/src/utils/TIMESTAMPS.md +176 -176
  250. package/src/utils/TimestampUtils.ts +241 -241
  251. package/src/utils/index.ts +1 -1
  252. package/src/validations/appointment.schema.ts +574 -574
  253. package/src/validations/calendar.schema.ts +225 -225
  254. package/src/validations/clinic.schema.ts +493 -493
  255. package/src/validations/common.schema.ts +25 -25
  256. package/src/validations/documentation-templates/index.ts +1 -1
  257. package/src/validations/documentation-templates/template.schema.ts +220 -220
  258. package/src/validations/documentation-templates.schema.ts +10 -10
  259. package/src/validations/index.ts +20 -20
  260. package/src/validations/media.schema.ts +10 -10
  261. package/src/validations/notification.schema.ts +90 -90
  262. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  263. package/src/validations/patient/medical-info.schema.ts +125 -125
  264. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  265. package/src/validations/patient/token.schema.ts +29 -29
  266. package/src/validations/patient.schema.ts +217 -216
  267. package/src/validations/practitioner.schema.ts +222 -222
  268. package/src/validations/procedure-product.schema.ts +41 -41
  269. package/src/validations/procedure.schema.ts +124 -124
  270. package/src/validations/profile-info.schema.ts +41 -41
  271. package/src/validations/reviews.schema.ts +189 -195
  272. package/src/validations/schemas.ts +104 -104
  273. package/src/validations/shared.schema.ts +78 -78
@@ -1,395 +1,395 @@
1
- import {
2
- addDoc,
3
- collection,
4
- collectionGroup,
5
- deleteDoc,
6
- doc,
7
- DocumentData,
8
- getCountFromServer,
9
- getDoc,
10
- getDocs,
11
- limit,
12
- orderBy,
13
- query,
14
- setDoc,
15
- startAfter,
16
- updateDoc,
17
- where,
18
- } from "firebase/firestore";
19
- import {
20
- Subcategory,
21
- SUBCATEGORIES_COLLECTION,
22
- } from "../types/subcategory.types";
23
- import { BaseService } from "../../services/base.service";
24
- import { CATEGORIES_COLLECTION } from "../types/category.types";
25
-
26
- /**
27
- * Servis za upravljanje podkategorijama procedura.
28
- * Podkategorije su drugi nivo organizacije i pripadaju određenoj kategoriji.
29
- *
30
- * @example
31
- * const subcategoryService = new SubcategoryService();
32
- *
33
- * // Kreiranje nove podkategorije
34
- * const subcategory = await subcategoryService.create(categoryId, {
35
- * name: "Anti-Wrinkle",
36
- * description: "Treatments targeting facial wrinkles"
37
- * });
38
- */
39
- export class SubcategoryService extends BaseService {
40
- /**
41
- * Vraća referencu na Firestore kolekciju podkategorija za određenu kategoriju
42
- * @param categoryId - ID roditeljske kategorije
43
- */
44
- private getSubcategoriesRef(categoryId: string) {
45
- return collection(
46
- this.db,
47
- CATEGORIES_COLLECTION,
48
- categoryId,
49
- SUBCATEGORIES_COLLECTION
50
- );
51
- }
52
-
53
- /**
54
- * Kreira novu podkategoriju u okviru kategorije
55
- * @param categoryId - ID kategorije kojoj će pripadati nova podkategorija
56
- * @param subcategory - Podaci za novu podkategoriju
57
- * @returns Kreirana podkategorija sa generisanim ID-em
58
- */
59
- async create(
60
- categoryId: string,
61
- subcategory: Omit<Subcategory, "id" | "createdAt" | "updatedAt">
62
- ) {
63
- const now = new Date();
64
- const newSubcategory: Omit<Subcategory, "id"> = {
65
- ...subcategory,
66
- categoryId,
67
- createdAt: now,
68
- updatedAt: now,
69
- isActive: true,
70
- };
71
-
72
- const docRef = await addDoc(
73
- this.getSubcategoriesRef(categoryId),
74
- newSubcategory
75
- );
76
- return { id: docRef.id, ...newSubcategory };
77
- }
78
-
79
- /**
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
102
- * @param categoryId - ID kategorije čije podkategorije tražimo
103
- * @param options - Pagination options
104
- * @returns Lista aktivnih podkategorija i poslednji vidljiv dokument
105
- */
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) {
182
- const q = query(
183
- this.getSubcategoriesRef(categoryId),
184
- where("isActive", "==", true)
185
- );
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(
207
- (doc) =>
208
- ({
209
- id: doc.id,
210
- ...doc.data(),
211
- } as Subcategory)
212
- );
213
- }
214
-
215
- /**
216
- * Ažurira postojeću podkategoriju
217
- * @param categoryId - ID kategorije kojoj pripada podkategorija
218
- * @param subcategoryId - ID podkategorije koja se ažurira
219
- * @param subcategory - Novi podaci za podkategoriju
220
- * @returns Ažurirana podkategorija
221
- */
222
- async update(
223
- categoryId: string,
224
- subcategoryId: string,
225
- subcategory: Partial<Omit<Subcategory, "id" | "createdAt">>
226
- ) {
227
- const newCategoryId = subcategory.categoryId;
228
-
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
- }
273
- }
274
-
275
- /**
276
- * Soft delete podkategorije (postavlja isActive na false)
277
- * @param categoryId - ID kategorije kojoj pripada podkategorija
278
- * @param subcategoryId - ID podkategorije koja se briše
279
- */
280
- async delete(categoryId: string, subcategoryId: string) {
281
- await this.update(categoryId, subcategoryId, { isActive: false });
282
- }
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
-
293
- /**
294
- * Vraća podkategoriju po ID-u
295
- * @param categoryId - ID kategorije kojoj pripada podkategorija
296
- * @param subcategoryId - ID tražene podkategorije
297
- * @returns Podkategorija ili null ako ne postoji
298
- */
299
- async getById(categoryId: string, subcategoryId: string) {
300
- const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
301
- const docSnap = await getDoc(docRef);
302
- if (!docSnap.exists()) return null;
303
- return {
304
- id: docSnap.id,
305
- ...docSnap.data(),
306
- } as Subcategory;
307
- }
308
-
309
- /**
310
- * Exports subcategories to CSV string, suitable for Excel/Sheets.
311
- * Includes headers and optional UTF-8 BOM.
312
- * By default exports only active subcategories (set includeInactive to true to export all).
313
- */
314
- async exportToCsv(options?: {
315
- includeInactive?: boolean;
316
- includeBom?: boolean;
317
- }): Promise<string> {
318
- const includeInactive = options?.includeInactive ?? false;
319
- const includeBom = options?.includeBom ?? true;
320
-
321
- const headers = [
322
- "id",
323
- "name",
324
- "categoryId",
325
- "description",
326
- "isActive",
327
- ];
328
-
329
- const rows: string[] = [];
330
- rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
331
-
332
- const PAGE_SIZE = 1000;
333
- let cursor: any | undefined;
334
-
335
- // Build base constraints
336
- const constraints: any[] = [];
337
- if (!includeInactive) {
338
- constraints.push(where("isActive", "==", true));
339
- }
340
- constraints.push(orderBy("name"));
341
-
342
- // Page through all results using collectionGroup
343
- // eslint-disable-next-line no-constant-condition
344
- while (true) {
345
- const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
346
- if (cursor) queryConstraints.push(startAfter(cursor));
347
-
348
- const q = query(
349
- collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
350
- ...queryConstraints
351
- );
352
- const snapshot = await getDocs(q);
353
- if (snapshot.empty) break;
354
-
355
- for (const d of snapshot.docs) {
356
- const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
357
- rows.push(this.subcategoryToCsvRow(subcategory));
358
- }
359
-
360
- cursor = snapshot.docs[snapshot.docs.length - 1];
361
- if (snapshot.size < PAGE_SIZE) break;
362
- }
363
-
364
- const csvBody = rows.join("\r\n");
365
- return includeBom ? "\uFEFF" + csvBody : csvBody;
366
- }
367
-
368
- private subcategoryToCsvRow(subcategory: Subcategory): string {
369
- const values = [
370
- subcategory.id ?? "",
371
- subcategory.name ?? "",
372
- subcategory.categoryId ?? "",
373
- subcategory.description ?? "",
374
- String(subcategory.isActive ?? ""),
375
- ];
376
- return values.map((v) => this.formatCsvValue(v)).join(",");
377
- }
378
-
379
- private formatDateIso(value: any): string {
380
- // Firestore timestamps may come back as Date or Timestamp; handle both
381
- if (value instanceof Date) return value.toISOString();
382
- if (value && typeof value.toDate === "function") {
383
- const d = value.toDate();
384
- return d instanceof Date ? d.toISOString() : String(value);
385
- }
386
- return String(value ?? "");
387
- }
388
-
389
- private formatCsvValue(value: any): string {
390
- const str = value === null || value === undefined ? "" : String(value);
391
- // Escape double quotes by doubling them and wrap in quotes
392
- const escaped = str.replace(/"/g, '""');
393
- return `"${escaped}"`;
394
- }
395
- }
1
+ import {
2
+ addDoc,
3
+ collection,
4
+ collectionGroup,
5
+ deleteDoc,
6
+ doc,
7
+ DocumentData,
8
+ getCountFromServer,
9
+ getDoc,
10
+ getDocs,
11
+ limit,
12
+ orderBy,
13
+ query,
14
+ setDoc,
15
+ startAfter,
16
+ updateDoc,
17
+ where,
18
+ } from "firebase/firestore";
19
+ import {
20
+ Subcategory,
21
+ SUBCATEGORIES_COLLECTION,
22
+ } from "../types/subcategory.types";
23
+ import { BaseService } from "../../services/base.service";
24
+ import { CATEGORIES_COLLECTION } from "../types/category.types";
25
+
26
+ /**
27
+ * Servis za upravljanje podkategorijama procedura.
28
+ * Podkategorije su drugi nivo organizacije i pripadaju određenoj kategoriji.
29
+ *
30
+ * @example
31
+ * const subcategoryService = new SubcategoryService();
32
+ *
33
+ * // Kreiranje nove podkategorije
34
+ * const subcategory = await subcategoryService.create(categoryId, {
35
+ * name: "Anti-Wrinkle",
36
+ * description: "Treatments targeting facial wrinkles"
37
+ * });
38
+ */
39
+ export class SubcategoryService extends BaseService {
40
+ /**
41
+ * Vraća referencu na Firestore kolekciju podkategorija za određenu kategoriju
42
+ * @param categoryId - ID roditeljske kategorije
43
+ */
44
+ private getSubcategoriesRef(categoryId: string) {
45
+ return collection(
46
+ this.db,
47
+ CATEGORIES_COLLECTION,
48
+ categoryId,
49
+ SUBCATEGORIES_COLLECTION
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Kreira novu podkategoriju u okviru kategorije
55
+ * @param categoryId - ID kategorije kojoj će pripadati nova podkategorija
56
+ * @param subcategory - Podaci za novu podkategoriju
57
+ * @returns Kreirana podkategorija sa generisanim ID-em
58
+ */
59
+ async create(
60
+ categoryId: string,
61
+ subcategory: Omit<Subcategory, "id" | "createdAt" | "updatedAt">
62
+ ) {
63
+ const now = new Date();
64
+ const newSubcategory: Omit<Subcategory, "id"> = {
65
+ ...subcategory,
66
+ categoryId,
67
+ createdAt: now,
68
+ updatedAt: now,
69
+ isActive: true,
70
+ };
71
+
72
+ const docRef = await addDoc(
73
+ this.getSubcategoriesRef(categoryId),
74
+ newSubcategory
75
+ );
76
+ return { id: docRef.id, ...newSubcategory };
77
+ }
78
+
79
+ /**
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
102
+ * @param categoryId - ID kategorije čije podkategorije tražimo
103
+ * @param options - Pagination options
104
+ * @returns Lista aktivnih podkategorija i poslednji vidljiv dokument
105
+ */
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) {
182
+ const q = query(
183
+ this.getSubcategoriesRef(categoryId),
184
+ where("isActive", "==", true)
185
+ );
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(
207
+ (doc) =>
208
+ ({
209
+ id: doc.id,
210
+ ...doc.data(),
211
+ } as Subcategory)
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Ažurira postojeću podkategoriju
217
+ * @param categoryId - ID kategorije kojoj pripada podkategorija
218
+ * @param subcategoryId - ID podkategorije koja se ažurira
219
+ * @param subcategory - Novi podaci za podkategoriju
220
+ * @returns Ažurirana podkategorija
221
+ */
222
+ async update(
223
+ categoryId: string,
224
+ subcategoryId: string,
225
+ subcategory: Partial<Omit<Subcategory, "id" | "createdAt">>
226
+ ) {
227
+ const newCategoryId = subcategory.categoryId;
228
+
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
+ }
273
+ }
274
+
275
+ /**
276
+ * Soft delete podkategorije (postavlja isActive na false)
277
+ * @param categoryId - ID kategorije kojoj pripada podkategorija
278
+ * @param subcategoryId - ID podkategorije koja se briše
279
+ */
280
+ async delete(categoryId: string, subcategoryId: string) {
281
+ await this.update(categoryId, subcategoryId, { isActive: false });
282
+ }
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
+
293
+ /**
294
+ * Vraća podkategoriju po ID-u
295
+ * @param categoryId - ID kategorije kojoj pripada podkategorija
296
+ * @param subcategoryId - ID tražene podkategorije
297
+ * @returns Podkategorija ili null ako ne postoji
298
+ */
299
+ async getById(categoryId: string, subcategoryId: string) {
300
+ const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
301
+ const docSnap = await getDoc(docRef);
302
+ if (!docSnap.exists()) return null;
303
+ return {
304
+ id: docSnap.id,
305
+ ...docSnap.data(),
306
+ } as Subcategory;
307
+ }
308
+
309
+ /**
310
+ * Exports subcategories to CSV string, suitable for Excel/Sheets.
311
+ * Includes headers and optional UTF-8 BOM.
312
+ * By default exports only active subcategories (set includeInactive to true to export all).
313
+ */
314
+ async exportToCsv(options?: {
315
+ includeInactive?: boolean;
316
+ includeBom?: boolean;
317
+ }): Promise<string> {
318
+ const includeInactive = options?.includeInactive ?? false;
319
+ const includeBom = options?.includeBom ?? true;
320
+
321
+ const headers = [
322
+ "id",
323
+ "name",
324
+ "categoryId",
325
+ "description",
326
+ "isActive",
327
+ ];
328
+
329
+ const rows: string[] = [];
330
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
331
+
332
+ const PAGE_SIZE = 1000;
333
+ let cursor: any | undefined;
334
+
335
+ // Build base constraints
336
+ const constraints: any[] = [];
337
+ if (!includeInactive) {
338
+ constraints.push(where("isActive", "==", true));
339
+ }
340
+ constraints.push(orderBy("name"));
341
+
342
+ // Page through all results using collectionGroup
343
+ // eslint-disable-next-line no-constant-condition
344
+ while (true) {
345
+ const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
346
+ if (cursor) queryConstraints.push(startAfter(cursor));
347
+
348
+ const q = query(
349
+ collectionGroup(this.db, SUBCATEGORIES_COLLECTION),
350
+ ...queryConstraints
351
+ );
352
+ const snapshot = await getDocs(q);
353
+ if (snapshot.empty) break;
354
+
355
+ for (const d of snapshot.docs) {
356
+ const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
357
+ rows.push(this.subcategoryToCsvRow(subcategory));
358
+ }
359
+
360
+ cursor = snapshot.docs[snapshot.docs.length - 1];
361
+ if (snapshot.size < PAGE_SIZE) break;
362
+ }
363
+
364
+ const csvBody = rows.join("\r\n");
365
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
366
+ }
367
+
368
+ private subcategoryToCsvRow(subcategory: Subcategory): string {
369
+ const values = [
370
+ subcategory.id ?? "",
371
+ subcategory.name ?? "",
372
+ subcategory.categoryId ?? "",
373
+ subcategory.description ?? "",
374
+ String(subcategory.isActive ?? ""),
375
+ ];
376
+ return values.map((v) => this.formatCsvValue(v)).join(",");
377
+ }
378
+
379
+ private formatDateIso(value: any): string {
380
+ // Firestore timestamps may come back as Date or Timestamp; handle both
381
+ if (value instanceof Date) return value.toISOString();
382
+ if (value && typeof value.toDate === "function") {
383
+ const d = value.toDate();
384
+ return d instanceof Date ? d.toISOString() : String(value);
385
+ }
386
+ return String(value ?? "");
387
+ }
388
+
389
+ private formatCsvValue(value: any): string {
390
+ const str = value === null || value === undefined ? "" : String(value);
391
+ // Escape double quotes by doubling them and wrap in quotes
392
+ const escaped = str.replace(/"/g, '""');
393
+ return `"${escaped}"`;
394
+ }
395
+ }