@blackcode_sa/metaestetics-api 1.13.4 → 1.13.5

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 (293) hide show
  1. package/dist/admin/index.d.mts +15 -28
  2. package/dist/admin/index.d.ts +15 -28
  3. package/dist/index.d.mts +16 -29
  4. package/dist/index.d.ts +16 -29
  5. package/dist/index.js +1 -0
  6. package/dist/index.mjs +1 -0
  7. package/package.json +121 -119
  8. package/src/__mocks__/firstore.ts +10 -10
  9. package/src/admin/aggregation/README.md +79 -79
  10. package/src/admin/aggregation/appointment/README.md +128 -128
  11. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  12. package/src/admin/aggregation/appointment/index.ts +1 -1
  13. package/src/admin/aggregation/clinic/README.md +52 -52
  14. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  15. package/src/admin/aggregation/clinic/index.ts +1 -1
  16. package/src/admin/aggregation/forms/README.md +13 -13
  17. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  18. package/src/admin/aggregation/forms/index.ts +1 -1
  19. package/src/admin/aggregation/index.ts +8 -8
  20. package/src/admin/aggregation/patient/README.md +27 -27
  21. package/src/admin/aggregation/patient/index.ts +1 -1
  22. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  23. package/src/admin/aggregation/practitioner/README.md +42 -42
  24. package/src/admin/aggregation/practitioner/index.ts +1 -1
  25. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  26. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  28. package/src/admin/aggregation/procedure/README.md +43 -43
  29. package/src/admin/aggregation/procedure/index.ts +1 -1
  30. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  31. package/src/admin/aggregation/reviews/index.ts +1 -1
  32. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  33. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  34. package/src/admin/analytics/index.ts +2 -2
  35. package/src/admin/booking/README.md +125 -125
  36. package/src/admin/booking/booking.admin.ts +1037 -1037
  37. package/src/admin/booking/booking.calculator.ts +712 -712
  38. package/src/admin/booking/booking.types.ts +59 -59
  39. package/src/admin/booking/index.ts +3 -3
  40. package/src/admin/booking/timezones-problem.md +185 -185
  41. package/src/admin/calendar/README.md +7 -7
  42. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  43. package/src/admin/calendar/index.ts +1 -1
  44. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  45. package/src/admin/documentation-templates/index.ts +1 -1
  46. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  47. package/src/admin/free-consultation/index.ts +1 -1
  48. package/src/admin/index.ts +81 -81
  49. package/src/admin/logger/index.ts +78 -78
  50. package/src/admin/mailing/README.md +95 -95
  51. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  52. package/src/admin/mailing/appointment/index.ts +1 -1
  53. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  54. package/src/admin/mailing/base.mailing.service.ts +208 -208
  55. package/src/admin/mailing/index.ts +3 -3
  56. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  57. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  58. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  59. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  60. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  61. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  62. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  63. package/src/admin/notifications/index.ts +1 -1
  64. package/src/admin/notifications/notifications.admin.ts +710 -710
  65. package/src/admin/requirements/README.md +128 -128
  66. package/src/admin/requirements/index.ts +1 -1
  67. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  68. package/src/admin/users/index.ts +1 -1
  69. package/src/admin/users/user-profile.admin.ts +405 -405
  70. package/src/backoffice/constants/certification.constants.ts +13 -13
  71. package/src/backoffice/constants/index.ts +1 -1
  72. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  73. package/src/backoffice/errors/index.ts +1 -1
  74. package/src/backoffice/expo-safe/README.md +26 -26
  75. package/src/backoffice/expo-safe/index.ts +41 -41
  76. package/src/backoffice/index.ts +5 -5
  77. package/src/backoffice/services/FIXES_README.md +102 -102
  78. package/src/backoffice/services/README.md +57 -57
  79. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  80. package/src/backoffice/services/analytics.service.summary.md +143 -143
  81. package/src/backoffice/services/brand.service.ts +256 -256
  82. package/src/backoffice/services/category.service.ts +384 -384
  83. package/src/backoffice/services/constants.service.ts +385 -385
  84. package/src/backoffice/services/documentation-template.service.ts +202 -202
  85. package/src/backoffice/services/index.ts +10 -10
  86. package/src/backoffice/services/migrate-products.ts +116 -116
  87. package/src/backoffice/services/product.service.ts +553 -553
  88. package/src/backoffice/services/requirement.service.ts +235 -235
  89. package/src/backoffice/services/subcategory.service.ts +461 -461
  90. package/src/backoffice/services/technology.service.ts +1151 -1151
  91. package/src/backoffice/types/README.md +12 -12
  92. package/src/backoffice/types/admin-constants.types.ts +69 -69
  93. package/src/backoffice/types/brand.types.ts +29 -29
  94. package/src/backoffice/types/category.types.ts +67 -67
  95. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  96. package/src/backoffice/types/index.ts +10 -10
  97. package/src/backoffice/types/procedure-product.types.ts +38 -38
  98. package/src/backoffice/types/product.types.ts +240 -240
  99. package/src/backoffice/types/requirement.types.ts +63 -63
  100. package/src/backoffice/types/static/README.md +18 -18
  101. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  102. package/src/backoffice/types/static/certification.types.ts +37 -37
  103. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  104. package/src/backoffice/types/static/index.ts +6 -6
  105. package/src/backoffice/types/static/pricing.types.ts +16 -16
  106. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  107. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  108. package/src/backoffice/types/subcategory.types.ts +34 -34
  109. package/src/backoffice/types/technology.types.ts +168 -168
  110. package/src/backoffice/validations/index.ts +1 -1
  111. package/src/backoffice/validations/schemas.ts +164 -164
  112. package/src/config/__mocks__/firebase.ts +99 -99
  113. package/src/config/firebase.ts +78 -78
  114. package/src/config/index.ts +9 -9
  115. package/src/errors/auth.error.ts +6 -6
  116. package/src/errors/auth.errors.ts +200 -200
  117. package/src/errors/clinic.errors.ts +32 -32
  118. package/src/errors/firebase.errors.ts +47 -47
  119. package/src/errors/user.errors.ts +99 -99
  120. package/src/index.backup.ts +407 -407
  121. package/src/index.ts +6 -6
  122. package/src/locales/en.ts +31 -31
  123. package/src/recommender/admin/index.ts +1 -1
  124. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  125. package/src/recommender/front/index.ts +1 -1
  126. package/src/recommender/front/services/onboarding.service.ts +5 -5
  127. package/src/recommender/front/services/recommender.service.ts +3 -3
  128. package/src/recommender/index.ts +1 -1
  129. package/src/services/PATIENTAUTH.MD +197 -197
  130. package/src/services/README.md +106 -106
  131. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  132. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  133. package/src/services/__tests__/auth.service.test.ts +346 -346
  134. package/src/services/__tests__/base.service.test.ts +77 -77
  135. package/src/services/__tests__/user.service.test.ts +528 -528
  136. package/src/services/analytics/ARCHITECTURE.md +199 -199
  137. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  138. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  139. package/src/services/analytics/QUICK_START.md +393 -393
  140. package/src/services/analytics/README.md +304 -304
  141. package/src/services/analytics/SUMMARY.md +141 -141
  142. package/src/services/analytics/TRENDS.md +380 -380
  143. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  144. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  145. package/src/services/analytics/analytics.service.ts +2142 -2142
  146. package/src/services/analytics/index.ts +4 -4
  147. package/src/services/analytics/review-analytics.service.ts +941 -941
  148. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  149. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  150. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  151. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  152. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  153. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  154. package/src/services/appointment/README.md +17 -17
  155. package/src/services/appointment/appointment.service.ts +2558 -2558
  156. package/src/services/appointment/index.ts +1 -1
  157. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  158. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  159. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  160. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  161. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  162. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  163. package/src/services/auth/auth.service.ts +989 -989
  164. package/src/services/auth/auth.v2.service.ts +961 -961
  165. package/src/services/auth/index.ts +7 -7
  166. package/src/services/auth/utils/error.utils.ts +90 -90
  167. package/src/services/auth/utils/firebase.utils.ts +49 -49
  168. package/src/services/auth/utils/index.ts +21 -21
  169. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  170. package/src/services/base.service.ts +41 -41
  171. package/src/services/calendar/calendar.service.ts +1077 -1077
  172. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  173. package/src/services/calendar/calendar.v3.service.ts +313 -313
  174. package/src/services/calendar/externalCalendar.service.ts +178 -178
  175. package/src/services/calendar/index.ts +5 -5
  176. package/src/services/calendar/synced-calendars.service.ts +743 -743
  177. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  178. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  179. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  180. package/src/services/calendar/utils/docs.utils.ts +157 -157
  181. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  182. package/src/services/calendar/utils/index.ts +8 -8
  183. package/src/services/calendar/utils/patient.utils.ts +198 -198
  184. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  185. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  186. package/src/services/clinic/README.md +204 -204
  187. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  188. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  189. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  190. package/src/services/clinic/billing-transactions.service.ts +217 -217
  191. package/src/services/clinic/clinic-admin.service.ts +202 -202
  192. package/src/services/clinic/clinic-group.service.ts +310 -310
  193. package/src/services/clinic/clinic.service.ts +708 -708
  194. package/src/services/clinic/index.ts +5 -5
  195. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  196. package/src/services/clinic/utils/admin.utils.ts +551 -551
  197. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  198. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  199. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  200. package/src/services/clinic/utils/filter.utils.ts +446 -446
  201. package/src/services/clinic/utils/index.ts +11 -11
  202. package/src/services/clinic/utils/photos.utils.ts +188 -188
  203. package/src/services/clinic/utils/search.utils.ts +84 -84
  204. package/src/services/clinic/utils/tag.utils.ts +124 -124
  205. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  206. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  207. package/src/services/documentation-templates/index.ts +2 -2
  208. package/src/services/index.ts +14 -14
  209. package/src/services/media/index.ts +1 -1
  210. package/src/services/media/media.service.ts +418 -418
  211. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  212. package/src/services/notifications/index.ts +1 -1
  213. package/src/services/notifications/notification.service.ts +215 -215
  214. package/src/services/patient/README.md +48 -48
  215. package/src/services/patient/To-Do.md +43 -43
  216. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  217. package/src/services/patient/index.ts +2 -2
  218. package/src/services/patient/patient.service.ts +883 -883
  219. package/src/services/patient/patientRequirements.service.ts +285 -285
  220. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  221. package/src/services/patient/utils/clinic.utils.ts +80 -80
  222. package/src/services/patient/utils/docs.utils.ts +142 -142
  223. package/src/services/patient/utils/index.ts +9 -9
  224. package/src/services/patient/utils/location.utils.ts +126 -126
  225. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  226. package/src/services/patient/utils/medical.utils.ts +458 -458
  227. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  228. package/src/services/patient/utils/profile.utils.ts +510 -510
  229. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  230. package/src/services/patient/utils/token.utils.ts +211 -211
  231. package/src/services/practitioner/README.md +145 -145
  232. package/src/services/practitioner/index.ts +1 -1
  233. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  234. package/src/services/procedure/README.md +163 -163
  235. package/src/services/procedure/index.ts +1 -1
  236. package/src/services/procedure/procedure.service.ts +2200 -2200
  237. package/src/services/reviews/index.ts +1 -1
  238. package/src/services/reviews/reviews.service.ts +734 -734
  239. package/src/services/user/index.ts +1 -1
  240. package/src/services/user/user.service.ts +489 -489
  241. package/src/services/user/user.v2.service.ts +466 -466
  242. package/src/types/analytics/analytics.types.ts +597 -597
  243. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  244. package/src/types/analytics/index.ts +4 -4
  245. package/src/types/analytics/stored-analytics.types.ts +137 -137
  246. package/src/types/appointment/index.ts +480 -480
  247. package/src/types/calendar/index.ts +258 -258
  248. package/src/types/calendar/synced-calendar.types.ts +66 -66
  249. package/src/types/clinic/index.ts +498 -489
  250. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  251. package/src/types/clinic/preferences.types.ts +159 -159
  252. package/src/types/clinic/to-do +3 -3
  253. package/src/types/documentation-templates/index.ts +308 -308
  254. package/src/types/index.ts +47 -47
  255. package/src/types/notifications/README.md +77 -77
  256. package/src/types/notifications/index.ts +286 -286
  257. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  258. package/src/types/patient/allergies.ts +58 -58
  259. package/src/types/patient/index.ts +275 -275
  260. package/src/types/patient/medical-info.types.ts +152 -152
  261. package/src/types/patient/patient-requirements.ts +92 -92
  262. package/src/types/patient/token.types.ts +61 -61
  263. package/src/types/practitioner/index.ts +206 -206
  264. package/src/types/procedure/index.ts +181 -181
  265. package/src/types/profile/index.ts +39 -39
  266. package/src/types/reviews/index.ts +132 -132
  267. package/src/types/tz-lookup.d.ts +4 -4
  268. package/src/types/user/index.ts +38 -38
  269. package/src/utils/TIMESTAMPS.md +176 -176
  270. package/src/utils/TimestampUtils.ts +241 -241
  271. package/src/utils/index.ts +1 -1
  272. package/src/validations/appointment.schema.ts +574 -574
  273. package/src/validations/calendar.schema.ts +225 -225
  274. package/src/validations/clinic.schema.ts +494 -493
  275. package/src/validations/common.schema.ts +25 -25
  276. package/src/validations/documentation-templates/index.ts +1 -1
  277. package/src/validations/documentation-templates/template.schema.ts +220 -220
  278. package/src/validations/documentation-templates.schema.ts +10 -10
  279. package/src/validations/index.ts +20 -20
  280. package/src/validations/media.schema.ts +10 -10
  281. package/src/validations/notification.schema.ts +90 -90
  282. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  283. package/src/validations/patient/medical-info.schema.ts +125 -125
  284. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  285. package/src/validations/patient/token.schema.ts +29 -29
  286. package/src/validations/patient.schema.ts +217 -217
  287. package/src/validations/practitioner.schema.ts +222 -222
  288. package/src/validations/procedure-product.schema.ts +41 -41
  289. package/src/validations/procedure.schema.ts +124 -124
  290. package/src/validations/profile-info.schema.ts +41 -41
  291. package/src/validations/reviews.schema.ts +195 -195
  292. package/src/validations/schemas.ts +104 -104
  293. package/src/validations/shared.schema.ts +78 -78
