@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/index.js CHANGED
@@ -1096,7 +1096,7 @@ var BaseService = class {
1096
1096
  };
1097
1097
 
1098
1098
  // src/services/user.service.ts
1099
- var import_firestore17 = require("firebase/firestore");
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 import_firestore16 = require("firebase/firestore");
4063
+ var import_firestore18 = require("firebase/firestore");
4064
4064
 
4065
- // src/validations/practitioner.schema.ts
4066
- var import_zod14 = require("zod");
4065
+ // src/services/media/media.service.ts
4067
4066
  var import_firestore15 = require("firebase/firestore");
4068
-
4069
- // src/backoffice/types/static/certification.types.ts
4070
- var CertificationLevel = /* @__PURE__ */ ((CertificationLevel2) => {
4071
- CertificationLevel2["AESTHETICIAN"] = "aesthetician";
4072
- CertificationLevel2["NURSE_ASSISTANT"] = "nurse_assistant";
4073
- CertificationLevel2["NURSE"] = "nurse";
4074
- CertificationLevel2["NURSE_PRACTITIONER"] = "nurse_practitioner";
4075
- CertificationLevel2["PHYSICIAN_ASSISTANT"] = "physician_assistant";
4076
- CertificationLevel2["DOCTOR"] = "doctor";
4077
- CertificationLevel2["SPECIALIST"] = "specialist";
4078
- CertificationLevel2["PLASTIC_SURGEON"] = "plastic_surgeon";
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
- * Creates a new practitioner
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 createPractitioner(data) {
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 validData = createPractitionerSchema.parse(data);
4245
- const practitionerId = this.generateId();
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
- const practitionerRef = (0, import_firestore16.doc)(
4279
- this.db,
4280
- PRACTITIONERS_COLLECTION,
4281
- practitionerId
4282
- );
4283
- await (0, import_firestore16.setDoc)(practitionerRef, practitioner);
4284
- const createdPractitioner = await this.getPractitioner(practitionerId);
4285
- if (!createdPractitioner) {
4286
- throw new Error(
4287
- `Failed to retrieve created practitioner ${practitionerId}`
4288
- );
4289
- }
4290
- return createdPractitioner;
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
- if (error instanceof import_zod15.z.ZodError) {
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
- * Kreira novi draft profil zdravstvenog radnika bez povezanog korisnika
4301
- * Koristi se od strane administratora klinike za kreiranje profila i kasnije pozivanje
4302
- * @param data Podaci za kreiranje draft profila
4303
- * @param createdBy ID administratora koji kreira profil
4304
- * @param clinicId ID klinike za koju se kreira profil
4305
- * @returns Objekt koji sadrži kreirani draft profil i token za registraciju
4306
- */
4307
- async createDraftPractitioner(data, createdBy, clinicId) {
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 validatedData = createDraftPractitionerSchema.parse(data);
4310
- const clinic = await this.getClinicService().getClinic(clinicId);
4311
- if (!clinic) {
4312
- throw new Error(`Clinic ${clinicId} not found`);
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
- const tokenString = this.generateId().slice(0, 6).toUpperCase();
4388
- const expiration = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
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
- if (error instanceof import_zod15.z.ZodError) {
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
- * Creates a token for inviting practitioner to claim their profile
4413
- * @param data Data for creating token
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 createPractitionerToken(data, createdBy) {
4418
- try {
4419
- const validatedData = createPractitionerTokenSchema.parse(data);
4420
- const practitioner = await this.getPractitioner(
4421
- validatedData.practitionerId
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
- if (!practitioner) {
4424
- throw new Error("Practitioner not found");
4425
- }
4426
- if (practitioner.status !== "draft" /* DRAFT */) {
4427
- throw new Error(
4428
- "Can only create tokens for practitioners in DRAFT status"
4429
- );
4430
- }
4431
- const clinic = await this.getClinicService().getClinic(
4432
- validatedData.clinicId
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
- if (error instanceof import_zod15.z.ZodError) {
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
- * Gets active tokens for a practitioner
4466
- * @param practitionerId ID of the practitioner
4467
- * @returns Array of active tokens
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 getPractitionerActiveTokens(practitionerId) {
4470
- const tokensRef = (0, import_firestore16.collection)(
4471
- this.db,
4472
- `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
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 querySnapshot = await (0, import_firestore16.getDocs)(q);
4480
- return querySnapshot.docs.map((doc34) => doc34.data());
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
- `[PRACTITIONER] Validating token for practitioner ${practitionerId}`,
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
- const tokenSnapshot = await (0, import_firestore16.getDocs)(q);
4511
- console.log(
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
- `[PRACTITIONER] Error validating token for practitioner ${practitionerId}:`,
4222
+ `[MediaService] Error updating timestamp for media ID ${mediaId}:`,
4529
4223
  error
4530
4224
  );
4531
4225
  throw error;
4532
4226
  }
4533
4227
  }
4534
- return null;
4535
- }
4536
- /**
4537
- * Marks a token as used
4538
- * @param tokenId ID of the token
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
- await (0, import_firestore16.updateDoc)(tokenRef, {
4548
- status: "used" /* USED */,
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
- const validData = data;
4622
- const practitionerRef = (0, import_firestore16.doc)(
4623
- this.db,
4624
- PRACTITIONERS_COLLECTION,
4625
- practitionerId
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
- ...validData,
4634
- updatedAt: (0, import_firestore16.serverTimestamp)()
4254
+ accessLevel: newAccessLevel,
4255
+ path: newStoragePath,
4256
+ url: newDownloadURL,
4257
+ updatedAt: import_firestore15.Timestamp.now()
4635
4258
  };
4636
- await (0, import_firestore16.updateDoc)(practitionerRef, updateData);
4637
- const updatedPractitioner = await this.getPractitioner(practitionerId);
4638
- if (!updatedPractitioner) {
4639
- throw new Error(
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
- const practitionerDoc = await (0, import_firestore16.getDoc)(practitionerRef);
4664
- if (!practitionerDoc.exists()) {
4665
- throw new Error(`Practitioner ${practitionerId} not found`);
4666
- }
4667
- const practitioner = practitionerDoc.data();
4668
- if ((_a = practitioner.clinics) == null ? void 0 : _a.includes(clinicId)) {
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
- `Clinic ${clinicId} already added to practitioner ${practitionerId}`
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
- await (0, import_firestore16.updateDoc)(practitionerRef, {
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 adding clinic ${clinicId} to practitioner ${practitionerId}:`,
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
- * Removes a clinic from a practitioner
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 removeClinic(practitionerId, clinicId) {
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 practitionerRef = (0, import_firestore16.doc)(
4692
- this.db,
4693
- PRACTITIONERS_COLLECTION,
4694
- practitionerId
4336
+ const querySnapshot = await (0, import_firestore16.getDocs)(finalQuery);
4337
+ const mediaList = querySnapshot.docs.map(
4338
+ (doc34) => doc34.data()
4695
4339
  );
4696
- const practitionerDoc = await (0, import_firestore16.getDoc)(practitionerRef);
4697
- if (!practitionerDoc.exists()) {
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
- * Deaktivira profil zdravstvenog radnika
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 deletePractitioner(practitionerId) {
4732
- const practitioner = await this.getPractitioner(practitionerId);
4733
- if (!practitioner) {
4734
- throw new Error("Practitioner not found");
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
- await (0, import_firestore16.deleteDoc)((0, import_firestore16.doc)(this.db, PRACTITIONERS_COLLECTION, practitionerId));
4358
+ console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
4359
+ return null;
4737
4360
  }
4738
- /**
4739
- * Validates a registration token and claims the associated draft practitioner profile
4740
- * @param tokenString The token provided by the practitioner
4741
- * @param userId The ID of the user claiming the profile
4742
- * @returns The claimed practitioner profile or null if token is invalid
4743
- */
4744
- async validateTokenAndClaimProfile(tokenString, userId) {
4745
- console.log("[PRACTITIONER] Validating token for claiming profile", {
4746
- tokenString,
4747
- userId
4748
- });
4749
- const token = await this.validateToken(tokenString);
4750
- if (!token) {
4751
- console.log(
4752
- "[PRACTITIONER] Token validation failed - token not found or not valid",
4753
- {
4754
- tokenString
4755
- }
4756
- );
4757
- return null;
4758
- }
4759
- console.log("[PRACTITIONER] Token successfully validated", {
4760
- tokenId: token.id,
4761
- practitionerId: token.practitionerId
4762
- });
4763
- const practitioner = await this.getPractitioner(token.practitionerId);
4764
- if (!practitioner) {
4765
- console.log("[PRACTITIONER] Practitioner not found", {
4766
- practitionerId: token.practitionerId
4767
- });
4768
- return null;
4769
- }
4770
- if (practitioner.status !== "draft" /* DRAFT */) {
4771
- console.log("[PRACTITIONER] Practitioner status is not DRAFT", {
4772
- practitionerId: practitioner.id,
4773
- status: practitioner.status
4774
- });
4775
- throw new Error("This practitioner profile has already been claimed");
4776
- }
4777
- const existingPractitioner = await this.getPractitionerByUserRef(userId);
4778
- if (existingPractitioner) {
4779
- throw new Error("User already has a practitioner profile");
4780
- }
4781
- const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
4782
- userRef: userId,
4783
- status: "active" /* ACTIVE */
4784
- });
4785
- await this.markTokenAsUsed(token.id, token.practitionerId, userId);
4786
- console.log("[PRACTITIONER] Profile claimed successfully", {
4787
- practitionerId: updatedPractitioner.id,
4788
- userId
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, import_firestore16.where)("status", "==", "active" /* ACTIVE */));
5164
+ constraints.push((0, import_firestore18.where)("status", "==", "active" /* ACTIVE */));
4806
5165
  }
4807
- constraints.push((0, import_firestore16.orderBy)("basicInfo.lastName", "asc"));
4808
- constraints.push((0, import_firestore16.orderBy)("basicInfo.firstName", "asc"));
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, import_firestore16.startAfter)(options.lastDoc));
5170
+ constraints.push((0, import_firestore18.startAfter)(options.lastDoc));
4812
5171
  }
4813
- constraints.push((0, import_firestore16.limit)(options.pagination));
5172
+ constraints.push((0, import_firestore18.limit)(options.pagination));
4814
5173
  }
4815
- const q = (0, import_firestore16.query)(
4816
- (0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION),
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, import_firestore16.getDocs)(q);
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, import_firestore16.where)("status", "==", "active" /* ACTIVE */));
5223
+ constraints.push((0, import_firestore18.where)("status", "==", "active" /* ACTIVE */));
4865
5224
  }
4866
- constraints.push((0, import_firestore16.where)("isActive", "==", true));
5225
+ constraints.push((0, import_firestore18.where)("isActive", "==", true));
4867
5226
  if (filters.certifications && filters.certifications.length > 0) {
4868
5227
  constraints.push(
4869
- (0, import_firestore16.where)(
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, import_firestore16.orderBy)("basicInfo.lastName", "asc"));
4877
- constraints.push((0, import_firestore16.orderBy)("basicInfo.firstName", "asc"));
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, import_firestore16.startAfter)(filters.lastDoc));
5239
+ constraints.push((0, import_firestore18.startAfter)(filters.lastDoc));
4881
5240
  }
4882
- constraints.push((0, import_firestore16.limit)(filters.pagination));
5241
+ constraints.push((0, import_firestore18.limit)(filters.pagination));
4883
5242
  }
4884
- const q = (0, import_firestore16.query)(
4885
- (0, import_firestore16.collection)(this.db, PRACTITIONERS_COLLECTION),
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, import_firestore16.getDocs)(q);
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, import_firestore17.serverTimestamp)(),
5011
- updatedAt: (0, import_firestore17.serverTimestamp)(),
5012
- lastLoginAt: (0, import_firestore17.serverTimestamp)()
5369
+ createdAt: (0, import_firestore19.serverTimestamp)(),
5370
+ updatedAt: (0, import_firestore19.serverTimestamp)(),
5371
+ lastLoginAt: (0, import_firestore19.serverTimestamp)()
5013
5372
  };
5014
- await (0, import_firestore17.setDoc)((0, import_firestore17.doc)(this.db, USERS_COLLECTION, userData.uid), userData);
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, import_firestore17.updateDoc)((0, import_firestore17.doc)(this.db, USERS_COLLECTION, userData.uid), profiles);
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: import_firestore17.Timestamp.now(),
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: import_firestore17.Timestamp.now(),
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, import_firestore17.getDoc)((0, import_firestore17.doc)(this.db, USERS_COLLECTION, uid));
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, import_firestore17.collection)(this.db, USERS_COLLECTION);
5138
- const q = (0, import_firestore17.query)(usersRef, (0, import_firestore17.where)("email", "==", email));
5139
- const querySnapshot = await (0, import_firestore17.getDocs)(q);
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, import_firestore17.where)("roles", "array-contains", role)
5505
+ (0, import_firestore19.where)("roles", "array-contains", role)
5147
5506
  ];
