@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/dist/admin/index.d.mts +20 -1
  2. package/dist/admin/index.d.ts +20 -1
  3. package/dist/admin/index.js +217 -1
  4. package/dist/admin/index.mjs +217 -1
  5. package/dist/index.d.mts +26 -3
  6. package/dist/index.d.ts +26 -3
  7. package/dist/index.js +168 -6
  8. package/dist/index.mjs +168 -6
  9. package/package.json +121 -121
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  35. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  36. package/src/admin/analytics/index.ts +2 -2
  37. package/src/admin/booking/README.md +125 -125
  38. package/src/admin/booking/booking.admin.ts +1037 -1037
  39. package/src/admin/booking/booking.calculator.ts +712 -712
  40. package/src/admin/booking/booking.types.ts +59 -59
  41. package/src/admin/booking/index.ts +3 -3
  42. package/src/admin/booking/timezones-problem.md +185 -185
  43. package/src/admin/calendar/README.md +7 -7
  44. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  45. package/src/admin/calendar/index.ts +1 -1
  46. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  47. package/src/admin/documentation-templates/index.ts +1 -1
  48. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  49. package/src/admin/free-consultation/index.ts +1 -1
  50. package/src/admin/index.ts +81 -81
  51. package/src/admin/logger/index.ts +78 -78
  52. package/src/admin/mailing/README.md +95 -95
  53. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  54. package/src/admin/mailing/appointment/index.ts +1 -1
  55. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  56. package/src/admin/mailing/base.mailing.service.ts +208 -208
  57. package/src/admin/mailing/index.ts +3 -3
  58. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  59. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  60. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  61. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  62. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  63. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  64. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  65. package/src/admin/notifications/index.ts +1 -1
  66. package/src/admin/notifications/notifications.admin.ts +710 -710
  67. package/src/admin/requirements/README.md +128 -128
  68. package/src/admin/requirements/index.ts +1 -1
  69. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  70. package/src/admin/users/index.ts +1 -1
  71. package/src/admin/users/user-profile.admin.ts +405 -405
  72. package/src/backoffice/constants/certification.constants.ts +13 -13
  73. package/src/backoffice/constants/index.ts +1 -1
  74. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  75. package/src/backoffice/errors/index.ts +1 -1
  76. package/src/backoffice/expo-safe/README.md +26 -26
  77. package/src/backoffice/expo-safe/index.ts +41 -41
  78. package/src/backoffice/index.ts +5 -5
  79. package/src/backoffice/services/FIXES_README.md +102 -102
  80. package/src/backoffice/services/README.md +57 -57
  81. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  82. package/src/backoffice/services/analytics.service.summary.md +143 -143
  83. package/src/backoffice/services/brand.service.ts +256 -256
  84. package/src/backoffice/services/category.service.ts +384 -384
  85. package/src/backoffice/services/constants.service.ts +385 -385
  86. package/src/backoffice/services/documentation-template.service.ts +202 -202
  87. package/src/backoffice/services/index.ts +10 -10
  88. package/src/backoffice/services/migrate-products.ts +116 -116
  89. package/src/backoffice/services/product.service.ts +553 -553
  90. package/src/backoffice/services/requirement.service.ts +235 -235
  91. package/src/backoffice/services/subcategory.service.ts +461 -461
  92. package/src/backoffice/services/technology.service.ts +1151 -1151
  93. package/src/backoffice/types/README.md +12 -12
  94. package/src/backoffice/types/admin-constants.types.ts +69 -69
  95. package/src/backoffice/types/brand.types.ts +29 -29
  96. package/src/backoffice/types/category.types.ts +67 -67
  97. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  98. package/src/backoffice/types/index.ts +10 -10
  99. package/src/backoffice/types/procedure-product.types.ts +38 -38
  100. package/src/backoffice/types/product.types.ts +240 -240
  101. package/src/backoffice/types/requirement.types.ts +63 -63
  102. package/src/backoffice/types/static/README.md +18 -18
  103. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  104. package/src/backoffice/types/static/certification.types.ts +37 -37
  105. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  106. package/src/backoffice/types/static/index.ts +6 -6
  107. package/src/backoffice/types/static/pricing.types.ts +16 -16
  108. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  109. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  110. package/src/backoffice/types/subcategory.types.ts +34 -34
  111. package/src/backoffice/types/technology.types.ts +168 -168
  112. package/src/backoffice/validations/index.ts +1 -1
  113. package/src/backoffice/validations/schemas.ts +164 -164
  114. package/src/config/__mocks__/firebase.ts +99 -99
  115. package/src/config/firebase.ts +78 -78
  116. package/src/config/index.ts +9 -9
  117. package/src/errors/auth.error.ts +6 -6
  118. package/src/errors/auth.errors.ts +211 -200
  119. package/src/errors/clinic.errors.ts +32 -32
  120. package/src/errors/firebase.errors.ts +47 -47
  121. package/src/errors/user.errors.ts +99 -99
  122. package/src/index.backup.ts +407 -407
  123. package/src/index.ts +6 -6
  124. package/src/locales/en.ts +31 -31
  125. package/src/recommender/admin/index.ts +1 -1
  126. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  127. package/src/recommender/front/index.ts +1 -1
  128. package/src/recommender/front/services/onboarding.service.ts +5 -5
  129. package/src/recommender/front/services/recommender.service.ts +3 -3
  130. package/src/recommender/index.ts +1 -1
  131. package/src/services/PATIENTAUTH.MD +197 -197
  132. package/src/services/README.md +106 -106
  133. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  134. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  135. package/src/services/__tests__/auth.service.test.ts +346 -346
  136. package/src/services/__tests__/base.service.test.ts +77 -77
  137. package/src/services/__tests__/user.service.test.ts +528 -528
  138. package/src/services/analytics/ARCHITECTURE.md +199 -199
  139. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  140. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  141. package/src/services/analytics/QUICK_START.md +393 -393
  142. package/src/services/analytics/README.md +304 -304
  143. package/src/services/analytics/SUMMARY.md +141 -141
  144. package/src/services/analytics/TRENDS.md +380 -380
  145. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  146. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  147. package/src/services/analytics/analytics.service.ts +2142 -2142
  148. package/src/services/analytics/index.ts +4 -4
  149. package/src/services/analytics/review-analytics.service.ts +941 -941
  150. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  151. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  152. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  153. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  154. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  155. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  156. package/src/services/appointment/README.md +17 -17
  157. package/src/services/appointment/appointment.service.ts +2558 -2558
  158. package/src/services/appointment/index.ts +1 -1
  159. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  160. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  161. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  162. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  163. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  164. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  165. package/src/services/auth/auth.service.ts +1043 -989
  166. package/src/services/auth/auth.v2.service.ts +961 -961
  167. package/src/services/auth/index.ts +7 -7
  168. package/src/services/auth/utils/error.utils.ts +90 -90
  169. package/src/services/auth/utils/firebase.utils.ts +49 -49
  170. package/src/services/auth/utils/index.ts +21 -21
  171. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  172. package/src/services/base.service.ts +41 -41
  173. package/src/services/calendar/calendar.service.ts +1077 -1077
  174. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  175. package/src/services/calendar/calendar.v3.service.ts +313 -313
  176. package/src/services/calendar/externalCalendar.service.ts +178 -178
  177. package/src/services/calendar/index.ts +5 -5
  178. package/src/services/calendar/synced-calendars.service.ts +743 -743
  179. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  180. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  181. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  182. package/src/services/calendar/utils/docs.utils.ts +157 -157
  183. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  184. package/src/services/calendar/utils/index.ts +8 -8
  185. package/src/services/calendar/utils/patient.utils.ts +198 -198
  186. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  187. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  188. package/src/services/clinic/README.md +204 -204
  189. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  190. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  191. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  192. package/src/services/clinic/billing-transactions.service.ts +217 -217
  193. package/src/services/clinic/clinic-admin.service.ts +202 -202
  194. package/src/services/clinic/clinic-group.service.ts +310 -310
  195. package/src/services/clinic/clinic.service.ts +708 -708
  196. package/src/services/clinic/index.ts +5 -5
  197. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  198. package/src/services/clinic/utils/admin.utils.ts +551 -551
  199. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  200. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  201. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  202. package/src/services/clinic/utils/filter.utils.ts +446 -446
  203. package/src/services/clinic/utils/index.ts +11 -11
  204. package/src/services/clinic/utils/photos.utils.ts +188 -188
  205. package/src/services/clinic/utils/search.utils.ts +84 -84
  206. package/src/services/clinic/utils/tag.utils.ts +124 -124
  207. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  208. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  209. package/src/services/documentation-templates/index.ts +2 -2
  210. package/src/services/index.ts +14 -14
  211. package/src/services/media/index.ts +1 -1
  212. package/src/services/media/media.service.ts +418 -418
  213. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  214. package/src/services/notifications/index.ts +1 -1
  215. package/src/services/notifications/notification.service.ts +215 -215
  216. package/src/services/patient/README.md +48 -48
  217. package/src/services/patient/To-Do.md +43 -43
  218. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  219. package/src/services/patient/index.ts +2 -2
  220. package/src/services/patient/patient.service.ts +883 -883
  221. package/src/services/patient/patientRequirements.service.ts +285 -285
  222. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  223. package/src/services/patient/utils/clinic.utils.ts +80 -80
  224. package/src/services/patient/utils/docs.utils.ts +142 -142
  225. package/src/services/patient/utils/index.ts +9 -9
  226. package/src/services/patient/utils/location.utils.ts +126 -126
  227. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  228. package/src/services/patient/utils/medical.utils.ts +458 -458
  229. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  230. package/src/services/patient/utils/profile.utils.ts +510 -510
  231. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  232. package/src/services/patient/utils/token.utils.ts +211 -211
  233. package/src/services/practitioner/README.md +145 -145
  234. package/src/services/practitioner/index.ts +1 -1
  235. package/src/services/practitioner/practitioner.service.ts +1799 -1742
  236. package/src/services/procedure/README.md +163 -163
  237. package/src/services/procedure/index.ts +1 -1
  238. package/src/services/procedure/procedure.service.ts +2307 -2200
  239. package/src/services/reviews/index.ts +1 -1
  240. package/src/services/reviews/reviews.service.ts +734 -734
  241. package/src/services/user/index.ts +1 -1
  242. package/src/services/user/user.service.ts +489 -489
  243. package/src/services/user/user.v2.service.ts +466 -466
  244. package/src/types/analytics/analytics.types.ts +597 -597
  245. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  246. package/src/types/analytics/index.ts +4 -4
  247. package/src/types/analytics/stored-analytics.types.ts +137 -137
  248. package/src/types/appointment/index.ts +480 -480
  249. package/src/types/calendar/index.ts +258 -258
  250. package/src/types/calendar/synced-calendar.types.ts +66 -66
  251. package/src/types/clinic/index.ts +498 -498
  252. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  253. package/src/types/clinic/preferences.types.ts +159 -159
  254. package/src/types/clinic/to-do +3 -3
  255. package/src/types/documentation-templates/index.ts +308 -308
  256. package/src/types/index.ts +47 -47
  257. package/src/types/notifications/README.md +77 -77
  258. package/src/types/notifications/index.ts +286 -286
  259. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  260. package/src/types/patient/allergies.ts +58 -58
  261. package/src/types/patient/index.ts +275 -275
  262. package/src/types/patient/medical-info.types.ts +152 -152
  263. package/src/types/patient/patient-requirements.ts +92 -92
  264. package/src/types/patient/token.types.ts +61 -61
  265. package/src/types/practitioner/index.ts +206 -206
  266. package/src/types/procedure/index.ts +181 -181
  267. package/src/types/profile/index.ts +39 -39
  268. package/src/types/reviews/index.ts +132 -132
  269. package/src/types/tz-lookup.d.ts +4 -4
  270. package/src/types/user/index.ts +38 -38
  271. package/src/utils/TIMESTAMPS.md +176 -176
  272. package/src/utils/TimestampUtils.ts +241 -241
  273. package/src/utils/index.ts +1 -1
  274. package/src/validations/appointment.schema.ts +574 -574
  275. package/src/validations/calendar.schema.ts +225 -225
  276. package/src/validations/clinic.schema.ts +494 -494
  277. package/src/validations/common.schema.ts +25 -25
  278. package/src/validations/documentation-templates/index.ts +1 -1
  279. package/src/validations/documentation-templates/template.schema.ts +220 -220
  280. package/src/validations/documentation-templates.schema.ts +10 -10
  281. package/src/validations/index.ts +20 -20
  282. package/src/validations/media.schema.ts +10 -10
  283. package/src/validations/notification.schema.ts +90 -90
  284. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  285. package/src/validations/patient/medical-info.schema.ts +125 -125
  286. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  287. package/src/validations/patient/token.schema.ts +29 -29
  288. package/src/validations/patient.schema.ts +217 -217
  289. package/src/validations/practitioner.schema.ts +222 -222
  290. package/src/validations/procedure-product.schema.ts +41 -41
  291. package/src/validations/procedure.schema.ts +124 -124
  292. package/src/validations/profile-info.schema.ts +41 -41
  293. package/src/validations/reviews.schema.ts +195 -195
  294. package/src/validations/schemas.ts +104 -104
  295. package/src/validations/shared.schema.ts +78 -78