@@ -1,384 +1,384 @@
1
- import {
2
- addDoc,
3
- collection,
4
- doc,
5
- DocumentData,
6
- getCountFromServer,
7
- getDoc,
8
- getDocs,
9
- limit,
10
- orderBy,
11
- query,
12
- startAfter,
13
- updateDoc,
14
- where,
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
-
20
- /**
21
- * ID of the consultation category that should be hidden from admin backoffice.
22
- * This category is used internally for free consultation procedures.
23
- */
24
- const EXCLUDED_CATEGORY_ID = 'consultation';
25
-
26
- /**
27
- * Servis za upravljanje kategorijama procedura.
28
- * Kategorije su prvi nivo organizacije nakon procedure family (aesthetics/surgery).
29
- *
30
- * @example
31
- * const categoryService = new CategoryService();
32
- *
33
- * // Kreiranje nove kategorije
34
- * const category = await categoryService.create({
35
- * name: "Injectables",
36
- * family: ProcedureFamily.AESTHETICS
37
- * });
38
- */
39
- export class CategoryService extends BaseService implements ICategoryService {
40
- /**
41
- * Filters out excluded categories from a list.
42
- * @param categories - List of categories to filter
43
- * @returns Filtered list without excluded categories
44
- */
45
- private filterExcludedCategories(categories: Category[]): Category[] {
46
- return categories.filter(cat => cat.id !== EXCLUDED_CATEGORY_ID);
47
- }
48
- /**
49
- * Referenca na Firestore kolekciju kategorija
50
- */
51
- private get categoriesRef() {
52
- return collection(this.db, CATEGORIES_COLLECTION);
53
- }
54
-
55
- /**
56
- * Kreira novu kategoriju u sistemu
57
- * @param category - Podaci za novu kategoriju
58
- * @returns Kreirana kategorija sa generisanim ID-em
59
- */
60
- async create(category: Omit<Category, 'id' | 'createdAt' | 'updatedAt'>) {
61
- const now = new Date();
62
- const newCategory: Omit<Category, 'id'> = {
63
- ...category,
64
- createdAt: now,
65
- updatedAt: now,
66
- isActive: true,
67
- };
68
-
69
- const docRef = await addDoc(this.categoriesRef, newCategory);
70
- return { id: docRef.id, ...newCategory };
71
- }
72
-
73
- /**
74
- * Returns counts of categories for each family.
75
- * @param active - Whether to count active or inactive categories.
76
- * @returns A record mapping family to category count.
77
- */
78
- async getCategoryCounts(active = true) {
79
- const counts: Record<string, number> = {};
80
- const families = Object.values(ProcedureFamily);
81
-
82
- for (const family of families) {
83
- const q = query(
84
- this.categoriesRef,
85
- where('family', '==', family),
86
- where('isActive', '==', active),
87
- );
88
- const snapshot = await getDocs(q);
89
- // Filter out excluded category and count
90
- const filteredDocs = snapshot.docs.filter(doc => doc.id !== EXCLUDED_CATEGORY_ID);
91
- counts[family] = filteredDocs.length;
92
- }
93
- return counts;
94
- }
95
-
96
- /**
97
- * Vraća sve kategorije za potrebe filtera (bez paginacije)
98
- * @returns Lista svih aktivnih kategorija
99
- */
100
- async getAllForFilter() {
101
- const q = query(this.categoriesRef, where('isActive', '==', true));
102
- const snapshot = await getDocs(q);
103
- const categories = snapshot.docs.map(
104
- doc =>
105
- ({
106
- id: doc.id,
107
- ...doc.data(),
108
- } as Category),
109
- );
110
- return this.filterExcludedCategories(categories);
111
- }
112
-
113
- /**
114
- * Vraća sve kategorije za određenu familiju za potrebe filtera (bez paginacije)
115
- * @param family - Familija procedura (aesthetics/surgery)
116
- * @returns Lista aktivnih kategorija koje pripadaju traženoj familiji
117
- */
118
- async getAllForFilterByFamily(family: ProcedureFamily) {
119
- const q = query(
120
- this.categoriesRef,
121
- where('family', '==', family),
122
- where('isActive', '==', true),
123
- orderBy('name'),
124
- );
125
- const snapshot = await getDocs(q);
126
- const categories = snapshot.docs.map(
127
- doc =>
128
- ({
129
- id: doc.id,
130
- ...doc.data(),
131
- } as Category),
132
- );
133
- return this.filterExcludedCategories(categories);
134
- }
135
-
136
- /**
137
- * Vraća sve kategorije sa paginacijom
138
- * @param options - Pagination and filter options
139
- * @returns Lista kategorija i poslednji vidljiv dokument
140
- */
141
- async getAll(
142
- options: {
143
- active?: boolean;
144
- limit?: number;
145
- lastVisible?: DocumentData;
146
- } = {},
147
- ) {
148
- const { active = true, limit: queryLimit = 10, lastVisible } = options;
149
- const constraints = [
150
- where('isActive', '==', active),
151
- orderBy('name'),
152
- queryLimit ? limit(queryLimit) : undefined,
153
- lastVisible ? startAfter(lastVisible) : undefined,
154
- ].filter((c): c is NonNullable<typeof c> => !!c);
155
-
156
- const q = query(this.categoriesRef, ...constraints);
157
- const snapshot = await getDocs(q);
158
- const categories = snapshot.docs.map(
159
- doc =>
160
- ({
161
- id: doc.id,
162
- ...doc.data(),
163
- } as Category),
164
- );
165
- const filteredCategories = this.filterExcludedCategories(categories);
166
- const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
167
- return { categories: filteredCategories, lastVisible: newLastVisible };
168
- }
169
-
170
- /**
171
- * Vraća sve aktivne kategorije za određenu familiju procedura sa paginacijom
172
- * @param family - Familija procedura (aesthetics/surgery)
173
- * @param options - Pagination options
174
- * @returns Lista kategorija koje pripadaju traženoj familiji
175
- */
176
- async getAllByFamily(
177
- family: ProcedureFamily,
178
- options: {
179
- active?: boolean;
180
- limit?: number;
181
- lastVisible?: DocumentData;
182
- } = {},
183
- ) {
184
- const { active = true, limit: queryLimit = 10, lastVisible } = options;
185
- const constraints = [
186
- where('family', '==', family),
187
- where('isActive', '==', active),
188
- orderBy('name'),
189
- queryLimit ? limit(queryLimit) : undefined,
190
- lastVisible ? startAfter(lastVisible) : undefined,
191
- ].filter((c): c is NonNullable<typeof c> => !!c);
192
-
193
- const q = query(this.categoriesRef, ...constraints);
194
- const snapshot = await getDocs(q);
195
- const categories = snapshot.docs.map(
196
- doc =>
197
- ({
198
- id: doc.id,
199
- ...doc.data(),
200
- } as Category),
201
- );
202
- const filteredCategories = this.filterExcludedCategories(categories);
203
- const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
204
- return { categories: filteredCategories, lastVisible: newLastVisible };
205
- }
206
-
207
- /**
208
- * Ažurira postojeću kategoriju
209
- * @param id - ID kategorije koja se ažurira
210
- * @param category - Novi podaci za kategoriju
211
- * @returns Ažurirana kategorija
212
- */
213
- async update(id: string, category: Partial<Omit<Category, 'id' | 'createdAt'>>) {
214
- const updateData = {
215
- ...category,
216
- updatedAt: new Date(),
217
- };
218
-
219
- const docRef = doc(this.categoriesRef, id);
220
- await updateDoc(docRef, updateData);
221
- return this.getById(id);
222
- }
223
-
224
- /**
225
- * Soft delete kategorije (postavlja isActive na false)
226
- * @param id - ID kategorije koja se briše
227
- */
228
- async delete(id: string) {
229
- await this.update(id, { isActive: false });
230
- }
231
-
232
- /**
233
- * Reactivates a category by setting its isActive flag to true.
234
- * @param id - The ID of the category to reactivate.
235
- */
236
- async reactivate(id: string) {
237
- await this.update(id, { isActive: true });
238
- }
239
-
240
- /**
241
- * Vraća kategoriju po ID-u
242
- * @param id - ID tražene kategorije
243
- * @returns Kategorija ili null ako ne postoji
244
- */
245
- async getById(id: string) {
246
- // Prevent access to excluded category
247
- if (id === EXCLUDED_CATEGORY_ID) return null;
248
-
249
- const docRef = doc(this.categoriesRef, id);
250
- const docSnap = await getDoc(docRef);
251
- if (!docSnap.exists()) return null;
252
- return {
253
- id: docSnap.id,
254
- ...docSnap.data(),
255
- } as Category;
256
- }
257
-
258
- /**
259
- * Internal method to get category by ID without filtering.
260
- * Used internally for consultation procedures.
261
- * @param id - ID of the category to get
262
- * @returns Category or null if not found
263
- */
264
- async getByIdInternal(id: string): Promise<Category | null> {
265
- const docRef = doc(this.categoriesRef, id);
266
- const docSnap = await getDoc(docRef);
267
- if (!docSnap.exists()) return null;
268
- return {
269
- id: docSnap.id,
270
- ...docSnap.data(),
271
- } as Category;
272
- }
273
-
274
- /**
275
- * Finds a category by exact name match within a specific family.
276
- * Used for CSV import matching.
277
- * @param name - Exact name of the category to find
278
- * @param family - Procedure family to search within
279
- * @returns Category if found, null otherwise
280
- */
281
- async findByNameAndFamily(name: string, family: ProcedureFamily): Promise<Category | null> {
282
- const q = query(
283
- this.categoriesRef,
284
- where('name', '==', name),
285
- where('family', '==', family),
286
- where('isActive', '==', true),
287
- );
288
- const snapshot = await getDocs(q);
289
- if (snapshot.empty) return null;
290
- const doc = snapshot.docs[0];
291
- // Exclude consultation category
292
- if (doc.id === EXCLUDED_CATEGORY_ID) return null;
293
- return {
294
- id: doc.id,
295
- ...doc.data(),
296
- } as Category;
297
- }
298
-
299
- /**
300
- * Exports categories to CSV string, suitable for Excel/Sheets.
301
- * Includes headers and optional UTF-8 BOM.
302
- * By default exports only active categories (set includeInactive to true to export all).
303
- */
304
- async exportToCsv(options?: {
305
- includeInactive?: boolean;
306
- includeBom?: boolean;
307
- }): Promise<string> {
308
- const includeInactive = options?.includeInactive ?? false;
309
- const includeBom = options?.includeBom ?? true;
310
-
311
- const headers = [
312
- "id",
313
- "name",
314
- "description",
315
- "family",
316
- "isActive",
317
- ];
318
-
319
- const rows: string[] = [];
320
- rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
321
-
322
- const PAGE_SIZE = 1000;
323
- let cursor: any | undefined;
324
-
325
- // Build base constraints
326
- const constraints: any[] = [];
327
- if (!includeInactive) {
328
- constraints.push(where("isActive", "==", true));
329
- }
330
- constraints.push(orderBy("name"));
331
-
332
- // Page through all results
333
- // eslint-disable-next-line no-constant-condition
334
- while (true) {
335
- const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
336
- if (cursor) queryConstraints.push(startAfter(cursor));
337
-
338
- const q = query(this.categoriesRef, ...queryConstraints);
339
- const snapshot = await getDocs(q);
340
- if (snapshot.empty) break;
341
-
342
- for (const d of snapshot.docs) {
343
- // Exclude consultation category from CSV export
344
- if (d.id === EXCLUDED_CATEGORY_ID) continue;
345
- const category = ({ id: d.id, ...d.data() } as unknown) as Category;
346
- rows.push(this.categoryToCsvRow(category));
347
- }
348
-
349
- cursor = snapshot.docs[snapshot.docs.length - 1];
350
- if (snapshot.size < PAGE_SIZE) break;
351
- }
352
-
353
- const csvBody = rows.join("\r\n");
354
- return includeBom ? "\uFEFF" + csvBody : csvBody;
355
- }
356
-
357
- private categoryToCsvRow(category: Category): string {
358
- const values = [
359
- category.id ?? "",
360
- category.name ?? "",
361
- category.description ?? "",
362
- category.family ?? "",
363
- String(category.isActive ?? ""),
364
- ];
365
- return values.map((v) => this.formatCsvValue(v)).join(",");
366
- }
367
-
368
- private formatDateIso(value: any): string {
369
- // Firestore timestamps may come back as Date or Timestamp; handle both
370
- if (value instanceof Date) return value.toISOString();
371
- if (value && typeof value.toDate === "function") {
372
- const d = value.toDate();
373
- return d instanceof Date ? d.toISOString() : String(value);
374
- }
375
- return String(value ?? "");
376
- }
377
-
378
- private formatCsvValue(value: any): string {
379
- const str = value === null || value === undefined ? "" : String(value);
380
- // Escape double quotes by doubling them and wrap in quotes
381
- const escaped = str.replace(/"/g, '""');
382
- return `"${escaped}"`;
383
- }
384
- }
1
+ import {
2
+ addDoc,
3
+ collection,
4
+ doc,
5
+ DocumentData,
6
+ getCountFromServer,
7
+ getDoc,
8
+ getDocs,
9
+ limit,
10
+ orderBy,
11
+ query,
12
+ startAfter,
13
+ updateDoc,
14
+ where,
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
+
20
+ /**
21
+ * ID of the consultation category that should be hidden from admin backoffice.
22
+ * This category is used internally for free consultation procedures.
23
+ */
24
+ const EXCLUDED_CATEGORY_ID = 'consultation';
25
+
26
+ /**
27
+ * Servis za upravljanje kategorijama procedura.
28
+ * Kategorije su prvi nivo organizacije nakon procedure family (aesthetics/surgery).
29
+ *
30
+ * @example
31
+ * const categoryService = new CategoryService();
32
+ *
33
+ * // Kreiranje nove kategorije
34
+ * const category = await categoryService.create({
35
+ * name: "Injectables",
36
+ * family: ProcedureFamily.AESTHETICS
37
+ * });
38
+ */
39
+ export class CategoryService extends BaseService implements ICategoryService {
40
+ /**
41
+ * Filters out excluded categories from a list.
42
+ * @param categories - List of categories to filter
43
+ * @returns Filtered list without excluded categories
44
+ */
45
+ private filterExcludedCategories(categories: Category[]): Category[] {
46
+ return categories.filter(cat => cat.id !== EXCLUDED_CATEGORY_ID);
47
+ }
48
+ /**
49
+ * Referenca na Firestore kolekciju kategorija
50
+ */
51
+ private get categoriesRef() {
52
+ return collection(this.db, CATEGORIES_COLLECTION);
53
+ }
54
+
55
+ /**
56
+ * Kreira novu kategoriju u sistemu
57
+ * @param category - Podaci za novu kategoriju
58
+ * @returns Kreirana kategorija sa generisanim ID-em
59
+ */
60
+ async create(category: Omit<Category, 'id' | 'createdAt' | 'updatedAt'>) {
61
+ const now = new Date();
62
+ const newCategory: Omit<Category, 'id'> = {
63
+ ...category,
64
+ createdAt: now,
65
+ updatedAt: now,
66
+ isActive: true,
67
+ };
68
+
69
+ const docRef = await addDoc(this.categoriesRef, newCategory);
70
+ return { id: docRef.id, ...newCategory };
71
+ }
72
+
73
+ /**
74
+ * Returns counts of categories for each family.
75
+ * @param active - Whether to count active or inactive categories.
76
+ * @returns A record mapping family to category count.
77
+ */
78
+ async getCategoryCounts(active = true) {
79
+ const counts: Record<string, number> = {};
80
+ const families = Object.values(ProcedureFamily);
81
+
82
+ for (const family of families) {
83
+ const q = query(
84
+ this.categoriesRef,
85
+ where('family', '==', family),
86
+ where('isActive', '==', active),
87
+ );
88
+ const snapshot = await getDocs(q);
89
+ // Filter out excluded category and count
90
+ const filteredDocs = snapshot.docs.filter(doc => doc.id !== EXCLUDED_CATEGORY_ID);
91
+ counts[family] = filteredDocs.length;
92
+ }
93
+ return counts;
94
+ }
95
+
96
+ /**
97
+ * Vraća sve kategorije za potrebe filtera (bez paginacije)
98
+ * @returns Lista svih aktivnih kategorija
99
+ */
100
+ async getAllForFilter() {
101
+ const q = query(this.categoriesRef, where('isActive', '==', true));
102
+ const snapshot = await getDocs(q);
103
+ const categories = snapshot.docs.map(
104
+ doc =>
105
+ ({
106
+ id: doc.id,
107
+ ...doc.data(),
108
+ } as Category),
109
+ );
110
+ return this.filterExcludedCategories(categories);
111
+ }
112
+
113
+ /**
114
+ * Vraća sve kategorije za određenu familiju za potrebe filtera (bez paginacije)
115
+ * @param family - Familija procedura (aesthetics/surgery)
116
+ * @returns Lista aktivnih kategorija koje pripadaju traženoj familiji
117
+ */
118
+ async getAllForFilterByFamily(family: ProcedureFamily) {
119
+ const q = query(
120
+ this.categoriesRef,
121
+ where('family', '==', family),
122
+ where('isActive', '==', true),
123
+ orderBy('name'),
124
+ );
125
+ const snapshot = await getDocs(q);
126
+ const categories = snapshot.docs.map(
127
+ doc =>
128
+ ({
129
+ id: doc.id,
130
+ ...doc.data(),
131
+ } as Category),
132
+ );
133
+ return this.filterExcludedCategories(categories);
134
+ }
135
+
136
+ /**
137
+ * Vraća sve kategorije sa paginacijom
138
+ * @param options - Pagination and filter options
139
+ * @returns Lista kategorija i poslednji vidljiv dokument
140
+ */
141
+ async getAll(
142
+ options: {
143
+ active?: boolean;
144
+ limit?: number;
145
+ lastVisible?: DocumentData;
146
+ } = {},
147
+ ) {
148
+ const { active = true, limit: queryLimit = 10, lastVisible } = options;
149
+ const constraints = [
150
+ where('isActive', '==', active),
151
+ orderBy('name'),
152
+ queryLimit ? limit(queryLimit) : undefined,
153
+ lastVisible ? startAfter(lastVisible) : undefined,
154
+ ].filter((c): c is NonNullable<typeof c> => !!c);
155
+
156
+ const q = query(this.categoriesRef, ...constraints);
157
+ const snapshot = await getDocs(q);
158
+ const categories = snapshot.docs.map(
159
+ doc =>
160
+ ({
161
+ id: doc.id,
162
+ ...doc.data(),
163
+ } as Category),
164
+ );
165
+ const filteredCategories = this.filterExcludedCategories(categories);
166
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
167
+ return { categories: filteredCategories, lastVisible: newLastVisible };
168
+ }
169
+
170
+ /**
171
+ * Vraća sve aktivne kategorije za određenu familiju procedura sa paginacijom
172
+ * @param family - Familija procedura (aesthetics/surgery)
173
+ * @param options - Pagination options
174
+ * @returns Lista kategorija koje pripadaju traženoj familiji
175
+ */
176
+ async getAllByFamily(
177
+ family: ProcedureFamily,
178
+ options: {
179
+ active?: boolean;
180
+ limit?: number;
181
+ lastVisible?: DocumentData;
182
+ } = {},
183
+ ) {
184
+ const { active = true, limit: queryLimit = 10, lastVisible } = options;
185
+ const constraints = [
186
+ where('family', '==', family),
187
+ where('isActive', '==', active),
188
+ orderBy('name'),
189
+ queryLimit ? limit(queryLimit) : undefined,
190
+ lastVisible ? startAfter(lastVisible) : undefined,
191
+ ].filter((c): c is NonNullable<typeof c> => !!c);
192
+
193
+ const q = query(this.categoriesRef, ...constraints);
194
+ const snapshot = await getDocs(q);
195
+ const categories = snapshot.docs.map(
196
+ doc =>
197
+ ({
198
+ id: doc.id,
199
+ ...doc.data(),
200
+ } as Category),
201
+ );
202
+ const filteredCategories = this.filterExcludedCategories(categories);
203
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
204
+ return { categories: filteredCategories, lastVisible: newLastVisible };
205
+ }
206
+
207
+ /**
208
+ * Ažurira postojeću kategoriju
209
+ * @param id - ID kategorije koja se ažurira
210
+ * @param category - Novi podaci za kategoriju
211
+ * @returns Ažurirana kategorija
212
+ */
213
+ async update(id: string, category: Partial<Omit<Category, 'id' | 'createdAt'>>) {
214
+ const updateData = {
215
+ ...category,
216
+ updatedAt: new Date(),
217
+ };
218
+
219
+ const docRef = doc(this.categoriesRef, id);
220
+ await updateDoc(docRef, updateData);
221
+ return this.getById(id);
222
+ }
223
+
224
+ /**
225
+ * Soft delete kategorije (postavlja isActive na false)
226
+ * @param id - ID kategorije koja se briše
227
+ */
228
+ async delete(id: string) {
229
+ await this.update(id, { isActive: false });
230
+ }
231
+
232
+ /**
233
+ * Reactivates a category by setting its isActive flag to true.
234
+ * @param id - The ID of the category to reactivate.
235
+ */
236
+ async reactivate(id: string) {
237
+ await this.update(id, { isActive: true });
238
+ }
239
+
240
+ /**
241
+ * Vraća kategoriju po ID-u
242
+ * @param id - ID tražene kategorije
243
+ * @returns Kategorija ili null ako ne postoji
244
+ */
245
+ async getById(id: string) {
246
+ // Prevent access to excluded category
247
+ if (id === EXCLUDED_CATEGORY_ID) return null;
248
+
249
+ const docRef = doc(this.categoriesRef, id);
250
+ const docSnap = await getDoc(docRef);
251
+ if (!docSnap.exists()) return null;
252
+ return {
253
+ id: docSnap.id,
254
+ ...docSnap.data(),
255
+ } as Category;
256
+ }
257
+
258
+ /**
259
+ * Internal method to get category by ID without filtering.
260
+ * Used internally for consultation procedures.
261
+ * @param id - ID of the category to get
262
+ * @returns Category or null if not found
263
+ */
264
+ async getByIdInternal(id: string): Promise<Category | null> {
265
+ const docRef = doc(this.categoriesRef, id);
266
+ const docSnap = await getDoc(docRef);
267
+ if (!docSnap.exists()) return null;
268
+ return {
269
+ id: docSnap.id,
270
+ ...docSnap.data(),
271
+ } as Category;
272
+ }
273
+
274
+ /**
275
+ * Finds a category by exact name match within a specific family.
276
+ * Used for CSV import matching.
277
+ * @param name - Exact name of the category to find
278
+ * @param family - Procedure family to search within
279
+ * @returns Category if found, null otherwise
280
+ */
281
+ async findByNameAndFamily(name: string, family: ProcedureFamily): Promise<Category | null> {
282
+ const q = query(
283
+ this.categoriesRef,
284
+ where('name', '==', name),
285
+ where('family', '==', family),
286
+ where('isActive', '==', true),
287
+ );
288
+ const snapshot = await getDocs(q);
289
+ if (snapshot.empty) return null;
290
+ const doc = snapshot.docs[0];
291
+ // Exclude consultation category
292
+ if (doc.id === EXCLUDED_CATEGORY_ID) return null;
293
+ return {
294
+ id: doc.id,
295
+ ...doc.data(),
296
+ } as Category;
297
+ }
298
+
299
+ /**
300
+ * Exports categories to CSV string, suitable for Excel/Sheets.
301
+ * Includes headers and optional UTF-8 BOM.
302
+ * By default exports only active categories (set includeInactive to true to export all).
303
+ */
304
+ async exportToCsv(options?: {
305
+ includeInactive?: boolean;
306
+ includeBom?: boolean;
307
+ }): Promise<string> {
308
+ const includeInactive = options?.includeInactive ?? false;
309
+ const includeBom = options?.includeBom ?? true;
310
+
311
+ const headers = [
312
+ "id",
313
+ "name",
314
+ "description",
315
+ "family",
316
+ "isActive",
317
+ ];
318
+
319
+ const rows: string[] = [];
320
+ rows.push(headers.map((h) => this.formatCsvValue(h)).join(","));
321
+
322
+ const PAGE_SIZE = 1000;
323
+ let cursor: any | undefined;
324
+
325
+ // Build base constraints
326
+ const constraints: any[] = [];
327
+ if (!includeInactive) {
328
+ constraints.push(where("isActive", "==", true));
329
+ }
330
+ constraints.push(orderBy("name"));
331
+
332
+ // Page through all results
333
+ // eslint-disable-next-line no-constant-condition
334
+ while (true) {
335
+ const queryConstraints: any[] = [...constraints, limit(PAGE_SIZE)];
336
+ if (cursor) queryConstraints.push(startAfter(cursor));
337
+
338
+ const q = query(this.categoriesRef, ...queryConstraints);
339
+ const snapshot = await getDocs(q);
340
+ if (snapshot.empty) break;
341
+
342
+ for (const d of snapshot.docs) {
343
+ // Exclude consultation category from CSV export
344
+ if (d.id === EXCLUDED_CATEGORY_ID) continue;
345
+ const category = ({ id: d.id, ...d.data() } as unknown) as Category;
346
+ rows.push(this.categoryToCsvRow(category));
347
+ }
348
+
349
+ cursor = snapshot.docs[snapshot.docs.length - 1];
350
+ if (snapshot.size < PAGE_SIZE) break;
351
+ }
352
+
353
+ const csvBody = rows.join("\r\n");
354
+ return includeBom ? "\uFEFF" + csvBody : csvBody;
355
+ }
356
+
357
+ private categoryToCsvRow(category: Category): string {
358
+ const values = [
359
+ category.id ?? "",
360
+ category.name ?? "",
361
+ category.description ?? "",
362
+ category.family ?? "",
363
+ String(category.isActive ?? ""),
364
+ ];
365
+ return values.map((v) => this.formatCsvValue(v)).join(",");
366
+ }
367
+
368
+ private formatDateIso(value: any): string {
369
+ // Firestore timestamps may come back as Date or Timestamp; handle both
370
+ if (value instanceof Date) return value.toISOString();
371
+ if (value && typeof value.toDate === "function") {
372
+ const d = value.toDate();
373
+ return d instanceof Date ? d.toISOString() : String(value);
374
+ }
375
+ return String(value ?? "");
376
+ }
377
+
378
+ private formatCsvValue(value: any): string {
379
+ const str = value === null || value === undefined ? "" : String(value);
380
+ // Escape double quotes by doubling them and wrap in quotes
381
+ const escaped = str.replace(/"/g, '""');
382
+ return `"${escaped}"`;
383
+ }
384
+ }