5148
- const q = (0, import_firestore17.query)((0, import_firestore17.collection)(this.db, USERS_COLLECTION), ...constraints);
5149
- const querySnapshot = await (0, import_firestore17.getDocs)(q);
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, import_firestore17.doc)(this.db, USERS_COLLECTION, uid);
5158
- const userDoc = await (0, import_firestore17.getDoc)(userRef);
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, import_firestore17.updateDoc)(userRef, {
5163
- lastLoginAt: (0, import_firestore17.serverTimestamp)(),
5164
- updatedAt: (0, import_firestore17.serverTimestamp)()
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, import_firestore17.doc)(this.db, USERS_COLLECTION, uid);
5170
- const userDoc = await (0, import_firestore17.getDoc)(userRef);
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, import_firestore17.updateDoc)(userRef, {
5533
+ await (0, import_firestore19.updateDoc)(userRef, {
5175
5534
  email,
5176
5535
  isAnonymous: false,
5177
- updatedAt: (0, import_firestore17.serverTimestamp)()
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, import_firestore17.doc)(this.db, USERS_COLLECTION, uid);
5183
- const userDoc = await (0, import_firestore17.getDoc)(userRef);
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, import_firestore17.serverTimestamp)()
5551
+ updatedAt: (0, import_firestore19.serverTimestamp)()
5193
5552
  };
5194
5553
  userSchema.parse(updatedUser);
5195
- await (0, import_firestore17.updateDoc)(userRef, {
5554
+ await (0, import_firestore19.updateDoc)(userRef, {
5196
5555
  ...updates,
5197
- updatedAt: (0, import_firestore17.serverTimestamp)()
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, import_firestore17.updateDoc)((0, import_firestore17.doc)(this.db, USERS_COLLECTION, uid), {
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, import_firestore17.serverTimestamp)()
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, import_firestore17.updateDoc)((0, import_firestore17.doc)(this.db, USERS_COLLECTION, uid), {
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, import_firestore17.serverTimestamp)()
5610
+ updatedAt: (0, import_firestore19.serverTimestamp)()
5252
5611
  });
5253
5612
  }
5254
5613
  // Delete operations
5255
5614
  async deleteUser(uid) {
5256
- const userRef = (0, import_firestore17.doc)(this.db, USERS_COLLECTION, uid);
5257
- const userDoc = await (0, import_firestore17.getDoc)(userRef);
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, import_firestore17.deleteDoc)(userRef);
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 import_firestore18 = require("firebase/firestore");
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 import_storage4 = require("firebase/storage");
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, import_storage4.getStorage)(app);
5659
+ const storage = (0, import_storage5.getStorage)(app);
5301
5660
  const storageFileName = fileName || `${photoType}-${Date.now()}`;
5302
- const storageRef = (0, import_storage4.ref)(
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, import_storage4.uploadBytes)(storageRef, blob, { contentType });
5315
- const downloadUrl = await (0, import_storage4.getDownloadURL)(storageRef);
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 = import_firestore18.Timestamp.now();
5762
+ const now = import_firestore20.Timestamp.now();
5404
5763
  console.log("[CLINIC_GROUP] Preparing clinic group data object");
5405
- const groupId = (0, import_firestore18.doc)((0, import_firestore18.collection)(db, CLINIC_GROUPS_COLLECTION)).id;
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, import_firestore18.setDoc)((0, import_firestore18.doc)(db, CLINIC_GROUPS_COLLECTION, groupData.id), groupData);
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, import_firestore18.doc)(db, CLINIC_GROUPS_COLLECTION, groupId);
5502
- const docSnap = await (0, import_firestore18.getDoc)(docRef);
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, import_firestore18.query)(
5510
- (0, import_firestore18.collection)(db, CLINIC_GROUPS_COLLECTION),
5511
- (0, import_firestore18.where)("isActive", "==", true)
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, import_firestore18.getDocs)(q);
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: import_firestore18.Timestamp.now()
5901
+ updatedAt: import_firestore20.Timestamp.now()
5543
5902
  };
