@blackcode_sa/metaestetics-api 1.7.19 → 1.7.21

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.
@@ -34,6 +34,11 @@ import {
34
34
  } from "../../types/practitioner";
35
35
  import { ProcedureSummaryInfo } from "../../types/procedure";
36
36
  import { ClinicService } from "../clinic/clinic.service";
37
+ import {
38
+ MediaService,
39
+ MediaAccessLevel,
40
+ MediaResource,
41
+ } from "../media/media.service";
37
42
  import {
38
43
  practitionerSchema,
39
44
  createPractitionerSchema,
@@ -53,6 +58,7 @@ import { ClinicInfo } from "../../types/profile";
53
58
 
54
59
  export class PractitionerService extends BaseService {
55
60
  private clinicService?: ClinicService;
61
+ private mediaService: MediaService;
56
62
 
57
63
  constructor(
58
64
  db: Firestore,
@@ -62,6 +68,7 @@ export class PractitionerService extends BaseService {
62
68
  ) {
63
69
  super(db, auth, app);
64
70
  this.clinicService = clinicService;
71
+ this.mediaService = new MediaService(db, auth, app);
65
72
  }
66
73
 
67
74
  private getClinicService(): ClinicService {
@@ -75,6 +82,71 @@ export class PractitionerService extends BaseService {
75
82
  this.clinicService = clinicService;
76
83
  }
77
84
 
85
+ /**
86
+ * Handles profile photo upload for practitioners
87
+ * @param profilePhoto - MediaResource (File, Blob, or URL string)
88
+ * @param practitionerId - ID of the practitioner
89
+ * @returns URL string of the uploaded or existing photo
90
+ */
91
+ private async handleProfilePhotoUpload(
92
+ profilePhoto: MediaResource | undefined,
93
+ practitionerId: string
94
+ ): Promise<string | undefined> {
95
+ if (!profilePhoto) {
96
+ return undefined;
97
+ }
98
+
99
+ // If it's already a URL string, return it as is
100
+ if (typeof profilePhoto === "string") {
101
+ return profilePhoto;
102
+ }
103
+
104
+ // If it's a File or Blob, upload it
105
+ if (profilePhoto instanceof File || profilePhoto instanceof Blob) {
106
+ console.log(
107
+ `[PractitionerService] Uploading profile photo for practitioner ${practitionerId}`
108
+ );
109
+
110
+ const mediaMetadata = await this.mediaService.uploadMedia(
111
+ profilePhoto,
112
+ practitionerId, // Using practitionerId as ownerId
113
+ MediaAccessLevel.PUBLIC, // Profile photos should be public
114
+ "practitioner_profile_photos",
115
+ profilePhoto instanceof File
116
+ ? profilePhoto.name
117
+ : `profile_photo_${practitionerId}`
118
+ );
119
+
120
+ return mediaMetadata.url;
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ /**
127
+ * Processes BasicPractitionerInfo to handle profile photo uploads
128
+ * @param basicInfo - The basic info containing potential MediaResource profile photo
129
+ * @param practitionerId - ID of the practitioner
130
+ * @returns Processed basic info with URL string for profileImageUrl
131
+ */
132
+ private async processBasicInfo(
133
+ basicInfo: PractitionerBasicInfo & { profileImageUrl?: MediaResource },
134
+ practitionerId: string
135
+ ): Promise<PractitionerBasicInfo> {
136
+ const processedBasicInfo = { ...basicInfo };
137
+
138
+ // Handle profile photo upload if needed
139
+ if (basicInfo.profileImageUrl) {
140
+ const uploadedUrl = await this.handleProfilePhotoUpload(
141
+ basicInfo.profileImageUrl,
142
+ practitionerId
143
+ );
144
+ processedBasicInfo.profileImageUrl = uploadedUrl;
145
+ }
146
+
147
+ return processedBasicInfo;
148
+ }
149
+
78
150
  /**
79
151
  * Creates a new practitioner
80
152
  */
@@ -104,7 +176,10 @@ export class PractitionerService extends BaseService {
104
176
  } = {
105
177
  id: practitionerId,
106
178
  userRef: validData.userRef,
107
- basicInfo: validData.basicInfo,
179
+ basicInfo: await this.processBasicInfo(
180
+ validData.basicInfo,
181
+ practitionerId
182
+ ),
108
183
  certification: validData.certification,
109
184
  clinics: validData.clinics || [],
110
185
  clinicWorkingHours: validData.clinicWorkingHours || [],
@@ -254,7 +329,10 @@ export class PractitionerService extends BaseService {
254
329
  } = {
255
330
  id: practitionerId,
256
331
  userRef: "", // Prazno - biće popunjeno kada korisnik kreira nalog
257
- basicInfo: validatedData.basicInfo,
332
+ basicInfo: await this.processBasicInfo(
333
+ validatedData.basicInfo,
334
+ practitionerId
335
+ ),
258
336
  certification: validatedData.certification,
259
337
  clinics: clinics,
260
338
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
@@ -619,9 +697,20 @@ export class PractitionerService extends BaseService {
619
697
 
620
698
  const currentPractitioner = practitionerDoc.data() as Practitioner;
621
699
 
700
+ // Process basicInfo if it's being updated to handle profile photo uploads
701
+ let processedData = { ...validData };
702
+ if (validData.basicInfo) {
703
+ processedData.basicInfo = await this.processBasicInfo(
704
+ validData.basicInfo as PractitionerBasicInfo & {
705
+ profileImageUrl?: MediaResource;
706
+ },
707
+ practitionerId
708
+ );
709
+ }
710
+
622
711
  // Prepare update data
623
712
  const updateData = {
624
- ...validData,
713
+ ...processedData,
625
714
  updatedAt: serverTimestamp(),
626
715
  };
627
716
 
@@ -251,7 +251,10 @@ export class ProcedureService extends BaseService {
251
251
  id: practitionerSnapshot.id,
252
252
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
253
253
  description: practitioner.basicInfo.bio || "",
254
- photo: practitioner.basicInfo.profileImageUrl || "",
254
+ photo:
255
+ typeof practitioner.basicInfo.profileImageUrl === "string"
256
+ ? practitioner.basicInfo.profileImageUrl
257
+ : "", // Default to empty string if not a processed URL
255
258
  rating: practitioner.reviewInfo?.averageRating || 0,
256
259
  services: practitioner.procedures || [],
257
260
  };
@@ -412,7 +415,10 @@ export class ProcedureService extends BaseService {
412
415
  id: newPractitioner.id,
413
416
  name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
414
417
  description: newPractitioner.basicInfo.bio || "",
415
- photo: newPractitioner.basicInfo.profileImageUrl || "",
418
+ photo:
419
+ typeof newPractitioner.basicInfo.profileImageUrl === "string"
420
+ ? newPractitioner.basicInfo.profileImageUrl
421
+ : "", // Default to empty string if not a processed URL
416
422
  rating: newPractitioner.reviewInfo?.averageRating || 0,
417
423
  services: newPractitioner.procedures || [],
418
424
  };
@@ -1,24 +1,25 @@
1
- import { Timestamp, FieldValue } from 'firebase/firestore';
2
- import { User } from '..';
3
- import type { PatientMedicalInfo } from './medical-info.types';
4
- import { PATIENT_MEDICAL_INFO_COLLECTION } from './medical-info.types';
1
+ import { Timestamp, FieldValue } from "firebase/firestore";
2
+ import { User } from "..";
3
+ import type { PatientMedicalInfo } from "./medical-info.types";
4
+ import { PATIENT_MEDICAL_INFO_COLLECTION } from "./medical-info.types";
5
+ import type { MediaResource } from "../../services/media/media.service";
5
6
 
6
- export const PATIENTS_COLLECTION = 'patients';
7
- export const PATIENT_SENSITIVE_INFO_COLLECTION = 'sensitive-info';
8
- export const PATIENT_MEDICAL_HISTORY_COLLECTION = 'medical-history';
9
- export const PATIENT_APPOINTMENTS_COLLECTION = 'appointments';
10
- export const PATIENT_LOCATION_INFO_COLLECTION = 'location-info';
7
+ export const PATIENTS_COLLECTION = "patients";
8
+ export const PATIENT_SENSITIVE_INFO_COLLECTION = "sensitive-info";
9
+ export const PATIENT_MEDICAL_HISTORY_COLLECTION = "medical-history";
10
+ export const PATIENT_APPOINTMENTS_COLLECTION = "appointments";
11
+ export const PATIENT_LOCATION_INFO_COLLECTION = "location-info";
11
12
 
12
13
  /**
13
14
  * Enumeracija za pol pacijenta
14
15
  */
15
16
  export enum Gender {
16
- MALE = 'male',
17
- FEMALE = 'female',
18
- TRANSGENDER_MALE = 'transgender_male',
19
- TRANSGENDER_FEMALE = 'transgender_female',
20
- PREFER_NOT_TO_SAY = 'prefer_not_to_say',
21
- OTHER = 'other',
17
+ MALE = "male",
18
+ FEMALE = "female",
19
+ TRANSGENDER_MALE = "transgender_male",
20
+ TRANSGENDER_FEMALE = "transgender_female",
21
+ PREFER_NOT_TO_SAY = "prefer_not_to_say",
22
+ OTHER = "other",
22
23
  }
23
24
 
24
25
  /**
@@ -83,7 +84,8 @@ export interface CreatePatientLocationInfoData {
83
84
  /**
84
85
  * Tip za ažuriranje lokacijskih informacija
85
86
  */
86
- export interface UpdatePatientLocationInfoData extends Partial<CreatePatientLocationInfoData> {
87
+ export interface UpdatePatientLocationInfoData
88
+ extends Partial<CreatePatientLocationInfoData> {
87
89
  updatedAt?: FieldValue;
88
90
  }
89
91
 
@@ -93,7 +95,7 @@ export interface UpdatePatientLocationInfoData extends Partial<CreatePatientLoca
93
95
  export interface PatientSensitiveInfo {
94
96
  patientId: string;
95
97
  userRef: string;
96
- photoUrl?: string;
98
+ photoUrl?: string | null;
97
99
  firstName: string;
98
100
  lastName: string;
99
101
  dateOfBirth: Timestamp | null;
@@ -113,7 +115,7 @@ export interface PatientSensitiveInfo {
113
115
  export interface CreatePatientSensitiveInfoData {
114
116
  patientId: string;
115
117
  userRef: string;
116
- photoUrl?: string;
118
+ photoUrl?: MediaResource | null;
117
119
  firstName: string;
118
120
  lastName: string;
119
121
  dateOfBirth: Timestamp | null;
@@ -128,7 +130,8 @@ export interface CreatePatientSensitiveInfoData {
128
130
  /**
129
131
  * Tip za ažuriranje osetljivih informacija
130
132
  */
131
- export interface UpdatePatientSensitiveInfoData extends Partial<CreatePatientSensitiveInfoData> {
133
+ export interface UpdatePatientSensitiveInfoData
134
+ extends Partial<CreatePatientSensitiveInfoData> {
132
135
  updatedAt?: FieldValue;
133
136
  }
134
137
 
@@ -161,7 +164,6 @@ export interface PatientProfile {
161
164
  id: string;
162
165
  userRef: string;
163
166
  displayName: string; // Inicijali ili pseudonim
164
- profilePhoto: string | null; // URL slike
165
167
  gamification: GamificationInfo;
166
168
  expoTokens: string[];
167
169
  isActive: boolean;
@@ -196,7 +198,7 @@ export interface CreatePatientProfileData {
196
198
  * Tip za ažuriranje Patient profila
197
199
  */
198
200
  export interface UpdatePatientProfileData
199
- extends Partial<Omit<PatientProfile, 'id' | 'createdAt' | 'updatedAt'>> {
201
+ extends Partial<Omit<PatientProfile, "id" | "createdAt" | "updatedAt">> {
200
202
  // Use Omit to exclude base fields
201
203
  updatedAt?: FieldValue;
202
204
  // Note: doctors, clinics, doctorIds, clinicIds should ideally be updated via specific methods (add/removeDoctor/Clinic)
@@ -219,14 +221,14 @@ export interface RequesterInfo {
219
221
  /** ID of the clinic admin user or practitioner user making the request. */
220
222
  id: string;
221
223
  /** Role of the requester, determining the search context. */
222
- role: 'clinic_admin' | 'practitioner';
224
+ role: "clinic_admin" | "practitioner";
223
225
  /** If role is 'clinic_admin', this is the associated clinic ID. */
224
226
  associatedClinicId?: string;
225
227
  /** If role is 'practitioner', this is the associated practitioner profile ID. */
226
228
  associatedPractitionerId?: string;
227
229
  }
228
230
 
229
- export * from './medical-info.types';
231
+ export * from "./medical-info.types";
230
232
 
231
233
  // This is a type that combines all the patient data - used only in UI Frontend App
232
234
  export interface PatientProfileComplete {
@@ -11,6 +11,7 @@ import {
11
11
  PricingMeasure,
12
12
  } from "../../backoffice/types/static/pricing.types";
13
13
  import { ProcedureSummaryInfo } from "../procedure";
14
+ import type { MediaResource } from "../../services/media/media.service";
14
15
 
15
16
  export const PRACTITIONERS_COLLECTION = "practitioners";
16
17
  export const REGISTER_TOKENS_COLLECTION = "register_tokens";
@@ -26,7 +27,7 @@ export interface PractitionerBasicInfo {
26
27
  phoneNumber: string;
27
28
  dateOfBirth: Timestamp | Date;
28
29
  gender: "male" | "female" | "other";
29
- profileImageUrl?: string;
30
+ profileImageUrl?: MediaResource;
30
31
  bio?: string;
31
32
  languages: string[];
32
33
  }
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * Schema for validating both URL strings and File objects
5
5
  */
6
6
  export const mediaResourceSchema = z.union([
7
- z.string(),
7
+ z.string().url(),
8
8
  z.instanceof(File),
9
9
  z.instanceof(Blob),
10
10
  ]);
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { Timestamp } from "firebase/firestore";
3
3
  import { Gender } from "../types/patient";
4
+ import { mediaResourceSchema } from "./media.schema";
4
5
 
5
6
  /**
6
7
  * Šema za validaciju geoprostornih podataka
@@ -65,7 +66,6 @@ export const createPatientLocationInfoSchema = z.object({
65
66
  export const patientSensitiveInfoSchema = z.object({
66
67
  patientId: z.string(),
67
68
  userRef: z.string(),
68
- photoUrl: z.string().optional(),
69
69
  firstName: z.string().min(2),
70
70
  lastName: z.string().min(2),
71
71
  dateOfBirth: z.instanceof(Timestamp).nullable(),
@@ -108,11 +108,12 @@ export const patientProfileSchema = z.object({
108
108
  id: z.string(),
109
109
  userRef: z.string(),
110
110
  displayName: z.string(),
111
- profilePhoto: z.string().url().nullable(),
112
111
  gamification: gamificationSchema,
113
112
  expoTokens: z.array(z.string()),
114
113
  isActive: z.boolean(),
115
114
  isVerified: z.boolean(),
115
+ phoneNumber: z.string().nullable().optional(),
116
+ dateOfBirth: z.instanceof(Timestamp).nullable().optional(),
116
117
  doctors: z.array(patientDoctorSchema),
117
118
  clinics: z.array(patientClinicSchema),
118
119
  doctorIds: z.array(z.string()),
@@ -127,7 +128,6 @@ export const patientProfileSchema = z.object({
127
128
  export const createPatientProfileSchema = z.object({
128
129
  userRef: z.string(),
129
130
  displayName: z.string(),
130
- profilePhoto: z.string().url().nullable().optional(),
131
131
  expoTokens: z.array(z.string()),
132
132
  gamification: gamificationSchema.optional(),
133
133
  isActive: z.boolean(),
@@ -144,7 +144,7 @@ export const createPatientProfileSchema = z.object({
144
144
  export const createPatientSensitiveInfoSchema = z.object({
145
145
  patientId: z.string(),
146
146
  userRef: z.string(),
147
- photoUrl: z.string().optional(),
147
+ photoUrl: mediaResourceSchema.nullable().optional(),
148
148
  firstName: z.string().min(2),
149
149
  lastName: z.string().min(2),
150
150
  dateOfBirth: z.instanceof(Timestamp).nullable(),
@@ -15,6 +15,7 @@ import {
15
15
  PricingMeasure,
16
16
  } from "../backoffice/types/static/pricing.types";
17
17
  import { clinicInfoSchema, procedureSummaryInfoSchema } from "./shared.schema";
18
+ import { mediaResourceSchema } from "./media.schema";
18
19
 
19
20
  /**
20
21
  * Šema za validaciju osnovnih informacija o zdravstvenom radniku
@@ -27,7 +28,7 @@ export const practitionerBasicInfoSchema = z.object({
27
28
  phoneNumber: z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
28
29
  dateOfBirth: z.instanceof(Timestamp).or(z.date()),
29
30
  gender: z.enum(["male", "female", "other"]),
30
- profileImageUrl: z.string().url().optional(),
31
+ profileImageUrl: mediaResourceSchema.optional(),
31
32
  bio: z.string().max(1000).optional(),
32
33
  languages: z.array(z.string()).min(1),
33
34
  });
@@ -204,7 +205,7 @@ export const practitionerSignupSchema = z.object({
204
205
  basicInfo: z
205
206
  .object({
206
207
  phoneNumber: z.string().optional(),
207
- profileImageUrl: z.string().optional(),
208
+ profileImageUrl: mediaResourceSchema.optional(),
208
209
  gender: z.enum(["male", "female", "other"]).optional(),
209
210
  bio: z.string().optional(),
210
211
  })