@@ -1,949 +1,949 @@
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
- QueryConstraint,
14
- addDoc,
15
- writeBatch,
16
- limit,
17
- startAfter,
18
- } from "firebase/firestore";
19
- import {
20
- Clinic,
21
- CreateClinicData,
22
- CLINICS_COLLECTION,
23
- ClinicTag,
24
- ClinicGroup,
25
- ClinicBranchSetupData,
26
- ClinicLocation,
27
- } from "../../../types/clinic";
28
- import {
29
- geohashForLocation,
30
- distanceBetween,
31
- geohashQueryBounds,
32
- } from "geofire-common";
33
- import {
34
- clinicSchema,
35
- createClinicSchema,
36
- } from "../../../validations/clinic.schema";
37
- import { z } from "zod";
38
- import { FirebaseApp } from "firebase/app";
39
- import {
40
- uploadPhoto,
41
- uploadMultiplePhotos,
42
- processEntityPhotos,
43
- } from "./photos.utils";
44
-
45
- /**
46
- * Creates a new clinic
47
- * @param db - Firestore database instance
48
- * @param data - Clinic data
49
- * @param creatorAdminId - ID of the admin creating the clinic
50
- * @param clinicGroupService - Service for clinic group operations
51
- * @param clinicAdminService - Service for clinic admin operations
52
- * @param app - Firebase app instance
53
- * @returns The created clinic
54
- */
55
- export async function createClinic(
56
- db: Firestore,
57
- data: CreateClinicData,
58
- creatorAdminId: string,
59
- clinicGroupService: any,
60
- clinicAdminService: any,
61
- app: FirebaseApp
62
- ): Promise<Clinic> {
63
- console.log("[CLINIC] Starting clinic creation", { creatorAdminId });
64
- console.log("[CLINIC] Input data:", JSON.stringify(data, null, 2));
65
-
66
- // Validacija podataka
67
- try {
68
- const validatedData = createClinicSchema.parse(data);
69
- console.log("[CLINIC] Data validation passed");
70
- } catch (validationError) {
71
- console.error("[CLINIC] Data validation failed:", validationError);
72
- throw validationError;
73
- }
74
-
75
- const validatedData = createClinicSchema.parse(data);
76
-
77
- // Proveravamo da li admin postoji i da li pripada grupi
78
- try {
79
- console.log("[CLINIC] Checking if admin exists and belongs to group");
80
- const admin = await clinicAdminService.getClinicAdmin(creatorAdminId);
81
- if (!admin) {
82
- console.error("[CLINIC] Admin not found", { creatorAdminId });
83
- throw new Error("Admin not found");
84
- }
85
-
86
- if (admin.clinicGroupId !== validatedData.clinicGroupId) {
87
- console.error("[CLINIC] Admin does not belong to this clinic group", {
88
- adminGroupId: admin.clinicGroupId,
89
- requestedGroupId: validatedData.clinicGroupId,
90
- });
91
- throw new Error("Admin does not belong to this clinic group");
92
- }
93
- console.log("[CLINIC] Admin verified");
94
- } catch (adminError) {
95
- console.error("[CLINIC] Error verifying admin:", adminError);
96
- throw adminError;
97
- }
98
-
99
- // Proveravamo da li grupa postoji
100
- try {
101
- console.log("[CLINIC] Checking if clinic group exists");
102
- const group = await clinicGroupService.getClinicGroup(
103
- validatedData.clinicGroupId
104
- );
105
- if (!group) {
106
- console.error("[CLINIC] Clinic group not found", {
107
- groupId: validatedData.clinicGroupId,
108
- });
109
- throw new Error("Clinic group not found");
110
- }
111
- console.log("[CLINIC] Clinic group verified");
112
- } catch (groupError) {
113
- console.error("[CLINIC] Error verifying clinic group:", groupError);
114
- throw groupError;
115
- }
116
-
117
- // Generišemo geohash za lokaciju
118
- console.log("[CLINIC] Generating geohash for location");
119
- if (validatedData.location) {
120
- try {
121
- validatedData.location.geohash = geohashForLocation([
122
- validatedData.location.latitude,
123
- validatedData.location.longitude,
124
- ]);
125
- console.log("[CLINIC] Geohash generated successfully", {
126
- geohash: validatedData.location.geohash,
127
- });
128
- } catch (geohashError) {
129
- console.error("[CLINIC] Error generating geohash:", geohashError);
130
- throw geohashError;
131
- }
132
- }
133
-
134
- // Generate a unique ID for the clinic
135
- const clinicId = doc(collection(db, CLINICS_COLLECTION)).id;
136
- console.log("[CLINIC] Generated clinic ID:", clinicId);
137
-
138
- // Process photos
139
- console.log("[CLINIC] Processing photos");
140
-
141
- // Handle logo upload if provided
142
- let logoUrl = null;
143
- if (validatedData.logo) {
144
- console.log("[CLINIC] Processing logo");
145
- try {
146
- logoUrl = await uploadPhoto(
147
- validatedData.logo,
148
- "clinics",
149
- clinicId,
150
- "logo",
151
- app
152
- );
153
- console.log("[CLINIC] Logo processed", { logoUrl });
154
- } catch (logoError) {
155
- console.error("[CLINIC] Error processing logo:", logoError);
156
- // Continue with clinic creation even if logo upload fails
157
- }
158
- }
159
-
160
- // Handle cover photo upload
161
- let processedCoverPhoto: string | null = null;
162
- if (validatedData.coverPhoto) {
163
- console.log("[CLINIC] Processing cover photo");
164
- try {
165
- processedCoverPhoto = await uploadPhoto(
166
- validatedData.coverPhoto,
167
- "clinics",
168
- clinicId,
169
- "cover",
170
- app
171
- );
172
- console.log("[CLINIC] Cover photo processed", {
173
- coverPhoto: processedCoverPhoto,
174
- });
175
- } catch (coverPhotoError) {
176
- console.error("[CLINIC] Error processing cover photo:", coverPhotoError);
177
- // Continue with clinic creation even if cover photo upload fails
178
- processedCoverPhoto = validatedData.coverPhoto.startsWith("data:")
179
- ? null
180
- : validatedData.coverPhoto;
181
- }
182
- }
183
-
184
- // Handle featured photos upload
185
- let processedFeaturedPhotos: string[] = [];
186
- if (validatedData.featuredPhotos && validatedData.featuredPhotos.length > 0) {
187
- console.log("[CLINIC] Processing featured photos");
188
- try {
189
- processedFeaturedPhotos = await uploadMultiplePhotos(
190
- validatedData.featuredPhotos,
191
- "clinics",
192
- clinicId,
193
- "featured",
194
- app
195
- );
196
- console.log("[CLINIC] Featured photos processed", {
197
- count: processedFeaturedPhotos.length,
198
- });
199
- } catch (featuredError) {
200
- console.error(
201
- "[CLINIC] Error processing featured photos:",
202
- featuredError
203
- );
204
- // Continue with clinic creation even if featured photos upload fails
205
- processedFeaturedPhotos = validatedData.featuredPhotos.filter(
206
- (photo) => !photo.startsWith("data:")
207
- );
208
- }
209
- }
210
-
211
- // Handle photos with tags
212
- let processedPhotosWithTags = validatedData.photosWithTags || [];
213
- if (processedPhotosWithTags.length > 0) {
214
- console.log("[CLINIC] Processing photos with tags");
215
- try {
216
- const updatedPhotosWithTags = [];
217
- for (const photoWithTag of processedPhotosWithTags) {
218
- if (photoWithTag.url && photoWithTag.url.startsWith("data:")) {
219
- const uploadedUrl = await uploadPhoto(
220
- photoWithTag.url,
221
- "clinics",
222
- clinicId,
223
- `tagged-${photoWithTag.tag}`,
224
- app
225
- );
226
- if (uploadedUrl) {
227
- updatedPhotosWithTags.push({
228
- url: uploadedUrl,
229
- tag: photoWithTag.tag,
230
- });
231
- }
232
- } else {
233
- updatedPhotosWithTags.push(photoWithTag);
234
- }
235
- }
236
- processedPhotosWithTags = updatedPhotosWithTags;
237
- console.log("[CLINIC] Photos with tags processed", {
238
- count: processedPhotosWithTags.length,
239
- });
240
- } catch (tagsError) {
241
- console.error("[CLINIC] Error processing photos with tags:", tagsError);
242
- // Continue with clinic creation even if photos with tags upload fails
243
- processedPhotosWithTags =
244
- validatedData.photosWithTags?.filter(
245
- (photo) => !photo.url.startsWith("data:")
246
- ) || [];
247
- }
248
- }
249
-
250
- const now = Timestamp.now();
251
- console.log("[CLINIC] Preparing clinic data object");
252
-
253
- const clinicData: Clinic = {
254
- ...validatedData,
255
- id: clinicId,
256
- description: validatedData.description || "",
257
- location: {
258
- address: validatedData.location.address || "",
259
- city: validatedData.location.city || "",
260
- country: validatedData.location.country || "",
261
- postalCode: validatedData.location.postalCode || "",
262
- latitude: validatedData.location.latitude || 0,
263
- longitude: validatedData.location.longitude || 0,
264
- geohash: validatedData.location.geohash || "",
265
- },
266
- contactInfo: {
267
- email: validatedData.contactInfo.email || "",
268
- phoneNumber: validatedData.contactInfo.phoneNumber || "",
269
- alternativePhoneNumber:
270
- validatedData.contactInfo.alternativePhoneNumber || "",
271
- website: validatedData.contactInfo.website || "",
272
- },
273
- logo: logoUrl || "",
274
- tags: validatedData.tags || [],
275
- featuredPhotos: processedFeaturedPhotos || [],
276
- coverPhoto: processedCoverPhoto,
277
- photosWithTags: processedPhotosWithTags,
278
- doctors: [],
279
- doctorsInfo: [],
280
- procedures: [],
281
- proceduresInfo: [],
282
- reviewInfo: {
283
- totalReviews: 0,
284
- averageRating: 0,
285
- cleanliness: 0,
286
- facilities: 0,
287
- staffFriendliness: 0,
288
- waitingTime: 0,
289
- accessibility: 0,
290
- recommendationPercentage: 0,
291
- },
292
- admins: [creatorAdminId],
293
- createdAt: now,
294
- updatedAt: now,
295
- isActive: true,
296
- isVerified: false,
297
- };
298
-
299
- try {
300
- // Validiramo kompletan objekat
301
- console.log("[CLINIC] Validating complete clinic object");
302
- try {
303
- clinicSchema.parse(clinicData);
304
- console.log("[CLINIC] Clinic validation passed");
305
- } catch (schemaError) {
306
- console.error(
307
- "[CLINIC] Clinic validation failed:",
308
- JSON.stringify(schemaError, null, 2)
309
- );
310
- throw schemaError;
311
- }
312
-
313
- // Čuvamo u Firestore
314
- console.log("[CLINIC] Saving clinic to Firestore", {
315
- clinicId: clinicData.id,
316
- });
317
- try {
318
- await setDoc(doc(db, CLINICS_COLLECTION, clinicData.id), clinicData);
319
- console.log("[CLINIC] Clinic saved successfully");
320
- } catch (firestoreError) {
321
- console.error("[CLINIC] Error saving to Firestore:", firestoreError);
322
- throw firestoreError;
323
- }
324
-
325
- // Dodajemo kliniku u grupaciju
326
- console.log("[CLINIC] Adding clinic to clinic group");
327
- try {
328
- const group = await clinicGroupService.getClinicGroup(
329
- validatedData.clinicGroupId
330
- );
331
- if (group) {
332
- await clinicGroupService.updateClinicGroup(
333
- validatedData.clinicGroupId,
334
- {
335
- clinics: [...group.clinics, clinicData.id],
336
- }
337
- );
338
- console.log("[CLINIC] Clinic added to group successfully");
339
- }
340
- } catch (groupUpdateError) {
341
- console.error("[CLINIC] Error adding clinic to group:", groupUpdateError);
342
- // Continue even if adding to group fails
343
- }
344
-
345
- // Dodajemo kliniku adminu koji ju je kreirao
346
- console.log("[CLINIC] Adding clinic to admin's managed clinics");
347
- try {
348
- await clinicAdminService.addClinicToManaged(
349
- creatorAdminId,
350
- clinicData.id
351
- );
352
- console.log(
353
- "[CLINIC] Clinic added to admin's managed clinics successfully"
354
- );
355
- } catch (adminUpdateError) {
356
- console.error(
357
- "[CLINIC] Error adding clinic to admin's managed clinics:",
358
- adminUpdateError
359
- );
360
- // Continue even if adding to admin fails
361
- }
362
-
363
- console.log("[CLINIC] Clinic creation completed successfully", {
364
- clinicId: clinicData.id,
365
- clinicName: clinicData.name,
366
- });
367
- return clinicData;
368
- } catch (error) {
369
- if (error instanceof z.ZodError) {
370
- console.error(
371
- "[CLINIC] Zod validation error:",
372
- JSON.stringify(error.errors, null, 2)
373
- );
374
- throw new Error("Invalid clinic data: " + error.message);
375
- }
376
- console.error("[CLINIC] Unhandled error in createClinic:", error);
377
- throw error;
378
- }
379
- }
380
-
381
- /**
382
- * Gets a clinic by ID
383
- * @param db - Firestore database instance
384
- * @param clinicId - ID of the clinic to get
385
- * @returns The clinic or null if not found
386
- */
387
- export async function getClinic(
388
- db: Firestore,
389
- clinicId: string
390
- ): Promise<Clinic | null> {
391
- const docRef = doc(db, CLINICS_COLLECTION, clinicId);
392
- const docSnap = await getDoc(docRef);
393
-
394
- if (docSnap.exists()) {
395
- return docSnap.data() as Clinic;
396
- }
397
-
398
- return null;
399
- }
400
-
401
- /**
402
- * Gets all clinics in a group
403
- * @param db - Firestore database instance
404
- * @param groupId - ID of the clinic group
405
- * @returns Array of clinics in the group
406
- */
407
- export async function getClinicsByGroup(
408
- db: Firestore,
409
- groupId: string
410
- ): Promise<Clinic[]> {
411
- const q = query(
412
- collection(db, CLINICS_COLLECTION),
413
- where("clinicGroupId", "==", groupId),
414
- where("isActive", "==", true)
415
- );
416
-
417
- const querySnapshot = await getDocs(q);
418
- return querySnapshot.docs.map((doc) => doc.data() as Clinic);
419
- }
420
-
421
- /**
422
- * Updates a clinic
423
- * @param db - Firestore database instance
424
- * @param clinicId - ID of the clinic to update
425
- * @param data - Data to update
426
- * @param adminId - ID of the admin making the update
427
- * @param clinicAdminService - Service for clinic admin operations
428
- * @param app - Firebase app instance
429
- * @returns The updated clinic
430
- */
431
- export async function updateClinic(
432
- db: Firestore,
433
- clinicId: string,
434
- data: Partial<Clinic>,
435
- adminId: string,
436
- clinicAdminService: any,
437
- app: FirebaseApp
438
- ): Promise<Clinic> {
439
- console.log("[CLINIC] Starting clinic update", { clinicId, adminId });
440
-
441
- const clinic = await getClinic(db, clinicId);
442
- if (!clinic) {
443
- console.error("[CLINIC] Clinic not found", { clinicId });
444
- throw new Error("Clinic not found");
445
- }
446
-
447
- // Proveravamo da li admin ima prava da ažurira kliniku
448
- try {
449
- console.log("[CLINIC] Checking admin permissions");
450
- const admin = await clinicAdminService.getClinicAdmin(adminId);
451
- if (!admin) {
452
- console.error("[CLINIC] Admin not found", { adminId });
453
- throw new Error("Admin not found");
454
- }
455
-
456
- // Check if admin is either:
457
- // 1. The owner of the clinic group that this clinic belongs to, OR
458
- // 2. Has this clinic in their managed clinics list AND is listed in the clinic's admins array
459
- const hasPermission =
460
- (admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
461
- (admin.clinicsManaged.includes(clinicId) &&
462
- clinic.admins &&
463
- clinic.admins.includes(adminId));
464
-
465
- if (!hasPermission) {
466
- console.error(
467
- "[CLINIC] Admin does not have permission to update this clinic",
468
- {
469
- adminId,
470
- clinicId,
471
- isGroupOwner: admin.isGroupOwner,
472
- clinicsManaged: admin.clinicsManaged,
473
- isClinicAdmin: clinic.admins && clinic.admins.includes(adminId),
474
- }
475
- );
476
- throw new Error("Admin does not have permission to update this clinic");
477
- }
478
- console.log("[CLINIC] Admin permissions verified");
479
- } catch (adminError) {
480
- console.error("[CLINIC] Error verifying admin permissions:", adminError);
481
- throw adminError;
482
- }
483
-
484
- // Process photos in the update data
485
- let updatedData = { ...data };
486
-
487
- // Handle logo update if provided
488
- if (
489
- data.logo &&
490
- typeof data.logo === "string" &&
491
- data.logo.startsWith("data:")
492
- ) {
493
- console.log("[CLINIC] Processing logo update");
494
- try {
495
- const logoUrl = await uploadPhoto(
496
- data.logo,
497
- "clinics",
498
- clinicId,
499
- "logo",
500
- app
501
- );
502
- console.log("[CLINIC] Logo update processed", { logoUrl });
503
-
504
- if (logoUrl !== null) {
505
- updatedData.logo = logoUrl;
506
- }
507
- } catch (logoError) {
508
- console.error("[CLINIC] Error processing logo update:", logoError);
509
- // Continue with update even if logo upload fails
510
- }
511
- }
512
-
513
- // Handle cover photo update if provided
514
- if (data.coverPhoto) {
515
- console.log("[CLINIC] Processing cover photo update");
516
- try {
517
- // Check if it's a data URL that needs to be uploaded
518
- if (
519
- typeof data.coverPhoto === "string" &&
520
- data.coverPhoto.startsWith("data:")
521
- ) {
522
- const uploadedPhoto = await uploadPhoto(
523
- data.coverPhoto,
524
- "clinics",
525
- clinicId,
526
- "cover",
527
- app
528
- );
529
- if (uploadedPhoto) {
530
- updatedData.coverPhoto = uploadedPhoto;
531
- }
532
- } else {
533
- // Keep existing photo URL
534
- updatedData.coverPhoto = data.coverPhoto;
535
- }
536
- console.log("[CLINIC] Cover photo update processed");
537
- } catch (photoError) {
538
- console.error(
539
- "[CLINIC] Error processing cover photo update:",
540
- photoError
541
- );
542
- // Keep existing photo if upload fails
543
- if (
544
- typeof data.coverPhoto === "string" &&
545
- !data.coverPhoto.startsWith("data:")
546
- ) {
547
- updatedData.coverPhoto = data.coverPhoto;
548
- }
549
- }
550
- }
551
-
552
- // Handle featured photos update if provided
553
- if (data.featuredPhotos && data.featuredPhotos.length > 0) {
554
- console.log("[CLINIC] Processing featured photos update");
555
- try {
556
- // Filter out only data URLs that need to be uploaded
557
- const dataUrlPhotos = data.featuredPhotos.filter(
558
- (photo) => typeof photo === "string" && photo.startsWith("data:")
559
- );
560
- const existingPhotos = data.featuredPhotos.filter(
561
- (photo) => typeof photo === "string" && !photo.startsWith("data:")
562
- );
563
-
564
- if (dataUrlPhotos.length > 0) {
565
- const uploadedPhotos = await uploadMultiplePhotos(
566
- dataUrlPhotos,
567
- "clinics",
568
- clinicId,
569
- "featured",
570
- app
571
- );
572
- console.log("[CLINIC] Featured photos update processed", {
573
- count: uploadedPhotos.length,
574
- });
575
-
576
- // Combine existing photos with newly uploaded ones
577
- updatedData.featuredPhotos = [...existingPhotos, ...uploadedPhotos];
578
- } else {
579
- // If no new photos to upload, just use the existing ones
580
- updatedData.featuredPhotos = existingPhotos;
581
- }
582
- } catch (featuredError) {
583
- console.error(
584
- "[CLINIC] Error processing featured photos update:",
585
- featuredError
586
- );
587
- // Continue with update even if featured photos upload fails
588
- updatedData.featuredPhotos = data.featuredPhotos.filter(
589
- (photo) => typeof photo === "string" && !photo.startsWith("data:")
590
- );
591
- }
592
- }
593
-
594
- // Handle photos with tags update if provided
595
- if (data.photosWithTags && data.photosWithTags.length > 0) {
596
- console.log("[CLINIC] Processing photos with tags update");
597
- try {
598
- const updatedPhotosWithTags = [];
599
-
600
- // Process each photo with tag
601
- for (const photoWithTag of data.photosWithTags) {
602
- if (photoWithTag.url && photoWithTag.url.startsWith("data:")) {
603
- // Upload new photo
604
- const uploadedUrl = await uploadPhoto(
605
- photoWithTag.url,
606
- "clinics",
607
- clinicId,
608
- `tagged-${photoWithTag.tag}`,
609
- app
610
- );
611
-
612
- if (uploadedUrl) {
613
- updatedPhotosWithTags.push({
614
- url: uploadedUrl,
615
- tag: photoWithTag.tag,
616
- });
617
- }
618
- } else {
619
- // Keep existing photo
620
- updatedPhotosWithTags.push(photoWithTag);
621
- }
622
- }
623
-
624
- updatedData.photosWithTags = updatedPhotosWithTags;
625
- console.log("[CLINIC] Photos with tags update processed", {
626
- count: updatedPhotosWithTags.length,
627
- });
628
- } catch (tagsError) {
629
- console.error(
630
- "[CLINIC] Error processing photos with tags update:",
631
- tagsError
632
- );
633
- // Continue with update even if photos with tags upload fails
634
- updatedData.photosWithTags = data.photosWithTags.filter(
635
- (photo) => !photo.url.startsWith("data:")
636
- );
637
- }
638
- }
639
-
640
- // Add timestamp
641
- updatedData = {
642
- ...updatedData,
643
- updatedAt: Timestamp.now(),
644
- };
645
-
646
- console.log("[CLINIC] Updating clinic in Firestore");
647
- try {
648
- await updateDoc(doc(db, CLINICS_COLLECTION, clinicId), updatedData);
649
- console.log("[CLINIC] Clinic updated successfully");
650
- } catch (updateError) {
651
- console.error("[CLINIC] Error updating clinic in Firestore:", updateError);
652
- throw updateError;
653
- }
654
-
655
- // Return updated data
656
- const updatedClinic = await getClinic(db, clinicId);
657
- if (!updatedClinic) {
658
- console.error("[CLINIC] Failed to retrieve updated clinic");
659
- throw new Error("Failed to retrieve updated clinic");
660
- }
661
-
662
- console.log("[CLINIC] Clinic update completed successfully");
663
- return updatedClinic;
664
- }
665
-
666
- /**
667
- * Deactivates a clinic
668
- * @param db - Firestore database instance
669
- * @param clinicId - ID of the clinic to deactivate
670
- * @param adminId - ID of the admin making the deactivation
671
- * @param clinicAdminService - Service for clinic admin operations
672
- */
673
- export async function deactivateClinic(
674
- db: Firestore,
675
- clinicId: string,
676
- adminId: string,
677
- clinicAdminService: any
678
- ): Promise<void> {
679
- const clinic = await getClinic(db, clinicId);
680
- if (!clinic) {
681
- throw new Error("Clinic not found");
682
- }
683
-
684
- // Proveravamo da li admin ima prava da deaktivira kliniku
685
- const admin = await clinicAdminService.getClinicAdmin(adminId);
686
- if (!admin) {
687
- throw new Error("Admin not found");
688
- }
689
-
690
- // Check if admin is either:
691
- // 1. The owner of the clinic group, OR
692
- // 2. Has this clinic in their managed clinics list, OR
693
- // 3. Is listed in the clinic's admins array
694
- const hasPermission =
695
- (admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
696
- (admin.clinicsManaged.includes(clinicId) &&
697
- clinic.admins &&
698
- clinic.admins.includes(adminId));
699
-
700
- if (!hasPermission) {
701
- throw new Error("Admin does not have permission to deactivate this clinic");
702
- }
703
-
704
- await updateDoc(doc(db, CLINICS_COLLECTION, clinicId), {
705
- isActive: false,
706
- updatedAt: Timestamp.now(),
707
- });
708
- }
709
-
710
- /**
711
- * Gets clinics managed by an admin
712
- * @param db - Firestore database instance
713
- * @param adminId - ID of the admin
714
- * @param options - Options for filtering clinics
715
- * @param clinicAdminService - Service for clinic admin operations
716
- * @param clinicGroupService - Service for clinic group operations
717
- * @returns Array of clinics managed by the admin
718
- */
719
- export async function getClinicsByAdmin(
720
- db: Firestore,
721
- adminId: string,
722
- options: {
723
- isActive?: boolean;
724
- includeGroupClinics?: boolean;
725
- } = {},
726
- clinicAdminService: any,
727
- clinicGroupService: any
728
- ): Promise<Clinic[]> {
729
- const admin = await clinicAdminService.getClinicAdmin(adminId);
730
- if (!admin) {
731
- throw new Error("Admin not found");
732
- }
733
-
734
- let clinicIds: string[] = [...admin.clinicsManaged];
735
-
736
- // Ako je admin vlasnik grupe i tražimo sve klinike grupe
737
- if (admin.isGroupOwner && options.includeGroupClinics) {
738
- const group = await clinicGroupService.getClinicGroup(admin.clinicGroupId);
739
- if (group) {
740
- clinicIds = [...new Set([...clinicIds, ...group.clinics])];
741
- }
742
- }
743
-
744
- if (clinicIds.length === 0) {
745
- return [];
746
- }
747
-
748
- const constraints: QueryConstraint[] = [where("id", "in", clinicIds)];
749
-
750
- if (options.isActive !== undefined) {
751
- constraints.push(where("isActive", "==", options.isActive));
752
- }
753
-
754
- const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
755
- const querySnapshot = await getDocs(q);
756
-
757
- return querySnapshot.docs.map((doc) => doc.data() as Clinic);
758
- }
759
-
760
- /**
761
- * Gets active clinics managed by an admin
762
- * @param db - Firestore database instance
763
- * @param adminId - ID of the admin
764
- * @param clinicAdminService - Service for clinic admin operations
765
- * @param clinicGroupService - Service for clinic group operations
766
- * @returns Array of active clinics managed by the admin
767
- */
768
- export async function getActiveClinicsByAdmin(
769
- db: Firestore,
770
- adminId: string,
771
- clinicAdminService: any,
772
- clinicGroupService: any
773
- ): Promise<Clinic[]> {
774
- return getClinicsByAdmin(
775
- db,
776
- adminId,
777
- { isActive: true },
778
- clinicAdminService,
779
- clinicGroupService
780
- );
781
- }
782
-
783
- /**
784
- * Retrieves a clinic by its ID
785
- *
786
- * @param db - Firestore database instance
787
- * @param clinicId - ID of the clinic to retrieve
788
- * @returns The clinic if found, null otherwise
789
- */
790
- export async function getClinicById(
791
- db: Firestore,
792
- clinicId: string
793
- ): Promise<Clinic | null> {
794
- try {
795
- const clinicRef = doc(db, CLINICS_COLLECTION, clinicId);
796
- const clinicSnapshot = await getDoc(clinicRef);
797
-
798
- if (!clinicSnapshot.exists()) {
799
- return null;
800
- }
801
-
802
- const clinicData = clinicSnapshot.data() as Clinic;
803
- return {
804
- ...clinicData,
805
- id: clinicSnapshot.id,
806
- };
807
- } catch (error) {
808
- console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
809
- throw error;
810
- }
811
- }
812
-
813
- /**
814
- * Retrieves all clinics with optional pagination
815
- *
816
- * @param db - Firestore database instance
817
- * @param pagination - Optional number of clinics per page (0 or undefined returns all)
818
- * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
819
- * @returns Array of clinics and the last document for pagination
820
- */
821
- export async function getAllClinics(
822
- db: Firestore,
823
- pagination?: number,
824
- lastDoc?: any
825
- ): Promise<{ clinics: Clinic[]; lastDoc: any }> {
826
- try {
827
- const clinicsCollection = collection(db, CLINICS_COLLECTION);
828
- let clinicsQuery = query(clinicsCollection);
829
-
830
- // If pagination is specified and greater than 0, limit the query
831
- if (pagination && pagination > 0) {
832
- if (lastDoc) {
833
- clinicsQuery = query(
834
- clinicsCollection,
835
- startAfter(lastDoc),
836
- limit(pagination)
837
- );
838
- } else {
839
- clinicsQuery = query(clinicsCollection, limit(pagination));
840
- }
841
- }
842
-
843
- const clinicsSnapshot = await getDocs(clinicsQuery);
844
- const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
845
-
846
- const clinics = clinicsSnapshot.docs.map((doc) => {
847
- const data = doc.data() as Clinic;
848
- return {
849
- ...data,
850
- id: doc.id,
851
- };
852
- });
853
-
854
- return {
855
- clinics,
856
- lastDoc: lastVisible,
857
- };
858
- } catch (error) {
859
- console.error("[CLINIC_UTILS] Error getting all clinics:", error);
860
- throw error;
861
- }
862
- }
863
-
864
- /**
865
- * Retrieves all clinics within a specified range from a location with optional pagination
866
- *
867
- * @param db - Firestore database instance
868
- * @param center - The center location coordinates {latitude, longitude}
869
- * @param rangeInKm - The range in kilometers to search within
870
- * @param pagination - Optional number of clinics per page (0 or undefined returns all)
871
- * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
872
- * @returns Array of clinics with distance information and the last document for pagination
873
- */
874
- export async function getAllClinicsInRange(
875
- db: Firestore,
876
- center: { latitude: number; longitude: number },
877
- rangeInKm: number,
878
- pagination?: number,
879
- lastDoc?: any
880
- ): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
881
- const bounds = geohashQueryBounds(
882
- [center.latitude, center.longitude],
883
- rangeInKm * 1000
884
- );
885
-
886
- const matchingClinics: (Clinic & { distance: number })[] = [];
887
- let lastDocSnapshot = null;
888
-
889
- for (const b of bounds) {
890
- const constraints: QueryConstraint[] = [
891
- where("location.geohash", ">=", b[0]),
892
- where("location.geohash", "<=", b[1]),
893
- where("isActive", "==", true),
894
- ];
895
-
896
- const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
897
- const querySnapshot = await getDocs(q);
898
-
899
- for (const doc of querySnapshot.docs) {
900
- const clinic = doc.data() as Clinic;
901
- const distance = distanceBetween(
902
- [center.latitude, center.longitude],
903
- [clinic.location.latitude, clinic.location.longitude]
904
- );
905
-
906
- // Convert to kilometers
907
- const distanceInKm = distance / 1000;
908
-
909
- if (distanceInKm <= rangeInKm) {
910
- matchingClinics.push({
911
- ...clinic,
912
- distance: distanceInKm,
913
- });
914
- }
915
- }
916
- }
917
-
918
- // Sort by distance
919
- matchingClinics.sort((a, b) => a.distance - b.distance);
920
-
921
- if (pagination && pagination > 0) {
922
- // Paginate results
923
- let result = matchingClinics;
924
- if (lastDoc && matchingClinics.length > 0) {
925
- const lastIndex = matchingClinics.findIndex(
926
- (clinic) => clinic.id === lastDoc.id
927
- );
928
- if (lastIndex !== -1) {
929
- result = matchingClinics.slice(lastIndex + 1);
930
- }
931
- }
932
-
933
- const paginatedClinics = result.slice(0, pagination);
934
- const newLastDoc =
935
- paginatedClinics.length > 0
936
- ? paginatedClinics[paginatedClinics.length - 1]
937
- : null;
938
-
939
- return {
940
- clinics: paginatedClinics,
941
- lastDoc: newLastDoc,
942
- };
943
- }
944
-
945
- return {
946
- clinics: matchingClinics,
947
- lastDoc: null,
948
- };
949
- }
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
+ QueryConstraint,
14
+ addDoc,
15
+ writeBatch,
16
+ limit,
17
+ startAfter,
18
+ } from "firebase/firestore";
19
+ import {
20
+ Clinic,
21
+ CreateClinicData,
22
+ CLINICS_COLLECTION,
23
+ ClinicTag,
24
+ ClinicGroup,
25
+ ClinicBranchSetupData,
26
+ ClinicLocation,
27
+ } from "../../../types/clinic";
28
+ import {
29
+ geohashForLocation,
30
+ distanceBetween,
31
+ geohashQueryBounds,
32
+ } from "geofire-common";
33
+ import {
34
+ clinicSchema,
35
+ createClinicSchema,
36
+ } from "../../../validations/clinic.schema";
37
+ import { z } from "zod";
38
+ import { FirebaseApp } from "firebase/app";
39
+ import {
40
+ uploadPhoto,
41
+ uploadMultiplePhotos,
42
+ processEntityPhotos,
43
+ } from "./photos.utils";
44
+
45
+ /**
46
+ * Creates a new clinic
47
+ * @param db - Firestore database instance
48
+ * @param data - Clinic data
49
+ * @param creatorAdminId - ID of the admin creating the clinic
50
+ * @param clinicGroupService - Service for clinic group operations
51
+ * @param clinicAdminService - Service for clinic admin operations
52
+ * @param app - Firebase app instance
53
+ * @returns The created clinic
54
+ */
55
+ export async function createClinic(
56
+ db: Firestore,
57
+ data: CreateClinicData,
58
+ creatorAdminId: string,
59
+ clinicGroupService: any,
60
+ clinicAdminService: any,
61
+ app: FirebaseApp
62
+ ): Promise<Clinic> {
63
+ console.log("[CLINIC] Starting clinic creation", { creatorAdminId });
64
+ console.log("[CLINIC] Input data:", JSON.stringify(data, null, 2));
65
+
66
+ // Validacija podataka
67
+ try {
68
+ const validatedData = createClinicSchema.parse(data);
69
+ console.log("[CLINIC] Data validation passed");
70
+ } catch (validationError) {
71
+ console.error("[CLINIC] Data validation failed:", validationError);
72
+ throw validationError;
73
+ }
74
+
75
+ const validatedData = createClinicSchema.parse(data);
76
+
77
+ // Proveravamo da li admin postoji i da li pripada grupi
78
+ try {
79
+ console.log("[CLINIC] Checking if admin exists and belongs to group");
80
+ const admin = await clinicAdminService.getClinicAdmin(creatorAdminId);
81
+ if (!admin) {
82
+ console.error("[CLINIC] Admin not found", { creatorAdminId });
83
+ throw new Error("Admin not found");
84
+ }
85
+
86
+ if (admin.clinicGroupId !== validatedData.clinicGroupId) {
87
+ console.error("[CLINIC] Admin does not belong to this clinic group", {
88
+ adminGroupId: admin.clinicGroupId,
89
+ requestedGroupId: validatedData.clinicGroupId,
90
+ });
91
+ throw new Error("Admin does not belong to this clinic group");
92
+ }
93
+ console.log("[CLINIC] Admin verified");
94
+ } catch (adminError) {
95
+ console.error("[CLINIC] Error verifying admin:", adminError);
96
+ throw adminError;
97
+ }
98
+
99
+ // Proveravamo da li grupa postoji
100
+ try {
101
+ console.log("[CLINIC] Checking if clinic group exists");
102
+ const group = await clinicGroupService.getClinicGroup(
103
+ validatedData.clinicGroupId
104
+ );
105
+ if (!group) {
106
+ console.error("[CLINIC] Clinic group not found", {
107
+ groupId: validatedData.clinicGroupId,
108
+ });
109
+ throw new Error("Clinic group not found");
110
+ }
111
+ console.log("[CLINIC] Clinic group verified");
112
+ } catch (groupError) {
113
+ console.error("[CLINIC] Error verifying clinic group:", groupError);
114
+ throw groupError;
115
+ }
116
+
117
+ // Generišemo geohash za lokaciju
118
+ console.log("[CLINIC] Generating geohash for location");
119
+ if (validatedData.location) {
120
+ try {
121
+ validatedData.location.geohash = geohashForLocation([
122
+ validatedData.location.latitude,
123
+ validatedData.location.longitude,
124
+ ]);
125
+ console.log("[CLINIC] Geohash generated successfully", {
126
+ geohash: validatedData.location.geohash,
127
+ });
128
+ } catch (geohashError) {
129
+ console.error("[CLINIC] Error generating geohash:", geohashError);
130
+ throw geohashError;
131
+ }
132
+ }
133
+
134
+ // Generate a unique ID for the clinic
135
+ const clinicId = doc(collection(db, CLINICS_COLLECTION)).id;
136
+ console.log("[CLINIC] Generated clinic ID:", clinicId);
137
+
138
+ // Process photos
139
+ console.log("[CLINIC] Processing photos");
140
+
141
+ // Handle logo upload if provided
142
+ let logoUrl = null;
143
+ if (validatedData.logo) {
144
+ console.log("[CLINIC] Processing logo");
145
+ try {
146
+ logoUrl = await uploadPhoto(
147
+ validatedData.logo,
148
+ "clinics",
149
+ clinicId,
150
+ "logo",
151
+ app
152
+ );
153
+ console.log("[CLINIC] Logo processed", { logoUrl });
154
+ } catch (logoError) {
155
+ console.error("[CLINIC] Error processing logo:", logoError);
156
+ // Continue with clinic creation even if logo upload fails
157
+ }
158
+ }
159
+
160
+ // Handle cover photo upload
161
+ let processedCoverPhoto: string | null = null;
162
+ if (validatedData.coverPhoto) {
163
+ console.log("[CLINIC] Processing cover photo");
164
+ try {
165
+ processedCoverPhoto = await uploadPhoto(
166
+ validatedData.coverPhoto,
167
+ "clinics",
168
+ clinicId,
169
+ "cover",
170
+ app
171
+ );
172
+ console.log("[CLINIC] Cover photo processed", {
173
+ coverPhoto: processedCoverPhoto,
174
+ });
175
+ } catch (coverPhotoError) {
176
+ console.error("[CLINIC] Error processing cover photo:", coverPhotoError);
177
+ // Continue with clinic creation even if cover photo upload fails
178
+ processedCoverPhoto = validatedData.coverPhoto.startsWith("data:")
179
+ ? null
180
+ : validatedData.coverPhoto;
181
+ }
182
+ }
183
+
184
+ // Handle featured photos upload
185
+ let processedFeaturedPhotos: string[] = [];
186
+ if (validatedData.featuredPhotos && validatedData.featuredPhotos.length > 0) {
187
+ console.log("[CLINIC] Processing featured photos");
188
+ try {
189
+ processedFeaturedPhotos = await uploadMultiplePhotos(
190
+ validatedData.featuredPhotos,
191
+ "clinics",
192
+ clinicId,
193
+ "featured",
194
+ app
195
+ );
196
+ console.log("[CLINIC] Featured photos processed", {
197
+ count: processedFeaturedPhotos.length,
198
+ });
199
+ } catch (featuredError) {
200
+ console.error(
201
+ "[CLINIC] Error processing featured photos:",
202
+ featuredError
203
+ );
204
+ // Continue with clinic creation even if featured photos upload fails
205
+ processedFeaturedPhotos = validatedData.featuredPhotos.filter(
206
+ (photo) => !photo.startsWith("data:")
207
+ );
208
+ }
209
+ }
210
+
211
+ // Handle photos with tags
212
+ let processedPhotosWithTags = validatedData.photosWithTags || [];
213
+ if (processedPhotosWithTags.length > 0) {
214
+ console.log("[CLINIC] Processing photos with tags");
215
+ try {
216
+ const updatedPhotosWithTags = [];
217
+ for (const photoWithTag of processedPhotosWithTags) {
218
+ if (photoWithTag.url && photoWithTag.url.startsWith("data:")) {
219
+ const uploadedUrl = await uploadPhoto(
220
+ photoWithTag.url,
221
+ "clinics",
222
+ clinicId,
223
+ `tagged-${photoWithTag.tag}`,
224
+ app
225
+ );
226
+ if (uploadedUrl) {
227
+ updatedPhotosWithTags.push({
228
+ url: uploadedUrl,
229
+ tag: photoWithTag.tag,
230
+ });
231
+ }
232
+ } else {
233
+ updatedPhotosWithTags.push(photoWithTag);
234
+ }
235
+ }
236
+ processedPhotosWithTags = updatedPhotosWithTags;
237
+ console.log("[CLINIC] Photos with tags processed", {
238
+ count: processedPhotosWithTags.length,
239
+ });
240
+ } catch (tagsError) {
241
+ console.error("[CLINIC] Error processing photos with tags:", tagsError);
242
+ // Continue with clinic creation even if photos with tags upload fails
243
+ processedPhotosWithTags =
244
+ validatedData.photosWithTags?.filter(
245
+ (photo) => !photo.url.startsWith("data:")
246
+ ) || [];
247
+ }
248
+ }
249
+
250
+ const now = Timestamp.now();
251
+ console.log("[CLINIC] Preparing clinic data object");
252
+
253
+ const clinicData: Clinic = {
254
+ ...validatedData,
255
+ id: clinicId,
256
+ description: validatedData.description || "",
257
+ location: {
258
+ address: validatedData.location.address || "",
259
+ city: validatedData.location.city || "",
260
+ country: validatedData.location.country || "",
261
+ postalCode: validatedData.location.postalCode || "",
262
+ latitude: validatedData.location.latitude || 0,
263
+ longitude: validatedData.location.longitude || 0,
264
+ geohash: validatedData.location.geohash || "",
265
+ },
266
+ contactInfo: {
267
+ email: validatedData.contactInfo.email || "",
268
+ phoneNumber: validatedData.contactInfo.phoneNumber || "",
269
+ alternativePhoneNumber:
270
+ validatedData.contactInfo.alternativePhoneNumber || "",
271
+ website: validatedData.contactInfo.website || "",
272
+ },
273
+ logo: logoUrl || "",
274
+ tags: validatedData.tags || [],
275
+ featuredPhotos: processedFeaturedPhotos || [],
276
+ coverPhoto: processedCoverPhoto,
277
+ photosWithTags: processedPhotosWithTags,
278
+ doctors: [],
279
+ doctorsInfo: [],
280
+ procedures: [],
281
+ proceduresInfo: [],
282
+ reviewInfo: {
283
+ totalReviews: 0,
284
+ averageRating: 0,
285
+ cleanliness: 0,
286
+ facilities: 0,
287
+ staffFriendliness: 0,
288
+ waitingTime: 0,
289
+ accessibility: 0,
290
+ recommendationPercentage: 0,
291
+ },
292
+ admins: [creatorAdminId],
293
+ createdAt: now,
294
+ updatedAt: now,
295
+ isActive: true,
296
+ isVerified: false,
297
+ };
298
+
299
+ try {
300
+ // Validiramo kompletan objekat
301
+ console.log("[CLINIC] Validating complete clinic object");
302
+ try {
303
+ clinicSchema.parse(clinicData);
304
+ console.log("[CLINIC] Clinic validation passed");
305
+ } catch (schemaError) {
306
+ console.error(
307
+ "[CLINIC] Clinic validation failed:",
308
+ JSON.stringify(schemaError, null, 2)
309
+ );
310
+ throw schemaError;
311
+ }
312
+
313
+ // Čuvamo u Firestore
314
+ console.log("[CLINIC] Saving clinic to Firestore", {
315
+ clinicId: clinicData.id,
316
+ });
317
+ try {
318
+ await setDoc(doc(db, CLINICS_COLLECTION, clinicData.id), clinicData);
319
+ console.log("[CLINIC] Clinic saved successfully");
320
+ } catch (firestoreError) {
321
+ console.error("[CLINIC] Error saving to Firestore:", firestoreError);
322
+ throw firestoreError;
323
+ }
324
+
325
+ // Dodajemo kliniku u grupaciju
326
+ console.log("[CLINIC] Adding clinic to clinic group");
327
+ try {
328
+ const group = await clinicGroupService.getClinicGroup(
329
+ validatedData.clinicGroupId
330
+ );
331
+ if (group) {
332
+ await clinicGroupService.updateClinicGroup(
333
+ validatedData.clinicGroupId,
334
+ {
335
+ clinics: [...group.clinics, clinicData.id],
336
+ }
337
+ );
338
+ console.log("[CLINIC] Clinic added to group successfully");
339
+ }
340
+ } catch (groupUpdateError) {
341
+ console.error("[CLINIC] Error adding clinic to group:", groupUpdateError);
342
+ // Continue even if adding to group fails
343
+ }
344
+
345
+ // Dodajemo kliniku adminu koji ju je kreirao
346
+ console.log("[CLINIC] Adding clinic to admin's managed clinics");
347
+ try {
348
+ await clinicAdminService.addClinicToManaged(
349
+ creatorAdminId,
350
+ clinicData.id
351
+ );
352
+ console.log(
353
+ "[CLINIC] Clinic added to admin's managed clinics successfully"
354
+ );
355
+ } catch (adminUpdateError) {
356
+ console.error(
357
+ "[CLINIC] Error adding clinic to admin's managed clinics:",
358
+ adminUpdateError
359
+ );
360
+ // Continue even if adding to admin fails
361
+ }
362
+
363
+ console.log("[CLINIC] Clinic creation completed successfully", {
364
+ clinicId: clinicData.id,
365
+ clinicName: clinicData.name,
366
+ });
367
+ return clinicData;
368
+ } catch (error) {
369
+ if (error instanceof z.ZodError) {
370
+ console.error(
371
+ "[CLINIC] Zod validation error:",
372
+ JSON.stringify(error.errors, null, 2)
373
+ );
374
+ throw new Error("Invalid clinic data: " + error.message);
375
+ }
376
+ console.error("[CLINIC] Unhandled error in createClinic:", error);
377
+ throw error;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Gets a clinic by ID
383
+ * @param db - Firestore database instance
384
+ * @param clinicId - ID of the clinic to get
385
+ * @returns The clinic or null if not found
386
+ */
387
+ export async function getClinic(
388
+ db: Firestore,
389
+ clinicId: string
390
+ ): Promise<Clinic | null> {
391
+ const docRef = doc(db, CLINICS_COLLECTION, clinicId);
392
+ const docSnap = await getDoc(docRef);
393
+
394
+ if (docSnap.exists()) {
395
+ return docSnap.data() as Clinic;
396
+ }
397
+
398
+ return null;
399
+ }
400
+
401
+ /**
402
+ * Gets all clinics in a group
403
+ * @param db - Firestore database instance
404
+ * @param groupId - ID of the clinic group
405
+ * @returns Array of clinics in the group
406
+ */
407
+ export async function getClinicsByGroup(
408
+ db: Firestore,
409
+ groupId: string
410
+ ): Promise<Clinic[]> {
411
+ const q = query(
412
+ collection(db, CLINICS_COLLECTION),
413
+ where("clinicGroupId", "==", groupId),
414
+ where("isActive", "==", true)
415
+ );
416
+
417
+ const querySnapshot = await getDocs(q);
418
+ return querySnapshot.docs.map((doc) => doc.data() as Clinic);
419
+ }
420
+
421
+ /**
422
+ * Updates a clinic
423
+ * @param db - Firestore database instance
424
+ * @param clinicId - ID of the clinic to update
425
+ * @param data - Data to update
426
+ * @param adminId - ID of the admin making the update
427
+ * @param clinicAdminService - Service for clinic admin operations
428
+ * @param app - Firebase app instance
429
+ * @returns The updated clinic
430
+ */
431
+ export async function updateClinic(
432
+ db: Firestore,
433
+ clinicId: string,
434
+ data: Partial<Clinic>,
435
+ adminId: string,
436
+ clinicAdminService: any,
437
+ app: FirebaseApp
438
+ ): Promise<Clinic> {
439
+ console.log("[CLINIC] Starting clinic update", { clinicId, adminId });
440
+
441
+ const clinic = await getClinic(db, clinicId);
442
+ if (!clinic) {
443
+ console.error("[CLINIC] Clinic not found", { clinicId });
444
+ throw new Error("Clinic not found");
445
+ }
446
+
447
+ // Proveravamo da li admin ima prava da ažurira kliniku
448
+ try {
449
+ console.log("[CLINIC] Checking admin permissions");
450
+ const admin = await clinicAdminService.getClinicAdmin(adminId);
451
+ if (!admin) {
452
+ console.error("[CLINIC] Admin not found", { adminId });
453
+ throw new Error("Admin not found");
454
+ }
455
+
456
+ // Check if admin is either:
457
+ // 1. The owner of the clinic group that this clinic belongs to, OR
458
+ // 2. Has this clinic in their managed clinics list AND is listed in the clinic's admins array
459
+ const hasPermission =
460
+ (admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
461
+ (admin.clinicsManaged.includes(clinicId) &&
462
+ clinic.admins &&
463
+ clinic.admins.includes(adminId));
464
+
465
+ if (!hasPermission) {
466
+ console.error(
467
+ "[CLINIC] Admin does not have permission to update this clinic",
468
+ {
469
+ adminId,
470
+ clinicId,
471
+ isGroupOwner: admin.isGroupOwner,
472
+ clinicsManaged: admin.clinicsManaged,
473
+ isClinicAdmin: clinic.admins && clinic.admins.includes(adminId),
474
+ }
475
+ );
476
+ throw new Error("Admin does not have permission to update this clinic");
477
+ }
478
+ console.log("[CLINIC] Admin permissions verified");
479
+ } catch (adminError) {
480
+ console.error("[CLINIC] Error verifying admin permissions:", adminError);
481
+ throw adminError;
482
+ }
483
+
484
+ // Process photos in the update data
485
+ let updatedData = { ...data };
486
+
487
+ // Handle logo update if provided
488
+ if (
489
+ data.logo &&
490
+ typeof data.logo === "string" &&
491
+ data.logo.startsWith("data:")
492
+ ) {
493
+ console.log("[CLINIC] Processing logo update");
494
+ try {
495
+ const logoUrl = await uploadPhoto(
496
+ data.logo,
497
+ "clinics",
498
+ clinicId,
499
+ "logo",
500
+ app
501
+ );
502
+ console.log("[CLINIC] Logo update processed", { logoUrl });
503
+
504
+ if (logoUrl !== null) {
505
+ updatedData.logo = logoUrl;
506
+ }
507
+ } catch (logoError) {
508
+ console.error("[CLINIC] Error processing logo update:", logoError);
509
+ // Continue with update even if logo upload fails
510
+ }
511
+ }
512
+
513
+ // Handle cover photo update if provided
514
+ if (data.coverPhoto) {
515
+ console.log("[CLINIC] Processing cover photo update");
516
+ try {
517
+ // Check if it's a data URL that needs to be uploaded
518
+ if (
519
+ typeof data.coverPhoto === "string" &&
520
+ data.coverPhoto.startsWith("data:")
521
+ ) {
522
+ const uploadedPhoto = await uploadPhoto(
523
+ data.coverPhoto,
524
+ "clinics",
525
+ clinicId,
526
+ "cover",
527
+ app
528
+ );
529
+ if (uploadedPhoto) {
530
+ updatedData.coverPhoto = uploadedPhoto;
531
+ }
532
+ } else {
533
+ // Keep existing photo URL
534
+ updatedData.coverPhoto = data.coverPhoto;
535
+ }
536
+ console.log("[CLINIC] Cover photo update processed");
537
+ } catch (photoError) {
538
+ console.error(
539
+ "[CLINIC] Error processing cover photo update:",
540
+ photoError
541
+ );
542
+ // Keep existing photo if upload fails
543
+ if (
544
+ typeof data.coverPhoto === "string" &&
545
+ !data.coverPhoto.startsWith("data:")
546
+ ) {
547
+ updatedData.coverPhoto = data.coverPhoto;
548
+ }
549
+ }
550
+ }
551
+
552
+ // Handle featured photos update if provided
553
+ if (data.featuredPhotos && data.featuredPhotos.length > 0) {
554
+ console.log("[CLINIC] Processing featured photos update");
555
+ try {
556
+ // Filter out only data URLs that need to be uploaded
557
+ const dataUrlPhotos = data.featuredPhotos.filter(
558
+ (photo) => typeof photo === "string" && photo.startsWith("data:")
559
+ );
560
+ const existingPhotos = data.featuredPhotos.filter(
561
+ (photo) => typeof photo === "string" && !photo.startsWith("data:")
562
+ );
563
+
564
+ if (dataUrlPhotos.length > 0) {
565
+ const uploadedPhotos = await uploadMultiplePhotos(
566
+ dataUrlPhotos,
567
+ "clinics",
568
+ clinicId,
569
+ "featured",
570
+ app
571
+ );
572
+ console.log("[CLINIC] Featured photos update processed", {
573
+ count: uploadedPhotos.length,
574
+ });
575
+
576
+ // Combine existing photos with newly uploaded ones
577
+ updatedData.featuredPhotos = [...existingPhotos, ...uploadedPhotos];
578
+ } else {
579
+ // If no new photos to upload, just use the existing ones
580
+ updatedData.featuredPhotos = existingPhotos;
581
+ }
582
+ } catch (featuredError) {
583
+ console.error(
584
+ "[CLINIC] Error processing featured photos update:",
585
+ featuredError
586
+ );
587
+ // Continue with update even if featured photos upload fails
588
+ updatedData.featuredPhotos = data.featuredPhotos.filter(
589
+ (photo) => typeof photo === "string" && !photo.startsWith("data:")
590
+ );
591
+ }
592
+ }
593
+
594
+ // Handle photos with tags update if provided
595
+ if (data.photosWithTags && data.photosWithTags.length > 0) {
596
+ console.log("[CLINIC] Processing photos with tags update");
597
+ try {
598
+ const updatedPhotosWithTags = [];
599
+
600
+ // Process each photo with tag
601
+ for (const photoWithTag of data.photosWithTags) {
602
+ if (photoWithTag.url && photoWithTag.url.startsWith("data:")) {
603
+ // Upload new photo
604
+ const uploadedUrl = await uploadPhoto(
605
+ photoWithTag.url,
606
+ "clinics",
607
+ clinicId,
608
+ `tagged-${photoWithTag.tag}`,
609
+ app
610
+ );
611
+
612
+ if (uploadedUrl) {
613
+ updatedPhotosWithTags.push({
614
+ url: uploadedUrl,
615
+ tag: photoWithTag.tag,
616
+ });
617
+ }
618
+ } else {
619
+ // Keep existing photo
620
+ updatedPhotosWithTags.push(photoWithTag);
621
+ }
622
+ }
623
+
624
+ updatedData.photosWithTags = updatedPhotosWithTags;
625
+ console.log("[CLINIC] Photos with tags update processed", {
626
+ count: updatedPhotosWithTags.length,
627
+ });
628
+ } catch (tagsError) {
629
+ console.error(
630
+ "[CLINIC] Error processing photos with tags update:",
631
+ tagsError
632
+ );
633
+ // Continue with update even if photos with tags upload fails
634
+ updatedData.photosWithTags = data.photosWithTags.filter(
635
+ (photo) => !photo.url.startsWith("data:")
636
+ );
637
+ }
638
+ }
639
+
640
+ // Add timestamp
641
+ updatedData = {
642
+ ...updatedData,
643
+ updatedAt: Timestamp.now(),
644
+ };
645
+
646
+ console.log("[CLINIC] Updating clinic in Firestore");
647
+ try {
648
+ await updateDoc(doc(db, CLINICS_COLLECTION, clinicId), updatedData);
649
+ console.log("[CLINIC] Clinic updated successfully");
650
+ } catch (updateError) {
651
+ console.error("[CLINIC] Error updating clinic in Firestore:", updateError);
652
+ throw updateError;
653
+ }
654
+
655
+ // Return updated data
656
+ const updatedClinic = await getClinic(db, clinicId);
657
+ if (!updatedClinic) {
658
+ console.error("[CLINIC] Failed to retrieve updated clinic");
659
+ throw new Error("Failed to retrieve updated clinic");
660
+ }
661
+
662
+ console.log("[CLINIC] Clinic update completed successfully");
663
+ return updatedClinic;
664
+ }
665
+
666
+ /**
667
+ * Deactivates a clinic
668
+ * @param db - Firestore database instance
669
+ * @param clinicId - ID of the clinic to deactivate
670
+ * @param adminId - ID of the admin making the deactivation
671
+ * @param clinicAdminService - Service for clinic admin operations
672
+ */
673
+ export async function deactivateClinic(
674
+ db: Firestore,
675
+ clinicId: string,
676
+ adminId: string,
677
+ clinicAdminService: any
678
+ ): Promise<void> {
679
+ const clinic = await getClinic(db, clinicId);
680
+ if (!clinic) {
681
+ throw new Error("Clinic not found");
682
+ }
683
+
684
+ // Proveravamo da li admin ima prava da deaktivira kliniku
685
+ const admin = await clinicAdminService.getClinicAdmin(adminId);
686
+ if (!admin) {
687
+ throw new Error("Admin not found");
688
+ }
689
+
690
+ // Check if admin is either:
691
+ // 1. The owner of the clinic group, OR
692
+ // 2. Has this clinic in their managed clinics list, OR
693
+ // 3. Is listed in the clinic's admins array
694
+ const hasPermission =
695
+ (admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
696
+ (admin.clinicsManaged.includes(clinicId) &&
697
+ clinic.admins &&
698
+ clinic.admins.includes(adminId));
699
+
700
+ if (!hasPermission) {
701
+ throw new Error("Admin does not have permission to deactivate this clinic");
702
+ }
703
+
704
+ await updateDoc(doc(db, CLINICS_COLLECTION, clinicId), {
705
+ isActive: false,
706
+ updatedAt: Timestamp.now(),
707
+ });
708
+ }
709
+
710
+ /**
711
+ * Gets clinics managed by an admin
712
+ * @param db - Firestore database instance
713
+ * @param adminId - ID of the admin
714
+ * @param options - Options for filtering clinics
715
+ * @param clinicAdminService - Service for clinic admin operations
716
+ * @param clinicGroupService - Service for clinic group operations
717
+ * @returns Array of clinics managed by the admin
718
+ */
719
+ export async function getClinicsByAdmin(
720
+ db: Firestore,
721
+ adminId: string,
722
+ options: {
723
+ isActive?: boolean;
724
+ includeGroupClinics?: boolean;
725
+ } = {},
726
+ clinicAdminService: any,
727
+ clinicGroupService: any
728
+ ): Promise<Clinic[]> {
729
+ const admin = await clinicAdminService.getClinicAdmin(adminId);
730
+ if (!admin) {
731
+ throw new Error("Admin not found");
732
+ }
733
+
734
+ let clinicIds: string[] = [...admin.clinicsManaged];
735
+
736
+ // Ako je admin vlasnik grupe i tražimo sve klinike grupe
737
+ if (admin.isGroupOwner && options.includeGroupClinics) {
738
+ const group = await clinicGroupService.getClinicGroup(admin.clinicGroupId);
739
+ if (group) {
740
+ clinicIds = [...new Set([...clinicIds, ...group.clinics])];
741
+ }
742
+ }
743
+
744
+ if (clinicIds.length === 0) {
745
+ return [];
746
+ }
747
+
748
+ const constraints: QueryConstraint[] = [where("id", "in", clinicIds)];
749
+
750
+ if (options.isActive !== undefined) {
751
+ constraints.push(where("isActive", "==", options.isActive));
752
+ }
753
+
754
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
755
+ const querySnapshot = await getDocs(q);
756
+
757
+ return querySnapshot.docs.map((doc) => doc.data() as Clinic);
758
+ }
759
+
760
+ /**
761
+ * Gets active clinics managed by an admin
762
+ * @param db - Firestore database instance
763
+ * @param adminId - ID of the admin
764
+ * @param clinicAdminService - Service for clinic admin operations
765
+ * @param clinicGroupService - Service for clinic group operations
766
+ * @returns Array of active clinics managed by the admin
767
+ */
768
+ export async function getActiveClinicsByAdmin(
769
+ db: Firestore,
770
+ adminId: string,
771
+ clinicAdminService: any,
772
+ clinicGroupService: any
773
+ ): Promise<Clinic[]> {
774
+ return getClinicsByAdmin(
775
+ db,
776
+ adminId,
777
+ { isActive: true },
778
+ clinicAdminService,
779
+ clinicGroupService
780
+ );
781
+ }
782
+
783
+ /**
784
+ * Retrieves a clinic by its ID
785
+ *
786
+ * @param db - Firestore database instance
787
+ * @param clinicId - ID of the clinic to retrieve
788
+ * @returns The clinic if found, null otherwise
789
+ */
790
+ export async function getClinicById(
791
+ db: Firestore,
792
+ clinicId: string
793
+ ): Promise<Clinic | null> {
794
+ try {
795
+ const clinicRef = doc(db, CLINICS_COLLECTION, clinicId);
796
+ const clinicSnapshot = await getDoc(clinicRef);
797
+
798
+ if (!clinicSnapshot.exists()) {
799
+ return null;
800
+ }
801
+
802
+ const clinicData = clinicSnapshot.data() as Clinic;
803
+ return {
804
+ ...clinicData,
805
+ id: clinicSnapshot.id,
806
+ };
807
+ } catch (error) {
808
+ console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
809
+ throw error;
810
+ }
811
+ }
812
+
813
+ /**
814
+ * Retrieves all clinics with optional pagination
815
+ *
816
+ * @param db - Firestore database instance
817
+ * @param pagination - Optional number of clinics per page (0 or undefined returns all)
818
+ * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
819
+ * @returns Array of clinics and the last document for pagination
820
+ */
821
+ export async function getAllClinics(
822
+ db: Firestore,
823
+ pagination?: number,
824
+ lastDoc?: any
825
+ ): Promise<{ clinics: Clinic[]; lastDoc: any }> {
826
+ try {
827
+ const clinicsCollection = collection(db, CLINICS_COLLECTION);
828
+ let clinicsQuery = query(clinicsCollection);
829
+
830
+ // If pagination is specified and greater than 0, limit the query
831
+ if (pagination && pagination > 0) {
832
+ if (lastDoc) {
833
+ clinicsQuery = query(
834
+ clinicsCollection,
835
+ startAfter(lastDoc),
836
+ limit(pagination)
837
+ );
838
+ } else {
839
+ clinicsQuery = query(clinicsCollection, limit(pagination));
840
+ }
841
+ }
842
+
843
+ const clinicsSnapshot = await getDocs(clinicsQuery);
844
+ const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
845
+
846
+ const clinics = clinicsSnapshot.docs.map((doc) => {
847
+ const data = doc.data() as Clinic;
848
+ return {
849
+ ...data,
850
+ id: doc.id,
851
+ };
852
+ });
853
+
854
+ return {
855
+ clinics,
856
+ lastDoc: lastVisible,
857
+ };
858
+ } catch (error) {
859
+ console.error("[CLINIC_UTILS] Error getting all clinics:", error);
860
+ throw error;
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Retrieves all clinics within a specified range from a location with optional pagination
866
+ *
867
+ * @param db - Firestore database instance
868
+ * @param center - The center location coordinates {latitude, longitude}
869
+ * @param rangeInKm - The range in kilometers to search within
870
+ * @param pagination - Optional number of clinics per page (0 or undefined returns all)
871
+ * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
872
+ * @returns Array of clinics with distance information and the last document for pagination
873
+ */
874
+ export async function getAllClinicsInRange(
875
+ db: Firestore,
876
+ center: { latitude: number; longitude: number },
877
+ rangeInKm: number,
878
+ pagination?: number,
879
+ lastDoc?: any
880
+ ): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
881
+ const bounds = geohashQueryBounds(
882
+ [center.latitude, center.longitude],
883
+ rangeInKm * 1000
884
+ );
885
+
886
+ const matchingClinics: (Clinic & { distance: number })[] = [];
887
+ let lastDocSnapshot = null;
888
+
889
+ for (const b of bounds) {
890
+ const constraints: QueryConstraint[] = [
891
+ where("location.geohash", ">=", b[0]),
892
+ where("location.geohash", "<=", b[1]),
893
+ where("isActive", "==", true),
894
+ ];
895
+
896
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
897
+ const querySnapshot = await getDocs(q);
898
+
899
+ for (const doc of querySnapshot.docs) {
900
+ const clinic = doc.data() as Clinic;
901
+ const distance = distanceBetween(
902
+ [center.latitude, center.longitude],
903
+ [clinic.location.latitude, clinic.location.longitude]
904
+ );
905
+
906
+ // Convert to kilometers
907
+ const distanceInKm = distance / 1000;
908
+
909
+ if (distanceInKm <= rangeInKm) {
910
+ matchingClinics.push({
911
+ ...clinic,
912
+ distance: distanceInKm,
913
+ });
914
+ }
915
+ }
916
+ }
917
+
918
+ // Sort by distance
919
+ matchingClinics.sort((a, b) => a.distance - b.distance);
920
+
921
+ if (pagination && pagination > 0) {
922
+ // Paginate results
923
+ let result = matchingClinics;
924
+ if (lastDoc && matchingClinics.length > 0) {
925
+ const lastIndex = matchingClinics.findIndex(
926
+ (clinic) => clinic.id === lastDoc.id
927
+ );
928
+ if (lastIndex !== -1) {
929
+ result = matchingClinics.slice(lastIndex + 1);
930
+ }
931
+ }
932
+
933
+ const paginatedClinics = result.slice(0, pagination);
934
+ const newLastDoc =
935
+ paginatedClinics.length > 0
936
+ ? paginatedClinics[paginatedClinics.length - 1]
937
+ : null;
938
+
939
+ return {
940
+ clinics: paginatedClinics,
941
+ lastDoc: newLastDoc,
942
+ };
943
+ }
944
+
945
+ return {
946
+ clinics: matchingClinics,
947
+ lastDoc: null,
948
+ };
949
+ }