@blackcode_sa/metaestetics-api 1.13.5 → 1.13.6

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