@blackcode_sa/metaestetics-api 1.13.5 → 1.13.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/index.d.mts +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/package.json +121 -121
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +81 -81
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +67 -67
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +14 -14
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2200 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -498
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +494 -494
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -195
- package/src/validations/schemas.ts +104 -104
- 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
|
+
}
|