@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,708 +1,708 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collection,
|
|
3
|
-
doc,
|
|
4
|
-
getDoc,
|
|
5
|
-
getDocs,
|
|
6
|
-
query,
|
|
7
|
-
where,
|
|
8
|
-
updateDoc,
|
|
9
|
-
setDoc,
|
|
10
|
-
deleteDoc,
|
|
11
|
-
Timestamp,
|
|
12
|
-
serverTimestamp,
|
|
13
|
-
GeoPoint,
|
|
14
|
-
QueryConstraint,
|
|
15
|
-
FieldValue,
|
|
16
|
-
writeBatch,
|
|
17
|
-
arrayUnion,
|
|
18
|
-
arrayRemove,
|
|
19
|
-
} from "firebase/firestore";
|
|
20
|
-
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
21
|
-
import tzlookup from "tz-lookup";
|
|
22
|
-
import { BaseService } from "../base.service";
|
|
23
|
-
import {
|
|
24
|
-
Clinic,
|
|
25
|
-
CreateClinicData,
|
|
26
|
-
CLINICS_COLLECTION,
|
|
27
|
-
ClinicTag,
|
|
28
|
-
ClinicTags,
|
|
29
|
-
ClinicGroup,
|
|
30
|
-
CLINIC_GROUPS_COLLECTION,
|
|
31
|
-
ClinicBranchSetupData,
|
|
32
|
-
CLINIC_ADMINS_COLLECTION,
|
|
33
|
-
DoctorInfo,
|
|
34
|
-
} from "../../types/clinic";
|
|
35
|
-
// Correct imports
|
|
36
|
-
import { ProcedureSummaryInfo } from "../../types/procedure";
|
|
37
|
-
import { ClinicInfo } from "../../types/profile";
|
|
38
|
-
import { ClinicGroupService } from "./clinic-group.service";
|
|
39
|
-
import { ClinicAdminService } from "./clinic-admin.service";
|
|
40
|
-
import {
|
|
41
|
-
geohashForLocation,
|
|
42
|
-
geohashQueryBounds,
|
|
43
|
-
distanceBetween,
|
|
44
|
-
} from "geofire-common";
|
|
45
|
-
import {
|
|
46
|
-
clinicSchema,
|
|
47
|
-
createClinicSchema,
|
|
48
|
-
updateClinicSchema,
|
|
49
|
-
clinicBranchSetupSchema,
|
|
50
|
-
} from "../../validations/clinic.schema";
|
|
51
|
-
import { z } from "zod";
|
|
52
|
-
import { Auth } from "firebase/auth";
|
|
53
|
-
import { Firestore } from "firebase/firestore";
|
|
54
|
-
import { FirebaseApp } from "firebase/app";
|
|
55
|
-
import * as ClinicUtils from "./utils/clinic.utils";
|
|
56
|
-
import * as TagUtils from "./utils/tag.utils";
|
|
57
|
-
import * as SearchUtils from "./utils/search.utils";
|
|
58
|
-
import * as AdminUtils from "./utils/admin.utils";
|
|
59
|
-
import * as FilterUtils from "./utils/filter.utils";
|
|
60
|
-
import { ClinicReviewInfo } from "../../types/reviews";
|
|
61
|
-
import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
|
|
62
|
-
import { MediaService, MediaAccessLevel } from "../media/media.service";
|
|
63
|
-
|
|
64
|
-
export class ClinicService extends BaseService {
|
|
65
|
-
private clinicGroupService: ClinicGroupService;
|
|
66
|
-
private clinicAdminService: ClinicAdminService;
|
|
67
|
-
private mediaService: MediaService;
|
|
68
|
-
private functions: any;
|
|
69
|
-
|
|
70
|
-
constructor(
|
|
71
|
-
db: Firestore,
|
|
72
|
-
auth: Auth,
|
|
73
|
-
app: FirebaseApp,
|
|
74
|
-
clinicGroupService: ClinicGroupService,
|
|
75
|
-
clinicAdminService: ClinicAdminService,
|
|
76
|
-
mediaService: MediaService
|
|
77
|
-
) {
|
|
78
|
-
super(db, auth, app);
|
|
79
|
-
this.clinicAdminService = clinicAdminService;
|
|
80
|
-
this.clinicGroupService = clinicGroupService;
|
|
81
|
-
this.mediaService = mediaService;
|
|
82
|
-
this.functions = getFunctions(app, "europe-west6"); // All functions now in europe-west6
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Get timezone from coordinates using tz-lookup library
|
|
87
|
-
* @param lat Latitude
|
|
88
|
-
* @param lng Longitude
|
|
89
|
-
* @returns IANA timezone string
|
|
90
|
-
*/
|
|
91
|
-
private getTimezone(lat: number, lng: number): string | null {
|
|
92
|
-
try {
|
|
93
|
-
const timezone = tzlookup(lat, lng);
|
|
94
|
-
return timezone || null;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
console.error("[CLINIC_SERVICE] Error getting timezone:", error);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Process media resource (string URL or File object)
|
|
103
|
-
* @param media String URL or File object
|
|
104
|
-
* @param ownerId Owner ID for the media (usually clinicId)
|
|
105
|
-
* @param collectionName Collection name for organizing files
|
|
106
|
-
* @returns URL string after processing
|
|
107
|
-
*/
|
|
108
|
-
private async processMedia(
|
|
109
|
-
media: string | File | Blob | null | undefined,
|
|
110
|
-
ownerId: string,
|
|
111
|
-
collectionName: string
|
|
112
|
-
): Promise<string | null> {
|
|
113
|
-
if (!media) return null;
|
|
114
|
-
|
|
115
|
-
// If already a string URL, return it directly
|
|
116
|
-
if (typeof media === "string") {
|
|
117
|
-
return media;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// If it's a File, upload it using MediaService
|
|
121
|
-
if (media instanceof File || media instanceof Blob) {
|
|
122
|
-
console.log(
|
|
123
|
-
`[ClinicService] Uploading ${collectionName} media for ${ownerId}`
|
|
124
|
-
);
|
|
125
|
-
const metadata = await this.mediaService.uploadMedia(
|
|
126
|
-
media,
|
|
127
|
-
ownerId,
|
|
128
|
-
MediaAccessLevel.PUBLIC,
|
|
129
|
-
collectionName
|
|
130
|
-
);
|
|
131
|
-
return metadata.url;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Process array of media resources (strings or Files)
|
|
139
|
-
* @param mediaArray Array of string URLs or File objects
|
|
140
|
-
* @param ownerId Owner ID for the media
|
|
141
|
-
* @param collectionName Collection name for organizing files
|
|
142
|
-
* @returns Array of URL strings after processing
|
|
143
|
-
*/
|
|
144
|
-
private async processMediaArray(
|
|
145
|
-
mediaArray: (string | File | Blob)[] | undefined,
|
|
146
|
-
ownerId: string,
|
|
147
|
-
collectionName: string
|
|
148
|
-
): Promise<string[]> {
|
|
149
|
-
if (!mediaArray || mediaArray.length === 0) return [];
|
|
150
|
-
|
|
151
|
-
const result: string[] = [];
|
|
152
|
-
|
|
153
|
-
for (const media of mediaArray) {
|
|
154
|
-
const processedUrl = await this.processMedia(
|
|
155
|
-
media,
|
|
156
|
-
ownerId,
|
|
157
|
-
collectionName
|
|
158
|
-
);
|
|
159
|
-
if (processedUrl) {
|
|
160
|
-
result.push(processedUrl);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return result;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Process photos with tags array
|
|
169
|
-
* @param photosWithTags Array of objects containing media and tag
|
|
170
|
-
* @param ownerId Owner ID for the media
|
|
171
|
-
* @param collectionName Collection name for organizing files
|
|
172
|
-
* @returns Processed array with URL strings
|
|
173
|
-
*/
|
|
174
|
-
private async processPhotosWithTags(
|
|
175
|
-
photosWithTags: { url: string | File | Blob; tag: string }[] | undefined,
|
|
176
|
-
ownerId: string,
|
|
177
|
-
collectionName: string
|
|
178
|
-
): Promise<{ url: string; tag: string }[]> {
|
|
179
|
-
if (!photosWithTags || photosWithTags.length === 0) return [];
|
|
180
|
-
|
|
181
|
-
const result: { url: string; tag: string }[] = [];
|
|
182
|
-
|
|
183
|
-
for (const item of photosWithTags) {
|
|
184
|
-
const processedUrl = await this.processMedia(
|
|
185
|
-
item.url,
|
|
186
|
-
ownerId,
|
|
187
|
-
collectionName
|
|
188
|
-
);
|
|
189
|
-
if (processedUrl) {
|
|
190
|
-
result.push({
|
|
191
|
-
url: processedUrl,
|
|
192
|
-
tag: item.tag,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return result;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Creates a new clinic.
|
|
202
|
-
* Handles both URL strings and File uploads for media fields.
|
|
203
|
-
*/
|
|
204
|
-
async createClinic(
|
|
205
|
-
data: CreateClinicData,
|
|
206
|
-
creatorAdminId: string
|
|
207
|
-
): Promise<Clinic> {
|
|
208
|
-
try {
|
|
209
|
-
// Generate ID first so we can use it for media uploads
|
|
210
|
-
const clinicId = this.generateId();
|
|
211
|
-
|
|
212
|
-
// Validate data - this now works because mediaResourceSchema has been updated to support Files/Blobs
|
|
213
|
-
const validatedData = createClinicSchema.parse(data);
|
|
214
|
-
|
|
215
|
-
const group = await this.clinicGroupService.getClinicGroup(
|
|
216
|
-
validatedData.clinicGroupId
|
|
217
|
-
);
|
|
218
|
-
if (!group) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
`Clinic group ${validatedData.clinicGroupId} not found`
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Process media fields - convert File/Blob objects to URLs
|
|
225
|
-
const logoUrl = await this.processMedia(
|
|
226
|
-
validatedData.logo,
|
|
227
|
-
clinicId,
|
|
228
|
-
"clinic-logos"
|
|
229
|
-
);
|
|
230
|
-
const coverPhotoUrl = await this.processMedia(
|
|
231
|
-
validatedData.coverPhoto,
|
|
232
|
-
clinicId,
|
|
233
|
-
"clinic-cover-photos"
|
|
234
|
-
);
|
|
235
|
-
const featuredPhotos = await this.processMediaArray(
|
|
236
|
-
validatedData.featuredPhotos,
|
|
237
|
-
clinicId,
|
|
238
|
-
"clinic-featured-photos"
|
|
239
|
-
);
|
|
240
|
-
const photosWithTags = await this.processPhotosWithTags(
|
|
241
|
-
validatedData.photosWithTags,
|
|
242
|
-
clinicId,
|
|
243
|
-
"clinic-gallery"
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
const location = validatedData.location;
|
|
247
|
-
const hash = geohashForLocation([location.latitude, location.longitude]);
|
|
248
|
-
const tz = this.getTimezone(location.latitude, location.longitude);
|
|
249
|
-
console.log("🏥 Clinic timezone:", tz);
|
|
250
|
-
const defaultReviewInfo: ClinicReviewInfo = {
|
|
251
|
-
totalReviews: 0,
|
|
252
|
-
averageRating: 0,
|
|
253
|
-
cleanliness: 0,
|
|
254
|
-
facilities: 0,
|
|
255
|
-
staffFriendliness: 0,
|
|
256
|
-
waitingTime: 0,
|
|
257
|
-
accessibility: 0,
|
|
258
|
-
recommendationPercentage: 0,
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const clinicData: Omit<Clinic, "createdAt" | "updatedAt"> & {
|
|
262
|
-
createdAt: FieldValue;
|
|
263
|
-
updatedAt: FieldValue;
|
|
264
|
-
} = {
|
|
265
|
-
id: clinicId,
|
|
266
|
-
clinicGroupId: validatedData.clinicGroupId,
|
|
267
|
-
name: validatedData.name,
|
|
268
|
-
nameLower: validatedData.name.toLowerCase(), // Add this line
|
|
269
|
-
description: validatedData.description,
|
|
270
|
-
location: { ...location, geohash: hash, tz },
|
|
271
|
-
contactInfo: validatedData.contactInfo,
|
|
272
|
-
workingHours: validatedData.workingHours,
|
|
273
|
-
tags: validatedData.tags,
|
|
274
|
-
featuredPhotos: featuredPhotos,
|
|
275
|
-
coverPhoto: coverPhotoUrl,
|
|
276
|
-
photosWithTags: photosWithTags,
|
|
277
|
-
doctors: validatedData.doctors || [],
|
|
278
|
-
procedures: validatedData.procedures || [],
|
|
279
|
-
doctorsInfo: [],
|
|
280
|
-
proceduresInfo: validatedData.proceduresInfo || [],
|
|
281
|
-
reviewInfo: defaultReviewInfo,
|
|
282
|
-
admins: [creatorAdminId],
|
|
283
|
-
isActive:
|
|
284
|
-
validatedData.isActive !== undefined ? validatedData.isActive : true,
|
|
285
|
-
isVerified:
|
|
286
|
-
validatedData.isVerified !== undefined
|
|
287
|
-
? validatedData.isVerified
|
|
288
|
-
: false,
|
|
289
|
-
logo: logoUrl,
|
|
290
|
-
createdAt: serverTimestamp(),
|
|
291
|
-
updatedAt: serverTimestamp(),
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// We can validate the final object with URLs using clinicSchema which also supports mediaResourceSchema
|
|
295
|
-
// However, we need to be careful with timestamps
|
|
296
|
-
// The validation below is optional and can be uncommented if needed
|
|
297
|
-
/*
|
|
298
|
-
clinicSchema.parse({
|
|
299
|
-
...clinicData,
|
|
300
|
-
createdAt: Timestamp.now(),
|
|
301
|
-
updatedAt: Timestamp.now(),
|
|
302
|
-
});
|
|
303
|
-
*/
|
|
304
|
-
|
|
305
|
-
const batch = writeBatch(this.db);
|
|
306
|
-
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
307
|
-
batch.set(clinicRef, clinicData);
|
|
308
|
-
|
|
309
|
-
const adminRef = doc(this.db, CLINIC_ADMINS_COLLECTION, creatorAdminId);
|
|
310
|
-
batch.update(adminRef, {
|
|
311
|
-
clinicsManaged: arrayUnion(clinicId),
|
|
312
|
-
updatedAt: serverTimestamp(),
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
await batch.commit();
|
|
316
|
-
console.log(`[ClinicService] Clinic created successfully: ${clinicId}`);
|
|
317
|
-
|
|
318
|
-
const savedClinic = await this.getClinic(clinicId);
|
|
319
|
-
if (!savedClinic) throw new Error("Failed to retrieve created clinic");
|
|
320
|
-
return savedClinic;
|
|
321
|
-
} catch (error) {
|
|
322
|
-
if (error instanceof z.ZodError) {
|
|
323
|
-
throw new Error("Invalid clinic data: " + error.message);
|
|
324
|
-
}
|
|
325
|
-
console.error("Error creating clinic:", error);
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Updates a clinic.
|
|
332
|
-
* Handles both URL strings and File uploads for media fields.
|
|
333
|
-
*/
|
|
334
|
-
async updateClinic(
|
|
335
|
-
clinicId: string,
|
|
336
|
-
data: Partial<CreateClinicData>,
|
|
337
|
-
adminId: string
|
|
338
|
-
): Promise<Clinic> {
|
|
339
|
-
try {
|
|
340
|
-
// First check if clinic exists
|
|
341
|
-
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
342
|
-
const clinicDoc = await getDoc(clinicRef);
|
|
343
|
-
|
|
344
|
-
if (!clinicDoc.exists()) {
|
|
345
|
-
throw new Error(`Clinic ${clinicId} not found`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const currentClinic = clinicDoc.data() as Clinic;
|
|
349
|
-
|
|
350
|
-
// Validate update data - this works because updateClinicSchema supports Files/Blobs
|
|
351
|
-
const validatedData = updateClinicSchema.parse(data);
|
|
352
|
-
|
|
353
|
-
const updatePayload: Record<string, any> = {};
|
|
354
|
-
|
|
355
|
-
// Process media fields if provided
|
|
356
|
-
if (validatedData.logo !== undefined) {
|
|
357
|
-
updatePayload.logo = await this.processMedia(
|
|
358
|
-
validatedData.logo,
|
|
359
|
-
clinicId,
|
|
360
|
-
"clinic-logos"
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (validatedData.coverPhoto !== undefined) {
|
|
365
|
-
updatePayload.coverPhoto = await this.processMedia(
|
|
366
|
-
validatedData.coverPhoto,
|
|
367
|
-
clinicId,
|
|
368
|
-
"clinic-cover-photos"
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (validatedData.featuredPhotos !== undefined) {
|
|
373
|
-
updatePayload.featuredPhotos = await this.processMediaArray(
|
|
374
|
-
validatedData.featuredPhotos,
|
|
375
|
-
clinicId,
|
|
376
|
-
"clinic-featured-photos"
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (validatedData.photosWithTags !== undefined) {
|
|
381
|
-
updatePayload.photosWithTags = await this.processPhotosWithTags(
|
|
382
|
-
validatedData.photosWithTags,
|
|
383
|
-
clinicId,
|
|
384
|
-
"clinic-gallery"
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Process non-media fields
|
|
389
|
-
const fieldsToUpdate = [
|
|
390
|
-
"name",
|
|
391
|
-
"description",
|
|
392
|
-
"contactInfo",
|
|
393
|
-
"workingHours",
|
|
394
|
-
"tags",
|
|
395
|
-
"doctors",
|
|
396
|
-
"procedures",
|
|
397
|
-
"proceduresInfo",
|
|
398
|
-
"isActive",
|
|
399
|
-
"isVerified",
|
|
400
|
-
];
|
|
401
|
-
|
|
402
|
-
for (const field of fieldsToUpdate) {
|
|
403
|
-
if (validatedData[field as keyof typeof validatedData] !== undefined) {
|
|
404
|
-
updatePayload[field] =
|
|
405
|
-
validatedData[field as keyof typeof validatedData];
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Always update nameLower if name is changed
|
|
410
|
-
if (validatedData.name) {
|
|
411
|
-
updatePayload.nameLower = validatedData.name.toLowerCase();
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Handle location update with geohash
|
|
415
|
-
if (validatedData.location) {
|
|
416
|
-
const loc = validatedData.location;
|
|
417
|
-
const tz = this.getTimezone(loc.latitude, loc.longitude);
|
|
418
|
-
updatePayload.location = {
|
|
419
|
-
...loc,
|
|
420
|
-
geohash: geohashForLocation([loc.latitude, loc.longitude]),
|
|
421
|
-
tz,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Add timestamp
|
|
426
|
-
updatePayload.updatedAt = serverTimestamp();
|
|
427
|
-
|
|
428
|
-
// Update the clinic
|
|
429
|
-
await updateDoc(clinicRef, updatePayload);
|
|
430
|
-
console.log(`[ClinicService] Clinic ${clinicId} updated successfully`);
|
|
431
|
-
|
|
432
|
-
// Return the updated clinic
|
|
433
|
-
const updatedClinic = await this.getClinic(clinicId);
|
|
434
|
-
if (!updatedClinic) throw new Error("Failed to retrieve updated clinic");
|
|
435
|
-
return updatedClinic;
|
|
436
|
-
} catch (error) {
|
|
437
|
-
if (error instanceof z.ZodError) {
|
|
438
|
-
throw new Error("Invalid clinic update data: " + error.message);
|
|
439
|
-
}
|
|
440
|
-
console.error(`Error updating clinic ${clinicId}:`, error);
|
|
441
|
-
throw error;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Deactivates a clinic.
|
|
447
|
-
*/
|
|
448
|
-
async deactivateClinic(clinicId: string, adminId: string): Promise<void> {
|
|
449
|
-
// Permission check omitted
|
|
450
|
-
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
451
|
-
await updateDoc(clinicRef, {
|
|
452
|
-
isActive: false,
|
|
453
|
-
updatedAt: serverTimestamp(),
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* Activates a clinic.
|
|
459
|
-
*/
|
|
460
|
-
async activateClinic(clinicId: string, adminId: string): Promise<void> {
|
|
461
|
-
// Permission check omitted
|
|
462
|
-
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
463
|
-
await updateDoc(clinicRef, {
|
|
464
|
-
isActive: true,
|
|
465
|
-
updatedAt: serverTimestamp(),
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Dohvata kliniku po ID-u
|
|
471
|
-
*/
|
|
472
|
-
async getClinic(clinicId: string): Promise<Clinic | null> {
|
|
473
|
-
return ClinicUtils.getClinic(this.db, clinicId);
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/**
|
|
477
|
-
* Dohvata sve klinike u grupi
|
|
478
|
-
*/
|
|
479
|
-
async getClinicsByGroup(groupId: string): Promise<Clinic[]> {
|
|
480
|
-
return ClinicUtils.getClinicsByGroup(this.db, groupId);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Pretražuje klinike u određenom radijusu
|
|
485
|
-
*/
|
|
486
|
-
async findClinicsInRadius(
|
|
487
|
-
center: { latitude: number; longitude: number },
|
|
488
|
-
radiusInKm: number,
|
|
489
|
-
filters?: {
|
|
490
|
-
procedures?: string[];
|
|
491
|
-
tags?: ClinicTag[];
|
|
492
|
-
// Add other relevant filters based on Clinic/ProcedureSummaryInfo fields
|
|
493
|
-
}
|
|
494
|
-
): Promise<Clinic[]> {
|
|
495
|
-
return SearchUtils.findClinicsInRadius(
|
|
496
|
-
this.db,
|
|
497
|
-
center,
|
|
498
|
-
radiusInKm,
|
|
499
|
-
filters
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async addTags(
|
|
504
|
-
clinicId: string,
|
|
505
|
-
adminId: string,
|
|
506
|
-
newTags: {
|
|
507
|
-
tags?: ClinicTag[];
|
|
508
|
-
}
|
|
509
|
-
): Promise<Clinic> {
|
|
510
|
-
return TagUtils.addTags(
|
|
511
|
-
this.db,
|
|
512
|
-
clinicId,
|
|
513
|
-
adminId,
|
|
514
|
-
newTags,
|
|
515
|
-
this.clinicAdminService,
|
|
516
|
-
this.app
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async removeTags(
|
|
521
|
-
clinicId: string,
|
|
522
|
-
adminId: string,
|
|
523
|
-
tagsToRemove: {
|
|
524
|
-
tags?: ClinicTag[];
|
|
525
|
-
}
|
|
526
|
-
): Promise<Clinic> {
|
|
527
|
-
return TagUtils.removeTags(
|
|
528
|
-
this.db,
|
|
529
|
-
clinicId,
|
|
530
|
-
adminId,
|
|
531
|
-
tagsToRemove,
|
|
532
|
-
this.clinicAdminService,
|
|
533
|
-
this.app
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
async getClinicsByAdmin(
|
|
538
|
-
adminId: string,
|
|
539
|
-
options?: {
|
|
540
|
-
isActive?: boolean;
|
|
541
|
-
includeGroupClinics?: boolean; // ako je true i admin je owner, uključuje sve klinike grupe
|
|
542
|
-
}
|
|
543
|
-
): Promise<Clinic[]> {
|
|
544
|
-
return ClinicUtils.getClinicsByAdmin(
|
|
545
|
-
this.db,
|
|
546
|
-
adminId,
|
|
547
|
-
options,
|
|
548
|
-
this.clinicAdminService,
|
|
549
|
-
this.clinicGroupService
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
async getActiveClinicsByAdmin(adminId: string): Promise<Clinic[]> {
|
|
554
|
-
return ClinicUtils.getActiveClinicsByAdmin(
|
|
555
|
-
this.db,
|
|
556
|
-
adminId,
|
|
557
|
-
this.clinicAdminService,
|
|
558
|
-
this.clinicGroupService
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Creates a clinic branch from setup data.
|
|
564
|
-
* Handles both URL strings and File uploads for media fields.
|
|
565
|
-
*/
|
|
566
|
-
async createClinicBranch(
|
|
567
|
-
clinicGroupId: string,
|
|
568
|
-
setupData: ClinicBranchSetupData,
|
|
569
|
-
adminId: string
|
|
570
|
-
): Promise<Clinic> {
|
|
571
|
-
console.log("[CLINIC_SERVICE] Starting clinic branch creation", {
|
|
572
|
-
clinicGroupId,
|
|
573
|
-
adminId,
|
|
574
|
-
});
|
|
575
|
-
|
|
576
|
-
// Verify clinic group exists
|
|
577
|
-
const clinicGroup = await this.clinicGroupService.getClinicGroup(
|
|
578
|
-
clinicGroupId
|
|
579
|
-
);
|
|
580
|
-
if (!clinicGroup) {
|
|
581
|
-
console.error("[CLINIC_SERVICE] Clinic group not found", {
|
|
582
|
-
clinicGroupId,
|
|
583
|
-
});
|
|
584
|
-
throw new Error(`Clinic group with ID ${clinicGroupId} not found`);
|
|
585
|
-
}
|
|
586
|
-
console.log("[CLINIC_SERVICE] Clinic group verified");
|
|
587
|
-
|
|
588
|
-
// Validate branch setup data first
|
|
589
|
-
const validatedSetupData = clinicBranchSetupSchema.parse(setupData);
|
|
590
|
-
|
|
591
|
-
// Convert validated setup data to CreateClinicData
|
|
592
|
-
const createClinicData: CreateClinicData = {
|
|
593
|
-
clinicGroupId,
|
|
594
|
-
name: validatedSetupData.name,
|
|
595
|
-
description: validatedSetupData.description,
|
|
596
|
-
location: validatedSetupData.location,
|
|
597
|
-
contactInfo: validatedSetupData.contactInfo,
|
|
598
|
-
workingHours: validatedSetupData.workingHours,
|
|
599
|
-
tags: validatedSetupData.tags || [],
|
|
600
|
-
// Pass the media fields which can be string URLs or File objects
|
|
601
|
-
logo: validatedSetupData.logo,
|
|
602
|
-
coverPhoto: validatedSetupData.coverPhoto,
|
|
603
|
-
featuredPhotos: validatedSetupData.featuredPhotos,
|
|
604
|
-
photosWithTags: validatedSetupData.photosWithTags,
|
|
605
|
-
doctors: [],
|
|
606
|
-
procedures: [],
|
|
607
|
-
admins: [adminId],
|
|
608
|
-
isActive: true,
|
|
609
|
-
isVerified: false,
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
console.log("[CLINIC_SERVICE] Creating clinic branch", {
|
|
613
|
-
name: createClinicData.name,
|
|
614
|
-
hasLogo: !!createClinicData.logo,
|
|
615
|
-
hasCoverPhoto: !!createClinicData.coverPhoto,
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
// Use createClinic which now handles validation and media uploads
|
|
619
|
-
const clinic = await this.createClinic(createClinicData, adminId);
|
|
620
|
-
|
|
621
|
-
console.log("[CLINIC_SERVICE] Clinic branch created successfully", {
|
|
622
|
-
clinicId: clinic.id,
|
|
623
|
-
});
|
|
624
|
-
return clinic;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
async getClinicById(clinicId: string): Promise<Clinic | null> {
|
|
628
|
-
return ClinicUtils.getClinicById(this.db, clinicId);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
async getAllClinics(
|
|
632
|
-
pagination?: number,
|
|
633
|
-
lastDoc?: any
|
|
634
|
-
): Promise<{ clinics: Clinic[]; lastDoc: any }> {
|
|
635
|
-
return ClinicUtils.getAllClinics(this.db, pagination, lastDoc);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async getAllClinicsInRange(
|
|
639
|
-
center: { latitude: number; longitude: number },
|
|
640
|
-
rangeInKm: number,
|
|
641
|
-
pagination?: number,
|
|
642
|
-
lastDoc?: any
|
|
643
|
-
): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
|
|
644
|
-
return ClinicUtils.getAllClinicsInRange(
|
|
645
|
-
this.db,
|
|
646
|
-
center,
|
|
647
|
-
rangeInKm,
|
|
648
|
-
pagination,
|
|
649
|
-
lastDoc
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Get clinics based on multiple filtering criteria
|
|
655
|
-
*
|
|
656
|
-
* @param filters - Various filters to apply
|
|
657
|
-
* @returns Filtered clinics and the last document for pagination
|
|
658
|
-
*/
|
|
659
|
-
async getClinicsByFilters(filters: {
|
|
660
|
-
center?: { latitude: number; longitude: number };
|
|
661
|
-
radiusInKm?: number;
|
|
662
|
-
tags?: ClinicTag[];
|
|
663
|
-
procedureFamily?: string;
|
|
664
|
-
procedureCategory?: string;
|
|
665
|
-
procedureSubcategory?: string;
|
|
666
|
-
procedureTechnology?: string;
|
|
667
|
-
minRating?: number;
|
|
668
|
-
maxRating?: number;
|
|
669
|
-
nameSearch?: string;
|
|
670
|
-
pagination?: number;
|
|
671
|
-
lastDoc?: any;
|
|
672
|
-
isActive?: boolean;
|
|
673
|
-
}): Promise<{
|
|
674
|
-
clinics: (Clinic & { distance?: number })[];
|
|
675
|
-
lastDoc: any;
|
|
676
|
-
}> {
|
|
677
|
-
return FilterUtils.getClinicsByFilters(this.db, filters);
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Gets all clinics with minimal info for map display (id, name, address, latitude, longitude)
|
|
682
|
-
* This is optimized for mobile map usage to reduce payload size.
|
|
683
|
-
* @returns Array of minimal clinic info for map
|
|
684
|
-
*/
|
|
685
|
-
async getClinicsForMap(): Promise<
|
|
686
|
-
{
|
|
687
|
-
id: string;
|
|
688
|
-
name: string;
|
|
689
|
-
address: string;
|
|
690
|
-
latitude: number | undefined;
|
|
691
|
-
longitude: number | undefined;
|
|
692
|
-
}[]
|
|
693
|
-
> {
|
|
694
|
-
const clinicsRef = collection(this.db, CLINICS_COLLECTION);
|
|
695
|
-
const snapshot = await getDocs(clinicsRef);
|
|
696
|
-
const clinicsForMap = snapshot.docs.map((doc) => {
|
|
697
|
-
const data = doc.data();
|
|
698
|
-
return {
|
|
699
|
-
id: doc.id,
|
|
700
|
-
name: data.name,
|
|
701
|
-
address: data.location?.address || "",
|
|
702
|
-
latitude: data.location?.latitude,
|
|
703
|
-
longitude: data.location?.longitude,
|
|
704
|
-
};
|
|
705
|
-
});
|
|
706
|
-
return clinicsForMap;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
collection,
|
|
3
|
+
doc,
|
|
4
|
+
getDoc,
|
|
5
|
+
getDocs,
|
|
6
|
+
query,
|
|
7
|
+
where,
|
|
8
|
+
updateDoc,
|
|
9
|
+
setDoc,
|
|
10
|
+
deleteDoc,
|
|
11
|
+
Timestamp,
|
|
12
|
+
serverTimestamp,
|
|
13
|
+
GeoPoint,
|
|
14
|
+
QueryConstraint,
|
|
15
|
+
FieldValue,
|
|
16
|
+
writeBatch,
|
|
17
|
+
arrayUnion,
|
|
18
|
+
arrayRemove,
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
21
|
+
import tzlookup from "tz-lookup";
|
|
22
|
+
import { BaseService } from "../base.service";
|
|
23
|
+
import {
|
|
24
|
+
Clinic,
|
|
25
|
+
CreateClinicData,
|
|
26
|
+
CLINICS_COLLECTION,
|
|
27
|
+
ClinicTag,
|
|
28
|
+
ClinicTags,
|
|
29
|
+
ClinicGroup,
|
|
30
|
+
CLINIC_GROUPS_COLLECTION,
|
|
31
|
+
ClinicBranchSetupData,
|
|
32
|
+
CLINIC_ADMINS_COLLECTION,
|
|
33
|
+
DoctorInfo,
|
|
34
|
+
} from "../../types/clinic";
|
|
35
|
+
// Correct imports
|
|
36
|
+
import { ProcedureSummaryInfo } from "../../types/procedure";
|
|
37
|
+
import { ClinicInfo } from "../../types/profile";
|
|
38
|
+
import { ClinicGroupService } from "./clinic-group.service";
|
|
39
|
+
import { ClinicAdminService } from "./clinic-admin.service";
|
|
40
|
+
import {
|
|
41
|
+
geohashForLocation,
|
|
42
|
+
geohashQueryBounds,
|
|
43
|
+
distanceBetween,
|
|
44
|
+
} from "geofire-common";
|
|
45
|
+
import {
|
|
46
|
+
clinicSchema,
|
|
47
|
+
createClinicSchema,
|
|
48
|
+
updateClinicSchema,
|
|
49
|
+
clinicBranchSetupSchema,
|
|
50
|
+
} from "../../validations/clinic.schema";
|
|
51
|
+
import { z } from "zod";
|
|
52
|
+
import { Auth } from "firebase/auth";
|
|
53
|
+
import { Firestore } from "firebase/firestore";
|
|
54
|
+
import { FirebaseApp } from "firebase/app";
|
|
55
|
+
import * as ClinicUtils from "./utils/clinic.utils";
|
|
56
|
+
import * as TagUtils from "./utils/tag.utils";
|
|
57
|
+
import * as SearchUtils from "./utils/search.utils";
|
|
58
|
+
import * as AdminUtils from "./utils/admin.utils";
|
|
59
|
+
import * as FilterUtils from "./utils/filter.utils";
|
|
60
|
+
import { ClinicReviewInfo } from "../../types/reviews";
|
|
61
|
+
import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
|
|
62
|
+
import { MediaService, MediaAccessLevel } from "../media/media.service";
|
|
63
|
+
|
|
64
|
+
export class ClinicService extends BaseService {
|
|
65
|
+
private clinicGroupService: ClinicGroupService;
|
|
66
|
+
private clinicAdminService: ClinicAdminService;
|
|
67
|
+
private mediaService: MediaService;
|
|
68
|
+
private functions: any;
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
db: Firestore,
|
|
72
|
+
auth: Auth,
|
|
73
|
+
app: FirebaseApp,
|
|
74
|
+
clinicGroupService: ClinicGroupService,
|
|
75
|
+
clinicAdminService: ClinicAdminService,
|
|
76
|
+
mediaService: MediaService
|
|
77
|
+
) {
|
|
78
|
+
super(db, auth, app);
|
|
79
|
+
this.clinicAdminService = clinicAdminService;
|
|
80
|
+
this.clinicGroupService = clinicGroupService;
|
|
81
|
+
this.mediaService = mediaService;
|
|
82
|
+
this.functions = getFunctions(app, "europe-west6"); // All functions now in europe-west6
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get timezone from coordinates using tz-lookup library
|
|
87
|
+
* @param lat Latitude
|
|
88
|
+
* @param lng Longitude
|
|
89
|
+
* @returns IANA timezone string
|
|
90
|
+
*/
|
|
91
|
+
private getTimezone(lat: number, lng: number): string | null {
|
|
92
|
+
try {
|
|
93
|
+
const timezone = tzlookup(lat, lng);
|
|
94
|
+
return timezone || null;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("[CLINIC_SERVICE] Error getting timezone:", error);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Process media resource (string URL or File object)
|
|
103
|
+
* @param media String URL or File object
|
|
104
|
+
* @param ownerId Owner ID for the media (usually clinicId)
|
|
105
|
+
* @param collectionName Collection name for organizing files
|
|
106
|
+
* @returns URL string after processing
|
|
107
|
+
*/
|
|
108
|
+
private async processMedia(
|
|
109
|
+
media: string | File | Blob | null | undefined,
|
|
110
|
+
ownerId: string,
|
|
111
|
+
collectionName: string
|
|
112
|
+
): Promise<string | null> {
|
|
113
|
+
if (!media) return null;
|
|
114
|
+
|
|
115
|
+
// If already a string URL, return it directly
|
|
116
|
+
if (typeof media === "string") {
|
|
117
|
+
return media;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If it's a File, upload it using MediaService
|
|
121
|
+
if (media instanceof File || media instanceof Blob) {
|
|
122
|
+
console.log(
|
|
123
|
+
`[ClinicService] Uploading ${collectionName} media for ${ownerId}`
|
|
124
|
+
);
|
|
125
|
+
const metadata = await this.mediaService.uploadMedia(
|
|
126
|
+
media,
|
|
127
|
+
ownerId,
|
|
128
|
+
MediaAccessLevel.PUBLIC,
|
|
129
|
+
collectionName
|
|
130
|
+
);
|
|
131
|
+
return metadata.url;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Process array of media resources (strings or Files)
|
|
139
|
+
* @param mediaArray Array of string URLs or File objects
|
|
140
|
+
* @param ownerId Owner ID for the media
|
|
141
|
+
* @param collectionName Collection name for organizing files
|
|
142
|
+
* @returns Array of URL strings after processing
|
|
143
|
+
*/
|
|
144
|
+
private async processMediaArray(
|
|
145
|
+
mediaArray: (string | File | Blob)[] | undefined,
|
|
146
|
+
ownerId: string,
|
|
147
|
+
collectionName: string
|
|
148
|
+
): Promise<string[]> {
|
|
149
|
+
if (!mediaArray || mediaArray.length === 0) return [];
|
|
150
|
+
|
|
151
|
+
const result: string[] = [];
|
|
152
|
+
|
|
153
|
+
for (const media of mediaArray) {
|
|
154
|
+
const processedUrl = await this.processMedia(
|
|
155
|
+
media,
|
|
156
|
+
ownerId,
|
|
157
|
+
collectionName
|
|
158
|
+
);
|
|
159
|
+
if (processedUrl) {
|
|
160
|
+
result.push(processedUrl);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Process photos with tags array
|
|
169
|
+
* @param photosWithTags Array of objects containing media and tag
|
|
170
|
+
* @param ownerId Owner ID for the media
|
|
171
|
+
* @param collectionName Collection name for organizing files
|
|
172
|
+
* @returns Processed array with URL strings
|
|
173
|
+
*/
|
|
174
|
+
private async processPhotosWithTags(
|
|
175
|
+
photosWithTags: { url: string | File | Blob; tag: string }[] | undefined,
|
|
176
|
+
ownerId: string,
|
|
177
|
+
collectionName: string
|
|
178
|
+
): Promise<{ url: string; tag: string }[]> {
|
|
179
|
+
if (!photosWithTags || photosWithTags.length === 0) return [];
|
|
180
|
+
|
|
181
|
+
const result: { url: string; tag: string }[] = [];
|
|
182
|
+
|
|
183
|
+
for (const item of photosWithTags) {
|
|
184
|
+
const processedUrl = await this.processMedia(
|
|
185
|
+
item.url,
|
|
186
|
+
ownerId,
|
|
187
|
+
collectionName
|
|
188
|
+
);
|
|
189
|
+
if (processedUrl) {
|
|
190
|
+
result.push({
|
|
191
|
+
url: processedUrl,
|
|
192
|
+
tag: item.tag,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Creates a new clinic.
|
|
202
|
+
* Handles both URL strings and File uploads for media fields.
|
|
203
|
+
*/
|
|
204
|
+
async createClinic(
|
|
205
|
+
data: CreateClinicData,
|
|
206
|
+
creatorAdminId: string
|
|
207
|
+
): Promise<Clinic> {
|
|
208
|
+
try {
|
|
209
|
+
// Generate ID first so we can use it for media uploads
|
|
210
|
+
const clinicId = this.generateId();
|
|
211
|
+
|
|
212
|
+
// Validate data - this now works because mediaResourceSchema has been updated to support Files/Blobs
|
|
213
|
+
const validatedData = createClinicSchema.parse(data);
|
|
214
|
+
|
|
215
|
+
const group = await this.clinicGroupService.getClinicGroup(
|
|
216
|
+
validatedData.clinicGroupId
|
|
217
|
+
);
|
|
218
|
+
if (!group) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Clinic group ${validatedData.clinicGroupId} not found`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Process media fields - convert File/Blob objects to URLs
|
|
225
|
+
const logoUrl = await this.processMedia(
|
|
226
|
+
validatedData.logo,
|
|
227
|
+
clinicId,
|
|
228
|
+
"clinic-logos"
|
|
229
|
+
);
|
|
230
|
+
const coverPhotoUrl = await this.processMedia(
|
|
231
|
+
validatedData.coverPhoto,
|
|
232
|
+
clinicId,
|
|
233
|
+
"clinic-cover-photos"
|
|
234
|
+
);
|
|
235
|
+
const featuredPhotos = await this.processMediaArray(
|
|
236
|
+
validatedData.featuredPhotos,
|
|
237
|
+
clinicId,
|
|
238
|
+
"clinic-featured-photos"
|
|
239
|
+
);
|
|
240
|
+
const photosWithTags = await this.processPhotosWithTags(
|
|
241
|
+
validatedData.photosWithTags,
|
|
242
|
+
clinicId,
|
|
243
|
+
"clinic-gallery"
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const location = validatedData.location;
|
|
247
|
+
const hash = geohashForLocation([location.latitude, location.longitude]);
|
|
248
|
+
const tz = this.getTimezone(location.latitude, location.longitude);
|
|
249
|
+
console.log("🏥 Clinic timezone:", tz);
|
|
250
|
+
const defaultReviewInfo: ClinicReviewInfo = {
|
|
251
|
+
totalReviews: 0,
|
|
252
|
+
averageRating: 0,
|
|
253
|
+
cleanliness: 0,
|
|
254
|
+
facilities: 0,
|
|
255
|
+
staffFriendliness: 0,
|
|
256
|
+
waitingTime: 0,
|
|
257
|
+
accessibility: 0,
|
|
258
|
+
recommendationPercentage: 0,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const clinicData: Omit<Clinic, "createdAt" | "updatedAt"> & {
|
|
262
|
+
createdAt: FieldValue;
|
|
263
|
+
updatedAt: FieldValue;
|
|
264
|
+
} = {
|
|
265
|
+
id: clinicId,
|
|
266
|
+
clinicGroupId: validatedData.clinicGroupId,
|
|
267
|
+
name: validatedData.name,
|
|
268
|
+
nameLower: validatedData.name.toLowerCase(), // Add this line
|
|
269
|
+
description: validatedData.description,
|
|
270
|
+
location: { ...location, geohash: hash, tz },
|
|
271
|
+
contactInfo: validatedData.contactInfo,
|
|
272
|
+
workingHours: validatedData.workingHours,
|
|
273
|
+
tags: validatedData.tags,
|
|
274
|
+
featuredPhotos: featuredPhotos,
|
|
275
|
+
coverPhoto: coverPhotoUrl,
|
|
276
|
+
photosWithTags: photosWithTags,
|
|
277
|
+
doctors: validatedData.doctors || [],
|
|
278
|
+
procedures: validatedData.procedures || [],
|
|
279
|
+
doctorsInfo: [],
|
|
280
|
+
proceduresInfo: validatedData.proceduresInfo || [],
|
|
281
|
+
reviewInfo: defaultReviewInfo,
|
|
282
|
+
admins: [creatorAdminId],
|
|
283
|
+
isActive:
|
|
284
|
+
validatedData.isActive !== undefined ? validatedData.isActive : true,
|
|
285
|
+
isVerified:
|
|
286
|
+
validatedData.isVerified !== undefined
|
|
287
|
+
? validatedData.isVerified
|
|
288
|
+
: false,
|
|
289
|
+
logo: logoUrl,
|
|
290
|
+
createdAt: serverTimestamp(),
|
|
291
|
+
updatedAt: serverTimestamp(),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// We can validate the final object with URLs using clinicSchema which also supports mediaResourceSchema
|
|
295
|
+
// However, we need to be careful with timestamps
|
|
296
|
+
// The validation below is optional and can be uncommented if needed
|
|
297
|
+
/*
|
|
298
|
+
clinicSchema.parse({
|
|
299
|
+
...clinicData,
|
|
300
|
+
createdAt: Timestamp.now(),
|
|
301
|
+
updatedAt: Timestamp.now(),
|
|
302
|
+
});
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
const batch = writeBatch(this.db);
|
|
306
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
307
|
+
batch.set(clinicRef, clinicData);
|
|
308
|
+
|
|
309
|
+
const adminRef = doc(this.db, CLINIC_ADMINS_COLLECTION, creatorAdminId);
|
|
310
|
+
batch.update(adminRef, {
|
|
311
|
+
clinicsManaged: arrayUnion(clinicId),
|
|
312
|
+
updatedAt: serverTimestamp(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await batch.commit();
|
|
316
|
+
console.log(`[ClinicService] Clinic created successfully: ${clinicId}`);
|
|
317
|
+
|
|
318
|
+
const savedClinic = await this.getClinic(clinicId);
|
|
319
|
+
if (!savedClinic) throw new Error("Failed to retrieve created clinic");
|
|
320
|
+
return savedClinic;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof z.ZodError) {
|
|
323
|
+
throw new Error("Invalid clinic data: " + error.message);
|
|
324
|
+
}
|
|
325
|
+
console.error("Error creating clinic:", error);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Updates a clinic.
|
|
332
|
+
* Handles both URL strings and File uploads for media fields.
|
|
333
|
+
*/
|
|
334
|
+
async updateClinic(
|
|
335
|
+
clinicId: string,
|
|
336
|
+
data: Partial<CreateClinicData>,
|
|
337
|
+
adminId: string
|
|
338
|
+
): Promise<Clinic> {
|
|
339
|
+
try {
|
|
340
|
+
// First check if clinic exists
|
|
341
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
342
|
+
const clinicDoc = await getDoc(clinicRef);
|
|
343
|
+
|
|
344
|
+
if (!clinicDoc.exists()) {
|
|
345
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const currentClinic = clinicDoc.data() as Clinic;
|
|
349
|
+
|
|
350
|
+
// Validate update data - this works because updateClinicSchema supports Files/Blobs
|
|
351
|
+
const validatedData = updateClinicSchema.parse(data);
|
|
352
|
+
|
|
353
|
+
const updatePayload: Record<string, any> = {};
|
|
354
|
+
|
|
355
|
+
// Process media fields if provided
|
|
356
|
+
if (validatedData.logo !== undefined) {
|
|
357
|
+
updatePayload.logo = await this.processMedia(
|
|
358
|
+
validatedData.logo,
|
|
359
|
+
clinicId,
|
|
360
|
+
"clinic-logos"
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (validatedData.coverPhoto !== undefined) {
|
|
365
|
+
updatePayload.coverPhoto = await this.processMedia(
|
|
366
|
+
validatedData.coverPhoto,
|
|
367
|
+
clinicId,
|
|
368
|
+
"clinic-cover-photos"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (validatedData.featuredPhotos !== undefined) {
|
|
373
|
+
updatePayload.featuredPhotos = await this.processMediaArray(
|
|
374
|
+
validatedData.featuredPhotos,
|
|
375
|
+
clinicId,
|
|
376
|
+
"clinic-featured-photos"
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (validatedData.photosWithTags !== undefined) {
|
|
381
|
+
updatePayload.photosWithTags = await this.processPhotosWithTags(
|
|
382
|
+
validatedData.photosWithTags,
|
|
383
|
+
clinicId,
|
|
384
|
+
"clinic-gallery"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Process non-media fields
|
|
389
|
+
const fieldsToUpdate = [
|
|
390
|
+
"name",
|
|
391
|
+
"description",
|
|
392
|
+
"contactInfo",
|
|
393
|
+
"workingHours",
|
|
394
|
+
"tags",
|
|
395
|
+
"doctors",
|
|
396
|
+
"procedures",
|
|
397
|
+
"proceduresInfo",
|
|
398
|
+
"isActive",
|
|
399
|
+
"isVerified",
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
for (const field of fieldsToUpdate) {
|
|
403
|
+
if (validatedData[field as keyof typeof validatedData] !== undefined) {
|
|
404
|
+
updatePayload[field] =
|
|
405
|
+
validatedData[field as keyof typeof validatedData];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Always update nameLower if name is changed
|
|
410
|
+
if (validatedData.name) {
|
|
411
|
+
updatePayload.nameLower = validatedData.name.toLowerCase();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Handle location update with geohash
|
|
415
|
+
if (validatedData.location) {
|
|
416
|
+
const loc = validatedData.location;
|
|
417
|
+
const tz = this.getTimezone(loc.latitude, loc.longitude);
|
|
418
|
+
updatePayload.location = {
|
|
419
|
+
...loc,
|
|
420
|
+
geohash: geohashForLocation([loc.latitude, loc.longitude]),
|
|
421
|
+
tz,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Add timestamp
|
|
426
|
+
updatePayload.updatedAt = serverTimestamp();
|
|
427
|
+
|
|
428
|
+
// Update the clinic
|
|
429
|
+
await updateDoc(clinicRef, updatePayload);
|
|
430
|
+
console.log(`[ClinicService] Clinic ${clinicId} updated successfully`);
|
|
431
|
+
|
|
432
|
+
// Return the updated clinic
|
|
433
|
+
const updatedClinic = await this.getClinic(clinicId);
|
|
434
|
+
if (!updatedClinic) throw new Error("Failed to retrieve updated clinic");
|
|
435
|
+
return updatedClinic;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (error instanceof z.ZodError) {
|
|
438
|
+
throw new Error("Invalid clinic update data: " + error.message);
|
|
439
|
+
}
|
|
440
|
+
console.error(`Error updating clinic ${clinicId}:`, error);
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Deactivates a clinic.
|
|
447
|
+
*/
|
|
448
|
+
async deactivateClinic(clinicId: string, adminId: string): Promise<void> {
|
|
449
|
+
// Permission check omitted
|
|
450
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
451
|
+
await updateDoc(clinicRef, {
|
|
452
|
+
isActive: false,
|
|
453
|
+
updatedAt: serverTimestamp(),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Activates a clinic.
|
|
459
|
+
*/
|
|
460
|
+
async activateClinic(clinicId: string, adminId: string): Promise<void> {
|
|
461
|
+
// Permission check omitted
|
|
462
|
+
const clinicRef = doc(this.db, CLINICS_COLLECTION, clinicId);
|
|
463
|
+
await updateDoc(clinicRef, {
|
|
464
|
+
isActive: true,
|
|
465
|
+
updatedAt: serverTimestamp(),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Dohvata kliniku po ID-u
|
|
471
|
+
*/
|
|
472
|
+
async getClinic(clinicId: string): Promise<Clinic | null> {
|
|
473
|
+
return ClinicUtils.getClinic(this.db, clinicId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Dohvata sve klinike u grupi
|
|
478
|
+
*/
|
|
479
|
+
async getClinicsByGroup(groupId: string): Promise<Clinic[]> {
|
|
480
|
+
return ClinicUtils.getClinicsByGroup(this.db, groupId);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Pretražuje klinike u određenom radijusu
|
|
485
|
+
*/
|
|
486
|
+
async findClinicsInRadius(
|
|
487
|
+
center: { latitude: number; longitude: number },
|
|
488
|
+
radiusInKm: number,
|
|
489
|
+
filters?: {
|
|
490
|
+
procedures?: string[];
|
|
491
|
+
tags?: ClinicTag[];
|
|
492
|
+
// Add other relevant filters based on Clinic/ProcedureSummaryInfo fields
|
|
493
|
+
}
|
|
494
|
+
): Promise<Clinic[]> {
|
|
495
|
+
return SearchUtils.findClinicsInRadius(
|
|
496
|
+
this.db,
|
|
497
|
+
center,
|
|
498
|
+
radiusInKm,
|
|
499
|
+
filters
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async addTags(
|
|
504
|
+
clinicId: string,
|
|
505
|
+
adminId: string,
|
|
506
|
+
newTags: {
|
|
507
|
+
tags?: ClinicTag[];
|
|
508
|
+
}
|
|
509
|
+
): Promise<Clinic> {
|
|
510
|
+
return TagUtils.addTags(
|
|
511
|
+
this.db,
|
|
512
|
+
clinicId,
|
|
513
|
+
adminId,
|
|
514
|
+
newTags,
|
|
515
|
+
this.clinicAdminService,
|
|
516
|
+
this.app
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async removeTags(
|
|
521
|
+
clinicId: string,
|
|
522
|
+
adminId: string,
|
|
523
|
+
tagsToRemove: {
|
|
524
|
+
tags?: ClinicTag[];
|
|
525
|
+
}
|
|
526
|
+
): Promise<Clinic> {
|
|
527
|
+
return TagUtils.removeTags(
|
|
528
|
+
this.db,
|
|
529
|
+
clinicId,
|
|
530
|
+
adminId,
|
|
531
|
+
tagsToRemove,
|
|
532
|
+
this.clinicAdminService,
|
|
533
|
+
this.app
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async getClinicsByAdmin(
|
|
538
|
+
adminId: string,
|
|
539
|
+
options?: {
|
|
540
|
+
isActive?: boolean;
|
|
541
|
+
includeGroupClinics?: boolean; // ako je true i admin je owner, uključuje sve klinike grupe
|
|
542
|
+
}
|
|
543
|
+
): Promise<Clinic[]> {
|
|
544
|
+
return ClinicUtils.getClinicsByAdmin(
|
|
545
|
+
this.db,
|
|
546
|
+
adminId,
|
|
547
|
+
options,
|
|
548
|
+
this.clinicAdminService,
|
|
549
|
+
this.clinicGroupService
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async getActiveClinicsByAdmin(adminId: string): Promise<Clinic[]> {
|
|
554
|
+
return ClinicUtils.getActiveClinicsByAdmin(
|
|
555
|
+
this.db,
|
|
556
|
+
adminId,
|
|
557
|
+
this.clinicAdminService,
|
|
558
|
+
this.clinicGroupService
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Creates a clinic branch from setup data.
|
|
564
|
+
* Handles both URL strings and File uploads for media fields.
|
|
565
|
+
*/
|
|
566
|
+
async createClinicBranch(
|
|
567
|
+
clinicGroupId: string,
|
|
568
|
+
setupData: ClinicBranchSetupData,
|
|
569
|
+
adminId: string
|
|
570
|
+
): Promise<Clinic> {
|
|
571
|
+
console.log("[CLINIC_SERVICE] Starting clinic branch creation", {
|
|
572
|
+
clinicGroupId,
|
|
573
|
+
adminId,
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Verify clinic group exists
|
|
577
|
+
const clinicGroup = await this.clinicGroupService.getClinicGroup(
|
|
578
|
+
clinicGroupId
|
|
579
|
+
);
|
|
580
|
+
if (!clinicGroup) {
|
|
581
|
+
console.error("[CLINIC_SERVICE] Clinic group not found", {
|
|
582
|
+
clinicGroupId,
|
|
583
|
+
});
|
|
584
|
+
throw new Error(`Clinic group with ID ${clinicGroupId} not found`);
|
|
585
|
+
}
|
|
586
|
+
console.log("[CLINIC_SERVICE] Clinic group verified");
|
|
587
|
+
|
|
588
|
+
// Validate branch setup data first
|
|
589
|
+
const validatedSetupData = clinicBranchSetupSchema.parse(setupData);
|
|
590
|
+
|
|
591
|
+
// Convert validated setup data to CreateClinicData
|
|
592
|
+
const createClinicData: CreateClinicData = {
|
|
593
|
+
clinicGroupId,
|
|
594
|
+
name: validatedSetupData.name,
|
|
595
|
+
description: validatedSetupData.description,
|
|
596
|
+
location: validatedSetupData.location,
|
|
597
|
+
contactInfo: validatedSetupData.contactInfo,
|
|
598
|
+
workingHours: validatedSetupData.workingHours,
|
|
599
|
+
tags: validatedSetupData.tags || [],
|
|
600
|
+
// Pass the media fields which can be string URLs or File objects
|
|
601
|
+
logo: validatedSetupData.logo,
|
|
602
|
+
coverPhoto: validatedSetupData.coverPhoto,
|
|
603
|
+
featuredPhotos: validatedSetupData.featuredPhotos,
|
|
604
|
+
photosWithTags: validatedSetupData.photosWithTags,
|
|
605
|
+
doctors: [],
|
|
606
|
+
procedures: [],
|
|
607
|
+
admins: [adminId],
|
|
608
|
+
isActive: true,
|
|
609
|
+
isVerified: false,
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
console.log("[CLINIC_SERVICE] Creating clinic branch", {
|
|
613
|
+
name: createClinicData.name,
|
|
614
|
+
hasLogo: !!createClinicData.logo,
|
|
615
|
+
hasCoverPhoto: !!createClinicData.coverPhoto,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Use createClinic which now handles validation and media uploads
|
|
619
|
+
const clinic = await this.createClinic(createClinicData, adminId);
|
|
620
|
+
|
|
621
|
+
console.log("[CLINIC_SERVICE] Clinic branch created successfully", {
|
|
622
|
+
clinicId: clinic.id,
|
|
623
|
+
});
|
|
624
|
+
return clinic;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async getClinicById(clinicId: string): Promise<Clinic | null> {
|
|
628
|
+
return ClinicUtils.getClinicById(this.db, clinicId);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async getAllClinics(
|
|
632
|
+
pagination?: number,
|
|
633
|
+
lastDoc?: any
|
|
634
|
+
): Promise<{ clinics: Clinic[]; lastDoc: any }> {
|
|
635
|
+
return ClinicUtils.getAllClinics(this.db, pagination, lastDoc);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async getAllClinicsInRange(
|
|
639
|
+
center: { latitude: number; longitude: number },
|
|
640
|
+
rangeInKm: number,
|
|
641
|
+
pagination?: number,
|
|
642
|
+
lastDoc?: any
|
|
643
|
+
): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
|
|
644
|
+
return ClinicUtils.getAllClinicsInRange(
|
|
645
|
+
this.db,
|
|
646
|
+
center,
|
|
647
|
+
rangeInKm,
|
|
648
|
+
pagination,
|
|
649
|
+
lastDoc
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Get clinics based on multiple filtering criteria
|
|
655
|
+
*
|
|
656
|
+
* @param filters - Various filters to apply
|
|
657
|
+
* @returns Filtered clinics and the last document for pagination
|
|
658
|
+
*/
|
|
659
|
+
async getClinicsByFilters(filters: {
|
|
660
|
+
center?: { latitude: number; longitude: number };
|
|
661
|
+
radiusInKm?: number;
|
|
662
|
+
tags?: ClinicTag[];
|
|
663
|
+
procedureFamily?: string;
|
|
664
|
+
procedureCategory?: string;
|
|
665
|
+
procedureSubcategory?: string;
|
|
666
|
+
procedureTechnology?: string;
|
|
667
|
+
minRating?: number;
|
|
668
|
+
maxRating?: number;
|
|
669
|
+
nameSearch?: string;
|
|
670
|
+
pagination?: number;
|
|
671
|
+
lastDoc?: any;
|
|
672
|
+
isActive?: boolean;
|
|
673
|
+
}): Promise<{
|
|
674
|
+
clinics: (Clinic & { distance?: number })[];
|
|
675
|
+
lastDoc: any;
|
|
676
|
+
}> {
|
|
677
|
+
return FilterUtils.getClinicsByFilters(this.db, filters);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Gets all clinics with minimal info for map display (id, name, address, latitude, longitude)
|
|
682
|
+
* This is optimized for mobile map usage to reduce payload size.
|
|
683
|
+
* @returns Array of minimal clinic info for map
|
|
684
|
+
*/
|
|
685
|
+
async getClinicsForMap(): Promise<
|
|
686
|
+
{
|
|
687
|
+
id: string;
|
|
688
|
+
name: string;
|
|
689
|
+
address: string;
|
|
690
|
+
latitude: number | undefined;
|
|
691
|
+
longitude: number | undefined;
|
|
692
|
+
}[]
|
|
693
|
+
> {
|
|
694
|
+
const clinicsRef = collection(this.db, CLINICS_COLLECTION);
|
|
695
|
+
const snapshot = await getDocs(clinicsRef);
|
|
696
|
+
const clinicsForMap = snapshot.docs.map((doc) => {
|
|
697
|
+
const data = doc.data();
|
|
698
|
+
return {
|
|
699
|
+
id: doc.id,
|
|
700
|
+
name: data.name,
|
|
701
|
+
address: data.location?.address || "",
|
|
702
|
+
latitude: data.location?.latitude,
|
|
703
|
+
longitude: data.location?.longitude,
|
|
704
|
+
};
|
|
705
|
+
});
|
|
706
|
+
return clinicsForMap;
|
|
707
|
+
}
|
|
708
|
+
}
|