@blackcode_sa/metaestetics-api 1.7.19 → 1.7.20
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 +1 -1
- package/dist/admin/index.d.ts +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/backoffice/index.d.mts +6 -1
- package/dist/backoffice/index.d.ts +6 -1
- package/dist/index.d.mts +41 -26
- package/dist/index.d.ts +41 -26
- package/dist/index.js +1240 -1177
- package/dist/index.mjs +1229 -1166
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +4 -1
- package/src/services/practitioner/practitioner.service.ts +92 -3
- package/src/services/procedure/procedure.service.ts +8 -2
- package/src/types/practitioner/index.ts +2 -1
- package/src/validations/media.schema.ts +1 -1
- package/src/validations/practitioner.schema.ts +3 -2
package/dist/index.mjs
CHANGED
|
@@ -877,16 +877,16 @@ var BaseService = class {
|
|
|
877
877
|
|
|
878
878
|
// src/services/user.service.ts
|
|
879
879
|
import {
|
|
880
|
-
collection as
|
|
881
|
-
doc as
|
|
882
|
-
getDoc as
|
|
883
|
-
getDocs as
|
|
884
|
-
query as
|
|
885
|
-
where as
|
|
886
|
-
updateDoc as
|
|
887
|
-
deleteDoc as
|
|
888
|
-
Timestamp as
|
|
889
|
-
setDoc as
|
|
880
|
+
collection as collection8,
|
|
881
|
+
doc as doc10,
|
|
882
|
+
getDoc as getDoc13,
|
|
883
|
+
getDocs as getDocs8,
|
|
884
|
+
query as query8,
|
|
885
|
+
where as where8,
|
|
886
|
+
updateDoc as updateDoc10,
|
|
887
|
+
deleteDoc as deleteDoc4,
|
|
888
|
+
Timestamp as Timestamp11,
|
|
889
|
+
setDoc as setDoc9,
|
|
890
890
|
serverTimestamp as serverTimestamp10
|
|
891
891
|
} from "firebase/firestore";
|
|
892
892
|
|
|
@@ -3195,7 +3195,7 @@ var doctorInfoSchema = z11.object({
|
|
|
3195
3195
|
// src/validations/media.schema.ts
|
|
3196
3196
|
import { z as z12 } from "zod";
|
|
3197
3197
|
var mediaResourceSchema = z12.union([
|
|
3198
|
-
z12.string(),
|
|
3198
|
+
z12.string().url(),
|
|
3199
3199
|
z12.instanceof(File),
|
|
3200
3200
|
z12.instanceof(Blob)
|
|
3201
3201
|
]);
|
|
@@ -3934,686 +3934,1063 @@ var ClinicAdminService = class extends BaseService {
|
|
|
3934
3934
|
|
|
3935
3935
|
// src/services/practitioner/practitioner.service.ts
|
|
3936
3936
|
import {
|
|
3937
|
-
collection as
|
|
3938
|
-
doc as
|
|
3939
|
-
getDoc as
|
|
3940
|
-
getDocs as
|
|
3941
|
-
query as
|
|
3942
|
-
where as
|
|
3943
|
-
updateDoc as
|
|
3944
|
-
setDoc as
|
|
3945
|
-
deleteDoc as
|
|
3946
|
-
Timestamp as
|
|
3937
|
+
collection as collection7,
|
|
3938
|
+
doc as doc9,
|
|
3939
|
+
getDoc as getDoc12,
|
|
3940
|
+
getDocs as getDocs7,
|
|
3941
|
+
query as query7,
|
|
3942
|
+
where as where7,
|
|
3943
|
+
updateDoc as updateDoc9,
|
|
3944
|
+
setDoc as setDoc8,
|
|
3945
|
+
deleteDoc as deleteDoc3,
|
|
3946
|
+
Timestamp as Timestamp10,
|
|
3947
3947
|
serverTimestamp as serverTimestamp9,
|
|
3948
|
-
limit as
|
|
3948
|
+
limit as limit5,
|
|
3949
3949
|
startAfter as startAfter4,
|
|
3950
|
-
orderBy,
|
|
3950
|
+
orderBy as orderBy2,
|
|
3951
3951
|
arrayUnion as arrayUnion5,
|
|
3952
3952
|
arrayRemove as arrayRemove4
|
|
3953
3953
|
} from "firebase/firestore";
|
|
3954
3954
|
|
|
3955
|
-
// src/
|
|
3956
|
-
import { z as z14 } from "zod";
|
|
3955
|
+
// src/services/media/media.service.ts
|
|
3957
3956
|
import { Timestamp as Timestamp8 } from "firebase/firestore";
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
var
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
title: z14.string().min(2).max(100),
|
|
3988
|
-
email: z14.string().email(),
|
|
3989
|
-
phoneNumber: z14.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
|
|
3990
|
-
dateOfBirth: z14.instanceof(Timestamp8).or(z14.date()),
|
|
3991
|
-
gender: z14.enum(["male", "female", "other"]),
|
|
3992
|
-
profileImageUrl: z14.string().url().optional(),
|
|
3993
|
-
bio: z14.string().max(1e3).optional(),
|
|
3994
|
-
languages: z14.array(z14.string()).min(1)
|
|
3995
|
-
});
|
|
3996
|
-
var practitionerCertificationSchema = z14.object({
|
|
3997
|
-
level: z14.nativeEnum(CertificationLevel),
|
|
3998
|
-
specialties: z14.array(z14.nativeEnum(CertificationSpecialty)),
|
|
3999
|
-
licenseNumber: z14.string().min(3).max(50),
|
|
4000
|
-
issuingAuthority: z14.string().min(2).max(100),
|
|
4001
|
-
issueDate: z14.instanceof(Timestamp8).or(z14.date()),
|
|
4002
|
-
expiryDate: z14.instanceof(Timestamp8).or(z14.date()).optional(),
|
|
4003
|
-
verificationStatus: z14.enum(["pending", "verified", "rejected"])
|
|
4004
|
-
});
|
|
4005
|
-
var timeSlotSchema = z14.object({
|
|
4006
|
-
start: z14.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format"),
|
|
4007
|
-
end: z14.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format")
|
|
4008
|
-
}).nullable();
|
|
4009
|
-
var practitionerWorkingHoursSchema = z14.object({
|
|
4010
|
-
practitionerId: z14.string().min(1),
|
|
4011
|
-
clinicId: z14.string().min(1),
|
|
4012
|
-
monday: timeSlotSchema,
|
|
4013
|
-
tuesday: timeSlotSchema,
|
|
4014
|
-
wednesday: timeSlotSchema,
|
|
4015
|
-
thursday: timeSlotSchema,
|
|
4016
|
-
friday: timeSlotSchema,
|
|
4017
|
-
saturday: timeSlotSchema,
|
|
4018
|
-
sunday: timeSlotSchema,
|
|
4019
|
-
createdAt: z14.instanceof(Timestamp8).or(z14.date()),
|
|
4020
|
-
updatedAt: z14.instanceof(Timestamp8).or(z14.date())
|
|
4021
|
-
});
|
|
4022
|
-
var practitionerClinicWorkingHoursSchema = z14.object({
|
|
4023
|
-
clinicId: z14.string().min(1),
|
|
4024
|
-
workingHours: z14.object({
|
|
4025
|
-
monday: timeSlotSchema,
|
|
4026
|
-
tuesday: timeSlotSchema,
|
|
4027
|
-
wednesday: timeSlotSchema,
|
|
4028
|
-
thursday: timeSlotSchema,
|
|
4029
|
-
friday: timeSlotSchema,
|
|
4030
|
-
saturday: timeSlotSchema,
|
|
4031
|
-
sunday: timeSlotSchema
|
|
4032
|
-
}),
|
|
4033
|
-
isActive: z14.boolean(),
|
|
4034
|
-
createdAt: z14.instanceof(Timestamp8).or(z14.date()),
|
|
4035
|
-
updatedAt: z14.instanceof(Timestamp8).or(z14.date())
|
|
4036
|
-
});
|
|
4037
|
-
var practitionerSchema = z14.object({
|
|
4038
|
-
id: z14.string().min(1),
|
|
4039
|
-
userRef: z14.string().min(1),
|
|
4040
|
-
basicInfo: practitionerBasicInfoSchema,
|
|
4041
|
-
certification: practitionerCertificationSchema,
|
|
4042
|
-
clinics: z14.array(z14.string()),
|
|
4043
|
-
clinicWorkingHours: z14.array(practitionerClinicWorkingHoursSchema),
|
|
4044
|
-
clinicsInfo: z14.array(clinicInfoSchema),
|
|
4045
|
-
procedures: z14.array(z14.string()),
|
|
4046
|
-
proceduresInfo: z14.array(procedureSummaryInfoSchema),
|
|
4047
|
-
reviewInfo: practitionerReviewInfoSchema,
|
|
4048
|
-
isActive: z14.boolean(),
|
|
4049
|
-
isVerified: z14.boolean(),
|
|
4050
|
-
status: z14.nativeEnum(PractitionerStatus),
|
|
4051
|
-
createdAt: z14.instanceof(Timestamp8).or(z14.date()),
|
|
4052
|
-
updatedAt: z14.instanceof(Timestamp8).or(z14.date())
|
|
4053
|
-
});
|
|
4054
|
-
var createPractitionerSchema = z14.object({
|
|
4055
|
-
userRef: z14.string().min(1),
|
|
4056
|
-
basicInfo: practitionerBasicInfoSchema,
|
|
4057
|
-
certification: practitionerCertificationSchema,
|
|
4058
|
-
clinics: z14.array(z14.string()).optional(),
|
|
4059
|
-
clinicWorkingHours: z14.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4060
|
-
clinicsInfo: z14.array(clinicInfoSchema).optional(),
|
|
4061
|
-
proceduresInfo: z14.array(procedureSummaryInfoSchema).optional(),
|
|
4062
|
-
isActive: z14.boolean(),
|
|
4063
|
-
isVerified: z14.boolean(),
|
|
4064
|
-
status: z14.nativeEnum(PractitionerStatus).optional()
|
|
4065
|
-
});
|
|
4066
|
-
var createDraftPractitionerSchema = z14.object({
|
|
4067
|
-
basicInfo: practitionerBasicInfoSchema,
|
|
4068
|
-
certification: practitionerCertificationSchema,
|
|
4069
|
-
clinics: z14.array(z14.string()).optional(),
|
|
4070
|
-
clinicWorkingHours: z14.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4071
|
-
clinicsInfo: z14.array(clinicInfoSchema).optional(),
|
|
4072
|
-
proceduresInfo: z14.array(procedureSummaryInfoSchema).optional(),
|
|
4073
|
-
isActive: z14.boolean().optional().default(false),
|
|
4074
|
-
isVerified: z14.boolean().optional().default(false)
|
|
4075
|
-
});
|
|
4076
|
-
var practitionerTokenSchema = z14.object({
|
|
4077
|
-
id: z14.string().min(1),
|
|
4078
|
-
token: z14.string().min(6),
|
|
4079
|
-
practitionerId: z14.string().min(1),
|
|
4080
|
-
email: z14.string().email(),
|
|
4081
|
-
clinicId: z14.string().min(1),
|
|
4082
|
-
status: z14.nativeEnum(PractitionerTokenStatus),
|
|
4083
|
-
createdBy: z14.string().min(1),
|
|
4084
|
-
createdAt: z14.instanceof(Timestamp8).or(z14.date()),
|
|
4085
|
-
expiresAt: z14.instanceof(Timestamp8).or(z14.date()),
|
|
4086
|
-
usedBy: z14.string().optional(),
|
|
4087
|
-
usedAt: z14.instanceof(Timestamp8).or(z14.date()).optional()
|
|
4088
|
-
});
|
|
4089
|
-
var createPractitionerTokenSchema = z14.object({
|
|
4090
|
-
practitionerId: z14.string().min(1),
|
|
4091
|
-
email: z14.string().email(),
|
|
4092
|
-
clinicId: z14.string().min(1),
|
|
4093
|
-
expiresAt: z14.date().optional()
|
|
4094
|
-
});
|
|
4095
|
-
var practitionerSignupSchema = z14.object({
|
|
4096
|
-
email: z14.string().email(),
|
|
4097
|
-
password: z14.string().min(8),
|
|
4098
|
-
firstName: z14.string().min(2).max(50).optional(),
|
|
4099
|
-
lastName: z14.string().min(2).max(50).optional(),
|
|
4100
|
-
token: z14.string().optional(),
|
|
4101
|
-
profileData: z14.object({
|
|
4102
|
-
basicInfo: z14.object({
|
|
4103
|
-
phoneNumber: z14.string().optional(),
|
|
4104
|
-
profileImageUrl: z14.string().optional(),
|
|
4105
|
-
gender: z14.enum(["male", "female", "other"]).optional(),
|
|
4106
|
-
bio: z14.string().optional()
|
|
4107
|
-
}).optional(),
|
|
4108
|
-
certification: z14.any().optional()
|
|
4109
|
-
}).optional()
|
|
4110
|
-
});
|
|
4111
|
-
|
|
4112
|
-
// src/services/practitioner/practitioner.service.ts
|
|
4113
|
-
import { z as z15 } from "zod";
|
|
4114
|
-
import { distanceBetween } from "geofire-common";
|
|
4115
|
-
var PractitionerService = class extends BaseService {
|
|
4116
|
-
constructor(db, auth, app, clinicService) {
|
|
3957
|
+
import {
|
|
3958
|
+
ref as ref2,
|
|
3959
|
+
uploadBytes as uploadBytes2,
|
|
3960
|
+
getDownloadURL as getDownloadURL2,
|
|
3961
|
+
deleteObject as deleteObject2,
|
|
3962
|
+
getBytes
|
|
3963
|
+
} from "firebase/storage";
|
|
3964
|
+
import {
|
|
3965
|
+
doc as doc8,
|
|
3966
|
+
getDoc as getDoc11,
|
|
3967
|
+
setDoc as setDoc7,
|
|
3968
|
+
updateDoc as updateDoc8,
|
|
3969
|
+
collection as collection6,
|
|
3970
|
+
query as query6,
|
|
3971
|
+
where as where6,
|
|
3972
|
+
limit as limit4,
|
|
3973
|
+
getDocs as getDocs6,
|
|
3974
|
+
deleteDoc as deleteDoc2,
|
|
3975
|
+
orderBy
|
|
3976
|
+
} from "firebase/firestore";
|
|
3977
|
+
var MediaAccessLevel = /* @__PURE__ */ ((MediaAccessLevel2) => {
|
|
3978
|
+
MediaAccessLevel2["PUBLIC"] = "public";
|
|
3979
|
+
MediaAccessLevel2["PRIVATE"] = "private";
|
|
3980
|
+
MediaAccessLevel2["CONFIDENTIAL"] = "confidential";
|
|
3981
|
+
return MediaAccessLevel2;
|
|
3982
|
+
})(MediaAccessLevel || {});
|
|
3983
|
+
var MEDIA_METADATA_COLLECTION = "media_metadata";
|
|
3984
|
+
var MediaService = class extends BaseService {
|
|
3985
|
+
constructor(db, auth, app) {
|
|
4117
3986
|
super(db, auth, app);
|
|
4118
|
-
this.clinicService = clinicService;
|
|
4119
|
-
}
|
|
4120
|
-
getClinicService() {
|
|
4121
|
-
if (!this.clinicService) {
|
|
4122
|
-
throw new Error("Clinic service not initialized!");
|
|
4123
|
-
}
|
|
4124
|
-
return this.clinicService;
|
|
4125
|
-
}
|
|
4126
|
-
setClinicService(clinicService) {
|
|
4127
|
-
this.clinicService = clinicService;
|
|
4128
3987
|
}
|
|
4129
3988
|
/**
|
|
4130
|
-
*
|
|
3989
|
+
* Upload a media file, store its metadata, and return the metadata including the URL.
|
|
3990
|
+
* @param file - The file to upload.
|
|
3991
|
+
* @param ownerId - ID of the owner (user, patient, clinic, etc.).
|
|
3992
|
+
* @param accessLevel - Access level (public, private, confidential).
|
|
3993
|
+
* @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
|
|
3994
|
+
* @param originalFileName - Optional: the original name of the file, if not using file.name.
|
|
3995
|
+
* @returns Promise with the media metadata.
|
|
4131
3996
|
*/
|
|
4132
|
-
async
|
|
3997
|
+
async uploadMedia(file, ownerId, accessLevel, collectionName, originalFileName) {
|
|
3998
|
+
const mediaId = this.generateId();
|
|
3999
|
+
const fileNameToUse = originalFileName || (file instanceof File ? file.name : file.toString());
|
|
4000
|
+
const uniqueFileName = `${mediaId}-${fileNameToUse}`;
|
|
4001
|
+
const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
|
|
4002
|
+
console.log(`[MediaService] Uploading file to: ${filePath}`);
|
|
4003
|
+
const storageRef = ref2(this.storage, filePath);
|
|
4133
4004
|
try {
|
|
4134
|
-
const
|
|
4135
|
-
|
|
4136
|
-
const reviewInfo = {
|
|
4137
|
-
totalReviews: 0,
|
|
4138
|
-
averageRating: 0,
|
|
4139
|
-
knowledgeAndExpertise: 0,
|
|
4140
|
-
communicationSkills: 0,
|
|
4141
|
-
bedSideManner: 0,
|
|
4142
|
-
thoroughness: 0,
|
|
4143
|
-
trustworthiness: 0,
|
|
4144
|
-
recommendationPercentage: 0
|
|
4145
|
-
};
|
|
4146
|
-
const practitioner = {
|
|
4147
|
-
id: practitionerId,
|
|
4148
|
-
userRef: validData.userRef,
|
|
4149
|
-
basicInfo: validData.basicInfo,
|
|
4150
|
-
certification: validData.certification,
|
|
4151
|
-
clinics: validData.clinics || [],
|
|
4152
|
-
clinicWorkingHours: validData.clinicWorkingHours || [],
|
|
4153
|
-
clinicsInfo: [],
|
|
4154
|
-
procedures: [],
|
|
4155
|
-
proceduresInfo: [],
|
|
4156
|
-
reviewInfo,
|
|
4157
|
-
isActive: validData.isActive !== void 0 ? validData.isActive : true,
|
|
4158
|
-
isVerified: validData.isVerified !== void 0 ? validData.isVerified : false,
|
|
4159
|
-
status: validData.status || "active" /* ACTIVE */,
|
|
4160
|
-
createdAt: serverTimestamp9(),
|
|
4161
|
-
updatedAt: serverTimestamp9()
|
|
4162
|
-
};
|
|
4163
|
-
practitionerSchema.parse({
|
|
4164
|
-
...practitioner,
|
|
4165
|
-
createdAt: Timestamp9.now(),
|
|
4166
|
-
updatedAt: Timestamp9.now()
|
|
4005
|
+
const uploadResult = await uploadBytes2(storageRef, file, {
|
|
4006
|
+
contentType: file.type
|
|
4167
4007
|
});
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4008
|
+
console.log("[MediaService] File uploaded successfully", uploadResult);
|
|
4009
|
+
const downloadURL = await getDownloadURL2(uploadResult.ref);
|
|
4010
|
+
console.log("[MediaService] Got download URL:", downloadURL);
|
|
4011
|
+
const metadata = {
|
|
4012
|
+
id: mediaId,
|
|
4013
|
+
name: fileNameToUse,
|
|
4014
|
+
url: downloadURL,
|
|
4015
|
+
contentType: file.type,
|
|
4016
|
+
size: file.size,
|
|
4017
|
+
createdAt: Timestamp8.now(),
|
|
4018
|
+
accessLevel,
|
|
4019
|
+
ownerId,
|
|
4020
|
+
collectionName,
|
|
4021
|
+
path: filePath
|
|
4022
|
+
};
|
|
4023
|
+
const metadataDocRef = doc8(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4024
|
+
await setDoc7(metadataDocRef, metadata);
|
|
4025
|
+
console.log("[MediaService] Metadata stored in Firestore:", mediaId);
|
|
4026
|
+
return metadata;
|
|
4181
4027
|
} catch (error) {
|
|
4182
|
-
|
|
4183
|
-
throw new Error(`Invalid practitioner data: ${error.message}`);
|
|
4184
|
-
}
|
|
4185
|
-
console.error("Error creating practitioner:", error);
|
|
4028
|
+
console.error("[MediaService] Error during media upload:", error);
|
|
4186
4029
|
throw error;
|
|
4187
4030
|
}
|
|
4188
4031
|
}
|
|
4189
4032
|
/**
|
|
4190
|
-
*
|
|
4191
|
-
*
|
|
4192
|
-
* @
|
|
4193
|
-
* @param createdBy ID administratora koji kreira profil
|
|
4194
|
-
* @param clinicId ID klinike za koju se kreira profil
|
|
4195
|
-
* @returns Objekt koji sadrži kreirani draft profil i token za registraciju
|
|
4033
|
+
* Get media metadata from Firestore by its ID.
|
|
4034
|
+
* @param mediaId - ID of the media.
|
|
4035
|
+
* @returns Promise with the media metadata or null if not found.
|
|
4196
4036
|
*/
|
|
4197
|
-
async
|
|
4037
|
+
async getMediaMetadata(mediaId) {
|
|
4038
|
+
console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
|
|
4039
|
+
const docRef = doc8(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4040
|
+
const docSnap = await getDoc11(docRef);
|
|
4041
|
+
if (docSnap.exists()) {
|
|
4042
|
+
console.log("[MediaService] Metadata found:", docSnap.data());
|
|
4043
|
+
return docSnap.data();
|
|
4044
|
+
}
|
|
4045
|
+
console.log("[MediaService] No metadata found for ID:", mediaId);
|
|
4046
|
+
return null;
|
|
4047
|
+
}
|
|
4048
|
+
/**
|
|
4049
|
+
* Get media metadata from Firestore by its public URL.
|
|
4050
|
+
* @param url - The public URL of the media file.
|
|
4051
|
+
* @returns Promise with the media metadata or null if not found.
|
|
4052
|
+
*/
|
|
4053
|
+
async getMediaMetadataByUrl(url) {
|
|
4054
|
+
console.log(`[MediaService] Getting media metadata by URL: ${url}`);
|
|
4055
|
+
const q = query6(
|
|
4056
|
+
collection6(this.db, MEDIA_METADATA_COLLECTION),
|
|
4057
|
+
where6("url", "==", url),
|
|
4058
|
+
limit4(1)
|
|
4059
|
+
);
|
|
4198
4060
|
try {
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
const clinicsToAdd = /* @__PURE__ */ new Set([clinicId]);
|
|
4205
|
-
if (data.clinics && data.clinics.length > 0) {
|
|
4206
|
-
for (const cId of data.clinics) {
|
|
4207
|
-
if (cId !== clinicId) {
|
|
4208
|
-
const otherClinic = await this.getClinicService().getClinic(cId);
|
|
4209
|
-
if (!otherClinic) {
|
|
4210
|
-
throw new Error(`Clinic ${cId} not found`);
|
|
4211
|
-
}
|
|
4212
|
-
}
|
|
4213
|
-
clinicsToAdd.add(cId);
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
const clinics = Array.from(clinicsToAdd);
|
|
4217
|
-
const defaultReviewInfo = {
|
|
4218
|
-
totalReviews: 0,
|
|
4219
|
-
averageRating: 0,
|
|
4220
|
-
knowledgeAndExpertise: 0,
|
|
4221
|
-
communicationSkills: 0,
|
|
4222
|
-
bedSideManner: 0,
|
|
4223
|
-
thoroughness: 0,
|
|
4224
|
-
trustworthiness: 0,
|
|
4225
|
-
recommendationPercentage: 0
|
|
4226
|
-
};
|
|
4227
|
-
const practitionerId = this.generateId();
|
|
4228
|
-
const clinicsInfo = [];
|
|
4229
|
-
for (const cId of clinics) {
|
|
4230
|
-
const clinicData = await this.getClinicService().getClinic(cId);
|
|
4231
|
-
if (clinicData) {
|
|
4232
|
-
clinicsInfo.push({
|
|
4233
|
-
id: clinicData.id,
|
|
4234
|
-
name: clinicData.name,
|
|
4235
|
-
location: clinicData.location,
|
|
4236
|
-
contactInfo: clinicData.contactInfo,
|
|
4237
|
-
// Make sure we're using the right property for featuredPhoto
|
|
4238
|
-
featuredPhoto: clinicData.featuredPhotos && clinicData.featuredPhotos.length > 0 ? typeof clinicData.featuredPhotos[0] === "string" ? clinicData.featuredPhotos[0] : "" : (typeof clinicData.coverPhoto === "string" ? clinicData.coverPhoto : "") || "",
|
|
4239
|
-
description: clinicData.description || null
|
|
4240
|
-
});
|
|
4241
|
-
}
|
|
4242
|
-
}
|
|
4243
|
-
const finalClinicsInfo = validatedData.clinicsInfo && validatedData.clinicsInfo.length > 0 ? validatedData.clinicsInfo : clinicsInfo;
|
|
4244
|
-
const proceduresInfo = [];
|
|
4245
|
-
const practitionerData = {
|
|
4246
|
-
id: practitionerId,
|
|
4247
|
-
userRef: "",
|
|
4248
|
-
// Prazno - biće popunjeno kada korisnik kreira nalog
|
|
4249
|
-
basicInfo: validatedData.basicInfo,
|
|
4250
|
-
certification: validatedData.certification,
|
|
4251
|
-
clinics,
|
|
4252
|
-
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
4253
|
-
clinicsInfo: finalClinicsInfo,
|
|
4254
|
-
procedures: [],
|
|
4255
|
-
proceduresInfo,
|
|
4256
|
-
reviewInfo: defaultReviewInfo,
|
|
4257
|
-
isActive: validatedData.isActive !== void 0 ? validatedData.isActive : false,
|
|
4258
|
-
isVerified: validatedData.isVerified !== void 0 ? validatedData.isVerified : false,
|
|
4259
|
-
status: "draft" /* DRAFT */,
|
|
4260
|
-
createdAt: serverTimestamp9(),
|
|
4261
|
-
updatedAt: serverTimestamp9()
|
|
4262
|
-
};
|
|
4263
|
-
practitionerSchema.parse({
|
|
4264
|
-
...practitionerData,
|
|
4265
|
-
userRef: "temp-for-validation",
|
|
4266
|
-
createdAt: Timestamp9.now(),
|
|
4267
|
-
updatedAt: Timestamp9.now()
|
|
4268
|
-
});
|
|
4269
|
-
await setDoc7(
|
|
4270
|
-
doc8(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
|
|
4271
|
-
practitionerData
|
|
4272
|
-
);
|
|
4273
|
-
const savedPractitioner = await this.getPractitioner(practitionerData.id);
|
|
4274
|
-
if (!savedPractitioner) {
|
|
4275
|
-
throw new Error("Failed to create draft practitioner profile");
|
|
4061
|
+
const querySnapshot = await getDocs6(q);
|
|
4062
|
+
if (!querySnapshot.empty) {
|
|
4063
|
+
const metadata = querySnapshot.docs[0].data();
|
|
4064
|
+
console.log("[MediaService] Metadata found by URL:", metadata);
|
|
4065
|
+
return metadata;
|
|
4276
4066
|
}
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
const token = {
|
|
4280
|
-
id: this.generateId(),
|
|
4281
|
-
token: tokenString,
|
|
4282
|
-
practitionerId,
|
|
4283
|
-
email: practitionerData.basicInfo.email,
|
|
4284
|
-
clinicId,
|
|
4285
|
-
status: "active" /* ACTIVE */,
|
|
4286
|
-
createdBy,
|
|
4287
|
-
createdAt: Timestamp9.now(),
|
|
4288
|
-
expiresAt: Timestamp9.fromDate(expiration)
|
|
4289
|
-
};
|
|
4290
|
-
practitionerTokenSchema.parse(token);
|
|
4291
|
-
const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4292
|
-
await setDoc7(doc8(this.db, tokenPath), token);
|
|
4293
|
-
return { practitioner: savedPractitioner, token };
|
|
4067
|
+
console.log("[MediaService] No metadata found for URL:", url);
|
|
4068
|
+
return null;
|
|
4294
4069
|
} catch (error) {
|
|
4295
|
-
|
|
4296
|
-
throw new Error("Invalid practitioner data: " + error.message);
|
|
4297
|
-
}
|
|
4070
|
+
console.error("[MediaService] Error fetching metadata by URL:", error);
|
|
4298
4071
|
throw error;
|
|
4299
4072
|
}
|
|
4300
4073
|
}
|
|
4301
4074
|
/**
|
|
4302
|
-
*
|
|
4303
|
-
* @param
|
|
4304
|
-
* @param createdBy ID of the user creating the token
|
|
4305
|
-
* @returns Created token
|
|
4075
|
+
* Delete media from storage and remove metadata from Firestore.
|
|
4076
|
+
* @param mediaId - ID of the media to delete.
|
|
4306
4077
|
*/
|
|
4307
|
-
async
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4078
|
+
async deleteMedia(mediaId) {
|
|
4079
|
+
console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
|
|
4080
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
4081
|
+
if (!metadata) {
|
|
4082
|
+
console.warn(
|
|
4083
|
+
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
|
|
4312
4084
|
);
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4085
|
+
return;
|
|
4086
|
+
}
|
|
4087
|
+
const storageFileRef = ref2(this.storage, metadata.path);
|
|
4088
|
+
try {
|
|
4089
|
+
await deleteObject2(storageFileRef);
|
|
4090
|
+
console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
|
|
4091
|
+
const metadataDocRef = doc8(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4092
|
+
await deleteDoc2(metadataDocRef);
|
|
4093
|
+
console.log(
|
|
4094
|
+
`[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
|
|
4323
4095
|
);
|
|
4324
|
-
if (!clinic) {
|
|
4325
|
-
throw new Error(`Clinic ${validatedData.clinicId} not found`);
|
|
4326
|
-
}
|
|
4327
|
-
if (!practitioner.clinics.includes(validatedData.clinicId)) {
|
|
4328
|
-
throw new Error("Practitioner is not associated with this clinic");
|
|
4329
|
-
}
|
|
4330
|
-
const expiration = validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
4331
|
-
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
4332
|
-
const token = {
|
|
4333
|
-
id: this.generateId(),
|
|
4334
|
-
token: tokenString,
|
|
4335
|
-
practitionerId: validatedData.practitionerId,
|
|
4336
|
-
email: validatedData.email,
|
|
4337
|
-
clinicId: validatedData.clinicId,
|
|
4338
|
-
status: "active" /* ACTIVE */,
|
|
4339
|
-
createdBy,
|
|
4340
|
-
createdAt: Timestamp9.now(),
|
|
4341
|
-
expiresAt: Timestamp9.fromDate(expiration)
|
|
4342
|
-
};
|
|
4343
|
-
practitionerTokenSchema.parse(token);
|
|
4344
|
-
const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4345
|
-
await setDoc7(doc8(this.db, tokenPath), token);
|
|
4346
|
-
return token;
|
|
4347
4096
|
} catch (error) {
|
|
4348
|
-
|
|
4349
|
-
throw new Error("Invalid token data: " + error.message);
|
|
4350
|
-
}
|
|
4097
|
+
console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
|
|
4351
4098
|
throw error;
|
|
4352
4099
|
}
|
|
4353
4100
|
}
|
|
4354
4101
|
/**
|
|
4355
|
-
*
|
|
4356
|
-
*
|
|
4357
|
-
* @
|
|
4102
|
+
* Update media access level. This involves moving the file in Firebase Storage
|
|
4103
|
+
* to a new path reflecting the new access level, and updating its metadata.
|
|
4104
|
+
* @param mediaId - ID of the media to update.
|
|
4105
|
+
* @param newAccessLevel - New access level.
|
|
4106
|
+
* @returns Promise with the updated media metadata, or null if metadata not found.
|
|
4358
4107
|
*/
|
|
4359
|
-
async
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
);
|
|
4364
|
-
const q = query6(
|
|
4365
|
-
tokensRef,
|
|
4366
|
-
where6("status", "==", "active" /* ACTIVE */),
|
|
4367
|
-
where6("expiresAt", ">", Timestamp9.now())
|
|
4108
|
+
async updateMediaAccessLevel(mediaId, newAccessLevel) {
|
|
4109
|
+
var _a;
|
|
4110
|
+
console.log(
|
|
4111
|
+
`[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
|
|
4368
4112
|
);
|
|
4369
|
-
const
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
* Gets a token by its string value and validates it
|
|
4374
|
-
* @param tokenString The token string to find
|
|
4375
|
-
* @returns The token if found and valid, null otherwise
|
|
4376
|
-
*/
|
|
4377
|
-
async validateToken(tokenString) {
|
|
4378
|
-
const practitionersRef = collection6(this.db, PRACTITIONERS_COLLECTION);
|
|
4379
|
-
const practitionersSnapshot = await getDocs6(practitionersRef);
|
|
4380
|
-
for (const practitionerDoc of practitionersSnapshot.docs) {
|
|
4381
|
-
const practitionerId = practitionerDoc.id;
|
|
4382
|
-
const tokensRef = collection6(
|
|
4383
|
-
this.db,
|
|
4384
|
-
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
4113
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
4114
|
+
if (!metadata) {
|
|
4115
|
+
console.warn(
|
|
4116
|
+
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
|
|
4385
4117
|
);
|
|
4118
|
+
return null;
|
|
4119
|
+
}
|
|
4120
|
+
if (metadata.accessLevel === newAccessLevel) {
|
|
4386
4121
|
console.log(
|
|
4387
|
-
`[
|
|
4388
|
-
{
|
|
4389
|
-
tokenString,
|
|
4390
|
-
timestamp: Timestamp9.now().toDate()
|
|
4391
|
-
}
|
|
4392
|
-
);
|
|
4393
|
-
const q = query6(
|
|
4394
|
-
tokensRef,
|
|
4395
|
-
where6("token", "==", tokenString),
|
|
4396
|
-
where6("status", "==", "active" /* ACTIVE */),
|
|
4397
|
-
where6("expiresAt", ">", Timestamp9.now())
|
|
4122
|
+
`[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
|
|
4398
4123
|
);
|
|
4124
|
+
const metadataDocRef = doc8(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4399
4125
|
try {
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
`[PRACTITIONER] Token query results for practitioner ${practitionerId}`,
|
|
4403
|
-
{
|
|
4404
|
-
found: !tokenSnapshot.empty,
|
|
4405
|
-
count: tokenSnapshot.size
|
|
4406
|
-
}
|
|
4407
|
-
);
|
|
4408
|
-
if (!tokenSnapshot.empty) {
|
|
4409
|
-
const tokenData = tokenSnapshot.docs[0].data();
|
|
4410
|
-
console.log(`[PRACTITIONER] Valid token found`, {
|
|
4411
|
-
tokenId: tokenData.id,
|
|
4412
|
-
expiresAt: tokenData.expiresAt.toDate()
|
|
4413
|
-
});
|
|
4414
|
-
return tokenData;
|
|
4415
|
-
}
|
|
4126
|
+
await updateDoc8(metadataDocRef, { updatedAt: Timestamp8.now() });
|
|
4127
|
+
return { ...metadata, updatedAt: Timestamp8.now() };
|
|
4416
4128
|
} catch (error) {
|
|
4417
4129
|
console.error(
|
|
4418
|
-
`[
|
|
4130
|
+
`[MediaService] Error updating timestamp for media ID ${mediaId}:`,
|
|
4419
4131
|
error
|
|
4420
4132
|
);
|
|
4421
4133
|
throw error;
|
|
4422
4134
|
}
|
|
4423
4135
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
* @param practitionerId ID of the practitioner
|
|
4430
|
-
* @param userId ID of the user using the token
|
|
4431
|
-
*/
|
|
4432
|
-
async markTokenAsUsed(tokenId, practitionerId, userId) {
|
|
4433
|
-
const tokenRef = doc8(
|
|
4434
|
-
this.db,
|
|
4435
|
-
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
|
|
4436
|
-
);
|
|
4437
|
-
await updateDoc8(tokenRef, {
|
|
4438
|
-
status: "used" /* USED */,
|
|
4439
|
-
usedBy: userId,
|
|
4440
|
-
usedAt: Timestamp9.now()
|
|
4441
|
-
});
|
|
4442
|
-
}
|
|
4443
|
-
/**
|
|
4444
|
-
* Dohvata zdravstvenog radnika po ID-u
|
|
4445
|
-
*/
|
|
4446
|
-
async getPractitioner(practitionerId) {
|
|
4447
|
-
const practitionerDoc = await getDoc11(
|
|
4448
|
-
doc8(this.db, PRACTITIONERS_COLLECTION, practitionerId)
|
|
4449
|
-
);
|
|
4450
|
-
if (!practitionerDoc.exists()) {
|
|
4451
|
-
return null;
|
|
4452
|
-
}
|
|
4453
|
-
return practitionerDoc.data();
|
|
4454
|
-
}
|
|
4455
|
-
/**
|
|
4456
|
-
* Dohvata zdravstvenog radnika po User ID-u
|
|
4457
|
-
*/
|
|
4458
|
-
async getPractitionerByUserRef(userRef) {
|
|
4459
|
-
const q = query6(
|
|
4460
|
-
collection6(this.db, PRACTITIONERS_COLLECTION),
|
|
4461
|
-
where6("userRef", "==", userRef)
|
|
4462
|
-
);
|
|
4463
|
-
const querySnapshot = await getDocs6(q);
|
|
4464
|
-
if (querySnapshot.empty) {
|
|
4465
|
-
return null;
|
|
4466
|
-
}
|
|
4467
|
-
return querySnapshot.docs[0].data();
|
|
4468
|
-
}
|
|
4469
|
-
/**
|
|
4470
|
-
* Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
|
|
4471
|
-
*/
|
|
4472
|
-
async getPractitionersByClinic(clinicId) {
|
|
4473
|
-
const q = query6(
|
|
4474
|
-
collection6(this.db, PRACTITIONERS_COLLECTION),
|
|
4475
|
-
where6("clinics", "array-contains", clinicId),
|
|
4476
|
-
where6("isActive", "==", true),
|
|
4477
|
-
where6("status", "==", "active" /* ACTIVE */)
|
|
4478
|
-
);
|
|
4479
|
-
const querySnapshot = await getDocs6(q);
|
|
4480
|
-
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4481
|
-
}
|
|
4482
|
-
/**
|
|
4483
|
-
* Dohvata sve zdravstvene radnike za određenu kliniku
|
|
4484
|
-
*/
|
|
4485
|
-
async getAllPractitionersByClinic(clinicId) {
|
|
4486
|
-
const q = query6(
|
|
4487
|
-
collection6(this.db, PRACTITIONERS_COLLECTION),
|
|
4488
|
-
where6("clinics", "array-contains", clinicId),
|
|
4489
|
-
where6("isActive", "==", true)
|
|
4490
|
-
);
|
|
4491
|
-
const querySnapshot = await getDocs6(q);
|
|
4492
|
-
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4493
|
-
}
|
|
4494
|
-
/**
|
|
4495
|
-
* Dohvata sve draft zdravstvene radnike za određenu kliniku sa statusom DRAFT
|
|
4496
|
-
*/
|
|
4497
|
-
async getDraftPractitionersByClinic(clinicId) {
|
|
4498
|
-
const q = query6(
|
|
4499
|
-
collection6(this.db, PRACTITIONERS_COLLECTION),
|
|
4500
|
-
where6("clinics", "array-contains", clinicId),
|
|
4501
|
-
where6("status", "==", "draft" /* DRAFT */)
|
|
4136
|
+
const oldStoragePath = metadata.path;
|
|
4137
|
+
const fileNamePart = `${metadata.id}-${metadata.name}`;
|
|
4138
|
+
const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
|
|
4139
|
+
console.log(
|
|
4140
|
+
`[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
|
|
4502
4141
|
);
|
|
4503
|
-
const
|
|
4504
|
-
|
|
4505
|
-
}
|
|
4506
|
-
/**
|
|
4507
|
-
* Updates a practitioner
|
|
4508
|
-
*/
|
|
4509
|
-
async updatePractitioner(practitionerId, data) {
|
|
4142
|
+
const oldStorageFileRef = ref2(this.storage, oldStoragePath);
|
|
4143
|
+
const newStorageFileRef = ref2(this.storage, newStoragePath);
|
|
4510
4144
|
try {
|
|
4511
|
-
|
|
4512
|
-
const
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4145
|
+
console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
|
|
4146
|
+
const fileBytes = await getBytes(oldStorageFileRef);
|
|
4147
|
+
console.log(
|
|
4148
|
+
`[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
|
|
4149
|
+
);
|
|
4150
|
+
console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
|
|
4151
|
+
await uploadBytes2(newStorageFileRef, fileBytes, {
|
|
4152
|
+
contentType: metadata.contentType
|
|
4153
|
+
});
|
|
4154
|
+
console.log(
|
|
4155
|
+
`[MediaService] Successfully uploaded bytes to ${newStoragePath}`
|
|
4156
|
+
);
|
|
4157
|
+
const newDownloadURL = await getDownloadURL2(newStorageFileRef);
|
|
4158
|
+
console.log(
|
|
4159
|
+
`[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
|
|
4516
4160
|
);
|
|
4517
|
-
const practitionerDoc = await getDoc11(practitionerRef);
|
|
4518
|
-
if (!practitionerDoc.exists()) {
|
|
4519
|
-
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4520
|
-
}
|
|
4521
|
-
const currentPractitioner = practitionerDoc.data();
|
|
4522
4161
|
const updateData = {
|
|
4523
|
-
|
|
4524
|
-
|
|
4162
|
+
accessLevel: newAccessLevel,
|
|
4163
|
+
path: newStoragePath,
|
|
4164
|
+
url: newDownloadURL,
|
|
4165
|
+
updatedAt: Timestamp8.now()
|
|
4525
4166
|
};
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
`Failed to retrieve updated practitioner ${practitionerId}`
|
|
4531
|
-
);
|
|
4532
|
-
}
|
|
4533
|
-
return updatedPractitioner;
|
|
4534
|
-
} catch (error) {
|
|
4535
|
-
if (error instanceof z15.ZodError) {
|
|
4536
|
-
throw new Error(`Invalid practitioner update data: ${error.message}`);
|
|
4537
|
-
}
|
|
4538
|
-
console.error(`Error updating practitioner ${practitionerId}:`, error);
|
|
4539
|
-
throw error;
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
/**
|
|
4543
|
-
* Adds a clinic to a practitioner
|
|
4544
|
-
*/
|
|
4545
|
-
async addClinic(practitionerId, clinicId) {
|
|
4546
|
-
var _a;
|
|
4547
|
-
try {
|
|
4548
|
-
const practitionerRef = doc8(
|
|
4549
|
-
this.db,
|
|
4550
|
-
PRACTITIONERS_COLLECTION,
|
|
4551
|
-
practitionerId
|
|
4167
|
+
const metadataDocRef = doc8(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4168
|
+
console.log(
|
|
4169
|
+
`[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
|
|
4170
|
+
updateData
|
|
4552
4171
|
);
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4172
|
+
await updateDoc8(metadataDocRef, updateData);
|
|
4173
|
+
console.log(
|
|
4174
|
+
`[MediaService] Successfully updated Firestore metadata for ${mediaId}`
|
|
4175
|
+
);
|
|
4176
|
+
try {
|
|
4177
|
+
console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
|
|
4178
|
+
await deleteObject2(oldStorageFileRef);
|
|
4559
4179
|
console.log(
|
|
4560
|
-
`
|
|
4180
|
+
`[MediaService] Successfully deleted old file from ${oldStoragePath}`
|
|
4181
|
+
);
|
|
4182
|
+
} catch (deleteError) {
|
|
4183
|
+
console.error(
|
|
4184
|
+
`[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
|
|
4185
|
+
deleteError
|
|
4561
4186
|
);
|
|
4562
|
-
return;
|
|
4563
4187
|
}
|
|
4564
|
-
|
|
4565
|
-
clinics: arrayUnion5(clinicId),
|
|
4566
|
-
updatedAt: serverTimestamp9()
|
|
4567
|
-
});
|
|
4188
|
+
return { ...metadata, ...updateData };
|
|
4568
4189
|
} catch (error) {
|
|
4569
4190
|
console.error(
|
|
4570
|
-
`Error
|
|
4191
|
+
`[MediaService] Error updating media access level and moving file for ${mediaId}:`,
|
|
4571
4192
|
error
|
|
4572
4193
|
);
|
|
4194
|
+
if (newStorageFileRef && error.code !== "storage/object-not-found" && ((_a = error.message) == null ? void 0 : _a.includes("uploadBytes"))) {
|
|
4195
|
+
console.warn(
|
|
4196
|
+
`[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
|
|
4197
|
+
);
|
|
4198
|
+
try {
|
|
4199
|
+
await deleteObject2(newStorageFileRef);
|
|
4200
|
+
console.warn(
|
|
4201
|
+
`[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
|
|
4202
|
+
);
|
|
4203
|
+
} catch (cleanupError) {
|
|
4204
|
+
console.error(
|
|
4205
|
+
`[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
|
|
4206
|
+
cleanupError
|
|
4207
|
+
);
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4573
4210
|
throw error;
|
|
4574
4211
|
}
|
|
4575
4212
|
}
|
|
4576
4213
|
/**
|
|
4577
|
-
*
|
|
4214
|
+
* List all media for an owner, optionally filtered by collection and access level.
|
|
4215
|
+
* @param ownerId - ID of the owner.
|
|
4216
|
+
* @param collectionName - Optional: Filter by collection name.
|
|
4217
|
+
* @param accessLevel - Optional: Filter by access level.
|
|
4218
|
+
* @param count - Optional: Number of items to fetch.
|
|
4219
|
+
* @param startAfterId - Optional: ID of the document to start after (for pagination).
|
|
4578
4220
|
*/
|
|
4579
|
-
async
|
|
4221
|
+
async listMedia(ownerId, collectionName, accessLevel, count, startAfterId) {
|
|
4222
|
+
console.log(`[MediaService] Listing media for owner: ${ownerId}`);
|
|
4223
|
+
let qConstraints = [where6("ownerId", "==", ownerId)];
|
|
4224
|
+
if (collectionName) {
|
|
4225
|
+
qConstraints.push(where6("collectionName", "==", collectionName));
|
|
4226
|
+
}
|
|
4227
|
+
if (accessLevel) {
|
|
4228
|
+
qConstraints.push(where6("accessLevel", "==", accessLevel));
|
|
4229
|
+
}
|
|
4230
|
+
qConstraints.push(orderBy("createdAt", "desc"));
|
|
4231
|
+
if (count) {
|
|
4232
|
+
qConstraints.push(limit4(count));
|
|
4233
|
+
}
|
|
4234
|
+
if (startAfterId) {
|
|
4235
|
+
const startAfterDoc = await this.getMediaMetadata(startAfterId);
|
|
4236
|
+
if (startAfterDoc) {
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
const finalQuery = query6(
|
|
4240
|
+
collection6(this.db, MEDIA_METADATA_COLLECTION),
|
|
4241
|
+
...qConstraints
|
|
4242
|
+
);
|
|
4580
4243
|
try {
|
|
4581
|
-
const
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
practitionerId
|
|
4244
|
+
const querySnapshot = await getDocs6(finalQuery);
|
|
4245
|
+
const mediaList = querySnapshot.docs.map(
|
|
4246
|
+
(doc34) => doc34.data()
|
|
4585
4247
|
);
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4589
|
-
}
|
|
4590
|
-
await updateDoc8(practitionerRef, {
|
|
4591
|
-
clinics: arrayRemove4(clinicId),
|
|
4592
|
-
updatedAt: serverTimestamp9()
|
|
4593
|
-
});
|
|
4248
|
+
console.log(`[MediaService] Found ${mediaList.length} media items.`);
|
|
4249
|
+
return mediaList;
|
|
4594
4250
|
} catch (error) {
|
|
4595
|
-
console.error(
|
|
4596
|
-
`Error removing clinic ${clinicId} from practitioner ${practitionerId}:`,
|
|
4597
|
-
error
|
|
4598
|
-
);
|
|
4251
|
+
console.error("[MediaService] Error listing media:", error);
|
|
4599
4252
|
throw error;
|
|
4600
4253
|
}
|
|
4601
4254
|
}
|
|
4602
4255
|
/**
|
|
4603
|
-
*
|
|
4256
|
+
* Get download URL for media. (Convenience, as URL is in metadata)
|
|
4257
|
+
* @param mediaId - ID of the media.
|
|
4604
4258
|
*/
|
|
4605
|
-
async
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4259
|
+
async getMediaDownloadUrl(mediaId) {
|
|
4260
|
+
console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
|
|
4261
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
4262
|
+
if (metadata && metadata.url) {
|
|
4263
|
+
console.log(`[MediaService] URL found: ${metadata.url}`);
|
|
4264
|
+
return metadata.url;
|
|
4265
|
+
}
|
|
4266
|
+
console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
|
|
4267
|
+
return null;
|
|
4609
4268
|
}
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4269
|
+
};
|
|
4270
|
+
|
|
4271
|
+
// src/validations/practitioner.schema.ts
|
|
4272
|
+
import { z as z14 } from "zod";
|
|
4273
|
+
import { Timestamp as Timestamp9 } from "firebase/firestore";
|
|
4274
|
+
|
|
4275
|
+
// src/backoffice/types/static/certification.types.ts
|
|
4276
|
+
var CertificationLevel = /* @__PURE__ */ ((CertificationLevel2) => {
|
|
4277
|
+
CertificationLevel2["AESTHETICIAN"] = "aesthetician";
|
|
4278
|
+
CertificationLevel2["NURSE_ASSISTANT"] = "nurse_assistant";
|
|
4279
|
+
CertificationLevel2["NURSE"] = "nurse";
|
|
4280
|
+
CertificationLevel2["NURSE_PRACTITIONER"] = "nurse_practitioner";
|
|
4281
|
+
CertificationLevel2["PHYSICIAN_ASSISTANT"] = "physician_assistant";
|
|
4282
|
+
CertificationLevel2["DOCTOR"] = "doctor";
|
|
4283
|
+
CertificationLevel2["SPECIALIST"] = "specialist";
|
|
4284
|
+
CertificationLevel2["PLASTIC_SURGEON"] = "plastic_surgeon";
|
|
4285
|
+
return CertificationLevel2;
|
|
4286
|
+
})(CertificationLevel || {});
|
|
4287
|
+
var CertificationSpecialty = /* @__PURE__ */ ((CertificationSpecialty3) => {
|
|
4288
|
+
CertificationSpecialty3["LASER"] = "laser";
|
|
4289
|
+
CertificationSpecialty3["INJECTABLES"] = "injectables";
|
|
4290
|
+
CertificationSpecialty3["CHEMICAL_PEELS"] = "chemical_peels";
|
|
4291
|
+
CertificationSpecialty3["MICRODERMABRASION"] = "microdermabrasion";
|
|
4292
|
+
CertificationSpecialty3["BODY_CONTOURING"] = "body_contouring";
|
|
4293
|
+
CertificationSpecialty3["SKIN_CARE"] = "skin_care";
|
|
4294
|
+
CertificationSpecialty3["WOUND_CARE"] = "wound_care";
|
|
4295
|
+
CertificationSpecialty3["ANESTHESIA"] = "anesthesia";
|
|
4296
|
+
return CertificationSpecialty3;
|
|
4297
|
+
})(CertificationSpecialty || {});
|
|
4298
|
+
|
|
4299
|
+
// src/validations/practitioner.schema.ts
|
|
4300
|
+
var practitionerBasicInfoSchema = z14.object({
|
|
4301
|
+
firstName: z14.string().min(2).max(50),
|
|
4302
|
+
lastName: z14.string().min(2).max(50),
|
|
4303
|
+
title: z14.string().min(2).max(100),
|
|
4304
|
+
email: z14.string().email(),
|
|
4305
|
+
phoneNumber: z14.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
|
|
4306
|
+
dateOfBirth: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4307
|
+
gender: z14.enum(["male", "female", "other"]),
|
|
4308
|
+
profileImageUrl: mediaResourceSchema.optional(),
|
|
4309
|
+
bio: z14.string().max(1e3).optional(),
|
|
4310
|
+
languages: z14.array(z14.string()).min(1)
|
|
4311
|
+
});
|
|
4312
|
+
var practitionerCertificationSchema = z14.object({
|
|
4313
|
+
level: z14.nativeEnum(CertificationLevel),
|
|
4314
|
+
specialties: z14.array(z14.nativeEnum(CertificationSpecialty)),
|
|
4315
|
+
licenseNumber: z14.string().min(3).max(50),
|
|
4316
|
+
issuingAuthority: z14.string().min(2).max(100),
|
|
4317
|
+
issueDate: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4318
|
+
expiryDate: z14.instanceof(Timestamp9).or(z14.date()).optional(),
|
|
4319
|
+
verificationStatus: z14.enum(["pending", "verified", "rejected"])
|
|
4320
|
+
});
|
|
4321
|
+
var timeSlotSchema = z14.object({
|
|
4322
|
+
start: z14.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format"),
|
|
4323
|
+
end: z14.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format")
|
|
4324
|
+
}).nullable();
|
|
4325
|
+
var practitionerWorkingHoursSchema = z14.object({
|
|
4326
|
+
practitionerId: z14.string().min(1),
|
|
4327
|
+
clinicId: z14.string().min(1),
|
|
4328
|
+
monday: timeSlotSchema,
|
|
4329
|
+
tuesday: timeSlotSchema,
|
|
4330
|
+
wednesday: timeSlotSchema,
|
|
4331
|
+
thursday: timeSlotSchema,
|
|
4332
|
+
friday: timeSlotSchema,
|
|
4333
|
+
saturday: timeSlotSchema,
|
|
4334
|
+
sunday: timeSlotSchema,
|
|
4335
|
+
createdAt: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4336
|
+
updatedAt: z14.instanceof(Timestamp9).or(z14.date())
|
|
4337
|
+
});
|
|
4338
|
+
var practitionerClinicWorkingHoursSchema = z14.object({
|
|
4339
|
+
clinicId: z14.string().min(1),
|
|
4340
|
+
workingHours: z14.object({
|
|
4341
|
+
monday: timeSlotSchema,
|
|
4342
|
+
tuesday: timeSlotSchema,
|
|
4343
|
+
wednesday: timeSlotSchema,
|
|
4344
|
+
thursday: timeSlotSchema,
|
|
4345
|
+
friday: timeSlotSchema,
|
|
4346
|
+
saturday: timeSlotSchema,
|
|
4347
|
+
sunday: timeSlotSchema
|
|
4348
|
+
}),
|
|
4349
|
+
isActive: z14.boolean(),
|
|
4350
|
+
createdAt: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4351
|
+
updatedAt: z14.instanceof(Timestamp9).or(z14.date())
|
|
4352
|
+
});
|
|
4353
|
+
var practitionerSchema = z14.object({
|
|
4354
|
+
id: z14.string().min(1),
|
|
4355
|
+
userRef: z14.string().min(1),
|
|
4356
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
4357
|
+
certification: practitionerCertificationSchema,
|
|
4358
|
+
clinics: z14.array(z14.string()),
|
|
4359
|
+
clinicWorkingHours: z14.array(practitionerClinicWorkingHoursSchema),
|
|
4360
|
+
clinicsInfo: z14.array(clinicInfoSchema),
|
|
4361
|
+
procedures: z14.array(z14.string()),
|
|
4362
|
+
proceduresInfo: z14.array(procedureSummaryInfoSchema),
|
|
4363
|
+
reviewInfo: practitionerReviewInfoSchema,
|
|
4364
|
+
isActive: z14.boolean(),
|
|
4365
|
+
isVerified: z14.boolean(),
|
|
4366
|
+
status: z14.nativeEnum(PractitionerStatus),
|
|
4367
|
+
createdAt: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4368
|
+
updatedAt: z14.instanceof(Timestamp9).or(z14.date())
|
|
4369
|
+
});
|
|
4370
|
+
var createPractitionerSchema = z14.object({
|
|
4371
|
+
userRef: z14.string().min(1),
|
|
4372
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
4373
|
+
certification: practitionerCertificationSchema,
|
|
4374
|
+
clinics: z14.array(z14.string()).optional(),
|
|
4375
|
+
clinicWorkingHours: z14.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4376
|
+
clinicsInfo: z14.array(clinicInfoSchema).optional(),
|
|
4377
|
+
proceduresInfo: z14.array(procedureSummaryInfoSchema).optional(),
|
|
4378
|
+
isActive: z14.boolean(),
|
|
4379
|
+
isVerified: z14.boolean(),
|
|
4380
|
+
status: z14.nativeEnum(PractitionerStatus).optional()
|
|
4381
|
+
});
|
|
4382
|
+
var createDraftPractitionerSchema = z14.object({
|
|
4383
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
4384
|
+
certification: practitionerCertificationSchema,
|
|
4385
|
+
clinics: z14.array(z14.string()).optional(),
|
|
4386
|
+
clinicWorkingHours: z14.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4387
|
+
clinicsInfo: z14.array(clinicInfoSchema).optional(),
|
|
4388
|
+
proceduresInfo: z14.array(procedureSummaryInfoSchema).optional(),
|
|
4389
|
+
isActive: z14.boolean().optional().default(false),
|
|
4390
|
+
isVerified: z14.boolean().optional().default(false)
|
|
4391
|
+
});
|
|
4392
|
+
var practitionerTokenSchema = z14.object({
|
|
4393
|
+
id: z14.string().min(1),
|
|
4394
|
+
token: z14.string().min(6),
|
|
4395
|
+
practitionerId: z14.string().min(1),
|
|
4396
|
+
email: z14.string().email(),
|
|
4397
|
+
clinicId: z14.string().min(1),
|
|
4398
|
+
status: z14.nativeEnum(PractitionerTokenStatus),
|
|
4399
|
+
createdBy: z14.string().min(1),
|
|
4400
|
+
createdAt: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4401
|
+
expiresAt: z14.instanceof(Timestamp9).or(z14.date()),
|
|
4402
|
+
usedBy: z14.string().optional(),
|
|
4403
|
+
usedAt: z14.instanceof(Timestamp9).or(z14.date()).optional()
|
|
4404
|
+
});
|
|
4405
|
+
var createPractitionerTokenSchema = z14.object({
|
|
4406
|
+
practitionerId: z14.string().min(1),
|
|
4407
|
+
email: z14.string().email(),
|
|
4408
|
+
clinicId: z14.string().min(1),
|
|
4409
|
+
expiresAt: z14.date().optional()
|
|
4410
|
+
});
|
|
4411
|
+
var practitionerSignupSchema = z14.object({
|
|
4412
|
+
email: z14.string().email(),
|
|
4413
|
+
password: z14.string().min(8),
|
|
4414
|
+
firstName: z14.string().min(2).max(50).optional(),
|
|
4415
|
+
lastName: z14.string().min(2).max(50).optional(),
|
|
4416
|
+
token: z14.string().optional(),
|
|
4417
|
+
profileData: z14.object({
|
|
4418
|
+
basicInfo: z14.object({
|
|
4419
|
+
phoneNumber: z14.string().optional(),
|
|
4420
|
+
profileImageUrl: mediaResourceSchema.optional(),
|
|
4421
|
+
gender: z14.enum(["male", "female", "other"]).optional(),
|
|
4422
|
+
bio: z14.string().optional()
|
|
4423
|
+
}).optional(),
|
|
4424
|
+
certification: z14.any().optional()
|
|
4425
|
+
}).optional()
|
|
4426
|
+
});
|
|
4427
|
+
|
|
4428
|
+
// src/services/practitioner/practitioner.service.ts
|
|
4429
|
+
import { z as z15 } from "zod";
|
|
4430
|
+
import { distanceBetween } from "geofire-common";
|
|
4431
|
+
var PractitionerService = class extends BaseService {
|
|
4432
|
+
constructor(db, auth, app, clinicService) {
|
|
4433
|
+
super(db, auth, app);
|
|
4434
|
+
this.clinicService = clinicService;
|
|
4435
|
+
this.mediaService = new MediaService(db, auth, app);
|
|
4436
|
+
}
|
|
4437
|
+
getClinicService() {
|
|
4438
|
+
if (!this.clinicService) {
|
|
4439
|
+
throw new Error("Clinic service not initialized!");
|
|
4440
|
+
}
|
|
4441
|
+
return this.clinicService;
|
|
4442
|
+
}
|
|
4443
|
+
setClinicService(clinicService) {
|
|
4444
|
+
this.clinicService = clinicService;
|
|
4445
|
+
}
|
|
4446
|
+
/**
|
|
4447
|
+
* Handles profile photo upload for practitioners
|
|
4448
|
+
* @param profilePhoto - MediaResource (File, Blob, or URL string)
|
|
4449
|
+
* @param practitionerId - ID of the practitioner
|
|
4450
|
+
* @returns URL string of the uploaded or existing photo
|
|
4451
|
+
*/
|
|
4452
|
+
async handleProfilePhotoUpload(profilePhoto, practitionerId) {
|
|
4453
|
+
if (!profilePhoto) {
|
|
4454
|
+
return void 0;
|
|
4455
|
+
}
|
|
4456
|
+
if (typeof profilePhoto === "string") {
|
|
4457
|
+
return profilePhoto;
|
|
4458
|
+
}
|
|
4459
|
+
if (profilePhoto instanceof File || profilePhoto instanceof Blob) {
|
|
4460
|
+
console.log(
|
|
4461
|
+
`[PractitionerService] Uploading profile photo for practitioner ${practitionerId}`
|
|
4462
|
+
);
|
|
4463
|
+
const mediaMetadata = await this.mediaService.uploadMedia(
|
|
4464
|
+
profilePhoto,
|
|
4465
|
+
practitionerId,
|
|
4466
|
+
// Using practitionerId as ownerId
|
|
4467
|
+
"public" /* PUBLIC */,
|
|
4468
|
+
// Profile photos should be public
|
|
4469
|
+
"practitioner_profile_photos",
|
|
4470
|
+
profilePhoto instanceof File ? profilePhoto.name : `profile_photo_${practitionerId}`
|
|
4471
|
+
);
|
|
4472
|
+
return mediaMetadata.url;
|
|
4473
|
+
}
|
|
4474
|
+
return void 0;
|
|
4475
|
+
}
|
|
4476
|
+
/**
|
|
4477
|
+
* Processes BasicPractitionerInfo to handle profile photo uploads
|
|
4478
|
+
* @param basicInfo - The basic info containing potential MediaResource profile photo
|
|
4479
|
+
* @param practitionerId - ID of the practitioner
|
|
4480
|
+
* @returns Processed basic info with URL string for profileImageUrl
|
|
4481
|
+
*/
|
|
4482
|
+
async processBasicInfo(basicInfo, practitionerId) {
|
|
4483
|
+
const processedBasicInfo = { ...basicInfo };
|
|
4484
|
+
if (basicInfo.profileImageUrl) {
|
|
4485
|
+
const uploadedUrl = await this.handleProfilePhotoUpload(
|
|
4486
|
+
basicInfo.profileImageUrl,
|
|
4487
|
+
practitionerId
|
|
4488
|
+
);
|
|
4489
|
+
processedBasicInfo.profileImageUrl = uploadedUrl;
|
|
4490
|
+
}
|
|
4491
|
+
return processedBasicInfo;
|
|
4492
|
+
}
|
|
4493
|
+
/**
|
|
4494
|
+
* Creates a new practitioner
|
|
4495
|
+
*/
|
|
4496
|
+
async createPractitioner(data) {
|
|
4497
|
+
try {
|
|
4498
|
+
const validData = createPractitionerSchema.parse(data);
|
|
4499
|
+
const practitionerId = this.generateId();
|
|
4500
|
+
const reviewInfo = {
|
|
4501
|
+
totalReviews: 0,
|
|
4502
|
+
averageRating: 0,
|
|
4503
|
+
knowledgeAndExpertise: 0,
|
|
4504
|
+
communicationSkills: 0,
|
|
4505
|
+
bedSideManner: 0,
|
|
4506
|
+
thoroughness: 0,
|
|
4507
|
+
trustworthiness: 0,
|
|
4508
|
+
recommendationPercentage: 0
|
|
4509
|
+
};
|
|
4510
|
+
const practitioner = {
|
|
4511
|
+
id: practitionerId,
|
|
4512
|
+
userRef: validData.userRef,
|
|
4513
|
+
basicInfo: await this.processBasicInfo(
|
|
4514
|
+
validData.basicInfo,
|
|
4515
|
+
practitionerId
|
|
4516
|
+
),
|
|
4517
|
+
certification: validData.certification,
|
|
4518
|
+
clinics: validData.clinics || [],
|
|
4519
|
+
clinicWorkingHours: validData.clinicWorkingHours || [],
|
|
4520
|
+
clinicsInfo: [],
|
|
4521
|
+
procedures: [],
|
|
4522
|
+
proceduresInfo: [],
|
|
4523
|
+
reviewInfo,
|
|
4524
|
+
isActive: validData.isActive !== void 0 ? validData.isActive : true,
|
|
4525
|
+
isVerified: validData.isVerified !== void 0 ? validData.isVerified : false,
|
|
4526
|
+
status: validData.status || "active" /* ACTIVE */,
|
|
4527
|
+
createdAt: serverTimestamp9(),
|
|
4528
|
+
updatedAt: serverTimestamp9()
|
|
4529
|
+
};
|
|
4530
|
+
practitionerSchema.parse({
|
|
4531
|
+
...practitioner,
|
|
4532
|
+
createdAt: Timestamp10.now(),
|
|
4533
|
+
updatedAt: Timestamp10.now()
|
|
4534
|
+
});
|
|
4535
|
+
const practitionerRef = doc9(
|
|
4536
|
+
this.db,
|
|
4537
|
+
PRACTITIONERS_COLLECTION,
|
|
4538
|
+
practitionerId
|
|
4539
|
+
);
|
|
4540
|
+
await setDoc8(practitionerRef, practitioner);
|
|
4541
|
+
const createdPractitioner = await this.getPractitioner(practitionerId);
|
|
4542
|
+
if (!createdPractitioner) {
|
|
4543
|
+
throw new Error(
|
|
4544
|
+
`Failed to retrieve created practitioner ${practitionerId}`
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
return createdPractitioner;
|
|
4548
|
+
} catch (error) {
|
|
4549
|
+
if (error instanceof z15.ZodError) {
|
|
4550
|
+
throw new Error(`Invalid practitioner data: ${error.message}`);
|
|
4551
|
+
}
|
|
4552
|
+
console.error("Error creating practitioner:", error);
|
|
4553
|
+
throw error;
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
/**
|
|
4557
|
+
* Kreira novi draft profil zdravstvenog radnika bez povezanog korisnika
|
|
4558
|
+
* Koristi se od strane administratora klinike za kreiranje profila i kasnije pozivanje
|
|
4559
|
+
* @param data Podaci za kreiranje draft profila
|
|
4560
|
+
* @param createdBy ID administratora koji kreira profil
|
|
4561
|
+
* @param clinicId ID klinike za koju se kreira profil
|
|
4562
|
+
* @returns Objekt koji sadrži kreirani draft profil i token za registraciju
|
|
4563
|
+
*/
|
|
4564
|
+
async createDraftPractitioner(data, createdBy, clinicId) {
|
|
4565
|
+
try {
|
|
4566
|
+
const validatedData = createDraftPractitionerSchema.parse(data);
|
|
4567
|
+
const clinic = await this.getClinicService().getClinic(clinicId);
|
|
4568
|
+
if (!clinic) {
|
|
4569
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
4570
|
+
}
|
|
4571
|
+
const clinicsToAdd = /* @__PURE__ */ new Set([clinicId]);
|
|
4572
|
+
if (data.clinics && data.clinics.length > 0) {
|
|
4573
|
+
for (const cId of data.clinics) {
|
|
4574
|
+
if (cId !== clinicId) {
|
|
4575
|
+
const otherClinic = await this.getClinicService().getClinic(cId);
|
|
4576
|
+
if (!otherClinic) {
|
|
4577
|
+
throw new Error(`Clinic ${cId} not found`);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
clinicsToAdd.add(cId);
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
const clinics = Array.from(clinicsToAdd);
|
|
4584
|
+
const defaultReviewInfo = {
|
|
4585
|
+
totalReviews: 0,
|
|
4586
|
+
averageRating: 0,
|
|
4587
|
+
knowledgeAndExpertise: 0,
|
|
4588
|
+
communicationSkills: 0,
|
|
4589
|
+
bedSideManner: 0,
|
|
4590
|
+
thoroughness: 0,
|
|
4591
|
+
trustworthiness: 0,
|
|
4592
|
+
recommendationPercentage: 0
|
|
4593
|
+
};
|
|
4594
|
+
const practitionerId = this.generateId();
|
|
4595
|
+
const clinicsInfo = [];
|
|
4596
|
+
for (const cId of clinics) {
|
|
4597
|
+
const clinicData = await this.getClinicService().getClinic(cId);
|
|
4598
|
+
if (clinicData) {
|
|
4599
|
+
clinicsInfo.push({
|
|
4600
|
+
id: clinicData.id,
|
|
4601
|
+
name: clinicData.name,
|
|
4602
|
+
location: clinicData.location,
|
|
4603
|
+
contactInfo: clinicData.contactInfo,
|
|
4604
|
+
// Make sure we're using the right property for featuredPhoto
|
|
4605
|
+
featuredPhoto: clinicData.featuredPhotos && clinicData.featuredPhotos.length > 0 ? typeof clinicData.featuredPhotos[0] === "string" ? clinicData.featuredPhotos[0] : "" : (typeof clinicData.coverPhoto === "string" ? clinicData.coverPhoto : "") || "",
|
|
4606
|
+
description: clinicData.description || null
|
|
4607
|
+
});
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
const finalClinicsInfo = validatedData.clinicsInfo && validatedData.clinicsInfo.length > 0 ? validatedData.clinicsInfo : clinicsInfo;
|
|
4611
|
+
const proceduresInfo = [];
|
|
4612
|
+
const practitionerData = {
|
|
4613
|
+
id: practitionerId,
|
|
4614
|
+
userRef: "",
|
|
4615
|
+
// Prazno - biće popunjeno kada korisnik kreira nalog
|
|
4616
|
+
basicInfo: await this.processBasicInfo(
|
|
4617
|
+
validatedData.basicInfo,
|
|
4618
|
+
practitionerId
|
|
4619
|
+
),
|
|
4620
|
+
certification: validatedData.certification,
|
|
4621
|
+
clinics,
|
|
4622
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
4623
|
+
clinicsInfo: finalClinicsInfo,
|
|
4624
|
+
procedures: [],
|
|
4625
|
+
proceduresInfo,
|
|
4626
|
+
reviewInfo: defaultReviewInfo,
|
|
4627
|
+
isActive: validatedData.isActive !== void 0 ? validatedData.isActive : false,
|
|
4628
|
+
isVerified: validatedData.isVerified !== void 0 ? validatedData.isVerified : false,
|
|
4629
|
+
status: "draft" /* DRAFT */,
|
|
4630
|
+
createdAt: serverTimestamp9(),
|
|
4631
|
+
updatedAt: serverTimestamp9()
|
|
4632
|
+
};
|
|
4633
|
+
practitionerSchema.parse({
|
|
4634
|
+
...practitionerData,
|
|
4635
|
+
userRef: "temp-for-validation",
|
|
4636
|
+
createdAt: Timestamp10.now(),
|
|
4637
|
+
updatedAt: Timestamp10.now()
|
|
4638
|
+
});
|
|
4639
|
+
await setDoc8(
|
|
4640
|
+
doc9(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
|
|
4641
|
+
practitionerData
|
|
4642
|
+
);
|
|
4643
|
+
const savedPractitioner = await this.getPractitioner(practitionerData.id);
|
|
4644
|
+
if (!savedPractitioner) {
|
|
4645
|
+
throw new Error("Failed to create draft practitioner profile");
|
|
4646
|
+
}
|
|
4647
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
4648
|
+
const expiration = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
4649
|
+
const token = {
|
|
4650
|
+
id: this.generateId(),
|
|
4651
|
+
token: tokenString,
|
|
4652
|
+
practitionerId,
|
|
4653
|
+
email: practitionerData.basicInfo.email,
|
|
4654
|
+
clinicId,
|
|
4655
|
+
status: "active" /* ACTIVE */,
|
|
4656
|
+
createdBy,
|
|
4657
|
+
createdAt: Timestamp10.now(),
|
|
4658
|
+
expiresAt: Timestamp10.fromDate(expiration)
|
|
4659
|
+
};
|
|
4660
|
+
practitionerTokenSchema.parse(token);
|
|
4661
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4662
|
+
await setDoc8(doc9(this.db, tokenPath), token);
|
|
4663
|
+
return { practitioner: savedPractitioner, token };
|
|
4664
|
+
} catch (error) {
|
|
4665
|
+
if (error instanceof z15.ZodError) {
|
|
4666
|
+
throw new Error("Invalid practitioner data: " + error.message);
|
|
4667
|
+
}
|
|
4668
|
+
throw error;
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
/**
|
|
4672
|
+
* Creates a token for inviting practitioner to claim their profile
|
|
4673
|
+
* @param data Data for creating token
|
|
4674
|
+
* @param createdBy ID of the user creating the token
|
|
4675
|
+
* @returns Created token
|
|
4676
|
+
*/
|
|
4677
|
+
async createPractitionerToken(data, createdBy) {
|
|
4678
|
+
try {
|
|
4679
|
+
const validatedData = createPractitionerTokenSchema.parse(data);
|
|
4680
|
+
const practitioner = await this.getPractitioner(
|
|
4681
|
+
validatedData.practitionerId
|
|
4682
|
+
);
|
|
4683
|
+
if (!practitioner) {
|
|
4684
|
+
throw new Error("Practitioner not found");
|
|
4685
|
+
}
|
|
4686
|
+
if (practitioner.status !== "draft" /* DRAFT */) {
|
|
4687
|
+
throw new Error(
|
|
4688
|
+
"Can only create tokens for practitioners in DRAFT status"
|
|
4689
|
+
);
|
|
4690
|
+
}
|
|
4691
|
+
const clinic = await this.getClinicService().getClinic(
|
|
4692
|
+
validatedData.clinicId
|
|
4693
|
+
);
|
|
4694
|
+
if (!clinic) {
|
|
4695
|
+
throw new Error(`Clinic ${validatedData.clinicId} not found`);
|
|
4696
|
+
}
|
|
4697
|
+
if (!practitioner.clinics.includes(validatedData.clinicId)) {
|
|
4698
|
+
throw new Error("Practitioner is not associated with this clinic");
|
|
4699
|
+
}
|
|
4700
|
+
const expiration = validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
4701
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
4702
|
+
const token = {
|
|
4703
|
+
id: this.generateId(),
|
|
4704
|
+
token: tokenString,
|
|
4705
|
+
practitionerId: validatedData.practitionerId,
|
|
4706
|
+
email: validatedData.email,
|
|
4707
|
+
clinicId: validatedData.clinicId,
|
|
4708
|
+
status: "active" /* ACTIVE */,
|
|
4709
|
+
createdBy,
|
|
4710
|
+
createdAt: Timestamp10.now(),
|
|
4711
|
+
expiresAt: Timestamp10.fromDate(expiration)
|
|
4712
|
+
};
|
|
4713
|
+
practitionerTokenSchema.parse(token);
|
|
4714
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4715
|
+
await setDoc8(doc9(this.db, tokenPath), token);
|
|
4716
|
+
return token;
|
|
4717
|
+
} catch (error) {
|
|
4718
|
+
if (error instanceof z15.ZodError) {
|
|
4719
|
+
throw new Error("Invalid token data: " + error.message);
|
|
4720
|
+
}
|
|
4721
|
+
throw error;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
/**
|
|
4725
|
+
* Gets active tokens for a practitioner
|
|
4726
|
+
* @param practitionerId ID of the practitioner
|
|
4727
|
+
* @returns Array of active tokens
|
|
4728
|
+
*/
|
|
4729
|
+
async getPractitionerActiveTokens(practitionerId) {
|
|
4730
|
+
const tokensRef = collection7(
|
|
4731
|
+
this.db,
|
|
4732
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
4733
|
+
);
|
|
4734
|
+
const q = query7(
|
|
4735
|
+
tokensRef,
|
|
4736
|
+
where7("status", "==", "active" /* ACTIVE */),
|
|
4737
|
+
where7("expiresAt", ">", Timestamp10.now())
|
|
4738
|
+
);
|
|
4739
|
+
const querySnapshot = await getDocs7(q);
|
|
4740
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4741
|
+
}
|
|
4742
|
+
/**
|
|
4743
|
+
* Gets a token by its string value and validates it
|
|
4744
|
+
* @param tokenString The token string to find
|
|
4745
|
+
* @returns The token if found and valid, null otherwise
|
|
4746
|
+
*/
|
|
4747
|
+
async validateToken(tokenString) {
|
|
4748
|
+
const practitionersRef = collection7(this.db, PRACTITIONERS_COLLECTION);
|
|
4749
|
+
const practitionersSnapshot = await getDocs7(practitionersRef);
|
|
4750
|
+
for (const practitionerDoc of practitionersSnapshot.docs) {
|
|
4751
|
+
const practitionerId = practitionerDoc.id;
|
|
4752
|
+
const tokensRef = collection7(
|
|
4753
|
+
this.db,
|
|
4754
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
4755
|
+
);
|
|
4756
|
+
console.log(
|
|
4757
|
+
`[PRACTITIONER] Validating token for practitioner ${practitionerId}`,
|
|
4758
|
+
{
|
|
4759
|
+
tokenString,
|
|
4760
|
+
timestamp: Timestamp10.now().toDate()
|
|
4761
|
+
}
|
|
4762
|
+
);
|
|
4763
|
+
const q = query7(
|
|
4764
|
+
tokensRef,
|
|
4765
|
+
where7("token", "==", tokenString),
|
|
4766
|
+
where7("status", "==", "active" /* ACTIVE */),
|
|
4767
|
+
where7("expiresAt", ">", Timestamp10.now())
|
|
4768
|
+
);
|
|
4769
|
+
try {
|
|
4770
|
+
const tokenSnapshot = await getDocs7(q);
|
|
4771
|
+
console.log(
|
|
4772
|
+
`[PRACTITIONER] Token query results for practitioner ${practitionerId}`,
|
|
4773
|
+
{
|
|
4774
|
+
found: !tokenSnapshot.empty,
|
|
4775
|
+
count: tokenSnapshot.size
|
|
4776
|
+
}
|
|
4777
|
+
);
|
|
4778
|
+
if (!tokenSnapshot.empty) {
|
|
4779
|
+
const tokenData = tokenSnapshot.docs[0].data();
|
|
4780
|
+
console.log(`[PRACTITIONER] Valid token found`, {
|
|
4781
|
+
tokenId: tokenData.id,
|
|
4782
|
+
expiresAt: tokenData.expiresAt.toDate()
|
|
4783
|
+
});
|
|
4784
|
+
return tokenData;
|
|
4785
|
+
}
|
|
4786
|
+
} catch (error) {
|
|
4787
|
+
console.error(
|
|
4788
|
+
`[PRACTITIONER] Error validating token for practitioner ${practitionerId}:`,
|
|
4789
|
+
error
|
|
4790
|
+
);
|
|
4791
|
+
throw error;
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
return null;
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Marks a token as used
|
|
4798
|
+
* @param tokenId ID of the token
|
|
4799
|
+
* @param practitionerId ID of the practitioner
|
|
4800
|
+
* @param userId ID of the user using the token
|
|
4801
|
+
*/
|
|
4802
|
+
async markTokenAsUsed(tokenId, practitionerId, userId) {
|
|
4803
|
+
const tokenRef = doc9(
|
|
4804
|
+
this.db,
|
|
4805
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
|
|
4806
|
+
);
|
|
4807
|
+
await updateDoc9(tokenRef, {
|
|
4808
|
+
status: "used" /* USED */,
|
|
4809
|
+
usedBy: userId,
|
|
4810
|
+
usedAt: Timestamp10.now()
|
|
4811
|
+
});
|
|
4812
|
+
}
|
|
4813
|
+
/**
|
|
4814
|
+
* Dohvata zdravstvenog radnika po ID-u
|
|
4815
|
+
*/
|
|
4816
|
+
async getPractitioner(practitionerId) {
|
|
4817
|
+
const practitionerDoc = await getDoc12(
|
|
4818
|
+
doc9(this.db, PRACTITIONERS_COLLECTION, practitionerId)
|
|
4819
|
+
);
|
|
4820
|
+
if (!practitionerDoc.exists()) {
|
|
4821
|
+
return null;
|
|
4822
|
+
}
|
|
4823
|
+
return practitionerDoc.data();
|
|
4824
|
+
}
|
|
4825
|
+
/**
|
|
4826
|
+
* Dohvata zdravstvenog radnika po User ID-u
|
|
4827
|
+
*/
|
|
4828
|
+
async getPractitionerByUserRef(userRef) {
|
|
4829
|
+
const q = query7(
|
|
4830
|
+
collection7(this.db, PRACTITIONERS_COLLECTION),
|
|
4831
|
+
where7("userRef", "==", userRef)
|
|
4832
|
+
);
|
|
4833
|
+
const querySnapshot = await getDocs7(q);
|
|
4834
|
+
if (querySnapshot.empty) {
|
|
4835
|
+
return null;
|
|
4836
|
+
}
|
|
4837
|
+
return querySnapshot.docs[0].data();
|
|
4838
|
+
}
|
|
4839
|
+
/**
|
|
4840
|
+
* Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
|
|
4841
|
+
*/
|
|
4842
|
+
async getPractitionersByClinic(clinicId) {
|
|
4843
|
+
const q = query7(
|
|
4844
|
+
collection7(this.db, PRACTITIONERS_COLLECTION),
|
|
4845
|
+
where7("clinics", "array-contains", clinicId),
|
|
4846
|
+
where7("isActive", "==", true),
|
|
4847
|
+
where7("status", "==", "active" /* ACTIVE */)
|
|
4848
|
+
);
|
|
4849
|
+
const querySnapshot = await getDocs7(q);
|
|
4850
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4851
|
+
}
|
|
4852
|
+
/**
|
|
4853
|
+
* Dohvata sve zdravstvene radnike za određenu kliniku
|
|
4854
|
+
*/
|
|
4855
|
+
async getAllPractitionersByClinic(clinicId) {
|
|
4856
|
+
const q = query7(
|
|
4857
|
+
collection7(this.db, PRACTITIONERS_COLLECTION),
|
|
4858
|
+
where7("clinics", "array-contains", clinicId),
|
|
4859
|
+
where7("isActive", "==", true)
|
|
4860
|
+
);
|
|
4861
|
+
const querySnapshot = await getDocs7(q);
|
|
4862
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4863
|
+
}
|
|
4864
|
+
/**
|
|
4865
|
+
* Dohvata sve draft zdravstvene radnike za određenu kliniku sa statusom DRAFT
|
|
4866
|
+
*/
|
|
4867
|
+
async getDraftPractitionersByClinic(clinicId) {
|
|
4868
|
+
const q = query7(
|
|
4869
|
+
collection7(this.db, PRACTITIONERS_COLLECTION),
|
|
4870
|
+
where7("clinics", "array-contains", clinicId),
|
|
4871
|
+
where7("status", "==", "draft" /* DRAFT */)
|
|
4872
|
+
);
|
|
4873
|
+
const querySnapshot = await getDocs7(q);
|
|
4874
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4875
|
+
}
|
|
4876
|
+
/**
|
|
4877
|
+
* Updates a practitioner
|
|
4878
|
+
*/
|
|
4879
|
+
async updatePractitioner(practitionerId, data) {
|
|
4880
|
+
try {
|
|
4881
|
+
const validData = data;
|
|
4882
|
+
const practitionerRef = doc9(
|
|
4883
|
+
this.db,
|
|
4884
|
+
PRACTITIONERS_COLLECTION,
|
|
4885
|
+
practitionerId
|
|
4886
|
+
);
|
|
4887
|
+
const practitionerDoc = await getDoc12(practitionerRef);
|
|
4888
|
+
if (!practitionerDoc.exists()) {
|
|
4889
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4890
|
+
}
|
|
4891
|
+
const currentPractitioner = practitionerDoc.data();
|
|
4892
|
+
let processedData = { ...validData };
|
|
4893
|
+
if (validData.basicInfo) {
|
|
4894
|
+
processedData.basicInfo = await this.processBasicInfo(
|
|
4895
|
+
validData.basicInfo,
|
|
4896
|
+
practitionerId
|
|
4897
|
+
);
|
|
4898
|
+
}
|
|
4899
|
+
const updateData = {
|
|
4900
|
+
...processedData,
|
|
4901
|
+
updatedAt: serverTimestamp9()
|
|
4902
|
+
};
|
|
4903
|
+
await updateDoc9(practitionerRef, updateData);
|
|
4904
|
+
const updatedPractitioner = await this.getPractitioner(practitionerId);
|
|
4905
|
+
if (!updatedPractitioner) {
|
|
4906
|
+
throw new Error(
|
|
4907
|
+
`Failed to retrieve updated practitioner ${practitionerId}`
|
|
4908
|
+
);
|
|
4909
|
+
}
|
|
4910
|
+
return updatedPractitioner;
|
|
4911
|
+
} catch (error) {
|
|
4912
|
+
if (error instanceof z15.ZodError) {
|
|
4913
|
+
throw new Error(`Invalid practitioner update data: ${error.message}`);
|
|
4914
|
+
}
|
|
4915
|
+
console.error(`Error updating practitioner ${practitionerId}:`, error);
|
|
4916
|
+
throw error;
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
/**
|
|
4920
|
+
* Adds a clinic to a practitioner
|
|
4921
|
+
*/
|
|
4922
|
+
async addClinic(practitionerId, clinicId) {
|
|
4923
|
+
var _a;
|
|
4924
|
+
try {
|
|
4925
|
+
const practitionerRef = doc9(
|
|
4926
|
+
this.db,
|
|
4927
|
+
PRACTITIONERS_COLLECTION,
|
|
4928
|
+
practitionerId
|
|
4929
|
+
);
|
|
4930
|
+
const practitionerDoc = await getDoc12(practitionerRef);
|
|
4931
|
+
if (!practitionerDoc.exists()) {
|
|
4932
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4933
|
+
}
|
|
4934
|
+
const practitioner = practitionerDoc.data();
|
|
4935
|
+
if ((_a = practitioner.clinics) == null ? void 0 : _a.includes(clinicId)) {
|
|
4936
|
+
console.log(
|
|
4937
|
+
`Clinic ${clinicId} already added to practitioner ${practitionerId}`
|
|
4938
|
+
);
|
|
4939
|
+
return;
|
|
4940
|
+
}
|
|
4941
|
+
await updateDoc9(practitionerRef, {
|
|
4942
|
+
clinics: arrayUnion5(clinicId),
|
|
4943
|
+
updatedAt: serverTimestamp9()
|
|
4944
|
+
});
|
|
4945
|
+
} catch (error) {
|
|
4946
|
+
console.error(
|
|
4947
|
+
`Error adding clinic ${clinicId} to practitioner ${practitionerId}:`,
|
|
4948
|
+
error
|
|
4949
|
+
);
|
|
4950
|
+
throw error;
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
/**
|
|
4954
|
+
* Removes a clinic from a practitioner
|
|
4955
|
+
*/
|
|
4956
|
+
async removeClinic(practitionerId, clinicId) {
|
|
4957
|
+
try {
|
|
4958
|
+
const practitionerRef = doc9(
|
|
4959
|
+
this.db,
|
|
4960
|
+
PRACTITIONERS_COLLECTION,
|
|
4961
|
+
practitionerId
|
|
4962
|
+
);
|
|
4963
|
+
const practitionerDoc = await getDoc12(practitionerRef);
|
|
4964
|
+
if (!practitionerDoc.exists()) {
|
|
4965
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4966
|
+
}
|
|
4967
|
+
await updateDoc9(practitionerRef, {
|
|
4968
|
+
clinics: arrayRemove4(clinicId),
|
|
4969
|
+
updatedAt: serverTimestamp9()
|
|
4970
|
+
});
|
|
4971
|
+
} catch (error) {
|
|
4972
|
+
console.error(
|
|
4973
|
+
`Error removing clinic ${clinicId} from practitioner ${practitionerId}:`,
|
|
4974
|
+
error
|
|
4975
|
+
);
|
|
4976
|
+
throw error;
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
/**
|
|
4980
|
+
* Deaktivira profil zdravstvenog radnika
|
|
4981
|
+
*/
|
|
4982
|
+
async deactivatePractitioner(practitionerId) {
|
|
4983
|
+
await this.updatePractitioner(practitionerId, {
|
|
4984
|
+
isActive: false
|
|
4985
|
+
});
|
|
4986
|
+
}
|
|
4987
|
+
/**
|
|
4988
|
+
* Aktivira profil zdravstvenog radnika
|
|
4989
|
+
*/
|
|
4990
|
+
async activatePractitioner(practitionerId) {
|
|
4991
|
+
await this.updatePractitioner(practitionerId, {
|
|
4992
|
+
isActive: true
|
|
4993
|
+
});
|
|
4617
4994
|
}
|
|
4618
4995
|
/**
|
|
4619
4996
|
* Briše profil zdravstvenog radnika
|
|
@@ -4623,7 +5000,7 @@ var PractitionerService = class extends BaseService {
|
|
|
4623
5000
|
if (!practitioner) {
|
|
4624
5001
|
throw new Error("Practitioner not found");
|
|
4625
5002
|
}
|
|
4626
|
-
await
|
|
5003
|
+
await deleteDoc3(doc9(this.db, PRACTITIONERS_COLLECTION, practitionerId));
|
|
4627
5004
|
}
|
|
4628
5005
|
/**
|
|
4629
5006
|
* Validates a registration token and claims the associated draft practitioner profile
|
|
@@ -4692,21 +5069,21 @@ var PractitionerService = class extends BaseService {
|
|
|
4692
5069
|
try {
|
|
4693
5070
|
const constraints = [];
|
|
4694
5071
|
if (!(options == null ? void 0 : options.includeDraftPractitioners)) {
|
|
4695
|
-
constraints.push(
|
|
5072
|
+
constraints.push(where7("status", "==", "active" /* ACTIVE */));
|
|
4696
5073
|
}
|
|
4697
|
-
constraints.push(
|
|
4698
|
-
constraints.push(
|
|
5074
|
+
constraints.push(orderBy2("basicInfo.lastName", "asc"));
|
|
5075
|
+
constraints.push(orderBy2("basicInfo.firstName", "asc"));
|
|
4699
5076
|
if ((options == null ? void 0 : options.pagination) && options.pagination > 0) {
|
|
4700
5077
|
if (options.lastDoc) {
|
|
4701
5078
|
constraints.push(startAfter4(options.lastDoc));
|
|
4702
5079
|
}
|
|
4703
|
-
constraints.push(
|
|
5080
|
+
constraints.push(limit5(options.pagination));
|
|
4704
5081
|
}
|
|
4705
|
-
const q =
|
|
4706
|
-
|
|
5082
|
+
const q = query7(
|
|
5083
|
+
collection7(this.db, PRACTITIONERS_COLLECTION),
|
|
4707
5084
|
...constraints
|
|
4708
5085
|
);
|
|
4709
|
-
const querySnapshot = await
|
|
5086
|
+
const querySnapshot = await getDocs7(q);
|
|
4710
5087
|
const practitioners = querySnapshot.docs.map(
|
|
4711
5088
|
(doc34) => doc34.data()
|
|
4712
5089
|
);
|
|
@@ -4751,31 +5128,31 @@ var PractitionerService = class extends BaseService {
|
|
|
4751
5128
|
);
|
|
4752
5129
|
const constraints = [];
|
|
4753
5130
|
if (!filters.includeDraftPractitioners) {
|
|
4754
|
-
constraints.push(
|
|
5131
|
+
constraints.push(where7("status", "==", "active" /* ACTIVE */));
|
|
4755
5132
|
}
|
|
4756
|
-
constraints.push(
|
|
5133
|
+
constraints.push(where7("isActive", "==", true));
|
|
4757
5134
|
if (filters.certifications && filters.certifications.length > 0) {
|
|
4758
5135
|
constraints.push(
|
|
4759
|
-
|
|
5136
|
+
where7(
|
|
4760
5137
|
"certification.certifications",
|
|
4761
5138
|
"array-contains-any",
|
|
4762
5139
|
filters.certifications
|
|
4763
5140
|
)
|
|
4764
5141
|
);
|
|
4765
5142
|
}
|
|
4766
|
-
constraints.push(
|
|
4767
|
-
constraints.push(
|
|
5143
|
+
constraints.push(orderBy2("basicInfo.lastName", "asc"));
|
|
5144
|
+
constraints.push(orderBy2("basicInfo.firstName", "asc"));
|
|
4768
5145
|
if (filters.pagination && filters.pagination > 0) {
|
|
4769
5146
|
if (filters.lastDoc) {
|
|
4770
5147
|
constraints.push(startAfter4(filters.lastDoc));
|
|
4771
5148
|
}
|
|
4772
|
-
constraints.push(
|
|
5149
|
+
constraints.push(limit5(filters.pagination));
|
|
4773
5150
|
}
|
|
4774
|
-
const q =
|
|
4775
|
-
|
|
5151
|
+
const q = query7(
|
|
5152
|
+
collection7(this.db, PRACTITIONERS_COLLECTION),
|
|
4776
5153
|
...constraints
|
|
4777
5154
|
);
|
|
4778
|
-
const querySnapshot = await
|
|
5155
|
+
const querySnapshot = await getDocs7(q);
|
|
4779
5156
|
console.log(
|
|
4780
5157
|
`[PRACTITIONER_SERVICE] Found ${querySnapshot.docs.length} practitioners with base query`
|
|
4781
5158
|
);
|
|
@@ -4901,7 +5278,7 @@ var UserService = class extends BaseService {
|
|
|
4901
5278
|
updatedAt: serverTimestamp10(),
|
|
4902
5279
|
lastLoginAt: serverTimestamp10()
|
|
4903
5280
|
};
|
|
4904
|
-
await
|
|
5281
|
+
await setDoc9(doc10(this.db, USERS_COLLECTION, userData.uid), userData);
|
|
4905
5282
|
if (options == null ? void 0 : options.skipProfileCreation) {
|
|
4906
5283
|
return this.getUserById(userData.uid);
|
|
4907
5284
|
}
|
|
@@ -4910,7 +5287,7 @@ var UserService = class extends BaseService {
|
|
|
4910
5287
|
roles,
|
|
4911
5288
|
options
|
|
4912
5289
|
);
|
|
4913
|
-
await
|
|
5290
|
+
await updateDoc10(doc10(this.db, USERS_COLLECTION, userData.uid), profiles);
|
|
4914
5291
|
return this.getUserById(userData.uid);
|
|
4915
5292
|
}
|
|
4916
5293
|
/**
|
|
@@ -4988,7 +5365,7 @@ var UserService = class extends BaseService {
|
|
|
4988
5365
|
email: "",
|
|
4989
5366
|
phoneNumber: "",
|
|
4990
5367
|
title: "",
|
|
4991
|
-
dateOfBirth:
|
|
5368
|
+
dateOfBirth: Timestamp11.now(),
|
|
4992
5369
|
gender: "other",
|
|
4993
5370
|
languages: ["Serbian"]
|
|
4994
5371
|
},
|
|
@@ -4997,7 +5374,7 @@ var UserService = class extends BaseService {
|
|
|
4997
5374
|
specialties: [],
|
|
4998
5375
|
licenseNumber: "",
|
|
4999
5376
|
issuingAuthority: "",
|
|
5000
|
-
issueDate:
|
|
5377
|
+
issueDate: Timestamp11.now(),
|
|
5001
5378
|
verificationStatus: "pending"
|
|
5002
5379
|
},
|
|
5003
5380
|
isActive: true,
|
|
@@ -5013,7 +5390,7 @@ var UserService = class extends BaseService {
|
|
|
5013
5390
|
* Dohvata korisnika po ID-u
|
|
5014
5391
|
*/
|
|
5015
5392
|
async getUserById(uid) {
|
|
5016
|
-
const userDoc = await
|
|
5393
|
+
const userDoc = await getDoc13(doc10(this.db, USERS_COLLECTION, uid));
|
|
5017
5394
|
if (!userDoc.exists()) {
|
|
5018
5395
|
throw USER_ERRORS.NOT_FOUND;
|
|
5019
5396
|
}
|
|
@@ -5024,19 +5401,19 @@ var UserService = class extends BaseService {
|
|
|
5024
5401
|
* Dohvata korisnika po email-u
|
|
5025
5402
|
*/
|
|
5026
5403
|
async getUserByEmail(email) {
|
|
5027
|
-
const usersRef =
|
|
5028
|
-
const q =
|
|
5029
|
-
const querySnapshot = await
|
|
5404
|
+
const usersRef = collection8(this.db, USERS_COLLECTION);
|
|
5405
|
+
const q = query8(usersRef, where8("email", "==", email));
|
|
5406
|
+
const querySnapshot = await getDocs8(q);
|
|
5030
5407
|
if (querySnapshot.empty) return null;
|
|
5031
5408
|
const userData = querySnapshot.docs[0].data();
|
|
5032
5409
|
return userSchema.parse(userData);
|
|
5033
5410
|
}
|
|
5034
5411
|
async getUsersByRole(role) {
|
|
5035
5412
|
const constraints = [
|
|
5036
|
-
|
|
5413
|
+
where8("roles", "array-contains", role)
|
|
5037
5414
|
];
|
|
5038
|
-
const q =
|
|
5039
|
-
const querySnapshot = await
|
|
5415
|
+
const q = query8(collection8(this.db, USERS_COLLECTION), ...constraints);
|
|
5416
|
+
const querySnapshot = await getDocs8(q);
|
|
5040
5417
|
const users = querySnapshot.docs.map((doc34) => doc34.data());
|
|
5041
5418
|
return Promise.all(users.map((userData) => userSchema.parse(userData)));
|
|
5042
5419
|
}
|
|
@@ -5044,24 +5421,24 @@ var UserService = class extends BaseService {
|
|
|
5044
5421
|
* Ažurira timestamp poslednjeg logovanja
|
|
5045
5422
|
*/
|
|
5046
5423
|
async updateUserLoginTimestamp(uid) {
|
|
5047
|
-
const userRef =
|
|
5048
|
-
const userDoc = await
|
|
5424
|
+
const userRef = doc10(this.db, USERS_COLLECTION, uid);
|
|
5425
|
+
const userDoc = await getDoc13(userRef);
|
|
5049
5426
|
if (!userDoc.exists()) {
|
|
5050
5427
|
throw AUTH_ERRORS.USER_NOT_FOUND;
|
|
5051
5428
|
}
|
|
5052
|
-
await
|
|
5429
|
+
await updateDoc10(userRef, {
|
|
5053
5430
|
lastLoginAt: serverTimestamp10(),
|
|
5054
5431
|
updatedAt: serverTimestamp10()
|
|
5055
5432
|
});
|
|
5056
5433
|
return this.getUserById(uid);
|
|
5057
5434
|
}
|
|
5058
5435
|
async upgradeAnonymousUser(uid, email) {
|
|
5059
|
-
const userRef =
|
|
5060
|
-
const userDoc = await
|
|
5436
|
+
const userRef = doc10(this.db, USERS_COLLECTION, uid);
|
|
5437
|
+
const userDoc = await getDoc13(userRef);
|
|
5061
5438
|
if (!userDoc.exists()) {
|
|
5062
5439
|
throw USER_ERRORS.NOT_FOUND;
|
|
5063
5440
|
}
|
|
5064
|
-
await
|
|
5441
|
+
await updateDoc10(userRef, {
|
|
5065
5442
|
email,
|
|
5066
5443
|
isAnonymous: false,
|
|
5067
5444
|
updatedAt: serverTimestamp10()
|
|
@@ -5069,8 +5446,8 @@ var UserService = class extends BaseService {
|
|
|
5069
5446
|
return this.getUserById(uid);
|
|
5070
5447
|
}
|
|
5071
5448
|
async updateUser(uid, updates) {
|
|
5072
|
-
const userRef =
|
|
5073
|
-
const userDoc = await
|
|
5449
|
+
const userRef = doc10(this.db, USERS_COLLECTION, uid);
|
|
5450
|
+
const userDoc = await getDoc13(userRef);
|
|
5074
5451
|
if (!userDoc.exists()) {
|
|
5075
5452
|
throw USER_ERRORS.NOT_FOUND;
|
|
5076
5453
|
}
|
|
@@ -5082,7 +5459,7 @@ var UserService = class extends BaseService {
|
|
|
5082
5459
|
updatedAt: serverTimestamp10()
|
|
5083
5460
|
};
|
|
5084
5461
|
userSchema.parse(updatedUser);
|
|
5085
|
-
await
|
|
5462
|
+
await updateDoc10(userRef, {
|
|
5086
5463
|
...updates,
|
|
5087
5464
|
updatedAt: serverTimestamp10()
|
|
5088
5465
|
});
|
|
@@ -5101,7 +5478,7 @@ var UserService = class extends BaseService {
|
|
|
5101
5478
|
const user = await this.getUserById(uid);
|
|
5102
5479
|
if (user.roles.includes(role)) return;
|
|
5103
5480
|
const profiles = await this.createProfilesForRoles(uid, [role], options);
|
|
5104
|
-
await
|
|
5481
|
+
await updateDoc10(doc10(this.db, USERS_COLLECTION, uid), {
|
|
5105
5482
|
roles: [...user.roles, role],
|
|
5106
5483
|
...profiles,
|
|
5107
5484
|
updatedAt: serverTimestamp10()
|
|
@@ -5136,15 +5513,15 @@ var UserService = class extends BaseService {
|
|
|
5136
5513
|
}
|
|
5137
5514
|
break;
|
|
5138
5515
|
}
|
|
5139
|
-
await
|
|
5516
|
+
await updateDoc10(doc10(this.db, USERS_COLLECTION, uid), {
|
|
5140
5517
|
roles: user.roles.filter((r) => r !== role),
|
|
5141
5518
|
updatedAt: serverTimestamp10()
|
|
5142
5519
|
});
|
|
5143
5520
|
}
|
|
5144
5521
|
// Delete operations
|
|
5145
5522
|
async deleteUser(uid) {
|
|
5146
|
-
const userRef =
|
|
5147
|
-
const userDoc = await
|
|
5523
|
+
const userRef = doc10(this.db, USERS_COLLECTION, uid);
|
|
5524
|
+
const userDoc = await getDoc13(userRef);
|
|
5148
5525
|
if (!userDoc.exists()) {
|
|
5149
5526
|
throw USER_ERRORS.NOT_FOUND;
|
|
5150
5527
|
}
|
|
@@ -5165,7 +5542,7 @@ var UserService = class extends BaseService {
|
|
|
5165
5542
|
userData.adminProfile
|
|
5166
5543
|
);
|
|
5167
5544
|
}
|
|
5168
|
-
await
|
|
5545
|
+
await deleteDoc4(userRef);
|
|
5169
5546
|
} catch (error) {
|
|
5170
5547
|
throw error;
|
|
5171
5548
|
}
|
|
@@ -5174,15 +5551,15 @@ var UserService = class extends BaseService {
|
|
|
5174
5551
|
|
|
5175
5552
|
// src/services/clinic/utils/clinic-group.utils.ts
|
|
5176
5553
|
import {
|
|
5177
|
-
collection as
|
|
5178
|
-
doc as
|
|
5179
|
-
getDoc as
|
|
5180
|
-
getDocs as
|
|
5181
|
-
query as
|
|
5182
|
-
where as
|
|
5183
|
-
updateDoc as
|
|
5184
|
-
setDoc as
|
|
5185
|
-
Timestamp as
|
|
5554
|
+
collection as collection9,
|
|
5555
|
+
doc as doc11,
|
|
5556
|
+
getDoc as getDoc14,
|
|
5557
|
+
getDocs as getDocs9,
|
|
5558
|
+
query as query9,
|
|
5559
|
+
where as where9,
|
|
5560
|
+
updateDoc as updateDoc11,
|
|
5561
|
+
setDoc as setDoc10,
|
|
5562
|
+
Timestamp as Timestamp12
|
|
5186
5563
|
} from "firebase/firestore";
|
|
5187
5564
|
import { geohashForLocation as geohashForLocation2 } from "geofire-common";
|
|
5188
5565
|
import { z as z17 } from "zod";
|
|
@@ -5190,10 +5567,10 @@ import { z as z17 } from "zod";
|
|
|
5190
5567
|
// src/services/clinic/utils/photos.utils.ts
|
|
5191
5568
|
import {
|
|
5192
5569
|
getStorage as getStorage3,
|
|
5193
|
-
ref as
|
|
5194
|
-
uploadBytes as
|
|
5195
|
-
getDownloadURL as
|
|
5196
|
-
deleteObject as
|
|
5570
|
+
ref as ref3,
|
|
5571
|
+
uploadBytes as uploadBytes3,
|
|
5572
|
+
getDownloadURL as getDownloadURL3,
|
|
5573
|
+
deleteObject as deleteObject3
|
|
5197
5574
|
} from "firebase/storage";
|
|
5198
5575
|
async function uploadPhoto(photo, entityType, entityId, photoType, app, fileName) {
|
|
5199
5576
|
if (!photo || typeof photo !== "string" || !photo.startsWith("data:")) {
|
|
@@ -5205,7 +5582,7 @@ async function uploadPhoto(photo, entityType, entityId, photoType, app, fileName
|
|
|
5205
5582
|
);
|
|
5206
5583
|
const storage = getStorage3(app);
|
|
5207
5584
|
const storageFileName = fileName || `${photoType}-${Date.now()}`;
|
|
5208
|
-
const storageRef =
|
|
5585
|
+
const storageRef = ref3(
|
|
5209
5586
|
storage,
|
|
5210
5587
|
`${entityType}/${entityId}/${storageFileName}`
|
|
5211
5588
|
);
|
|
@@ -5217,8 +5594,8 @@ async function uploadPhoto(photo, entityType, entityId, photoType, app, fileName
|
|
|
5217
5594
|
byteArrays.push(byteCharacters.charCodeAt(i));
|
|
5218
5595
|
}
|
|
5219
5596
|
const blob = new Blob([new Uint8Array(byteArrays)], { type: contentType });
|
|
5220
|
-
await
|
|
5221
|
-
const downloadUrl = await
|
|
5597
|
+
await uploadBytes3(storageRef, blob, { contentType });
|
|
5598
|
+
const downloadUrl = await getDownloadURL3(storageRef);
|
|
5222
5599
|
console.log(`[PHOTO_UTILS] ${photoType} uploaded successfully`, {
|
|
5223
5600
|
downloadUrl
|
|
5224
5601
|
});
|
|
@@ -5306,9 +5683,9 @@ async function createClinicGroup(db, data, ownerId, isDefault = false, clinicAdm
|
|
|
5306
5683
|
throw geohashError;
|
|
5307
5684
|
}
|
|
5308
5685
|
}
|
|
5309
|
-
const now =
|
|
5686
|
+
const now = Timestamp12.now();
|
|
5310
5687
|
console.log("[CLINIC_GROUP] Preparing clinic group data object");
|
|
5311
|
-
const groupId =
|
|
5688
|
+
const groupId = doc11(collection9(db, CLINIC_GROUPS_COLLECTION)).id;
|
|
5312
5689
|
console.log("[CLINIC_GROUP] Logo value:", {
|
|
5313
5690
|
logoValue: validatedData.logo,
|
|
5314
5691
|
logoType: validatedData.logo === null ? "null" : typeof validatedData.logo
|
|
@@ -5358,7 +5735,7 @@ async function createClinicGroup(db, data, ownerId, isDefault = false, clinicAdm
|
|
|
5358
5735
|
groupId: groupData.id
|
|
5359
5736
|
});
|
|
5360
5737
|
try {
|
|
5361
|
-
await
|
|
5738
|
+
await setDoc10(doc11(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
|
|
5362
5739
|
console.log("[CLINIC_GROUP] Clinic group saved successfully");
|
|
5363
5740
|
} catch (firestoreError) {
|
|
5364
5741
|
console.error(
|
|
@@ -5404,19 +5781,19 @@ async function createClinicGroup(db, data, ownerId, isDefault = false, clinicAdm
|
|
|
5404
5781
|
}
|
|
5405
5782
|
}
|
|
5406
5783
|
async function getClinicGroup(db, groupId) {
|
|
5407
|
-
const docRef =
|
|
5408
|
-
const docSnap = await
|
|
5784
|
+
const docRef = doc11(db, CLINIC_GROUPS_COLLECTION, groupId);
|
|
5785
|
+
const docSnap = await getDoc14(docRef);
|
|
5409
5786
|
if (docSnap.exists()) {
|
|
5410
5787
|
return docSnap.data();
|
|
5411
5788
|
}
|
|
5412
5789
|
return null;
|
|
5413
5790
|
}
|
|
5414
5791
|
async function getAllActiveGroups(db) {
|
|
5415
|
-
const q =
|
|
5416
|
-
|
|
5417
|
-
|
|
5792
|
+
const q = query9(
|
|
5793
|
+
collection9(db, CLINIC_GROUPS_COLLECTION),
|
|
5794
|
+
where9("isActive", "==", true)
|
|
5418
5795
|
);
|
|
5419
|
-
const querySnapshot = await
|
|
5796
|
+
const querySnapshot = await getDocs9(q);
|
|
5420
5797
|
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
5421
5798
|
}
|
|
5422
5799
|
async function updateClinicGroup(db, groupId, data, app) {
|
|
@@ -5445,10 +5822,10 @@ async function updateClinicGroup(db, groupId, data, app) {
|
|
|
5445
5822
|
}
|
|
5446
5823
|
updatedData = {
|
|
5447
5824
|
...updatedData,
|
|
5448
|
-
updatedAt:
|
|
5825
|
+
updatedAt: Timestamp12.now()
|
|
5449
5826
|
};
|
|
5450
5827
|
console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
|
|
5451
|
-
await
|
|
5828
|
+
await updateDoc11(doc11(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
|
|
5452
5829
|
console.log("[CLINIC_GROUP] Clinic group updated successfully");
|
|
5453
5830
|
const updatedGroup = await getClinicGroup(db, groupId);
|
|
5454
5831
|
if (!updatedGroup) {
|
|
@@ -5529,10 +5906,10 @@ async function createAdminToken(db, groupId, creatorAdminId, app, data) {
|
|
|
5529
5906
|
if (!group.admins.includes(creatorAdminId)) {
|
|
5530
5907
|
throw new Error("Admin does not belong to this clinic group");
|
|
5531
5908
|
}
|
|
5532
|
-
const now =
|
|
5909
|
+
const now = Timestamp12.now();
|
|
5533
5910
|
const expiresInDays = (data == null ? void 0 : data.expiresInDays) || 7;
|
|
5534
5911
|
const email = (data == null ? void 0 : data.email) || null;
|
|
5535
|
-
const expiresAt = new
|
|
5912
|
+
const expiresAt = new Timestamp12(
|
|
5536
5913
|
now.seconds + expiresInDays * 24 * 60 * 60,
|
|
5537
5914
|
now.nanoseconds
|
|
5538
5915
|
);
|
|
@@ -5566,7 +5943,7 @@ async function verifyAndUseAdminToken(db, groupId, token, userRef, app) {
|
|
|
5566
5943
|
if (adminToken.status !== "active" /* ACTIVE */) {
|
|
5567
5944
|
throw new Error("Admin token is not active");
|
|
5568
5945
|
}
|
|
5569
|
-
const now =
|
|
5946
|
+
const now = Timestamp12.now();
|
|
5570
5947
|
if (adminToken.expiresAt.seconds < now.seconds) {
|
|
5571
5948
|
const updatedTokens2 = group.adminTokens.map(
|
|
5572
5949
|
(t) => t.id === adminToken.id ? { ...t, status: "expired" /* EXPIRED */ } : t
|
|
@@ -5849,16 +6226,16 @@ import { z as z19 } from "zod";
|
|
|
5849
6226
|
|
|
5850
6227
|
// src/services/clinic/utils/clinic.utils.ts
|
|
5851
6228
|
import {
|
|
5852
|
-
collection as
|
|
5853
|
-
doc as
|
|
5854
|
-
getDoc as
|
|
5855
|
-
getDocs as
|
|
5856
|
-
query as
|
|
5857
|
-
where as
|
|
5858
|
-
updateDoc as
|
|
5859
|
-
setDoc as
|
|
5860
|
-
Timestamp as
|
|
5861
|
-
limit as
|
|
6229
|
+
collection as collection10,
|
|
6230
|
+
doc as doc12,
|
|
6231
|
+
getDoc as getDoc15,
|
|
6232
|
+
getDocs as getDocs10,
|
|
6233
|
+
query as query10,
|
|
6234
|
+
where as where10,
|
|
6235
|
+
updateDoc as updateDoc12,
|
|
6236
|
+
setDoc as setDoc11,
|
|
6237
|
+
Timestamp as Timestamp13,
|
|
6238
|
+
limit as limit6,
|
|
5862
6239
|
startAfter as startAfter5
|
|
5863
6240
|
} from "firebase/firestore";
|
|
5864
6241
|
import {
|
|
@@ -5868,20 +6245,20 @@ import {
|
|
|
5868
6245
|
} from "geofire-common";
|
|
5869
6246
|
import { z as z18 } from "zod";
|
|
5870
6247
|
async function getClinic(db, clinicId) {
|
|
5871
|
-
const docRef =
|
|
5872
|
-
const docSnap = await
|
|
6248
|
+
const docRef = doc12(db, CLINICS_COLLECTION, clinicId);
|
|
6249
|
+
const docSnap = await getDoc15(docRef);
|
|
5873
6250
|
if (docSnap.exists()) {
|
|
5874
6251
|
return docSnap.data();
|
|
5875
6252
|
}
|
|
5876
6253
|
return null;
|
|
5877
6254
|
}
|
|
5878
6255
|
async function getClinicsByGroup(db, groupId) {
|
|
5879
|
-
const q =
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6256
|
+
const q = query10(
|
|
6257
|
+
collection10(db, CLINICS_COLLECTION),
|
|
6258
|
+
where10("clinicGroupId", "==", groupId),
|
|
6259
|
+
where10("isActive", "==", true)
|
|
5883
6260
|
);
|
|
5884
|
-
const querySnapshot = await
|
|
6261
|
+
const querySnapshot = await getDocs10(q);
|
|
5885
6262
|
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
5886
6263
|
}
|
|
5887
6264
|
async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app) {
|
|
@@ -6037,11 +6414,11 @@ async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app
|
|
|
6037
6414
|
}
|
|
6038
6415
|
updatedData = {
|
|
6039
6416
|
...updatedData,
|
|
6040
|
-
updatedAt:
|
|
6417
|
+
updatedAt: Timestamp13.now()
|
|
6041
6418
|
};
|
|
6042
6419
|
console.log("[CLINIC] Updating clinic in Firestore");
|
|
6043
6420
|
try {
|
|
6044
|
-
await
|
|
6421
|
+
await updateDoc12(doc12(db, CLINICS_COLLECTION, clinicId), updatedData);
|
|
6045
6422
|
console.log("[CLINIC] Clinic updated successfully");
|
|
6046
6423
|
} catch (updateError) {
|
|
6047
6424
|
console.error("[CLINIC] Error updating clinic in Firestore:", updateError);
|
|
@@ -6070,12 +6447,12 @@ async function getClinicsByAdmin(db, adminId, options = {}, clinicAdminService,
|
|
|
6070
6447
|
if (clinicIds.length === 0) {
|
|
6071
6448
|
return [];
|
|
6072
6449
|
}
|
|
6073
|
-
const constraints = [
|
|
6450
|
+
const constraints = [where10("id", "in", clinicIds)];
|
|
6074
6451
|
if (options.isActive !== void 0) {
|
|
6075
|
-
constraints.push(
|
|
6452
|
+
constraints.push(where10("isActive", "==", options.isActive));
|
|
6076
6453
|
}
|
|
6077
|
-
const q =
|
|
6078
|
-
const querySnapshot = await
|
|
6454
|
+
const q = query10(collection10(db, CLINICS_COLLECTION), ...constraints);
|
|
6455
|
+
const querySnapshot = await getDocs10(q);
|
|
6079
6456
|
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
6080
6457
|
}
|
|
6081
6458
|
async function getActiveClinicsByAdmin(db, adminId, clinicAdminService, clinicGroupService) {
|
|
@@ -6089,8 +6466,8 @@ async function getActiveClinicsByAdmin(db, adminId, clinicAdminService, clinicGr
|
|
|
6089
6466
|
}
|
|
6090
6467
|
async function getClinicById(db, clinicId) {
|
|
6091
6468
|
try {
|
|
6092
|
-
const clinicRef =
|
|
6093
|
-
const clinicSnapshot = await
|
|
6469
|
+
const clinicRef = doc12(db, CLINICS_COLLECTION, clinicId);
|
|
6470
|
+
const clinicSnapshot = await getDoc15(clinicRef);
|
|
6094
6471
|
if (!clinicSnapshot.exists()) {
|
|
6095
6472
|
return null;
|
|
6096
6473
|
}
|
|
@@ -6106,20 +6483,20 @@ async function getClinicById(db, clinicId) {
|
|
|
6106
6483
|
}
|
|
6107
6484
|
async function getAllClinics(db, pagination, lastDoc) {
|
|
6108
6485
|
try {
|
|
6109
|
-
const clinicsCollection =
|
|
6110
|
-
let clinicsQuery =
|
|
6486
|
+
const clinicsCollection = collection10(db, CLINICS_COLLECTION);
|
|
6487
|
+
let clinicsQuery = query10(clinicsCollection);
|
|
6111
6488
|
if (pagination && pagination > 0) {
|
|
6112
6489
|
if (lastDoc) {
|
|
6113
|
-
clinicsQuery =
|
|
6490
|
+
clinicsQuery = query10(
|
|
6114
6491
|
clinicsCollection,
|
|
6115
6492
|
startAfter5(lastDoc),
|
|
6116
|
-
|
|
6493
|
+
limit6(pagination)
|
|
6117
6494
|
);
|
|
6118
6495
|
} else {
|
|
6119
|
-
clinicsQuery =
|
|
6496
|
+
clinicsQuery = query10(clinicsCollection, limit6(pagination));
|
|
6120
6497
|
}
|
|
6121
6498
|
}
|
|
6122
|
-
const clinicsSnapshot = await
|
|
6499
|
+
const clinicsSnapshot = await getDocs10(clinicsQuery);
|
|
6123
6500
|
const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
|
|
6124
6501
|
const clinics = clinicsSnapshot.docs.map((doc34) => {
|
|
6125
6502
|
const data = doc34.data();
|
|
@@ -6146,12 +6523,12 @@ async function getAllClinicsInRange(db, center, rangeInKm, pagination, lastDoc)
|
|
|
6146
6523
|
let lastDocSnapshot = null;
|
|
6147
6524
|
for (const b of bounds) {
|
|
6148
6525
|
const constraints = [
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6526
|
+
where10("location.geohash", ">=", b[0]),
|
|
6527
|
+
where10("location.geohash", "<=", b[1]),
|
|
6528
|
+
where10("isActive", "==", true)
|
|
6152
6529
|
];
|
|
6153
|
-
const q =
|
|
6154
|
-
const querySnapshot = await
|
|
6530
|
+
const q = query10(collection10(db, CLINICS_COLLECTION), ...constraints);
|
|
6531
|
+
const querySnapshot = await getDocs10(q);
|
|
6155
6532
|
for (const doc34 of querySnapshot.docs) {
|
|
6156
6533
|
const clinic = doc34.data();
|
|
6157
6534
|
const distance = distanceBetween2(
|
|
@@ -6247,10 +6624,10 @@ async function removeTags(db, clinicId, adminId, tagsToRemove, clinicAdminServic
|
|
|
6247
6624
|
|
|
6248
6625
|
// src/services/clinic/utils/search.utils.ts
|
|
6249
6626
|
import {
|
|
6250
|
-
collection as
|
|
6251
|
-
query as
|
|
6252
|
-
where as
|
|
6253
|
-
getDocs as
|
|
6627
|
+
collection as collection11,
|
|
6628
|
+
query as query11,
|
|
6629
|
+
where as where11,
|
|
6630
|
+
getDocs as getDocs11
|
|
6254
6631
|
} from "firebase/firestore";
|
|
6255
6632
|
import { geohashQueryBounds as geohashQueryBounds2, distanceBetween as distanceBetween3 } from "geofire-common";
|
|
6256
6633
|
async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
@@ -6261,20 +6638,20 @@ async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
|
6261
6638
|
const matchingDocs = [];
|
|
6262
6639
|
for (const b of bounds) {
|
|
6263
6640
|
const constraints = [
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6641
|
+
where11("location.geohash", ">=", b[0]),
|
|
6642
|
+
where11("location.geohash", "<=", b[1]),
|
|
6643
|
+
where11("isActive", "==", true)
|
|
6267
6644
|
];
|
|
6268
6645
|
if (filters == null ? void 0 : filters.services) {
|
|
6269
6646
|
constraints.push(
|
|
6270
|
-
|
|
6647
|
+
where11("services", "array-contains-any", filters.services)
|
|
6271
6648
|
);
|
|
6272
6649
|
}
|
|
6273
6650
|
if ((filters == null ? void 0 : filters.tags) && filters.tags.length > 0) {
|
|
6274
|
-
constraints.push(
|
|
6651
|
+
constraints.push(where11("tags", "array-contains-any", filters.tags));
|
|
6275
6652
|
}
|
|
6276
|
-
const q =
|
|
6277
|
-
const querySnapshot = await
|
|
6653
|
+
const q = query11(collection11(db, CLINICS_COLLECTION), ...constraints);
|
|
6654
|
+
const querySnapshot = await getDocs11(q);
|
|
6278
6655
|
for (const doc34 of querySnapshot.docs) {
|
|
6279
6656
|
const clinic = doc34.data();
|
|
6280
6657
|
const distance = distanceBetween3(
|
|
@@ -6302,13 +6679,13 @@ async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
|
6302
6679
|
|
|
6303
6680
|
// src/services/clinic/utils/filter.utils.ts
|
|
6304
6681
|
import {
|
|
6305
|
-
collection as
|
|
6306
|
-
query as
|
|
6307
|
-
where as
|
|
6308
|
-
getDocs as
|
|
6682
|
+
collection as collection12,
|
|
6683
|
+
query as query12,
|
|
6684
|
+
where as where12,
|
|
6685
|
+
getDocs as getDocs12,
|
|
6309
6686
|
startAfter as startAfter6,
|
|
6310
|
-
limit as
|
|
6311
|
-
orderBy as
|
|
6687
|
+
limit as limit7,
|
|
6688
|
+
orderBy as orderBy3
|
|
6312
6689
|
} from "firebase/firestore";
|
|
6313
6690
|
import { geohashQueryBounds as geohashQueryBounds3, distanceBetween as distanceBetween4 } from "geofire-common";
|
|
6314
6691
|
async function getClinicsByFilters(db, filters) {
|
|
@@ -6319,37 +6696,37 @@ async function getClinicsByFilters(db, filters) {
|
|
|
6319
6696
|
const isGeoQuery = filters.center && filters.radiusInKm && filters.radiusInKm > 0;
|
|
6320
6697
|
const constraints = [];
|
|
6321
6698
|
if (filters.isActive !== void 0) {
|
|
6322
|
-
constraints.push(
|
|
6699
|
+
constraints.push(where12("isActive", "==", filters.isActive));
|
|
6323
6700
|
} else {
|
|
6324
|
-
constraints.push(
|
|
6701
|
+
constraints.push(where12("isActive", "==", true));
|
|
6325
6702
|
}
|
|
6326
6703
|
if (filters.tags && filters.tags.length > 0) {
|
|
6327
|
-
constraints.push(
|
|
6704
|
+
constraints.push(where12("tags", "array-contains", filters.tags[0]));
|
|
6328
6705
|
}
|
|
6329
6706
|
if (filters.procedureTechnology) {
|
|
6330
6707
|
constraints.push(
|
|
6331
|
-
|
|
6708
|
+
where12("servicesInfo.technology", "==", filters.procedureTechnology)
|
|
6332
6709
|
);
|
|
6333
6710
|
} else if (filters.procedureSubcategory) {
|
|
6334
6711
|
constraints.push(
|
|
6335
|
-
|
|
6712
|
+
where12("servicesInfo.subCategory", "==", filters.procedureSubcategory)
|
|
6336
6713
|
);
|
|
6337
6714
|
} else if (filters.procedureCategory) {
|
|
6338
6715
|
constraints.push(
|
|
6339
|
-
|
|
6716
|
+
where12("servicesInfo.category", "==", filters.procedureCategory)
|
|
6340
6717
|
);
|
|
6341
6718
|
} else if (filters.procedureFamily) {
|
|
6342
6719
|
constraints.push(
|
|
6343
|
-
|
|
6720
|
+
where12("servicesInfo.procedureFamily", "==", filters.procedureFamily)
|
|
6344
6721
|
);
|
|
6345
6722
|
}
|
|
6346
6723
|
if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
|
|
6347
6724
|
constraints.push(startAfter6(filters.lastDoc));
|
|
6348
|
-
constraints.push(
|
|
6725
|
+
constraints.push(limit7(filters.pagination));
|
|
6349
6726
|
} else if (filters.pagination && filters.pagination > 0) {
|
|
6350
|
-
constraints.push(
|
|
6727
|
+
constraints.push(limit7(filters.pagination));
|
|
6351
6728
|
}
|
|
6352
|
-
constraints.push(
|
|
6729
|
+
constraints.push(orderBy3("location.geohash"));
|
|
6353
6730
|
let clinicsResult = [];
|
|
6354
6731
|
let lastVisibleDoc = null;
|
|
6355
6732
|
if (isGeoQuery) {
|
|
@@ -6357,440 +6734,124 @@ async function getClinicsByFilters(db, filters) {
|
|
|
6357
6734
|
const radiusInKm = filters.radiusInKm;
|
|
6358
6735
|
const bounds = geohashQueryBounds3(
|
|
6359
6736
|
[center.latitude, center.longitude],
|
|
6360
|
-
radiusInKm * 1e3
|
|
6361
|
-
// Convert to meters
|
|
6362
|
-
);
|
|
6363
|
-
const matchingClinics = [];
|
|
6364
|
-
for (const bound of bounds) {
|
|
6365
|
-
const geoConstraints = [
|
|
6366
|
-
...constraints,
|
|
6367
|
-
where11("location.geohash", ">=", bound[0]),
|
|
6368
|
-
where11("location.geohash", "<=", bound[1])
|
|
6369
|
-
];
|
|
6370
|
-
const q = query11(collection11(db, CLINICS_COLLECTION), ...geoConstraints);
|
|
6371
|
-
const querySnapshot = await getDocs11(q);
|
|
6372
|
-
console.log(
|
|
6373
|
-
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
|
|
6374
|
-
);
|
|
6375
|
-
for (const doc34 of querySnapshot.docs) {
|
|
6376
|
-
const clinic = { ...doc34.data(), id: doc34.id };
|
|
6377
|
-
const distance = distanceBetween4(
|
|
6378
|
-
[center.latitude, center.longitude],
|
|
6379
|
-
[clinic.location.latitude, clinic.location.longitude]
|
|
6380
|
-
);
|
|
6381
|
-
const distanceInKm = distance / 1e3;
|
|
6382
|
-
if (distanceInKm <= radiusInKm) {
|
|
6383
|
-
matchingClinics.push({
|
|
6384
|
-
...clinic,
|
|
6385
|
-
distance: distanceInKm
|
|
6386
|
-
});
|
|
6387
|
-
}
|
|
6388
|
-
}
|
|
6389
|
-
}
|
|
6390
|
-
let filteredClinics = matchingClinics;
|
|
6391
|
-
if (filters.tags && filters.tags.length > 1) {
|
|
6392
|
-
filteredClinics = filteredClinics.filter((clinic) => {
|
|
6393
|
-
return filters.tags.every((tag) => clinic.tags.includes(tag));
|
|
6394
|
-
});
|
|
6395
|
-
}
|
|
6396
|
-
if (filters.minRating !== void 0) {
|
|
6397
|
-
filteredClinics = filteredClinics.filter(
|
|
6398
|
-
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating
|
|
6399
|
-
);
|
|
6400
|
-
}
|
|
6401
|
-
if (filters.maxRating !== void 0) {
|
|
6402
|
-
filteredClinics = filteredClinics.filter(
|
|
6403
|
-
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6404
|
-
);
|
|
6405
|
-
}
|
|
6406
|
-
filteredClinics.sort((a, b) => a.distance - b.distance);
|
|
6407
|
-
if (filters.pagination && filters.pagination > 0) {
|
|
6408
|
-
let startIndex = 0;
|
|
6409
|
-
if (filters.lastDoc) {
|
|
6410
|
-
const lastDocIndex = filteredClinics.findIndex(
|
|
6411
|
-
(clinic) => clinic.id === filters.lastDoc.id
|
|
6412
|
-
);
|
|
6413
|
-
if (lastDocIndex !== -1) {
|
|
6414
|
-
startIndex = lastDocIndex + 1;
|
|
6415
|
-
}
|
|
6416
|
-
}
|
|
6417
|
-
const paginatedClinics = filteredClinics.slice(
|
|
6418
|
-
startIndex,
|
|
6419
|
-
startIndex + filters.pagination
|
|
6420
|
-
);
|
|
6421
|
-
lastVisibleDoc = paginatedClinics.length > 0 ? paginatedClinics[paginatedClinics.length - 1] : null;
|
|
6422
|
-
clinicsResult = paginatedClinics;
|
|
6423
|
-
} else {
|
|
6424
|
-
clinicsResult = filteredClinics;
|
|
6425
|
-
}
|
|
6426
|
-
} else {
|
|
6427
|
-
const q = query11(collection11(db, CLINICS_COLLECTION), ...constraints);
|
|
6428
|
-
const querySnapshot = await getDocs11(q);
|
|
6429
|
-
console.log(
|
|
6430
|
-
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
|
|
6431
|
-
);
|
|
6432
|
-
const clinics = querySnapshot.docs.map((doc34) => {
|
|
6433
|
-
return { ...doc34.data(), id: doc34.id };
|
|
6434
|
-
});
|
|
6435
|
-
let filteredClinics = clinics;
|
|
6436
|
-
if (filters.center) {
|
|
6437
|
-
const center = filters.center;
|
|
6438
|
-
const clinicsWithDistance = [];
|
|
6439
|
-
filteredClinics.forEach((clinic) => {
|
|
6440
|
-
const distance = distanceBetween4(
|
|
6441
|
-
[center.latitude, center.longitude],
|
|
6442
|
-
[clinic.location.latitude, clinic.location.longitude]
|
|
6443
|
-
);
|
|
6444
|
-
clinicsWithDistance.push({
|
|
6445
|
-
...clinic,
|
|
6446
|
-
distance: distance / 1e3
|
|
6447
|
-
// Convert to kilometers
|
|
6448
|
-
});
|
|
6449
|
-
});
|
|
6450
|
-
filteredClinics = clinicsWithDistance;
|
|
6451
|
-
filteredClinics.sort(
|
|
6452
|
-
(a, b) => a.distance - b.distance
|
|
6453
|
-
);
|
|
6454
|
-
}
|
|
6455
|
-
if (filters.tags && filters.tags.length > 1) {
|
|
6456
|
-
filteredClinics = filteredClinics.filter((clinic) => {
|
|
6457
|
-
return filters.tags.every((tag) => clinic.tags.includes(tag));
|
|
6458
|
-
});
|
|
6459
|
-
}
|
|
6460
|
-
if (filters.minRating !== void 0) {
|
|
6461
|
-
filteredClinics = filteredClinics.filter(
|
|
6462
|
-
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating
|
|
6463
|
-
);
|
|
6464
|
-
}
|
|
6465
|
-
if (filters.maxRating !== void 0) {
|
|
6466
|
-
filteredClinics = filteredClinics.filter(
|
|
6467
|
-
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6468
|
-
);
|
|
6469
|
-
}
|
|
6470
|
-
lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
6471
|
-
clinicsResult = filteredClinics;
|
|
6472
|
-
}
|
|
6473
|
-
return {
|
|
6474
|
-
clinics: clinicsResult,
|
|
6475
|
-
lastDoc: lastVisibleDoc
|
|
6476
|
-
};
|
|
6477
|
-
}
|
|
6478
|
-
|
|
6479
|
-
// src/services/media/media.service.ts
|
|
6480
|
-
import { Timestamp as Timestamp13 } from "firebase/firestore";
|
|
6481
|
-
import {
|
|
6482
|
-
ref as ref3,
|
|
6483
|
-
uploadBytes as uploadBytes3,
|
|
6484
|
-
getDownloadURL as getDownloadURL3,
|
|
6485
|
-
deleteObject as deleteObject3,
|
|
6486
|
-
getBytes
|
|
6487
|
-
} from "firebase/storage";
|
|
6488
|
-
import {
|
|
6489
|
-
doc as doc12,
|
|
6490
|
-
getDoc as getDoc15,
|
|
6491
|
-
setDoc as setDoc11,
|
|
6492
|
-
updateDoc as updateDoc12,
|
|
6493
|
-
collection as collection12,
|
|
6494
|
-
query as query12,
|
|
6495
|
-
where as where12,
|
|
6496
|
-
limit as limit7,
|
|
6497
|
-
getDocs as getDocs12,
|
|
6498
|
-
deleteDoc as deleteDoc6,
|
|
6499
|
-
orderBy as orderBy3
|
|
6500
|
-
} from "firebase/firestore";
|
|
6501
|
-
var MediaAccessLevel = /* @__PURE__ */ ((MediaAccessLevel2) => {
|
|
6502
|
-
MediaAccessLevel2["PUBLIC"] = "public";
|
|
6503
|
-
MediaAccessLevel2["PRIVATE"] = "private";
|
|
6504
|
-
MediaAccessLevel2["CONFIDENTIAL"] = "confidential";
|
|
6505
|
-
return MediaAccessLevel2;
|
|
6506
|
-
})(MediaAccessLevel || {});
|
|
6507
|
-
var MEDIA_METADATA_COLLECTION = "media_metadata";
|
|
6508
|
-
var MediaService = class extends BaseService {
|
|
6509
|
-
constructor(db, auth, app) {
|
|
6510
|
-
super(db, auth, app);
|
|
6511
|
-
}
|
|
6512
|
-
/**
|
|
6513
|
-
* Upload a media file, store its metadata, and return the metadata including the URL.
|
|
6514
|
-
* @param file - The file to upload.
|
|
6515
|
-
* @param ownerId - ID of the owner (user, patient, clinic, etc.).
|
|
6516
|
-
* @param accessLevel - Access level (public, private, confidential).
|
|
6517
|
-
* @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
|
|
6518
|
-
* @param originalFileName - Optional: the original name of the file, if not using file.name.
|
|
6519
|
-
* @returns Promise with the media metadata.
|
|
6520
|
-
*/
|
|
6521
|
-
async uploadMedia(file, ownerId, accessLevel, collectionName, originalFileName) {
|
|
6522
|
-
const mediaId = this.generateId();
|
|
6523
|
-
const fileNameToUse = originalFileName || (file instanceof File ? file.name : file.toString());
|
|
6524
|
-
const uniqueFileName = `${mediaId}-${fileNameToUse}`;
|
|
6525
|
-
const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
|
|
6526
|
-
console.log(`[MediaService] Uploading file to: ${filePath}`);
|
|
6527
|
-
const storageRef = ref3(this.storage, filePath);
|
|
6528
|
-
try {
|
|
6529
|
-
const uploadResult = await uploadBytes3(storageRef, file, {
|
|
6530
|
-
contentType: file.type
|
|
6531
|
-
});
|
|
6532
|
-
console.log("[MediaService] File uploaded successfully", uploadResult);
|
|
6533
|
-
const downloadURL = await getDownloadURL3(uploadResult.ref);
|
|
6534
|
-
console.log("[MediaService] Got download URL:", downloadURL);
|
|
6535
|
-
const metadata = {
|
|
6536
|
-
id: mediaId,
|
|
6537
|
-
name: fileNameToUse,
|
|
6538
|
-
url: downloadURL,
|
|
6539
|
-
contentType: file.type,
|
|
6540
|
-
size: file.size,
|
|
6541
|
-
createdAt: Timestamp13.now(),
|
|
6542
|
-
accessLevel,
|
|
6543
|
-
ownerId,
|
|
6544
|
-
collectionName,
|
|
6545
|
-
path: filePath
|
|
6546
|
-
};
|
|
6547
|
-
const metadataDocRef = doc12(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
6548
|
-
await setDoc11(metadataDocRef, metadata);
|
|
6549
|
-
console.log("[MediaService] Metadata stored in Firestore:", mediaId);
|
|
6550
|
-
return metadata;
|
|
6551
|
-
} catch (error) {
|
|
6552
|
-
console.error("[MediaService] Error during media upload:", error);
|
|
6553
|
-
throw error;
|
|
6554
|
-
}
|
|
6555
|
-
}
|
|
6556
|
-
/**
|
|
6557
|
-
* Get media metadata from Firestore by its ID.
|
|
6558
|
-
* @param mediaId - ID of the media.
|
|
6559
|
-
* @returns Promise with the media metadata or null if not found.
|
|
6560
|
-
*/
|
|
6561
|
-
async getMediaMetadata(mediaId) {
|
|
6562
|
-
console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
|
|
6563
|
-
const docRef = doc12(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
6564
|
-
const docSnap = await getDoc15(docRef);
|
|
6565
|
-
if (docSnap.exists()) {
|
|
6566
|
-
console.log("[MediaService] Metadata found:", docSnap.data());
|
|
6567
|
-
return docSnap.data();
|
|
6568
|
-
}
|
|
6569
|
-
console.log("[MediaService] No metadata found for ID:", mediaId);
|
|
6570
|
-
return null;
|
|
6571
|
-
}
|
|
6572
|
-
/**
|
|
6573
|
-
* Get media metadata from Firestore by its public URL.
|
|
6574
|
-
* @param url - The public URL of the media file.
|
|
6575
|
-
* @returns Promise with the media metadata or null if not found.
|
|
6576
|
-
*/
|
|
6577
|
-
async getMediaMetadataByUrl(url) {
|
|
6578
|
-
console.log(`[MediaService] Getting media metadata by URL: ${url}`);
|
|
6579
|
-
const q = query12(
|
|
6580
|
-
collection12(this.db, MEDIA_METADATA_COLLECTION),
|
|
6581
|
-
where12("url", "==", url),
|
|
6582
|
-
limit7(1)
|
|
6737
|
+
radiusInKm * 1e3
|
|
6738
|
+
// Convert to meters
|
|
6583
6739
|
);
|
|
6584
|
-
|
|
6740
|
+
const matchingClinics = [];
|
|
6741
|
+
for (const bound of bounds) {
|
|
6742
|
+
const geoConstraints = [
|
|
6743
|
+
...constraints,
|
|
6744
|
+
where12("location.geohash", ">=", bound[0]),
|
|
6745
|
+
where12("location.geohash", "<=", bound[1])
|
|
6746
|
+
];
|
|
6747
|
+
const q = query12(collection12(db, CLINICS_COLLECTION), ...geoConstraints);
|
|
6585
6748
|
const querySnapshot = await getDocs12(q);
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6749
|
+
console.log(
|
|
6750
|
+
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
|
|
6751
|
+
);
|
|
6752
|
+
for (const doc34 of querySnapshot.docs) {
|
|
6753
|
+
const clinic = { ...doc34.data(), id: doc34.id };
|
|
6754
|
+
const distance = distanceBetween4(
|
|
6755
|
+
[center.latitude, center.longitude],
|
|
6756
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
6757
|
+
);
|
|
6758
|
+
const distanceInKm = distance / 1e3;
|
|
6759
|
+
if (distanceInKm <= radiusInKm) {
|
|
6760
|
+
matchingClinics.push({
|
|
6761
|
+
...clinic,
|
|
6762
|
+
distance: distanceInKm
|
|
6763
|
+
});
|
|
6764
|
+
}
|
|
6590
6765
|
}
|
|
6591
|
-
console.log("[MediaService] No metadata found for URL:", url);
|
|
6592
|
-
return null;
|
|
6593
|
-
} catch (error) {
|
|
6594
|
-
console.error("[MediaService] Error fetching metadata by URL:", error);
|
|
6595
|
-
throw error;
|
|
6596
6766
|
}
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
async deleteMedia(mediaId) {
|
|
6603
|
-
console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
|
|
6604
|
-
const metadata = await this.getMediaMetadata(mediaId);
|
|
6605
|
-
if (!metadata) {
|
|
6606
|
-
console.warn(
|
|
6607
|
-
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
|
|
6608
|
-
);
|
|
6609
|
-
return;
|
|
6767
|
+
let filteredClinics = matchingClinics;
|
|
6768
|
+
if (filters.tags && filters.tags.length > 1) {
|
|
6769
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
6770
|
+
return filters.tags.every((tag) => clinic.tags.includes(tag));
|
|
6771
|
+
});
|
|
6610
6772
|
}
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
|
|
6615
|
-
const metadataDocRef = doc12(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
6616
|
-
await deleteDoc6(metadataDocRef);
|
|
6617
|
-
console.log(
|
|
6618
|
-
`[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
|
|
6773
|
+
if (filters.minRating !== void 0) {
|
|
6774
|
+
filteredClinics = filteredClinics.filter(
|
|
6775
|
+
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating
|
|
6619
6776
|
);
|
|
6620
|
-
} catch (error) {
|
|
6621
|
-
console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
|
|
6622
|
-
throw error;
|
|
6623
6777
|
}
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
* to a new path reflecting the new access level, and updating its metadata.
|
|
6628
|
-
* @param mediaId - ID of the media to update.
|
|
6629
|
-
* @param newAccessLevel - New access level.
|
|
6630
|
-
* @returns Promise with the updated media metadata, or null if metadata not found.
|
|
6631
|
-
*/
|
|
6632
|
-
async updateMediaAccessLevel(mediaId, newAccessLevel) {
|
|
6633
|
-
var _a;
|
|
6634
|
-
console.log(
|
|
6635
|
-
`[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
|
|
6636
|
-
);
|
|
6637
|
-
const metadata = await this.getMediaMetadata(mediaId);
|
|
6638
|
-
if (!metadata) {
|
|
6639
|
-
console.warn(
|
|
6640
|
-
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
|
|
6778
|
+
if (filters.maxRating !== void 0) {
|
|
6779
|
+
filteredClinics = filteredClinics.filter(
|
|
6780
|
+
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6641
6781
|
);
|
|
6642
|
-
return null;
|
|
6643
6782
|
}
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
)
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
await updateDoc12(metadataDocRef, { updatedAt: Timestamp13.now() });
|
|
6651
|
-
return { ...metadata, updatedAt: Timestamp13.now() };
|
|
6652
|
-
} catch (error) {
|
|
6653
|
-
console.error(
|
|
6654
|
-
`[MediaService] Error updating timestamp for media ID ${mediaId}:`,
|
|
6655
|
-
error
|
|
6783
|
+
filteredClinics.sort((a, b) => a.distance - b.distance);
|
|
6784
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
6785
|
+
let startIndex = 0;
|
|
6786
|
+
if (filters.lastDoc) {
|
|
6787
|
+
const lastDocIndex = filteredClinics.findIndex(
|
|
6788
|
+
(clinic) => clinic.id === filters.lastDoc.id
|
|
6656
6789
|
);
|
|
6657
|
-
|
|
6790
|
+
if (lastDocIndex !== -1) {
|
|
6791
|
+
startIndex = lastDocIndex + 1;
|
|
6792
|
+
}
|
|
6658
6793
|
}
|
|
6794
|
+
const paginatedClinics = filteredClinics.slice(
|
|
6795
|
+
startIndex,
|
|
6796
|
+
startIndex + filters.pagination
|
|
6797
|
+
);
|
|
6798
|
+
lastVisibleDoc = paginatedClinics.length > 0 ? paginatedClinics[paginatedClinics.length - 1] : null;
|
|
6799
|
+
clinicsResult = paginatedClinics;
|
|
6800
|
+
} else {
|
|
6801
|
+
clinicsResult = filteredClinics;
|
|
6659
6802
|
}
|
|
6660
|
-
|
|
6661
|
-
const
|
|
6662
|
-
const
|
|
6803
|
+
} else {
|
|
6804
|
+
const q = query12(collection12(db, CLINICS_COLLECTION), ...constraints);
|
|
6805
|
+
const querySnapshot = await getDocs12(q);
|
|
6663
6806
|
console.log(
|
|
6664
|
-
`[
|
|
6807
|
+
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
|
|
6665
6808
|
);
|
|
6666
|
-
const
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
)
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
});
|
|
6678
|
-
console.log(
|
|
6679
|
-
`[MediaService] Successfully uploaded bytes to ${newStoragePath}`
|
|
6680
|
-
);
|
|
6681
|
-
const newDownloadURL = await getDownloadURL3(newStorageFileRef);
|
|
6682
|
-
console.log(
|
|
6683
|
-
`[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
|
|
6684
|
-
);
|
|
6685
|
-
const updateData = {
|
|
6686
|
-
accessLevel: newAccessLevel,
|
|
6687
|
-
path: newStoragePath,
|
|
6688
|
-
url: newDownloadURL,
|
|
6689
|
-
updatedAt: Timestamp13.now()
|
|
6690
|
-
};
|
|
6691
|
-
const metadataDocRef = doc12(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
6692
|
-
console.log(
|
|
6693
|
-
`[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
|
|
6694
|
-
updateData
|
|
6695
|
-
);
|
|
6696
|
-
await updateDoc12(metadataDocRef, updateData);
|
|
6697
|
-
console.log(
|
|
6698
|
-
`[MediaService] Successfully updated Firestore metadata for ${mediaId}`
|
|
6699
|
-
);
|
|
6700
|
-
try {
|
|
6701
|
-
console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
|
|
6702
|
-
await deleteObject3(oldStorageFileRef);
|
|
6703
|
-
console.log(
|
|
6704
|
-
`[MediaService] Successfully deleted old file from ${oldStoragePath}`
|
|
6705
|
-
);
|
|
6706
|
-
} catch (deleteError) {
|
|
6707
|
-
console.error(
|
|
6708
|
-
`[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
|
|
6709
|
-
deleteError
|
|
6809
|
+
const clinics = querySnapshot.docs.map((doc34) => {
|
|
6810
|
+
return { ...doc34.data(), id: doc34.id };
|
|
6811
|
+
});
|
|
6812
|
+
let filteredClinics = clinics;
|
|
6813
|
+
if (filters.center) {
|
|
6814
|
+
const center = filters.center;
|
|
6815
|
+
const clinicsWithDistance = [];
|
|
6816
|
+
filteredClinics.forEach((clinic) => {
|
|
6817
|
+
const distance = distanceBetween4(
|
|
6818
|
+
[center.latitude, center.longitude],
|
|
6819
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
6710
6820
|
);
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6821
|
+
clinicsWithDistance.push({
|
|
6822
|
+
...clinic,
|
|
6823
|
+
distance: distance / 1e3
|
|
6824
|
+
// Convert to kilometers
|
|
6825
|
+
});
|
|
6826
|
+
});
|
|
6827
|
+
filteredClinics = clinicsWithDistance;
|
|
6828
|
+
filteredClinics.sort(
|
|
6829
|
+
(a, b) => a.distance - b.distance
|
|
6717
6830
|
);
|
|
6718
|
-
if (newStorageFileRef && error.code !== "storage/object-not-found" && ((_a = error.message) == null ? void 0 : _a.includes("uploadBytes"))) {
|
|
6719
|
-
console.warn(
|
|
6720
|
-
`[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
|
|
6721
|
-
);
|
|
6722
|
-
try {
|
|
6723
|
-
await deleteObject3(newStorageFileRef);
|
|
6724
|
-
console.warn(
|
|
6725
|
-
`[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
|
|
6726
|
-
);
|
|
6727
|
-
} catch (cleanupError) {
|
|
6728
|
-
console.error(
|
|
6729
|
-
`[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
|
|
6730
|
-
cleanupError
|
|
6731
|
-
);
|
|
6732
|
-
}
|
|
6733
|
-
}
|
|
6734
|
-
throw error;
|
|
6735
|
-
}
|
|
6736
|
-
}
|
|
6737
|
-
/**
|
|
6738
|
-
* List all media for an owner, optionally filtered by collection and access level.
|
|
6739
|
-
* @param ownerId - ID of the owner.
|
|
6740
|
-
* @param collectionName - Optional: Filter by collection name.
|
|
6741
|
-
* @param accessLevel - Optional: Filter by access level.
|
|
6742
|
-
* @param count - Optional: Number of items to fetch.
|
|
6743
|
-
* @param startAfterId - Optional: ID of the document to start after (for pagination).
|
|
6744
|
-
*/
|
|
6745
|
-
async listMedia(ownerId, collectionName, accessLevel, count, startAfterId) {
|
|
6746
|
-
console.log(`[MediaService] Listing media for owner: ${ownerId}`);
|
|
6747
|
-
let qConstraints = [where12("ownerId", "==", ownerId)];
|
|
6748
|
-
if (collectionName) {
|
|
6749
|
-
qConstraints.push(where12("collectionName", "==", collectionName));
|
|
6750
|
-
}
|
|
6751
|
-
if (accessLevel) {
|
|
6752
|
-
qConstraints.push(where12("accessLevel", "==", accessLevel));
|
|
6753
6831
|
}
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
if (startAfterId) {
|
|
6759
|
-
const startAfterDoc = await this.getMediaMetadata(startAfterId);
|
|
6760
|
-
if (startAfterDoc) {
|
|
6761
|
-
}
|
|
6832
|
+
if (filters.tags && filters.tags.length > 1) {
|
|
6833
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
6834
|
+
return filters.tags.every((tag) => clinic.tags.includes(tag));
|
|
6835
|
+
});
|
|
6762
6836
|
}
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
);
|
|
6767
|
-
try {
|
|
6768
|
-
const querySnapshot = await getDocs12(finalQuery);
|
|
6769
|
-
const mediaList = querySnapshot.docs.map(
|
|
6770
|
-
(doc34) => doc34.data()
|
|
6837
|
+
if (filters.minRating !== void 0) {
|
|
6838
|
+
filteredClinics = filteredClinics.filter(
|
|
6839
|
+
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating
|
|
6771
6840
|
);
|
|
6772
|
-
console.log(`[MediaService] Found ${mediaList.length} media items.`);
|
|
6773
|
-
return mediaList;
|
|
6774
|
-
} catch (error) {
|
|
6775
|
-
console.error("[MediaService] Error listing media:", error);
|
|
6776
|
-
throw error;
|
|
6777
6841
|
}
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
*/
|
|
6783
|
-
async getMediaDownloadUrl(mediaId) {
|
|
6784
|
-
console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
|
|
6785
|
-
const metadata = await this.getMediaMetadata(mediaId);
|
|
6786
|
-
if (metadata && metadata.url) {
|
|
6787
|
-
console.log(`[MediaService] URL found: ${metadata.url}`);
|
|
6788
|
-
return metadata.url;
|
|
6842
|
+
if (filters.maxRating !== void 0) {
|
|
6843
|
+
filteredClinics = filteredClinics.filter(
|
|
6844
|
+
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6845
|
+
);
|
|
6789
6846
|
}
|
|
6790
|
-
|
|
6791
|
-
|
|
6847
|
+
lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
6848
|
+
clinicsResult = filteredClinics;
|
|
6792
6849
|
}
|
|
6793
|
-
|
|
6850
|
+
return {
|
|
6851
|
+
clinics: clinicsResult,
|
|
6852
|
+
lastDoc: lastVisibleDoc
|
|
6853
|
+
};
|
|
6854
|
+
}
|
|
6794
6855
|
|
|
6795
6856
|
// src/services/clinic/clinic.service.ts
|
|
6796
6857
|
var ClinicService = class extends BaseService {
|
|
@@ -8522,7 +8583,8 @@ var ProcedureService = class extends BaseService {
|
|
|
8522
8583
|
id: practitionerSnapshot.id,
|
|
8523
8584
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
8524
8585
|
description: practitioner.basicInfo.bio || "",
|
|
8525
|
-
photo: practitioner.basicInfo.profileImageUrl
|
|
8586
|
+
photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
|
|
8587
|
+
// Default to empty string if not a processed URL
|
|
8526
8588
|
rating: ((_a = practitioner.reviewInfo) == null ? void 0 : _a.averageRating) || 0,
|
|
8527
8589
|
services: practitioner.procedures || []
|
|
8528
8590
|
};
|
|
@@ -8656,7 +8718,8 @@ var ProcedureService = class extends BaseService {
|
|
|
8656
8718
|
id: newPractitioner.id,
|
|
8657
8719
|
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
8658
8720
|
description: newPractitioner.basicInfo.bio || "",
|
|
8659
|
-
photo: newPractitioner.basicInfo.profileImageUrl
|
|
8721
|
+
photo: typeof newPractitioner.basicInfo.profileImageUrl === "string" ? newPractitioner.basicInfo.profileImageUrl : "",
|
|
8722
|
+
// Default to empty string if not a processed URL
|
|
8660
8723
|
rating: ((_a = newPractitioner.reviewInfo) == null ? void 0 : _a.averageRating) || 0,
|
|
8661
8724
|
services: newPractitioner.procedures || []
|
|
8662
8725
|
};
|