@blackcode_sa/metaestetics-api 1.12.61 → 1.12.62

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 (269) 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/index.d.mts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +53 -11
  8. package/dist/index.mjs +53 -11
  9. package/package.json +119 -119
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
  35. package/src/admin/booking/README.md +125 -125
  36. package/src/admin/booking/booking.admin.ts +1037 -1037
  37. package/src/admin/booking/booking.calculator.ts +712 -712
  38. package/src/admin/booking/booking.types.ts +59 -59
  39. package/src/admin/booking/index.ts +3 -3
  40. package/src/admin/booking/timezones-problem.md +185 -185
  41. package/src/admin/calendar/README.md +7 -7
  42. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  43. package/src/admin/calendar/index.ts +1 -1
  44. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  45. package/src/admin/documentation-templates/index.ts +1 -1
  46. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  47. package/src/admin/free-consultation/index.ts +1 -1
  48. package/src/admin/index.ts +75 -75
  49. package/src/admin/logger/index.ts +78 -78
  50. package/src/admin/mailing/README.md +95 -95
  51. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  52. package/src/admin/mailing/appointment/index.ts +1 -1
  53. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  54. package/src/admin/mailing/base.mailing.service.ts +208 -208
  55. package/src/admin/mailing/index.ts +3 -3
  56. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  57. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  58. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  59. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  60. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  61. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  62. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  63. package/src/admin/notifications/index.ts +1 -1
  64. package/src/admin/notifications/notifications.admin.ts +710 -710
  65. package/src/admin/requirements/README.md +128 -128
  66. package/src/admin/requirements/index.ts +1 -1
  67. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  68. package/src/admin/users/index.ts +1 -1
  69. package/src/admin/users/user-profile.admin.ts +405 -405
  70. package/src/backoffice/constants/certification.constants.ts +13 -13
  71. package/src/backoffice/constants/index.ts +1 -1
  72. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  73. package/src/backoffice/errors/index.ts +1 -1
  74. package/src/backoffice/expo-safe/README.md +26 -26
  75. package/src/backoffice/expo-safe/index.ts +41 -41
  76. package/src/backoffice/index.ts +5 -5
  77. package/src/backoffice/services/FIXES_README.md +102 -102
  78. package/src/backoffice/services/README.md +40 -40
  79. package/src/backoffice/services/brand.service.ts +256 -256
  80. package/src/backoffice/services/category.service.ts +318 -318
  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 +8 -8
  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 +395 -395
  88. package/src/backoffice/services/technology.service.ts +1070 -1070
  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 +62 -62
  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 +161 -161
  108. package/src/backoffice/validations/index.ts +1 -1
  109. package/src/backoffice/validations/schemas.ts +163 -163
  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/appointment/README.md +17 -17
  135. package/src/services/appointment/appointment.service.ts +2082 -2082
  136. package/src/services/appointment/index.ts +1 -1
  137. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  138. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  139. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  140. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  141. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  142. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  143. package/src/services/auth/auth.service.ts +989 -989
  144. package/src/services/auth/auth.v2.service.ts +961 -961
  145. package/src/services/auth/index.ts +7 -7
  146. package/src/services/auth/utils/error.utils.ts +90 -90
  147. package/src/services/auth/utils/firebase.utils.ts +49 -49
  148. package/src/services/auth/utils/index.ts +21 -21
  149. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  150. package/src/services/base.service.ts +41 -41
  151. package/src/services/calendar/calendar.service.ts +1077 -1077
  152. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  153. package/src/services/calendar/calendar.v3.service.ts +313 -313
  154. package/src/services/calendar/externalCalendar.service.ts +178 -178
  155. package/src/services/calendar/index.ts +5 -5
  156. package/src/services/calendar/synced-calendars.service.ts +743 -743
  157. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  158. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  159. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  160. package/src/services/calendar/utils/docs.utils.ts +157 -157
  161. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  162. package/src/services/calendar/utils/index.ts +8 -8
  163. package/src/services/calendar/utils/patient.utils.ts +198 -198
  164. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  165. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  166. package/src/services/clinic/README.md +204 -204
  167. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  168. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  169. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  170. package/src/services/clinic/billing-transactions.service.ts +217 -217
  171. package/src/services/clinic/clinic-admin.service.ts +202 -202
  172. package/src/services/clinic/clinic-group.service.ts +310 -310
  173. package/src/services/clinic/clinic.service.ts +708 -708
  174. package/src/services/clinic/index.ts +5 -5
  175. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  176. package/src/services/clinic/utils/admin.utils.ts +551 -551
  177. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  178. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  179. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  180. package/src/services/clinic/utils/filter.utils.ts +446 -446
  181. package/src/services/clinic/utils/index.ts +11 -11
  182. package/src/services/clinic/utils/photos.utils.ts +188 -188
  183. package/src/services/clinic/utils/search.utils.ts +84 -84
  184. package/src/services/clinic/utils/tag.utils.ts +124 -124
  185. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  186. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  187. package/src/services/documentation-templates/index.ts +2 -2
  188. package/src/services/index.ts +13 -13
  189. package/src/services/media/index.ts +1 -1
  190. package/src/services/media/media.service.ts +418 -418
  191. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  192. package/src/services/notifications/index.ts +1 -1
  193. package/src/services/notifications/notification.service.ts +215 -215
  194. package/src/services/patient/README.md +48 -48
  195. package/src/services/patient/To-Do.md +43 -43
  196. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  197. package/src/services/patient/index.ts +2 -2
  198. package/src/services/patient/patient.service.ts +883 -883
  199. package/src/services/patient/patientRequirements.service.ts +285 -285
  200. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  201. package/src/services/patient/utils/clinic.utils.ts +80 -80
  202. package/src/services/patient/utils/docs.utils.ts +142 -142
  203. package/src/services/patient/utils/index.ts +9 -9
  204. package/src/services/patient/utils/location.utils.ts +126 -126
  205. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  206. package/src/services/patient/utils/medical.utils.ts +458 -458
  207. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  208. package/src/services/patient/utils/profile.utils.ts +510 -510
  209. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  210. package/src/services/patient/utils/token.utils.ts +211 -211
  211. package/src/services/practitioner/README.md +145 -145
  212. package/src/services/practitioner/index.ts +1 -1
  213. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  214. package/src/services/procedure/README.md +163 -163
  215. package/src/services/procedure/index.ts +1 -1
  216. package/src/services/procedure/procedure.service.ts +1682 -1682
  217. package/src/services/reviews/index.ts +1 -1
  218. package/src/services/reviews/reviews.service.ts +683 -636
  219. package/src/services/user/index.ts +1 -1
  220. package/src/services/user/user.service.ts +489 -489
  221. package/src/services/user/user.v2.service.ts +466 -466
  222. package/src/types/appointment/index.ts +453 -453
  223. package/src/types/calendar/index.ts +258 -258
  224. package/src/types/calendar/synced-calendar.types.ts +66 -66
  225. package/src/types/clinic/index.ts +489 -489
  226. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  227. package/src/types/clinic/preferences.types.ts +159 -159
  228. package/src/types/clinic/to-do +3 -3
  229. package/src/types/documentation-templates/index.ts +308 -308
  230. package/src/types/index.ts +44 -44
  231. package/src/types/notifications/README.md +77 -77
  232. package/src/types/notifications/index.ts +265 -265
  233. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  234. package/src/types/patient/allergies.ts +58 -58
  235. package/src/types/patient/index.ts +273 -273
  236. package/src/types/patient/medical-info.types.ts +152 -152
  237. package/src/types/patient/patient-requirements.ts +92 -92
  238. package/src/types/patient/token.types.ts +61 -61
  239. package/src/types/practitioner/index.ts +206 -206
  240. package/src/types/procedure/index.ts +181 -181
  241. package/src/types/profile/index.ts +39 -39
  242. package/src/types/reviews/index.ts +132 -130
  243. package/src/types/tz-lookup.d.ts +4 -4
  244. package/src/types/user/index.ts +38 -38
  245. package/src/utils/TIMESTAMPS.md +176 -176
  246. package/src/utils/TimestampUtils.ts +241 -241
  247. package/src/utils/index.ts +1 -1
  248. package/src/validations/appointment.schema.ts +574 -574
  249. package/src/validations/calendar.schema.ts +225 -225
  250. package/src/validations/clinic.schema.ts +493 -493
  251. package/src/validations/common.schema.ts +25 -25
  252. package/src/validations/documentation-templates/index.ts +1 -1
  253. package/src/validations/documentation-templates/template.schema.ts +220 -220
  254. package/src/validations/documentation-templates.schema.ts +10 -10
  255. package/src/validations/index.ts +20 -20
  256. package/src/validations/media.schema.ts +10 -10
  257. package/src/validations/notification.schema.ts +90 -90
  258. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  259. package/src/validations/patient/medical-info.schema.ts +125 -125
  260. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  261. package/src/validations/patient/token.schema.ts +29 -29
  262. package/src/validations/patient.schema.ts +216 -216
  263. package/src/validations/practitioner.schema.ts +222 -222
  264. package/src/validations/procedure-product.schema.ts +41 -41
  265. package/src/validations/procedure.schema.ts +124 -124
  266. package/src/validations/profile-info.schema.ts +41 -41
  267. package/src/validations/reviews.schema.ts +195 -189
  268. package/src/validations/schemas.ts +104 -104
  269. 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
+ }