@blackcode_sa/metaestetics-api 1.7.18 → 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 +63 -60
- package/dist/index.d.ts +63 -60
- package/dist/index.js +1954 -1967
- package/dist/index.mjs +1341 -1354
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +4 -1
- package/src/services/documentation-templates/filled-document.service.ts +49 -153
- 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.js
CHANGED
|
@@ -1096,7 +1096,7 @@ var BaseService = class {
|
|
|
1096
1096
|
};
|
|
1097
1097
|
|
|
1098
1098
|
// src/services/user.service.ts
|
|
1099
|
-
var
|
|
1099
|
+
var import_firestore19 = require("firebase/firestore");
|
|
1100
1100
|
|
|
1101
1101
|
// src/errors/user.errors.ts
|
|
1102
1102
|
var USER_ERRORS = {
|
|
@@ -3322,7 +3322,7 @@ var doctorInfoSchema = import_zod11.z.object({
|
|
|
3322
3322
|
// src/validations/media.schema.ts
|
|
3323
3323
|
var import_zod12 = require("zod");
|
|
3324
3324
|
var mediaResourceSchema = import_zod12.z.union([
|
|
3325
|
-
import_zod12.z.string(),
|
|
3325
|
+
import_zod12.z.string().url(),
|
|
3326
3326
|
import_zod12.z.instanceof(File),
|
|
3327
3327
|
import_zod12.z.instanceof(Blob)
|
|
3328
3328
|
]);
|
|
@@ -4060,732 +4060,1091 @@ var ClinicAdminService = class extends BaseService {
|
|
|
4060
4060
|
};
|
|
4061
4061
|
|
|
4062
4062
|
// src/services/practitioner/practitioner.service.ts
|
|
4063
|
-
var
|
|
4063
|
+
var import_firestore18 = require("firebase/firestore");
|
|
4064
4064
|
|
|
4065
|
-
// src/
|
|
4066
|
-
var import_zod14 = require("zod");
|
|
4065
|
+
// src/services/media/media.service.ts
|
|
4067
4066
|
var import_firestore15 = require("firebase/firestore");
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
var
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
return CertificationLevel2;
|
|
4080
|
-
})(CertificationLevel || {});
|
|
4081
|
-
var CertificationSpecialty = /* @__PURE__ */ ((CertificationSpecialty3) => {
|
|
4082
|
-
CertificationSpecialty3["LASER"] = "laser";
|
|
4083
|
-
CertificationSpecialty3["INJECTABLES"] = "injectables";
|
|
4084
|
-
CertificationSpecialty3["CHEMICAL_PEELS"] = "chemical_peels";
|
|
4085
|
-
CertificationSpecialty3["MICRODERMABRASION"] = "microdermabrasion";
|
|
4086
|
-
CertificationSpecialty3["BODY_CONTOURING"] = "body_contouring";
|
|
4087
|
-
CertificationSpecialty3["SKIN_CARE"] = "skin_care";
|
|
4088
|
-
CertificationSpecialty3["WOUND_CARE"] = "wound_care";
|
|
4089
|
-
CertificationSpecialty3["ANESTHESIA"] = "anesthesia";
|
|
4090
|
-
return CertificationSpecialty3;
|
|
4091
|
-
})(CertificationSpecialty || {});
|
|
4092
|
-
|
|
4093
|
-
// src/validations/practitioner.schema.ts
|
|
4094
|
-
var practitionerBasicInfoSchema = import_zod14.z.object({
|
|
4095
|
-
firstName: import_zod14.z.string().min(2).max(50),
|
|
4096
|
-
lastName: import_zod14.z.string().min(2).max(50),
|
|
4097
|
-
title: import_zod14.z.string().min(2).max(100),
|
|
4098
|
-
email: import_zod14.z.string().email(),
|
|
4099
|
-
phoneNumber: import_zod14.z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
|
|
4100
|
-
dateOfBirth: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4101
|
-
gender: import_zod14.z.enum(["male", "female", "other"]),
|
|
4102
|
-
profileImageUrl: import_zod14.z.string().url().optional(),
|
|
4103
|
-
bio: import_zod14.z.string().max(1e3).optional(),
|
|
4104
|
-
languages: import_zod14.z.array(import_zod14.z.string()).min(1)
|
|
4105
|
-
});
|
|
4106
|
-
var practitionerCertificationSchema = import_zod14.z.object({
|
|
4107
|
-
level: import_zod14.z.nativeEnum(CertificationLevel),
|
|
4108
|
-
specialties: import_zod14.z.array(import_zod14.z.nativeEnum(CertificationSpecialty)),
|
|
4109
|
-
licenseNumber: import_zod14.z.string().min(3).max(50),
|
|
4110
|
-
issuingAuthority: import_zod14.z.string().min(2).max(100),
|
|
4111
|
-
issueDate: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4112
|
-
expiryDate: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()).optional(),
|
|
4113
|
-
verificationStatus: import_zod14.z.enum(["pending", "verified", "rejected"])
|
|
4114
|
-
});
|
|
4115
|
-
var timeSlotSchema = import_zod14.z.object({
|
|
4116
|
-
start: import_zod14.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format"),
|
|
4117
|
-
end: import_zod14.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format")
|
|
4118
|
-
}).nullable();
|
|
4119
|
-
var practitionerWorkingHoursSchema = import_zod14.z.object({
|
|
4120
|
-
practitionerId: import_zod14.z.string().min(1),
|
|
4121
|
-
clinicId: import_zod14.z.string().min(1),
|
|
4122
|
-
monday: timeSlotSchema,
|
|
4123
|
-
tuesday: timeSlotSchema,
|
|
4124
|
-
wednesday: timeSlotSchema,
|
|
4125
|
-
thursday: timeSlotSchema,
|
|
4126
|
-
friday: timeSlotSchema,
|
|
4127
|
-
saturday: timeSlotSchema,
|
|
4128
|
-
sunday: timeSlotSchema,
|
|
4129
|
-
createdAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4130
|
-
updatedAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date())
|
|
4131
|
-
});
|
|
4132
|
-
var practitionerClinicWorkingHoursSchema = import_zod14.z.object({
|
|
4133
|
-
clinicId: import_zod14.z.string().min(1),
|
|
4134
|
-
workingHours: import_zod14.z.object({
|
|
4135
|
-
monday: timeSlotSchema,
|
|
4136
|
-
tuesday: timeSlotSchema,
|
|
4137
|
-
wednesday: timeSlotSchema,
|
|
4138
|
-
thursday: timeSlotSchema,
|
|
4139
|
-
friday: timeSlotSchema,
|
|
4140
|
-
saturday: timeSlotSchema,
|
|
4141
|
-
sunday: timeSlotSchema
|
|
4142
|
-
}),
|
|
4143
|
-
isActive: import_zod14.z.boolean(),
|
|
4144
|
-
createdAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4145
|
-
updatedAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date())
|
|
4146
|
-
});
|
|
4147
|
-
var practitionerSchema = import_zod14.z.object({
|
|
4148
|
-
id: import_zod14.z.string().min(1),
|
|
4149
|
-
userRef: import_zod14.z.string().min(1),
|
|
4150
|
-
basicInfo: practitionerBasicInfoSchema,
|
|
4151
|
-
certification: practitionerCertificationSchema,
|
|
4152
|
-
clinics: import_zod14.z.array(import_zod14.z.string()),
|
|
4153
|
-
clinicWorkingHours: import_zod14.z.array(practitionerClinicWorkingHoursSchema),
|
|
4154
|
-
clinicsInfo: import_zod14.z.array(clinicInfoSchema),
|
|
4155
|
-
procedures: import_zod14.z.array(import_zod14.z.string()),
|
|
4156
|
-
proceduresInfo: import_zod14.z.array(procedureSummaryInfoSchema),
|
|
4157
|
-
reviewInfo: practitionerReviewInfoSchema,
|
|
4158
|
-
isActive: import_zod14.z.boolean(),
|
|
4159
|
-
isVerified: import_zod14.z.boolean(),
|
|
4160
|
-
status: import_zod14.z.nativeEnum(PractitionerStatus),
|
|
4161
|
-
createdAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4162
|
-
updatedAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date())
|
|
4163
|
-
});
|
|
4164
|
-
var createPractitionerSchema = import_zod14.z.object({
|
|
4165
|
-
userRef: import_zod14.z.string().min(1),
|
|
4166
|
-
basicInfo: practitionerBasicInfoSchema,
|
|
4167
|
-
certification: practitionerCertificationSchema,
|
|
4168
|
-
clinics: import_zod14.z.array(import_zod14.z.string()).optional(),
|
|
4169
|
-
clinicWorkingHours: import_zod14.z.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4170
|
-
clinicsInfo: import_zod14.z.array(clinicInfoSchema).optional(),
|
|
4171
|
-
proceduresInfo: import_zod14.z.array(procedureSummaryInfoSchema).optional(),
|
|
4172
|
-
isActive: import_zod14.z.boolean(),
|
|
4173
|
-
isVerified: import_zod14.z.boolean(),
|
|
4174
|
-
status: import_zod14.z.nativeEnum(PractitionerStatus).optional()
|
|
4175
|
-
});
|
|
4176
|
-
var createDraftPractitionerSchema = import_zod14.z.object({
|
|
4177
|
-
basicInfo: practitionerBasicInfoSchema,
|
|
4178
|
-
certification: practitionerCertificationSchema,
|
|
4179
|
-
clinics: import_zod14.z.array(import_zod14.z.string()).optional(),
|
|
4180
|
-
clinicWorkingHours: import_zod14.z.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4181
|
-
clinicsInfo: import_zod14.z.array(clinicInfoSchema).optional(),
|
|
4182
|
-
proceduresInfo: import_zod14.z.array(procedureSummaryInfoSchema).optional(),
|
|
4183
|
-
isActive: import_zod14.z.boolean().optional().default(false),
|
|
4184
|
-
isVerified: import_zod14.z.boolean().optional().default(false)
|
|
4185
|
-
});
|
|
4186
|
-
var practitionerTokenSchema = import_zod14.z.object({
|
|
4187
|
-
id: import_zod14.z.string().min(1),
|
|
4188
|
-
token: import_zod14.z.string().min(6),
|
|
4189
|
-
practitionerId: import_zod14.z.string().min(1),
|
|
4190
|
-
email: import_zod14.z.string().email(),
|
|
4191
|
-
clinicId: import_zod14.z.string().min(1),
|
|
4192
|
-
status: import_zod14.z.nativeEnum(PractitionerTokenStatus),
|
|
4193
|
-
createdBy: import_zod14.z.string().min(1),
|
|
4194
|
-
createdAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4195
|
-
expiresAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()),
|
|
4196
|
-
usedBy: import_zod14.z.string().optional(),
|
|
4197
|
-
usedAt: import_zod14.z.instanceof(import_firestore15.Timestamp).or(import_zod14.z.date()).optional()
|
|
4198
|
-
});
|
|
4199
|
-
var createPractitionerTokenSchema = import_zod14.z.object({
|
|
4200
|
-
practitionerId: import_zod14.z.string().min(1),
|
|
4201
|
-
email: import_zod14.z.string().email(),
|
|
4202
|
-
clinicId: import_zod14.z.string().min(1),
|
|
4203
|
-
expiresAt: import_zod14.z.date().optional()
|
|
4204
|
-
});
|
|
4205
|
-
var practitionerSignupSchema = import_zod14.z.object({
|
|
4206
|
-
email: import_zod14.z.string().email(),
|
|
4207
|
-
password: import_zod14.z.string().min(8),
|
|
4208
|
-
firstName: import_zod14.z.string().min(2).max(50).optional(),
|
|
4209
|
-
lastName: import_zod14.z.string().min(2).max(50).optional(),
|
|
4210
|
-
token: import_zod14.z.string().optional(),
|
|
4211
|
-
profileData: import_zod14.z.object({
|
|
4212
|
-
basicInfo: import_zod14.z.object({
|
|
4213
|
-
phoneNumber: import_zod14.z.string().optional(),
|
|
4214
|
-
profileImageUrl: import_zod14.z.string().optional(),
|
|
4215
|
-
gender: import_zod14.z.enum(["male", "female", "other"]).optional(),
|
|
4216
|
-
bio: import_zod14.z.string().optional()
|
|
4217
|
-
}).optional(),
|
|
4218
|
-
certification: import_zod14.z.any().optional()
|
|
4219
|
-
}).optional()
|
|
4220
|
-
});
|
|
4221
|
-
|
|
4222
|
-
// src/services/practitioner/practitioner.service.ts
|
|
4223
|
-
var import_zod15 = require("zod");
|
|
4224
|
-
var import_geofire_common2 = require("geofire-common");
|
|
4225
|
-
var PractitionerService = class extends BaseService {
|
|
4226
|
-
constructor(db, auth, app, clinicService) {
|
|
4067
|
+
var import_storage4 = require("firebase/storage");
|
|
4068
|
+
var import_firestore16 = require("firebase/firestore");
|
|
4069
|
+
var MediaAccessLevel = /* @__PURE__ */ ((MediaAccessLevel2) => {
|
|
4070
|
+
MediaAccessLevel2["PUBLIC"] = "public";
|
|
4071
|
+
MediaAccessLevel2["PRIVATE"] = "private";
|
|
4072
|
+
MediaAccessLevel2["CONFIDENTIAL"] = "confidential";
|
|
4073
|
+
return MediaAccessLevel2;
|
|
4074
|
+
})(MediaAccessLevel || {});
|
|
4075
|
+
var MEDIA_METADATA_COLLECTION = "media_metadata";
|
|
4076
|
+
var MediaService = class extends BaseService {
|
|
4077
|
+
constructor(db, auth, app) {
|
|
4227
4078
|
super(db, auth, app);
|
|
4228
|
-
this.clinicService = clinicService;
|
|
4229
|
-
}
|
|
4230
|
-
getClinicService() {
|
|
4231
|
-
if (!this.clinicService) {
|
|
4232
|
-
throw new Error("Clinic service not initialized!");
|
|
4233
|
-
}
|
|
4234
|
-
return this.clinicService;
|
|
4235
|
-
}
|
|
4236
|
-
setClinicService(clinicService) {
|
|
4237
|
-
this.clinicService = clinicService;
|
|
4238
4079
|
}
|
|
4239
4080
|
/**
|
|
4240
|
-
*
|
|
4081
|
+
* Upload a media file, store its metadata, and return the metadata including the URL.
|
|
4082
|
+
* @param file - The file to upload.
|
|
4083
|
+
* @param ownerId - ID of the owner (user, patient, clinic, etc.).
|
|
4084
|
+
* @param accessLevel - Access level (public, private, confidential).
|
|
4085
|
+
* @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
|
|
4086
|
+
* @param originalFileName - Optional: the original name of the file, if not using file.name.
|
|
4087
|
+
* @returns Promise with the media metadata.
|
|
4241
4088
|
*/
|
|
4242
|
-
async
|
|
4089
|
+
async uploadMedia(file, ownerId, accessLevel, collectionName, originalFileName) {
|
|
4090
|
+
const mediaId = this.generateId();
|
|
4091
|
+
const fileNameToUse = originalFileName || (file instanceof File ? file.name : file.toString());
|
|
4092
|
+
const uniqueFileName = `${mediaId}-${fileNameToUse}`;
|
|
4093
|
+
const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
|
|
4094
|
+
console.log(`[MediaService] Uploading file to: ${filePath}`);
|
|
4095
|
+
const storageRef = (0, import_storage4.ref)(this.storage, filePath);
|
|
4243
4096
|
try {
|
|
4244
|
-
const
|
|
4245
|
-
|
|
4246
|
-
const reviewInfo = {
|
|
4247
|
-
totalReviews: 0,
|
|
4248
|
-
averageRating: 0,
|
|
4249
|
-
knowledgeAndExpertise: 0,
|
|
4250
|
-
communicationSkills: 0,
|
|
4251
|
-
bedSideManner: 0,
|
|
4252
|
-
thoroughness: 0,
|
|
4253
|
-
trustworthiness: 0,
|
|
4254
|
-
recommendationPercentage: 0
|
|
4255
|
-
};
|
|
4256
|
-
const practitioner = {
|
|
4257
|
-
id: practitionerId,
|
|
4258
|
-
userRef: validData.userRef,
|
|
4259
|
-
basicInfo: validData.basicInfo,
|
|
4260
|
-
certification: validData.certification,
|
|
4261
|
-
clinics: validData.clinics || [],
|
|
4262
|
-
clinicWorkingHours: validData.clinicWorkingHours || [],
|
|
4263
|
-
clinicsInfo: [],
|
|
4264
|
-
procedures: [],
|
|
4265
|
-
proceduresInfo: [],
|
|
4266
|
-
reviewInfo,
|
|
4267
|
-
isActive: validData.isActive !== void 0 ? validData.isActive : true,
|
|
4268
|
-
isVerified: validData.isVerified !== void 0 ? validData.isVerified : false,
|
|
4269
|
-
status: validData.status || "active" /* ACTIVE */,
|
|
4270
|
-
createdAt: (0, import_firestore16.serverTimestamp)(),
|
|
4271
|
-
updatedAt: (0, import_firestore16.serverTimestamp)()
|
|
4272
|
-
};
|
|
4273
|
-
practitionerSchema.parse({
|
|
4274
|
-
...practitioner,
|
|
4275
|
-
createdAt: import_firestore16.Timestamp.now(),
|
|
4276
|
-
updatedAt: import_firestore16.Timestamp.now()
|
|
4097
|
+
const uploadResult = await (0, import_storage4.uploadBytes)(storageRef, file, {
|
|
4098
|
+
contentType: file.type
|
|
4277
4099
|
});
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4100
|
+
console.log("[MediaService] File uploaded successfully", uploadResult);
|
|
4101
|
+
const downloadURL = await (0, import_storage4.getDownloadURL)(uploadResult.ref);
|
|
4102
|
+
console.log("[MediaService] Got download URL:", downloadURL);
|
|
4103
|
+
const metadata = {
|
|
4104
|
+
id: mediaId,
|
|
4105
|
+
name: fileNameToUse,
|
|
4106
|
+
url: downloadURL,
|
|
4107
|
+
contentType: file.type,
|
|
4108
|
+
size: file.size,
|
|
4109
|
+
createdAt: import_firestore15.Timestamp.now(),
|
|
4110
|
+
accessLevel,
|
|
4111
|
+
ownerId,
|
|
4112
|
+
collectionName,
|
|
4113
|
+
path: filePath
|
|
4114
|
+
};
|
|
4115
|
+
const metadataDocRef = (0, import_firestore16.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4116
|
+
await (0, import_firestore16.setDoc)(metadataDocRef, metadata);
|
|
4117
|
+
console.log("[MediaService] Metadata stored in Firestore:", mediaId);
|
|
4118
|
+
return metadata;
|
|
4291
4119
|
} catch (error) {
|
|
4292
|
-
|
|
4293
|
-
throw new Error(`Invalid practitioner data: ${error.message}`);
|
|
4294
|
-
}
|
|
4295
|
-
console.error("Error creating practitioner:", error);
|
|
4120
|
+
console.error("[MediaService] Error during media upload:", error);
|
|
4296
4121
|
throw error;
|
|
4297
4122
|
}
|
|
4298
4123
|
}
|
|
4299
4124
|
/**
|
|
4300
|
-
*
|
|
4301
|
-
*
|
|
4302
|
-
* @
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4125
|
+
* Get media metadata from Firestore by its ID.
|
|
4126
|
+
* @param mediaId - ID of the media.
|
|
4127
|
+
* @returns Promise with the media metadata or null if not found.
|
|
4128
|
+
*/
|
|
4129
|
+
async getMediaMetadata(mediaId) {
|
|
4130
|
+
console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
|
|
4131
|
+
const docRef = (0, import_firestore16.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4132
|
+
const docSnap = await (0, import_firestore16.getDoc)(docRef);
|
|
4133
|
+
if (docSnap.exists()) {
|
|
4134
|
+
console.log("[MediaService] Metadata found:", docSnap.data());
|
|
4135
|
+
return docSnap.data();
|
|
4136
|
+
}
|
|
4137
|
+
console.log("[MediaService] No metadata found for ID:", mediaId);
|
|
4138
|
+
return null;
|
|
4139
|
+
}
|
|
4140
|
+
/**
|
|
4141
|
+
* Get media metadata from Firestore by its public URL.
|
|
4142
|
+
* @param url - The public URL of the media file.
|
|
4143
|
+
* @returns Promise with the media metadata or null if not found.
|
|
4144
|
+
*/
|
|
4145
|
+
async getMediaMetadataByUrl(url) {
|
|
4146
|
+
console.log(`[MediaService] Getting media metadata by URL: ${url}`);
|
|
4147
|
+
const q = (0, import_firestore16.query)(
|
|
4148
|
+
(0, import_firestore16.collection)(this.db, MEDIA_METADATA_COLLECTION),
|
|
4149
|
+
(0, import_firestore16.where)("url", "==", url),
|
|
4150
|
+
(0, import_firestore16.limit)(1)
|
|
4151
|
+
);
|
|
4308
4152
|
try {
|
|
4309
|
-
const
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
const clinicsToAdd = /* @__PURE__ */ new Set([clinicId]);
|
|
4315
|
-
if (data.clinics && data.clinics.length > 0) {
|
|
4316
|
-
for (const cId of data.clinics) {
|
|
4317
|
-
if (cId !== clinicId) {
|
|
4318
|
-
const otherClinic = await this.getClinicService().getClinic(cId);
|
|
4319
|
-
if (!otherClinic) {
|
|
4320
|
-
throw new Error(`Clinic ${cId} not found`);
|
|
4321
|
-
}
|
|
4322
|
-
}
|
|
4323
|
-
clinicsToAdd.add(cId);
|
|
4324
|
-
}
|
|
4325
|
-
}
|
|
4326
|
-
const clinics = Array.from(clinicsToAdd);
|
|
4327
|
-
const defaultReviewInfo = {
|
|
4328
|
-
totalReviews: 0,
|
|
4329
|
-
averageRating: 0,
|
|
4330
|
-
knowledgeAndExpertise: 0,
|
|
4331
|
-
communicationSkills: 0,
|
|
4332
|
-
bedSideManner: 0,
|
|
4333
|
-
thoroughness: 0,
|
|
4334
|
-
trustworthiness: 0,
|
|
4335
|
-
recommendationPercentage: 0
|
|
4336
|
-
};
|
|
4337
|
-
const practitionerId = this.generateId();
|
|
4338
|
-
const clinicsInfo = [];
|
|
4339
|
-
for (const cId of clinics) {
|
|
4340
|
-
const clinicData = await this.getClinicService().getClinic(cId);
|
|
4341
|
-
if (clinicData) {
|
|
4342
|
-
clinicsInfo.push({
|
|
4343
|
-
id: clinicData.id,
|
|
4344
|
-
name: clinicData.name,
|
|
4345
|
-
location: clinicData.location,
|
|
4346
|
-
contactInfo: clinicData.contactInfo,
|
|
4347
|
-
// Make sure we're using the right property for featuredPhoto
|
|
4348
|
-
featuredPhoto: clinicData.featuredPhotos && clinicData.featuredPhotos.length > 0 ? typeof clinicData.featuredPhotos[0] === "string" ? clinicData.featuredPhotos[0] : "" : (typeof clinicData.coverPhoto === "string" ? clinicData.coverPhoto : "") || "",
|
|
4349
|
-
description: clinicData.description || null
|
|
4350
|
-
});
|
|
4351
|
-
}
|
|
4352
|
-
}
|
|
4353
|
-
const finalClinicsInfo = validatedData.clinicsInfo && validatedData.clinicsInfo.length > 0 ? validatedData.clinicsInfo : clinicsInfo;
|
|
4354
|
-
const proceduresInfo = [];
|
|
4355
|
-
const practitionerData = {
|
|
4356
|
-
id: practitionerId,
|
|
4357
|
-
userRef: "",
|
|
4358
|
-
// Prazno - biće popunjeno kada korisnik kreira nalog
|
|
4359
|
-
basicInfo: validatedData.basicInfo,
|
|
4360
|
-
certification: validatedData.certification,
|
|
4361
|
-
clinics,
|
|
4362
|
-
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
4363
|
-
clinicsInfo: finalClinicsInfo,
|
|
4364
|
-
procedures: [],
|
|
4365
|
-
proceduresInfo,
|
|
4366
|
-
reviewInfo: defaultReviewInfo,
|
|
4367
|
-
isActive: validatedData.isActive !== void 0 ? validatedData.isActive : false,
|
|
4368
|
-
isVerified: validatedData.isVerified !== void 0 ? validatedData.isVerified : false,
|
|
4369
|
-
status: "draft" /* DRAFT */,
|
|
4370
|
-
createdAt: (0, import_firestore16.serverTimestamp)(),
|
|
4371
|
-
updatedAt: (0, import_firestore16.serverTimestamp)()
|
|
4372
|
-
};
|
|
4373
|
-
practitionerSchema.parse({
|
|
4374
|
-
...practitionerData,
|
|
4375
|
-
userRef: "temp-for-validation",
|
|
4376
|
-
createdAt: import_firestore16.Timestamp.now(),
|
|
4377
|
-
updatedAt: import_firestore16.Timestamp.now()
|
|
4378
|
-
});
|
|
4379
|
-
await (0, import_firestore16.setDoc)(
|
|
4380
|
-
(0, import_firestore16.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
|
|
4381
|
-
practitionerData
|
|
4382
|
-
);
|
|
4383
|
-
const savedPractitioner = await this.getPractitioner(practitionerData.id);
|
|
4384
|
-
if (!savedPractitioner) {
|
|
4385
|
-
throw new Error("Failed to create draft practitioner profile");
|
|
4153
|
+
const querySnapshot = await (0, import_firestore16.getDocs)(q);
|
|
4154
|
+
if (!querySnapshot.empty) {
|
|
4155
|
+
const metadata = querySnapshot.docs[0].data();
|
|
4156
|
+
console.log("[MediaService] Metadata found by URL:", metadata);
|
|
4157
|
+
return metadata;
|
|
4386
4158
|
}
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
const token = {
|
|
4390
|
-
id: this.generateId(),
|
|
4391
|
-
token: tokenString,
|
|
4392
|
-
practitionerId,
|
|
4393
|
-
email: practitionerData.basicInfo.email,
|
|
4394
|
-
clinicId,
|
|
4395
|
-
status: "active" /* ACTIVE */,
|
|
4396
|
-
createdBy,
|
|
4397
|
-
createdAt: import_firestore16.Timestamp.now(),
|
|
4398
|
-
expiresAt: import_firestore16.Timestamp.fromDate(expiration)
|
|
4399
|
-
};
|
|
4400
|
-
practitionerTokenSchema.parse(token);
|
|
4401
|
-
const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4402
|
-
await (0, import_firestore16.setDoc)((0, import_firestore16.doc)(this.db, tokenPath), token);
|
|
4403
|
-
return { practitioner: savedPractitioner, token };
|
|
4159
|
+
console.log("[MediaService] No metadata found for URL:", url);
|
|
4160
|
+
return null;
|
|
4404
4161
|
} catch (error) {
|
|
4405
|
-
|
|
4406
|
-
throw new Error("Invalid practitioner data: " + error.message);
|
|
4407
|
-
}
|
|
4162
|
+
console.error("[MediaService] Error fetching metadata by URL:", error);
|
|
4408
4163
|
throw error;
|
|
4409
4164
|
}
|
|
4410
4165
|
}
|
|
4411
4166
|
/**
|
|
4412
|
-
*
|
|
4413
|
-
* @param
|
|
4414
|
-
* @param createdBy ID of the user creating the token
|
|
4415
|
-
* @returns Created token
|
|
4167
|
+
* Delete media from storage and remove metadata from Firestore.
|
|
4168
|
+
* @param mediaId - ID of the media to delete.
|
|
4416
4169
|
*/
|
|
4417
|
-
async
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4170
|
+
async deleteMedia(mediaId) {
|
|
4171
|
+
console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
|
|
4172
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
4173
|
+
if (!metadata) {
|
|
4174
|
+
console.warn(
|
|
4175
|
+
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
|
|
4422
4176
|
);
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
const storageFileRef = (0, import_storage4.ref)(this.storage, metadata.path);
|
|
4180
|
+
try {
|
|
4181
|
+
await (0, import_storage4.deleteObject)(storageFileRef);
|
|
4182
|
+
console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
|
|
4183
|
+
const metadataDocRef = (0, import_firestore16.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4184
|
+
await (0, import_firestore16.deleteDoc)(metadataDocRef);
|
|
4185
|
+
console.log(
|
|
4186
|
+
`[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
|
|
4433
4187
|
);
|
|
4434
|
-
if (!clinic) {
|
|
4435
|
-
throw new Error(`Clinic ${validatedData.clinicId} not found`);
|
|
4436
|
-
}
|
|
4437
|
-
if (!practitioner.clinics.includes(validatedData.clinicId)) {
|
|
4438
|
-
throw new Error("Practitioner is not associated with this clinic");
|
|
4439
|
-
}
|
|
4440
|
-
const expiration = validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
4441
|
-
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
4442
|
-
const token = {
|
|
4443
|
-
id: this.generateId(),
|
|
4444
|
-
token: tokenString,
|
|
4445
|
-
practitionerId: validatedData.practitionerId,
|
|
4446
|
-
email: validatedData.email,
|
|
4447
|
-
clinicId: validatedData.clinicId,
|
|
4448
|
-
status: "active" /* ACTIVE */,
|
|
4449
|
-
createdBy,
|
|
4450
|
-
createdAt: import_firestore16.Timestamp.now(),
|
|
4451
|
-
expiresAt: import_firestore16.Timestamp.fromDate(expiration)
|
|
4452
|
-
};
|
|
4453
|
-
practitionerTokenSchema.parse(token);
|
|
4454
|
-
const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4455
|
-
await (0, import_firestore16.setDoc)((0, import_firestore16.doc)(this.db, tokenPath), token);
|
|
4456
|
-
return token;
|
|
4457
4188
|
} catch (error) {
|
|
4458
|
-
|
|
4459
|
-
throw new Error("Invalid token data: " + error.message);
|
|
4460
|
-
}
|
|
4189
|
+
console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
|
|
4461
4190
|
throw error;
|
|
4462
4191
|
}
|
|
4463
4192
|
}
|
|
4464
4193
|
/**
|
|
4465
|
-
*
|
|
4466
|
-
*
|
|
4467
|
-
* @
|
|
4194
|
+
* Update media access level. This involves moving the file in Firebase Storage
|
|
4195
|
+
* to a new path reflecting the new access level, and updating its metadata.
|
|
4196
|
+
* @param mediaId - ID of the media to update.
|
|
4197
|
+
* @param newAccessLevel - New access level.
|
|
4198
|
+
* @returns Promise with the updated media metadata, or null if metadata not found.
|
|
4468
4199
|
*/
|
|
4469
|
-
async
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
);
|
|
4474
|
-
const q = (0, import_firestore16.query)(
|
|
4475
|
-
tokensRef,
|
|
4476
|
-
(0, import_firestore16.where)("status", "==", "active" /* ACTIVE */),
|
|
4477
|
-
(0, import_firestore16.where)("expiresAt", ">", import_firestore16.Timestamp.now())
|
|
4200
|
+
async updateMediaAccessLevel(mediaId, newAccessLevel) {
|
|
4201
|
+
var _a;
|
|
4202
|
+
console.log(
|
|
4203
|
+
`[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
|
|
4478
4204
|
);
|
|
4479
|
-
const
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
* Gets a token by its string value and validates it
|
|
4484
|
-
* @param tokenString The token string to find
|
|
4485
|
-
* @returns The token if found and valid, null otherwise
|
|
4486
|
-
*/
|
|
4487
|
-
async validateToken(tokenString) {
|
|
4488
|
-
const practitionersRef = (0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION);
|
|
4489
|
-
const practitionersSnapshot = await (0, import_firestore16.getDocs)(practitionersRef);
|
|
4490
|
-
for (const practitionerDoc of practitionersSnapshot.docs) {
|
|
4491
|
-
const practitionerId = practitionerDoc.id;
|
|
4492
|
-
const tokensRef = (0, import_firestore16.collection)(
|
|
4493
|
-
this.db,
|
|
4494
|
-
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
4205
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
4206
|
+
if (!metadata) {
|
|
4207
|
+
console.warn(
|
|
4208
|
+
`[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
|
|
4495
4209
|
);
|
|
4210
|
+
return null;
|
|
4211
|
+
}
|
|
4212
|
+
if (metadata.accessLevel === newAccessLevel) {
|
|
4496
4213
|
console.log(
|
|
4497
|
-
`[
|
|
4498
|
-
{
|
|
4499
|
-
tokenString,
|
|
4500
|
-
timestamp: import_firestore16.Timestamp.now().toDate()
|
|
4501
|
-
}
|
|
4502
|
-
);
|
|
4503
|
-
const q = (0, import_firestore16.query)(
|
|
4504
|
-
tokensRef,
|
|
4505
|
-
(0, import_firestore16.where)("token", "==", tokenString),
|
|
4506
|
-
(0, import_firestore16.where)("status", "==", "active" /* ACTIVE */),
|
|
4507
|
-
(0, import_firestore16.where)("expiresAt", ">", import_firestore16.Timestamp.now())
|
|
4214
|
+
`[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
|
|
4508
4215
|
);
|
|
4216
|
+
const metadataDocRef = (0, import_firestore16.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4509
4217
|
try {
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
`[PRACTITIONER] Token query results for practitioner ${practitionerId}`,
|
|
4513
|
-
{
|
|
4514
|
-
found: !tokenSnapshot.empty,
|
|
4515
|
-
count: tokenSnapshot.size
|
|
4516
|
-
}
|
|
4517
|
-
);
|
|
4518
|
-
if (!tokenSnapshot.empty) {
|
|
4519
|
-
const tokenData = tokenSnapshot.docs[0].data();
|
|
4520
|
-
console.log(`[PRACTITIONER] Valid token found`, {
|
|
4521
|
-
tokenId: tokenData.id,
|
|
4522
|
-
expiresAt: tokenData.expiresAt.toDate()
|
|
4523
|
-
});
|
|
4524
|
-
return tokenData;
|
|
4525
|
-
}
|
|
4218
|
+
await (0, import_firestore16.updateDoc)(metadataDocRef, { updatedAt: import_firestore15.Timestamp.now() });
|
|
4219
|
+
return { ...metadata, updatedAt: import_firestore15.Timestamp.now() };
|
|
4526
4220
|
} catch (error) {
|
|
4527
4221
|
console.error(
|
|
4528
|
-
`[
|
|
4222
|
+
`[MediaService] Error updating timestamp for media ID ${mediaId}:`,
|
|
4529
4223
|
error
|
|
4530
4224
|
);
|
|
4531
4225
|
throw error;
|
|
4532
4226
|
}
|
|
4533
4227
|
}
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
* @param practitionerId ID of the practitioner
|
|
4540
|
-
* @param userId ID of the user using the token
|
|
4541
|
-
*/
|
|
4542
|
-
async markTokenAsUsed(tokenId, practitionerId, userId) {
|
|
4543
|
-
const tokenRef = (0, import_firestore16.doc)(
|
|
4544
|
-
this.db,
|
|
4545
|
-
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
|
|
4228
|
+
const oldStoragePath = metadata.path;
|
|
4229
|
+
const fileNamePart = `${metadata.id}-${metadata.name}`;
|
|
4230
|
+
const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
|
|
4231
|
+
console.log(
|
|
4232
|
+
`[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
|
|
4546
4233
|
);
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
usedBy: userId,
|
|
4550
|
-
usedAt: import_firestore16.Timestamp.now()
|
|
4551
|
-
});
|
|
4552
|
-
}
|
|
4553
|
-
/**
|
|
4554
|
-
* Dohvata zdravstvenog radnika po ID-u
|
|
4555
|
-
*/
|
|
4556
|
-
async getPractitioner(practitionerId) {
|
|
4557
|
-
const practitionerDoc = await (0, import_firestore16.getDoc)(
|
|
4558
|
-
(0, import_firestore16.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerId)
|
|
4559
|
-
);
|
|
4560
|
-
if (!practitionerDoc.exists()) {
|
|
4561
|
-
return null;
|
|
4562
|
-
}
|
|
4563
|
-
return practitionerDoc.data();
|
|
4564
|
-
}
|
|
4565
|
-
/**
|
|
4566
|
-
* Dohvata zdravstvenog radnika po User ID-u
|
|
4567
|
-
*/
|
|
4568
|
-
async getPractitionerByUserRef(userRef) {
|
|
4569
|
-
const q = (0, import_firestore16.query)(
|
|
4570
|
-
(0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4571
|
-
(0, import_firestore16.where)("userRef", "==", userRef)
|
|
4572
|
-
);
|
|
4573
|
-
const querySnapshot = await (0, import_firestore16.getDocs)(q);
|
|
4574
|
-
if (querySnapshot.empty) {
|
|
4575
|
-
return null;
|
|
4576
|
-
}
|
|
4577
|
-
return querySnapshot.docs[0].data();
|
|
4578
|
-
}
|
|
4579
|
-
/**
|
|
4580
|
-
* Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
|
|
4581
|
-
*/
|
|
4582
|
-
async getPractitionersByClinic(clinicId) {
|
|
4583
|
-
const q = (0, import_firestore16.query)(
|
|
4584
|
-
(0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4585
|
-
(0, import_firestore16.where)("clinics", "array-contains", clinicId),
|
|
4586
|
-
(0, import_firestore16.where)("isActive", "==", true),
|
|
4587
|
-
(0, import_firestore16.where)("status", "==", "active" /* ACTIVE */)
|
|
4588
|
-
);
|
|
4589
|
-
const querySnapshot = await (0, import_firestore16.getDocs)(q);
|
|
4590
|
-
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4591
|
-
}
|
|
4592
|
-
/**
|
|
4593
|
-
* Dohvata sve zdravstvene radnike za određenu kliniku
|
|
4594
|
-
*/
|
|
4595
|
-
async getAllPractitionersByClinic(clinicId) {
|
|
4596
|
-
const q = (0, import_firestore16.query)(
|
|
4597
|
-
(0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4598
|
-
(0, import_firestore16.where)("clinics", "array-contains", clinicId),
|
|
4599
|
-
(0, import_firestore16.where)("isActive", "==", true)
|
|
4600
|
-
);
|
|
4601
|
-
const querySnapshot = await (0, import_firestore16.getDocs)(q);
|
|
4602
|
-
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4603
|
-
}
|
|
4604
|
-
/**
|
|
4605
|
-
* Dohvata sve draft zdravstvene radnike za određenu kliniku sa statusom DRAFT
|
|
4606
|
-
*/
|
|
4607
|
-
async getDraftPractitionersByClinic(clinicId) {
|
|
4608
|
-
const q = (0, import_firestore16.query)(
|
|
4609
|
-
(0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4610
|
-
(0, import_firestore16.where)("clinics", "array-contains", clinicId),
|
|
4611
|
-
(0, import_firestore16.where)("status", "==", "draft" /* DRAFT */)
|
|
4612
|
-
);
|
|
4613
|
-
const querySnapshot = await (0, import_firestore16.getDocs)(q);
|
|
4614
|
-
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4615
|
-
}
|
|
4616
|
-
/**
|
|
4617
|
-
* Updates a practitioner
|
|
4618
|
-
*/
|
|
4619
|
-
async updatePractitioner(practitionerId, data) {
|
|
4234
|
+
const oldStorageFileRef = (0, import_storage4.ref)(this.storage, oldStoragePath);
|
|
4235
|
+
const newStorageFileRef = (0, import_storage4.ref)(this.storage, newStoragePath);
|
|
4620
4236
|
try {
|
|
4621
|
-
|
|
4622
|
-
const
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4237
|
+
console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
|
|
4238
|
+
const fileBytes = await (0, import_storage4.getBytes)(oldStorageFileRef);
|
|
4239
|
+
console.log(
|
|
4240
|
+
`[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
|
|
4241
|
+
);
|
|
4242
|
+
console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
|
|
4243
|
+
await (0, import_storage4.uploadBytes)(newStorageFileRef, fileBytes, {
|
|
4244
|
+
contentType: metadata.contentType
|
|
4245
|
+
});
|
|
4246
|
+
console.log(
|
|
4247
|
+
`[MediaService] Successfully uploaded bytes to ${newStoragePath}`
|
|
4248
|
+
);
|
|
4249
|
+
const newDownloadURL = await (0, import_storage4.getDownloadURL)(newStorageFileRef);
|
|
4250
|
+
console.log(
|
|
4251
|
+
`[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
|
|
4626
4252
|
);
|
|
4627
|
-
const practitionerDoc = await (0, import_firestore16.getDoc)(practitionerRef);
|
|
4628
|
-
if (!practitionerDoc.exists()) {
|
|
4629
|
-
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4630
|
-
}
|
|
4631
|
-
const currentPractitioner = practitionerDoc.data();
|
|
4632
4253
|
const updateData = {
|
|
4633
|
-
|
|
4634
|
-
|
|
4254
|
+
accessLevel: newAccessLevel,
|
|
4255
|
+
path: newStoragePath,
|
|
4256
|
+
url: newDownloadURL,
|
|
4257
|
+
updatedAt: import_firestore15.Timestamp.now()
|
|
4635
4258
|
};
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
`Failed to retrieve updated practitioner ${practitionerId}`
|
|
4641
|
-
);
|
|
4642
|
-
}
|
|
4643
|
-
return updatedPractitioner;
|
|
4644
|
-
} catch (error) {
|
|
4645
|
-
if (error instanceof import_zod15.z.ZodError) {
|
|
4646
|
-
throw new Error(`Invalid practitioner update data: ${error.message}`);
|
|
4647
|
-
}
|
|
4648
|
-
console.error(`Error updating practitioner ${practitionerId}:`, error);
|
|
4649
|
-
throw error;
|
|
4650
|
-
}
|
|
4651
|
-
}
|
|
4652
|
-
/**
|
|
4653
|
-
* Adds a clinic to a practitioner
|
|
4654
|
-
*/
|
|
4655
|
-
async addClinic(practitionerId, clinicId) {
|
|
4656
|
-
var _a;
|
|
4657
|
-
try {
|
|
4658
|
-
const practitionerRef = (0, import_firestore16.doc)(
|
|
4659
|
-
this.db,
|
|
4660
|
-
PRACTITIONERS_COLLECTION,
|
|
4661
|
-
practitionerId
|
|
4259
|
+
const metadataDocRef = (0, import_firestore16.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
4260
|
+
console.log(
|
|
4261
|
+
`[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
|
|
4262
|
+
updateData
|
|
4662
4263
|
);
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4264
|
+
await (0, import_firestore16.updateDoc)(metadataDocRef, updateData);
|
|
4265
|
+
console.log(
|
|
4266
|
+
`[MediaService] Successfully updated Firestore metadata for ${mediaId}`
|
|
4267
|
+
);
|
|
4268
|
+
try {
|
|
4269
|
+
console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
|
|
4270
|
+
await (0, import_storage4.deleteObject)(oldStorageFileRef);
|
|
4669
4271
|
console.log(
|
|
4670
|
-
`
|
|
4272
|
+
`[MediaService] Successfully deleted old file from ${oldStoragePath}`
|
|
4273
|
+
);
|
|
4274
|
+
} catch (deleteError) {
|
|
4275
|
+
console.error(
|
|
4276
|
+
`[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
|
|
4277
|
+
deleteError
|
|
4671
4278
|
);
|
|
4672
|
-
return;
|
|
4673
4279
|
}
|
|
4674
|
-
|
|
4675
|
-
clinics: (0, import_firestore16.arrayUnion)(clinicId),
|
|
4676
|
-
updatedAt: (0, import_firestore16.serverTimestamp)()
|
|
4677
|
-
});
|
|
4280
|
+
return { ...metadata, ...updateData };
|
|
4678
4281
|
} catch (error) {
|
|
4679
4282
|
console.error(
|
|
4680
|
-
`Error
|
|
4283
|
+
`[MediaService] Error updating media access level and moving file for ${mediaId}:`,
|
|
4681
4284
|
error
|
|
4682
4285
|
);
|
|
4286
|
+
if (newStorageFileRef && error.code !== "storage/object-not-found" && ((_a = error.message) == null ? void 0 : _a.includes("uploadBytes"))) {
|
|
4287
|
+
console.warn(
|
|
4288
|
+
`[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
|
|
4289
|
+
);
|
|
4290
|
+
try {
|
|
4291
|
+
await (0, import_storage4.deleteObject)(newStorageFileRef);
|
|
4292
|
+
console.warn(
|
|
4293
|
+
`[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
|
|
4294
|
+
);
|
|
4295
|
+
} catch (cleanupError) {
|
|
4296
|
+
console.error(
|
|
4297
|
+
`[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
|
|
4298
|
+
cleanupError
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4683
4302
|
throw error;
|
|
4684
4303
|
}
|
|
4685
4304
|
}
|
|
4686
4305
|
/**
|
|
4687
|
-
*
|
|
4306
|
+
* List all media for an owner, optionally filtered by collection and access level.
|
|
4307
|
+
* @param ownerId - ID of the owner.
|
|
4308
|
+
* @param collectionName - Optional: Filter by collection name.
|
|
4309
|
+
* @param accessLevel - Optional: Filter by access level.
|
|
4310
|
+
* @param count - Optional: Number of items to fetch.
|
|
4311
|
+
* @param startAfterId - Optional: ID of the document to start after (for pagination).
|
|
4688
4312
|
*/
|
|
4689
|
-
async
|
|
4313
|
+
async listMedia(ownerId, collectionName, accessLevel, count, startAfterId) {
|
|
4314
|
+
console.log(`[MediaService] Listing media for owner: ${ownerId}`);
|
|
4315
|
+
let qConstraints = [(0, import_firestore16.where)("ownerId", "==", ownerId)];
|
|
4316
|
+
if (collectionName) {
|
|
4317
|
+
qConstraints.push((0, import_firestore16.where)("collectionName", "==", collectionName));
|
|
4318
|
+
}
|
|
4319
|
+
if (accessLevel) {
|
|
4320
|
+
qConstraints.push((0, import_firestore16.where)("accessLevel", "==", accessLevel));
|
|
4321
|
+
}
|
|
4322
|
+
qConstraints.push((0, import_firestore16.orderBy)("createdAt", "desc"));
|
|
4323
|
+
if (count) {
|
|
4324
|
+
qConstraints.push((0, import_firestore16.limit)(count));
|
|
4325
|
+
}
|
|
4326
|
+
if (startAfterId) {
|
|
4327
|
+
const startAfterDoc = await this.getMediaMetadata(startAfterId);
|
|
4328
|
+
if (startAfterDoc) {
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
const finalQuery = (0, import_firestore16.query)(
|
|
4332
|
+
(0, import_firestore16.collection)(this.db, MEDIA_METADATA_COLLECTION),
|
|
4333
|
+
...qConstraints
|
|
4334
|
+
);
|
|
4690
4335
|
try {
|
|
4691
|
-
const
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
practitionerId
|
|
4336
|
+
const querySnapshot = await (0, import_firestore16.getDocs)(finalQuery);
|
|
4337
|
+
const mediaList = querySnapshot.docs.map(
|
|
4338
|
+
(doc34) => doc34.data()
|
|
4695
4339
|
);
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4699
|
-
}
|
|
4700
|
-
await (0, import_firestore16.updateDoc)(practitionerRef, {
|
|
4701
|
-
clinics: (0, import_firestore16.arrayRemove)(clinicId),
|
|
4702
|
-
updatedAt: (0, import_firestore16.serverTimestamp)()
|
|
4703
|
-
});
|
|
4340
|
+
console.log(`[MediaService] Found ${mediaList.length} media items.`);
|
|
4341
|
+
return mediaList;
|
|
4704
4342
|
} catch (error) {
|
|
4705
|
-
console.error(
|
|
4706
|
-
`Error removing clinic ${clinicId} from practitioner ${practitionerId}:`,
|
|
4707
|
-
error
|
|
4708
|
-
);
|
|
4343
|
+
console.error("[MediaService] Error listing media:", error);
|
|
4709
4344
|
throw error;
|
|
4710
4345
|
}
|
|
4711
4346
|
}
|
|
4712
4347
|
/**
|
|
4713
|
-
*
|
|
4714
|
-
|
|
4715
|
-
async deactivatePractitioner(practitionerId) {
|
|
4716
|
-
await this.updatePractitioner(practitionerId, {
|
|
4717
|
-
isActive: false
|
|
4718
|
-
});
|
|
4719
|
-
}
|
|
4720
|
-
/**
|
|
4721
|
-
* Aktivira profil zdravstvenog radnika
|
|
4722
|
-
*/
|
|
4723
|
-
async activatePractitioner(practitionerId) {
|
|
4724
|
-
await this.updatePractitioner(practitionerId, {
|
|
4725
|
-
isActive: true
|
|
4726
|
-
});
|
|
4727
|
-
}
|
|
4728
|
-
/**
|
|
4729
|
-
* Briše profil zdravstvenog radnika
|
|
4348
|
+
* Get download URL for media. (Convenience, as URL is in metadata)
|
|
4349
|
+
* @param mediaId - ID of the media.
|
|
4730
4350
|
*/
|
|
4731
|
-
async
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4351
|
+
async getMediaDownloadUrl(mediaId) {
|
|
4352
|
+
console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
|
|
4353
|
+
const metadata = await this.getMediaMetadata(mediaId);
|
|
4354
|
+
if (metadata && metadata.url) {
|
|
4355
|
+
console.log(`[MediaService] URL found: ${metadata.url}`);
|
|
4356
|
+
return metadata.url;
|
|
4735
4357
|
}
|
|
4736
|
-
|
|
4358
|
+
console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
|
|
4359
|
+
return null;
|
|
4737
4360
|
}
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4361
|
+
};
|
|
4362
|
+
|
|
4363
|
+
// src/validations/practitioner.schema.ts
|
|
4364
|
+
var import_zod14 = require("zod");
|
|
4365
|
+
var import_firestore17 = require("firebase/firestore");
|
|
4366
|
+
|
|
4367
|
+
// src/backoffice/types/static/certification.types.ts
|
|
4368
|
+
var CertificationLevel = /* @__PURE__ */ ((CertificationLevel2) => {
|
|
4369
|
+
CertificationLevel2["AESTHETICIAN"] = "aesthetician";
|
|
4370
|
+
CertificationLevel2["NURSE_ASSISTANT"] = "nurse_assistant";
|
|
4371
|
+
CertificationLevel2["NURSE"] = "nurse";
|
|
4372
|
+
CertificationLevel2["NURSE_PRACTITIONER"] = "nurse_practitioner";
|
|
4373
|
+
CertificationLevel2["PHYSICIAN_ASSISTANT"] = "physician_assistant";
|
|
4374
|
+
CertificationLevel2["DOCTOR"] = "doctor";
|
|
4375
|
+
CertificationLevel2["SPECIALIST"] = "specialist";
|
|
4376
|
+
CertificationLevel2["PLASTIC_SURGEON"] = "plastic_surgeon";
|
|
4377
|
+
return CertificationLevel2;
|
|
4378
|
+
})(CertificationLevel || {});
|
|
4379
|
+
var CertificationSpecialty = /* @__PURE__ */ ((CertificationSpecialty3) => {
|
|
4380
|
+
CertificationSpecialty3["LASER"] = "laser";
|
|
4381
|
+
CertificationSpecialty3["INJECTABLES"] = "injectables";
|
|
4382
|
+
CertificationSpecialty3["CHEMICAL_PEELS"] = "chemical_peels";
|
|
4383
|
+
CertificationSpecialty3["MICRODERMABRASION"] = "microdermabrasion";
|
|
4384
|
+
CertificationSpecialty3["BODY_CONTOURING"] = "body_contouring";
|
|
4385
|
+
CertificationSpecialty3["SKIN_CARE"] = "skin_care";
|
|
4386
|
+
CertificationSpecialty3["WOUND_CARE"] = "wound_care";
|
|
4387
|
+
CertificationSpecialty3["ANESTHESIA"] = "anesthesia";
|
|
4388
|
+
return CertificationSpecialty3;
|
|
4389
|
+
})(CertificationSpecialty || {});
|
|
4390
|
+
|
|
4391
|
+
// src/validations/practitioner.schema.ts
|
|
4392
|
+
var practitionerBasicInfoSchema = import_zod14.z.object({
|
|
4393
|
+
firstName: import_zod14.z.string().min(2).max(50),
|
|
4394
|
+
lastName: import_zod14.z.string().min(2).max(50),
|
|
4395
|
+
title: import_zod14.z.string().min(2).max(100),
|
|
4396
|
+
email: import_zod14.z.string().email(),
|
|
4397
|
+
phoneNumber: import_zod14.z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
|
|
4398
|
+
dateOfBirth: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4399
|
+
gender: import_zod14.z.enum(["male", "female", "other"]),
|
|
4400
|
+
profileImageUrl: mediaResourceSchema.optional(),
|
|
4401
|
+
bio: import_zod14.z.string().max(1e3).optional(),
|
|
4402
|
+
languages: import_zod14.z.array(import_zod14.z.string()).min(1)
|
|
4403
|
+
});
|
|
4404
|
+
var practitionerCertificationSchema = import_zod14.z.object({
|
|
4405
|
+
level: import_zod14.z.nativeEnum(CertificationLevel),
|
|
4406
|
+
specialties: import_zod14.z.array(import_zod14.z.nativeEnum(CertificationSpecialty)),
|
|
4407
|
+
licenseNumber: import_zod14.z.string().min(3).max(50),
|
|
4408
|
+
issuingAuthority: import_zod14.z.string().min(2).max(100),
|
|
4409
|
+
issueDate: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4410
|
+
expiryDate: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()).optional(),
|
|
4411
|
+
verificationStatus: import_zod14.z.enum(["pending", "verified", "rejected"])
|
|
4412
|
+
});
|
|
4413
|
+
var timeSlotSchema = import_zod14.z.object({
|
|
4414
|
+
start: import_zod14.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format"),
|
|
4415
|
+
end: import_zod14.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/, "Invalid time format")
|
|
4416
|
+
}).nullable();
|
|
4417
|
+
var practitionerWorkingHoursSchema = import_zod14.z.object({
|
|
4418
|
+
practitionerId: import_zod14.z.string().min(1),
|
|
4419
|
+
clinicId: import_zod14.z.string().min(1),
|
|
4420
|
+
monday: timeSlotSchema,
|
|
4421
|
+
tuesday: timeSlotSchema,
|
|
4422
|
+
wednesday: timeSlotSchema,
|
|
4423
|
+
thursday: timeSlotSchema,
|
|
4424
|
+
friday: timeSlotSchema,
|
|
4425
|
+
saturday: timeSlotSchema,
|
|
4426
|
+
sunday: timeSlotSchema,
|
|
4427
|
+
createdAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4428
|
+
updatedAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date())
|
|
4429
|
+
});
|
|
4430
|
+
var practitionerClinicWorkingHoursSchema = import_zod14.z.object({
|
|
4431
|
+
clinicId: import_zod14.z.string().min(1),
|
|
4432
|
+
workingHours: import_zod14.z.object({
|
|
4433
|
+
monday: timeSlotSchema,
|
|
4434
|
+
tuesday: timeSlotSchema,
|
|
4435
|
+
wednesday: timeSlotSchema,
|
|
4436
|
+
thursday: timeSlotSchema,
|
|
4437
|
+
friday: timeSlotSchema,
|
|
4438
|
+
saturday: timeSlotSchema,
|
|
4439
|
+
sunday: timeSlotSchema
|
|
4440
|
+
}),
|
|
4441
|
+
isActive: import_zod14.z.boolean(),
|
|
4442
|
+
createdAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4443
|
+
updatedAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date())
|
|
4444
|
+
});
|
|
4445
|
+
var practitionerSchema = import_zod14.z.object({
|
|
4446
|
+
id: import_zod14.z.string().min(1),
|
|
4447
|
+
userRef: import_zod14.z.string().min(1),
|
|
4448
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
4449
|
+
certification: practitionerCertificationSchema,
|
|
4450
|
+
clinics: import_zod14.z.array(import_zod14.z.string()),
|
|
4451
|
+
clinicWorkingHours: import_zod14.z.array(practitionerClinicWorkingHoursSchema),
|
|
4452
|
+
clinicsInfo: import_zod14.z.array(clinicInfoSchema),
|
|
4453
|
+
procedures: import_zod14.z.array(import_zod14.z.string()),
|
|
4454
|
+
proceduresInfo: import_zod14.z.array(procedureSummaryInfoSchema),
|
|
4455
|
+
reviewInfo: practitionerReviewInfoSchema,
|
|
4456
|
+
isActive: import_zod14.z.boolean(),
|
|
4457
|
+
isVerified: import_zod14.z.boolean(),
|
|
4458
|
+
status: import_zod14.z.nativeEnum(PractitionerStatus),
|
|
4459
|
+
createdAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4460
|
+
updatedAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date())
|
|
4461
|
+
});
|
|
4462
|
+
var createPractitionerSchema = import_zod14.z.object({
|
|
4463
|
+
userRef: import_zod14.z.string().min(1),
|
|
4464
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
4465
|
+
certification: practitionerCertificationSchema,
|
|
4466
|
+
clinics: import_zod14.z.array(import_zod14.z.string()).optional(),
|
|
4467
|
+
clinicWorkingHours: import_zod14.z.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4468
|
+
clinicsInfo: import_zod14.z.array(clinicInfoSchema).optional(),
|
|
4469
|
+
proceduresInfo: import_zod14.z.array(procedureSummaryInfoSchema).optional(),
|
|
4470
|
+
isActive: import_zod14.z.boolean(),
|
|
4471
|
+
isVerified: import_zod14.z.boolean(),
|
|
4472
|
+
status: import_zod14.z.nativeEnum(PractitionerStatus).optional()
|
|
4473
|
+
});
|
|
4474
|
+
var createDraftPractitionerSchema = import_zod14.z.object({
|
|
4475
|
+
basicInfo: practitionerBasicInfoSchema,
|
|
4476
|
+
certification: practitionerCertificationSchema,
|
|
4477
|
+
clinics: import_zod14.z.array(import_zod14.z.string()).optional(),
|
|
4478
|
+
clinicWorkingHours: import_zod14.z.array(practitionerClinicWorkingHoursSchema).optional(),
|
|
4479
|
+
clinicsInfo: import_zod14.z.array(clinicInfoSchema).optional(),
|
|
4480
|
+
proceduresInfo: import_zod14.z.array(procedureSummaryInfoSchema).optional(),
|
|
4481
|
+
isActive: import_zod14.z.boolean().optional().default(false),
|
|
4482
|
+
isVerified: import_zod14.z.boolean().optional().default(false)
|
|
4483
|
+
});
|
|
4484
|
+
var practitionerTokenSchema = import_zod14.z.object({
|
|
4485
|
+
id: import_zod14.z.string().min(1),
|
|
4486
|
+
token: import_zod14.z.string().min(6),
|
|
4487
|
+
practitionerId: import_zod14.z.string().min(1),
|
|
4488
|
+
email: import_zod14.z.string().email(),
|
|
4489
|
+
clinicId: import_zod14.z.string().min(1),
|
|
4490
|
+
status: import_zod14.z.nativeEnum(PractitionerTokenStatus),
|
|
4491
|
+
createdBy: import_zod14.z.string().min(1),
|
|
4492
|
+
createdAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4493
|
+
expiresAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()),
|
|
4494
|
+
usedBy: import_zod14.z.string().optional(),
|
|
4495
|
+
usedAt: import_zod14.z.instanceof(import_firestore17.Timestamp).or(import_zod14.z.date()).optional()
|
|
4496
|
+
});
|
|
4497
|
+
var createPractitionerTokenSchema = import_zod14.z.object({
|
|
4498
|
+
practitionerId: import_zod14.z.string().min(1),
|
|
4499
|
+
email: import_zod14.z.string().email(),
|
|
4500
|
+
clinicId: import_zod14.z.string().min(1),
|
|
4501
|
+
expiresAt: import_zod14.z.date().optional()
|
|
4502
|
+
});
|
|
4503
|
+
var practitionerSignupSchema = import_zod14.z.object({
|
|
4504
|
+
email: import_zod14.z.string().email(),
|
|
4505
|
+
password: import_zod14.z.string().min(8),
|
|
4506
|
+
firstName: import_zod14.z.string().min(2).max(50).optional(),
|
|
4507
|
+
lastName: import_zod14.z.string().min(2).max(50).optional(),
|
|
4508
|
+
token: import_zod14.z.string().optional(),
|
|
4509
|
+
profileData: import_zod14.z.object({
|
|
4510
|
+
basicInfo: import_zod14.z.object({
|
|
4511
|
+
phoneNumber: import_zod14.z.string().optional(),
|
|
4512
|
+
profileImageUrl: mediaResourceSchema.optional(),
|
|
4513
|
+
gender: import_zod14.z.enum(["male", "female", "other"]).optional(),
|
|
4514
|
+
bio: import_zod14.z.string().optional()
|
|
4515
|
+
}).optional(),
|
|
4516
|
+
certification: import_zod14.z.any().optional()
|
|
4517
|
+
}).optional()
|
|
4518
|
+
});
|
|
4519
|
+
|
|
4520
|
+
// src/services/practitioner/practitioner.service.ts
|
|
4521
|
+
var import_zod15 = require("zod");
|
|
4522
|
+
var import_geofire_common2 = require("geofire-common");
|
|
4523
|
+
var PractitionerService = class extends BaseService {
|
|
4524
|
+
constructor(db, auth, app, clinicService) {
|
|
4525
|
+
super(db, auth, app);
|
|
4526
|
+
this.clinicService = clinicService;
|
|
4527
|
+
this.mediaService = new MediaService(db, auth, app);
|
|
4528
|
+
}
|
|
4529
|
+
getClinicService() {
|
|
4530
|
+
if (!this.clinicService) {
|
|
4531
|
+
throw new Error("Clinic service not initialized!");
|
|
4532
|
+
}
|
|
4533
|
+
return this.clinicService;
|
|
4534
|
+
}
|
|
4535
|
+
setClinicService(clinicService) {
|
|
4536
|
+
this.clinicService = clinicService;
|
|
4537
|
+
}
|
|
4538
|
+
/**
|
|
4539
|
+
* Handles profile photo upload for practitioners
|
|
4540
|
+
* @param profilePhoto - MediaResource (File, Blob, or URL string)
|
|
4541
|
+
* @param practitionerId - ID of the practitioner
|
|
4542
|
+
* @returns URL string of the uploaded or existing photo
|
|
4543
|
+
*/
|
|
4544
|
+
async handleProfilePhotoUpload(profilePhoto, practitionerId) {
|
|
4545
|
+
if (!profilePhoto) {
|
|
4546
|
+
return void 0;
|
|
4547
|
+
}
|
|
4548
|
+
if (typeof profilePhoto === "string") {
|
|
4549
|
+
return profilePhoto;
|
|
4550
|
+
}
|
|
4551
|
+
if (profilePhoto instanceof File || profilePhoto instanceof Blob) {
|
|
4552
|
+
console.log(
|
|
4553
|
+
`[PractitionerService] Uploading profile photo for practitioner ${practitionerId}`
|
|
4554
|
+
);
|
|
4555
|
+
const mediaMetadata = await this.mediaService.uploadMedia(
|
|
4556
|
+
profilePhoto,
|
|
4557
|
+
practitionerId,
|
|
4558
|
+
// Using practitionerId as ownerId
|
|
4559
|
+
"public" /* PUBLIC */,
|
|
4560
|
+
// Profile photos should be public
|
|
4561
|
+
"practitioner_profile_photos",
|
|
4562
|
+
profilePhoto instanceof File ? profilePhoto.name : `profile_photo_${practitionerId}`
|
|
4563
|
+
);
|
|
4564
|
+
return mediaMetadata.url;
|
|
4565
|
+
}
|
|
4566
|
+
return void 0;
|
|
4567
|
+
}
|
|
4568
|
+
/**
|
|
4569
|
+
* Processes BasicPractitionerInfo to handle profile photo uploads
|
|
4570
|
+
* @param basicInfo - The basic info containing potential MediaResource profile photo
|
|
4571
|
+
* @param practitionerId - ID of the practitioner
|
|
4572
|
+
* @returns Processed basic info with URL string for profileImageUrl
|
|
4573
|
+
*/
|
|
4574
|
+
async processBasicInfo(basicInfo, practitionerId) {
|
|
4575
|
+
const processedBasicInfo = { ...basicInfo };
|
|
4576
|
+
if (basicInfo.profileImageUrl) {
|
|
4577
|
+
const uploadedUrl = await this.handleProfilePhotoUpload(
|
|
4578
|
+
basicInfo.profileImageUrl,
|
|
4579
|
+
practitionerId
|
|
4580
|
+
);
|
|
4581
|
+
processedBasicInfo.profileImageUrl = uploadedUrl;
|
|
4582
|
+
}
|
|
4583
|
+
return processedBasicInfo;
|
|
4584
|
+
}
|
|
4585
|
+
/**
|
|
4586
|
+
* Creates a new practitioner
|
|
4587
|
+
*/
|
|
4588
|
+
async createPractitioner(data) {
|
|
4589
|
+
try {
|
|
4590
|
+
const validData = createPractitionerSchema.parse(data);
|
|
4591
|
+
const practitionerId = this.generateId();
|
|
4592
|
+
const reviewInfo = {
|
|
4593
|
+
totalReviews: 0,
|
|
4594
|
+
averageRating: 0,
|
|
4595
|
+
knowledgeAndExpertise: 0,
|
|
4596
|
+
communicationSkills: 0,
|
|
4597
|
+
bedSideManner: 0,
|
|
4598
|
+
thoroughness: 0,
|
|
4599
|
+
trustworthiness: 0,
|
|
4600
|
+
recommendationPercentage: 0
|
|
4601
|
+
};
|
|
4602
|
+
const practitioner = {
|
|
4603
|
+
id: practitionerId,
|
|
4604
|
+
userRef: validData.userRef,
|
|
4605
|
+
basicInfo: await this.processBasicInfo(
|
|
4606
|
+
validData.basicInfo,
|
|
4607
|
+
practitionerId
|
|
4608
|
+
),
|
|
4609
|
+
certification: validData.certification,
|
|
4610
|
+
clinics: validData.clinics || [],
|
|
4611
|
+
clinicWorkingHours: validData.clinicWorkingHours || [],
|
|
4612
|
+
clinicsInfo: [],
|
|
4613
|
+
procedures: [],
|
|
4614
|
+
proceduresInfo: [],
|
|
4615
|
+
reviewInfo,
|
|
4616
|
+
isActive: validData.isActive !== void 0 ? validData.isActive : true,
|
|
4617
|
+
isVerified: validData.isVerified !== void 0 ? validData.isVerified : false,
|
|
4618
|
+
status: validData.status || "active" /* ACTIVE */,
|
|
4619
|
+
createdAt: (0, import_firestore18.serverTimestamp)(),
|
|
4620
|
+
updatedAt: (0, import_firestore18.serverTimestamp)()
|
|
4621
|
+
};
|
|
4622
|
+
practitionerSchema.parse({
|
|
4623
|
+
...practitioner,
|
|
4624
|
+
createdAt: import_firestore18.Timestamp.now(),
|
|
4625
|
+
updatedAt: import_firestore18.Timestamp.now()
|
|
4626
|
+
});
|
|
4627
|
+
const practitionerRef = (0, import_firestore18.doc)(
|
|
4628
|
+
this.db,
|
|
4629
|
+
PRACTITIONERS_COLLECTION,
|
|
4630
|
+
practitionerId
|
|
4631
|
+
);
|
|
4632
|
+
await (0, import_firestore18.setDoc)(practitionerRef, practitioner);
|
|
4633
|
+
const createdPractitioner = await this.getPractitioner(practitionerId);
|
|
4634
|
+
if (!createdPractitioner) {
|
|
4635
|
+
throw new Error(
|
|
4636
|
+
`Failed to retrieve created practitioner ${practitionerId}`
|
|
4637
|
+
);
|
|
4638
|
+
}
|
|
4639
|
+
return createdPractitioner;
|
|
4640
|
+
} catch (error) {
|
|
4641
|
+
if (error instanceof import_zod15.z.ZodError) {
|
|
4642
|
+
throw new Error(`Invalid practitioner data: ${error.message}`);
|
|
4643
|
+
}
|
|
4644
|
+
console.error("Error creating practitioner:", error);
|
|
4645
|
+
throw error;
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
/**
|
|
4649
|
+
* Kreira novi draft profil zdravstvenog radnika bez povezanog korisnika
|
|
4650
|
+
* Koristi se od strane administratora klinike za kreiranje profila i kasnije pozivanje
|
|
4651
|
+
* @param data Podaci za kreiranje draft profila
|
|
4652
|
+
* @param createdBy ID administratora koji kreira profil
|
|
4653
|
+
* @param clinicId ID klinike za koju se kreira profil
|
|
4654
|
+
* @returns Objekt koji sadrži kreirani draft profil i token za registraciju
|
|
4655
|
+
*/
|
|
4656
|
+
async createDraftPractitioner(data, createdBy, clinicId) {
|
|
4657
|
+
try {
|
|
4658
|
+
const validatedData = createDraftPractitionerSchema.parse(data);
|
|
4659
|
+
const clinic = await this.getClinicService().getClinic(clinicId);
|
|
4660
|
+
if (!clinic) {
|
|
4661
|
+
throw new Error(`Clinic ${clinicId} not found`);
|
|
4662
|
+
}
|
|
4663
|
+
const clinicsToAdd = /* @__PURE__ */ new Set([clinicId]);
|
|
4664
|
+
if (data.clinics && data.clinics.length > 0) {
|
|
4665
|
+
for (const cId of data.clinics) {
|
|
4666
|
+
if (cId !== clinicId) {
|
|
4667
|
+
const otherClinic = await this.getClinicService().getClinic(cId);
|
|
4668
|
+
if (!otherClinic) {
|
|
4669
|
+
throw new Error(`Clinic ${cId} not found`);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
clinicsToAdd.add(cId);
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
const clinics = Array.from(clinicsToAdd);
|
|
4676
|
+
const defaultReviewInfo = {
|
|
4677
|
+
totalReviews: 0,
|
|
4678
|
+
averageRating: 0,
|
|
4679
|
+
knowledgeAndExpertise: 0,
|
|
4680
|
+
communicationSkills: 0,
|
|
4681
|
+
bedSideManner: 0,
|
|
4682
|
+
thoroughness: 0,
|
|
4683
|
+
trustworthiness: 0,
|
|
4684
|
+
recommendationPercentage: 0
|
|
4685
|
+
};
|
|
4686
|
+
const practitionerId = this.generateId();
|
|
4687
|
+
const clinicsInfo = [];
|
|
4688
|
+
for (const cId of clinics) {
|
|
4689
|
+
const clinicData = await this.getClinicService().getClinic(cId);
|
|
4690
|
+
if (clinicData) {
|
|
4691
|
+
clinicsInfo.push({
|
|
4692
|
+
id: clinicData.id,
|
|
4693
|
+
name: clinicData.name,
|
|
4694
|
+
location: clinicData.location,
|
|
4695
|
+
contactInfo: clinicData.contactInfo,
|
|
4696
|
+
// Make sure we're using the right property for featuredPhoto
|
|
4697
|
+
featuredPhoto: clinicData.featuredPhotos && clinicData.featuredPhotos.length > 0 ? typeof clinicData.featuredPhotos[0] === "string" ? clinicData.featuredPhotos[0] : "" : (typeof clinicData.coverPhoto === "string" ? clinicData.coverPhoto : "") || "",
|
|
4698
|
+
description: clinicData.description || null
|
|
4699
|
+
});
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
const finalClinicsInfo = validatedData.clinicsInfo && validatedData.clinicsInfo.length > 0 ? validatedData.clinicsInfo : clinicsInfo;
|
|
4703
|
+
const proceduresInfo = [];
|
|
4704
|
+
const practitionerData = {
|
|
4705
|
+
id: practitionerId,
|
|
4706
|
+
userRef: "",
|
|
4707
|
+
// Prazno - biće popunjeno kada korisnik kreira nalog
|
|
4708
|
+
basicInfo: await this.processBasicInfo(
|
|
4709
|
+
validatedData.basicInfo,
|
|
4710
|
+
practitionerId
|
|
4711
|
+
),
|
|
4712
|
+
certification: validatedData.certification,
|
|
4713
|
+
clinics,
|
|
4714
|
+
clinicWorkingHours: validatedData.clinicWorkingHours || [],
|
|
4715
|
+
clinicsInfo: finalClinicsInfo,
|
|
4716
|
+
procedures: [],
|
|
4717
|
+
proceduresInfo,
|
|
4718
|
+
reviewInfo: defaultReviewInfo,
|
|
4719
|
+
isActive: validatedData.isActive !== void 0 ? validatedData.isActive : false,
|
|
4720
|
+
isVerified: validatedData.isVerified !== void 0 ? validatedData.isVerified : false,
|
|
4721
|
+
status: "draft" /* DRAFT */,
|
|
4722
|
+
createdAt: (0, import_firestore18.serverTimestamp)(),
|
|
4723
|
+
updatedAt: (0, import_firestore18.serverTimestamp)()
|
|
4724
|
+
};
|
|
4725
|
+
practitionerSchema.parse({
|
|
4726
|
+
...practitionerData,
|
|
4727
|
+
userRef: "temp-for-validation",
|
|
4728
|
+
createdAt: import_firestore18.Timestamp.now(),
|
|
4729
|
+
updatedAt: import_firestore18.Timestamp.now()
|
|
4730
|
+
});
|
|
4731
|
+
await (0, import_firestore18.setDoc)(
|
|
4732
|
+
(0, import_firestore18.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
|
|
4733
|
+
practitionerData
|
|
4734
|
+
);
|
|
4735
|
+
const savedPractitioner = await this.getPractitioner(practitionerData.id);
|
|
4736
|
+
if (!savedPractitioner) {
|
|
4737
|
+
throw new Error("Failed to create draft practitioner profile");
|
|
4738
|
+
}
|
|
4739
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
4740
|
+
const expiration = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
4741
|
+
const token = {
|
|
4742
|
+
id: this.generateId(),
|
|
4743
|
+
token: tokenString,
|
|
4744
|
+
practitionerId,
|
|
4745
|
+
email: practitionerData.basicInfo.email,
|
|
4746
|
+
clinicId,
|
|
4747
|
+
status: "active" /* ACTIVE */,
|
|
4748
|
+
createdBy,
|
|
4749
|
+
createdAt: import_firestore18.Timestamp.now(),
|
|
4750
|
+
expiresAt: import_firestore18.Timestamp.fromDate(expiration)
|
|
4751
|
+
};
|
|
4752
|
+
practitionerTokenSchema.parse(token);
|
|
4753
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4754
|
+
await (0, import_firestore18.setDoc)((0, import_firestore18.doc)(this.db, tokenPath), token);
|
|
4755
|
+
return { practitioner: savedPractitioner, token };
|
|
4756
|
+
} catch (error) {
|
|
4757
|
+
if (error instanceof import_zod15.z.ZodError) {
|
|
4758
|
+
throw new Error("Invalid practitioner data: " + error.message);
|
|
4759
|
+
}
|
|
4760
|
+
throw error;
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
/**
|
|
4764
|
+
* Creates a token for inviting practitioner to claim their profile
|
|
4765
|
+
* @param data Data for creating token
|
|
4766
|
+
* @param createdBy ID of the user creating the token
|
|
4767
|
+
* @returns Created token
|
|
4768
|
+
*/
|
|
4769
|
+
async createPractitionerToken(data, createdBy) {
|
|
4770
|
+
try {
|
|
4771
|
+
const validatedData = createPractitionerTokenSchema.parse(data);
|
|
4772
|
+
const practitioner = await this.getPractitioner(
|
|
4773
|
+
validatedData.practitionerId
|
|
4774
|
+
);
|
|
4775
|
+
if (!practitioner) {
|
|
4776
|
+
throw new Error("Practitioner not found");
|
|
4777
|
+
}
|
|
4778
|
+
if (practitioner.status !== "draft" /* DRAFT */) {
|
|
4779
|
+
throw new Error(
|
|
4780
|
+
"Can only create tokens for practitioners in DRAFT status"
|
|
4781
|
+
);
|
|
4782
|
+
}
|
|
4783
|
+
const clinic = await this.getClinicService().getClinic(
|
|
4784
|
+
validatedData.clinicId
|
|
4785
|
+
);
|
|
4786
|
+
if (!clinic) {
|
|
4787
|
+
throw new Error(`Clinic ${validatedData.clinicId} not found`);
|
|
4788
|
+
}
|
|
4789
|
+
if (!practitioner.clinics.includes(validatedData.clinicId)) {
|
|
4790
|
+
throw new Error("Practitioner is not associated with this clinic");
|
|
4791
|
+
}
|
|
4792
|
+
const expiration = validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
|
|
4793
|
+
const tokenString = this.generateId().slice(0, 6).toUpperCase();
|
|
4794
|
+
const token = {
|
|
4795
|
+
id: this.generateId(),
|
|
4796
|
+
token: tokenString,
|
|
4797
|
+
practitionerId: validatedData.practitionerId,
|
|
4798
|
+
email: validatedData.email,
|
|
4799
|
+
clinicId: validatedData.clinicId,
|
|
4800
|
+
status: "active" /* ACTIVE */,
|
|
4801
|
+
createdBy,
|
|
4802
|
+
createdAt: import_firestore18.Timestamp.now(),
|
|
4803
|
+
expiresAt: import_firestore18.Timestamp.fromDate(expiration)
|
|
4804
|
+
};
|
|
4805
|
+
practitionerTokenSchema.parse(token);
|
|
4806
|
+
const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
|
|
4807
|
+
await (0, import_firestore18.setDoc)((0, import_firestore18.doc)(this.db, tokenPath), token);
|
|
4808
|
+
return token;
|
|
4809
|
+
} catch (error) {
|
|
4810
|
+
if (error instanceof import_zod15.z.ZodError) {
|
|
4811
|
+
throw new Error("Invalid token data: " + error.message);
|
|
4812
|
+
}
|
|
4813
|
+
throw error;
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
/**
|
|
4817
|
+
* Gets active tokens for a practitioner
|
|
4818
|
+
* @param practitionerId ID of the practitioner
|
|
4819
|
+
* @returns Array of active tokens
|
|
4820
|
+
*/
|
|
4821
|
+
async getPractitionerActiveTokens(practitionerId) {
|
|
4822
|
+
const tokensRef = (0, import_firestore18.collection)(
|
|
4823
|
+
this.db,
|
|
4824
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
4825
|
+
);
|
|
4826
|
+
const q = (0, import_firestore18.query)(
|
|
4827
|
+
tokensRef,
|
|
4828
|
+
(0, import_firestore18.where)("status", "==", "active" /* ACTIVE */),
|
|
4829
|
+
(0, import_firestore18.where)("expiresAt", ">", import_firestore18.Timestamp.now())
|
|
4830
|
+
);
|
|
4831
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4832
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4833
|
+
}
|
|
4834
|
+
/**
|
|
4835
|
+
* Gets a token by its string value and validates it
|
|
4836
|
+
* @param tokenString The token string to find
|
|
4837
|
+
* @returns The token if found and valid, null otherwise
|
|
4838
|
+
*/
|
|
4839
|
+
async validateToken(tokenString) {
|
|
4840
|
+
const practitionersRef = (0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION);
|
|
4841
|
+
const practitionersSnapshot = await (0, import_firestore18.getDocs)(practitionersRef);
|
|
4842
|
+
for (const practitionerDoc of practitionersSnapshot.docs) {
|
|
4843
|
+
const practitionerId = practitionerDoc.id;
|
|
4844
|
+
const tokensRef = (0, import_firestore18.collection)(
|
|
4845
|
+
this.db,
|
|
4846
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
|
|
4847
|
+
);
|
|
4848
|
+
console.log(
|
|
4849
|
+
`[PRACTITIONER] Validating token for practitioner ${practitionerId}`,
|
|
4850
|
+
{
|
|
4851
|
+
tokenString,
|
|
4852
|
+
timestamp: import_firestore18.Timestamp.now().toDate()
|
|
4853
|
+
}
|
|
4854
|
+
);
|
|
4855
|
+
const q = (0, import_firestore18.query)(
|
|
4856
|
+
tokensRef,
|
|
4857
|
+
(0, import_firestore18.where)("token", "==", tokenString),
|
|
4858
|
+
(0, import_firestore18.where)("status", "==", "active" /* ACTIVE */),
|
|
4859
|
+
(0, import_firestore18.where)("expiresAt", ">", import_firestore18.Timestamp.now())
|
|
4860
|
+
);
|
|
4861
|
+
try {
|
|
4862
|
+
const tokenSnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4863
|
+
console.log(
|
|
4864
|
+
`[PRACTITIONER] Token query results for practitioner ${practitionerId}`,
|
|
4865
|
+
{
|
|
4866
|
+
found: !tokenSnapshot.empty,
|
|
4867
|
+
count: tokenSnapshot.size
|
|
4868
|
+
}
|
|
4869
|
+
);
|
|
4870
|
+
if (!tokenSnapshot.empty) {
|
|
4871
|
+
const tokenData = tokenSnapshot.docs[0].data();
|
|
4872
|
+
console.log(`[PRACTITIONER] Valid token found`, {
|
|
4873
|
+
tokenId: tokenData.id,
|
|
4874
|
+
expiresAt: tokenData.expiresAt.toDate()
|
|
4875
|
+
});
|
|
4876
|
+
return tokenData;
|
|
4877
|
+
}
|
|
4878
|
+
} catch (error) {
|
|
4879
|
+
console.error(
|
|
4880
|
+
`[PRACTITIONER] Error validating token for practitioner ${practitionerId}:`,
|
|
4881
|
+
error
|
|
4882
|
+
);
|
|
4883
|
+
throw error;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
return null;
|
|
4887
|
+
}
|
|
4888
|
+
/**
|
|
4889
|
+
* Marks a token as used
|
|
4890
|
+
* @param tokenId ID of the token
|
|
4891
|
+
* @param practitionerId ID of the practitioner
|
|
4892
|
+
* @param userId ID of the user using the token
|
|
4893
|
+
*/
|
|
4894
|
+
async markTokenAsUsed(tokenId, practitionerId, userId) {
|
|
4895
|
+
const tokenRef = (0, import_firestore18.doc)(
|
|
4896
|
+
this.db,
|
|
4897
|
+
`${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
|
|
4898
|
+
);
|
|
4899
|
+
await (0, import_firestore18.updateDoc)(tokenRef, {
|
|
4900
|
+
status: "used" /* USED */,
|
|
4901
|
+
usedBy: userId,
|
|
4902
|
+
usedAt: import_firestore18.Timestamp.now()
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* Dohvata zdravstvenog radnika po ID-u
|
|
4907
|
+
*/
|
|
4908
|
+
async getPractitioner(practitionerId) {
|
|
4909
|
+
const practitionerDoc = await (0, import_firestore18.getDoc)(
|
|
4910
|
+
(0, import_firestore18.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerId)
|
|
4911
|
+
);
|
|
4912
|
+
if (!practitionerDoc.exists()) {
|
|
4913
|
+
return null;
|
|
4914
|
+
}
|
|
4915
|
+
return practitionerDoc.data();
|
|
4916
|
+
}
|
|
4917
|
+
/**
|
|
4918
|
+
* Dohvata zdravstvenog radnika po User ID-u
|
|
4919
|
+
*/
|
|
4920
|
+
async getPractitionerByUserRef(userRef) {
|
|
4921
|
+
const q = (0, import_firestore18.query)(
|
|
4922
|
+
(0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4923
|
+
(0, import_firestore18.where)("userRef", "==", userRef)
|
|
4924
|
+
);
|
|
4925
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4926
|
+
if (querySnapshot.empty) {
|
|
4927
|
+
return null;
|
|
4928
|
+
}
|
|
4929
|
+
return querySnapshot.docs[0].data();
|
|
4930
|
+
}
|
|
4931
|
+
/**
|
|
4932
|
+
* Dohvata sve zdravstvene radnike za određenu kliniku sa statusom ACTIVE
|
|
4933
|
+
*/
|
|
4934
|
+
async getPractitionersByClinic(clinicId) {
|
|
4935
|
+
const q = (0, import_firestore18.query)(
|
|
4936
|
+
(0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4937
|
+
(0, import_firestore18.where)("clinics", "array-contains", clinicId),
|
|
4938
|
+
(0, import_firestore18.where)("isActive", "==", true),
|
|
4939
|
+
(0, import_firestore18.where)("status", "==", "active" /* ACTIVE */)
|
|
4940
|
+
);
|
|
4941
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4942
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4943
|
+
}
|
|
4944
|
+
/**
|
|
4945
|
+
* Dohvata sve zdravstvene radnike za određenu kliniku
|
|
4946
|
+
*/
|
|
4947
|
+
async getAllPractitionersByClinic(clinicId) {
|
|
4948
|
+
const q = (0, import_firestore18.query)(
|
|
4949
|
+
(0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4950
|
+
(0, import_firestore18.where)("clinics", "array-contains", clinicId),
|
|
4951
|
+
(0, import_firestore18.where)("isActive", "==", true)
|
|
4952
|
+
);
|
|
4953
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4954
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4955
|
+
}
|
|
4956
|
+
/**
|
|
4957
|
+
* Dohvata sve draft zdravstvene radnike za određenu kliniku sa statusom DRAFT
|
|
4958
|
+
*/
|
|
4959
|
+
async getDraftPractitionersByClinic(clinicId) {
|
|
4960
|
+
const q = (0, import_firestore18.query)(
|
|
4961
|
+
(0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4962
|
+
(0, import_firestore18.where)("clinics", "array-contains", clinicId),
|
|
4963
|
+
(0, import_firestore18.where)("status", "==", "draft" /* DRAFT */)
|
|
4964
|
+
);
|
|
4965
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4966
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
4967
|
+
}
|
|
4968
|
+
/**
|
|
4969
|
+
* Updates a practitioner
|
|
4970
|
+
*/
|
|
4971
|
+
async updatePractitioner(practitionerId, data) {
|
|
4972
|
+
try {
|
|
4973
|
+
const validData = data;
|
|
4974
|
+
const practitionerRef = (0, import_firestore18.doc)(
|
|
4975
|
+
this.db,
|
|
4976
|
+
PRACTITIONERS_COLLECTION,
|
|
4977
|
+
practitionerId
|
|
4978
|
+
);
|
|
4979
|
+
const practitionerDoc = await (0, import_firestore18.getDoc)(practitionerRef);
|
|
4980
|
+
if (!practitionerDoc.exists()) {
|
|
4981
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
4982
|
+
}
|
|
4983
|
+
const currentPractitioner = practitionerDoc.data();
|
|
4984
|
+
let processedData = { ...validData };
|
|
4985
|
+
if (validData.basicInfo) {
|
|
4986
|
+
processedData.basicInfo = await this.processBasicInfo(
|
|
4987
|
+
validData.basicInfo,
|
|
4988
|
+
practitionerId
|
|
4989
|
+
);
|
|
4990
|
+
}
|
|
4991
|
+
const updateData = {
|
|
4992
|
+
...processedData,
|
|
4993
|
+
updatedAt: (0, import_firestore18.serverTimestamp)()
|
|
4994
|
+
};
|
|
4995
|
+
await (0, import_firestore18.updateDoc)(practitionerRef, updateData);
|
|
4996
|
+
const updatedPractitioner = await this.getPractitioner(practitionerId);
|
|
4997
|
+
if (!updatedPractitioner) {
|
|
4998
|
+
throw new Error(
|
|
4999
|
+
`Failed to retrieve updated practitioner ${practitionerId}`
|
|
5000
|
+
);
|
|
5001
|
+
}
|
|
5002
|
+
return updatedPractitioner;
|
|
5003
|
+
} catch (error) {
|
|
5004
|
+
if (error instanceof import_zod15.z.ZodError) {
|
|
5005
|
+
throw new Error(`Invalid practitioner update data: ${error.message}`);
|
|
5006
|
+
}
|
|
5007
|
+
console.error(`Error updating practitioner ${practitionerId}:`, error);
|
|
5008
|
+
throw error;
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
/**
|
|
5012
|
+
* Adds a clinic to a practitioner
|
|
5013
|
+
*/
|
|
5014
|
+
async addClinic(practitionerId, clinicId) {
|
|
5015
|
+
var _a;
|
|
5016
|
+
try {
|
|
5017
|
+
const practitionerRef = (0, import_firestore18.doc)(
|
|
5018
|
+
this.db,
|
|
5019
|
+
PRACTITIONERS_COLLECTION,
|
|
5020
|
+
practitionerId
|
|
5021
|
+
);
|
|
5022
|
+
const practitionerDoc = await (0, import_firestore18.getDoc)(practitionerRef);
|
|
5023
|
+
if (!practitionerDoc.exists()) {
|
|
5024
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
5025
|
+
}
|
|
5026
|
+
const practitioner = practitionerDoc.data();
|
|
5027
|
+
if ((_a = practitioner.clinics) == null ? void 0 : _a.includes(clinicId)) {
|
|
5028
|
+
console.log(
|
|
5029
|
+
`Clinic ${clinicId} already added to practitioner ${practitionerId}`
|
|
5030
|
+
);
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
5033
|
+
await (0, import_firestore18.updateDoc)(practitionerRef, {
|
|
5034
|
+
clinics: (0, import_firestore18.arrayUnion)(clinicId),
|
|
5035
|
+
updatedAt: (0, import_firestore18.serverTimestamp)()
|
|
5036
|
+
});
|
|
5037
|
+
} catch (error) {
|
|
5038
|
+
console.error(
|
|
5039
|
+
`Error adding clinic ${clinicId} to practitioner ${practitionerId}:`,
|
|
5040
|
+
error
|
|
5041
|
+
);
|
|
5042
|
+
throw error;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
/**
|
|
5046
|
+
* Removes a clinic from a practitioner
|
|
5047
|
+
*/
|
|
5048
|
+
async removeClinic(practitionerId, clinicId) {
|
|
5049
|
+
try {
|
|
5050
|
+
const practitionerRef = (0, import_firestore18.doc)(
|
|
5051
|
+
this.db,
|
|
5052
|
+
PRACTITIONERS_COLLECTION,
|
|
5053
|
+
practitionerId
|
|
5054
|
+
);
|
|
5055
|
+
const practitionerDoc = await (0, import_firestore18.getDoc)(practitionerRef);
|
|
5056
|
+
if (!practitionerDoc.exists()) {
|
|
5057
|
+
throw new Error(`Practitioner ${practitionerId} not found`);
|
|
5058
|
+
}
|
|
5059
|
+
await (0, import_firestore18.updateDoc)(practitionerRef, {
|
|
5060
|
+
clinics: (0, import_firestore18.arrayRemove)(clinicId),
|
|
5061
|
+
updatedAt: (0, import_firestore18.serverTimestamp)()
|
|
5062
|
+
});
|
|
5063
|
+
} catch (error) {
|
|
5064
|
+
console.error(
|
|
5065
|
+
`Error removing clinic ${clinicId} from practitioner ${practitionerId}:`,
|
|
5066
|
+
error
|
|
5067
|
+
);
|
|
5068
|
+
throw error;
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* Deaktivira profil zdravstvenog radnika
|
|
5073
|
+
*/
|
|
5074
|
+
async deactivatePractitioner(practitionerId) {
|
|
5075
|
+
await this.updatePractitioner(practitionerId, {
|
|
5076
|
+
isActive: false
|
|
5077
|
+
});
|
|
5078
|
+
}
|
|
5079
|
+
/**
|
|
5080
|
+
* Aktivira profil zdravstvenog radnika
|
|
5081
|
+
*/
|
|
5082
|
+
async activatePractitioner(practitionerId) {
|
|
5083
|
+
await this.updatePractitioner(practitionerId, {
|
|
5084
|
+
isActive: true
|
|
5085
|
+
});
|
|
5086
|
+
}
|
|
5087
|
+
/**
|
|
5088
|
+
* Briše profil zdravstvenog radnika
|
|
5089
|
+
*/
|
|
5090
|
+
async deletePractitioner(practitionerId) {
|
|
5091
|
+
const practitioner = await this.getPractitioner(practitionerId);
|
|
5092
|
+
if (!practitioner) {
|
|
5093
|
+
throw new Error("Practitioner not found");
|
|
5094
|
+
}
|
|
5095
|
+
await (0, import_firestore18.deleteDoc)((0, import_firestore18.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerId));
|
|
5096
|
+
}
|
|
5097
|
+
/**
|
|
5098
|
+
* Validates a registration token and claims the associated draft practitioner profile
|
|
5099
|
+
* @param tokenString The token provided by the practitioner
|
|
5100
|
+
* @param userId The ID of the user claiming the profile
|
|
5101
|
+
* @returns The claimed practitioner profile or null if token is invalid
|
|
5102
|
+
*/
|
|
5103
|
+
async validateTokenAndClaimProfile(tokenString, userId) {
|
|
5104
|
+
console.log("[PRACTITIONER] Validating token for claiming profile", {
|
|
5105
|
+
tokenString,
|
|
5106
|
+
userId
|
|
5107
|
+
});
|
|
5108
|
+
const token = await this.validateToken(tokenString);
|
|
5109
|
+
if (!token) {
|
|
5110
|
+
console.log(
|
|
5111
|
+
"[PRACTITIONER] Token validation failed - token not found or not valid",
|
|
5112
|
+
{
|
|
5113
|
+
tokenString
|
|
5114
|
+
}
|
|
5115
|
+
);
|
|
5116
|
+
return null;
|
|
5117
|
+
}
|
|
5118
|
+
console.log("[PRACTITIONER] Token successfully validated", {
|
|
5119
|
+
tokenId: token.id,
|
|
5120
|
+
practitionerId: token.practitionerId
|
|
5121
|
+
});
|
|
5122
|
+
const practitioner = await this.getPractitioner(token.practitionerId);
|
|
5123
|
+
if (!practitioner) {
|
|
5124
|
+
console.log("[PRACTITIONER] Practitioner not found", {
|
|
5125
|
+
practitionerId: token.practitionerId
|
|
5126
|
+
});
|
|
5127
|
+
return null;
|
|
5128
|
+
}
|
|
5129
|
+
if (practitioner.status !== "draft" /* DRAFT */) {
|
|
5130
|
+
console.log("[PRACTITIONER] Practitioner status is not DRAFT", {
|
|
5131
|
+
practitionerId: practitioner.id,
|
|
5132
|
+
status: practitioner.status
|
|
5133
|
+
});
|
|
5134
|
+
throw new Error("This practitioner profile has already been claimed");
|
|
5135
|
+
}
|
|
5136
|
+
const existingPractitioner = await this.getPractitionerByUserRef(userId);
|
|
5137
|
+
if (existingPractitioner) {
|
|
5138
|
+
throw new Error("User already has a practitioner profile");
|
|
5139
|
+
}
|
|
5140
|
+
const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
|
|
5141
|
+
userRef: userId,
|
|
5142
|
+
status: "active" /* ACTIVE */
|
|
5143
|
+
});
|
|
5144
|
+
await this.markTokenAsUsed(token.id, token.practitionerId, userId);
|
|
5145
|
+
console.log("[PRACTITIONER] Profile claimed successfully", {
|
|
5146
|
+
practitionerId: updatedPractitioner.id,
|
|
5147
|
+
userId
|
|
4789
5148
|
});
|
|
4790
5149
|
return updatedPractitioner;
|
|
4791
5150
|
}
|
|
@@ -4802,21 +5161,21 @@ var PractitionerService = class extends BaseService {
|
|
|
4802
5161
|
try {
|
|
4803
5162
|
const constraints = [];
|
|
4804
5163
|
if (!(options == null ? void 0 : options.includeDraftPractitioners)) {
|
|
4805
|
-
constraints.push((0,
|
|
5164
|
+
constraints.push((0, import_firestore18.where)("status", "==", "active" /* ACTIVE */));
|
|
4806
5165
|
}
|
|
4807
|
-
constraints.push((0,
|
|
4808
|
-
constraints.push((0,
|
|
5166
|
+
constraints.push((0, import_firestore18.orderBy)("basicInfo.lastName", "asc"));
|
|
5167
|
+
constraints.push((0, import_firestore18.orderBy)("basicInfo.firstName", "asc"));
|
|
4809
5168
|
if ((options == null ? void 0 : options.pagination) && options.pagination > 0) {
|
|
4810
5169
|
if (options.lastDoc) {
|
|
4811
|
-
constraints.push((0,
|
|
5170
|
+
constraints.push((0, import_firestore18.startAfter)(options.lastDoc));
|
|
4812
5171
|
}
|
|
4813
|
-
constraints.push((0,
|
|
5172
|
+
constraints.push((0, import_firestore18.limit)(options.pagination));
|
|
4814
5173
|
}
|
|
4815
|
-
const q = (0,
|
|
4816
|
-
(0,
|
|
5174
|
+
const q = (0, import_firestore18.query)(
|
|
5175
|
+
(0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4817
5176
|
...constraints
|
|
4818
5177
|
);
|
|
4819
|
-
const querySnapshot = await (0,
|
|
5178
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4820
5179
|
const practitioners = querySnapshot.docs.map(
|
|
4821
5180
|
(doc34) => doc34.data()
|
|
4822
5181
|
);
|
|
@@ -4861,31 +5220,31 @@ var PractitionerService = class extends BaseService {
|
|
|
4861
5220
|
);
|
|
4862
5221
|
const constraints = [];
|
|
4863
5222
|
if (!filters.includeDraftPractitioners) {
|
|
4864
|
-
constraints.push((0,
|
|
5223
|
+
constraints.push((0, import_firestore18.where)("status", "==", "active" /* ACTIVE */));
|
|
4865
5224
|
}
|
|
4866
|
-
constraints.push((0,
|
|
5225
|
+
constraints.push((0, import_firestore18.where)("isActive", "==", true));
|
|
4867
5226
|
if (filters.certifications && filters.certifications.length > 0) {
|
|
4868
5227
|
constraints.push(
|
|
4869
|
-
(0,
|
|
5228
|
+
(0, import_firestore18.where)(
|
|
4870
5229
|
"certification.certifications",
|
|
4871
5230
|
"array-contains-any",
|
|
4872
5231
|
filters.certifications
|
|
4873
5232
|
)
|
|
4874
5233
|
);
|
|
4875
5234
|
}
|
|
4876
|
-
constraints.push((0,
|
|
4877
|
-
constraints.push((0,
|
|
5235
|
+
constraints.push((0, import_firestore18.orderBy)("basicInfo.lastName", "asc"));
|
|
5236
|
+
constraints.push((0, import_firestore18.orderBy)("basicInfo.firstName", "asc"));
|
|
4878
5237
|
if (filters.pagination && filters.pagination > 0) {
|
|
4879
5238
|
if (filters.lastDoc) {
|
|
4880
|
-
constraints.push((0,
|
|
5239
|
+
constraints.push((0, import_firestore18.startAfter)(filters.lastDoc));
|
|
4881
5240
|
}
|
|
4882
|
-
constraints.push((0,
|
|
5241
|
+
constraints.push((0, import_firestore18.limit)(filters.pagination));
|
|
4883
5242
|
}
|
|
4884
|
-
const q = (0,
|
|
4885
|
-
(0,
|
|
5243
|
+
const q = (0, import_firestore18.query)(
|
|
5244
|
+
(0, import_firestore18.collection)(this.db, PRACTITIONERS_COLLECTION),
|
|
4886
5245
|
...constraints
|
|
4887
5246
|
);
|
|
4888
|
-
const querySnapshot = await (0,
|
|
5247
|
+
const querySnapshot = await (0, import_firestore18.getDocs)(q);
|
|
4889
5248
|
console.log(
|
|
4890
5249
|
`[PRACTITIONER_SERVICE] Found ${querySnapshot.docs.length} practitioners with base query`
|
|
4891
5250
|
);
|
|
@@ -5007,11 +5366,11 @@ var UserService = class extends BaseService {
|
|
|
5007
5366
|
email: firebaseUser.email,
|
|
5008
5367
|
roles: roles.length > 0 ? roles : ["patient" /* PATIENT */],
|
|
5009
5368
|
isAnonymous: firebaseUser.isAnonymous,
|
|
5010
|
-
createdAt: (0,
|
|
5011
|
-
updatedAt: (0,
|
|
5012
|
-
lastLoginAt: (0,
|
|
5369
|
+
createdAt: (0, import_firestore19.serverTimestamp)(),
|
|
5370
|
+
updatedAt: (0, import_firestore19.serverTimestamp)(),
|
|
5371
|
+
lastLoginAt: (0, import_firestore19.serverTimestamp)()
|
|
5013
5372
|
};
|
|
5014
|
-
await (0,
|
|
5373
|
+
await (0, import_firestore19.setDoc)((0, import_firestore19.doc)(this.db, USERS_COLLECTION, userData.uid), userData);
|
|
5015
5374
|
if (options == null ? void 0 : options.skipProfileCreation) {
|
|
5016
5375
|
return this.getUserById(userData.uid);
|
|
5017
5376
|
}
|
|
@@ -5020,7 +5379,7 @@ var UserService = class extends BaseService {
|
|
|
5020
5379
|
roles,
|
|
5021
5380
|
options
|
|
5022
5381
|
);
|
|
5023
|
-
await (0,
|
|
5382
|
+
await (0, import_firestore19.updateDoc)((0, import_firestore19.doc)(this.db, USERS_COLLECTION, userData.uid), profiles);
|
|
5024
5383
|
return this.getUserById(userData.uid);
|
|
5025
5384
|
}
|
|
5026
5385
|
/**
|
|
@@ -5098,7 +5457,7 @@ var UserService = class extends BaseService {
|
|
|
5098
5457
|
email: "",
|
|
5099
5458
|
phoneNumber: "",
|
|
5100
5459
|
title: "",
|
|
5101
|
-
dateOfBirth:
|
|
5460
|
+
dateOfBirth: import_firestore19.Timestamp.now(),
|
|
5102
5461
|
gender: "other",
|
|
5103
5462
|
languages: ["Serbian"]
|
|
5104
5463
|
},
|
|
@@ -5107,7 +5466,7 @@ var UserService = class extends BaseService {
|
|
|
5107
5466
|
specialties: [],
|
|
5108
5467
|
licenseNumber: "",
|
|
5109
5468
|
issuingAuthority: "",
|
|
5110
|
-
issueDate:
|
|
5469
|
+
issueDate: import_firestore19.Timestamp.now(),
|
|
5111
5470
|
verificationStatus: "pending"
|
|
5112
5471
|
},
|
|
5113
5472
|
isActive: true,
|
|
@@ -5123,7 +5482,7 @@ var UserService = class extends BaseService {
|
|
|
5123
5482
|
* Dohvata korisnika po ID-u
|
|
5124
5483
|
*/
|
|
5125
5484
|
async getUserById(uid) {
|
|
5126
|
-
const userDoc = await (0,
|
|
5485
|
+
const userDoc = await (0, import_firestore19.getDoc)((0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid));
|
|
5127
5486
|
if (!userDoc.exists()) {
|
|
5128
5487
|
throw USER_ERRORS.NOT_FOUND;
|
|
5129
5488
|
}
|
|
@@ -5134,19 +5493,19 @@ var UserService = class extends BaseService {
|
|
|
5134
5493
|
* Dohvata korisnika po email-u
|
|
5135
5494
|
*/
|
|
5136
5495
|
async getUserByEmail(email) {
|
|
5137
|
-
const usersRef = (0,
|
|
5138
|
-
const q = (0,
|
|
5139
|
-
const querySnapshot = await (0,
|
|
5496
|
+
const usersRef = (0, import_firestore19.collection)(this.db, USERS_COLLECTION);
|
|
5497
|
+
const q = (0, import_firestore19.query)(usersRef, (0, import_firestore19.where)("email", "==", email));
|
|
5498
|
+
const querySnapshot = await (0, import_firestore19.getDocs)(q);
|
|
5140
5499
|
if (querySnapshot.empty) return null;
|
|
5141
5500
|
const userData = querySnapshot.docs[0].data();
|
|
5142
5501
|
return userSchema.parse(userData);
|
|
5143
5502
|
}
|
|
5144
5503
|
async getUsersByRole(role) {
|
|
5145
5504
|
const constraints = [
|
|
5146
|
-
(0,
|
|
5505
|
+
(0, import_firestore19.where)("roles", "array-contains", role)
|
|
5147
5506
|
];
|
|
5148
|
-
const q = (0,
|
|
5149
|
-
const querySnapshot = await (0,
|
|
5507
|
+
const q = (0, import_firestore19.query)((0, import_firestore19.collection)(this.db, USERS_COLLECTION), ...constraints);
|
|
5508
|
+
const querySnapshot = await (0, import_firestore19.getDocs)(q);
|
|
5150
5509
|
const users = querySnapshot.docs.map((doc34) => doc34.data());
|
|
5151
5510
|
return Promise.all(users.map((userData) => userSchema.parse(userData)));
|
|
5152
5511
|
}
|
|
@@ -5154,33 +5513,33 @@ var UserService = class extends BaseService {
|
|
|
5154
5513
|
* Ažurira timestamp poslednjeg logovanja
|
|
5155
5514
|
*/
|
|
5156
5515
|
async updateUserLoginTimestamp(uid) {
|
|
5157
|
-
const userRef = (0,
|
|
5158
|
-
const userDoc = await (0,
|
|
5516
|
+
const userRef = (0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid);
|
|
5517
|
+
const userDoc = await (0, import_firestore19.getDoc)(userRef);
|
|
5159
5518
|
if (!userDoc.exists()) {
|
|
5160
5519
|
throw AUTH_ERRORS.USER_NOT_FOUND;
|
|
5161
5520
|
}
|
|
5162
|
-
await (0,
|
|
5163
|
-
lastLoginAt: (0,
|
|
5164
|
-
updatedAt: (0,
|
|
5521
|
+
await (0, import_firestore19.updateDoc)(userRef, {
|
|
5522
|
+
lastLoginAt: (0, import_firestore19.serverTimestamp)(),
|
|
5523
|
+
updatedAt: (0, import_firestore19.serverTimestamp)()
|
|
5165
5524
|
});
|
|
5166
5525
|
return this.getUserById(uid);
|
|
5167
5526
|
}
|
|
5168
5527
|
async upgradeAnonymousUser(uid, email) {
|
|
5169
|
-
const userRef = (0,
|
|
5170
|
-
const userDoc = await (0,
|
|
5528
|
+
const userRef = (0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid);
|
|
5529
|
+
const userDoc = await (0, import_firestore19.getDoc)(userRef);
|
|
5171
5530
|
if (!userDoc.exists()) {
|
|
5172
5531
|
throw USER_ERRORS.NOT_FOUND;
|
|
5173
5532
|
}
|
|
5174
|
-
await (0,
|
|
5533
|
+
await (0, import_firestore19.updateDoc)(userRef, {
|
|
5175
5534
|
email,
|
|
5176
5535
|
isAnonymous: false,
|
|
5177
|
-
updatedAt: (0,
|
|
5536
|
+
updatedAt: (0, import_firestore19.serverTimestamp)()
|
|
5178
5537
|
});
|
|
5179
5538
|
return this.getUserById(uid);
|
|
5180
5539
|
}
|
|
5181
5540
|
async updateUser(uid, updates) {
|
|
5182
|
-
const userRef = (0,
|
|
5183
|
-
const userDoc = await (0,
|
|
5541
|
+
const userRef = (0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid);
|
|
5542
|
+
const userDoc = await (0, import_firestore19.getDoc)(userRef);
|
|
5184
5543
|
if (!userDoc.exists()) {
|
|
5185
5544
|
throw USER_ERRORS.NOT_FOUND;
|
|
5186
5545
|
}
|
|
@@ -5189,12 +5548,12 @@ var UserService = class extends BaseService {
|
|
|
5189
5548
|
const updatedUser = {
|
|
5190
5549
|
...currentUser,
|
|
5191
5550
|
...updates,
|
|
5192
|
-
updatedAt: (0,
|
|
5551
|
+
updatedAt: (0, import_firestore19.serverTimestamp)()
|
|
5193
5552
|
};
|
|
5194
5553
|
userSchema.parse(updatedUser);
|
|
5195
|
-
await (0,
|
|
5554
|
+
await (0, import_firestore19.updateDoc)(userRef, {
|
|
5196
5555
|
...updates,
|
|
5197
|
-
updatedAt: (0,
|
|
5556
|
+
updatedAt: (0, import_firestore19.serverTimestamp)()
|
|
5198
5557
|
});
|
|
5199
5558
|
return this.getUserById(uid);
|
|
5200
5559
|
} catch (error) {
|
|
@@ -5211,10 +5570,10 @@ var UserService = class extends BaseService {
|
|
|
5211
5570
|
const user = await this.getUserById(uid);
|
|
5212
5571
|
if (user.roles.includes(role)) return;
|
|
5213
5572
|
const profiles = await this.createProfilesForRoles(uid, [role], options);
|
|
5214
|
-
await (0,
|
|
5573
|
+
await (0, import_firestore19.updateDoc)((0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid), {
|
|
5215
5574
|
roles: [...user.roles, role],
|
|
5216
5575
|
...profiles,
|
|
5217
|
-
updatedAt: (0,
|
|
5576
|
+
updatedAt: (0, import_firestore19.serverTimestamp)()
|
|
5218
5577
|
});
|
|
5219
5578
|
}
|
|
5220
5579
|
/**
|
|
@@ -5246,15 +5605,15 @@ var UserService = class extends BaseService {
|
|
|
5246
5605
|
}
|
|
5247
5606
|
break;
|
|
5248
5607
|
}
|
|
5249
|
-
await (0,
|
|
5608
|
+
await (0, import_firestore19.updateDoc)((0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid), {
|
|
5250
5609
|
roles: user.roles.filter((r) => r !== role),
|
|
5251
|
-
updatedAt: (0,
|
|
5610
|
+
updatedAt: (0, import_firestore19.serverTimestamp)()
|
|
5252
5611
|
});
|
|
5253
5612
|
}
|
|
5254
5613
|
// Delete operations
|
|
5255
5614
|
async deleteUser(uid) {
|
|
5256
|
-
const userRef = (0,
|
|
5257
|
-
const userDoc = await (0,
|
|
5615
|
+
const userRef = (0, import_firestore19.doc)(this.db, USERS_COLLECTION, uid);
|
|
5616
|
+
const userDoc = await (0, import_firestore19.getDoc)(userRef);
|
|
5258
5617
|
if (!userDoc.exists()) {
|
|
5259
5618
|
throw USER_ERRORS.NOT_FOUND;
|
|
5260
5619
|
}
|
|
@@ -5275,7 +5634,7 @@ var UserService = class extends BaseService {
|
|
|
5275
5634
|
userData.adminProfile
|
|
5276
5635
|
);
|
|
5277
5636
|
}
|
|
5278
|
-
await (0,
|
|
5637
|
+
await (0, import_firestore19.deleteDoc)(userRef);
|
|
5279
5638
|
} catch (error) {
|
|
5280
5639
|
throw error;
|
|
5281
5640
|
}
|
|
@@ -5283,12 +5642,12 @@ var UserService = class extends BaseService {
|
|
|
5283
5642
|
};
|
|
5284
5643
|
|
|
5285
5644
|
// src/services/clinic/utils/clinic-group.utils.ts
|
|
5286
|
-
var
|
|
5645
|
+
var import_firestore20 = require("firebase/firestore");
|
|
5287
5646
|
var import_geofire_common3 = require("geofire-common");
|
|
5288
5647
|
var import_zod17 = require("zod");
|
|
5289
5648
|
|
|
5290
5649
|
// src/services/clinic/utils/photos.utils.ts
|
|
5291
|
-
var
|
|
5650
|
+
var import_storage5 = require("firebase/storage");
|
|
5292
5651
|
async function uploadPhoto(photo, entityType, entityId, photoType, app, fileName) {
|
|
5293
5652
|
if (!photo || typeof photo !== "string" || !photo.startsWith("data:")) {
|
|
5294
5653
|
return photo;
|
|
@@ -5297,9 +5656,9 @@ async function uploadPhoto(photo, entityType, entityId, photoType, app, fileName
|
|
|
5297
5656
|
console.log(
|
|
5298
5657
|
`[PHOTO_UTILS] Uploading ${photoType} for ${entityType}/${entityId}`
|
|
5299
5658
|
);
|
|
5300
|
-
const storage = (0,
|
|
5659
|
+
const storage = (0, import_storage5.getStorage)(app);
|
|
5301
5660
|
const storageFileName = fileName || `${photoType}-${Date.now()}`;
|
|
5302
|
-
const storageRef = (0,
|
|
5661
|
+
const storageRef = (0, import_storage5.ref)(
|
|
5303
5662
|
storage,
|
|
5304
5663
|
`${entityType}/${entityId}/${storageFileName}`
|
|
5305
5664
|
);
|
|
@@ -5311,8 +5670,8 @@ async function uploadPhoto(photo, entityType, entityId, photoType, app, fileName
|
|
|
5311
5670
|
byteArrays.push(byteCharacters.charCodeAt(i));
|
|
5312
5671
|
}
|
|
5313
5672
|
const blob = new Blob([new Uint8Array(byteArrays)], { type: contentType });
|
|
5314
|
-
await (0,
|
|
5315
|
-
const downloadUrl = await (0,
|
|
5673
|
+
await (0, import_storage5.uploadBytes)(storageRef, blob, { contentType });
|
|
5674
|
+
const downloadUrl = await (0, import_storage5.getDownloadURL)(storageRef);
|
|
5316
5675
|
console.log(`[PHOTO_UTILS] ${photoType} uploaded successfully`, {
|
|
5317
5676
|
downloadUrl
|
|
5318
5677
|
});
|
|
@@ -5400,9 +5759,9 @@ async function createClinicGroup(db, data, ownerId, isDefault = false, clinicAdm
|
|
|
5400
5759
|
throw geohashError;
|
|
5401
5760
|
}
|
|
5402
5761
|
}
|
|
5403
|
-
const now =
|
|
5762
|
+
const now = import_firestore20.Timestamp.now();
|
|
5404
5763
|
console.log("[CLINIC_GROUP] Preparing clinic group data object");
|
|
5405
|
-
const groupId = (0,
|
|
5764
|
+
const groupId = (0, import_firestore20.doc)((0, import_firestore20.collection)(db, CLINIC_GROUPS_COLLECTION)).id;
|
|
5406
5765
|
console.log("[CLINIC_GROUP] Logo value:", {
|
|
5407
5766
|
logoValue: validatedData.logo,
|
|
5408
5767
|
logoType: validatedData.logo === null ? "null" : typeof validatedData.logo
|
|
@@ -5452,7 +5811,7 @@ async function createClinicGroup(db, data, ownerId, isDefault = false, clinicAdm
|
|
|
5452
5811
|
groupId: groupData.id
|
|
5453
5812
|
});
|
|
5454
5813
|
try {
|
|
5455
|
-
await (0,
|
|
5814
|
+
await (0, import_firestore20.setDoc)((0, import_firestore20.doc)(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
|
|
5456
5815
|
console.log("[CLINIC_GROUP] Clinic group saved successfully");
|
|
5457
5816
|
} catch (firestoreError) {
|
|
5458
5817
|
console.error(
|
|
@@ -5498,19 +5857,19 @@ async function createClinicGroup(db, data, ownerId, isDefault = false, clinicAdm
|
|
|
5498
5857
|
}
|
|
5499
5858
|
}
|
|
5500
5859
|
async function getClinicGroup(db, groupId) {
|
|
5501
|
-
const docRef = (0,
|
|
5502
|
-
const docSnap = await (0,
|
|
5860
|
+
const docRef = (0, import_firestore20.doc)(db, CLINIC_GROUPS_COLLECTION, groupId);
|
|
5861
|
+
const docSnap = await (0, import_firestore20.getDoc)(docRef);
|
|
5503
5862
|
if (docSnap.exists()) {
|
|
5504
5863
|
return docSnap.data();
|
|
5505
5864
|
}
|
|
5506
5865
|
return null;
|
|
5507
5866
|
}
|
|
5508
5867
|
async function getAllActiveGroups(db) {
|
|
5509
|
-
const q = (0,
|
|
5510
|
-
(0,
|
|
5511
|
-
(0,
|
|
5868
|
+
const q = (0, import_firestore20.query)(
|
|
5869
|
+
(0, import_firestore20.collection)(db, CLINIC_GROUPS_COLLECTION),
|
|
5870
|
+
(0, import_firestore20.where)("isActive", "==", true)
|
|
5512
5871
|
);
|
|
5513
|
-
const querySnapshot = await (0,
|
|
5872
|
+
const querySnapshot = await (0, import_firestore20.getDocs)(q);
|
|
5514
5873
|
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
5515
5874
|
}
|
|
5516
5875
|
async function updateClinicGroup(db, groupId, data, app) {
|
|
@@ -5539,10 +5898,10 @@ async function updateClinicGroup(db, groupId, data, app) {
|
|
|
5539
5898
|
}
|
|
5540
5899
|
updatedData = {
|
|
5541
5900
|
...updatedData,
|
|
5542
|
-
updatedAt:
|
|
5901
|
+
updatedAt: import_firestore20.Timestamp.now()
|
|
5543
5902
|
};
|
|
5544
5903
|
console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
|
|
5545
|
-
await (0,
|
|
5904
|
+
await (0, import_firestore20.updateDoc)((0, import_firestore20.doc)(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
|
|
5546
5905
|
console.log("[CLINIC_GROUP] Clinic group updated successfully");
|
|
5547
5906
|
const updatedGroup = await getClinicGroup(db, groupId);
|
|
5548
5907
|
if (!updatedGroup) {
|
|
@@ -5589,1246 +5948,948 @@ async function removeAdminFromGroup(db, groupId, adminId, app) {
|
|
|
5589
5948
|
if (group.ownerId === adminId) {
|
|
5590
5949
|
throw new Error("Cannot remove the owner from the group");
|
|
5591
5950
|
}
|
|
5592
|
-
if (!group.admins.includes(adminId)) {
|
|
5593
|
-
return;
|
|
5594
|
-
}
|
|
5595
|
-
await updateClinicGroup(
|
|
5596
|
-
db,
|
|
5597
|
-
groupId,
|
|
5598
|
-
{
|
|
5599
|
-
admins: group.admins.filter((id) => id !== adminId)
|
|
5600
|
-
},
|
|
5601
|
-
app
|
|
5602
|
-
);
|
|
5603
|
-
}
|
|
5604
|
-
async function deactivateClinicGroup(db, groupId, app) {
|
|
5605
|
-
const group = await getClinicGroup(db, groupId);
|
|
5606
|
-
if (!group) {
|
|
5607
|
-
throw new Error("Clinic group not found");
|
|
5608
|
-
}
|
|
5609
|
-
await updateClinicGroup(
|
|
5610
|
-
db,
|
|
5611
|
-
groupId,
|
|
5612
|
-
{
|
|
5613
|
-
isActive: false
|
|
5614
|
-
},
|
|
5615
|
-
app
|
|
5616
|
-
);
|
|
5617
|
-
}
|
|
5618
|
-
async function createAdminToken(db, groupId, creatorAdminId, app, data) {
|
|
5619
|
-
const group = await getClinicGroup(db, groupId);
|
|
5620
|
-
if (!group) {
|
|
5621
|
-
throw new Error("Clinic group not found");
|
|
5622
|
-
}
|
|
5623
|
-
if (!group.admins.includes(creatorAdminId)) {
|
|
5624
|
-
throw new Error("Admin does not belong to this clinic group");
|
|
5625
|
-
}
|
|
5626
|
-
const now = import_firestore18.Timestamp.now();
|
|
5627
|
-
const expiresInDays = (data == null ? void 0 : data.expiresInDays) || 7;
|
|
5628
|
-
const email = (data == null ? void 0 : data.email) || null;
|
|
5629
|
-
const expiresAt = new import_firestore18.Timestamp(
|
|
5630
|
-
now.seconds + expiresInDays * 24 * 60 * 60,
|
|
5631
|
-
now.nanoseconds
|
|
5632
|
-
);
|
|
5633
|
-
const token = {
|
|
5634
|
-
id: generateId(),
|
|
5635
|
-
token: generateId(),
|
|
5636
|
-
status: "active" /* ACTIVE */,
|
|
5637
|
-
email,
|
|
5638
|
-
createdAt: now,
|
|
5639
|
-
expiresAt
|
|
5640
|
-
};
|
|
5641
|
-
await updateClinicGroup(
|
|
5642
|
-
db,
|
|
5643
|
-
groupId,
|
|
5644
|
-
{
|
|
5645
|
-
adminTokens: [...group.adminTokens, token]
|
|
5646
|
-
},
|
|
5647
|
-
app
|
|
5648
|
-
);
|
|
5649
|
-
return token;
|
|
5650
|
-
}
|
|
5651
|
-
async function verifyAndUseAdminToken(db, groupId, token, userRef, app) {
|
|
5652
|
-
const group = await getClinicGroup(db, groupId);
|
|
5653
|
-
if (!group) {
|
|
5654
|
-
throw new Error("Clinic group not found");
|
|
5655
|
-
}
|
|
5656
|
-
const adminToken = group.adminTokens.find((t) => t.token === token);
|
|
5657
|
-
if (!adminToken) {
|
|
5658
|
-
throw new Error("Admin token not found");
|
|
5659
|
-
}
|
|
5660
|
-
if (adminToken.status !== "active" /* ACTIVE */) {
|
|
5661
|
-
throw new Error("Admin token is not active");
|
|
5662
|
-
}
|
|
5663
|
-
const now = import_firestore18.Timestamp.now();
|
|
5664
|
-
if (adminToken.expiresAt.seconds < now.seconds) {
|
|
5665
|
-
const updatedTokens2 = group.adminTokens.map(
|
|
5666
|
-
(t) => t.id === adminToken.id ? { ...t, status: "expired" /* EXPIRED */ } : t
|
|
5667
|
-
);
|
|
5668
|
-
await updateClinicGroup(
|
|
5669
|
-
db,
|
|
5670
|
-
groupId,
|
|
5671
|
-
{
|
|
5672
|
-
adminTokens: updatedTokens2
|
|
5673
|
-
},
|
|
5674
|
-
app
|
|
5675
|
-
);
|
|
5676
|
-
throw new Error("Admin token has expired");
|
|
5951
|
+
if (!group.admins.includes(adminId)) {
|
|
5952
|
+
return;
|
|
5677
5953
|
}
|
|
5678
|
-
const updatedTokens = group.adminTokens.map(
|
|
5679
|
-
(t) => t.id === adminToken.id ? {
|
|
5680
|
-
...t,
|
|
5681
|
-
status: "used" /* USED */,
|
|
5682
|
-
usedByUserRef: userRef
|
|
5683
|
-
} : t
|
|
5684
|
-
);
|
|
5685
5954
|
await updateClinicGroup(
|
|
5686
5955
|
db,
|
|
5687
5956
|
groupId,
|
|
5688
5957
|
{
|
|
5689
|
-
|
|
5958
|
+
admins: group.admins.filter((id) => id !== adminId)
|
|
5690
5959
|
},
|
|
5691
5960
|
app
|
|
5692
5961
|
);
|
|
5693
|
-
return true;
|
|
5694
5962
|
}
|
|
5695
|
-
async function
|
|
5963
|
+
async function deactivateClinicGroup(db, groupId, app) {
|
|
5696
5964
|
const group = await getClinicGroup(db, groupId);
|
|
5697
5965
|
if (!group) {
|
|
5698
5966
|
throw new Error("Clinic group not found");
|
|
5699
5967
|
}
|
|
5700
|
-
if (!group.admins.includes(adminId)) {
|
|
5701
|
-
throw new Error("Admin does not belong to this clinic group");
|
|
5702
|
-
}
|
|
5703
|
-
const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
|
|
5704
5968
|
await updateClinicGroup(
|
|
5705
5969
|
db,
|
|
5706
5970
|
groupId,
|
|
5707
5971
|
{
|
|
5708
|
-
|
|
5972
|
+
isActive: false
|
|
5709
5973
|
},
|
|
5710
5974
|
app
|
|
5711
5975
|
);
|
|
5712
5976
|
}
|
|
5713
|
-
async function
|
|
5977
|
+
async function createAdminToken(db, groupId, creatorAdminId, app, data) {
|
|
5714
5978
|
const group = await getClinicGroup(db, groupId);
|
|
5715
5979
|
if (!group) {
|
|
5716
5980
|
throw new Error("Clinic group not found");
|
|
5717
5981
|
}
|
|
5718
|
-
if (!group.admins.includes(
|
|
5982
|
+
if (!group.admins.includes(creatorAdminId)) {
|
|
5719
5983
|
throw new Error("Admin does not belong to this clinic group");
|
|
5720
5984
|
}
|
|
5721
|
-
|
|
5985
|
+
const now = import_firestore20.Timestamp.now();
|
|
5986
|
+
const expiresInDays = (data == null ? void 0 : data.expiresInDays) || 7;
|
|
5987
|
+
const email = (data == null ? void 0 : data.email) || null;
|
|
5988
|
+
const expiresAt = new import_firestore20.Timestamp(
|
|
5989
|
+
now.seconds + expiresInDays * 24 * 60 * 60,
|
|
5990
|
+
now.nanoseconds
|
|
5991
|
+
);
|
|
5992
|
+
const token = {
|
|
5993
|
+
id: generateId(),
|
|
5994
|
+
token: generateId(),
|
|
5995
|
+
status: "active" /* ACTIVE */,
|
|
5996
|
+
email,
|
|
5997
|
+
createdAt: now,
|
|
5998
|
+
expiresAt
|
|
5999
|
+
};
|
|
6000
|
+
await updateClinicGroup(
|
|
6001
|
+
db,
|
|
6002
|
+
groupId,
|
|
6003
|
+
{
|
|
6004
|
+
adminTokens: [...group.adminTokens, token]
|
|
6005
|
+
},
|
|
6006
|
+
app
|
|
6007
|
+
);
|
|
6008
|
+
return token;
|
|
5722
6009
|
}
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
super(db, auth, app);
|
|
5728
|
-
this.clinicAdminService = clinicAdminService;
|
|
5729
|
-
}
|
|
5730
|
-
/**
|
|
5731
|
-
* Kreira novu grupaciju klinika
|
|
5732
|
-
*/
|
|
5733
|
-
async createClinicGroup(data, ownerId, isDefault = false) {
|
|
5734
|
-
return createClinicGroup(
|
|
5735
|
-
this.db,
|
|
5736
|
-
data,
|
|
5737
|
-
ownerId,
|
|
5738
|
-
isDefault,
|
|
5739
|
-
this.clinicAdminService,
|
|
5740
|
-
this.app
|
|
5741
|
-
);
|
|
5742
|
-
}
|
|
5743
|
-
/**
|
|
5744
|
-
* Dohvata grupaciju klinika po ID-u
|
|
5745
|
-
*/
|
|
5746
|
-
async getClinicGroup(groupId) {
|
|
5747
|
-
return getClinicGroup(this.db, groupId);
|
|
5748
|
-
}
|
|
5749
|
-
/**
|
|
5750
|
-
* Dohvata sve aktivne grupacije klinika
|
|
5751
|
-
*/
|
|
5752
|
-
async getAllActiveGroups() {
|
|
5753
|
-
return getAllActiveGroups(this.db);
|
|
5754
|
-
}
|
|
5755
|
-
/**
|
|
5756
|
-
* Ažurira grupaciju klinika
|
|
5757
|
-
*/
|
|
5758
|
-
async updateClinicGroup(groupId, data) {
|
|
5759
|
-
return updateClinicGroup(this.db, groupId, data, this.app);
|
|
5760
|
-
}
|
|
5761
|
-
/**
|
|
5762
|
-
* Dodaje admina u grupaciju
|
|
5763
|
-
*/
|
|
5764
|
-
async addAdminToGroup(groupId, adminId) {
|
|
5765
|
-
return addAdminToGroup(
|
|
5766
|
-
this.db,
|
|
5767
|
-
groupId,
|
|
5768
|
-
adminId,
|
|
5769
|
-
this.app
|
|
5770
|
-
);
|
|
5771
|
-
}
|
|
5772
|
-
/**
|
|
5773
|
-
* Uklanja admina iz grupacije
|
|
5774
|
-
*/
|
|
5775
|
-
async removeAdminFromGroup(groupId, adminId) {
|
|
5776
|
-
return removeAdminFromGroup(
|
|
5777
|
-
this.db,
|
|
5778
|
-
groupId,
|
|
5779
|
-
adminId,
|
|
5780
|
-
this.app
|
|
5781
|
-
);
|
|
5782
|
-
}
|
|
5783
|
-
/**
|
|
5784
|
-
* Deaktivira grupaciju klinika
|
|
5785
|
-
*/
|
|
5786
|
-
async deactivateClinicGroup(groupId) {
|
|
5787
|
-
return deactivateClinicGroup(this.db, groupId, this.app);
|
|
5788
|
-
}
|
|
5789
|
-
/**
|
|
5790
|
-
* Sets up additional clinic group information after initial creation
|
|
5791
|
-
*
|
|
5792
|
-
* @param groupId - The ID of the clinic group to set up
|
|
5793
|
-
* @param setupData - The setup data for the clinic group
|
|
5794
|
-
* @returns The updated clinic group
|
|
5795
|
-
*/
|
|
5796
|
-
async setupClinicGroup(groupId, setupData) {
|
|
5797
|
-
console.log("[CLINIC_GROUP] Setting up clinic group", { groupId });
|
|
5798
|
-
const clinicGroup = await this.getClinicGroup(groupId);
|
|
5799
|
-
if (!clinicGroup) {
|
|
5800
|
-
console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
|
|
5801
|
-
throw new Error(`Clinic group with ID ${groupId} not found`);
|
|
5802
|
-
}
|
|
5803
|
-
let logoUrl = setupData.logo;
|
|
5804
|
-
if (logoUrl && typeof logoUrl === "string" && logoUrl.startsWith("data:")) {
|
|
5805
|
-
console.log("[CLINIC_GROUP] Processing logo in setupClinicGroup");
|
|
5806
|
-
try {
|
|
5807
|
-
const uploadedLogoUrl = await uploadPhoto(
|
|
5808
|
-
logoUrl,
|
|
5809
|
-
"clinic-groups",
|
|
5810
|
-
groupId,
|
|
5811
|
-
"logo",
|
|
5812
|
-
this.app
|
|
5813
|
-
);
|
|
5814
|
-
console.log("[CLINIC_GROUP] Logo processed in setupClinicGroup", {
|
|
5815
|
-
uploadedLogoUrl
|
|
5816
|
-
});
|
|
5817
|
-
if (uploadedLogoUrl !== null) {
|
|
5818
|
-
logoUrl = uploadedLogoUrl;
|
|
5819
|
-
}
|
|
5820
|
-
} catch (error) {
|
|
5821
|
-
console.error(
|
|
5822
|
-
"[CLINIC_GROUP] Error processing logo in setupClinicGroup:",
|
|
5823
|
-
error
|
|
5824
|
-
);
|
|
5825
|
-
}
|
|
5826
|
-
}
|
|
5827
|
-
const updateData = {
|
|
5828
|
-
languages: setupData.languages,
|
|
5829
|
-
practiceType: setupData.practiceType,
|
|
5830
|
-
description: setupData.description,
|
|
5831
|
-
logo: logoUrl,
|
|
5832
|
-
calendarSyncEnabled: setupData.calendarSyncEnabled,
|
|
5833
|
-
autoConfirmAppointments: setupData.autoConfirmAppointments,
|
|
5834
|
-
businessIdentificationNumber: setupData.businessIdentificationNumber
|
|
5835
|
-
};
|
|
5836
|
-
console.log("[CLINIC_GROUP] Updating clinic group with setup data");
|
|
5837
|
-
return this.updateClinicGroup(groupId, updateData);
|
|
5838
|
-
}
|
|
5839
|
-
/**
|
|
5840
|
-
* Kreira admin token za grupaciju
|
|
5841
|
-
*/
|
|
5842
|
-
async createAdminToken(groupId, creatorAdminId, data) {
|
|
5843
|
-
return createAdminToken(
|
|
5844
|
-
this.db,
|
|
5845
|
-
groupId,
|
|
5846
|
-
creatorAdminId,
|
|
5847
|
-
this.app,
|
|
5848
|
-
data
|
|
5849
|
-
);
|
|
5850
|
-
}
|
|
5851
|
-
/**
|
|
5852
|
-
* Verifikuje i koristi admin token
|
|
5853
|
-
*/
|
|
5854
|
-
async verifyAndUseAdminToken(groupId, token, userRef) {
|
|
5855
|
-
return verifyAndUseAdminToken(
|
|
5856
|
-
this.db,
|
|
5857
|
-
groupId,
|
|
5858
|
-
token,
|
|
5859
|
-
userRef,
|
|
5860
|
-
this.app
|
|
5861
|
-
);
|
|
5862
|
-
}
|
|
5863
|
-
/**
|
|
5864
|
-
* Briše admin token
|
|
5865
|
-
*/
|
|
5866
|
-
async deleteAdminToken(groupId, tokenId, adminId) {
|
|
5867
|
-
return deleteAdminToken(
|
|
5868
|
-
this.db,
|
|
5869
|
-
groupId,
|
|
5870
|
-
tokenId,
|
|
5871
|
-
adminId,
|
|
5872
|
-
this.app
|
|
5873
|
-
);
|
|
5874
|
-
}
|
|
5875
|
-
/**
|
|
5876
|
-
* Dohvata aktivne admin tokene
|
|
5877
|
-
*/
|
|
5878
|
-
async getActiveAdminTokens(groupId, adminId) {
|
|
5879
|
-
return getActiveAdminTokens(
|
|
5880
|
-
this.db,
|
|
5881
|
-
groupId,
|
|
5882
|
-
adminId,
|
|
5883
|
-
this.app
|
|
5884
|
-
);
|
|
5885
|
-
}
|
|
5886
|
-
// TODO: Add a method to get all admin tokens for a clinic group (not just active ones)
|
|
5887
|
-
// TODO: Refactor admin token methods not to add tokens to the clinicGroup document,
|
|
5888
|
-
// but to add them to a subcollection called adminTokens that belongs to a specific clinicGroup document
|
|
5889
|
-
// TODO: Add granular control over admin permissions, e.g. only allow admins to manage certain clinics to tokens directly
|
|
5890
|
-
// TODO: Generally refactor admin tokens and invites, also create cloud function to send invites and send updates when sombody uses the token
|
|
5891
|
-
/**
|
|
5892
|
-
* Updates the onboarding status for a clinic group
|
|
5893
|
-
*
|
|
5894
|
-
* @param groupId - The ID of the clinic group to update
|
|
5895
|
-
* @param onboardingData - The onboarding data to update
|
|
5896
|
-
* @returns The updated clinic group
|
|
5897
|
-
*/
|
|
5898
|
-
async setOnboarding(groupId, onboardingData) {
|
|
5899
|
-
console.log("[CLINIC_GROUP] Updating onboarding status", {
|
|
5900
|
-
groupId,
|
|
5901
|
-
onboardingData
|
|
5902
|
-
});
|
|
5903
|
-
return this.updateClinicGroup(groupId, {
|
|
5904
|
-
onboarding: onboardingData
|
|
5905
|
-
});
|
|
5906
|
-
}
|
|
5907
|
-
/**
|
|
5908
|
-
* Sets the current onboarding step for a clinic group
|
|
5909
|
-
*
|
|
5910
|
-
* @param groupId - The ID of the clinic group to update
|
|
5911
|
-
* @param step - The current onboarding step number
|
|
5912
|
-
* @returns The updated clinic group
|
|
5913
|
-
*/
|
|
5914
|
-
async setOnboardingStep(groupId, step) {
|
|
5915
|
-
console.log("[CLINIC_GROUP] Setting onboarding step", { groupId, step });
|
|
5916
|
-
return this.setOnboarding(groupId, { step, completed: false });
|
|
6010
|
+
async function verifyAndUseAdminToken(db, groupId, token, userRef, app) {
|
|
6011
|
+
const group = await getClinicGroup(db, groupId);
|
|
6012
|
+
if (!group) {
|
|
6013
|
+
throw new Error("Clinic group not found");
|
|
5917
6014
|
}
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
* @param groupId - The ID of the clinic group to update
|
|
5922
|
-
* @returns The updated clinic group
|
|
5923
|
-
*/
|
|
5924
|
-
async completeOnboarding(groupId) {
|
|
5925
|
-
console.log("[CLINIC_GROUP] Completing onboarding", { groupId });
|
|
5926
|
-
return this.setOnboarding(groupId, { completed: true });
|
|
6015
|
+
const adminToken = group.adminTokens.find((t) => t.token === token);
|
|
6016
|
+
if (!adminToken) {
|
|
6017
|
+
throw new Error("Admin token not found");
|
|
5927
6018
|
}
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
// src/services/clinic/clinic.service.ts
|
|
5931
|
-
var import_firestore24 = require("firebase/firestore");
|
|
5932
|
-
var import_geofire_common7 = require("geofire-common");
|
|
5933
|
-
var import_zod19 = require("zod");
|
|
5934
|
-
|
|
5935
|
-
// src/services/clinic/utils/clinic.utils.ts
|
|
5936
|
-
var import_firestore19 = require("firebase/firestore");
|
|
5937
|
-
var import_geofire_common4 = require("geofire-common");
|
|
5938
|
-
var import_zod18 = require("zod");
|
|
5939
|
-
async function getClinic(db, clinicId) {
|
|
5940
|
-
const docRef = (0, import_firestore19.doc)(db, CLINICS_COLLECTION, clinicId);
|
|
5941
|
-
const docSnap = await (0, import_firestore19.getDoc)(docRef);
|
|
5942
|
-
if (docSnap.exists()) {
|
|
5943
|
-
return docSnap.data();
|
|
6019
|
+
if (adminToken.status !== "active" /* ACTIVE */) {
|
|
6020
|
+
throw new Error("Admin token is not active");
|
|
5944
6021
|
}
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
6022
|
+
const now = import_firestore20.Timestamp.now();
|
|
6023
|
+
if (adminToken.expiresAt.seconds < now.seconds) {
|
|
6024
|
+
const updatedTokens2 = group.adminTokens.map(
|
|
6025
|
+
(t) => t.id === adminToken.id ? { ...t, status: "expired" /* EXPIRED */ } : t
|
|
6026
|
+
);
|
|
6027
|
+
await updateClinicGroup(
|
|
6028
|
+
db,
|
|
6029
|
+
groupId,
|
|
6030
|
+
{
|
|
6031
|
+
adminTokens: updatedTokens2
|
|
6032
|
+
},
|
|
6033
|
+
app
|
|
6034
|
+
);
|
|
6035
|
+
throw new Error("Admin token has expired");
|
|
6036
|
+
}
|
|
6037
|
+
const updatedTokens = group.adminTokens.map(
|
|
6038
|
+
(t) => t.id === adminToken.id ? {
|
|
6039
|
+
...t,
|
|
6040
|
+
status: "used" /* USED */,
|
|
6041
|
+
usedByUserRef: userRef
|
|
6042
|
+
} : t
|
|
5952
6043
|
);
|
|
5953
|
-
|
|
5954
|
-
|
|
6044
|
+
await updateClinicGroup(
|
|
6045
|
+
db,
|
|
6046
|
+
groupId,
|
|
6047
|
+
{
|
|
6048
|
+
adminTokens: updatedTokens
|
|
6049
|
+
},
|
|
6050
|
+
app
|
|
6051
|
+
);
|
|
6052
|
+
return true;
|
|
5955
6053
|
}
|
|
5956
|
-
async function
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
console.error("[CLINIC] Clinic not found", { clinicId });
|
|
5961
|
-
throw new Error("Clinic not found");
|
|
6054
|
+
async function deleteAdminToken(db, groupId, tokenId, adminId, app) {
|
|
6055
|
+
const group = await getClinicGroup(db, groupId);
|
|
6056
|
+
if (!group) {
|
|
6057
|
+
throw new Error("Clinic group not found");
|
|
5962
6058
|
}
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
const admin = await clinicAdminService.getClinicAdmin(adminId);
|
|
5966
|
-
if (!admin) {
|
|
5967
|
-
console.error("[CLINIC] Admin not found", { adminId });
|
|
5968
|
-
throw new Error("Admin not found");
|
|
5969
|
-
}
|
|
5970
|
-
const hasPermission = admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId || admin.clinicsManaged.includes(clinicId) && clinic.admins && clinic.admins.includes(adminId);
|
|
5971
|
-
if (!hasPermission) {
|
|
5972
|
-
console.error(
|
|
5973
|
-
"[CLINIC] Admin does not have permission to update this clinic",
|
|
5974
|
-
{
|
|
5975
|
-
adminId,
|
|
5976
|
-
clinicId,
|
|
5977
|
-
isGroupOwner: admin.isGroupOwner,
|
|
5978
|
-
clinicsManaged: admin.clinicsManaged,
|
|
5979
|
-
isClinicAdmin: clinic.admins && clinic.admins.includes(adminId)
|
|
5980
|
-
}
|
|
5981
|
-
);
|
|
5982
|
-
throw new Error("Admin does not have permission to update this clinic");
|
|
5983
|
-
}
|
|
5984
|
-
console.log("[CLINIC] Admin permissions verified");
|
|
5985
|
-
} catch (adminError) {
|
|
5986
|
-
console.error("[CLINIC] Error verifying admin permissions:", adminError);
|
|
5987
|
-
throw adminError;
|
|
6059
|
+
if (!group.admins.includes(adminId)) {
|
|
6060
|
+
throw new Error("Admin does not belong to this clinic group");
|
|
5988
6061
|
}
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
}
|
|
6004
|
-
} catch (logoError) {
|
|
6005
|
-
console.error("[CLINIC] Error processing logo update:", logoError);
|
|
6006
|
-
}
|
|
6062
|
+
const updatedTokens = group.adminTokens.filter((t) => t.id !== tokenId);
|
|
6063
|
+
await updateClinicGroup(
|
|
6064
|
+
db,
|
|
6065
|
+
groupId,
|
|
6066
|
+
{
|
|
6067
|
+
adminTokens: updatedTokens
|
|
6068
|
+
},
|
|
6069
|
+
app
|
|
6070
|
+
);
|
|
6071
|
+
}
|
|
6072
|
+
async function getActiveAdminTokens(db, groupId, adminId, app) {
|
|
6073
|
+
const group = await getClinicGroup(db, groupId);
|
|
6074
|
+
if (!group) {
|
|
6075
|
+
throw new Error("Clinic group not found");
|
|
6007
6076
|
}
|
|
6008
|
-
if (
|
|
6009
|
-
|
|
6010
|
-
try {
|
|
6011
|
-
if (typeof data.coverPhoto === "string" && data.coverPhoto.startsWith("data:")) {
|
|
6012
|
-
const uploadedPhoto = await uploadPhoto(
|
|
6013
|
-
data.coverPhoto,
|
|
6014
|
-
"clinics",
|
|
6015
|
-
clinicId,
|
|
6016
|
-
"cover",
|
|
6017
|
-
app
|
|
6018
|
-
);
|
|
6019
|
-
if (uploadedPhoto) {
|
|
6020
|
-
updatedData.coverPhoto = uploadedPhoto;
|
|
6021
|
-
}
|
|
6022
|
-
} else {
|
|
6023
|
-
updatedData.coverPhoto = data.coverPhoto;
|
|
6024
|
-
}
|
|
6025
|
-
console.log("[CLINIC] Cover photo update processed");
|
|
6026
|
-
} catch (photoError) {
|
|
6027
|
-
console.error(
|
|
6028
|
-
"[CLINIC] Error processing cover photo update:",
|
|
6029
|
-
photoError
|
|
6030
|
-
);
|
|
6031
|
-
if (typeof data.coverPhoto === "string" && !data.coverPhoto.startsWith("data:")) {
|
|
6032
|
-
updatedData.coverPhoto = data.coverPhoto;
|
|
6033
|
-
}
|
|
6034
|
-
}
|
|
6077
|
+
if (!group.admins.includes(adminId)) {
|
|
6078
|
+
throw new Error("Admin does not belong to this clinic group");
|
|
6035
6079
|
}
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
);
|
|
6045
|
-
if (dataUrlPhotos.length > 0) {
|
|
6046
|
-
const uploadedPhotos = await uploadMultiplePhotos(
|
|
6047
|
-
dataUrlPhotos,
|
|
6048
|
-
"clinics",
|
|
6049
|
-
clinicId,
|
|
6050
|
-
"featured",
|
|
6051
|
-
app
|
|
6052
|
-
);
|
|
6053
|
-
console.log("[CLINIC] Featured photos update processed", {
|
|
6054
|
-
count: uploadedPhotos.length
|
|
6055
|
-
});
|
|
6056
|
-
updatedData.featuredPhotos = [...existingPhotos, ...uploadedPhotos];
|
|
6057
|
-
} else {
|
|
6058
|
-
updatedData.featuredPhotos = existingPhotos;
|
|
6059
|
-
}
|
|
6060
|
-
} catch (featuredError) {
|
|
6061
|
-
console.error(
|
|
6062
|
-
"[CLINIC] Error processing featured photos update:",
|
|
6063
|
-
featuredError
|
|
6064
|
-
);
|
|
6065
|
-
updatedData.featuredPhotos = data.featuredPhotos.filter(
|
|
6066
|
-
(photo) => typeof photo === "string" && !photo.startsWith("data:")
|
|
6067
|
-
);
|
|
6068
|
-
}
|
|
6080
|
+
return group.adminTokens.filter((t) => t.status === "active" /* ACTIVE */);
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
// src/services/clinic/clinic-group.service.ts
|
|
6084
|
+
var ClinicGroupService = class extends BaseService {
|
|
6085
|
+
constructor(db, auth, app, clinicAdminService) {
|
|
6086
|
+
super(db, auth, app);
|
|
6087
|
+
this.clinicAdminService = clinicAdminService;
|
|
6069
6088
|
}
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
);
|
|
6083
|
-
if (uploadedUrl) {
|
|
6084
|
-
updatedPhotosWithTags.push({
|
|
6085
|
-
url: uploadedUrl,
|
|
6086
|
-
tag: photoWithTag.tag
|
|
6087
|
-
});
|
|
6088
|
-
}
|
|
6089
|
-
} else {
|
|
6090
|
-
updatedPhotosWithTags.push(photoWithTag);
|
|
6091
|
-
}
|
|
6092
|
-
}
|
|
6093
|
-
updatedData.photosWithTags = updatedPhotosWithTags;
|
|
6094
|
-
console.log("[CLINIC] Photos with tags update processed", {
|
|
6095
|
-
count: updatedPhotosWithTags.length
|
|
6096
|
-
});
|
|
6097
|
-
} catch (tagsError) {
|
|
6098
|
-
console.error(
|
|
6099
|
-
"[CLINIC] Error processing photos with tags update:",
|
|
6100
|
-
tagsError
|
|
6101
|
-
);
|
|
6102
|
-
updatedData.photosWithTags = data.photosWithTags.filter(
|
|
6103
|
-
(photo) => !photo.url.startsWith("data:")
|
|
6104
|
-
);
|
|
6105
|
-
}
|
|
6089
|
+
/**
|
|
6090
|
+
* Kreira novu grupaciju klinika
|
|
6091
|
+
*/
|
|
6092
|
+
async createClinicGroup(data, ownerId, isDefault = false) {
|
|
6093
|
+
return createClinicGroup(
|
|
6094
|
+
this.db,
|
|
6095
|
+
data,
|
|
6096
|
+
ownerId,
|
|
6097
|
+
isDefault,
|
|
6098
|
+
this.clinicAdminService,
|
|
6099
|
+
this.app
|
|
6100
|
+
);
|
|
6106
6101
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
try {
|
|
6113
|
-
await (0, import_firestore19.updateDoc)((0, import_firestore19.doc)(db, CLINICS_COLLECTION, clinicId), updatedData);
|
|
6114
|
-
console.log("[CLINIC] Clinic updated successfully");
|
|
6115
|
-
} catch (updateError) {
|
|
6116
|
-
console.error("[CLINIC] Error updating clinic in Firestore:", updateError);
|
|
6117
|
-
throw updateError;
|
|
6102
|
+
/**
|
|
6103
|
+
* Dohvata grupaciju klinika po ID-u
|
|
6104
|
+
*/
|
|
6105
|
+
async getClinicGroup(groupId) {
|
|
6106
|
+
return getClinicGroup(this.db, groupId);
|
|
6118
6107
|
}
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6108
|
+
/**
|
|
6109
|
+
* Dohvata sve aktivne grupacije klinika
|
|
6110
|
+
*/
|
|
6111
|
+
async getAllActiveGroups() {
|
|
6112
|
+
return getAllActiveGroups(this.db);
|
|
6123
6113
|
}
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
async
|
|
6128
|
-
|
|
6129
|
-
if (!admin) {
|
|
6130
|
-
throw new Error("Admin not found");
|
|
6114
|
+
/**
|
|
6115
|
+
* Ažurira grupaciju klinika
|
|
6116
|
+
*/
|
|
6117
|
+
async updateClinicGroup(groupId, data) {
|
|
6118
|
+
return updateClinicGroup(this.db, groupId, data, this.app);
|
|
6131
6119
|
}
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6120
|
+
/**
|
|
6121
|
+
* Dodaje admina u grupaciju
|
|
6122
|
+
*/
|
|
6123
|
+
async addAdminToGroup(groupId, adminId) {
|
|
6124
|
+
return addAdminToGroup(
|
|
6125
|
+
this.db,
|
|
6126
|
+
groupId,
|
|
6127
|
+
adminId,
|
|
6128
|
+
this.app
|
|
6129
|
+
);
|
|
6138
6130
|
}
|
|
6139
|
-
|
|
6140
|
-
|
|
6131
|
+
/**
|
|
6132
|
+
* Uklanja admina iz grupacije
|
|
6133
|
+
*/
|
|
6134
|
+
async removeAdminFromGroup(groupId, adminId) {
|
|
6135
|
+
return removeAdminFromGroup(
|
|
6136
|
+
this.db,
|
|
6137
|
+
groupId,
|
|
6138
|
+
adminId,
|
|
6139
|
+
this.app
|
|
6140
|
+
);
|
|
6141
6141
|
}
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6142
|
+
/**
|
|
6143
|
+
* Deaktivira grupaciju klinika
|
|
6144
|
+
*/
|
|
6145
|
+
async deactivateClinicGroup(groupId) {
|
|
6146
|
+
return deactivateClinicGroup(this.db, groupId, this.app);
|
|
6145
6147
|
}
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
{
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
}
|
|
6159
|
-
async function getClinicById(db, clinicId) {
|
|
6160
|
-
try {
|
|
6161
|
-
const clinicRef = (0, import_firestore19.doc)(db, CLINICS_COLLECTION, clinicId);
|
|
6162
|
-
const clinicSnapshot = await (0, import_firestore19.getDoc)(clinicRef);
|
|
6163
|
-
if (!clinicSnapshot.exists()) {
|
|
6164
|
-
return null;
|
|
6148
|
+
/**
|
|
6149
|
+
* Sets up additional clinic group information after initial creation
|
|
6150
|
+
*
|
|
6151
|
+
* @param groupId - The ID of the clinic group to set up
|
|
6152
|
+
* @param setupData - The setup data for the clinic group
|
|
6153
|
+
* @returns The updated clinic group
|
|
6154
|
+
*/
|
|
6155
|
+
async setupClinicGroup(groupId, setupData) {
|
|
6156
|
+
console.log("[CLINIC_GROUP] Setting up clinic group", { groupId });
|
|
6157
|
+
const clinicGroup = await this.getClinicGroup(groupId);
|
|
6158
|
+
if (!clinicGroup) {
|
|
6159
|
+
console.error("[CLINIC_GROUP] Clinic group not found", { groupId });
|
|
6160
|
+
throw new Error(`Clinic group with ID ${groupId} not found`);
|
|
6165
6161
|
}
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6162
|
+
let logoUrl = setupData.logo;
|
|
6163
|
+
if (logoUrl && typeof logoUrl === "string" && logoUrl.startsWith("data:")) {
|
|
6164
|
+
console.log("[CLINIC_GROUP] Processing logo in setupClinicGroup");
|
|
6165
|
+
try {
|
|
6166
|
+
const uploadedLogoUrl = await uploadPhoto(
|
|
6167
|
+
logoUrl,
|
|
6168
|
+
"clinic-groups",
|
|
6169
|
+
groupId,
|
|
6170
|
+
"logo",
|
|
6171
|
+
this.app
|
|
6172
|
+
);
|
|
6173
|
+
console.log("[CLINIC_GROUP] Logo processed in setupClinicGroup", {
|
|
6174
|
+
uploadedLogoUrl
|
|
6175
|
+
});
|
|
6176
|
+
if (uploadedLogoUrl !== null) {
|
|
6177
|
+
logoUrl = uploadedLogoUrl;
|
|
6178
|
+
}
|
|
6179
|
+
} catch (error) {
|
|
6180
|
+
console.error(
|
|
6181
|
+
"[CLINIC_GROUP] Error processing logo in setupClinicGroup:",
|
|
6182
|
+
error
|
|
6186
6183
|
);
|
|
6187
|
-
} else {
|
|
6188
|
-
clinicsQuery = (0, import_firestore19.query)(clinicsCollection, (0, import_firestore19.limit)(pagination));
|
|
6189
6184
|
}
|
|
6190
6185
|
}
|
|
6191
|
-
const
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
});
|
|
6200
|
-
return {
|
|
6201
|
-
clinics,
|
|
6202
|
-
lastDoc: lastVisible
|
|
6186
|
+
const updateData = {
|
|
6187
|
+
languages: setupData.languages,
|
|
6188
|
+
practiceType: setupData.practiceType,
|
|
6189
|
+
description: setupData.description,
|
|
6190
|
+
logo: logoUrl,
|
|
6191
|
+
calendarSyncEnabled: setupData.calendarSyncEnabled,
|
|
6192
|
+
autoConfirmAppointments: setupData.autoConfirmAppointments,
|
|
6193
|
+
businessIdentificationNumber: setupData.businessIdentificationNumber
|
|
6203
6194
|
};
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
throw error;
|
|
6195
|
+
console.log("[CLINIC_GROUP] Updating clinic group with setup data");
|
|
6196
|
+
return this.updateClinicGroup(groupId, updateData);
|
|
6207
6197
|
}
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
(0, import_firestore19.where)("location.geohash", "<=", b[1]),
|
|
6220
|
-
(0, import_firestore19.where)("isActive", "==", true)
|
|
6221
|
-
];
|
|
6222
|
-
const q = (0, import_firestore19.query)((0, import_firestore19.collection)(db, CLINICS_COLLECTION), ...constraints);
|
|
6223
|
-
const querySnapshot = await (0, import_firestore19.getDocs)(q);
|
|
6224
|
-
for (const doc34 of querySnapshot.docs) {
|
|
6225
|
-
const clinic = doc34.data();
|
|
6226
|
-
const distance = (0, import_geofire_common4.distanceBetween)(
|
|
6227
|
-
[center.latitude, center.longitude],
|
|
6228
|
-
[clinic.location.latitude, clinic.location.longitude]
|
|
6229
|
-
);
|
|
6230
|
-
const distanceInKm = distance / 1e3;
|
|
6231
|
-
if (distanceInKm <= rangeInKm) {
|
|
6232
|
-
matchingClinics.push({
|
|
6233
|
-
...clinic,
|
|
6234
|
-
distance: distanceInKm
|
|
6235
|
-
});
|
|
6236
|
-
}
|
|
6237
|
-
}
|
|
6198
|
+
/**
|
|
6199
|
+
* Kreira admin token za grupaciju
|
|
6200
|
+
*/
|
|
6201
|
+
async createAdminToken(groupId, creatorAdminId, data) {
|
|
6202
|
+
return createAdminToken(
|
|
6203
|
+
this.db,
|
|
6204
|
+
groupId,
|
|
6205
|
+
creatorAdminId,
|
|
6206
|
+
this.app,
|
|
6207
|
+
data
|
|
6208
|
+
);
|
|
6238
6209
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
const paginatedClinics = result.slice(0, pagination);
|
|
6251
|
-
const newLastDoc = paginatedClinics.length > 0 ? paginatedClinics[paginatedClinics.length - 1] : null;
|
|
6252
|
-
return {
|
|
6253
|
-
clinics: paginatedClinics,
|
|
6254
|
-
lastDoc: newLastDoc
|
|
6255
|
-
};
|
|
6210
|
+
/**
|
|
6211
|
+
* Verifikuje i koristi admin token
|
|
6212
|
+
*/
|
|
6213
|
+
async verifyAndUseAdminToken(groupId, token, userRef) {
|
|
6214
|
+
return verifyAndUseAdminToken(
|
|
6215
|
+
this.db,
|
|
6216
|
+
groupId,
|
|
6217
|
+
token,
|
|
6218
|
+
userRef,
|
|
6219
|
+
this.app
|
|
6220
|
+
);
|
|
6256
6221
|
}
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6222
|
+
/**
|
|
6223
|
+
* Briše admin token
|
|
6224
|
+
*/
|
|
6225
|
+
async deleteAdminToken(groupId, tokenId, adminId) {
|
|
6226
|
+
return deleteAdminToken(
|
|
6227
|
+
this.db,
|
|
6228
|
+
groupId,
|
|
6229
|
+
tokenId,
|
|
6230
|
+
adminId,
|
|
6231
|
+
this.app
|
|
6232
|
+
);
|
|
6233
|
+
}
|
|
6234
|
+
/**
|
|
6235
|
+
* Dohvata aktivne admin tokene
|
|
6236
|
+
*/
|
|
6237
|
+
async getActiveAdminTokens(groupId, adminId) {
|
|
6238
|
+
return getActiveAdminTokens(
|
|
6239
|
+
this.db,
|
|
6240
|
+
groupId,
|
|
6241
|
+
adminId,
|
|
6242
|
+
this.app
|
|
6243
|
+
);
|
|
6244
|
+
}
|
|
6245
|
+
// TODO: Add a method to get all admin tokens for a clinic group (not just active ones)
|
|
6246
|
+
// TODO: Refactor admin token methods not to add tokens to the clinicGroup document,
|
|
6247
|
+
// but to add them to a subcollection called adminTokens that belongs to a specific clinicGroup document
|
|
6248
|
+
// TODO: Add granular control over admin permissions, e.g. only allow admins to manage certain clinics to tokens directly
|
|
6249
|
+
// TODO: Generally refactor admin tokens and invites, also create cloud function to send invites and send updates when sombody uses the token
|
|
6250
|
+
/**
|
|
6251
|
+
* Updates the onboarding status for a clinic group
|
|
6252
|
+
*
|
|
6253
|
+
* @param groupId - The ID of the clinic group to update
|
|
6254
|
+
* @param onboardingData - The onboarding data to update
|
|
6255
|
+
* @returns The updated clinic group
|
|
6256
|
+
*/
|
|
6257
|
+
async setOnboarding(groupId, onboardingData) {
|
|
6258
|
+
console.log("[CLINIC_GROUP] Updating onboarding status", {
|
|
6259
|
+
groupId,
|
|
6260
|
+
onboardingData
|
|
6261
|
+
});
|
|
6262
|
+
return this.updateClinicGroup(groupId, {
|
|
6263
|
+
onboarding: onboardingData
|
|
6264
|
+
});
|
|
6268
6265
|
}
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6266
|
+
/**
|
|
6267
|
+
* Sets the current onboarding step for a clinic group
|
|
6268
|
+
*
|
|
6269
|
+
* @param groupId - The ID of the clinic group to update
|
|
6270
|
+
* @param step - The current onboarding step number
|
|
6271
|
+
* @returns The updated clinic group
|
|
6272
|
+
*/
|
|
6273
|
+
async setOnboardingStep(groupId, step) {
|
|
6274
|
+
console.log("[CLINIC_GROUP] Setting onboarding step", { groupId, step });
|
|
6275
|
+
return this.setOnboarding(groupId, { step, completed: false });
|
|
6272
6276
|
}
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6277
|
+
/**
|
|
6278
|
+
* Marks the onboarding process as completed for a clinic group
|
|
6279
|
+
*
|
|
6280
|
+
* @param groupId - The ID of the clinic group to update
|
|
6281
|
+
* @returns The updated clinic group
|
|
6282
|
+
*/
|
|
6283
|
+
async completeOnboarding(groupId) {
|
|
6284
|
+
console.log("[CLINIC_GROUP] Completing onboarding", { groupId });
|
|
6285
|
+
return this.setOnboarding(groupId, { completed: true });
|
|
6276
6286
|
}
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
+
};
|
|
6288
|
+
|
|
6289
|
+
// src/services/clinic/clinic.service.ts
|
|
6290
|
+
var import_firestore24 = require("firebase/firestore");
|
|
6291
|
+
var import_geofire_common7 = require("geofire-common");
|
|
6292
|
+
var import_zod19 = require("zod");
|
|
6293
|
+
|
|
6294
|
+
// src/services/clinic/utils/clinic.utils.ts
|
|
6295
|
+
var import_firestore21 = require("firebase/firestore");
|
|
6296
|
+
var import_geofire_common4 = require("geofire-common");
|
|
6297
|
+
var import_zod18 = require("zod");
|
|
6298
|
+
async function getClinic(db, clinicId) {
|
|
6299
|
+
const docRef = (0, import_firestore21.doc)(db, CLINICS_COLLECTION, clinicId);
|
|
6300
|
+
const docSnap = await (0, import_firestore21.getDoc)(docRef);
|
|
6301
|
+
if (docSnap.exists()) {
|
|
6302
|
+
return docSnap.data();
|
|
6303
|
+
}
|
|
6304
|
+
return null;
|
|
6305
|
+
}
|
|
6306
|
+
async function getClinicsByGroup(db, groupId) {
|
|
6307
|
+
const q = (0, import_firestore21.query)(
|
|
6308
|
+
(0, import_firestore21.collection)(db, CLINICS_COLLECTION),
|
|
6309
|
+
(0, import_firestore21.where)("clinicGroupId", "==", groupId),
|
|
6310
|
+
(0, import_firestore21.where)("isActive", "==", true)
|
|
6287
6311
|
);
|
|
6312
|
+
const querySnapshot = await (0, import_firestore21.getDocs)(q);
|
|
6313
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
6288
6314
|
}
|
|
6289
|
-
async function
|
|
6315
|
+
async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app) {
|
|
6316
|
+
console.log("[CLINIC] Starting clinic update", { clinicId, adminId });
|
|
6290
6317
|
const clinic = await getClinic(db, clinicId);
|
|
6291
6318
|
if (!clinic) {
|
|
6319
|
+
console.error("[CLINIC] Clinic not found", { clinicId });
|
|
6292
6320
|
throw new Error("Clinic not found");
|
|
6293
6321
|
}
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
throw new Error("Admin does not have permission to update this clinic");
|
|
6301
|
-
}
|
|
6302
|
-
const updatedTags = clinic.tags.filter(
|
|
6303
|
-
(tag) => !tagsToRemove.tags || !tagsToRemove.tags.includes(tag)
|
|
6304
|
-
);
|
|
6305
|
-
return updateClinic(
|
|
6306
|
-
db,
|
|
6307
|
-
clinicId,
|
|
6308
|
-
{
|
|
6309
|
-
tags: updatedTags
|
|
6310
|
-
},
|
|
6311
|
-
adminId,
|
|
6312
|
-
clinicAdminService,
|
|
6313
|
-
app
|
|
6314
|
-
);
|
|
6315
|
-
}
|
|
6316
|
-
|
|
6317
|
-
// src/services/clinic/utils/search.utils.ts
|
|
6318
|
-
var import_firestore20 = require("firebase/firestore");
|
|
6319
|
-
var import_geofire_common5 = require("geofire-common");
|
|
6320
|
-
async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
6321
|
-
const bounds = (0, import_geofire_common5.geohashQueryBounds)(
|
|
6322
|
-
[center.latitude, center.longitude],
|
|
6323
|
-
radiusInKm * 1e3
|
|
6324
|
-
);
|
|
6325
|
-
const matchingDocs = [];
|
|
6326
|
-
for (const b of bounds) {
|
|
6327
|
-
const constraints = [
|
|
6328
|
-
(0, import_firestore20.where)("location.geohash", ">=", b[0]),
|
|
6329
|
-
(0, import_firestore20.where)("location.geohash", "<=", b[1]),
|
|
6330
|
-
(0, import_firestore20.where)("isActive", "==", true)
|
|
6331
|
-
];
|
|
6332
|
-
if (filters == null ? void 0 : filters.services) {
|
|
6333
|
-
constraints.push(
|
|
6334
|
-
(0, import_firestore20.where)("services", "array-contains-any", filters.services)
|
|
6335
|
-
);
|
|
6336
|
-
}
|
|
6337
|
-
if ((filters == null ? void 0 : filters.tags) && filters.tags.length > 0) {
|
|
6338
|
-
constraints.push((0, import_firestore20.where)("tags", "array-contains-any", filters.tags));
|
|
6322
|
+
try {
|
|
6323
|
+
console.log("[CLINIC] Checking admin permissions");
|
|
6324
|
+
const admin = await clinicAdminService.getClinicAdmin(adminId);
|
|
6325
|
+
if (!admin) {
|
|
6326
|
+
console.error("[CLINIC] Admin not found", { adminId });
|
|
6327
|
+
throw new Error("Admin not found");
|
|
6339
6328
|
}
|
|
6340
|
-
const
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6329
|
+
const hasPermission = admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId || admin.clinicsManaged.includes(clinicId) && clinic.admins && clinic.admins.includes(adminId);
|
|
6330
|
+
if (!hasPermission) {
|
|
6331
|
+
console.error(
|
|
6332
|
+
"[CLINIC] Admin does not have permission to update this clinic",
|
|
6333
|
+
{
|
|
6334
|
+
adminId,
|
|
6335
|
+
clinicId,
|
|
6336
|
+
isGroupOwner: admin.isGroupOwner,
|
|
6337
|
+
clinicsManaged: admin.clinicsManaged,
|
|
6338
|
+
isClinicAdmin: clinic.admins && clinic.admins.includes(adminId)
|
|
6339
|
+
}
|
|
6347
6340
|
);
|
|
6348
|
-
|
|
6349
|
-
if (distanceInKm <= radiusInKm) {
|
|
6350
|
-
matchingDocs.push(clinic);
|
|
6351
|
-
}
|
|
6341
|
+
throw new Error("Admin does not have permission to update this clinic");
|
|
6352
6342
|
}
|
|
6343
|
+
console.log("[CLINIC] Admin permissions verified");
|
|
6344
|
+
} catch (adminError) {
|
|
6345
|
+
console.error("[CLINIC] Error verifying admin permissions:", adminError);
|
|
6346
|
+
throw adminError;
|
|
6353
6347
|
}
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
});
|
|
6365
|
-
}
|
|
6366
|
-
|
|
6367
|
-
// src/services/clinic/utils/filter.utils.ts
|
|
6368
|
-
var import_firestore21 = require("firebase/firestore");
|
|
6369
|
-
var import_geofire_common6 = require("geofire-common");
|
|
6370
|
-
async function getClinicsByFilters(db, filters) {
|
|
6371
|
-
console.log(
|
|
6372
|
-
"[FILTER_UTILS] Starting clinic filtering with criteria:",
|
|
6373
|
-
filters
|
|
6374
|
-
);
|
|
6375
|
-
const isGeoQuery = filters.center && filters.radiusInKm && filters.radiusInKm > 0;
|
|
6376
|
-
const constraints = [];
|
|
6377
|
-
if (filters.isActive !== void 0) {
|
|
6378
|
-
constraints.push((0, import_firestore21.where)("isActive", "==", filters.isActive));
|
|
6379
|
-
} else {
|
|
6380
|
-
constraints.push((0, import_firestore21.where)("isActive", "==", true));
|
|
6381
|
-
}
|
|
6382
|
-
if (filters.tags && filters.tags.length > 0) {
|
|
6383
|
-
constraints.push((0, import_firestore21.where)("tags", "array-contains", filters.tags[0]));
|
|
6384
|
-
}
|
|
6385
|
-
if (filters.procedureTechnology) {
|
|
6386
|
-
constraints.push(
|
|
6387
|
-
(0, import_firestore21.where)("servicesInfo.technology", "==", filters.procedureTechnology)
|
|
6388
|
-
);
|
|
6389
|
-
} else if (filters.procedureSubcategory) {
|
|
6390
|
-
constraints.push(
|
|
6391
|
-
(0, import_firestore21.where)("servicesInfo.subCategory", "==", filters.procedureSubcategory)
|
|
6392
|
-
);
|
|
6393
|
-
} else if (filters.procedureCategory) {
|
|
6394
|
-
constraints.push(
|
|
6395
|
-
(0, import_firestore21.where)("servicesInfo.category", "==", filters.procedureCategory)
|
|
6396
|
-
);
|
|
6397
|
-
} else if (filters.procedureFamily) {
|
|
6398
|
-
constraints.push(
|
|
6399
|
-
(0, import_firestore21.where)("servicesInfo.procedureFamily", "==", filters.procedureFamily)
|
|
6400
|
-
);
|
|
6401
|
-
}
|
|
6402
|
-
if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
|
|
6403
|
-
constraints.push((0, import_firestore21.startAfter)(filters.lastDoc));
|
|
6404
|
-
constraints.push((0, import_firestore21.limit)(filters.pagination));
|
|
6405
|
-
} else if (filters.pagination && filters.pagination > 0) {
|
|
6406
|
-
constraints.push((0, import_firestore21.limit)(filters.pagination));
|
|
6407
|
-
}
|
|
6408
|
-
constraints.push((0, import_firestore21.orderBy)("location.geohash"));
|
|
6409
|
-
let clinicsResult = [];
|
|
6410
|
-
let lastVisibleDoc = null;
|
|
6411
|
-
if (isGeoQuery) {
|
|
6412
|
-
const center = filters.center;
|
|
6413
|
-
const radiusInKm = filters.radiusInKm;
|
|
6414
|
-
const bounds = (0, import_geofire_common6.geohashQueryBounds)(
|
|
6415
|
-
[center.latitude, center.longitude],
|
|
6416
|
-
radiusInKm * 1e3
|
|
6417
|
-
// Convert to meters
|
|
6418
|
-
);
|
|
6419
|
-
const matchingClinics = [];
|
|
6420
|
-
for (const bound of bounds) {
|
|
6421
|
-
const geoConstraints = [
|
|
6422
|
-
...constraints,
|
|
6423
|
-
(0, import_firestore21.where)("location.geohash", ">=", bound[0]),
|
|
6424
|
-
(0, import_firestore21.where)("location.geohash", "<=", bound[1])
|
|
6425
|
-
];
|
|
6426
|
-
const q = (0, import_firestore21.query)((0, import_firestore21.collection)(db, CLINICS_COLLECTION), ...geoConstraints);
|
|
6427
|
-
const querySnapshot = await (0, import_firestore21.getDocs)(q);
|
|
6428
|
-
console.log(
|
|
6429
|
-
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
|
|
6348
|
+
let updatedData = { ...data };
|
|
6349
|
+
if (data.logo && typeof data.logo === "string" && data.logo.startsWith("data:")) {
|
|
6350
|
+
console.log("[CLINIC] Processing logo update");
|
|
6351
|
+
try {
|
|
6352
|
+
const logoUrl = await uploadPhoto(
|
|
6353
|
+
data.logo,
|
|
6354
|
+
"clinics",
|
|
6355
|
+
clinicId,
|
|
6356
|
+
"logo",
|
|
6357
|
+
app
|
|
6430
6358
|
);
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
[center.latitude, center.longitude],
|
|
6435
|
-
[clinic.location.latitude, clinic.location.longitude]
|
|
6436
|
-
);
|
|
6437
|
-
const distanceInKm = distance / 1e3;
|
|
6438
|
-
if (distanceInKm <= radiusInKm) {
|
|
6439
|
-
matchingClinics.push({
|
|
6440
|
-
...clinic,
|
|
6441
|
-
distance: distanceInKm
|
|
6442
|
-
});
|
|
6443
|
-
}
|
|
6359
|
+
console.log("[CLINIC] Logo update processed", { logoUrl });
|
|
6360
|
+
if (logoUrl !== null) {
|
|
6361
|
+
updatedData.logo = logoUrl;
|
|
6444
6362
|
}
|
|
6363
|
+
} catch (logoError) {
|
|
6364
|
+
console.error("[CLINIC] Error processing logo update:", logoError);
|
|
6445
6365
|
}
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
if (filters.maxRating !== void 0) {
|
|
6458
|
-
filteredClinics = filteredClinics.filter(
|
|
6459
|
-
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6460
|
-
);
|
|
6461
|
-
}
|
|
6462
|
-
filteredClinics.sort((a, b) => a.distance - b.distance);
|
|
6463
|
-
if (filters.pagination && filters.pagination > 0) {
|
|
6464
|
-
let startIndex = 0;
|
|
6465
|
-
if (filters.lastDoc) {
|
|
6466
|
-
const lastDocIndex = filteredClinics.findIndex(
|
|
6467
|
-
(clinic) => clinic.id === filters.lastDoc.id
|
|
6366
|
+
}
|
|
6367
|
+
if (data.coverPhoto) {
|
|
6368
|
+
console.log("[CLINIC] Processing cover photo update");
|
|
6369
|
+
try {
|
|
6370
|
+
if (typeof data.coverPhoto === "string" && data.coverPhoto.startsWith("data:")) {
|
|
6371
|
+
const uploadedPhoto = await uploadPhoto(
|
|
6372
|
+
data.coverPhoto,
|
|
6373
|
+
"clinics",
|
|
6374
|
+
clinicId,
|
|
6375
|
+
"cover",
|
|
6376
|
+
app
|
|
6468
6377
|
);
|
|
6469
|
-
if (
|
|
6470
|
-
|
|
6378
|
+
if (uploadedPhoto) {
|
|
6379
|
+
updatedData.coverPhoto = uploadedPhoto;
|
|
6471
6380
|
}
|
|
6381
|
+
} else {
|
|
6382
|
+
updatedData.coverPhoto = data.coverPhoto;
|
|
6472
6383
|
}
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6384
|
+
console.log("[CLINIC] Cover photo update processed");
|
|
6385
|
+
} catch (photoError) {
|
|
6386
|
+
console.error(
|
|
6387
|
+
"[CLINIC] Error processing cover photo update:",
|
|
6388
|
+
photoError
|
|
6476
6389
|
);
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
clinicsResult = filteredClinics;
|
|
6390
|
+
if (typeof data.coverPhoto === "string" && !data.coverPhoto.startsWith("data:")) {
|
|
6391
|
+
updatedData.coverPhoto = data.coverPhoto;
|
|
6392
|
+
}
|
|
6481
6393
|
}
|
|
6482
|
-
}
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6394
|
+
}
|
|
6395
|
+
if (data.featuredPhotos && data.featuredPhotos.length > 0) {
|
|
6396
|
+
console.log("[CLINIC] Processing featured photos update");
|
|
6397
|
+
try {
|
|
6398
|
+
const dataUrlPhotos = data.featuredPhotos.filter(
|
|
6399
|
+
(photo) => typeof photo === "string" && photo.startsWith("data:")
|
|
6400
|
+
);
|
|
6401
|
+
const existingPhotos = data.featuredPhotos.filter(
|
|
6402
|
+
(photo) => typeof photo === "string" && !photo.startsWith("data:")
|
|
6403
|
+
);
|
|
6404
|
+
if (dataUrlPhotos.length > 0) {
|
|
6405
|
+
const uploadedPhotos = await uploadMultiplePhotos(
|
|
6406
|
+
dataUrlPhotos,
|
|
6407
|
+
"clinics",
|
|
6408
|
+
clinicId,
|
|
6409
|
+
"featured",
|
|
6410
|
+
app
|
|
6499
6411
|
);
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
distance: distance / 1e3
|
|
6503
|
-
// Convert to kilometers
|
|
6412
|
+
console.log("[CLINIC] Featured photos update processed", {
|
|
6413
|
+
count: uploadedPhotos.length
|
|
6504
6414
|
});
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6415
|
+
updatedData.featuredPhotos = [...existingPhotos, ...uploadedPhotos];
|
|
6416
|
+
} else {
|
|
6417
|
+
updatedData.featuredPhotos = existingPhotos;
|
|
6418
|
+
}
|
|
6419
|
+
} catch (featuredError) {
|
|
6420
|
+
console.error(
|
|
6421
|
+
"[CLINIC] Error processing featured photos update:",
|
|
6422
|
+
featuredError
|
|
6423
|
+
);
|
|
6424
|
+
updatedData.featuredPhotos = data.featuredPhotos.filter(
|
|
6425
|
+
(photo) => typeof photo === "string" && !photo.startsWith("data:")
|
|
6509
6426
|
);
|
|
6510
6427
|
}
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6428
|
+
}
|
|
6429
|
+
if (data.photosWithTags && data.photosWithTags.length > 0) {
|
|
6430
|
+
console.log("[CLINIC] Processing photos with tags update");
|
|
6431
|
+
try {
|
|
6432
|
+
const updatedPhotosWithTags = [];
|
|
6433
|
+
for (const photoWithTag of data.photosWithTags) {
|
|
6434
|
+
if (photoWithTag.url && photoWithTag.url.startsWith("data:")) {
|
|
6435
|
+
const uploadedUrl = await uploadPhoto(
|
|
6436
|
+
photoWithTag.url,
|
|
6437
|
+
"clinics",
|
|
6438
|
+
clinicId,
|
|
6439
|
+
`tagged-${photoWithTag.tag}`,
|
|
6440
|
+
app
|
|
6441
|
+
);
|
|
6442
|
+
if (uploadedUrl) {
|
|
6443
|
+
updatedPhotosWithTags.push({
|
|
6444
|
+
url: uploadedUrl,
|
|
6445
|
+
tag: photoWithTag.tag
|
|
6446
|
+
});
|
|
6447
|
+
}
|
|
6448
|
+
} else {
|
|
6449
|
+
updatedPhotosWithTags.push(photoWithTag);
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
updatedData.photosWithTags = updatedPhotosWithTags;
|
|
6453
|
+
console.log("[CLINIC] Photos with tags update processed", {
|
|
6454
|
+
count: updatedPhotosWithTags.length
|
|
6514
6455
|
});
|
|
6515
|
-
}
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6456
|
+
} catch (tagsError) {
|
|
6457
|
+
console.error(
|
|
6458
|
+
"[CLINIC] Error processing photos with tags update:",
|
|
6459
|
+
tagsError
|
|
6519
6460
|
);
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
filteredClinics = filteredClinics.filter(
|
|
6523
|
-
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6461
|
+
updatedData.photosWithTags = data.photosWithTags.filter(
|
|
6462
|
+
(photo) => !photo.url.startsWith("data:")
|
|
6524
6463
|
);
|
|
6525
6464
|
}
|
|
6526
|
-
lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
6527
|
-
clinicsResult = filteredClinics;
|
|
6528
6465
|
}
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6466
|
+
updatedData = {
|
|
6467
|
+
...updatedData,
|
|
6468
|
+
updatedAt: import_firestore21.Timestamp.now()
|
|
6532
6469
|
};
|
|
6470
|
+
console.log("[CLINIC] Updating clinic in Firestore");
|
|
6471
|
+
try {
|
|
6472
|
+
await (0, import_firestore21.updateDoc)((0, import_firestore21.doc)(db, CLINICS_COLLECTION, clinicId), updatedData);
|
|
6473
|
+
console.log("[CLINIC] Clinic updated successfully");
|
|
6474
|
+
} catch (updateError) {
|
|
6475
|
+
console.error("[CLINIC] Error updating clinic in Firestore:", updateError);
|
|
6476
|
+
throw updateError;
|
|
6477
|
+
}
|
|
6478
|
+
const updatedClinic = await getClinic(db, clinicId);
|
|
6479
|
+
if (!updatedClinic) {
|
|
6480
|
+
console.error("[CLINIC] Failed to retrieve updated clinic");
|
|
6481
|
+
throw new Error("Failed to retrieve updated clinic");
|
|
6482
|
+
}
|
|
6483
|
+
console.log("[CLINIC] Clinic update completed successfully");
|
|
6484
|
+
return updatedClinic;
|
|
6533
6485
|
}
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
var import_firestore23 = require("firebase/firestore");
|
|
6539
|
-
var MediaAccessLevel = /* @__PURE__ */ ((MediaAccessLevel2) => {
|
|
6540
|
-
MediaAccessLevel2["PUBLIC"] = "public";
|
|
6541
|
-
MediaAccessLevel2["PRIVATE"] = "private";
|
|
6542
|
-
MediaAccessLevel2["CONFIDENTIAL"] = "confidential";
|
|
6543
|
-
return MediaAccessLevel2;
|
|
6544
|
-
})(MediaAccessLevel || {});
|
|
6545
|
-
var MEDIA_METADATA_COLLECTION = "media_metadata";
|
|
6546
|
-
var MediaService = class extends BaseService {
|
|
6547
|
-
constructor(db, auth, app) {
|
|
6548
|
-
super(db, auth, app);
|
|
6486
|
+
async function getClinicsByAdmin(db, adminId, options = {}, clinicAdminService, clinicGroupService) {
|
|
6487
|
+
const admin = await clinicAdminService.getClinicAdmin(adminId);
|
|
6488
|
+
if (!admin) {
|
|
6489
|
+
throw new Error("Admin not found");
|
|
6549
6490
|
}
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
* @param collectionName - The logical collection name this media belongs to (e.g., 'patient_profile_pictures', 'clinic_logos').
|
|
6556
|
-
* @param originalFileName - Optional: the original name of the file, if not using file.name.
|
|
6557
|
-
* @returns Promise with the media metadata.
|
|
6558
|
-
*/
|
|
6559
|
-
async uploadMedia(file, ownerId, accessLevel, collectionName, originalFileName) {
|
|
6560
|
-
const mediaId = this.generateId();
|
|
6561
|
-
const fileNameToUse = originalFileName || (file instanceof File ? file.name : file.toString());
|
|
6562
|
-
const uniqueFileName = `${mediaId}-${fileNameToUse}`;
|
|
6563
|
-
const filePath = `media/${accessLevel}/${ownerId}/${collectionName}/${uniqueFileName}`;
|
|
6564
|
-
console.log(`[MediaService] Uploading file to: ${filePath}`);
|
|
6565
|
-
const storageRef = (0, import_storage5.ref)(this.storage, filePath);
|
|
6566
|
-
try {
|
|
6567
|
-
const uploadResult = await (0, import_storage5.uploadBytes)(storageRef, file, {
|
|
6568
|
-
contentType: file.type
|
|
6569
|
-
});
|
|
6570
|
-
console.log("[MediaService] File uploaded successfully", uploadResult);
|
|
6571
|
-
const downloadURL = await (0, import_storage5.getDownloadURL)(uploadResult.ref);
|
|
6572
|
-
console.log("[MediaService] Got download URL:", downloadURL);
|
|
6573
|
-
const metadata = {
|
|
6574
|
-
id: mediaId,
|
|
6575
|
-
name: fileNameToUse,
|
|
6576
|
-
url: downloadURL,
|
|
6577
|
-
contentType: file.type,
|
|
6578
|
-
size: file.size,
|
|
6579
|
-
createdAt: import_firestore22.Timestamp.now(),
|
|
6580
|
-
accessLevel,
|
|
6581
|
-
ownerId,
|
|
6582
|
-
collectionName,
|
|
6583
|
-
path: filePath
|
|
6584
|
-
};
|
|
6585
|
-
const metadataDocRef = (0, import_firestore23.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
|
|
6586
|
-
await (0, import_firestore23.setDoc)(metadataDocRef, metadata);
|
|
6587
|
-
console.log("[MediaService] Metadata stored in Firestore:", mediaId);
|
|
6588
|
-
return metadata;
|
|
6589
|
-
} catch (error) {
|
|
6590
|
-
console.error("[MediaService] Error during media upload:", error);
|
|
6591
|
-
throw error;
|
|
6491
|
+
let clinicIds = [...admin.clinicsManaged];
|
|
6492
|
+
if (admin.isGroupOwner && options.includeGroupClinics) {
|
|
6493
|
+
const group = await clinicGroupService.getClinicGroup(admin.clinicGroupId);
|
|
6494
|
+
if (group) {
|
|
6495
|
+
clinicIds = [.../* @__PURE__ */ new Set([...clinicIds, ...group.clinics])];
|
|
6592
6496
|
}
|
|
6593
6497
|
}
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6498
|
+
if (clinicIds.length === 0) {
|
|
6499
|
+
return [];
|
|
6500
|
+
}
|
|
6501
|
+
const constraints = [(0, import_firestore21.where)("id", "in", clinicIds)];
|
|
6502
|
+
if (options.isActive !== void 0) {
|
|
6503
|
+
constraints.push((0, import_firestore21.where)("isActive", "==", options.isActive));
|
|
6504
|
+
}
|
|
6505
|
+
const q = (0, import_firestore21.query)((0, import_firestore21.collection)(db, CLINICS_COLLECTION), ...constraints);
|
|
6506
|
+
const querySnapshot = await (0, import_firestore21.getDocs)(q);
|
|
6507
|
+
return querySnapshot.docs.map((doc34) => doc34.data());
|
|
6508
|
+
}
|
|
6509
|
+
async function getActiveClinicsByAdmin(db, adminId, clinicAdminService, clinicGroupService) {
|
|
6510
|
+
return getClinicsByAdmin(
|
|
6511
|
+
db,
|
|
6512
|
+
adminId,
|
|
6513
|
+
{ isActive: true },
|
|
6514
|
+
clinicAdminService,
|
|
6515
|
+
clinicGroupService
|
|
6516
|
+
);
|
|
6517
|
+
}
|
|
6518
|
+
async function getClinicById(db, clinicId) {
|
|
6519
|
+
try {
|
|
6520
|
+
const clinicRef = (0, import_firestore21.doc)(db, CLINICS_COLLECTION, clinicId);
|
|
6521
|
+
const clinicSnapshot = await (0, import_firestore21.getDoc)(clinicRef);
|
|
6522
|
+
if (!clinicSnapshot.exists()) {
|
|
6523
|
+
return null;
|
|
6606
6524
|
}
|
|
6607
|
-
|
|
6608
|
-
return
|
|
6525
|
+
const clinicData = clinicSnapshot.data();
|
|
6526
|
+
return {
|
|
6527
|
+
...clinicData,
|
|
6528
|
+
id: clinicSnapshot.id
|
|
6529
|
+
};
|
|
6530
|
+
} catch (error) {
|
|
6531
|
+
console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
|
|
6532
|
+
throw error;
|
|
6609
6533
|
}
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
if (!querySnapshot.empty) {
|
|
6625
|
-
const metadata = querySnapshot.docs[0].data();
|
|
6626
|
-
console.log("[MediaService] Metadata found by URL:", metadata);
|
|
6627
|
-
return metadata;
|
|
6534
|
+
}
|
|
6535
|
+
async function getAllClinics(db, pagination, lastDoc) {
|
|
6536
|
+
try {
|
|
6537
|
+
const clinicsCollection = (0, import_firestore21.collection)(db, CLINICS_COLLECTION);
|
|
6538
|
+
let clinicsQuery = (0, import_firestore21.query)(clinicsCollection);
|
|
6539
|
+
if (pagination && pagination > 0) {
|
|
6540
|
+
if (lastDoc) {
|
|
6541
|
+
clinicsQuery = (0, import_firestore21.query)(
|
|
6542
|
+
clinicsCollection,
|
|
6543
|
+
(0, import_firestore21.startAfter)(lastDoc),
|
|
6544
|
+
(0, import_firestore21.limit)(pagination)
|
|
6545
|
+
);
|
|
6546
|
+
} else {
|
|
6547
|
+
clinicsQuery = (0, import_firestore21.query)(clinicsCollection, (0, import_firestore21.limit)(pagination));
|
|
6628
6548
|
}
|
|
6629
|
-
console.log("[MediaService] No metadata found for URL:", url);
|
|
6630
|
-
return null;
|
|
6631
|
-
} catch (error) {
|
|
6632
|
-
console.error("[MediaService] Error fetching metadata by URL:", error);
|
|
6633
|
-
throw error;
|
|
6634
6549
|
}
|
|
6550
|
+
const clinicsSnapshot = await (0, import_firestore21.getDocs)(clinicsQuery);
|
|
6551
|
+
const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
|
|
6552
|
+
const clinics = clinicsSnapshot.docs.map((doc34) => {
|
|
6553
|
+
const data = doc34.data();
|
|
6554
|
+
return {
|
|
6555
|
+
...data,
|
|
6556
|
+
id: doc34.id
|
|
6557
|
+
};
|
|
6558
|
+
});
|
|
6559
|
+
return {
|
|
6560
|
+
clinics,
|
|
6561
|
+
lastDoc: lastVisible
|
|
6562
|
+
};
|
|
6563
|
+
} catch (error) {
|
|
6564
|
+
console.error("[CLINIC_UTILS] Error getting all clinics:", error);
|
|
6565
|
+
throw error;
|
|
6635
6566
|
}
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6567
|
+
}
|
|
6568
|
+
async function getAllClinicsInRange(db, center, rangeInKm, pagination, lastDoc) {
|
|
6569
|
+
const bounds = (0, import_geofire_common4.geohashQueryBounds)(
|
|
6570
|
+
[center.latitude, center.longitude],
|
|
6571
|
+
rangeInKm * 1e3
|
|
6572
|
+
);
|
|
6573
|
+
const matchingClinics = [];
|
|
6574
|
+
let lastDocSnapshot = null;
|
|
6575
|
+
for (const b of bounds) {
|
|
6576
|
+
const constraints = [
|
|
6577
|
+
(0, import_firestore21.where)("location.geohash", ">=", b[0]),
|
|
6578
|
+
(0, import_firestore21.where)("location.geohash", "<=", b[1]),
|
|
6579
|
+
(0, import_firestore21.where)("isActive", "==", true)
|
|
6580
|
+
];
|
|
6581
|
+
const q = (0, import_firestore21.query)((0, import_firestore21.collection)(db, CLINICS_COLLECTION), ...constraints);
|
|
6582
|
+
const querySnapshot = await (0, import_firestore21.getDocs)(q);
|
|
6583
|
+
for (const doc34 of querySnapshot.docs) {
|
|
6584
|
+
const clinic = doc34.data();
|
|
6585
|
+
const distance = (0, import_geofire_common4.distanceBetween)(
|
|
6586
|
+
[center.latitude, center.longitude],
|
|
6587
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
6646
6588
|
);
|
|
6647
|
-
|
|
6589
|
+
const distanceInKm = distance / 1e3;
|
|
6590
|
+
if (distanceInKm <= rangeInKm) {
|
|
6591
|
+
matchingClinics.push({
|
|
6592
|
+
...clinic,
|
|
6593
|
+
distance: distanceInKm
|
|
6594
|
+
});
|
|
6595
|
+
}
|
|
6648
6596
|
}
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
`[MediaService] Metadata deleted from Firestore for ID: ${mediaId}`
|
|
6597
|
+
}
|
|
6598
|
+
matchingClinics.sort((a, b) => a.distance - b.distance);
|
|
6599
|
+
if (pagination && pagination > 0) {
|
|
6600
|
+
let result = matchingClinics;
|
|
6601
|
+
if (lastDoc && matchingClinics.length > 0) {
|
|
6602
|
+
const lastIndex = matchingClinics.findIndex(
|
|
6603
|
+
(clinic) => clinic.id === lastDoc.id
|
|
6657
6604
|
);
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6605
|
+
if (lastIndex !== -1) {
|
|
6606
|
+
result = matchingClinics.slice(lastIndex + 1);
|
|
6607
|
+
}
|
|
6661
6608
|
}
|
|
6609
|
+
const paginatedClinics = result.slice(0, pagination);
|
|
6610
|
+
const newLastDoc = paginatedClinics.length > 0 ? paginatedClinics[paginatedClinics.length - 1] : null;
|
|
6611
|
+
return {
|
|
6612
|
+
clinics: paginatedClinics,
|
|
6613
|
+
lastDoc: newLastDoc
|
|
6614
|
+
};
|
|
6662
6615
|
}
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6616
|
+
return {
|
|
6617
|
+
clinics: matchingClinics,
|
|
6618
|
+
lastDoc: null
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6622
|
+
// src/services/clinic/utils/tag.utils.ts
|
|
6623
|
+
async function addTags(db, clinicId, adminId, newTags, clinicAdminService, app) {
|
|
6624
|
+
const clinic = await getClinic(db, clinicId);
|
|
6625
|
+
if (!clinic) {
|
|
6626
|
+
throw new Error("Clinic not found");
|
|
6627
|
+
}
|
|
6628
|
+
const admin = await clinicAdminService.getClinicAdmin(adminId);
|
|
6629
|
+
if (!admin) {
|
|
6630
|
+
throw new Error("Admin not found");
|
|
6631
|
+
}
|
|
6632
|
+
const hasPermission = admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId || admin.clinicsManaged.includes(clinicId) && clinic.admins && clinic.admins.includes(adminId);
|
|
6633
|
+
if (!hasPermission) {
|
|
6634
|
+
throw new Error("Admin does not have permission to update this clinic");
|
|
6635
|
+
}
|
|
6636
|
+
const updatedTags = [.../* @__PURE__ */ new Set([...clinic.tags, ...newTags.tags || []])];
|
|
6637
|
+
return updateClinic(
|
|
6638
|
+
db,
|
|
6639
|
+
clinicId,
|
|
6640
|
+
{
|
|
6641
|
+
tags: updatedTags
|
|
6642
|
+
},
|
|
6643
|
+
adminId,
|
|
6644
|
+
clinicAdminService,
|
|
6645
|
+
app
|
|
6646
|
+
);
|
|
6647
|
+
}
|
|
6648
|
+
async function removeTags(db, clinicId, adminId, tagsToRemove, clinicAdminService, app) {
|
|
6649
|
+
const clinic = await getClinic(db, clinicId);
|
|
6650
|
+
if (!clinic) {
|
|
6651
|
+
throw new Error("Clinic not found");
|
|
6652
|
+
}
|
|
6653
|
+
const admin = await clinicAdminService.getClinicAdmin(adminId);
|
|
6654
|
+
if (!admin) {
|
|
6655
|
+
throw new Error("Admin not found");
|
|
6656
|
+
}
|
|
6657
|
+
const hasPermission = admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId || admin.clinicsManaged.includes(clinicId) && clinic.admins && clinic.admins.includes(adminId);
|
|
6658
|
+
if (!hasPermission) {
|
|
6659
|
+
throw new Error("Admin does not have permission to update this clinic");
|
|
6660
|
+
}
|
|
6661
|
+
const updatedTags = clinic.tags.filter(
|
|
6662
|
+
(tag) => !tagsToRemove.tags || !tagsToRemove.tags.includes(tag)
|
|
6663
|
+
);
|
|
6664
|
+
return updateClinic(
|
|
6665
|
+
db,
|
|
6666
|
+
clinicId,
|
|
6667
|
+
{
|
|
6668
|
+
tags: updatedTags
|
|
6669
|
+
},
|
|
6670
|
+
adminId,
|
|
6671
|
+
clinicAdminService,
|
|
6672
|
+
app
|
|
6673
|
+
);
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6676
|
+
// src/services/clinic/utils/search.utils.ts
|
|
6677
|
+
var import_firestore22 = require("firebase/firestore");
|
|
6678
|
+
var import_geofire_common5 = require("geofire-common");
|
|
6679
|
+
async function findClinicsInRadius(db, center, radiusInKm, filters) {
|
|
6680
|
+
const bounds = (0, import_geofire_common5.geohashQueryBounds)(
|
|
6681
|
+
[center.latitude, center.longitude],
|
|
6682
|
+
radiusInKm * 1e3
|
|
6683
|
+
);
|
|
6684
|
+
const matchingDocs = [];
|
|
6685
|
+
for (const b of bounds) {
|
|
6686
|
+
const constraints = [
|
|
6687
|
+
(0, import_firestore22.where)("location.geohash", ">=", b[0]),
|
|
6688
|
+
(0, import_firestore22.where)("location.geohash", "<=", b[1]),
|
|
6689
|
+
(0, import_firestore22.where)("isActive", "==", true)
|
|
6690
|
+
];
|
|
6691
|
+
if (filters == null ? void 0 : filters.services) {
|
|
6692
|
+
constraints.push(
|
|
6693
|
+
(0, import_firestore22.where)("services", "array-contains-any", filters.services)
|
|
6679
6694
|
);
|
|
6680
|
-
return null;
|
|
6681
6695
|
}
|
|
6682
|
-
if (
|
|
6683
|
-
|
|
6684
|
-
|
|
6696
|
+
if ((filters == null ? void 0 : filters.tags) && filters.tags.length > 0) {
|
|
6697
|
+
constraints.push((0, import_firestore22.where)("tags", "array-contains-any", filters.tags));
|
|
6698
|
+
}
|
|
6699
|
+
const q = (0, import_firestore22.query)((0, import_firestore22.collection)(db, CLINICS_COLLECTION), ...constraints);
|
|
6700
|
+
const querySnapshot = await (0, import_firestore22.getDocs)(q);
|
|
6701
|
+
for (const doc34 of querySnapshot.docs) {
|
|
6702
|
+
const clinic = doc34.data();
|
|
6703
|
+
const distance = (0, import_geofire_common5.distanceBetween)(
|
|
6704
|
+
[center.latitude, center.longitude],
|
|
6705
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
6685
6706
|
);
|
|
6686
|
-
const
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
return { ...metadata, updatedAt: import_firestore22.Timestamp.now() };
|
|
6690
|
-
} catch (error) {
|
|
6691
|
-
console.error(
|
|
6692
|
-
`[MediaService] Error updating timestamp for media ID ${mediaId}:`,
|
|
6693
|
-
error
|
|
6694
|
-
);
|
|
6695
|
-
throw error;
|
|
6707
|
+
const distanceInKm = distance / 1e3;
|
|
6708
|
+
if (distanceInKm <= radiusInKm) {
|
|
6709
|
+
matchingDocs.push(clinic);
|
|
6696
6710
|
}
|
|
6697
6711
|
}
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
const
|
|
6701
|
-
|
|
6702
|
-
|
|
6712
|
+
}
|
|
6713
|
+
return matchingDocs.sort((a, b) => {
|
|
6714
|
+
const distanceA = (0, import_geofire_common5.distanceBetween)(
|
|
6715
|
+
[center.latitude, center.longitude],
|
|
6716
|
+
[a.location.latitude, a.location.longitude]
|
|
6703
6717
|
);
|
|
6704
|
-
const
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6718
|
+
const distanceB = (0, import_geofire_common5.distanceBetween)(
|
|
6719
|
+
[center.latitude, center.longitude],
|
|
6720
|
+
[b.location.latitude, b.location.longitude]
|
|
6721
|
+
);
|
|
6722
|
+
return distanceA - distanceB;
|
|
6723
|
+
});
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6726
|
+
// src/services/clinic/utils/filter.utils.ts
|
|
6727
|
+
var import_firestore23 = require("firebase/firestore");
|
|
6728
|
+
var import_geofire_common6 = require("geofire-common");
|
|
6729
|
+
async function getClinicsByFilters(db, filters) {
|
|
6730
|
+
console.log(
|
|
6731
|
+
"[FILTER_UTILS] Starting clinic filtering with criteria:",
|
|
6732
|
+
filters
|
|
6733
|
+
);
|
|
6734
|
+
const isGeoQuery = filters.center && filters.radiusInKm && filters.radiusInKm > 0;
|
|
6735
|
+
const constraints = [];
|
|
6736
|
+
if (filters.isActive !== void 0) {
|
|
6737
|
+
constraints.push((0, import_firestore23.where)("isActive", "==", filters.isActive));
|
|
6738
|
+
} else {
|
|
6739
|
+
constraints.push((0, import_firestore23.where)("isActive", "==", true));
|
|
6740
|
+
}
|
|
6741
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
6742
|
+
constraints.push((0, import_firestore23.where)("tags", "array-contains", filters.tags[0]));
|
|
6743
|
+
}
|
|
6744
|
+
if (filters.procedureTechnology) {
|
|
6745
|
+
constraints.push(
|
|
6746
|
+
(0, import_firestore23.where)("servicesInfo.technology", "==", filters.procedureTechnology)
|
|
6747
|
+
);
|
|
6748
|
+
} else if (filters.procedureSubcategory) {
|
|
6749
|
+
constraints.push(
|
|
6750
|
+
(0, import_firestore23.where)("servicesInfo.subCategory", "==", filters.procedureSubcategory)
|
|
6751
|
+
);
|
|
6752
|
+
} else if (filters.procedureCategory) {
|
|
6753
|
+
constraints.push(
|
|
6754
|
+
(0, import_firestore23.where)("servicesInfo.category", "==", filters.procedureCategory)
|
|
6755
|
+
);
|
|
6756
|
+
} else if (filters.procedureFamily) {
|
|
6757
|
+
constraints.push(
|
|
6758
|
+
(0, import_firestore23.where)("servicesInfo.procedureFamily", "==", filters.procedureFamily)
|
|
6759
|
+
);
|
|
6760
|
+
}
|
|
6761
|
+
if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
|
|
6762
|
+
constraints.push((0, import_firestore23.startAfter)(filters.lastDoc));
|
|
6763
|
+
constraints.push((0, import_firestore23.limit)(filters.pagination));
|
|
6764
|
+
} else if (filters.pagination && filters.pagination > 0) {
|
|
6765
|
+
constraints.push((0, import_firestore23.limit)(filters.pagination));
|
|
6766
|
+
}
|
|
6767
|
+
constraints.push((0, import_firestore23.orderBy)("location.geohash"));
|
|
6768
|
+
let clinicsResult = [];
|
|
6769
|
+
let lastVisibleDoc = null;
|
|
6770
|
+
if (isGeoQuery) {
|
|
6771
|
+
const center = filters.center;
|
|
6772
|
+
const radiusInKm = filters.radiusInKm;
|
|
6773
|
+
const bounds = (0, import_geofire_common6.geohashQueryBounds)(
|
|
6774
|
+
[center.latitude, center.longitude],
|
|
6775
|
+
radiusInKm * 1e3
|
|
6776
|
+
// Convert to meters
|
|
6777
|
+
);
|
|
6778
|
+
const matchingClinics = [];
|
|
6779
|
+
for (const bound of bounds) {
|
|
6780
|
+
const geoConstraints = [
|
|
6781
|
+
...constraints,
|
|
6782
|
+
(0, import_firestore23.where)("location.geohash", ">=", bound[0]),
|
|
6783
|
+
(0, import_firestore23.where)("location.geohash", "<=", bound[1])
|
|
6784
|
+
];
|
|
6785
|
+
const q = (0, import_firestore23.query)((0, import_firestore23.collection)(db, CLINICS_COLLECTION), ...geoConstraints);
|
|
6786
|
+
const querySnapshot = await (0, import_firestore23.getDocs)(q);
|
|
6735
6787
|
console.log(
|
|
6736
|
-
`[
|
|
6737
|
-
);
|
|
6738
|
-
try {
|
|
6739
|
-
console.log(`[MediaService] Deleting old file from ${oldStoragePath}`);
|
|
6740
|
-
await (0, import_storage5.deleteObject)(oldStorageFileRef);
|
|
6741
|
-
console.log(
|
|
6742
|
-
`[MediaService] Successfully deleted old file from ${oldStoragePath}`
|
|
6743
|
-
);
|
|
6744
|
-
} catch (deleteError) {
|
|
6745
|
-
console.error(
|
|
6746
|
-
`[MediaService] Failed to delete old file from ${oldStoragePath} for media ID ${mediaId}. This file is now orphaned. Error:`,
|
|
6747
|
-
deleteError
|
|
6748
|
-
);
|
|
6749
|
-
}
|
|
6750
|
-
return { ...metadata, ...updateData };
|
|
6751
|
-
} catch (error) {
|
|
6752
|
-
console.error(
|
|
6753
|
-
`[MediaService] Error updating media access level and moving file for ${mediaId}:`,
|
|
6754
|
-
error
|
|
6788
|
+
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
|
|
6755
6789
|
);
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6790
|
+
for (const doc34 of querySnapshot.docs) {
|
|
6791
|
+
const clinic = { ...doc34.data(), id: doc34.id };
|
|
6792
|
+
const distance = (0, import_geofire_common6.distanceBetween)(
|
|
6793
|
+
[center.latitude, center.longitude],
|
|
6794
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
6759
6795
|
);
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
console.error(
|
|
6767
|
-
`[MediaService] Failed to cleanup partially uploaded file at ${newStoragePath}:`,
|
|
6768
|
-
cleanupError
|
|
6769
|
-
);
|
|
6796
|
+
const distanceInKm = distance / 1e3;
|
|
6797
|
+
if (distanceInKm <= radiusInKm) {
|
|
6798
|
+
matchingClinics.push({
|
|
6799
|
+
...clinic,
|
|
6800
|
+
distance: distanceInKm
|
|
6801
|
+
});
|
|
6770
6802
|
}
|
|
6771
6803
|
}
|
|
6772
|
-
throw error;
|
|
6773
6804
|
}
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
* @param accessLevel - Optional: Filter by access level.
|
|
6780
|
-
* @param count - Optional: Number of items to fetch.
|
|
6781
|
-
* @param startAfterId - Optional: ID of the document to start after (for pagination).
|
|
6782
|
-
*/
|
|
6783
|
-
async listMedia(ownerId, collectionName, accessLevel, count, startAfterId) {
|
|
6784
|
-
console.log(`[MediaService] Listing media for owner: ${ownerId}`);
|
|
6785
|
-
let qConstraints = [(0, import_firestore23.where)("ownerId", "==", ownerId)];
|
|
6786
|
-
if (collectionName) {
|
|
6787
|
-
qConstraints.push((0, import_firestore23.where)("collectionName", "==", collectionName));
|
|
6805
|
+
let filteredClinics = matchingClinics;
|
|
6806
|
+
if (filters.tags && filters.tags.length > 1) {
|
|
6807
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
6808
|
+
return filters.tags.every((tag) => clinic.tags.includes(tag));
|
|
6809
|
+
});
|
|
6788
6810
|
}
|
|
6789
|
-
if (
|
|
6790
|
-
|
|
6811
|
+
if (filters.minRating !== void 0) {
|
|
6812
|
+
filteredClinics = filteredClinics.filter(
|
|
6813
|
+
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating
|
|
6814
|
+
);
|
|
6791
6815
|
}
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6816
|
+
if (filters.maxRating !== void 0) {
|
|
6817
|
+
filteredClinics = filteredClinics.filter(
|
|
6818
|
+
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6819
|
+
);
|
|
6795
6820
|
}
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6821
|
+
filteredClinics.sort((a, b) => a.distance - b.distance);
|
|
6822
|
+
if (filters.pagination && filters.pagination > 0) {
|
|
6823
|
+
let startIndex = 0;
|
|
6824
|
+
if (filters.lastDoc) {
|
|
6825
|
+
const lastDocIndex = filteredClinics.findIndex(
|
|
6826
|
+
(clinic) => clinic.id === filters.lastDoc.id
|
|
6827
|
+
);
|
|
6828
|
+
if (lastDocIndex !== -1) {
|
|
6829
|
+
startIndex = lastDocIndex + 1;
|
|
6830
|
+
}
|
|
6799
6831
|
}
|
|
6832
|
+
const paginatedClinics = filteredClinics.slice(
|
|
6833
|
+
startIndex,
|
|
6834
|
+
startIndex + filters.pagination
|
|
6835
|
+
);
|
|
6836
|
+
lastVisibleDoc = paginatedClinics.length > 0 ? paginatedClinics[paginatedClinics.length - 1] : null;
|
|
6837
|
+
clinicsResult = paginatedClinics;
|
|
6838
|
+
} else {
|
|
6839
|
+
clinicsResult = filteredClinics;
|
|
6800
6840
|
}
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6841
|
+
} else {
|
|
6842
|
+
const q = (0, import_firestore23.query)((0, import_firestore23.collection)(db, CLINICS_COLLECTION), ...constraints);
|
|
6843
|
+
const querySnapshot = await (0, import_firestore23.getDocs)(q);
|
|
6844
|
+
console.log(
|
|
6845
|
+
`[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
|
|
6804
6846
|
);
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6847
|
+
const clinics = querySnapshot.docs.map((doc34) => {
|
|
6848
|
+
return { ...doc34.data(), id: doc34.id };
|
|
6849
|
+
});
|
|
6850
|
+
let filteredClinics = clinics;
|
|
6851
|
+
if (filters.center) {
|
|
6852
|
+
const center = filters.center;
|
|
6853
|
+
const clinicsWithDistance = [];
|
|
6854
|
+
filteredClinics.forEach((clinic) => {
|
|
6855
|
+
const distance = (0, import_geofire_common6.distanceBetween)(
|
|
6856
|
+
[center.latitude, center.longitude],
|
|
6857
|
+
[clinic.location.latitude, clinic.location.longitude]
|
|
6858
|
+
);
|
|
6859
|
+
clinicsWithDistance.push({
|
|
6860
|
+
...clinic,
|
|
6861
|
+
distance: distance / 1e3
|
|
6862
|
+
// Convert to kilometers
|
|
6863
|
+
});
|
|
6864
|
+
});
|
|
6865
|
+
filteredClinics = clinicsWithDistance;
|
|
6866
|
+
filteredClinics.sort(
|
|
6867
|
+
(a, b) => a.distance - b.distance
|
|
6809
6868
|
);
|
|
6810
|
-
console.log(`[MediaService] Found ${mediaList.length} media items.`);
|
|
6811
|
-
return mediaList;
|
|
6812
|
-
} catch (error) {
|
|
6813
|
-
console.error("[MediaService] Error listing media:", error);
|
|
6814
|
-
throw error;
|
|
6815
6869
|
}
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
*/
|
|
6821
|
-
async getMediaDownloadUrl(mediaId) {
|
|
6822
|
-
console.log(`[MediaService] Getting download URL for media ID: ${mediaId}`);
|
|
6823
|
-
const metadata = await this.getMediaMetadata(mediaId);
|
|
6824
|
-
if (metadata && metadata.url) {
|
|
6825
|
-
console.log(`[MediaService] URL found: ${metadata.url}`);
|
|
6826
|
-
return metadata.url;
|
|
6870
|
+
if (filters.tags && filters.tags.length > 1) {
|
|
6871
|
+
filteredClinics = filteredClinics.filter((clinic) => {
|
|
6872
|
+
return filters.tags.every((tag) => clinic.tags.includes(tag));
|
|
6873
|
+
});
|
|
6827
6874
|
}
|
|
6828
|
-
|
|
6829
|
-
|
|
6875
|
+
if (filters.minRating !== void 0) {
|
|
6876
|
+
filteredClinics = filteredClinics.filter(
|
|
6877
|
+
(clinic) => clinic.reviewInfo.averageRating >= filters.minRating
|
|
6878
|
+
);
|
|
6879
|
+
}
|
|
6880
|
+
if (filters.maxRating !== void 0) {
|
|
6881
|
+
filteredClinics = filteredClinics.filter(
|
|
6882
|
+
(clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
|
|
6883
|
+
);
|
|
6884
|
+
}
|
|
6885
|
+
lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
|
|
6886
|
+
clinicsResult = filteredClinics;
|
|
6830
6887
|
}
|
|
6831
|
-
|
|
6888
|
+
return {
|
|
6889
|
+
clinics: clinicsResult,
|
|
6890
|
+
lastDoc: lastVisibleDoc
|
|
6891
|
+
};
|
|
6892
|
+
}
|
|
6832
6893
|
|
|
6833
6894
|
// src/services/clinic/clinic.service.ts
|
|
6834
6895
|
var ClinicService = class extends BaseService {
|
|
@@ -8533,7 +8594,8 @@ var ProcedureService = class extends BaseService {
|
|
|
8533
8594
|
id: practitionerSnapshot.id,
|
|
8534
8595
|
name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
|
|
8535
8596
|
description: practitioner.basicInfo.bio || "",
|
|
8536
|
-
photo: practitioner.basicInfo.profileImageUrl
|
|
8597
|
+
photo: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : "",
|
|
8598
|
+
// Default to empty string if not a processed URL
|
|
8537
8599
|
rating: ((_a = practitioner.reviewInfo) == null ? void 0 : _a.averageRating) || 0,
|
|
8538
8600
|
services: practitioner.procedures || []
|
|
8539
8601
|
};
|
|
@@ -8667,7 +8729,8 @@ var ProcedureService = class extends BaseService {
|
|
|
8667
8729
|
id: newPractitioner.id,
|
|
8668
8730
|
name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
|
|
8669
8731
|
description: newPractitioner.basicInfo.bio || "",
|
|
8670
|
-
photo: newPractitioner.basicInfo.profileImageUrl
|
|
8732
|
+
photo: typeof newPractitioner.basicInfo.profileImageUrl === "string" ? newPractitioner.basicInfo.profileImageUrl : "",
|
|
8733
|
+
// Default to empty string if not a processed URL
|
|
8671
8734
|
rating: ((_a = newPractitioner.reviewInfo) == null ? void 0 : _a.averageRating) || 0,
|
|
8672
8735
|
services: newPractitioner.procedures || []
|
|
8673
8736
|
};
|
|
@@ -9627,37 +9690,30 @@ var FilledDocumentService = class extends BaseService {
|
|
|
9627
9690
|
return { documents: [], lastDoc: null };
|
|
9628
9691
|
}
|
|
9629
9692
|
/**
|
|
9630
|
-
* Upload a file
|
|
9631
|
-
*
|
|
9632
|
-
*
|
|
9633
|
-
* @param
|
|
9634
|
-
* @param
|
|
9635
|
-
* @param
|
|
9636
|
-
* @param
|
|
9637
|
-
* @returns The
|
|
9693
|
+
* Upload a file for a filled document field without updating the document.
|
|
9694
|
+
* This method only handles the upload and returns the file value to be used by the UI.
|
|
9695
|
+
*
|
|
9696
|
+
* @param appointmentId - ID of the appointment
|
|
9697
|
+
* @param formId - ID of the filled document
|
|
9698
|
+
* @param isUserForm - Boolean indicating if it's a user form or doctor form
|
|
9699
|
+
* @param file - The file to upload
|
|
9700
|
+
* @returns The file value object to be stored in the document
|
|
9638
9701
|
*/
|
|
9639
|
-
async uploadFileForFilledDocument(appointmentId, formId, isUserForm, file
|
|
9702
|
+
async uploadFileForFilledDocument(appointmentId, formId, isUserForm, file) {
|
|
9640
9703
|
console.log(
|
|
9641
|
-
`[FilledDocumentService] Uploading file for
|
|
9642
|
-
);
|
|
9643
|
-
const existingDoc = await this.getFilledDocumentFromAppointmentById(
|
|
9644
|
-
appointmentId,
|
|
9645
|
-
formId,
|
|
9646
|
-
isUserForm
|
|
9704
|
+
`[FilledDocumentService] Uploading file for form ${formId} in appointment ${appointmentId}`
|
|
9647
9705
|
);
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
}
|
|
9653
|
-
const ownerId = existingDoc.patientId;
|
|
9654
|
-
const collectionName = isUserForm ? "patient_forms_files" : "doctor_forms_files";
|
|
9706
|
+
const fileId = this.generateId();
|
|
9707
|
+
const formType = isUserForm ? "user-form" : "doctor-form";
|
|
9708
|
+
const collectionName = `${formType}/${formId}`;
|
|
9709
|
+
const accessLevel = "confidential" /* CONFIDENTIAL */;
|
|
9655
9710
|
const mediaMetadata = await this.mediaService.uploadMedia(
|
|
9656
9711
|
file,
|
|
9657
|
-
|
|
9712
|
+
appointmentId,
|
|
9713
|
+
// Using appointmentId as ownerId
|
|
9658
9714
|
accessLevel,
|
|
9659
9715
|
collectionName,
|
|
9660
|
-
file instanceof File ? file.name :
|
|
9716
|
+
file instanceof File ? file.name : `file_${fileId}`
|
|
9661
9717
|
);
|
|
9662
9718
|
const fileValue = {
|
|
9663
9719
|
mediaId: mediaMetadata.id,
|
|
@@ -9667,118 +9723,49 @@ var FilledDocumentService = class extends BaseService {
|
|
|
9667
9723
|
size: mediaMetadata.size,
|
|
9668
9724
|
uploadedAt: Date.now()
|
|
9669
9725
|
};
|
|
9670
|
-
|
|
9671
|
-
[fieldId]: fileValue
|
|
9672
|
-
};
|
|
9673
|
-
return this.updateFilledDocumentInAppointment(
|
|
9674
|
-
appointmentId,
|
|
9675
|
-
formId,
|
|
9676
|
-
isUserForm,
|
|
9677
|
-
values
|
|
9678
|
-
);
|
|
9726
|
+
return fileValue;
|
|
9679
9727
|
}
|
|
9680
9728
|
/**
|
|
9681
|
-
* Upload a signature image for a filled document
|
|
9729
|
+
* Upload a signature image for a filled document.
|
|
9682
9730
|
* This is a specialized version of uploadFileForFilledDocument specifically for signatures.
|
|
9683
|
-
*
|
|
9684
|
-
* @param
|
|
9685
|
-
* @param
|
|
9686
|
-
* @param
|
|
9687
|
-
* @param
|
|
9688
|
-
* @returns The
|
|
9731
|
+
*
|
|
9732
|
+
* @param appointmentId - ID of the appointment
|
|
9733
|
+
* @param formId - ID of the filled document
|
|
9734
|
+
* @param isUserForm - Boolean indicating if it's a user form or doctor form
|
|
9735
|
+
* @param signatureBlob - The signature image as a Blob
|
|
9736
|
+
* @returns The file value object to be stored in the document
|
|
9689
9737
|
*/
|
|
9690
|
-
async uploadSignatureForFilledDocument(appointmentId, formId, isUserForm, signatureBlob
|
|
9738
|
+
async uploadSignatureForFilledDocument(appointmentId, formId, isUserForm, signatureBlob) {
|
|
9691
9739
|
console.log(
|
|
9692
|
-
`[FilledDocumentService] Uploading signature for
|
|
9740
|
+
`[FilledDocumentService] Uploading signature for form ${formId}`
|
|
9693
9741
|
);
|
|
9742
|
+
const signatureId = this.generateId();
|
|
9694
9743
|
const signatureFile = new File(
|
|
9695
9744
|
[signatureBlob],
|
|
9696
|
-
`signature_${
|
|
9697
|
-
{
|
|
9698
|
-
type: "image/png"
|
|
9699
|
-
}
|
|
9745
|
+
`signature_${signatureId}.png`,
|
|
9746
|
+
{ type: "image/png" }
|
|
9700
9747
|
);
|
|
9701
9748
|
return this.uploadFileForFilledDocument(
|
|
9702
9749
|
appointmentId,
|
|
9703
9750
|
formId,
|
|
9704
9751
|
isUserForm,
|
|
9705
|
-
signatureFile
|
|
9706
|
-
fieldId,
|
|
9707
|
-
"confidential" /* CONFIDENTIAL */
|
|
9708
|
-
// Signatures should be confidential
|
|
9709
|
-
);
|
|
9710
|
-
}
|
|
9711
|
-
/**
|
|
9712
|
-
* Remove a file from a filled document field.
|
|
9713
|
-
* This will both update the document and delete the media file.
|
|
9714
|
-
* @param appointmentId - ID of the appointment.
|
|
9715
|
-
* @param formId - ID of the filled document.
|
|
9716
|
-
* @param isUserForm - Boolean indicating if it's a user form or doctor form.
|
|
9717
|
-
* @param fieldId - The ID of the field containing the file.
|
|
9718
|
-
* @returns The updated filled document with the file removed.
|
|
9719
|
-
*/
|
|
9720
|
-
async removeFileFromFilledDocument(appointmentId, formId, isUserForm, fieldId) {
|
|
9721
|
-
var _a;
|
|
9722
|
-
console.log(
|
|
9723
|
-
`[FilledDocumentService] Removing file from field ${fieldId} in form ${formId}`
|
|
9724
|
-
);
|
|
9725
|
-
const existingDoc = await this.getFilledDocumentFromAppointmentById(
|
|
9726
|
-
appointmentId,
|
|
9727
|
-
formId,
|
|
9728
|
-
isUserForm
|
|
9729
|
-
);
|
|
9730
|
-
if (!existingDoc) {
|
|
9731
|
-
throw new Error(
|
|
9732
|
-
`Filled document with ID ${formId} not found in appointment ${appointmentId}`
|
|
9733
|
-
);
|
|
9734
|
-
}
|
|
9735
|
-
const fileValue = (_a = existingDoc.values) == null ? void 0 : _a[fieldId];
|
|
9736
|
-
if (fileValue && fileValue.mediaId) {
|
|
9737
|
-
try {
|
|
9738
|
-
await this.mediaService.deleteMedia(fileValue.mediaId);
|
|
9739
|
-
} catch (error) {
|
|
9740
|
-
console.error(
|
|
9741
|
-
`[FilledDocumentService] Error deleting media ${fileValue.mediaId}:`,
|
|
9742
|
-
error
|
|
9743
|
-
);
|
|
9744
|
-
}
|
|
9745
|
-
}
|
|
9746
|
-
const values = {
|
|
9747
|
-
[fieldId]: null
|
|
9748
|
-
};
|
|
9749
|
-
return this.updateFilledDocumentInAppointment(
|
|
9750
|
-
appointmentId,
|
|
9751
|
-
formId,
|
|
9752
|
-
isUserForm,
|
|
9753
|
-
values
|
|
9752
|
+
signatureFile
|
|
9754
9753
|
);
|
|
9755
9754
|
}
|
|
9756
9755
|
/**
|
|
9757
|
-
*
|
|
9758
|
-
*
|
|
9759
|
-
* @param
|
|
9760
|
-
* @
|
|
9761
|
-
* @param fieldId - The ID of the field containing the file.
|
|
9762
|
-
* @returns The download URL for the file, or null if not found.
|
|
9756
|
+
* Delete a file using its mediaId.
|
|
9757
|
+
*
|
|
9758
|
+
* @param mediaId - ID of the media to delete
|
|
9759
|
+
* @returns Promise resolving when the deletion is complete
|
|
9763
9760
|
*/
|
|
9764
|
-
async
|
|
9765
|
-
var _a;
|
|
9761
|
+
async deleteFile(mediaId) {
|
|
9766
9762
|
console.log(
|
|
9767
|
-
`[FilledDocumentService]
|
|
9768
|
-
);
|
|
9769
|
-
const doc34 = await this.getFilledDocumentFromAppointmentById(
|
|
9770
|
-
appointmentId,
|
|
9771
|
-
formId,
|
|
9772
|
-
isUserForm
|
|
9763
|
+
`[FilledDocumentService] Deleting file with mediaId ${mediaId}`
|
|
9773
9764
|
);
|
|
9774
|
-
if (!
|
|
9775
|
-
|
|
9765
|
+
if (!mediaId) {
|
|
9766
|
+
throw new Error("MediaId is required to delete a file");
|
|
9776
9767
|
}
|
|
9777
|
-
|
|
9778
|
-
if (fileValue && fileValue.url) {
|
|
9779
|
-
return fileValue.url;
|
|
9780
|
-
}
|
|
9781
|
-
return null;
|
|
9768
|
+
await this.mediaService.deleteMedia(mediaId);
|
|
9782
9769
|
}
|
|
9783
9770
|
};
|
|
9784
9771
|
|