5544
5903
  console.log("[CLINIC_GROUP] Updating clinic group in Firestore");
5545
- await (0, import_firestore18.updateDoc)((0, import_firestore18.doc)(db, CLINIC_GROUPS_COLLECTION, groupId), updatedData);
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
- adminTokens: updatedTokens
5958
+ admins: group.admins.filter((id) => id !== adminId)
5690
5959
  },
5691
5960
  app
5692
5961
  );
5693
- return true;
5694
5962
  }
5695
- async function deleteAdminToken(db, groupId, tokenId, adminId, app) {
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
- adminTokens: updatedTokens
5972
+ isActive: false
5709
5973
  },
5710
5974
  app
5711
5975
  );
5712
5976
  }
5713
- async function getActiveAdminTokens(db, groupId, adminId, app) {
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(adminId)) {
5982
+ if (!group.admins.includes(creatorAdminId)) {
5719
5983
  throw new Error("Admin does not belong to this clinic group");
5720
5984
  }
5721
- return group.adminTokens.filter((t) => t.status === "active" /* ACTIVE */);
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
- // src/services/clinic/clinic-group.service.ts
5725
- var ClinicGroupService = class extends BaseService {
5726
- constructor(db, auth, app, clinicAdminService) {
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
- * Marks the onboarding process as completed for a clinic group
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
- return null;
5946
- }
5947
- async function getClinicsByGroup(db, groupId) {
5948
- const q = (0, import_firestore19.query)(
5949
- (0, import_firestore19.collection)(db, CLINICS_COLLECTION),
5950
- (0, import_firestore19.where)("clinicGroupId", "==", groupId),
5951
- (0, import_firestore19.where)("isActive", "==", true)
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
- const querySnapshot = await (0, import_firestore19.getDocs)(q);
5954
- return querySnapshot.docs.map((doc34) => doc34.data());
6044
+ await updateClinicGroup(
6045
+ db,
6046
+ groupId,
6047
+ {
6048
+ adminTokens: updatedTokens
6049
+ },
6050
+ app
6051
+ );
6052
+ return true;
5955
6053
  }
5956
- async function updateClinic(db, clinicId, data, adminId, clinicAdminService, app) {
5957
- console.log("[CLINIC] Starting clinic update", { clinicId, adminId });
5958
- const clinic = await getClinic(db, clinicId);
5959
- if (!clinic) {
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
- try {
5964
- console.log("[CLINIC] Checking admin permissions");
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
- let updatedData = { ...data };
5990
- if (data.logo && typeof data.logo === "string" && data.logo.startsWith("data:")) {
5991
- console.log("[CLINIC] Processing logo update");
5992
- try {
5993
- const logoUrl = await uploadPhoto(
5994
- data.logo,
5995
- "clinics",
5996
- clinicId,
5997
- "logo",
5998
- app
5999
- );
6000
- console.log("[CLINIC] Logo update processed", { logoUrl });
6001
- if (logoUrl !== null) {
6002
- updatedData.logo = logoUrl;
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 (data.coverPhoto) {
6009
- console.log("[CLINIC] Processing cover photo update");
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
- if (data.featuredPhotos && data.featuredPhotos.length > 0) {
6037
- console.log("[CLINIC] Processing featured photos update");
6038
- try {
6039
- const dataUrlPhotos = data.featuredPhotos.filter(
6040
- (photo) => typeof photo === "string" && photo.startsWith("data:")
6041
- );
6042
- const existingPhotos = data.featuredPhotos.filter(
6043
- (photo) => typeof photo === "string" && !photo.startsWith("data:")
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
- if (data.photosWithTags && data.photosWithTags.length > 0) {
6071
- console.log("[CLINIC] Processing photos with tags update");
6072
- try {
6073
- const updatedPhotosWithTags = [];
6074
- for (const photoWithTag of data.photosWithTags) {
6075
- if (photoWithTag.url && photoWithTag.url.startsWith("data:")) {
6076
- const uploadedUrl = await uploadPhoto(
6077
- photoWithTag.url,
6078
- "clinics",
6079
- clinicId,
6080
- `tagged-${photoWithTag.tag}`,
6081
- app
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
- updatedData = {
6108
- ...updatedData,
6109
- updatedAt: import_firestore19.Timestamp.now()
6110
- };
6111
- console.log("[CLINIC] Updating clinic in Firestore");
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
- const updatedClinic = await getClinic(db, clinicId);
6120
- if (!updatedClinic) {
6121
- console.error("[CLINIC] Failed to retrieve updated clinic");
6122
- throw new Error("Failed to retrieve updated clinic");
6108
+ /**
6109
+ * Dohvata sve aktivne grupacije klinika
6110
+ */
6111
+ async getAllActiveGroups() {
6112
+ return getAllActiveGroups(this.db);
6123
6113
  }
6124
- console.log("[CLINIC] Clinic update completed successfully");
6125
- return updatedClinic;
6126
- }
6127
- async function getClinicsByAdmin(db, adminId, options = {}, clinicAdminService, clinicGroupService) {
6128
- const admin = await clinicAdminService.getClinicAdmin(adminId);
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
- let clinicIds = [...admin.clinicsManaged];
6133
- if (admin.isGroupOwner && options.includeGroupClinics) {
6134
- const group = await clinicGroupService.getClinicGroup(admin.clinicGroupId);
6135
- if (group) {
6136
- clinicIds = [.../* @__PURE__ */ new Set([...clinicIds, ...group.clinics])];
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
- if (clinicIds.length === 0) {
6140
- return [];
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
- const constraints = [(0, import_firestore19.where)("id", "in", clinicIds)];
6143
- if (options.isActive !== void 0) {
6144
- constraints.push((0, import_firestore19.where)("isActive", "==", options.isActive));
6142
+ /**
6143
+ * Deaktivira grupaciju klinika
6144
+ */
6145
+ async deactivateClinicGroup(groupId) {
6146
+ return deactivateClinicGroup(this.db, groupId, this.app);
6145
6147
  }
6146
- const q = (0, import_firestore19.query)((0, import_firestore19.collection)(db, CLINICS_COLLECTION), ...constraints);
6147
- const querySnapshot = await (0, import_firestore19.getDocs)(q);
6148
- return querySnapshot.docs.map((doc34) => doc34.data());
6149
- }
6150
- async function getActiveClinicsByAdmin(db, adminId, clinicAdminService, clinicGroupService) {
6151
- return getClinicsByAdmin(
6152
- db,
6153
- adminId,
6154
- { isActive: true },
6155
- clinicAdminService,
6156
- clinicGroupService
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
- const clinicData = clinicSnapshot.data();
6167
- return {
6168
- ...clinicData,
6169
- id: clinicSnapshot.id
6170
- };
6171
- } catch (error) {
6172
- console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
6173
- throw error;
6174
- }
6175
- }
6176
- async function getAllClinics(db, pagination, lastDoc) {
6177
- try {
6178
- const clinicsCollection = (0, import_firestore19.collection)(db, CLINICS_COLLECTION);
6179
- let clinicsQuery = (0, import_firestore19.query)(clinicsCollection);
6180
- if (pagination && pagination > 0) {
6181
- if (lastDoc) {
6182
- clinicsQuery = (0, import_firestore19.query)(
6183
- clinicsCollection,
6184
- (0, import_firestore19.startAfter)(lastDoc),
6185
- (0, import_firestore19.limit)(pagination)
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 clinicsSnapshot = await (0, import_firestore19.getDocs)(clinicsQuery);
6192
- const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
6193
- const clinics = clinicsSnapshot.docs.map((doc34) => {
6194
- const data = doc34.data();
6195
- return {
6196
- ...data,
6197
- id: doc34.id
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
- } catch (error) {
6205
- console.error("[CLINIC_UTILS] Error getting all clinics:", error);
6206
- throw error;
6195
+ console.log("[CLINIC_GROUP] Updating clinic group with setup data");
6196
+ return this.updateClinicGroup(groupId, updateData);
6207
6197
  }
6208
- }
6209
- async function getAllClinicsInRange(db, center, rangeInKm, pagination, lastDoc) {
6210
- const bounds = (0, import_geofire_common4.geohashQueryBounds)(
6211
- [center.latitude, center.longitude],
6212
- rangeInKm * 1e3
6213
- );
6214
- const matchingClinics = [];
6215
- let lastDocSnapshot = null;
6216
- for (const b of bounds) {
6217
- const constraints = [
6218
- (0, import_firestore19.where)("location.geohash", ">=", b[0]),
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
- matchingClinics.sort((a, b) => a.distance - b.distance);
6240
- if (pagination && pagination > 0) {
6241
- let result = matchingClinics;
6242
- if (lastDoc && matchingClinics.length > 0) {
6243
- const lastIndex = matchingClinics.findIndex(
6244
- (clinic) => clinic.id === lastDoc.id
6245
- );
6246
- if (lastIndex !== -1) {
6247
- result = matchingClinics.slice(lastIndex + 1);
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
- return {
6258
- clinics: matchingClinics,
6259
- lastDoc: null
6260
- };
6261
- }
6262
-
6263
- // src/services/clinic/utils/tag.utils.ts
6264
- async function addTags(db, clinicId, adminId, newTags, clinicAdminService, app) {
6265
- const clinic = await getClinic(db, clinicId);
6266
- if (!clinic) {
6267
- throw new Error("Clinic not found");
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
- const admin = await clinicAdminService.getClinicAdmin(adminId);
6270
- if (!admin) {
6271
- throw new Error("Admin not found");
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
- const hasPermission = admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId || admin.clinicsManaged.includes(clinicId) && clinic.admins && clinic.admins.includes(adminId);
6274
- if (!hasPermission) {
6275
- throw new Error("Admin does not have permission to update this clinic");
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
- const updatedTags = [.../* @__PURE__ */ new Set([...clinic.tags, ...newTags.tags || []])];
6278
- return updateClinic(
6279
- db,
6280
- clinicId,
6281
- {
6282
- tags: updatedTags
6283
- },
6284
- adminId,
6285
- clinicAdminService,
6286
- app
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 removeTags(db, clinicId, adminId, tagsToRemove, clinicAdminService, app) {
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
- const admin = await clinicAdminService.getClinicAdmin(adminId);
6295
- if (!admin) {
6296
- throw new Error("Admin not found");
6297
- }
6298
- const hasPermission = admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId || admin.clinicsManaged.includes(clinicId) && clinic.admins && clinic.admins.includes(adminId);
6299
- if (!hasPermission) {
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 q = (0, import_firestore20.query)((0, import_firestore20.collection)(db, CLINICS_COLLECTION), ...constraints);
6341
- const querySnapshot = await (0, import_firestore20.getDocs)(q);
6342
- for (const doc34 of querySnapshot.docs) {
6343
- const clinic = doc34.data();
6344
- const distance = (0, import_geofire_common5.distanceBetween)(
6345
- [center.latitude, center.longitude],
6346
- [clinic.location.latitude, clinic.location.longitude]
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
- const distanceInKm = distance / 1e3;
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
- return matchingDocs.sort((a, b) => {
6355
- const distanceA = (0, import_geofire_common5.distanceBetween)(
6356
- [center.latitude, center.longitude],
6357
- [a.location.latitude, a.location.longitude]
6358
- );
6359
- const distanceB = (0, import_geofire_common5.distanceBetween)(
6360
- [center.latitude, center.longitude],
6361
- [b.location.latitude, b.location.longitude]
6362
- );
6363
- return distanceA - distanceB;
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
- for (const doc34 of querySnapshot.docs) {
6432
- const clinic = { ...doc34.data(), id: doc34.id };
6433
- const distance = (0, import_geofire_common6.distanceBetween)(
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
- let filteredClinics = matchingClinics;
6447
- if (filters.tags && filters.tags.length > 1) {
6448
- filteredClinics = filteredClinics.filter((clinic) => {
6449
- return filters.tags.every((tag) => clinic.tags.includes(tag));
6450
- });
6451
- }
6452
- if (filters.minRating !== void 0) {
6453
- filteredClinics = filteredClinics.filter(
6454
- (clinic) => clinic.reviewInfo.averageRating >= filters.minRating
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 (lastDocIndex !== -1) {
6470
- startIndex = lastDocIndex + 1;
6378
+ if (uploadedPhoto) {
6379
+ updatedData.coverPhoto = uploadedPhoto;
6471
6380
  }
6381
+ } else {
6382
+ updatedData.coverPhoto = data.coverPhoto;
6472
6383
  }
6473
- const paginatedClinics = filteredClinics.slice(
6474
- startIndex,
6475
- startIndex + filters.pagination
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
- lastVisibleDoc = paginatedClinics.length > 0 ? paginatedClinics[paginatedClinics.length - 1] : null;
6478
- clinicsResult = paginatedClinics;
6479
- } else {
6480
- clinicsResult = filteredClinics;
6390
+ if (typeof data.coverPhoto === "string" && !data.coverPhoto.startsWith("data:")) {
6391
+ updatedData.coverPhoto = data.coverPhoto;
6392
+ }
6481
6393
  }
6482
- } else {
6483
- const q = (0, import_firestore21.query)((0, import_firestore21.collection)(db, CLINICS_COLLECTION), ...constraints);
6484
- const querySnapshot = await (0, import_firestore21.getDocs)(q);
6485
- console.log(
6486
- `[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
6487
- );
6488
- const clinics = querySnapshot.docs.map((doc34) => {
6489
- return { ...doc34.data(), id: doc34.id };
6490
- });
6491
- let filteredClinics = clinics;
6492
- if (filters.center) {
6493
- const center = filters.center;
6494
- const clinicsWithDistance = [];
6495
- filteredClinics.forEach((clinic) => {
6496
- const distance = (0, import_geofire_common6.distanceBetween)(
6497
- [center.latitude, center.longitude],
6498
- [clinic.location.latitude, clinic.location.longitude]
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
- clinicsWithDistance.push({
6501
- ...clinic,
6502
- distance: distance / 1e3
6503
- // Convert to kilometers
6412
+ console.log("[CLINIC] Featured photos update processed", {
6413
+ count: uploadedPhotos.length
6504
6414
  });
6505
- });
6506
- filteredClinics = clinicsWithDistance;
6507
- filteredClinics.sort(
6508
- (a, b) => a.distance - b.distance
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
- if (filters.tags && filters.tags.length > 1) {
6512
- filteredClinics = filteredClinics.filter((clinic) => {
6513
- return filters.tags.every((tag) => clinic.tags.includes(tag));
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
- if (filters.minRating !== void 0) {
6517
- filteredClinics = filteredClinics.filter(
6518
- (clinic) => clinic.reviewInfo.averageRating >= filters.minRating
6456
+ } catch (tagsError) {
6457
+ console.error(
6458
+ "[CLINIC] Error processing photos with tags update:",
6459
+ tagsError
6519
6460
  );
6520
- }
6521
- if (filters.maxRating !== void 0) {
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
- return {
6530
- clinics: clinicsResult,
6531
- lastDoc: lastVisibleDoc
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
- // src/services/media/media.service.ts
6536
- var import_firestore22 = require("firebase/firestore");
6537
- var import_storage5 = require("firebase/storage");
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
- * Upload a media file, store its metadata, and return the metadata including the URL.
6552
- * @param file - The file to upload.
6553
- * @param ownerId - ID of the owner (user, patient, clinic, etc.).
6554
- * @param accessLevel - Access level (public, private, confidential).
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
- * Get media metadata from Firestore by its ID.
6596
- * @param mediaId - ID of the media.
6597
- * @returns Promise with the media metadata or null if not found.
6598
- */
6599
- async getMediaMetadata(mediaId) {
6600
- console.log(`[MediaService] Getting media metadata for ID: ${mediaId}`);
6601
- const docRef = (0, import_firestore23.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
6602
- const docSnap = await (0, import_firestore23.getDoc)(docRef);
6603
- if (docSnap.exists()) {
6604
- console.log("[MediaService] Metadata found:", docSnap.data());
6605
- return docSnap.data();
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
- console.log("[MediaService] No metadata found for ID:", mediaId);
6608
- return null;
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
- * Get media metadata from Firestore by its public URL.
6612
- * @param url - The public URL of the media file.
6613
- * @returns Promise with the media metadata or null if not found.
6614
- */
6615
- async getMediaMetadataByUrl(url) {
6616
- console.log(`[MediaService] Getting media metadata by URL: ${url}`);
6617
- const q = (0, import_firestore23.query)(
6618
- (0, import_firestore23.collection)(this.db, MEDIA_METADATA_COLLECTION),
6619
- (0, import_firestore23.where)("url", "==", url),
6620
- (0, import_firestore23.limit)(1)
6621
- );
6622
- try {
6623
- const querySnapshot = await (0, import_firestore23.getDocs)(q);
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
- * Delete media from storage and remove metadata from Firestore.
6638
- * @param mediaId - ID of the media to delete.
6639
- */
6640
- async deleteMedia(mediaId) {
6641
- console.log(`[MediaService] Deleting media with ID: ${mediaId}`);
6642
- const metadata = await this.getMediaMetadata(mediaId);
6643
- if (!metadata) {
6644
- console.warn(
6645
- `[MediaService] Metadata not found for media ID ${mediaId}. Cannot delete.`
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
- return;
6589
+ const distanceInKm = distance / 1e3;
6590
+ if (distanceInKm <= rangeInKm) {
6591
+ matchingClinics.push({
6592
+ ...clinic,
6593
+ distance: distanceInKm
6594
+ });
6595
+ }
6648
6596
  }
6649
- const storageFileRef = (0, import_storage5.ref)(this.storage, metadata.path);
6650
- try {
6651
- await (0, import_storage5.deleteObject)(storageFileRef);
6652
- console.log(`[MediaService] File deleted from Storage: ${metadata.path}`);
6653
- const metadataDocRef = (0, import_firestore23.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
6654
- await (0, import_firestore23.deleteDoc)(metadataDocRef);
6655
- console.log(
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
- } catch (error) {
6659
- console.error(`[MediaService] Error deleting media ${mediaId}:`, error);
6660
- throw error;
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
- * Update media access level. This involves moving the file in Firebase Storage
6665
- * to a new path reflecting the new access level, and updating its metadata.
6666
- * @param mediaId - ID of the media to update.
6667
- * @param newAccessLevel - New access level.
6668
- * @returns Promise with the updated media metadata, or null if metadata not found.
6669
- */
6670
- async updateMediaAccessLevel(mediaId, newAccessLevel) {
6671
- var _a;
6672
- console.log(
6673
- `[MediaService] Attempting to update access level for media ID: ${mediaId} to ${newAccessLevel}`
6674
- );
6675
- const metadata = await this.getMediaMetadata(mediaId);
6676
- if (!metadata) {
6677
- console.warn(
6678
- `[MediaService] Metadata not found for media ID ${mediaId}. Cannot update access level.`
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 (metadata.accessLevel === newAccessLevel) {
6683
- console.log(
6684
- `[MediaService] Media ID ${mediaId} already has access level ${newAccessLevel}. Updating timestamp only.`
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 metadataDocRef = (0, import_firestore23.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
6687
- try {
6688
- await (0, import_firestore23.updateDoc)(metadataDocRef, { updatedAt: import_firestore22.Timestamp.now() });
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
- const oldStoragePath = metadata.path;
6699
- const fileNamePart = `${metadata.id}-${metadata.name}`;
6700
- const newStoragePath = `media/${newAccessLevel}/${metadata.ownerId}/${metadata.collectionName}/${fileNamePart}`;
6701
- console.log(
6702
- `[MediaService] Moving file for ${mediaId} from ${oldStoragePath} to ${newStoragePath}`
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 oldStorageFileRef = (0, import_storage5.ref)(this.storage, oldStoragePath);
6705
- const newStorageFileRef = (0, import_storage5.ref)(this.storage, newStoragePath);
6706
- try {
6707
- console.log(`[MediaService] Downloading bytes from ${oldStoragePath}`);
6708
- const fileBytes = await (0, import_storage5.getBytes)(oldStorageFileRef);
6709
- console.log(
6710
- `[MediaService] Successfully downloaded ${fileBytes.byteLength} bytes from ${oldStoragePath}`
6711
- );
6712
- console.log(`[MediaService] Uploading bytes to ${newStoragePath}`);
6713
- await (0, import_storage5.uploadBytes)(newStorageFileRef, fileBytes, {
6714
- contentType: metadata.contentType
6715
- });
6716
- console.log(
6717
- `[MediaService] Successfully uploaded bytes to ${newStoragePath}`
6718
- );
6719
- const newDownloadURL = await (0, import_storage5.getDownloadURL)(newStorageFileRef);
6720
- console.log(
6721
- `[MediaService] Got new download URL for ${newStoragePath}: ${newDownloadURL}`
6722
- );
6723
- const updateData = {
6724
- accessLevel: newAccessLevel,
6725
- path: newStoragePath,
6726
- url: newDownloadURL,
6727
- updatedAt: import_firestore22.Timestamp.now()
6728
- };
6729
- const metadataDocRef = (0, import_firestore23.doc)(this.db, MEDIA_METADATA_COLLECTION, mediaId);
6730
- console.log(
6731
- `[MediaService] Updating Firestore metadata for ${mediaId} with new data:`,
6732
- updateData
6733
- );
6734
- await (0, import_firestore23.updateDoc)(metadataDocRef, updateData);
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
- `[MediaService] Successfully updated Firestore metadata for ${mediaId}`
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
- if (newStorageFileRef && error.code !== "storage/object-not-found" && ((_a = error.message) == null ? void 0 : _a.includes("uploadBytes"))) {
6757
- console.warn(
6758
- `[MediaService] Attempting to delete partially uploaded file at ${newStoragePath} due to error.`
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
- try {
6761
- await (0, import_storage5.deleteObject)(newStorageFileRef);
6762
- console.warn(
6763
- `[MediaService] Cleaned up partially uploaded file at ${newStoragePath}.`
6764
- );
6765
- } catch (cleanupError) {
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
- * List all media for an owner, optionally filtered by collection and access level.
6777
- * @param ownerId - ID of the owner.
6778
- * @param collectionName - Optional: Filter by collection name.
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 (accessLevel) {
6790
- qConstraints.push((0, import_firestore23.where)("accessLevel", "==", accessLevel));
6811
+ if (filters.minRating !== void 0) {
6812
+ filteredClinics = filteredClinics.filter(
6813
+ (clinic) => clinic.reviewInfo.averageRating >= filters.minRating
6814
+ );
6791
6815
  }
6792
- qConstraints.push((0, import_firestore23.orderBy)("createdAt", "desc"));
6793
- if (count) {
6794
- qConstraints.push((0, import_firestore23.limit)(count));
6816
+ if (filters.maxRating !== void 0) {
6817
+ filteredClinics = filteredClinics.filter(
6818
+ (clinic) => clinic.reviewInfo.averageRating <= filters.maxRating
6819
+ );
6795
6820
  }
6796
- if (startAfterId) {
6797
- const startAfterDoc = await this.getMediaMetadata(startAfterId);
6798
- if (startAfterDoc) {
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
- const finalQuery = (0, import_firestore23.query)(
6802
- (0, import_firestore23.collection)(this.db, MEDIA_METADATA_COLLECTION),
6803
- ...qConstraints
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
- try {
6806
- const querySnapshot = await (0, import_firestore23.getDocs)(finalQuery);
6807
- const mediaList = querySnapshot.docs.map(
6808
- (doc34) => doc34.data()
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
- * Get download URL for media. (Convenience, as URL is in metadata)
6819
- * @param mediaId - ID of the media.
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
- console.log(`[MediaService] URL not found for media ID: ${mediaId}`);
6829
- return null;
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 and associate it with a filled document field.
9631
- * @param appointmentId - ID of the appointment.
9632
- * @param formId - ID of the filled document.
9633
- * @param isUserForm - Boolean indicating if it's a user form or doctor form.
9634
- * @param file - The file to upload.
9635
- * @param fieldId - The ID of the field in the document to associate with this file.
9636
- * @param accessLevel - Access level for the file, defaults to PRIVATE.
9637
- * @returns The updated filled document with file information.
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, fieldId, accessLevel = "private" /* PRIVATE */) {
9702
+ async uploadFileForFilledDocument(appointmentId, formId, isUserForm, file) {
9640
9703
  console.log(
9641
- `[FilledDocumentService] Uploading file for field ${fieldId} in form ${formId}`
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
- if (!existingDoc) {
9649
- throw new Error(
9650
- `Filled document with ID ${formId} not found in appointment ${appointmentId}`
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
- ownerId,
9712
+ appointmentId,
9713
+ // Using appointmentId as ownerId
9658
9714
  accessLevel,
9659
9715
  collectionName,
9660
- file instanceof File ? file.name : `${fieldId}_file`
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
- const values = {
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 field.
9729
+ * Upload a signature image for a filled document.
9682
9730
  * This is a specialized version of uploadFileForFilledDocument specifically for signatures.
9683
- * @param appointmentId - ID of the appointment.
9684
- * @param formId - ID of the filled document.
9685
- * @param isUserForm - Boolean indicating if it's a user form or doctor form.
9686
- * @param signatureBlob - The signature image as a Blob.
9687
- * @param fieldId - The ID of the signature field in the document.
9688
- * @returns The updated filled document with signature information.
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, fieldId) {
9738
+ async uploadSignatureForFilledDocument(appointmentId, formId, isUserForm, signatureBlob) {
9691
9739
  console.log(
9692
- `[FilledDocumentService] Uploading signature for field ${fieldId} in form ${formId}`
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_${fieldId}.png`,
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
- * Get the download URL for a file in a filled document.
9758
- * @param appointmentId - ID of the appointment.
9759
- * @param formId - ID of the filled document.
9760
- * @param isUserForm - Boolean indicating if it's a user form or doctor form.
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 getFileUrlFromFilledDocument(appointmentId, formId, isUserForm, fieldId) {
9765
- var _a;
9761
+ async deleteFile(mediaId) {
9766
9762
  console.log(
9767
- `[FilledDocumentService] Getting file URL for field ${fieldId} in form ${formId}`
9768
- );
9769
- const doc34 = await this.getFilledDocumentFromAppointmentById(
9770
- appointmentId,
9771
- formId,
9772
- isUserForm
9763
+ `[FilledDocumentService] Deleting file with mediaId ${mediaId}`
9773
9764
  );
9774
- if (!doc34) {
9775
- return null;
9765
+ if (!mediaId) {
9766
+ throw new Error("MediaId is required to delete a file");
9776
9767
  }
9777
- const fileValue = (_a = doc34.values) == null ? void 0 : _a[fieldId];
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