@blackcode_sa/metaestetics-api 1.13.3 → 1.13.5
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 +15 -28
- package/dist/admin/index.d.ts +15 -28
- package/dist/index.d.mts +18 -30
- package/dist/index.d.ts +18 -30
- package/dist/index.js +11 -3
- package/dist/index.mjs +11 -3
- package/package.json +121 -119
- 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 +703 -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 -2191
- 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 -489
- 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 -493
- 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,646 +1,646 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collection,
|
|
3
|
-
doc,
|
|
4
|
-
getDoc,
|
|
5
|
-
getDocs,
|
|
6
|
-
query,
|
|
7
|
-
where,
|
|
8
|
-
updateDoc,
|
|
9
|
-
setDoc,
|
|
10
|
-
deleteDoc,
|
|
11
|
-
Timestamp,
|
|
12
|
-
Firestore,
|
|
13
|
-
serverTimestamp,
|
|
14
|
-
} from "firebase/firestore";
|
|
15
|
-
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
|
16
|
-
import {
|
|
17
|
-
ClinicGroup,
|
|
18
|
-
CreateClinicGroupData,
|
|
19
|
-
CLINIC_GROUPS_COLLECTION,
|
|
20
|
-
AdminToken,
|
|
21
|
-
AdminTokenStatus,
|
|
22
|
-
CreateAdminTokenData,
|
|
23
|
-
SubscriptionModel,
|
|
24
|
-
} from "../../../types/clinic";
|
|
25
|
-
import { geohashForLocation } from "geofire-common";
|
|
26
|
-
import {
|
|
27
|
-
clinicGroupSchema,
|
|
28
|
-
createClinicGroupSchema,
|
|
29
|
-
} from "../../../validations/clinic.schema";
|
|
30
|
-
import { z } from "zod";
|
|
31
|
-
import { uploadPhoto } from "./photos.utils";
|
|
32
|
-
import { FirebaseApp } from "firebase/app";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Generates a unique ID for documents
|
|
36
|
-
* Format: xxxxxxxxxxxx-timestamp
|
|
37
|
-
* Where x is a random character (number or letter)
|
|
38
|
-
*/
|
|
39
|
-
function generateId(): string {
|
|
40
|
-
const chars =
|
|
41
|
-
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
42
|
-
const timestamp = Date.now().toString(36);
|
|
43
|
-
const randomPart = Array.from({ length: 12 }, () =>
|
|
44
|
-
chars.charAt(Math.floor(Math.random() * chars.length))
|
|
45
|
-
).join("");
|
|
46
|
-
|
|
47
|
-
return `${randomPart}-${timestamp}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Creates a new clinic group
|
|
52
|
-
* @param db - Firestore database instance
|
|
53
|
-
* @param data - Clinic group data
|
|
54
|
-
* @param ownerId - ID of the owner
|
|
55
|
-
* @param isDefault - Whether this is a default group
|
|
56
|
-
* @param clinicAdminService - Service for clinic admin operations
|
|
57
|
-
* @param app - Firebase app instance
|
|
58
|
-
* @returns The created clinic group
|
|
59
|
-
*/
|
|
60
|
-
export async function createClinicGroup(
|
|
61
|
-
db: Firestore,
|
|
62
|
-
data: CreateClinicGroupData,
|
|
63
|
-
ownerId: string,
|
|
64
|
-
isDefault: boolean = false,
|
|
65
|
-
clinicAdminService: any,
|
|
66
|
-
app: FirebaseApp
|
|
67
|
-
): Promise<ClinicGroup> {
|
|
68
|
-
console.log("[CLINIC_GROUP] Starting clinic group creation", {
|
|
69
|
-
ownerId,
|
|
70
|
-
isDefault,
|
|
71
|
-
});
|
|
72
|
-
console.log("[CLINIC_GROUP] Input data:", JSON.stringify(data, null, 2));
|
|
73
|
-
|
|
74
|
-
let validatedData: CreateClinicGroupData;
|
|
75
|
-
// Validacija podataka
|
|
76
|
-
try {
|
|
77
|
-
validatedData = createClinicGroupSchema.parse(data);
|
|
78
|
-
console.log("[CLINIC_GROUP] Data validation passed");
|
|
79
|
-
} catch (validationError) {
|
|
80
|
-
console.error("[CLINIC_GROUP] Data validation failed:", validationError);
|
|
81
|
-
throw validationError;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Proveravamo da li owner postoji i da li je clinic admin
|
|
85
|
-
try {
|
|
86
|
-
console.log("[CLINIC_GROUP] Checking if owner exists", { ownerId });
|
|
87
|
-
// Skip owner verification for default groups since the admin profile doesn't exist yet
|
|
88
|
-
if (isDefault) {
|
|
89
|
-
console.log(
|
|
90
|
-
"[CLINIC_GROUP] Skipping owner verification for default group creation"
|
|
91
|
-
);
|
|
92
|
-
} else {
|
|
93
|
-
const owner = await clinicAdminService.getClinicAdmin(ownerId);
|
|
94
|
-
if (!owner) {
|
|
95
|
-
console.error(
|
|
96
|
-
"[CLINIC_GROUP] Owner not found or is not a clinic admin",
|
|
97
|
-
{
|
|
98
|
-
ownerId,
|
|
99
|
-
}
|
|
100
|
-
);
|
|
101
|
-
throw new Error("Owner not found or is not a clinic admin");
|
|
102
|
-
}
|
|
103
|
-
console.log("[CLINIC_GROUP] Owner verified as clinic admin");
|
|
104
|
-
}
|
|
105
|
-
} catch (ownerError) {
|
|
106
|
-
console.error("[CLINIC_GROUP] Error verifying owner:", ownerError);
|
|
107
|
-
throw ownerError;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Generišemo geohash za lokaciju
|
|
111
|
-
console.log("[CLINIC_GROUP] Generating geohash for location");
|
|
112
|
-
if (validatedData.hqLocation) {
|
|
113
|
-
try {
|
|
114
|
-
validatedData.hqLocation.geohash = geohashForLocation([
|
|
115
|
-
validatedData.hqLocation.latitude,
|
|
116
|
-
validatedData.hqLocation.longitude,
|
|
117
|
-
]);
|
|
118
|
-
console.log("[CLINIC_GROUP] Geohash generated successfully", {
|
|
119
|
-
geohash: validatedData.hqLocation.geohash,
|
|
120
|
-
});
|
|
121
|
-
} catch (geohashError) {
|
|
122
|
-
console.error("[CLINIC_GROUP] Error generating geohash:", geohashError);
|
|
123
|
-
throw geohashError;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const now = Timestamp.now();
|
|
128
|
-
console.log("[CLINIC_GROUP] Preparing clinic group data object");
|
|
129
|
-
|
|
130
|
-
// Generate a unique ID for the clinic group
|
|
131
|
-
const groupId = doc(collection(db, CLINIC_GROUPS_COLLECTION)).id;
|
|
132
|
-
|
|
133
|
-
// Log the logo value to debug null vs undefined issue
|
|
134
|
-
console.log("[CLINIC_GROUP] Logo value:", {
|
|
135
|
-
logoValue: validatedData.logo,
|
|
136
|
-
logoType: validatedData.logo === null ? "null" : typeof validatedData.logo,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Handle logo upload if provided
|
|
140
|
-
let logoUrl = await uploadPhoto(
|
|
141
|
-
validatedData.logo || null,
|
|
142
|
-
"clinic-groups",
|
|
143
|
-
groupId,
|
|
144
|
-
"logo",
|
|
145
|
-
app
|
|
146
|
-
);
|
|
147
|
-
console.log("[CLINIC_GROUP] Logo processed", { logoUrl });
|
|
148
|
-
|
|
149
|
-
const groupData: ClinicGroup = {
|
|
150
|
-
...validatedData,
|
|
151
|
-
id: groupId,
|
|
152
|
-
name: validatedData.name,
|
|
153
|
-
logo: logoUrl, // Use the uploaded logo URL or the original value
|
|
154
|
-
description: validatedData.description || "",
|
|
155
|
-
hqLocation: validatedData.hqLocation,
|
|
156
|
-
contactInfo: validatedData.contactInfo,
|
|
157
|
-
contactPerson: validatedData.contactPerson,
|
|
158
|
-
subscriptionModel:
|
|
159
|
-
validatedData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
|
|
160
|
-
clinics: [],
|
|
161
|
-
clinicsInfo: [],
|
|
162
|
-
admins: [ownerId],
|
|
163
|
-
adminsInfo: [],
|
|
164
|
-
adminTokens: [],
|
|
165
|
-
ownerId,
|
|
166
|
-
createdAt: now,
|
|
167
|
-
updatedAt: now,
|
|
168
|
-
isActive: true,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
// Validiramo kompletan objekat
|
|
173
|
-
console.log("[CLINIC_GROUP] Validating complete clinic group object");
|
|
174
|
-
try {
|
|
175
|
-
clinicGroupSchema.parse(groupData);
|
|
176
|
-
console.log("[CLINIC_GROUP] Clinic group validation passed");
|
|
177
|
-
} catch (schemaError) {
|
|
178
|
-
console.error(
|
|
179
|
-
"[CLINIC_GROUP] Clinic group validation failed:",
|
|
180
|
-
JSON.stringify(schemaError, null, 2)
|
|
181
|
-
);
|
|
182
|
-
throw schemaError;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Čuvamo u Firestore
|
|
186
|
-
console.log("[CLINIC_GROUP] Saving clinic group to Firestore", {
|
|
187
|
-
groupId: groupData.id,
|
|
188
|
-
});
|
|
189
|
-
try {
|
|
190
|
-
await setDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
|
|
191
|
-
console.log("[CLINIC_GROUP] Clinic group saved successfully");
|
|
192
|
-
} catch (firestoreError) {
|
|
193
|
-
console.error(
|
|
194
|
-
"[CLINIC_GROUP] Error saving to Firestore:",
|
|
195
|
-
firestoreError
|
|
196
|
-
);
|
|
197
|
-
throw firestoreError;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Ažuriramo clinic admin profil vlasnika
|
|
201
|
-
console.log("[CLINIC_GROUP] Updating clinic admin profile for owner", {
|
|
202
|
-
ownerId,
|
|
203
|
-
});
|
|
204
|
-
try {
|
|
205
|
-
await clinicAdminService.updateClinicAdmin(ownerId, {
|
|
206
|
-
clinicGroupId: groupData.id,
|
|
207
|
-
isGroupOwner: true,
|
|
208
|
-
});
|
|
209
|
-
console.log("[CLINIC_GROUP] Clinic admin profile updated successfully");
|
|
210
|
-
} catch (updateError) {
|
|
211
|
-
console.error(
|
|
212
|
-
"[CLINIC_GROUP] Error updating clinic admin profile:",
|
|
213
|
-
updateError
|
|
214
|
-
);
|
|
215
|
-
throw updateError;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
console.log("[CLINIC_GROUP] Clinic group creation completed successfully", {
|
|
219
|
-
groupId: groupData.id,
|
|
220
|
-
groupName: groupData.name,
|
|
221
|
-
});
|
|
222
|
-
return groupData;
|
|
223
|
-
} catch (error) {
|
|
224
|
-
if (error instanceof z.ZodError) {
|
|
225
|
-
console.error(
|
|
226
|
-
"[CLINIC_GROUP] Zod validation error:",
|
|
227
|
-
JSON.stringify(error.errors, null, 2)
|
|
228
|
-
);
|
|
229
|
-
throw new Error("Invalid clinic group data: " + error.message);
|
|
230
|
-
}
|
|
231
|
-
console.error(
|
|
232
|
-
"[CLINIC_GROUP] Unhandled error in createClinicGroup:",
|
|
233
|
-
error
|
|
234
|
-
);
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Gets a clinic group by ID
|
|
241
|
-
* @param db - Firestore database instance
|
|
242
|
-
* @param groupId - ID of the clinic group
|
|
243
|
-
* @returns The clinic group or null if not found
|
|
244
|
-
*/
|
|
245
|
-
export async function getClinicGroup(
|
|
246
|
-
db: Firestore,
|
|
247
|
-
groupId: string
|
|
248
|
-
): Promise<ClinicGroup | null> {
|
|
249
|
-
const docRef = doc(db, CLINIC_GROUPS_COLLECTION, groupId);
|
|
250
|
-
const docSnap = await getDoc(docRef);
|
|
251
|
-
|
|
252
|
-
if (docSnap.exists()) {
|
|
253
|
-
return docSnap.data() as ClinicGroup;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Gets all active clinic groups
|
|
261
|
-
* @param db - Firestore database instance
|
|
262
|
-
* @returns Array of active clinic groups
|
|
263
|
-
*/
|
|
264
|
-
export async function getAllActiveGroups(
|
|
265
|
-
db: Firestore
|
|
266
|
-
): Promise<ClinicGroup[]> {
|
|
267
|
-
const q = query(
|
|
268
|
-
collection(db, CLINIC_GROUPS_COLLECTION),
|
|
269
|
-
where("isActive", "==", true)
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
const querySnapshot = await getDocs(q);
|
|
273
|
-
return querySnapshot.docs.map((doc) => doc.data() as ClinicGroup);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Updates a clinic group
|
|
278
|
-
* @param db - Firestore database instance
|
|
279
|
-
* @param groupId - ID of the clinic group
|
|
280
|
-
* @param data - Data to update
|
|
281
|
-
* @param app - Firebase app instance
|
|
282
|
-
* @returns The updated clinic group
|
|
283
|
-
*/
|
|
284
|
-
export async function updateClinicGroup(
|
|
285
|
-
db: Firestore,
|
|
286
|
-
groupId: string,
|
|
287
|
-
data: Partial<ClinicGroup>,
|
|
288
|
-
app: FirebaseApp
|
|
289
|
-
): Promise<ClinicGroup> {
|
|
290
|
-
console.log("[CLINIC_GROUP] Updating clinic group", { groupId });
|
|
291
|
-
|
|
292
|
-
const group = await getClinicGroup(db, groupId);
|
|
293
|
-
if (!group) {
|
|
294
|
-
console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
|
|
295
|
-
throw new Error("Clinic group not found");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Process logo if it's a data URL
|
|
299
|
-
let updatedData = { ...data };
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
data.logo &&
|
|
303
|
-
typeof data.logo === "string" &&
|
|
304
|
-
data.logo.startsWith("data:")
|
|
305
|
-
) {
|
|
306
|
-
console.log("[CLINIC_GROUP] Processing logo for update");
|
|
307
|
-
try {
|
|
308
|
-
const logoUrl = await uploadPhoto(
|
|
309
|
-
data.logo,
|
|
310
|
-
"clinic-groups",
|
|
311
|
-
groupId,
|
|
312
|
-
"logo",
|
|
313
|
-
app
|
|
314
|
-
);
|
|
315
|
-
console.log("[CLINIC_GROUP] Logo processed for update", { logoUrl });
|
|
316
|
-
|
|
317
|
-
// Replace the data URL with the uploaded URL
|
|
318
|
-
updatedData.logo = logoUrl;
|
|
319
|
-
} catch (error) {
|
|
320
|
-
console.error("[CLINIC_GROUP] Error processing logo for update:", error);
|
|
321
|
-
// Continue with update even if logo upload fails
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Add timestamp
|
|
326
|
-
updatedData = {
|
|
327
|
-
...updatedData,
|
|
328
|
-
updatedAt: Timestamp.now(),
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
|
|
332
|
-
await updateDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
|
|
333
|
-
console.log("[CLINIC_GROUP] Clinic group updated successfully");
|
|
334
|
-
|
|
335
|
-
// Return updated data
|
|
336
|
-
const updatedGroup = await getClinicGroup(db, groupId);
|
|
337
|
-
if (!updatedGroup) {
|
|
338
|
-
console.error("[CLINIC_GROUP] Failed to retrieve updated clinic group");
|
|
339
|
-
throw new Error("Failed to retrieve updated clinic group");
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return updatedGroup;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Adds an admin to a clinic group
|
|
347
|
-
* @param db - Firestore database instance
|
|
348
|
-
* @param groupId - ID of the clinic group
|
|
349
|
-
* @param adminId - ID of the admin to add (this is the admin document ID, not the user UID)
|
|
350
|
-
* @param app - Firebase app instance
|
|
351
|
-
*/
|
|
352
|
-
export async function addAdminToGroup(
|
|
353
|
-
db: Firestore,
|
|
354
|
-
groupId: string,
|
|
355
|
-
adminId: string,
|
|
356
|
-
app: FirebaseApp
|
|
357
|
-
): Promise<void> {
|
|
358
|
-
console.log("[CLINIC_GROUP] Adding admin to group", { groupId, adminId });
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
const group = await getClinicGroup(db, groupId);
|
|
362
|
-
if (!group) {
|
|
363
|
-
console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
|
|
364
|
-
throw new Error("Clinic group not found");
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (group.admins.includes(adminId)) {
|
|
368
|
-
console.log("[CLINIC_GROUP] Admin is already in the group", {
|
|
369
|
-
adminId,
|
|
370
|
-
groupId,
|
|
371
|
-
});
|
|
372
|
-
return; // Admin is already in the group
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
console.log("[CLINIC_GROUP] Updating group with new admin");
|
|
376
|
-
await updateClinicGroup(
|
|
377
|
-
db,
|
|
378
|
-
groupId,
|
|
379
|
-
{
|
|
380
|
-
admins: [...group.admins, adminId],
|
|
381
|
-
},
|
|
382
|
-
app
|
|
383
|
-
);
|
|
384
|
-
console.log("[CLINIC_GROUP] Admin added to group successfully");
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.error("[CLINIC_GROUP] Error adding admin to group:", error);
|
|
387
|
-
throw error;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* Removes an admin from a clinic group
|
|
393
|
-
* @param db - Firestore database instance
|
|
394
|
-
* @param groupId - ID of the clinic group
|
|
395
|
-
* @param adminId - ID of the admin to remove
|
|
396
|
-
* @param app - Firebase app instance
|
|
397
|
-
*/
|
|
398
|
-
export async function removeAdminFromGroup(
|
|
399
|
-
db: Firestore,
|
|
400
|
-
groupId: string,
|
|
401
|
-
adminId: string,
|
|
402
|
-
app: FirebaseApp
|
|
403
|
-
): Promise<void> {
|
|
404
|
-
const group = await getClinicGroup(db, groupId);
|
|
405
|
-
if (!group) {
|
|
406
|
-
throw new Error("Clinic group not found");
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (group.ownerId === adminId) {
|
|
410
|
-
throw new Error("Cannot remove the owner from the group");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (!group.admins.includes(adminId)) {
|
|
414
|
-
return; // Admin is not in the group
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await updateClinicGroup(
|
|
418
|
-
db,
|
|
419
|
-
groupId,
|
|
420
|
-
{
|
|
421
|
-
admins: group.admins.filter((id) => id !== adminId),
|
|
422
|
-
},
|
|
423
|
-
app
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Deactivates a clinic group
|
|
429
|
-
* @param db - Firestore database instance
|
|
430
|
-
* @param groupId - ID of the clinic group
|
|
431
|
-
* @param app - Firebase app instance
|
|
432
|
-
*/
|
|
433
|
-
export async function deactivateClinicGroup(
|
|
434
|
-
db: Firestore,
|
|
435
|
-
groupId: string,
|
|
436
|
-
app: FirebaseApp
|
|
437
|
-
): Promise<void> {
|
|
438
|
-
const group = await getClinicGroup(db, groupId);
|
|
439
|
-
if (!group) {
|
|
440
|
-
throw new Error("Clinic group not found");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
await updateClinicGroup(
|
|
444
|
-
db,
|
|
445
|
-
groupId,
|
|
446
|
-
{
|
|
447
|
-
isActive: false,
|
|
448
|
-
},
|
|
449
|
-
app
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Creates an admin token for a clinic group
|
|
455
|
-
* @param db - Firestore database instance
|
|
456
|
-
* @param groupId - ID of the clinic group
|
|
457
|
-
* @param creatorAdminId - ID of the admin creating the token
|
|
458
|
-
* @param app - Firebase app instance
|
|
459
|
-
* @param data - Token data
|
|
460
|
-
* @returns The created admin token
|
|
461
|
-
*/
|
|
462
|
-
export async function createAdminToken(
|
|
463
|
-
db: Firestore,
|
|
464
|
-
groupId: string,
|
|
465
|
-
creatorAdminId: string,
|
|
466
|
-
app: FirebaseApp,
|
|
467
|
-
data?: CreateAdminTokenData
|
|
468
|
-
): Promise<AdminToken> {
|
|
469
|
-
const group = await getClinicGroup(db, groupId);
|
|
470
|
-
if (!group) {
|
|
471
|
-
throw new Error("Clinic group not found");
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Proveravamo da li admin pripada grupi
|
|
475
|
-
if (!group.admins.includes(creatorAdminId)) {
|
|
476
|
-
throw new Error("Admin does not belong to this clinic group");
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
const now = Timestamp.now();
|
|
480
|
-
const expiresInDays = data?.expiresInDays || 7; // Default 7 days
|
|
481
|
-
const email = data?.email || null;
|
|
482
|
-
const expiresAt = new Timestamp(
|
|
483
|
-
now.seconds + expiresInDays * 24 * 60 * 60,
|
|
484
|
-
now.nanoseconds
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
const token: AdminToken = {
|
|
488
|
-
id: generateId(),
|
|
489
|
-
token: generateId(),
|
|
490
|
-
status: AdminTokenStatus.ACTIVE,
|
|
491
|
-
email,
|
|
492
|
-
createdAt: now,
|
|
493
|
-
expiresAt,
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
// Dodajemo token u grupu
|
|
497
|
-
// Ovo treba promeniti, staviti admin tokene u sub-kolekciju u klinickoj grupi
|
|
498
|
-
await updateClinicGroup(
|
|
499
|
-
db,
|
|
500
|
-
groupId,
|
|
501
|
-
{
|
|
502
|
-
adminTokens: [...group.adminTokens, token],
|
|
503
|
-
},
|
|
504
|
-
app
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
return token;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Verifies and uses an admin token
|
|
512
|
-
* @param db - Firestore database instance
|
|
513
|
-
* @param groupId - ID of the clinic group
|
|
514
|
-
* @param token - Token to verify
|
|
515
|
-
* @param userRef - User reference
|
|
516
|
-
* @param app - Firebase app instance
|
|
517
|
-
* @returns Whether the token was successfully used
|
|
518
|
-
*/
|
|
519
|
-
export async function verifyAndUseAdminToken(
|
|
520
|
-
db: Firestore,
|
|
521
|
-
groupId: string,
|
|
522
|
-
token: string,
|
|
523
|
-
userRef: string,
|
|
524
|
-
app: FirebaseApp
|
|
525
|
-
): Promise<boolean> {
|
|
526
|
-
const group = await getClinicGroup(db, groupId);
|
|
527
|
-
if (!group) {
|
|
528
|
-
throw new Error("Clinic group not found");
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const adminToken = group.adminTokens.find((t) => t.token === token);
|
|
532
|
-
if (!adminToken) {
|
|
533
|
-
throw new Error("Admin token not found");
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
if (adminToken.status !== AdminTokenStatus.ACTIVE) {
|
|
537
|
-
throw new Error("Admin token is not active");
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const now = Timestamp.now();
|
|
541
|
-
if (adminToken.expiresAt.seconds < now.seconds) {
|
|
542
|
-
// Token je istekao, ažuriramo status
|
|
543
|
-
const updatedTokens = group.adminTokens.map((t) =>
|
|
544
|
-
t.id === adminToken.id ? { ...t, status: AdminTokenStatus.EXPIRED } : t
|
|
545
|
-
);
|
|
546
|
-
|
|
547
|
-
await updateClinicGroup(
|
|
548
|
-
db,
|
|
549
|
-
groupId,
|
|
550
|
-
{
|
|
551
|
-
adminTokens: updatedTokens,
|
|
552
|
-
},
|
|
553
|
-
app
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
throw new Error("Admin token has expired");
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Token je validan, ažuriramo status
|
|
560
|
-
const updatedTokens = group.adminTokens.map((t) =>
|
|
561
|
-
t.id === adminToken.id
|
|
562
|
-
? {
|
|
563
|
-
...t,
|
|
564
|
-
status: AdminTokenStatus.USED,
|
|
565
|
-
usedByUserRef: userRef,
|
|
566
|
-
}
|
|
567
|
-
: t
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
await updateClinicGroup(
|
|
571
|
-
db,
|
|
572
|
-
groupId,
|
|
573
|
-
{
|
|
574
|
-
adminTokens: updatedTokens,
|
|
575
|
-
},
|
|
576
|
-
app
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Deletes an admin token
|
|
584
|
-
* @param db - Firestore database instance
|
|
585
|
-
* @param groupId - ID of the clinic group
|
|
586
|
-
* @param tokenId - ID of the token to delete
|
|
587
|
-
* @param adminId - ID of the admin making the deletion
|
|
588
|
-
* @param app - Firebase app instance
|
|
589
|
-
*/
|
|
590
|
-
export async function deleteAdminToken(
|
|
591
|
-
db: Firestore,
|
|
592
|
-
groupId: string,
|
|
593
|
-
tokenId: string,
|
|
594
|
-
adminId: string,
|
|
595
|
-
app: FirebaseApp
|
|
596
|
-
): Promise<void> {
|
|
597
|
-
const group = await getClinicGroup(db, groupId);
|
|
598
|
-
if (!group) {
|
|
599
|
-
throw new Error("Clinic group not found");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Proveravamo da li admin pripada grupi
|
|
603
|
-
if (!group.admins.includes(adminId)) {
|
|
604
|
-
throw new Error("Admin does not belong to this clinic group");
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Uklanjamo token
|
|
608
|
-
const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
|
|
609
|
-
|
|
610
|
-
await updateClinicGroup(
|
|
611
|
-
db,
|
|
612
|
-
groupId,
|
|
613
|
-
{
|
|
614
|
-
adminTokens: updatedTokens,
|
|
615
|
-
},
|
|
616
|
-
app
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Gets active admin tokens for a clinic group
|
|
622
|
-
* @param db - Firestore database instance
|
|
623
|
-
* @param groupId - ID of the clinic group
|
|
624
|
-
* @param adminId - ID of the admin requesting the tokens
|
|
625
|
-
* @param app - Firebase app instance (not used but included for consistency)
|
|
626
|
-
* @returns Array of active admin tokens
|
|
627
|
-
*/
|
|
628
|
-
export async function getActiveAdminTokens(
|
|
629
|
-
db: Firestore,
|
|
630
|
-
groupId: string,
|
|
631
|
-
adminId: string,
|
|
632
|
-
app: FirebaseApp
|
|
633
|
-
): Promise<AdminToken[]> {
|
|
634
|
-
const group = await getClinicGroup(db, groupId);
|
|
635
|
-
if (!group) {
|
|
636
|
-
throw new Error("Clinic group not found");
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// Proveravamo da li admin pripada grupi
|
|
640
|
-
if (!group.admins.includes(adminId)) {
|
|
641
|
-
throw new Error("Admin does not belong to this clinic group");
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
// Vraćamo samo aktivne tokene
|
|
645
|
-
return group.adminTokens.filter((t) => t.status === AdminTokenStatus.ACTIVE);
|
|
646
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
collection,
|
|
3
|
+
doc,
|
|
4
|
+
getDoc,
|
|
5
|
+
getDocs,
|
|
6
|
+
query,
|
|
7
|
+
where,
|
|
8
|
+
updateDoc,
|
|
9
|
+
setDoc,
|
|
10
|
+
deleteDoc,
|
|
11
|
+
Timestamp,
|
|
12
|
+
Firestore,
|
|
13
|
+
serverTimestamp,
|
|
14
|
+
} from "firebase/firestore";
|
|
15
|
+
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
|
|
16
|
+
import {
|
|
17
|
+
ClinicGroup,
|
|
18
|
+
CreateClinicGroupData,
|
|
19
|
+
CLINIC_GROUPS_COLLECTION,
|
|
20
|
+
AdminToken,
|
|
21
|
+
AdminTokenStatus,
|
|
22
|
+
CreateAdminTokenData,
|
|
23
|
+
SubscriptionModel,
|
|
24
|
+
} from "../../../types/clinic";
|
|
25
|
+
import { geohashForLocation } from "geofire-common";
|
|
26
|
+
import {
|
|
27
|
+
clinicGroupSchema,
|
|
28
|
+
createClinicGroupSchema,
|
|
29
|
+
} from "../../../validations/clinic.schema";
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import { uploadPhoto } from "./photos.utils";
|
|
32
|
+
import { FirebaseApp } from "firebase/app";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generates a unique ID for documents
|
|
36
|
+
* Format: xxxxxxxxxxxx-timestamp
|
|
37
|
+
* Where x is a random character (number or letter)
|
|
38
|
+
*/
|
|
39
|
+
function generateId(): string {
|
|
40
|
+
const chars =
|
|
41
|
+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
|
42
|
+
const timestamp = Date.now().toString(36);
|
|
43
|
+
const randomPart = Array.from({ length: 12 }, () =>
|
|
44
|
+
chars.charAt(Math.floor(Math.random() * chars.length))
|
|
45
|
+
).join("");
|
|
46
|
+
|
|
47
|
+
return `${randomPart}-${timestamp}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new clinic group
|
|
52
|
+
* @param db - Firestore database instance
|
|
53
|
+
* @param data - Clinic group data
|
|
54
|
+
* @param ownerId - ID of the owner
|
|
55
|
+
* @param isDefault - Whether this is a default group
|
|
56
|
+
* @param clinicAdminService - Service for clinic admin operations
|
|
57
|
+
* @param app - Firebase app instance
|
|
58
|
+
* @returns The created clinic group
|
|
59
|
+
*/
|
|
60
|
+
export async function createClinicGroup(
|
|
61
|
+
db: Firestore,
|
|
62
|
+
data: CreateClinicGroupData,
|
|
63
|
+
ownerId: string,
|
|
64
|
+
isDefault: boolean = false,
|
|
65
|
+
clinicAdminService: any,
|
|
66
|
+
app: FirebaseApp
|
|
67
|
+
): Promise<ClinicGroup> {
|
|
68
|
+
console.log("[CLINIC_GROUP] Starting clinic group creation", {
|
|
69
|
+
ownerId,
|
|
70
|
+
isDefault,
|
|
71
|
+
});
|
|
72
|
+
console.log("[CLINIC_GROUP] Input data:", JSON.stringify(data, null, 2));
|
|
73
|
+
|
|
74
|
+
let validatedData: CreateClinicGroupData;
|
|
75
|
+
// Validacija podataka
|
|
76
|
+
try {
|
|
77
|
+
validatedData = createClinicGroupSchema.parse(data);
|
|
78
|
+
console.log("[CLINIC_GROUP] Data validation passed");
|
|
79
|
+
} catch (validationError) {
|
|
80
|
+
console.error("[CLINIC_GROUP] Data validation failed:", validationError);
|
|
81
|
+
throw validationError;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Proveravamo da li owner postoji i da li je clinic admin
|
|
85
|
+
try {
|
|
86
|
+
console.log("[CLINIC_GROUP] Checking if owner exists", { ownerId });
|
|
87
|
+
// Skip owner verification for default groups since the admin profile doesn't exist yet
|
|
88
|
+
if (isDefault) {
|
|
89
|
+
console.log(
|
|
90
|
+
"[CLINIC_GROUP] Skipping owner verification for default group creation"
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
const owner = await clinicAdminService.getClinicAdmin(ownerId);
|
|
94
|
+
if (!owner) {
|
|
95
|
+
console.error(
|
|
96
|
+
"[CLINIC_GROUP] Owner not found or is not a clinic admin",
|
|
97
|
+
{
|
|
98
|
+
ownerId,
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
throw new Error("Owner not found or is not a clinic admin");
|
|
102
|
+
}
|
|
103
|
+
console.log("[CLINIC_GROUP] Owner verified as clinic admin");
|
|
104
|
+
}
|
|
105
|
+
} catch (ownerError) {
|
|
106
|
+
console.error("[CLINIC_GROUP] Error verifying owner:", ownerError);
|
|
107
|
+
throw ownerError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Generišemo geohash za lokaciju
|
|
111
|
+
console.log("[CLINIC_GROUP] Generating geohash for location");
|
|
112
|
+
if (validatedData.hqLocation) {
|
|
113
|
+
try {
|
|
114
|
+
validatedData.hqLocation.geohash = geohashForLocation([
|
|
115
|
+
validatedData.hqLocation.latitude,
|
|
116
|
+
validatedData.hqLocation.longitude,
|
|
117
|
+
]);
|
|
118
|
+
console.log("[CLINIC_GROUP] Geohash generated successfully", {
|
|
119
|
+
geohash: validatedData.hqLocation.geohash,
|
|
120
|
+
});
|
|
121
|
+
} catch (geohashError) {
|
|
122
|
+
console.error("[CLINIC_GROUP] Error generating geohash:", geohashError);
|
|
123
|
+
throw geohashError;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const now = Timestamp.now();
|
|
128
|
+
console.log("[CLINIC_GROUP] Preparing clinic group data object");
|
|
129
|
+
|
|
130
|
+
// Generate a unique ID for the clinic group
|
|
131
|
+
const groupId = doc(collection(db, CLINIC_GROUPS_COLLECTION)).id;
|
|
132
|
+
|
|
133
|
+
// Log the logo value to debug null vs undefined issue
|
|
134
|
+
console.log("[CLINIC_GROUP] Logo value:", {
|
|
135
|
+
logoValue: validatedData.logo,
|
|
136
|
+
logoType: validatedData.logo === null ? "null" : typeof validatedData.logo,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Handle logo upload if provided
|
|
140
|
+
let logoUrl = await uploadPhoto(
|
|
141
|
+
validatedData.logo || null,
|
|
142
|
+
"clinic-groups",
|
|
143
|
+
groupId,
|
|
144
|
+
"logo",
|
|
145
|
+
app
|
|
146
|
+
);
|
|
147
|
+
console.log("[CLINIC_GROUP] Logo processed", { logoUrl });
|
|
148
|
+
|
|
149
|
+
const groupData: ClinicGroup = {
|
|
150
|
+
...validatedData,
|
|
151
|
+
id: groupId,
|
|
152
|
+
name: validatedData.name,
|
|
153
|
+
logo: logoUrl, // Use the uploaded logo URL or the original value
|
|
154
|
+
description: validatedData.description || "",
|
|
155
|
+
hqLocation: validatedData.hqLocation,
|
|
156
|
+
contactInfo: validatedData.contactInfo,
|
|
157
|
+
contactPerson: validatedData.contactPerson,
|
|
158
|
+
subscriptionModel:
|
|
159
|
+
validatedData.subscriptionModel || SubscriptionModel.NO_SUBSCRIPTION,
|
|
160
|
+
clinics: [],
|
|
161
|
+
clinicsInfo: [],
|
|
162
|
+
admins: [ownerId],
|
|
163
|
+
adminsInfo: [],
|
|
164
|
+
adminTokens: [],
|
|
165
|
+
ownerId,
|
|
166
|
+
createdAt: now,
|
|
167
|
+
updatedAt: now,
|
|
168
|
+
isActive: true,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Validiramo kompletan objekat
|
|
173
|
+
console.log("[CLINIC_GROUP] Validating complete clinic group object");
|
|
174
|
+
try {
|
|
175
|
+
clinicGroupSchema.parse(groupData);
|
|
176
|
+
console.log("[CLINIC_GROUP] Clinic group validation passed");
|
|
177
|
+
} catch (schemaError) {
|
|
178
|
+
console.error(
|
|
179
|
+
"[CLINIC_GROUP] Clinic group validation failed:",
|
|
180
|
+
JSON.stringify(schemaError, null, 2)
|
|
181
|
+
);
|
|
182
|
+
throw schemaError;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Čuvamo u Firestore
|
|
186
|
+
console.log("[CLINIC_GROUP] Saving clinic group to Firestore", {
|
|
187
|
+
groupId: groupData.id,
|
|
188
|
+
});
|
|
189
|
+
try {
|
|
190
|
+
await setDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
|
|
191
|
+
console.log("[CLINIC_GROUP] Clinic group saved successfully");
|
|
192
|
+
} catch (firestoreError) {
|
|
193
|
+
console.error(
|
|
194
|
+
"[CLINIC_GROUP] Error saving to Firestore:",
|
|
195
|
+
firestoreError
|
|
196
|
+
);
|
|
197
|
+
throw firestoreError;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Ažuriramo clinic admin profil vlasnika
|
|
201
|
+
console.log("[CLINIC_GROUP] Updating clinic admin profile for owner", {
|
|
202
|
+
ownerId,
|
|
203
|
+
});
|
|
204
|
+
try {
|
|
205
|
+
await clinicAdminService.updateClinicAdmin(ownerId, {
|
|
206
|
+
clinicGroupId: groupData.id,
|
|
207
|
+
isGroupOwner: true,
|
|
208
|
+
});
|
|
209
|
+
console.log("[CLINIC_GROUP] Clinic admin profile updated successfully");
|
|
210
|
+
} catch (updateError) {
|
|
211
|
+
console.error(
|
|
212
|
+
"[CLINIC_GROUP] Error updating clinic admin profile:",
|
|
213
|
+
updateError
|
|
214
|
+
);
|
|
215
|
+
throw updateError;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
console.log("[CLINIC_GROUP] Clinic group creation completed successfully", {
|
|
219
|
+
groupId: groupData.id,
|
|
220
|
+
groupName: groupData.name,
|
|
221
|
+
});
|
|
222
|
+
return groupData;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (error instanceof z.ZodError) {
|
|
225
|
+
console.error(
|
|
226
|
+
"[CLINIC_GROUP] Zod validation error:",
|
|
227
|
+
JSON.stringify(error.errors, null, 2)
|
|
228
|
+
);
|
|
229
|
+
throw new Error("Invalid clinic group data: " + error.message);
|
|
230
|
+
}
|
|
231
|
+
console.error(
|
|
232
|
+
"[CLINIC_GROUP] Unhandled error in createClinicGroup:",
|
|
233
|
+
error
|
|
234
|
+
);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Gets a clinic group by ID
|
|
241
|
+
* @param db - Firestore database instance
|
|
242
|
+
* @param groupId - ID of the clinic group
|
|
243
|
+
* @returns The clinic group or null if not found
|
|
244
|
+
*/
|
|
245
|
+
export async function getClinicGroup(
|
|
246
|
+
db: Firestore,
|
|
247
|
+
groupId: string
|
|
248
|
+
): Promise<ClinicGroup | null> {
|
|
249
|
+
const docRef = doc(db, CLINIC_GROUPS_COLLECTION, groupId);
|
|
250
|
+
const docSnap = await getDoc(docRef);
|
|
251
|
+
|
|
252
|
+
if (docSnap.exists()) {
|
|
253
|
+
return docSnap.data() as ClinicGroup;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Gets all active clinic groups
|
|
261
|
+
* @param db - Firestore database instance
|
|
262
|
+
* @returns Array of active clinic groups
|
|
263
|
+
*/
|
|
264
|
+
export async function getAllActiveGroups(
|
|
265
|
+
db: Firestore
|
|
266
|
+
): Promise<ClinicGroup[]> {
|
|
267
|
+
const q = query(
|
|
268
|
+
collection(db, CLINIC_GROUPS_COLLECTION),
|
|
269
|
+
where("isActive", "==", true)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const querySnapshot = await getDocs(q);
|
|
273
|
+
return querySnapshot.docs.map((doc) => doc.data() as ClinicGroup);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Updates a clinic group
|
|
278
|
+
* @param db - Firestore database instance
|
|
279
|
+
* @param groupId - ID of the clinic group
|
|
280
|
+
* @param data - Data to update
|
|
281
|
+
* @param app - Firebase app instance
|
|
282
|
+
* @returns The updated clinic group
|
|
283
|
+
*/
|
|
284
|
+
export async function updateClinicGroup(
|
|
285
|
+
db: Firestore,
|
|
286
|
+
groupId: string,
|
|
287
|
+
data: Partial<ClinicGroup>,
|
|
288
|
+
app: FirebaseApp
|
|
289
|
+
): Promise<ClinicGroup> {
|
|
290
|
+
console.log("[CLINIC_GROUP] Updating clinic group", { groupId });
|
|
291
|
+
|
|
292
|
+
const group = await getClinicGroup(db, groupId);
|
|
293
|
+
if (!group) {
|
|
294
|
+
console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
|
|
295
|
+
throw new Error("Clinic group not found");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Process logo if it's a data URL
|
|
299
|
+
let updatedData = { ...data };
|
|
300
|
+
|
|
301
|
+
if (
|
|
302
|
+
data.logo &&
|
|
303
|
+
typeof data.logo === "string" &&
|
|
304
|
+
data.logo.startsWith("data:")
|
|
305
|
+
) {
|
|
306
|
+
console.log("[CLINIC_GROUP] Processing logo for update");
|
|
307
|
+
try {
|
|
308
|
+
const logoUrl = await uploadPhoto(
|
|
309
|
+
data.logo,
|
|
310
|
+
"clinic-groups",
|
|
311
|
+
groupId,
|
|
312
|
+
"logo",
|
|
313
|
+
app
|
|
314
|
+
);
|
|
315
|
+
console.log("[CLINIC_GROUP] Logo processed for update", { logoUrl });
|
|
316
|
+
|
|
317
|
+
// Replace the data URL with the uploaded URL
|
|
318
|
+
updatedData.logo = logoUrl;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.error("[CLINIC_GROUP] Error processing logo for update:", error);
|
|
321
|
+
// Continue with update even if logo upload fails
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Add timestamp
|
|
326
|
+
updatedData = {
|
|
327
|
+
...updatedData,
|
|
328
|
+
updatedAt: Timestamp.now(),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
|
|
332
|
+
await updateDoc(doc(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
|
|
333
|
+
console.log("[CLINIC_GROUP] Clinic group updated successfully");
|
|
334
|
+
|
|
335
|
+
// Return updated data
|
|
336
|
+
const updatedGroup = await getClinicGroup(db, groupId);
|
|
337
|
+
if (!updatedGroup) {
|
|
338
|
+
console.error("[CLINIC_GROUP] Failed to retrieve updated clinic group");
|
|
339
|
+
throw new Error("Failed to retrieve updated clinic group");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return updatedGroup;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Adds an admin to a clinic group
|
|
347
|
+
* @param db - Firestore database instance
|
|
348
|
+
* @param groupId - ID of the clinic group
|
|
349
|
+
* @param adminId - ID of the admin to add (this is the admin document ID, not the user UID)
|
|
350
|
+
* @param app - Firebase app instance
|
|
351
|
+
*/
|
|
352
|
+
export async function addAdminToGroup(
|
|
353
|
+
db: Firestore,
|
|
354
|
+
groupId: string,
|
|
355
|
+
adminId: string,
|
|
356
|
+
app: FirebaseApp
|
|
357
|
+
): Promise<void> {
|
|
358
|
+
console.log("[CLINIC_GROUP] Adding admin to group", { groupId, adminId });
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const group = await getClinicGroup(db, groupId);
|
|
362
|
+
if (!group) {
|
|
363
|
+
console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
|
|
364
|
+
throw new Error("Clinic group not found");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (group.admins.includes(adminId)) {
|
|
368
|
+
console.log("[CLINIC_GROUP] Admin is already in the group", {
|
|
369
|
+
adminId,
|
|
370
|
+
groupId,
|
|
371
|
+
});
|
|
372
|
+
return; // Admin is already in the group
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log("[CLINIC_GROUP] Updating group with new admin");
|
|
376
|
+
await updateClinicGroup(
|
|
377
|
+
db,
|
|
378
|
+
groupId,
|
|
379
|
+
{
|
|
380
|
+
admins: [...group.admins, adminId],
|
|
381
|
+
},
|
|
382
|
+
app
|
|
383
|
+
);
|
|
384
|
+
console.log("[CLINIC_GROUP] Admin added to group successfully");
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error("[CLINIC_GROUP] Error adding admin to group:", error);
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Removes an admin from a clinic group
|
|
393
|
+
* @param db - Firestore database instance
|
|
394
|
+
* @param groupId - ID of the clinic group
|
|
395
|
+
* @param adminId - ID of the admin to remove
|
|
396
|
+
* @param app - Firebase app instance
|
|
397
|
+
*/
|
|
398
|
+
export async function removeAdminFromGroup(
|
|
399
|
+
db: Firestore,
|
|
400
|
+
groupId: string,
|
|
401
|
+
adminId: string,
|
|
402
|
+
app: FirebaseApp
|
|
403
|
+
): Promise<void> {
|
|
404
|
+
const group = await getClinicGroup(db, groupId);
|
|
405
|
+
if (!group) {
|
|
406
|
+
throw new Error("Clinic group not found");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (group.ownerId === adminId) {
|
|
410
|
+
throw new Error("Cannot remove the owner from the group");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!group.admins.includes(adminId)) {
|
|
414
|
+
return; // Admin is not in the group
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
await updateClinicGroup(
|
|
418
|
+
db,
|
|
419
|
+
groupId,
|
|
420
|
+
{
|
|
421
|
+
admins: group.admins.filter((id) => id !== adminId),
|
|
422
|
+
},
|
|
423
|
+
app
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Deactivates a clinic group
|
|
429
|
+
* @param db - Firestore database instance
|
|
430
|
+
* @param groupId - ID of the clinic group
|
|
431
|
+
* @param app - Firebase app instance
|
|
432
|
+
*/
|
|
433
|
+
export async function deactivateClinicGroup(
|
|
434
|
+
db: Firestore,
|
|
435
|
+
groupId: string,
|
|
436
|
+
app: FirebaseApp
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
const group = await getClinicGroup(db, groupId);
|
|
439
|
+
if (!group) {
|
|
440
|
+
throw new Error("Clinic group not found");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await updateClinicGroup(
|
|
444
|
+
db,
|
|
445
|
+
groupId,
|
|
446
|
+
{
|
|
447
|
+
isActive: false,
|
|
448
|
+
},
|
|
449
|
+
app
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Creates an admin token for a clinic group
|
|
455
|
+
* @param db - Firestore database instance
|
|
456
|
+
* @param groupId - ID of the clinic group
|
|
457
|
+
* @param creatorAdminId - ID of the admin creating the token
|
|
458
|
+
* @param app - Firebase app instance
|
|
459
|
+
* @param data - Token data
|
|
460
|
+
* @returns The created admin token
|
|
461
|
+
*/
|
|
462
|
+
export async function createAdminToken(
|
|
463
|
+
db: Firestore,
|
|
464
|
+
groupId: string,
|
|
465
|
+
creatorAdminId: string,
|
|
466
|
+
app: FirebaseApp,
|
|
467
|
+
data?: CreateAdminTokenData
|
|
468
|
+
): Promise<AdminToken> {
|
|
469
|
+
const group = await getClinicGroup(db, groupId);
|
|
470
|
+
if (!group) {
|
|
471
|
+
throw new Error("Clinic group not found");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Proveravamo da li admin pripada grupi
|
|
475
|
+
if (!group.admins.includes(creatorAdminId)) {
|
|
476
|
+
throw new Error("Admin does not belong to this clinic group");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const now = Timestamp.now();
|
|
480
|
+
const expiresInDays = data?.expiresInDays || 7; // Default 7 days
|
|
481
|
+
const email = data?.email || null;
|
|
482
|
+
const expiresAt = new Timestamp(
|
|
483
|
+
now.seconds + expiresInDays * 24 * 60 * 60,
|
|
484
|
+
now.nanoseconds
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
const token: AdminToken = {
|
|
488
|
+
id: generateId(),
|
|
489
|
+
token: generateId(),
|
|
490
|
+
status: AdminTokenStatus.ACTIVE,
|
|
491
|
+
email,
|
|
492
|
+
createdAt: now,
|
|
493
|
+
expiresAt,
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// Dodajemo token u grupu
|
|
497
|
+
// Ovo treba promeniti, staviti admin tokene u sub-kolekciju u klinickoj grupi
|
|
498
|
+
await updateClinicGroup(
|
|
499
|
+
db,
|
|
500
|
+
groupId,
|
|
501
|
+
{
|
|
502
|
+
adminTokens: [...group.adminTokens, token],
|
|
503
|
+
},
|
|
504
|
+
app
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return token;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Verifies and uses an admin token
|
|
512
|
+
* @param db - Firestore database instance
|
|
513
|
+
* @param groupId - ID of the clinic group
|
|
514
|
+
* @param token - Token to verify
|
|
515
|
+
* @param userRef - User reference
|
|
516
|
+
* @param app - Firebase app instance
|
|
517
|
+
* @returns Whether the token was successfully used
|
|
518
|
+
*/
|
|
519
|
+
export async function verifyAndUseAdminToken(
|
|
520
|
+
db: Firestore,
|
|
521
|
+
groupId: string,
|
|
522
|
+
token: string,
|
|
523
|
+
userRef: string,
|
|
524
|
+
app: FirebaseApp
|
|
525
|
+
): Promise<boolean> {
|
|
526
|
+
const group = await getClinicGroup(db, groupId);
|
|
527
|
+
if (!group) {
|
|
528
|
+
throw new Error("Clinic group not found");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const adminToken = group.adminTokens.find((t) => t.token === token);
|
|
532
|
+
if (!adminToken) {
|
|
533
|
+
throw new Error("Admin token not found");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (adminToken.status !== AdminTokenStatus.ACTIVE) {
|
|
537
|
+
throw new Error("Admin token is not active");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const now = Timestamp.now();
|
|
541
|
+
if (adminToken.expiresAt.seconds < now.seconds) {
|
|
542
|
+
// Token je istekao, ažuriramo status
|
|
543
|
+
const updatedTokens = group.adminTokens.map((t) =>
|
|
544
|
+
t.id === adminToken.id ? { ...t, status: AdminTokenStatus.EXPIRED } : t
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
await updateClinicGroup(
|
|
548
|
+
db,
|
|
549
|
+
groupId,
|
|
550
|
+
{
|
|
551
|
+
adminTokens: updatedTokens,
|
|
552
|
+
},
|
|
553
|
+
app
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
throw new Error("Admin token has expired");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Token je validan, ažuriramo status
|
|
560
|
+
const updatedTokens = group.adminTokens.map((t) =>
|
|
561
|
+
t.id === adminToken.id
|
|
562
|
+
? {
|
|
563
|
+
...t,
|
|
564
|
+
status: AdminTokenStatus.USED,
|
|
565
|
+
usedByUserRef: userRef,
|
|
566
|
+
}
|
|
567
|
+
: t
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
await updateClinicGroup(
|
|
571
|
+
db,
|
|
572
|
+
groupId,
|
|
573
|
+
{
|
|
574
|
+
adminTokens: updatedTokens,
|
|
575
|
+
},
|
|
576
|
+
app
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Deletes an admin token
|
|
584
|
+
* @param db - Firestore database instance
|
|
585
|
+
* @param groupId - ID of the clinic group
|
|
586
|
+
* @param tokenId - ID of the token to delete
|
|
587
|
+
* @param adminId - ID of the admin making the deletion
|
|
588
|
+
* @param app - Firebase app instance
|
|
589
|
+
*/
|
|
590
|
+
export async function deleteAdminToken(
|
|
591
|
+
db: Firestore,
|
|
592
|
+
groupId: string,
|
|
593
|
+
tokenId: string,
|
|
594
|
+
adminId: string,
|
|
595
|
+
app: FirebaseApp
|
|
596
|
+
): Promise<void> {
|
|
597
|
+
const group = await getClinicGroup(db, groupId);
|
|
598
|
+
if (!group) {
|
|
599
|
+
throw new Error("Clinic group not found");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Proveravamo da li admin pripada grupi
|
|
603
|
+
if (!group.admins.includes(adminId)) {
|
|
604
|
+
throw new Error("Admin does not belong to this clinic group");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Uklanjamo token
|
|
608
|
+
const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
|
|
609
|
+
|
|
610
|
+
await updateClinicGroup(
|
|
611
|
+
db,
|
|
612
|
+
groupId,
|
|
613
|
+
{
|
|
614
|
+
adminTokens: updatedTokens,
|
|
615
|
+
},
|
|
616
|
+
app
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Gets active admin tokens for a clinic group
|
|
622
|
+
* @param db - Firestore database instance
|
|
623
|
+
* @param groupId - ID of the clinic group
|
|
624
|
+
* @param adminId - ID of the admin requesting the tokens
|
|
625
|
+
* @param app - Firebase app instance (not used but included for consistency)
|
|
626
|
+
* @returns Array of active admin tokens
|
|
627
|
+
*/
|
|
628
|
+
export async function getActiveAdminTokens(
|
|
629
|
+
db: Firestore,
|
|
630
|
+
groupId: string,
|
|
631
|
+
adminId: string,
|
|
632
|
+
app: FirebaseApp
|
|
633
|
+
): Promise<AdminToken[]> {
|
|
634
|
+
const group = await getClinicGroup(db, groupId);
|
|
635
|
+
if (!group) {
|
|
636
|
+
throw new Error("Clinic group not found");
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Proveravamo da li admin pripada grupi
|
|
640
|
+
if (!group.admins.includes(adminId)) {
|
|
641
|
+
throw new Error("Admin does not belong to this clinic group");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Vraćamo samo aktivne tokene
|
|
645
|
+
return group.adminTokens.filter((t) => t.status === AdminTokenStatus.ACTIVE);
|
|
646
|
+
}
|