@blackcode_sa/metaestetics-api 1.15.14 → 1.15.17-staging.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 (326) hide show
  1. package/dist/admin/index.d.mts +377 -222
  2. package/dist/admin/index.d.ts +377 -222
  3. package/dist/admin/index.js +625 -206
  4. package/dist/admin/index.mjs +624 -206
  5. package/dist/backoffice/index.d.mts +24 -0
  6. package/dist/backoffice/index.d.ts +24 -0
  7. package/dist/index.d.mts +297 -9
  8. package/dist/index.d.ts +297 -9
  9. package/dist/index.js +1144 -632
  10. package/dist/index.mjs +1139 -619
  11. package/package.json +2 -1
  12. package/src/__mocks__/firstore.ts +10 -10
  13. package/src/admin/aggregation/README.md +79 -79
  14. package/src/admin/aggregation/appointment/README.md +151 -129
  15. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +2137 -2091
  16. package/src/admin/aggregation/appointment/index.ts +1 -1
  17. package/src/admin/aggregation/clinic/README.md +52 -52
  18. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -966
  19. package/src/admin/aggregation/clinic/index.ts +1 -1
  20. package/src/admin/aggregation/forms/README.md +13 -13
  21. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  22. package/src/admin/aggregation/forms/index.ts +1 -1
  23. package/src/admin/aggregation/index.ts +8 -8
  24. package/src/admin/aggregation/patient/README.md +27 -27
  25. package/src/admin/aggregation/patient/index.ts +1 -1
  26. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  27. package/src/admin/aggregation/practitioner/README.md +42 -42
  28. package/src/admin/aggregation/practitioner/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  30. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  31. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  32. package/src/admin/aggregation/procedure/README.md +43 -43
  33. package/src/admin/aggregation/procedure/index.ts +1 -1
  34. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  35. package/src/admin/aggregation/reviews/index.ts +1 -1
  36. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  37. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  38. package/src/admin/analytics/index.ts +2 -2
  39. package/src/admin/booking/README.md +184 -125
  40. package/src/admin/booking/booking.admin.ts +1330 -1073
  41. package/src/admin/booking/booking.calculator.ts +850 -712
  42. package/src/admin/booking/booking.types.ts +76 -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 +62 -7
  46. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  47. package/src/admin/calendar/index.ts +2 -1
  48. package/src/admin/calendar/resource-calendar.admin.ts +198 -0
  49. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  50. package/src/admin/documentation-templates/index.ts +1 -1
  51. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  52. package/src/admin/free-consultation/index.ts +1 -1
  53. package/src/admin/index.ts +83 -83
  54. package/src/admin/logger/index.ts +78 -78
  55. package/src/admin/mailing/README.md +139 -139
  56. package/src/admin/mailing/appointment/appointment.mailing.service.ts +1253 -1253
  57. package/src/admin/mailing/appointment/index.ts +1 -1
  58. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  59. package/src/admin/mailing/base.mailing.service.ts +208 -208
  60. package/src/admin/mailing/clinicWelcome/clinicWelcome.mailing.ts +292 -292
  61. package/src/admin/mailing/clinicWelcome/index.ts +1 -1
  62. package/src/admin/mailing/clinicWelcome/templates/welcome.template.ts +225 -225
  63. package/src/admin/mailing/index.ts +5 -5
  64. package/src/admin/mailing/patientInvite/index.ts +2 -2
  65. package/src/admin/mailing/patientInvite/patientInvite.mailing.ts +415 -415
  66. package/src/admin/mailing/patientInvite/templates/invitation.template.ts +105 -105
  67. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  68. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  69. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  70. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  71. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  72. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  73. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  74. package/src/admin/notifications/index.ts +1 -1
  75. package/src/admin/notifications/notifications.admin.ts +818 -818
  76. package/src/admin/requirements/README.md +128 -128
  77. package/src/admin/requirements/index.ts +1 -1
  78. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  79. package/src/admin/users/index.ts +1 -1
  80. package/src/admin/users/user-profile.admin.ts +405 -405
  81. package/src/backoffice/constants/certification.constants.ts +13 -13
  82. package/src/backoffice/constants/index.ts +1 -1
  83. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  84. package/src/backoffice/errors/index.ts +1 -1
  85. package/src/backoffice/expo-safe/README.md +26 -26
  86. package/src/backoffice/expo-safe/index.ts +41 -41
  87. package/src/backoffice/index.ts +5 -5
  88. package/src/backoffice/services/FIXES_README.md +102 -102
  89. package/src/backoffice/services/README.md +57 -57
  90. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  91. package/src/backoffice/services/analytics.service.summary.md +143 -143
  92. package/src/backoffice/services/brand.service.ts +260 -260
  93. package/src/backoffice/services/category.service.ts +384 -384
  94. package/src/backoffice/services/constants.service.ts +385 -385
  95. package/src/backoffice/services/documentation-template.service.ts +202 -202
  96. package/src/backoffice/services/index.ts +10 -10
  97. package/src/backoffice/services/migrate-products.ts +116 -116
  98. package/src/backoffice/services/product.service.ts +557 -557
  99. package/src/backoffice/services/requirement.service.ts +235 -235
  100. package/src/backoffice/services/subcategory.service.ts +461 -461
  101. package/src/backoffice/services/technology.service.ts +1153 -1153
  102. package/src/backoffice/types/README.md +12 -12
  103. package/src/backoffice/types/admin-constants.types.ts +69 -69
  104. package/src/backoffice/types/brand.types.ts +29 -29
  105. package/src/backoffice/types/category.types.ts +67 -67
  106. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  107. package/src/backoffice/types/index.ts +10 -10
  108. package/src/backoffice/types/procedure-product.types.ts +38 -38
  109. package/src/backoffice/types/product.types.ts +239 -239
  110. package/src/backoffice/types/requirement.types.ts +63 -63
  111. package/src/backoffice/types/static/README.md +18 -18
  112. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  113. package/src/backoffice/types/static/certification.types.ts +37 -37
  114. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  115. package/src/backoffice/types/static/index.ts +6 -6
  116. package/src/backoffice/types/static/pricing.types.ts +16 -16
  117. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  118. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  119. package/src/backoffice/types/subcategory.types.ts +34 -34
  120. package/src/backoffice/types/technology.types.ts +168 -168
  121. package/src/backoffice/validations/index.ts +1 -1
  122. package/src/backoffice/validations/schemas.ts +164 -164
  123. package/src/config/__mocks__/firebase.ts +99 -99
  124. package/src/config/firebase.ts +78 -78
  125. package/src/config/index.ts +17 -17
  126. package/src/config/tiers.config.ts +255 -229
  127. package/src/errors/auth.error.ts +6 -6
  128. package/src/errors/auth.errors.ts +211 -211
  129. package/src/errors/clinic.errors.ts +32 -32
  130. package/src/errors/firebase.errors.ts +47 -47
  131. package/src/errors/user.errors.ts +99 -99
  132. package/src/index.backup.ts +407 -407
  133. package/src/index.ts +6 -6
  134. package/src/locales/en.ts +31 -31
  135. package/src/recommender/admin/index.ts +1 -1
  136. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  137. package/src/recommender/front/index.ts +1 -1
  138. package/src/recommender/front/services/onboarding.service.ts +5 -5
  139. package/src/recommender/front/services/recommender.service.ts +3 -3
  140. package/src/recommender/index.ts +1 -1
  141. package/src/services/PATIENTAUTH.MD +197 -197
  142. package/src/services/README.md +106 -106
  143. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  144. package/src/services/__tests__/auth/auth.setup.ts +298 -298
  145. package/src/services/__tests__/auth.service.test.ts +310 -310
  146. package/src/services/__tests__/base.service.test.ts +36 -36
  147. package/src/services/__tests__/user.service.test.ts +530 -530
  148. package/src/services/analytics/ARCHITECTURE.md +199 -199
  149. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  150. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  151. package/src/services/analytics/QUICK_START.md +393 -393
  152. package/src/services/analytics/README.md +304 -304
  153. package/src/services/analytics/SUMMARY.md +141 -141
  154. package/src/services/analytics/TRENDS.md +380 -380
  155. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  156. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  157. package/src/services/analytics/analytics.service.ts +2148 -2148
  158. package/src/services/analytics/index.ts +4 -4
  159. package/src/services/analytics/review-analytics.service.ts +941 -941
  160. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  161. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  162. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  163. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  164. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  165. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  166. package/src/services/appointment/README.md +17 -17
  167. package/src/services/appointment/appointment.service.ts +2943 -2941
  168. package/src/services/appointment/index.ts +1 -1
  169. package/src/services/appointment/utils/appointment.utils.ts +620 -620
  170. package/src/services/appointment/utils/extended-procedure.utils.ts +354 -354
  171. package/src/services/appointment/utils/form-initialization.utils.ts +516 -516
  172. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  173. package/src/services/appointment/utils/zone-management.utils.ts +468 -468
  174. package/src/services/appointment/utils/zone-photo.utils.ts +302 -302
  175. package/src/services/auth/auth.service.ts +1435 -1435
  176. package/src/services/auth/auth.v2.service.ts +961 -961
  177. package/src/services/auth/index.ts +7 -7
  178. package/src/services/auth/utils/error.utils.ts +90 -90
  179. package/src/services/auth/utils/firebase.utils.ts +49 -49
  180. package/src/services/auth/utils/index.ts +21 -21
  181. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  182. package/src/services/base.service.ts +41 -41
  183. package/src/services/calendar/calendar.service.ts +1077 -1077
  184. package/src/services/calendar/calendar.v2.service.ts +1693 -1693
  185. package/src/services/calendar/calendar.v3.service.ts +313 -313
  186. package/src/services/calendar/externalCalendar.service.ts +178 -178
  187. package/src/services/calendar/index.ts +5 -5
  188. package/src/services/calendar/synced-calendars.service.ts +743 -743
  189. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  190. package/src/services/calendar/utils/calendar-event.utils.ts +676 -676
  191. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  192. package/src/services/calendar/utils/docs.utils.ts +157 -157
  193. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  194. package/src/services/calendar/utils/index.ts +8 -8
  195. package/src/services/calendar/utils/patient.utils.ts +198 -198
  196. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  197. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  198. package/src/services/clinic/README.md +204 -204
  199. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +265 -265
  200. package/src/services/clinic/__tests__/clinic-group.service.test.ts +222 -222
  201. package/src/services/clinic/__tests__/clinic.service.test.ts +302 -302
  202. package/src/services/clinic/billing-transactions.service.ts +217 -217
  203. package/src/services/clinic/clinic-admin.service.ts +202 -202
  204. package/src/services/clinic/clinic-group.service.ts +310 -310
  205. package/src/services/clinic/clinic.service.ts +720 -720
  206. package/src/services/clinic/index.ts +5 -5
  207. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  208. package/src/services/clinic/utils/admin.utils.ts +551 -551
  209. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  210. package/src/services/clinic/utils/clinic.utils.ts +1023 -1023
  211. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  212. package/src/services/clinic/utils/filter.utils.ts +462 -462
  213. package/src/services/clinic/utils/index.ts +10 -10
  214. package/src/services/clinic/utils/photos.utils.ts +188 -188
  215. package/src/services/clinic/utils/search.utils.ts +83 -83
  216. package/src/services/clinic/utils/tag.utils.ts +124 -124
  217. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  218. package/src/services/documentation-templates/filled-document.service.ts +597 -597
  219. package/src/services/documentation-templates/index.ts +2 -2
  220. package/src/services/index.ts +16 -15
  221. package/src/services/media/index.ts +1 -1
  222. package/src/services/media/media.service.ts +418 -418
  223. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  224. package/src/services/notifications/index.ts +1 -1
  225. package/src/services/notifications/notification.service.ts +215 -215
  226. package/src/services/patient/README.md +48 -48
  227. package/src/services/patient/To-Do.md +43 -43
  228. package/src/services/patient/__tests__/patient.service.test.ts +286 -286
  229. package/src/services/patient/index.ts +2 -2
  230. package/src/services/patient/patient.service.ts +1021 -1021
  231. package/src/services/patient/patientRequirements.service.ts +309 -309
  232. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  233. package/src/services/patient/utils/body-assessment.utils.ts +159 -159
  234. package/src/services/patient/utils/clinic.utils.ts +159 -159
  235. package/src/services/patient/utils/docs.utils.ts +142 -142
  236. package/src/services/patient/utils/hair-scalp-assessment.utils.ts +158 -158
  237. package/src/services/patient/utils/index.ts +9 -9
  238. package/src/services/patient/utils/location.utils.ts +126 -126
  239. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  240. package/src/services/patient/utils/medical.utils.ts +458 -458
  241. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  242. package/src/services/patient/utils/pre-surgical-assessment.utils.ts +161 -161
  243. package/src/services/patient/utils/profile.utils.ts +510 -510
  244. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  245. package/src/services/patient/utils/skin-quality-assessment.utils.ts +160 -160
  246. package/src/services/patient/utils/token.utils.ts +211 -211
  247. package/src/services/practitioner/README.md +145 -145
  248. package/src/services/practitioner/index.ts +1 -1
  249. package/src/services/practitioner/practitioner.service.ts +2355 -2354
  250. package/src/services/procedure/README.md +163 -163
  251. package/src/services/procedure/index.ts +1 -1
  252. package/src/services/procedure/procedure.service.ts +2521 -2521
  253. package/src/services/resource/README.md +119 -0
  254. package/src/services/resource/index.ts +1 -0
  255. package/src/services/resource/resource.service.ts +555 -0
  256. package/src/services/reviews/index.ts +1 -1
  257. package/src/services/reviews/reviews.service.ts +745 -745
  258. package/src/services/tier-enforcement.ts +240 -240
  259. package/src/services/user/index.ts +1 -1
  260. package/src/services/user/user.service.ts +533 -533
  261. package/src/services/user/user.v2.service.ts +467 -467
  262. package/src/types/analytics/analytics.types.ts +597 -597
  263. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  264. package/src/types/analytics/index.ts +4 -4
  265. package/src/types/analytics/stored-analytics.types.ts +137 -137
  266. package/src/types/appointment/index.ts +524 -517
  267. package/src/types/calendar/index.ts +261 -260
  268. package/src/types/calendar/synced-calendar.types.ts +66 -66
  269. package/src/types/clinic/index.ts +530 -529
  270. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  271. package/src/types/clinic/preferences.types.ts +159 -159
  272. package/src/types/clinic/rbac.types.ts +64 -63
  273. package/src/types/clinic/to-do +3 -3
  274. package/src/types/documentation-templates/index.ts +308 -308
  275. package/src/types/index.ts +50 -47
  276. package/src/types/notifications/README.md +77 -77
  277. package/src/types/notifications/index.ts +300 -300
  278. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  279. package/src/types/patient/allergies.ts +58 -58
  280. package/src/types/patient/body-assessment.types.ts +93 -93
  281. package/src/types/patient/hair-scalp-assessment.types.ts +98 -98
  282. package/src/types/patient/index.ts +279 -279
  283. package/src/types/patient/medical-info.types.ts +152 -152
  284. package/src/types/patient/patient-requirements.ts +92 -92
  285. package/src/types/patient/pre-surgical-assessment.types.ts +95 -95
  286. package/src/types/patient/skin-quality-assessment.types.ts +105 -105
  287. package/src/types/patient/token.types.ts +61 -61
  288. package/src/types/practitioner/index.ts +208 -208
  289. package/src/types/procedure/index.ts +189 -183
  290. package/src/types/profile/index.ts +39 -39
  291. package/src/types/resource/README.md +153 -0
  292. package/src/types/resource/index.ts +199 -0
  293. package/src/types/reviews/index.ts +132 -132
  294. package/src/types/tz-lookup.d.ts +4 -4
  295. package/src/types/user/index.ts +60 -60
  296. package/src/utils/TIMESTAMPS.md +176 -176
  297. package/src/utils/TimestampUtils.ts +241 -241
  298. package/src/utils/index.ts +1 -1
  299. package/src/validations/README.md +94 -0
  300. package/src/validations/appointment.schema.ts +589 -589
  301. package/src/validations/calendar.schema.ts +225 -225
  302. package/src/validations/clinic.schema.ts +494 -494
  303. package/src/validations/common.schema.ts +25 -25
  304. package/src/validations/documentation-templates/index.ts +1 -1
  305. package/src/validations/documentation-templates/template.schema.ts +220 -220
  306. package/src/validations/documentation-templates.schema.ts +10 -10
  307. package/src/validations/index.ts +21 -20
  308. package/src/validations/media.schema.ts +10 -10
  309. package/src/validations/notification.schema.ts +90 -90
  310. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  311. package/src/validations/patient/body-assessment.schema.ts +82 -82
  312. package/src/validations/patient/hair-scalp-assessment.schema.ts +70 -70
  313. package/src/validations/patient/medical-info.schema.ts +177 -177
  314. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  315. package/src/validations/patient/pre-surgical-assessment.schema.ts +78 -78
  316. package/src/validations/patient/skin-quality-assessment.schema.ts +70 -70
  317. package/src/validations/patient/token.schema.ts +29 -29
  318. package/src/validations/patient.schema.ts +217 -217
  319. package/src/validations/practitioner.schema.ts +224 -224
  320. package/src/validations/procedure-product.schema.ts +41 -41
  321. package/src/validations/procedure.schema.ts +136 -124
  322. package/src/validations/profile-info.schema.ts +41 -41
  323. package/src/validations/resource.schema.ts +57 -0
  324. package/src/validations/reviews.schema.ts +195 -195
  325. package/src/validations/schemas.ts +109 -109
  326. package/src/validations/shared.schema.ts +78 -78
