@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8

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 (295) hide show
  1. package/dist/admin/index.d.mts +20 -1
  2. package/dist/admin/index.d.ts +20 -1
  3. package/dist/admin/index.js +217 -1
  4. package/dist/admin/index.mjs +217 -1
  5. package/dist/index.d.mts +26 -3
  6. package/dist/index.d.ts +26 -3
  7. package/dist/index.js +168 -6
  8. package/dist/index.mjs +168 -6
  9. package/package.json +121 -121
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  35. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  36. package/src/admin/analytics/index.ts +2 -2
  37. package/src/admin/booking/README.md +125 -125
  38. package/src/admin/booking/booking.admin.ts +1037 -1037
  39. package/src/admin/booking/booking.calculator.ts +712 -712
  40. package/src/admin/booking/booking.types.ts +59 -59
  41. package/src/admin/booking/index.ts +3 -3
  42. package/src/admin/booking/timezones-problem.md +185 -185
  43. package/src/admin/calendar/README.md +7 -7
  44. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  45. package/src/admin/calendar/index.ts +1 -1
  46. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  47. package/src/admin/documentation-templates/index.ts +1 -1
  48. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  49. package/src/admin/free-consultation/index.ts +1 -1
  50. package/src/admin/index.ts +81 -81
  51. package/src/admin/logger/index.ts +78 -78
  52. package/src/admin/mailing/README.md +95 -95
  53. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  54. package/src/admin/mailing/appointment/index.ts +1 -1
  55. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  56. package/src/admin/mailing/base.mailing.service.ts +208 -208
  57. package/src/admin/mailing/index.ts +3 -3
  58. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  59. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  60. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  61. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  62. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  63. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  64. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  65. package/src/admin/notifications/index.ts +1 -1
  66. package/src/admin/notifications/notifications.admin.ts +710 -710
  67. package/src/admin/requirements/README.md +128 -128
  68. package/src/admin/requirements/index.ts +1 -1
  69. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  70. package/src/admin/users/index.ts +1 -1
  71. package/src/admin/users/user-profile.admin.ts +405 -405
  72. package/src/backoffice/constants/certification.constants.ts +13 -13
  73. package/src/backoffice/constants/index.ts +1 -1
  74. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  75. package/src/backoffice/errors/index.ts +1 -1
  76. package/src/backoffice/expo-safe/README.md +26 -26
  77. package/src/backoffice/expo-safe/index.ts +41 -41
  78. package/src/backoffice/index.ts +5 -5
  79. package/src/backoffice/services/FIXES_README.md +102 -102
  80. package/src/backoffice/services/README.md +57 -57
  81. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  82. package/src/backoffice/services/analytics.service.summary.md +143 -143
  83. package/src/backoffice/services/brand.service.ts +256 -256
  84. package/src/backoffice/services/category.service.ts +384 -384
  85. package/src/backoffice/services/constants.service.ts +385 -385
  86. package/src/backoffice/services/documentation-template.service.ts +202 -202
  87. package/src/backoffice/services/index.ts +10 -10
  88. package/src/backoffice/services/migrate-products.ts +116 -116
  89. package/src/backoffice/services/product.service.ts +553 -553
  90. package/src/backoffice/services/requirement.service.ts +235 -235
  91. package/src/backoffice/services/subcategory.service.ts +461 -461
  92. package/src/backoffice/services/technology.service.ts +1151 -1151
  93. package/src/backoffice/types/README.md +12 -12
  94. package/src/backoffice/types/admin-constants.types.ts +69 -69
  95. package/src/backoffice/types/brand.types.ts +29 -29
  96. package/src/backoffice/types/category.types.ts +67 -67
  97. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  98. package/src/backoffice/types/index.ts +10 -10
  99. package/src/backoffice/types/procedure-product.types.ts +38 -38
  100. package/src/backoffice/types/product.types.ts +240 -240
  101. package/src/backoffice/types/requirement.types.ts +63 -63
  102. package/src/backoffice/types/static/README.md +18 -18
  103. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  104. package/src/backoffice/types/static/certification.types.ts +37 -37
  105. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  106. package/src/backoffice/types/static/index.ts +6 -6
  107. package/src/backoffice/types/static/pricing.types.ts +16 -16
  108. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  109. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  110. package/src/backoffice/types/subcategory.types.ts +34 -34
  111. package/src/backoffice/types/technology.types.ts +168 -168
  112. package/src/backoffice/validations/index.ts +1 -1
  113. package/src/backoffice/validations/schemas.ts +164 -164
  114. package/src/config/__mocks__/firebase.ts +99 -99
  115. package/src/config/firebase.ts +78 -78
  116. package/src/config/index.ts +9 -9
  117. package/src/errors/auth.error.ts +6 -6
  118. package/src/errors/auth.errors.ts +211 -200
  119. package/src/errors/clinic.errors.ts +32 -32
  120. package/src/errors/firebase.errors.ts +47 -47
  121. package/src/errors/user.errors.ts +99 -99
  122. package/src/index.backup.ts +407 -407
  123. package/src/index.ts +6 -6
  124. package/src/locales/en.ts +31 -31
  125. package/src/recommender/admin/index.ts +1 -1
  126. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  127. package/src/recommender/front/index.ts +1 -1
  128. package/src/recommender/front/services/onboarding.service.ts +5 -5
  129. package/src/recommender/front/services/recommender.service.ts +3 -3
  130. package/src/recommender/index.ts +1 -1
  131. package/src/services/PATIENTAUTH.MD +197 -197
  132. package/src/services/README.md +106 -106
  133. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  134. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  135. package/src/services/__tests__/auth.service.test.ts +346 -346
  136. package/src/services/__tests__/base.service.test.ts +77 -77
  137. package/src/services/__tests__/user.service.test.ts +528 -528
  138. package/src/services/analytics/ARCHITECTURE.md +199 -199
  139. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  140. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  141. package/src/services/analytics/QUICK_START.md +393 -393
  142. package/src/services/analytics/README.md +304 -304
  143. package/src/services/analytics/SUMMARY.md +141 -141
  144. package/src/services/analytics/TRENDS.md +380 -380
  145. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  146. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  147. package/src/services/analytics/analytics.service.ts +2142 -2142
  148. package/src/services/analytics/index.ts +4 -4
  149. package/src/services/analytics/review-analytics.service.ts +941 -941
  150. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  151. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  152. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  153. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  154. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  155. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  156. package/src/services/appointment/README.md +17 -17
  157. package/src/services/appointment/appointment.service.ts +2558 -2558
  158. package/src/services/appointment/index.ts +1 -1
  159. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  160. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  161. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  162. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  163. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  164. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  165. package/src/services/auth/auth.service.ts +1043 -989
  166. package/src/services/auth/auth.v2.service.ts +961 -961
  167. package/src/services/auth/index.ts +7 -7
  168. package/src/services/auth/utils/error.utils.ts +90 -90
  169. package/src/services/auth/utils/firebase.utils.ts +49 -49
  170. package/src/services/auth/utils/index.ts +21 -21
  171. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  172. package/src/services/base.service.ts +41 -41
  173. package/src/services/calendar/calendar.service.ts +1077 -1077
  174. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  175. package/src/services/calendar/calendar.v3.service.ts +313 -313
  176. package/src/services/calendar/externalCalendar.service.ts +178 -178
  177. package/src/services/calendar/index.ts +5 -5
  178. package/src/services/calendar/synced-calendars.service.ts +743 -743
  179. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  180. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  181. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  182. package/src/services/calendar/utils/docs.utils.ts +157 -157
  183. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  184. package/src/services/calendar/utils/index.ts +8 -8
  185. package/src/services/calendar/utils/patient.utils.ts +198 -198
  186. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  187. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  188. package/src/services/clinic/README.md +204 -204
  189. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  190. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  191. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  192. package/src/services/clinic/billing-transactions.service.ts +217 -217
  193. package/src/services/clinic/clinic-admin.service.ts +202 -202
  194. package/src/services/clinic/clinic-group.service.ts +310 -310
  195. package/src/services/clinic/clinic.service.ts +708 -708
  196. package/src/services/clinic/index.ts +5 -5
  197. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  198. package/src/services/clinic/utils/admin.utils.ts +551 -551
  199. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  200. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  201. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  202. package/src/services/clinic/utils/filter.utils.ts +446 -446
  203. package/src/services/clinic/utils/index.ts +11 -11
  204. package/src/services/clinic/utils/photos.utils.ts +188 -188
  205. package/src/services/clinic/utils/search.utils.ts +84 -84
  206. package/src/services/clinic/utils/tag.utils.ts +124 -124
  207. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  208. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  209. package/src/services/documentation-templates/index.ts +2 -2
  210. package/src/services/index.ts +14 -14
  211. package/src/services/media/index.ts +1 -1
  212. package/src/services/media/media.service.ts +418 -418
  213. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  214. package/src/services/notifications/index.ts +1 -1
  215. package/src/services/notifications/notification.service.ts +215 -215
  216. package/src/services/patient/README.md +48 -48
  217. package/src/services/patient/To-Do.md +43 -43
  218. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  219. package/src/services/patient/index.ts +2 -2
  220. package/src/services/patient/patient.service.ts +883 -883
  221. package/src/services/patient/patientRequirements.service.ts +285 -285
  222. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  223. package/src/services/patient/utils/clinic.utils.ts +80 -80
  224. package/src/services/patient/utils/docs.utils.ts +142 -142
  225. package/src/services/patient/utils/index.ts +9 -9
  226. package/src/services/patient/utils/location.utils.ts +126 -126
  227. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  228. package/src/services/patient/utils/medical.utils.ts +458 -458
  229. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  230. package/src/services/patient/utils/profile.utils.ts +510 -510
  231. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  232. package/src/services/patient/utils/token.utils.ts +211 -211
  233. package/src/services/practitioner/README.md +145 -145
  234. package/src/services/practitioner/index.ts +1 -1
  235. package/src/services/practitioner/practitioner.service.ts +1799 -1742
  236. package/src/services/procedure/README.md +163 -163
  237. package/src/services/procedure/index.ts +1 -1
  238. package/src/services/procedure/procedure.service.ts +2307 -2200
  239. package/src/services/reviews/index.ts +1 -1
  240. package/src/services/reviews/reviews.service.ts +734 -734
  241. package/src/services/user/index.ts +1 -1
  242. package/src/services/user/user.service.ts +489 -489
  243. package/src/services/user/user.v2.service.ts +466 -466
  244. package/src/types/analytics/analytics.types.ts +597 -597
  245. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  246. package/src/types/analytics/index.ts +4 -4
  247. package/src/types/analytics/stored-analytics.types.ts +137 -137
  248. package/src/types/appointment/index.ts +480 -480
  249. package/src/types/calendar/index.ts +258 -258
  250. package/src/types/calendar/synced-calendar.types.ts +66 -66
  251. package/src/types/clinic/index.ts +498 -498
  252. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  253. package/src/types/clinic/preferences.types.ts +159 -159
  254. package/src/types/clinic/to-do +3 -3
  255. package/src/types/documentation-templates/index.ts +308 -308
  256. package/src/types/index.ts +47 -47
  257. package/src/types/notifications/README.md +77 -77
  258. package/src/types/notifications/index.ts +286 -286
  259. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  260. package/src/types/patient/allergies.ts +58 -58
  261. package/src/types/patient/index.ts +275 -275
  262. package/src/types/patient/medical-info.types.ts +152 -152
  263. package/src/types/patient/patient-requirements.ts +92 -92
  264. package/src/types/patient/token.types.ts +61 -61
  265. package/src/types/practitioner/index.ts +206 -206
  266. package/src/types/procedure/index.ts +181 -181
  267. package/src/types/profile/index.ts +39 -39
  268. package/src/types/reviews/index.ts +132 -132
  269. package/src/types/tz-lookup.d.ts +4 -4
  270. package/src/types/user/index.ts +38 -38
  271. package/src/utils/TIMESTAMPS.md +176 -176
  272. package/src/utils/TimestampUtils.ts +241 -241
  273. package/src/utils/index.ts +1 -1
  274. package/src/validations/appointment.schema.ts +574 -574
  275. package/src/validations/calendar.schema.ts +225 -225
  276. package/src/validations/clinic.schema.ts +494 -494
  277. package/src/validations/common.schema.ts +25 -25
  278. package/src/validations/documentation-templates/index.ts +1 -1
  279. package/src/validations/documentation-templates/template.schema.ts +220 -220
  280. package/src/validations/documentation-templates.schema.ts +10 -10
  281. package/src/validations/index.ts +20 -20
  282. package/src/validations/media.schema.ts +10 -10
  283. package/src/validations/notification.schema.ts +90 -90
  284. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  285. package/src/validations/patient/medical-info.schema.ts +125 -125
  286. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  287. package/src/validations/patient/token.schema.ts +29 -29
  288. package/src/validations/patient.schema.ts +217 -217
  289. package/src/validations/practitioner.schema.ts +222 -222
  290. package/src/validations/procedure-product.schema.ts +41 -41
  291. package/src/validations/procedure.schema.ts +124 -124
  292. package/src/validations/profile-info.schema.ts +41 -41
  293. package/src/validations/reviews.schema.ts +195 -195
  294. package/src/validations/schemas.ts +104 -104
  295. 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);
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 || 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);
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 || 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
+ }