@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.
- package/dist/admin/index.d.mts +1 -2
- package/dist/admin/index.d.ts +1 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/backoffice/index.d.mts +6 -1
- package/dist/backoffice/index.d.ts +6 -1
- package/dist/index.d.mts +80 -45
- package/dist/index.d.ts +80 -45
- package/dist/index.js +1635 -1407
- package/dist/index.mjs +1656 -1426
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +4 -1
- package/src/services/patient/patient.service.ts +162 -10
- package/src/services/patient/utils/profile.utils.ts +124 -135
- package/src/services/patient/utils/sensitive.utils.ts +76 -2
- package/src/services/practitioner/practitioner.service.ts +92 -3
- package/src/services/procedure/procedure.service.ts +8 -2
- package/src/types/patient/index.ts +25 -23
- package/src/types/practitioner/index.ts +2 -1
- package/src/validations/media.schema.ts +1 -1
- package/src/validations/patient.schema.ts +4 -4
- package/src/validations/practitioner.schema.ts +3 -2
|
@@ -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:
|
|
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:
|
|
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
|
-
...
|
|
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:
|
|
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:
|
|
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
|
|
2
|
-
import { User } from
|
|
3
|
-
import type { PatientMedicalInfo } from
|
|
4
|
-
import { PATIENT_MEDICAL_INFO_COLLECTION } from
|
|
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 =
|
|
7
|
-
export const PATIENT_SENSITIVE_INFO_COLLECTION =
|
|
8
|
-
export const PATIENT_MEDICAL_HISTORY_COLLECTION =
|
|
9
|
-
export const PATIENT_APPOINTMENTS_COLLECTION =
|
|
10
|
-
export const PATIENT_LOCATION_INFO_COLLECTION =
|
|
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 =
|
|
17
|
-
FEMALE =
|
|
18
|
-
TRANSGENDER_MALE =
|
|
19
|
-
TRANSGENDER_FEMALE =
|
|
20
|
-
PREFER_NOT_TO_SAY =
|
|
21
|
-
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
|
|
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?:
|
|
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
|
|
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,
|
|
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:
|
|
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
|
|
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?:
|
|
30
|
+
profileImageUrl?: MediaResource;
|
|
30
31
|
bio?: string;
|
|
31
32
|
languages: string[];
|
|
32
33
|
}
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
208
|
+
profileImageUrl: mediaResourceSchema.optional(),
|
|
208
209
|
gender: z.enum(["male", "female", "other"]).optional(),
|
|
209
210
|
bio: z.string().optional(),
|
|
210
211
|
})
|