@@ -1,646 +1,646 @@
1
- import {
2
- collection,
3
- doc,
4
- getDoc,
5
- getDocs,
6
- query,
7
- where,
8
- updateDoc,
9
- setDoc,
10
- deleteDoc,
11
- Timestamp,
12
- Firestore,
13
- serverTimestamp,
14
- } from "firebase/firestore";
15
- import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
16
- import {
17
- ClinicGroup,
18
- CreateClinicGroupData,
19
- CLINIC_GROUPS_COLLECTION,
20
- AdminToken,
21
- AdminTokenStatus,
22
- CreateAdminTokenData,
23
- SubscriptionModel,
24
- } from "../../../types/clinic";
25
- import { geohashForLocation } from "geofire-common";
26
- import {
27
- clinicGroupSchema,
28
- createClinicGroupSchema,
29
- } from "../../../validations/clinic.schema";
30
- import { z } from "zod";
31
- import { uploadPhoto } from "./photos.utils";
32
- import { FirebaseApp } from "firebase/app";
33
-
34
- /**
35
- * Generates a unique ID for documents
36
- * Format: xxxxxxxxxxxx-timestamp
37
- * Where x is a random character (number or letter)
38
- */
39
- function generateId(): string {
40
- const chars =
41
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
42
- const timestamp = Date.now().toString(36);
43
- const randomPart = Array.from({ length: 12 }, () =>
44
- chars.charAt(Math.floor(Math.random() * chars.length))
45
- ).join("");
46
-
47
- return `${randomPart}-${timestamp}`;
48
- }
49
-
50
- /**
51
- * Creates a new clinic group
52
- * @param db - Firestore database instance
53
- * @param data - Clinic group data
54
- * @param ownerId - ID of the owner
55
- * @param isDefault - Whether this is a default group
56
- * @param clinicAdminService - Service for clinic admin operations
57
- * @param app - Firebase app instance
58
- * @returns The created clinic group
59
- */
60
- export async function createClinicGroup(
61
- db: Firestore,
62
- data: CreateClinicGroupData,
63
- ownerId: string,
64
- isDefault: boolean = false,
65
- clinicAdminService: any,
66
- app: FirebaseApp
67
- ): Promise<ClinicGroup> {
68
- console.log("[CLINIC_GROUP] Starting clinic group creation", {
69
- ownerId,
70
- isDefault,
71
- });
72
- console.log("[CLINIC_GROUP] Input data:", JSON.stringify(data, null, 2));
73
-
74
- let validatedData: CreateClinicGroupData;
75
- // Validacija podataka
76
- try {
77
- validatedData = createClinicGroupSchema.parse(data) as CreateClinicGroupData;
78
- console.log("[CLINIC_GROUP] Data validation passed");
79
- } catch (validationError) {
80
- console.error("[CLINIC_GROUP] Data validation failed:", validationError);
81
- throw validationError;
82
- }
83
-
84
- // Proveravamo da li owner postoji i da li je clinic admin
85
- try {
86
- console.log("[CLINIC_GROUP] Checking if owner exists", { ownerId });
87
- // Skip owner verification for default groups since the admin profile doesn't exist yet
88
- if (isDefault) {
89
- console.log(
90
- "[CLINIC_GROUP] Skipping owner verification for default group creation"
91
- );
92
- } else {
93
- const owner = await clinicAdminService.getClinicAdmin(ownerId);
94
- if (!owner) {
95
- console.error(
96
- "[CLINIC_GROUP] Owner not found or is not a clinic admin",
97
- {
98
- ownerId,
99
- }
100
- );
101
- throw new Error("Owner not found or is not a clinic admin");
102
- }
103
- console.log("[CLINIC_GROUP] Owner verified as clinic admin");
104
- }
105
- } catch (ownerError) {
106
- console.error("[CLINIC_GROUP] Error verifying owner:", ownerError);
107
- throw ownerError;
108
- }
109
-
110
- // Generišemo geohash za lokaciju
111
- console.log("[CLINIC_GROUP] Generating geohash for location");
112
- if (validatedData.hqLocation) {
113
- try {
114
- validatedData.hqLocation.geohash = geohashForLocation([
115
- validatedData.hqLocation.latitude,
116
- validatedData.hqLocation.longitude,
117
- ]);
118
- console.log("[CLINIC_GROUP] Geohash generated successfully", {
119
- geohash: validatedData.hqLocation.geohash,
120
- });
121
- } catch (geohashError) {
122
- console.error("[CLINIC_GROUP] Error generating geohash:", geohashError);
123
- throw geohashError;
124
- }
125
- }
126
-
127
- const now = Timestamp.now();
128
- console.log("[CLINIC_GROUP] Preparing clinic group data object");
129
-
130
- // Generate a unique ID for the clinic group
131
- const groupId = doc(collection(db, CLINIC_GROUPS_COLLECTION)).id;
132
-
133
- // Log the logo value to debug null vs undefined issue
134
- console.log("[CLINIC_GROUP] Logo value:", {
135
- logoValue: validatedData.logo,
136
- logoType: validatedData.logo === null ? "null" : typeof validatedData.logo,
137
- });
138
-
139
- // Handle logo upload if provided
140
- let logoUrl = await uploadPhoto(
141
- (validatedData.logo as string) || null,
142
- "clinic-groups",
143
- groupId,
144
- "logo",
145
- app
146
- );
147
- console.log("[CLINIC_GROUP] Logo processed", { logoUrl });
148
-
149
- const groupData: ClinicGroup = {
150
- ...validatedData,
151
- id: groupId,
152
- name: validatedData.name,
153
- logo: logoUrl, // Use the uploaded logo URL or the original value
154
- description: validatedData.description || "",
155
- hqLocation: validatedData.hqLocation,
156
- contactInfo: validatedData.contactInfo,
157
- contactPerson: validatedData.contactPerson,
158
- subscriptionModel:
159
- validatedData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
160
- clinics: [],
161
- clinicsInfo: [],
162
- admins: [ownerId],
163
- adminsInfo: [],
164
- adminTokens: [],
165
- ownerId,
166
- createdAt: now,
167
- updatedAt: now,
168
- isActive: true,
169
- };
170
-
171
- try {
172
- // Validiramo kompletan objekat
173
- console.log("[CLINIC_GROUP] Validating complete clinic group object");
174
- try {
175
- clinicGroupSchema.parse(groupData);
176
- console.log("[CLINIC_GROUP] Clinic group validation passed");
177
- } catch (schemaError) {
178
- console.error(
179
- "[CLINIC_GROUP] Clinic group validation failed:",
180
- JSON.stringify(schemaError, null, 2)
181
- );
182
- throw schemaError;
183
- }
184
-
185
- // Čuvamo u Firestore
186
- console.log("[CLINIC_GROUP] Saving clinic group to Firestore", {
187
- groupId: groupData.id,
188
- });
189
- try {
190
- await setDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
191
- console.log("[CLINIC_GROUP] Clinic group saved successfully");
192
- } catch (firestoreError) {
193
- console.error(
194
- "[CLINIC_GROUP] Error saving to Firestore:",
195
- firestoreError
196
- );
197
- throw firestoreError;
198
- }
199
-
200
- // Ažuriramo clinic admin profil vlasnika
201
- console.log("[CLINIC_GROUP] Updating clinic admin profile for owner", {
202
- ownerId,
203
- });
204
- try {
205
- await clinicAdminService.updateClinicAdmin(ownerId, {
206
- clinicGroupId: groupData.id,
207
- isGroupOwner: true,
208
- });
209
- console.log("[CLINIC_GROUP] Clinic admin profile updated successfully");
210
- } catch (updateError) {
211
- console.error(
212
- "[CLINIC_GROUP] Error updating clinic admin profile:",
213
- updateError
214
- );
215
- throw updateError;
216
- }
217
-
218
- console.log("[CLINIC_GROUP] Clinic group creation completed successfully", {
219
- groupId: groupData.id,
220
- groupName: groupData.name,
221
- });
222
- return groupData;
223
- } catch (error) {
224
- if (error instanceof z.ZodError) {
225
- console.error(
226
- "[CLINIC_GROUP] Zod validation error:",
227
- JSON.stringify(error.errors, null, 2)
228
- );
229
- throw new Error("Invalid clinic group data: " + error.message);
230
- }
231
- console.error(
232
- "[CLINIC_GROUP] Unhandled error in createClinicGroup:",
233
- error
234
- );
235
- throw error;
236
- }
237
- }
238
-
239
- /**
240
- * Gets a clinic group by ID
241
- * @param db - Firestore database instance
242
- * @param groupId - ID of the clinic group
243
- * @returns The clinic group or null if not found
244
- */
245
- export async function getClinicGroup(
246
- db: Firestore,
247
- groupId: string
248
- ): Promise<ClinicGroup | null> {
249
- const docRef = doc(db, CLINIC_GROUPS_COLLECTION, groupId);
250
- const docSnap = await getDoc(docRef);
251
-
252
- if (docSnap.exists()) {
253
- return docSnap.data() as ClinicGroup;
254
- }
255
-
256
- return null;
257
- }
258
-
259
- /**
260
- * Gets all active clinic groups
261
- * @param db - Firestore database instance
262
- * @returns Array of active clinic groups
263
- */
264
- export async function getAllActiveGroups(
265
- db: Firestore
266
- ): Promise<ClinicGroup[]> {
267
- const q = query(
268
- collection(db, CLINIC_GROUPS_COLLECTION),
269
- where("isActive", "==", true)
270
- );
271
-
272
- const querySnapshot = await getDocs(q);
273
- return querySnapshot.docs.map((doc) => doc.data() as ClinicGroup);
274
- }
275
-
276
- /**
277
- * Updates a clinic group
278
- * @param db - Firestore database instance
279
- * @param groupId - ID of the clinic group
280
- * @param data - Data to update
281
- * @param app - Firebase app instance
282
- * @returns The updated clinic group
283
- */
284
- export async function updateClinicGroup(
285
- db: Firestore,
286
- groupId: string,
287
- data: Partial<ClinicGroup>,
288
- app: FirebaseApp
289
- ): Promise<ClinicGroup> {
290
- console.log("[CLINIC_GROUP] Updating clinic group", { groupId });
291
-
292
- const group = await getClinicGroup(db, groupId);
293
- if (!group) {
294
- console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
295
- throw new Error("Clinic group not found");
296
- }
297
-
298
- // Process logo if it's a data URL
299
- let updatedData = { ...data };
300
-
301
- if (
302
- data.logo &&
303
- typeof data.logo === "string" &&
304
- data.logo.startsWith("data:")
305
- ) {
306
- console.log("[CLINIC_GROUP] Processing logo for update");
307
- try {
308
- const logoUrl = await uploadPhoto(
309
- data.logo,
310
- "clinic-groups",
311
- groupId,
312
- "logo",
313
- app
314
- );
315
- console.log("[CLINIC_GROUP] Logo processed for update", { logoUrl });
316
-
317
- // Replace the data URL with the uploaded URL
318
- updatedData.logo = logoUrl;
319
- } catch (error) {
320
- console.error("[CLINIC_GROUP] Error processing logo for update:", error);
321
- // Continue with update even if logo upload fails
322
- }
323
- }
324
-
325
- // Add timestamp
326
- updatedData = {
327
- ...updatedData,
328
- updatedAt: Timestamp.now(),
329
- };
330
-
331
- console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
332
- await updateDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
333
- console.log("[CLINIC_GROUP] Clinic group updated successfully");
334
-
335
- // Return updated data
336
- const updatedGroup = await getClinicGroup(db, groupId);
337
- if (!updatedGroup) {
338
- console.error("[CLINIC_GROUP] Failed to retrieve updated clinic group");
339
- throw new Error("Failed to retrieve updated clinic group");
340
- }
341
-
342
- return updatedGroup;
343
- }
344
-
345
- /**
346
- * Adds an admin to a clinic group
347
- * @param db - Firestore database instance
348
- * @param groupId - ID of the clinic group
349
- * @param adminId - ID of the admin to add (this is the admin document ID, not the user UID)
350
- * @param app - Firebase app instance
351
- */
352
- export async function addAdminToGroup(
353
- db: Firestore,
354
- groupId: string,
355
- adminId: string,
356
- app: FirebaseApp
357
- ): Promise<void> {
358
- console.log("[CLINIC_GROUP] Adding admin to group", { groupId, adminId });
359
-
360
- try {
361
- const group = await getClinicGroup(db, groupId);
362
- if (!group) {
363
- console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
364
- throw new Error("Clinic group not found");
365
- }
366
-
367
- if (group.admins.includes(adminId)) {
368
- console.log("[CLINIC_GROUP] Admin is already in the group", {
369
- adminId,
370
- groupId,
371
- });
372
- return; // Admin is already in the group
373
- }
374
-
375
- console.log("[CLINIC_GROUP] Updating group with new admin");
376
- await updateClinicGroup(
377
- db,
378
- groupId,
379
- {
380
- admins: [...group.admins, adminId],
381
- },
382
- app
383
- );
384
- console.log("[CLINIC_GROUP] Admin added to group successfully");
385
- } catch (error) {
386
- console.error("[CLINIC_GROUP] Error adding admin to group:", error);
387
- throw error;
388
- }
389
- }
390
-
391
- /**
392
- * Removes an admin from a clinic group
393
- * @param db - Firestore database instance
394
- * @param groupId - ID of the clinic group
395
- * @param adminId - ID of the admin to remove
396
- * @param app - Firebase app instance
397
- */
398
- export async function removeAdminFromGroup(
399
- db: Firestore,
400
- groupId: string,
401
- adminId: string,
402
- app: FirebaseApp
403
- ): Promise<void> {
404
- const group = await getClinicGroup(db, groupId);
405
- if (!group) {
406
- throw new Error("Clinic group not found");
407
- }
408
-
409
- if (group.ownerId === adminId) {
410
- throw new Error("Cannot remove the owner from the group");
411
- }
412
-
413
- if (!group.admins.includes(adminId)) {
414
- return; // Admin is not in the group
415
- }
416
-
417
- await updateClinicGroup(
418
- db,
419
- groupId,
420
- {
421
- admins: group.admins.filter((id) => id !== adminId),
422
- },
423
- app
424
- );
425
- }
426
-
427
- /**
428
- * Deactivates a clinic group
429
- * @param db - Firestore database instance
430
- * @param groupId - ID of the clinic group
431
- * @param app - Firebase app instance
432
- */
433
- export async function deactivateClinicGroup(
434
- db: Firestore,
435
- groupId: string,
436
- app: FirebaseApp
437
- ): Promise<void> {
438
- const group = await getClinicGroup(db, groupId);
439
- if (!group) {
440
- throw new Error("Clinic group not found");
441
- }
442
-
443
- await updateClinicGroup(
444
- db,
445
- groupId,
446
- {
447
- isActive: false,
448
- },
449
- app
450
- );
451
- }
452
-
453
- /**
454
- * Creates an admin token for a clinic group
455
- * @param db - Firestore database instance
456
- * @param groupId - ID of the clinic group
457
- * @param creatorAdminId - ID of the admin creating the token
458
- * @param app - Firebase app instance
459
- * @param data - Token data
460
- * @returns The created admin token
461
- */
462
- export async function createAdminToken(
463
- db: Firestore,
464
- groupId: string,
465
- creatorAdminId: string,
466
- app: FirebaseApp,
467
- data?: CreateAdminTokenData
468
- ): Promise<AdminToken> {
469
- const group = await getClinicGroup(db, groupId);
470
- if (!group) {
471
- throw new Error("Clinic group not found");
472
- }
473
-
474
- // Proveravamo da li admin pripada grupi
475
- if (!group.admins.includes(creatorAdminId)) {
476
- throw new Error("Admin does not belong to this clinic group");
477
- }
478
-
479
- const now = Timestamp.now();
480
- const expiresInDays = data?.expiresInDays || 7; // Default 7 days
481
- const email = data?.email || null;
482
- const expiresAt = new Timestamp(
483
- now.seconds + expiresInDays * 24 * 60 * 60,
484
- now.nanoseconds
485
- );
486
-
487
- const token: AdminToken = {
488
- id: generateId(),
489
- token: generateId(),
490
- status: AdminTokenStatus.ACTIVE,
491
- email,
492
- createdAt: now,
493
- expiresAt,
494
- };
495
-
496
- // Dodajemo token u grupu
497
- // Ovo treba promeniti, staviti admin tokene u sub-kolekciju u klinickoj grupi
498
- await updateClinicGroup(
499
- db,
500
- groupId,
501
- {
502
- adminTokens: [...group.adminTokens, token],
503
- },
504
- app
505
- );
506
-
507
- return token;
508
- }
509
-
510
- /**
511
- * Verifies and uses an admin token
512
- * @param db - Firestore database instance
513
- * @param groupId - ID of the clinic group
514
- * @param token - Token to verify
515
- * @param userRef - User reference
516
- * @param app - Firebase app instance
517
- * @returns Whether the token was successfully used
518
- */
519
- export async function verifyAndUseAdminToken(
520
- db: Firestore,
521
- groupId: string,
522
- token: string,
523
- userRef: string,
524
- app: FirebaseApp
525
- ): Promise<boolean> {
526
- const group = await getClinicGroup(db, groupId);
527
- if (!group) {
528
- throw new Error("Clinic group not found");
529
- }
530
-
531
- const adminToken = group.adminTokens.find((t) => t.token === token);
532
- if (!adminToken) {
533
- throw new Error("Admin token not found");
534
- }
535
-
536
- if (adminToken.status !== AdminTokenStatus.ACTIVE) {
537
- throw new Error("Admin token is not active");
538
- }
539
-
540
- const now = Timestamp.now();
541
- if (adminToken.expiresAt.seconds < now.seconds) {
542
- // Token je istekao, ažuriramo status
543
- const updatedTokens = group.adminTokens.map((t) =>
544
- t.id === adminToken.id ? { ...t, status: AdminTokenStatus.EXPIRED } : t
545
- );
546
-
547
- await updateClinicGroup(
548
- db,
549
- groupId,
550
- {
551
- adminTokens: updatedTokens,
552
- },
553
- app
554
- );
555
-
556
- throw new Error("Admin token has expired");
557
- }
558
-
559
- // Token je validan, ažuriramo status
560
- const updatedTokens = group.adminTokens.map((t) =>
561
- t.id === adminToken.id
562
- ? {
563
- ...t,
564
- status: AdminTokenStatus.USED,
565
- usedByUserRef: userRef,
566
- }
567
- : t
568
- );
569
-
570
- await updateClinicGroup(
571
- db,
572
- groupId,
573
- {
574
- adminTokens: updatedTokens,
575
- },
576
- app
577
- );
578
-
579
- return true;
580
- }
581
-
582
- /**
583
- * Deletes an admin token
584
- * @param db - Firestore database instance
585
- * @param groupId - ID of the clinic group
586
- * @param tokenId - ID of the token to delete
587
- * @param adminId - ID of the admin making the deletion
588
- * @param app - Firebase app instance
589
- */
590
- export async function deleteAdminToken(
591
- db: Firestore,
592
- groupId: string,
593
- tokenId: string,
594
- adminId: string,
595
- app: FirebaseApp
596
- ): Promise<void> {
597
- const group = await getClinicGroup(db, groupId);
598
- if (!group) {
599
- throw new Error("Clinic group not found");
600
- }
601
-
602
- // Proveravamo da li admin pripada grupi
603
- if (!group.admins.includes(adminId)) {
604
- throw new Error("Admin does not belong to this clinic group");
605
- }
606
-
607
- // Uklanjamo token
608
- const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
609
-
610
- await updateClinicGroup(
611
- db,
612
- groupId,
613
- {
614
- adminTokens: updatedTokens,
615
- },
616
- app
617
- );
618
- }
619
-
620
- /**
621
- * Gets active admin tokens for a clinic group
622
- * @param db - Firestore database instance
623
- * @param groupId - ID of the clinic group
624
- * @param adminId - ID of the admin requesting the tokens
625
- * @param app - Firebase app instance (not used but included for consistency)
626
- * @returns Array of active admin tokens
627
- */
628
- export async function getActiveAdminTokens(
629
- db: Firestore,
630
- groupId: string,
631
- adminId: string,
632
- app: FirebaseApp
633
- ): Promise<AdminToken[]> {
634
- const group = await getClinicGroup(db, groupId);
635
- if (!group) {
636
- throw new Error("Clinic group not found");
637
- }
638
-
639
- // Proveravamo da li admin pripada grupi
640
- if (!group.admins.includes(adminId)) {
641
- throw new Error("Admin does not belong to this clinic group");
642
- }
643
-
644
- // Vraćamo samo aktivne tokene
645
- return group.adminTokens.filter((t) => t.status === AdminTokenStatus.ACTIVE);
646
- }
1
+ import {
2
+ collection,
3
+ doc,
4
+ getDoc,
5
+ getDocs,
6
+ query,
7
+ where,
8
+ updateDoc,
9
+ setDoc,
10
+ deleteDoc,
11
+ Timestamp,
12
+ Firestore,
13
+ serverTimestamp,
14
+ } from "firebase/firestore";
15
+ import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
16
+ import {
17
+ ClinicGroup,
18
+ CreateClinicGroupData,
19
+ CLINIC_GROUPS_COLLECTION,
20
+ AdminToken,
21
+ AdminTokenStatus,
22
+ CreateAdminTokenData,
23
+ SubscriptionModel,
24
+ } from "../../../types/clinic";
25
+ import { geohashForLocation } from "geofire-common";
26
+ import {
27
+ clinicGroupSchema,
28
+ createClinicGroupSchema,
29
+ } from "../../../validations/clinic.schema";
30
+ import { z } from "zod";
31
+ import { uploadPhoto } from "./photos.utils";
32
+ import { FirebaseApp } from "firebase/app";
33
+
34
+ /**
35
+ * Generates a unique ID for documents
36
+ * Format: xxxxxxxxxxxx-timestamp
37
+ * Where x is a random character (number or letter)
38
+ */
39
+ function generateId(): string {
40
+ const chars =
41
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
42
+ const timestamp = Date.now().toString(36);
43
+ const randomPart = Array.from({ length: 12 }, () =>
44
+ chars.charAt(Math.floor(Math.random() * chars.length))
45
+ ).join("");
46
+
47
+ return `${randomPart}-${timestamp}`;
48
+ }
49
+
50
+ /**
51
+ * Creates a new clinic group
52
+ * @param db - Firestore database instance
53
+ * @param data - Clinic group data
54
+ * @param ownerId - ID of the owner
55
+ * @param isDefault - Whether this is a default group
56
+ * @param clinicAdminService - Service for clinic admin operations
57
+ * @param app - Firebase app instance
58
+ * @returns The created clinic group
59
+ */
60
+ export async function createClinicGroup(
61
+ db: Firestore,
62
+ data: CreateClinicGroupData,
63
+ ownerId: string,
64
+ isDefault: boolean = false,
65
+ clinicAdminService: any,
66
+ app: FirebaseApp
67
+ ): Promise<ClinicGroup> {
68
+ console.log("[CLINIC_GROUP] Starting clinic group creation", {
69
+ ownerId,
70
+ isDefault,
71
+ });
72
+ console.log("[CLINIC_GROUP] Input data:", JSON.stringify(data, null, 2));
73
+
74
+ let validatedData: CreateClinicGroupData;
75
+ // Validacija podataka
76
+ try {
77
+ validatedData = createClinicGroupSchema.parse(data) as CreateClinicGroupData;
78
+ console.log("[CLINIC_GROUP] Data validation passed");
79
+ } catch (validationError) {
80
+ console.error("[CLINIC_GROUP] Data validation failed:", validationError);
81
+ throw validationError;
82
+ }
83
+
84
+ // Proveravamo da li owner postoji i da li je clinic admin
85
+ try {
86
+ console.log("[CLINIC_GROUP] Checking if owner exists", { ownerId });
87
+ // Skip owner verification for default groups since the admin profile doesn't exist yet
88
+ if (isDefault) {
89
+ console.log(
90
+ "[CLINIC_GROUP] Skipping owner verification for default group creation"
91
+ );
92
+ } else {
93
+ const owner = await clinicAdminService.getClinicAdmin(ownerId);
94
+ if (!owner) {
95
+ console.error(
96
+ "[CLINIC_GROUP] Owner not found or is not a clinic admin",
97
+ {
98
+ ownerId,
99
+ }
100
+ );
101
+ throw new Error("Owner not found or is not a clinic admin");
102
+ }
103
+ console.log("[CLINIC_GROUP] Owner verified as clinic admin");
104
+ }
105
+ } catch (ownerError) {
106
+ console.error("[CLINIC_GROUP] Error verifying owner:", ownerError);
107
+ throw ownerError;
108
+ }
109
+
110
+ // Generišemo geohash za lokaciju
111
+ console.log("[CLINIC_GROUP] Generating geohash for location");
112
+ if (validatedData.hqLocation) {
113
+ try {
114
+ validatedData.hqLocation.geohash = geohashForLocation([
115
+ validatedData.hqLocation.latitude,
116
+ validatedData.hqLocation.longitude,
117
+ ]);
118
+ console.log("[CLINIC_GROUP] Geohash generated successfully", {
119
+ geohash: validatedData.hqLocation.geohash,
120
+ });
121
+ } catch (geohashError) {
122
+ console.error("[CLINIC_GROUP] Error generating geohash:", geohashError);
123
+ throw geohashError;
124
+ }
125
+ }
126
+
127
+ const now = Timestamp.now();
128
+ console.log("[CLINIC_GROUP] Preparing clinic group data object");
129
+
130
+ // Generate a unique ID for the clinic group
131
+ const groupId = doc(collection(db, CLINIC_GROUPS_COLLECTION)).id;
132
+
133
+ // Log the logo value to debug null vs undefined issue
134
+ console.log("[CLINIC_GROUP] Logo value:", {
135
+ logoValue: validatedData.logo,
136
+ logoType: validatedData.logo === null ? "null" : typeof validatedData.logo,
137
+ });
138
+
139
+ // Handle logo upload if provided
140
+ let logoUrl = await uploadPhoto(
141
+ (validatedData.logo as string) || null,
142
+ "clinic-groups",
143
+ groupId,
144
+ "logo",
145
+ app
146
+ );
147
+ console.log("[CLINIC_GROUP] Logo processed", { logoUrl });
148
+
149
+ const groupData: ClinicGroup = {
150
+ ...validatedData,
151
+ id: groupId,
152
+ name: validatedData.name,
153
+ logo: logoUrl, // Use the uploaded logo URL or the original value
154
+ description: validatedData.description || "",
155
+ hqLocation: validatedData.hqLocation,
156
+ contactInfo: validatedData.contactInfo,
157
+ contactPerson: validatedData.contactPerson,
158
+ subscriptionModel:
159
+ validatedData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
160
+ clinics: [],
161
+ clinicsInfo: [],
162
+ admins: [ownerId],
163
+ adminsInfo: [],
164
+ adminTokens: [],
165
+ ownerId,
166
+ createdAt: now,
167
+ updatedAt: now,
168
+ isActive: true,
169
+ };
170
+
171
+ try {
172
+ // Validiramo kompletan objekat
173
+ console.log("[CLINIC_GROUP] Validating complete clinic group object");
174
+ try {
175
+ clinicGroupSchema.parse(groupData);
176
+ console.log("[CLINIC_GROUP] Clinic group validation passed");
177
+ } catch (schemaError) {
178
+ console.error(
179
+ "[CLINIC_GROUP] Clinic group validation failed:",
180
+ JSON.stringify(schemaError, null, 2)
181
+ );
182
+ throw schemaError;
183
+ }
184
+
185
+ // Čuvamo u Firestore
186
+ console.log("[CLINIC_GROUP] Saving clinic group to Firestore", {
187
+ groupId: groupData.id,
188
+ });
189
+ try {
190
+ await setDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
191
+ console.log("[CLINIC_GROUP] Clinic group saved successfully");
192
+ } catch (firestoreError) {
193
+ console.error(
194
+ "[CLINIC_GROUP] Error saving to Firestore:",
195
+ firestoreError
196
+ );
197
+ throw firestoreError;
198
+ }
199
+
200
+ // Ažuriramo clinic admin profil vlasnika
201
+ console.log("[CLINIC_GROUP] Updating clinic admin profile for owner", {
202
+ ownerId,
203
+ });
204
+ try {
205
+ await clinicAdminService.updateClinicAdmin(ownerId, {
206
+ clinicGroupId: groupData.id,
207
+ isGroupOwner: true,
208
+ });
209
+ console.log("[CLINIC_GROUP] Clinic admin profile updated successfully");
210
+ } catch (updateError) {
211
+ console.error(
212
+ "[CLINIC_GROUP] Error updating clinic admin profile:",
213
+ updateError
214
+ );
215
+ throw updateError;
216
+ }
217
+
218
+ console.log("[CLINIC_GROUP] Clinic group creation completed successfully", {
219
+ groupId: groupData.id,
220
+ groupName: groupData.name,
221
+ });
222
+ return groupData;
223
+ } catch (error) {
224
+ if (error instanceof z.ZodError) {
225
+ console.error(
226
+ "[CLINIC_GROUP] Zod validation error:",
227
+ JSON.stringify(error.errors, null, 2)
228
+ );
229
+ throw new Error("Invalid clinic group data: " + error.message);
230
+ }
231
+ console.error(
232
+ "[CLINIC_GROUP] Unhandled error in createClinicGroup:",
233
+ error
234
+ );
235
+ throw error;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Gets a clinic group by ID
241
+ * @param db - Firestore database instance
242
+ * @param groupId - ID of the clinic group
243
+ * @returns The clinic group or null if not found
244
+ */
245
+ export async function getClinicGroup(
246
+ db: Firestore,
247
+ groupId: string
248
+ ): Promise<ClinicGroup | null> {
249
+ const docRef = doc(db, CLINIC_GROUPS_COLLECTION, groupId);
250
+ const docSnap = await getDoc(docRef);
251
+
252
+ if (docSnap.exists()) {
253
+ return docSnap.data() as ClinicGroup;
254
+ }
255
+
256
+ return null;
257
+ }
258
+
259
+ /**
260
+ * Gets all active clinic groups
261
+ * @param db - Firestore database instance
262
+ * @returns Array of active clinic groups
263
+ */
264
+ export async function getAllActiveGroups(
265
+ db: Firestore
266
+ ): Promise<ClinicGroup[]> {
267
+ const q = query(
268
+ collection(db, CLINIC_GROUPS_COLLECTION),
269
+ where("isActive", "==", true)
270
+ );
271
+
272
+ const querySnapshot = await getDocs(q);
273
+ return querySnapshot.docs.map((doc) => doc.data() as ClinicGroup);
274
+ }
275
+
276
+ /**
277
+ * Updates a clinic group
278
+ * @param db - Firestore database instance
279
+ * @param groupId - ID of the clinic group
280
+ * @param data - Data to update
281
+ * @param app - Firebase app instance
282
+ * @returns The updated clinic group
283
+ */
284
+ export async function updateClinicGroup(
285
+ db: Firestore,
286
+ groupId: string,
287
+ data: Partial<ClinicGroup>,
288
+ app: FirebaseApp
289
+ ): Promise<ClinicGroup> {
290
+ console.log("[CLINIC_GROUP] Updating clinic group", { groupId });
291
+
292
+ const group = await getClinicGroup(db, groupId);
293
+ if (!group) {
294
+ console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
295
+ throw new Error("Clinic group not found");
296
+ }
297
+
298
+ // Process logo if it's a data URL
299
+ let updatedData = { ...data };
300
+
301
+ if (
302
+ data.logo &&
303
+ typeof data.logo === "string" &&
304
+ data.logo.startsWith("data:")
305
+ ) {
306
+ console.log("[CLINIC_GROUP] Processing logo for update");
307
+ try {
308
+ const logoUrl = await uploadPhoto(
309
+ data.logo,
310
+ "clinic-groups",
311
+ groupId,
312
+ "logo",
313
+ app
314
+ );
315
+ console.log("[CLINIC_GROUP] Logo processed for update", { logoUrl });
316
+
317
+ // Replace the data URL with the uploaded URL
318
+ updatedData.logo = logoUrl;
319
+ } catch (error) {
320
+ console.error("[CLINIC_GROUP] Error processing logo for update:", error);
321
+ // Continue with update even if logo upload fails
322
+ }
323
+ }
324
+
325
+ // Add timestamp
326
+ updatedData = {
327
+ ...updatedData,
328
+ updatedAt: Timestamp.now(),
329
+ };
330
+
331
+ console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
332
+ await updateDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
333
+ console.log("[CLINIC_GROUP] Clinic group updated successfully");
334
+
335
+ // Return updated data
336
+ const updatedGroup = await getClinicGroup(db, groupId);
337
+ if (!updatedGroup) {
338
+ console.error("[CLINIC_GROUP] Failed to retrieve updated clinic group");
339
+ throw new Error("Failed to retrieve updated clinic group");
340
+ }
341
+
342
+ return updatedGroup;
343
+ }
344
+
345
+ /**
346
+ * Adds an admin to a clinic group
347
+ * @param db - Firestore database instance
348
+ * @param groupId - ID of the clinic group
349
+ * @param adminId - ID of the admin to add (this is the admin document ID, not the user UID)
350
+ * @param app - Firebase app instance
351
+ */
352
+ export async function addAdminToGroup(
353
+ db: Firestore,
354
+ groupId: string,
355
+ adminId: string,
356
+ app: FirebaseApp
357
+ ): Promise<void> {
358
+ console.log("[CLINIC_GROUP] Adding admin to group", { groupId, adminId });
359
+
360
+ try {
361
+ const group = await getClinicGroup(db, groupId);
362
+ if (!group) {
363
+ console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
364
+ throw new Error("Clinic group not found");
365
+ }
366
+
367
+ if (group.admins.includes(adminId)) {
368
+ console.log("[CLINIC_GROUP] Admin is already in the group", {
369
+ adminId,
370
+ groupId,
371
+ });
372
+ return; // Admin is already in the group
373
+ }
374
+
375
+ console.log("[CLINIC_GROUP] Updating group with new admin");
376
+ await updateClinicGroup(
377
+ db,
378
+ groupId,
379
+ {
380
+ admins: [...group.admins, adminId],
381
+ },
382
+ app
383
+ );
384
+ console.log("[CLINIC_GROUP] Admin added to group successfully");
385
+ } catch (error) {
386
+ console.error("[CLINIC_GROUP] Error adding admin to group:", error);
387
+ throw error;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Removes an admin from a clinic group
393
+ * @param db - Firestore database instance
394
+ * @param groupId - ID of the clinic group
395
+ * @param adminId - ID of the admin to remove
396
+ * @param app - Firebase app instance
397
+ */
398
+ export async function removeAdminFromGroup(
399
+ db: Firestore,
400
+ groupId: string,
401
+ adminId: string,
402
+ app: FirebaseApp
403
+ ): Promise<void> {
404
+ const group = await getClinicGroup(db, groupId);
405
+ if (!group) {
406
+ throw new Error("Clinic group not found");
407
+ }
408
+
409
+ if (group.ownerId === adminId) {
410
+ throw new Error("Cannot remove the owner from the group");
411
+ }
412
+
413
+ if (!group.admins.includes(adminId)) {
414
+ return; // Admin is not in the group
415
+ }
416
+
417
+ await updateClinicGroup(
418
+ db,
419
+ groupId,
420
+ {
421
+ admins: group.admins.filter((id) => id !== adminId),
422
+ },
423
+ app
424
+ );
425
+ }
426
+
427
+ /**
428
+ * Deactivates a clinic group
429
+ * @param db - Firestore database instance
430
+ * @param groupId - ID of the clinic group
431
+ * @param app - Firebase app instance
432
+ */
433
+ export async function deactivateClinicGroup(
434
+ db: Firestore,
435
+ groupId: string,
436
+ app: FirebaseApp
437
+ ): Promise<void> {
438
+ const group = await getClinicGroup(db, groupId);
439
+ if (!group) {
440
+ throw new Error("Clinic group not found");
441
+ }
442
+
443
+ await updateClinicGroup(
444
+ db,
445
+ groupId,
446
+ {
447
+ isActive: false,
448
+ },
449
+ app
450
+ );
451
+ }
452
+
453
+ /**
454
+ * Creates an admin token for a clinic group
455
+ * @param db - Firestore database instance
456
+ * @param groupId - ID of the clinic group
457
+ * @param creatorAdminId - ID of the admin creating the token
458
+ * @param app - Firebase app instance
459
+ * @param data - Token data
460
+ * @returns The created admin token
461
+ */
462
+ export async function createAdminToken(
463
+ db: Firestore,
464
+ groupId: string,
465
+ creatorAdminId: string,
466
+ app: FirebaseApp,
467
+ data?: CreateAdminTokenData
468
+ ): Promise<AdminToken> {
469
+ const group = await getClinicGroup(db, groupId);
470
+ if (!group) {
471
+ throw new Error("Clinic group not found");
472
+ }
473
+
474
+ // Proveravamo da li admin pripada grupi
475
+ if (!group.admins.includes(creatorAdminId)) {
476
+ throw new Error("Admin does not belong to this clinic group");
477
+ }
478
+
479
+ const now = Timestamp.now();
480
+ const expiresInDays = data?.expiresInDays || 7; // Default 7 days
481
+ const email = data?.email || null;
482
+ const expiresAt = new Timestamp(
483
+ now.seconds + expiresInDays * 24 * 60 * 60,
484
+ now.nanoseconds
485
+ );
486
+
487
+ const token: AdminToken = {
488
+ id: generateId(),
489
+ token: generateId(),
490
+ status: AdminTokenStatus.ACTIVE,
491
+ email,
492
+ createdAt: now,
493
+ expiresAt,
494
+ };
495
+
496
+ // Dodajemo token u grupu
497
+ // Ovo treba promeniti, staviti admin tokene u sub-kolekciju u klinickoj grupi
498
+ await updateClinicGroup(
499
+ db,
500
+ groupId,
501
+ {
502
+ adminTokens: [...group.adminTokens, token],
503
+ },
504
+ app
505
+ );
506
+
507
+ return token;
508
+ }
509
+
510
+ /**
511
+ * Verifies and uses an admin token
512
+ * @param db - Firestore database instance
513
+ * @param groupId - ID of the clinic group
514
+ * @param token - Token to verify
515
+ * @param userRef - User reference
516
+ * @param app - Firebase app instance
517
+ * @returns Whether the token was successfully used
518
+ */
519
+ export async function verifyAndUseAdminToken(
520
+ db: Firestore,
521
+ groupId: string,
522
+ token: string,
523
+ userRef: string,
524
+ app: FirebaseApp
525
+ ): Promise<boolean> {
526
+ const group = await getClinicGroup(db, groupId);
527
+ if (!group) {
528
+ throw new Error("Clinic group not found");
529
+ }
530
+
531
+ const adminToken = group.adminTokens.find((t) => t.token === token);
532
+ if (!adminToken) {
533
+ throw new Error("Admin token not found");
534
+ }
535
+
536
+ if (adminToken.status !== AdminTokenStatus.ACTIVE) {
537
+ throw new Error("Admin token is not active");
538
+ }
539
+
540
+ const now = Timestamp.now();
541
+ if (adminToken.expiresAt.seconds < now.seconds) {
542
+ // Token je istekao, ažuriramo status
543
+ const updatedTokens = group.adminTokens.map((t) =>
544
+ t.id === adminToken.id ? { ...t, status: AdminTokenStatus.EXPIRED } : t
545
+ );
546
+
547
+ await updateClinicGroup(
548
+ db,
549
+ groupId,
550
+ {
551
+ adminTokens: updatedTokens,
552
+ },
553
+ app
554
+ );
555
+
556
+ throw new Error("Admin token has expired");
557
+ }
558
+
559
+ // Token je validan, ažuriramo status
560
+ const updatedTokens = group.adminTokens.map((t) =>
561
+ t.id === adminToken.id
562
+ ? {
563
+ ...t,
564
+ status: AdminTokenStatus.USED,
565
+ usedByUserRef: userRef,
566
+ }
567
+ : t
568
+ );
569
+
570
+ await updateClinicGroup(
571
+ db,
572
+ groupId,
573
+ {
574
+ adminTokens: updatedTokens,
575
+ },
576
+ app
577
+ );
578
+
579
+ return true;
580
+ }
581
+
582
+ /**
583
+ * Deletes an admin token
584
+ * @param db - Firestore database instance
585
+ * @param groupId - ID of the clinic group
586
+ * @param tokenId - ID of the token to delete
587
+ * @param adminId - ID of the admin making the deletion
588
+ * @param app - Firebase app instance
589
+ */
590
+ export async function deleteAdminToken(
591
+ db: Firestore,
592
+ groupId: string,
593
+ tokenId: string,
594
+ adminId: string,
595
+ app: FirebaseApp
596
+ ): Promise<void> {
597
+ const group = await getClinicGroup(db, groupId);
598
+ if (!group) {
599
+ throw new Error("Clinic group not found");
600
+ }
601
+
602
+ // Proveravamo da li admin pripada grupi
603
+ if (!group.admins.includes(adminId)) {
604
+ throw new Error("Admin does not belong to this clinic group");
605
+ }
606
+
607
+ // Uklanjamo token
608
+ const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
609
+
610
+ await updateClinicGroup(
611
+ db,
612
+ groupId,
613
+ {
614
+ adminTokens: updatedTokens,
615
+ },
616
+ app
617
+ );
618
+ }
619
+
620
+ /**
621
+ * Gets active admin tokens for a clinic group
622
+ * @param db - Firestore database instance
623
+ * @param groupId - ID of the clinic group
624
+ * @param adminId - ID of the admin requesting the tokens
625
+ * @param app - Firebase app instance (not used but included for consistency)
626
+ * @returns Array of active admin tokens
627
+ */
628
+ export async function getActiveAdminTokens(
629
+ db: Firestore,
630
+ groupId: string,
631
+ adminId: string,
632
+ app: FirebaseApp
633
+ ): Promise<AdminToken[]> {
634
+ const group = await getClinicGroup(db, groupId);
635
+ if (!group) {
636
+ throw new Error("Clinic group not found");
637
+ }
638
+
639
+ // Proveravamo da li admin pripada grupi
640
+ if (!group.admins.includes(adminId)) {
641
+ throw new Error("Admin does not belong to this clinic group");
642
+ }
643
+
644
+ // Vraćamo samo aktivne tokene
645
+ return group.adminTokens.filter((t) => t.status === AdminTokenStatus.ACTIVE);
646
+ }