@blackcode_sa/metaestetics-api 1.12.65 → 1.12.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/dist/admin/index.d.mts +2 -0
  2. package/dist/admin/index.d.ts +2 -0
  3. package/dist/admin/index.js +45 -4
  4. package/dist/admin/index.mjs +45 -4
  5. package/dist/backoffice/index.d.mts +33 -0
  6. package/dist/backoffice/index.d.ts +33 -0
  7. package/dist/backoffice/index.js +63 -0
  8. package/dist/backoffice/index.mjs +63 -0
  9. package/dist/index.d.mts +35 -0
  10. package/dist/index.d.ts +35 -0
  11. package/dist/index.js +116 -11
  12. package/dist/index.mjs +116 -11
  13. package/package.json +119 -119
  14. package/src/__mocks__/firstore.ts +10 -10
  15. package/src/admin/aggregation/README.md +79 -79
  16. package/src/admin/aggregation/appointment/README.md +128 -128
  17. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
  18. package/src/admin/aggregation/appointment/index.ts +1 -1
  19. package/src/admin/aggregation/clinic/README.md +52 -52
  20. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  21. package/src/admin/aggregation/clinic/index.ts +1 -1
  22. package/src/admin/aggregation/forms/README.md +13 -13
  23. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  24. package/src/admin/aggregation/forms/index.ts +1 -1
  25. package/src/admin/aggregation/index.ts +8 -8
  26. package/src/admin/aggregation/patient/README.md +27 -27
  27. package/src/admin/aggregation/patient/index.ts +1 -1
  28. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  29. package/src/admin/aggregation/practitioner/README.md +42 -42
  30. package/src/admin/aggregation/practitioner/index.ts +1 -1
  31. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  32. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  33. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  34. package/src/admin/aggregation/procedure/README.md +43 -43
  35. package/src/admin/aggregation/procedure/index.ts +1 -1
  36. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  37. package/src/admin/aggregation/reviews/index.ts +1 -1
  38. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
  39. package/src/admin/booking/README.md +125 -125
  40. package/src/admin/booking/booking.admin.ts +1037 -1037
  41. package/src/admin/booking/booking.calculator.ts +712 -712
  42. package/src/admin/booking/booking.types.ts +59 -59
  43. package/src/admin/booking/index.ts +3 -3
  44. package/src/admin/booking/timezones-problem.md +185 -185
  45. package/src/admin/calendar/README.md +7 -7
  46. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  47. package/src/admin/calendar/index.ts +1 -1
  48. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  49. package/src/admin/documentation-templates/index.ts +1 -1
  50. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  51. package/src/admin/free-consultation/index.ts +1 -1
  52. package/src/admin/index.ts +75 -75
  53. package/src/admin/logger/index.ts +78 -78
  54. package/src/admin/mailing/README.md +95 -95
  55. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  56. package/src/admin/mailing/appointment/index.ts +1 -1
  57. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  58. package/src/admin/mailing/base.mailing.service.ts +208 -208
  59. package/src/admin/mailing/index.ts +3 -3
  60. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  61. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  62. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  63. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  64. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  65. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  66. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  67. package/src/admin/notifications/index.ts +1 -1
  68. package/src/admin/notifications/notifications.admin.ts +710 -710
  69. package/src/admin/requirements/README.md +128 -128
  70. package/src/admin/requirements/index.ts +1 -1
  71. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  72. package/src/admin/users/index.ts +1 -1
  73. package/src/admin/users/user-profile.admin.ts +405 -405
  74. package/src/backoffice/constants/certification.constants.ts +13 -13
  75. package/src/backoffice/constants/index.ts +1 -1
  76. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  77. package/src/backoffice/errors/index.ts +1 -1
  78. package/src/backoffice/expo-safe/README.md +26 -26
  79. package/src/backoffice/expo-safe/index.ts +41 -41
  80. package/src/backoffice/index.ts +5 -5
  81. package/src/backoffice/services/FIXES_README.md +102 -102
  82. package/src/backoffice/services/README.md +40 -40
  83. package/src/backoffice/services/brand.service.ts +256 -256
  84. package/src/backoffice/services/category.service.ts +341 -318
  85. package/src/backoffice/services/constants.service.ts +385 -385
  86. package/src/backoffice/services/documentation-template.service.ts +202 -202
  87. package/src/backoffice/services/index.ts +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 +417 -395
  92. package/src/backoffice/services/technology.service.ts +1104 -1083
  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 -62
  97. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  98. package/src/backoffice/types/index.ts +10 -10
  99. package/src/backoffice/types/procedure-product.types.ts +38 -38
  100. package/src/backoffice/types/product.types.ts +240 -240
  101. package/src/backoffice/types/requirement.types.ts +63 -63
  102. package/src/backoffice/types/static/README.md +18 -18
  103. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  104. package/src/backoffice/types/static/certification.types.ts +37 -37
  105. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  106. package/src/backoffice/types/static/index.ts +6 -6
  107. package/src/backoffice/types/static/pricing.types.ts +16 -16
  108. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  109. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  110. package/src/backoffice/types/subcategory.types.ts +34 -34
  111. package/src/backoffice/types/technology.types.ts +168 -163
  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 +200 -200
  119. package/src/errors/clinic.errors.ts +32 -32
  120. package/src/errors/firebase.errors.ts +47 -47
  121. package/src/errors/user.errors.ts +99 -99
  122. package/src/index.backup.ts +407 -407
  123. package/src/index.ts +6 -6
  124. package/src/locales/en.ts +31 -31
  125. package/src/recommender/admin/index.ts +1 -1
  126. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  127. package/src/recommender/front/index.ts +1 -1
  128. package/src/recommender/front/services/onboarding.service.ts +5 -5
  129. package/src/recommender/front/services/recommender.service.ts +3 -3
  130. package/src/recommender/index.ts +1 -1
  131. package/src/services/PATIENTAUTH.MD +197 -197
  132. package/src/services/README.md +106 -106
  133. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  134. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  135. package/src/services/__tests__/auth.service.test.ts +346 -346
  136. package/src/services/__tests__/base.service.test.ts +77 -77
  137. package/src/services/__tests__/user.service.test.ts +528 -528
  138. package/src/services/appointment/README.md +17 -17
  139. package/src/services/appointment/appointment.service.ts +2505 -2505
  140. package/src/services/appointment/index.ts +1 -1
  141. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  142. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  143. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  144. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  145. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  146. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  147. package/src/services/auth/auth.service.ts +989 -989
  148. package/src/services/auth/auth.v2.service.ts +961 -961
  149. package/src/services/auth/index.ts +7 -7
  150. package/src/services/auth/utils/error.utils.ts +90 -90
  151. package/src/services/auth/utils/firebase.utils.ts +49 -49
  152. package/src/services/auth/utils/index.ts +21 -21
  153. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  154. package/src/services/base.service.ts +41 -41
  155. package/src/services/calendar/calendar.service.ts +1077 -1077
  156. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  157. package/src/services/calendar/calendar.v3.service.ts +313 -313
  158. package/src/services/calendar/externalCalendar.service.ts +178 -178
  159. package/src/services/calendar/index.ts +5 -5
  160. package/src/services/calendar/synced-calendars.service.ts +743 -743
  161. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  162. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  163. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  164. package/src/services/calendar/utils/docs.utils.ts +157 -157
  165. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  166. package/src/services/calendar/utils/index.ts +8 -8
  167. package/src/services/calendar/utils/patient.utils.ts +198 -198
  168. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  169. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  170. package/src/services/clinic/README.md +204 -204
  171. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  172. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  173. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  174. package/src/services/clinic/billing-transactions.service.ts +217 -217
  175. package/src/services/clinic/clinic-admin.service.ts +202 -202
  176. package/src/services/clinic/clinic-group.service.ts +310 -310
  177. package/src/services/clinic/clinic.service.ts +708 -708
  178. package/src/services/clinic/index.ts +5 -5
  179. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  180. package/src/services/clinic/utils/admin.utils.ts +551 -551
  181. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  182. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  183. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  184. package/src/services/clinic/utils/filter.utils.ts +446 -446
  185. package/src/services/clinic/utils/index.ts +11 -11
  186. package/src/services/clinic/utils/photos.utils.ts +188 -188
  187. package/src/services/clinic/utils/search.utils.ts +84 -84
  188. package/src/services/clinic/utils/tag.utils.ts +124 -124
  189. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  190. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  191. package/src/services/documentation-templates/index.ts +2 -2
  192. package/src/services/index.ts +13 -13
  193. package/src/services/media/index.ts +1 -1
  194. package/src/services/media/media.service.ts +418 -418
  195. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  196. package/src/services/notifications/index.ts +1 -1
  197. package/src/services/notifications/notification.service.ts +215 -215
  198. package/src/services/patient/README.md +48 -48
  199. package/src/services/patient/To-Do.md +43 -43
  200. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  201. package/src/services/patient/index.ts +2 -2
  202. package/src/services/patient/patient.service.ts +883 -883
  203. package/src/services/patient/patientRequirements.service.ts +285 -285
  204. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  205. package/src/services/patient/utils/clinic.utils.ts +80 -80
  206. package/src/services/patient/utils/docs.utils.ts +142 -142
  207. package/src/services/patient/utils/index.ts +9 -9
  208. package/src/services/patient/utils/location.utils.ts +126 -126
  209. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  210. package/src/services/patient/utils/medical.utils.ts +458 -458
  211. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  212. package/src/services/patient/utils/profile.utils.ts +510 -510
  213. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  214. package/src/services/patient/utils/token.utils.ts +211 -211
  215. package/src/services/practitioner/README.md +145 -145
  216. package/src/services/practitioner/index.ts +1 -1
  217. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  218. package/src/services/procedure/README.md +163 -163
  219. package/src/services/procedure/index.ts +1 -1
  220. package/src/services/procedure/procedure.service.ts +1715 -1715
  221. package/src/services/reviews/index.ts +1 -1
  222. package/src/services/reviews/reviews.service.ts +683 -636
  223. package/src/services/user/index.ts +1 -1
  224. package/src/services/user/user.service.ts +489 -489
  225. package/src/services/user/user.v2.service.ts +466 -466
  226. package/src/types/appointment/index.ts +480 -480
  227. package/src/types/calendar/index.ts +258 -258
  228. package/src/types/calendar/synced-calendar.types.ts +66 -66
  229. package/src/types/clinic/index.ts +489 -489
  230. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  231. package/src/types/clinic/preferences.types.ts +159 -159
  232. package/src/types/clinic/to-do +3 -3
  233. package/src/types/documentation-templates/index.ts +308 -308
  234. package/src/types/index.ts +44 -44
  235. package/src/types/notifications/README.md +77 -77
  236. package/src/types/notifications/index.ts +265 -265
  237. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  238. package/src/types/patient/allergies.ts +58 -58
  239. package/src/types/patient/index.ts +275 -275
  240. package/src/types/patient/medical-info.types.ts +152 -152
  241. package/src/types/patient/patient-requirements.ts +92 -92
  242. package/src/types/patient/token.types.ts +61 -61
  243. package/src/types/practitioner/index.ts +206 -206
  244. package/src/types/procedure/index.ts +181 -181
  245. package/src/types/profile/index.ts +39 -39
  246. package/src/types/reviews/index.ts +132 -130
  247. package/src/types/tz-lookup.d.ts +4 -4
  248. package/src/types/user/index.ts +38 -38
  249. package/src/utils/TIMESTAMPS.md +176 -176
  250. package/src/utils/TimestampUtils.ts +241 -241
  251. package/src/utils/index.ts +1 -1
  252. package/src/validations/appointment.schema.ts +574 -574
  253. package/src/validations/calendar.schema.ts +225 -225
  254. package/src/validations/clinic.schema.ts +493 -493
  255. package/src/validations/common.schema.ts +25 -25
  256. package/src/validations/documentation-templates/index.ts +1 -1
  257. package/src/validations/documentation-templates/template.schema.ts +220 -220
  258. package/src/validations/documentation-templates.schema.ts +10 -10
  259. package/src/validations/index.ts +20 -20
  260. package/src/validations/media.schema.ts +10 -10
  261. package/src/validations/notification.schema.ts +90 -90
  262. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  263. package/src/validations/patient/medical-info.schema.ts +125 -125
  264. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  265. package/src/validations/patient/token.schema.ts +29 -29
  266. package/src/validations/patient.schema.ts +217 -217
  267. package/src/validations/practitioner.schema.ts +222 -222
  268. package/src/validations/procedure-product.schema.ts +41 -41
  269. package/src/validations/procedure.schema.ts +124 -124
  270. package/src/validations/profile-info.schema.ts +41 -41
  271. package/src/validations/reviews.schema.ts +195 -189
  272. package/src/validations/schemas.ts +104 -104
  273. 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
+ }