@blackcode_sa/metaestetics-api 1.14.78 → 1.15.2
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 +5 -0
- package/dist/admin/index.d.ts +5 -0
- package/dist/admin/index.js +47 -5
- package/dist/admin/index.mjs +47 -5
- package/dist/index.d.mts +302 -1
- package/dist/index.d.ts +302 -1
- package/dist/index.js +2655 -1754
- package/dist/index.mjs +1880 -983
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +59 -6
- package/src/services/__tests__/auth/auth.mock.test.ts +2 -2
- package/src/services/__tests__/auth/auth.setup.ts +6 -1
- package/src/services/__tests__/auth.service.test.ts +8 -44
- package/src/services/__tests__/base.service.test.ts +4 -45
- package/src/services/__tests__/user.service.test.ts +6 -4
- package/src/services/appointment/utils/appointment.utils.ts +0 -3
- package/src/services/appointment/utils/extended-procedure.utils.ts +1 -0
- package/src/services/auth/auth.v2.service.ts +7 -7
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +11 -33
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +21 -151
- package/src/services/clinic/__tests__/clinic.service.test.ts +17 -69
- package/src/services/clinic/utils/clinic-group.utils.ts +2 -2
- package/src/services/clinic/utils/clinic.utils.ts +28 -22
- package/src/services/clinic/utils/index.ts +0 -1
- package/src/services/notifications/__tests__/notification.service.test.ts +5 -5
- package/src/services/patient/__tests__/patient.service.test.ts +17 -25
- package/src/services/patient/patient.service.ts +136 -0
- package/src/services/patient/utils/body-assessment.utils.ts +159 -0
- package/src/services/patient/utils/docs.utils.ts +1 -1
- package/src/services/patient/utils/hair-scalp-assessment.utils.ts +158 -0
- package/src/services/patient/utils/pre-surgical-assessment.utils.ts +161 -0
- package/src/services/patient/utils/skin-quality-assessment.utils.ts +160 -0
- package/src/services/user/user.v2.service.ts +4 -3
- package/src/types/patient/body-assessment.types.ts +93 -0
- package/src/types/patient/hair-scalp-assessment.types.ts +98 -0
- package/src/types/patient/index.ts +4 -0
- package/src/types/patient/pre-surgical-assessment.types.ts +95 -0
- package/src/types/patient/skin-quality-assessment.types.ts +105 -0
- package/src/validations/patient/body-assessment.schema.ts +82 -0
- package/src/validations/patient/hair-scalp-assessment.schema.ts +70 -0
- package/src/validations/patient/pre-surgical-assessment.schema.ts +78 -0
- package/src/validations/patient/skin-quality-assessment.schema.ts +70 -0
|
@@ -103,8 +103,40 @@ import {
|
|
|
103
103
|
AestheticAnalysis,
|
|
104
104
|
CreateAestheticAnalysisData,
|
|
105
105
|
UpdateAestheticAnalysisData,
|
|
106
|
+
SkinQualityAssessment,
|
|
107
|
+
CreateSkinQualityAssessmentData,
|
|
108
|
+
UpdateSkinQualityAssessmentData,
|
|
109
|
+
BodyAssessment,
|
|
110
|
+
CreateBodyAssessmentData,
|
|
111
|
+
UpdateBodyAssessmentData,
|
|
112
|
+
HairScalpAssessment,
|
|
113
|
+
CreateHairScalpAssessmentData,
|
|
114
|
+
UpdateHairScalpAssessmentData,
|
|
115
|
+
PreSurgicalAssessment,
|
|
116
|
+
CreatePreSurgicalAssessmentData,
|
|
117
|
+
UpdatePreSurgicalAssessmentData,
|
|
106
118
|
} from '../../types/patient';
|
|
107
119
|
|
|
120
|
+
import {
|
|
121
|
+
getSkinQualityAssessmentUtil,
|
|
122
|
+
createOrUpdateSkinQualityAssessmentUtil,
|
|
123
|
+
} from './utils/skin-quality-assessment.utils';
|
|
124
|
+
|
|
125
|
+
import {
|
|
126
|
+
getBodyAssessmentUtil,
|
|
127
|
+
createOrUpdateBodyAssessmentUtil,
|
|
128
|
+
} from './utils/body-assessment.utils';
|
|
129
|
+
|
|
130
|
+
import {
|
|
131
|
+
getHairScalpAssessmentUtil,
|
|
132
|
+
createOrUpdateHairScalpAssessmentUtil,
|
|
133
|
+
} from './utils/hair-scalp-assessment.utils';
|
|
134
|
+
|
|
135
|
+
import {
|
|
136
|
+
getPreSurgicalAssessmentUtil,
|
|
137
|
+
createOrUpdatePreSurgicalAssessmentUtil,
|
|
138
|
+
} from './utils/pre-surgical-assessment.utils';
|
|
139
|
+
|
|
108
140
|
import { CreatePatientTokenData, PatientToken } from '../../types/patient/token.types';
|
|
109
141
|
|
|
110
142
|
export class PatientService extends BaseService {
|
|
@@ -882,4 +914,108 @@ export class PatientService extends BaseService {
|
|
|
882
914
|
true
|
|
883
915
|
);
|
|
884
916
|
}
|
|
917
|
+
|
|
918
|
+
// Skin Quality Assessment methods
|
|
919
|
+
async getSkinQualityAssessment(patientId: string): Promise<SkinQualityAssessment | null> {
|
|
920
|
+
const currentUser = await this.getCurrentUser();
|
|
921
|
+
return getSkinQualityAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async createSkinQualityAssessment(
|
|
925
|
+
patientId: string,
|
|
926
|
+
data: CreateSkinQualityAssessmentData
|
|
927
|
+
): Promise<void> {
|
|
928
|
+
const currentUser = await this.getCurrentUser();
|
|
929
|
+
return createOrUpdateSkinQualityAssessmentUtil(
|
|
930
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, false
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
async updateSkinQualityAssessment(
|
|
935
|
+
patientId: string,
|
|
936
|
+
data: UpdateSkinQualityAssessmentData
|
|
937
|
+
): Promise<void> {
|
|
938
|
+
const currentUser = await this.getCurrentUser();
|
|
939
|
+
return createOrUpdateSkinQualityAssessmentUtil(
|
|
940
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, true
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Body Assessment methods
|
|
945
|
+
async getBodyAssessment(patientId: string): Promise<BodyAssessment | null> {
|
|
946
|
+
const currentUser = await this.getCurrentUser();
|
|
947
|
+
return getBodyAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async createBodyAssessment(
|
|
951
|
+
patientId: string,
|
|
952
|
+
data: CreateBodyAssessmentData
|
|
953
|
+
): Promise<void> {
|
|
954
|
+
const currentUser = await this.getCurrentUser();
|
|
955
|
+
return createOrUpdateBodyAssessmentUtil(
|
|
956
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, false
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async updateBodyAssessment(
|
|
961
|
+
patientId: string,
|
|
962
|
+
data: UpdateBodyAssessmentData
|
|
963
|
+
): Promise<void> {
|
|
964
|
+
const currentUser = await this.getCurrentUser();
|
|
965
|
+
return createOrUpdateBodyAssessmentUtil(
|
|
966
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, true
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Hair & Scalp Assessment methods
|
|
971
|
+
async getHairScalpAssessment(patientId: string): Promise<HairScalpAssessment | null> {
|
|
972
|
+
const currentUser = await this.getCurrentUser();
|
|
973
|
+
return getHairScalpAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async createHairScalpAssessment(
|
|
977
|
+
patientId: string,
|
|
978
|
+
data: CreateHairScalpAssessmentData
|
|
979
|
+
): Promise<void> {
|
|
980
|
+
const currentUser = await this.getCurrentUser();
|
|
981
|
+
return createOrUpdateHairScalpAssessmentUtil(
|
|
982
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, false
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async updateHairScalpAssessment(
|
|
987
|
+
patientId: string,
|
|
988
|
+
data: UpdateHairScalpAssessmentData
|
|
989
|
+
): Promise<void> {
|
|
990
|
+
const currentUser = await this.getCurrentUser();
|
|
991
|
+
return createOrUpdateHairScalpAssessmentUtil(
|
|
992
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, true
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Pre-Surgical Assessment methods
|
|
997
|
+
async getPreSurgicalAssessment(patientId: string): Promise<PreSurgicalAssessment | null> {
|
|
998
|
+
const currentUser = await this.getCurrentUser();
|
|
999
|
+
return getPreSurgicalAssessmentUtil(this.db, patientId, currentUser.uid, currentUser.roles);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async createPreSurgicalAssessment(
|
|
1003
|
+
patientId: string,
|
|
1004
|
+
data: CreatePreSurgicalAssessmentData
|
|
1005
|
+
): Promise<void> {
|
|
1006
|
+
const currentUser = await this.getCurrentUser();
|
|
1007
|
+
return createOrUpdatePreSurgicalAssessmentUtil(
|
|
1008
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, false
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async updatePreSurgicalAssessment(
|
|
1013
|
+
patientId: string,
|
|
1014
|
+
data: UpdatePreSurgicalAssessmentData
|
|
1015
|
+
): Promise<void> {
|
|
1016
|
+
const currentUser = await this.getCurrentUser();
|
|
1017
|
+
return createOrUpdatePreSurgicalAssessmentUtil(
|
|
1018
|
+
this.db, patientId, data, currentUser.uid, currentUser.roles, true
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
885
1021
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
|
|
2
|
+
import {
|
|
3
|
+
BodyAssessment,
|
|
4
|
+
CreateBodyAssessmentData,
|
|
5
|
+
UpdateBodyAssessmentData,
|
|
6
|
+
BODY_ASSESSMENT_COLLECTION,
|
|
7
|
+
PATIENTS_COLLECTION,
|
|
8
|
+
BodyAssessmentStatus,
|
|
9
|
+
} from '../../../types/patient';
|
|
10
|
+
import { UserRole } from '../../../types';
|
|
11
|
+
import {
|
|
12
|
+
createBodyAssessmentSchema,
|
|
13
|
+
updateBodyAssessmentSchema,
|
|
14
|
+
bodyAssessmentSchema,
|
|
15
|
+
} from '../../../validations/patient/body-assessment.schema';
|
|
16
|
+
import { getPatientDocRef } from './docs.utils';
|
|
17
|
+
import { AuthError } from '../../../errors/auth.errors';
|
|
18
|
+
import { getPractitionerProfileByUserRef } from './practitioner.utils';
|
|
19
|
+
import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
|
|
20
|
+
|
|
21
|
+
export const getBodyAssessmentDocRef = (db: Firestore, patientId: string) => {
|
|
22
|
+
return doc(db, PATIENTS_COLLECTION, patientId, BODY_ASSESSMENT_COLLECTION, patientId);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const checkBodyAssessmentAccessUtil = async (
|
|
26
|
+
db: Firestore,
|
|
27
|
+
patientId: string,
|
|
28
|
+
requesterId: string,
|
|
29
|
+
requesterRoles: UserRole[]
|
|
30
|
+
): Promise<void> => {
|
|
31
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
32
|
+
if (!patientDoc.exists()) {
|
|
33
|
+
throw new Error('Patient profile not found');
|
|
34
|
+
}
|
|
35
|
+
const patientData = patientDoc.data() as any;
|
|
36
|
+
|
|
37
|
+
if (patientData.userRef && patientData.userRef === requesterId) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
42
|
+
const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
|
|
43
|
+
if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
49
|
+
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
50
|
+
if (adminProfile && adminProfile.clinicsManaged) {
|
|
51
|
+
const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
|
|
52
|
+
patientData.clinicIds?.includes(managedClinicId)
|
|
53
|
+
);
|
|
54
|
+
if (hasAccess) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new AuthError(
|
|
61
|
+
'Unauthorized access to body assessment.',
|
|
62
|
+
'AUTH/UNAUTHORIZED_ACCESS',
|
|
63
|
+
403
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const calculateBodyAssessmentCompletionPercentage = (data: Partial<BodyAssessment>): number => {
|
|
68
|
+
let completed = 0;
|
|
69
|
+
const total = 4;
|
|
70
|
+
|
|
71
|
+
if (data.selectedZones && data.selectedZones.length > 0) completed++;
|
|
72
|
+
if (data.zoneAssessments && data.zoneAssessments.length > 0) completed++;
|
|
73
|
+
if (data.composition && (data.composition.bmi || data.composition.bodyFatPercentage || data.composition.waistHipRatio)) completed++;
|
|
74
|
+
if ((data.measurements && data.measurements.waist) || (data.symmetry && data.symmetry.overallSymmetry)) completed++;
|
|
75
|
+
|
|
76
|
+
return Math.round((completed / total) * 100);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const determineBodyAssessmentStatus = (completionPercentage: number): BodyAssessmentStatus => {
|
|
80
|
+
if (completionPercentage < 50) return 'incomplete';
|
|
81
|
+
if (completionPercentage >= 50) return 'ready_for_planning';
|
|
82
|
+
return 'incomplete';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const getBodyAssessmentUtil = async (
|
|
86
|
+
db: Firestore,
|
|
87
|
+
patientId: string,
|
|
88
|
+
requesterId: string,
|
|
89
|
+
requesterRoles: UserRole[]
|
|
90
|
+
): Promise<BodyAssessment | null> => {
|
|
91
|
+
await checkBodyAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
92
|
+
|
|
93
|
+
const docRef = getBodyAssessmentDocRef(db, patientId);
|
|
94
|
+
const snapshot = await getDoc(docRef);
|
|
95
|
+
|
|
96
|
+
if (!snapshot.exists()) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = snapshot.data();
|
|
101
|
+
return bodyAssessmentSchema.parse({
|
|
102
|
+
...data,
|
|
103
|
+
id: patientId,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const createOrUpdateBodyAssessmentUtil = async (
|
|
108
|
+
db: Firestore,
|
|
109
|
+
patientId: string,
|
|
110
|
+
data: CreateBodyAssessmentData | UpdateBodyAssessmentData,
|
|
111
|
+
requesterId: string,
|
|
112
|
+
requesterRoles: UserRole[],
|
|
113
|
+
isUpdate: boolean = false
|
|
114
|
+
): Promise<void> => {
|
|
115
|
+
await checkBodyAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
116
|
+
|
|
117
|
+
const validatedData = isUpdate
|
|
118
|
+
? updateBodyAssessmentSchema.parse(data)
|
|
119
|
+
: createBodyAssessmentSchema.parse(data);
|
|
120
|
+
|
|
121
|
+
const docRef = getBodyAssessmentDocRef(db, patientId);
|
|
122
|
+
const snapshot = await getDoc(docRef);
|
|
123
|
+
|
|
124
|
+
const requesterRole = requesterRoles.includes(UserRole.PRACTITIONER) ? 'PRACTITIONER' : 'PATIENT';
|
|
125
|
+
|
|
126
|
+
const existingData = snapshot.exists() ? snapshot.data() : null;
|
|
127
|
+
const mergedData: any = {
|
|
128
|
+
...(existingData || {}),
|
|
129
|
+
...validatedData,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const completionPercentage = calculateBodyAssessmentCompletionPercentage(mergedData);
|
|
133
|
+
const status = determineBodyAssessmentStatus(completionPercentage);
|
|
134
|
+
|
|
135
|
+
if (!snapshot.exists()) {
|
|
136
|
+
await setDoc(docRef, {
|
|
137
|
+
id: patientId,
|
|
138
|
+
patientId,
|
|
139
|
+
selectedZones: [],
|
|
140
|
+
zoneAssessments: [],
|
|
141
|
+
...validatedData,
|
|
142
|
+
completionPercentage,
|
|
143
|
+
status,
|
|
144
|
+
lastUpdatedBy: requesterId,
|
|
145
|
+
lastUpdatedByRole: requesterRole,
|
|
146
|
+
createdAt: serverTimestamp(),
|
|
147
|
+
updatedAt: serverTimestamp(),
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
await updateDoc(docRef, {
|
|
151
|
+
...validatedData,
|
|
152
|
+
completionPercentage,
|
|
153
|
+
status,
|
|
154
|
+
lastUpdatedBy: requesterId,
|
|
155
|
+
lastUpdatedByRole: requesterRole,
|
|
156
|
+
updatedAt: serverTimestamp(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
};
|
|
@@ -120,7 +120,7 @@ export const initSensitiveInfoDocIfNotExists = async (
|
|
|
120
120
|
)
|
|
121
121
|
);
|
|
122
122
|
|
|
123
|
-
await createSensitiveInfoUtil(db, defaultSensitiveInfo, userRef);
|
|
123
|
+
await createSensitiveInfoUtil(db, defaultSensitiveInfo, userRef, []);
|
|
124
124
|
|
|
125
125
|
// Verify document was created
|
|
126
126
|
const verifyDoc = await getDoc(sensitiveInfoRef);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
|
|
2
|
+
import {
|
|
3
|
+
HairScalpAssessment,
|
|
4
|
+
CreateHairScalpAssessmentData,
|
|
5
|
+
UpdateHairScalpAssessmentData,
|
|
6
|
+
HAIR_SCALP_ASSESSMENT_COLLECTION,
|
|
7
|
+
PATIENTS_COLLECTION,
|
|
8
|
+
HairScalpAssessmentStatus,
|
|
9
|
+
} from '../../../types/patient';
|
|
10
|
+
import { UserRole } from '../../../types';
|
|
11
|
+
import {
|
|
12
|
+
createHairScalpAssessmentSchema,
|
|
13
|
+
updateHairScalpAssessmentSchema,
|
|
14
|
+
hairScalpAssessmentSchema,
|
|
15
|
+
} from '../../../validations/patient/hair-scalp-assessment.schema';
|
|
16
|
+
import { getPatientDocRef } from './docs.utils';
|
|
17
|
+
import { AuthError } from '../../../errors/auth.errors';
|
|
18
|
+
import { getPractitionerProfileByUserRef } from './practitioner.utils';
|
|
19
|
+
import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
|
|
20
|
+
|
|
21
|
+
export const getHairScalpAssessmentDocRef = (db: Firestore, patientId: string) => {
|
|
22
|
+
return doc(db, PATIENTS_COLLECTION, patientId, HAIR_SCALP_ASSESSMENT_COLLECTION, patientId);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const checkHairScalpAssessmentAccessUtil = async (
|
|
26
|
+
db: Firestore,
|
|
27
|
+
patientId: string,
|
|
28
|
+
requesterId: string,
|
|
29
|
+
requesterRoles: UserRole[]
|
|
30
|
+
): Promise<void> => {
|
|
31
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
32
|
+
if (!patientDoc.exists()) {
|
|
33
|
+
throw new Error('Patient profile not found');
|
|
34
|
+
}
|
|
35
|
+
const patientData = patientDoc.data() as any;
|
|
36
|
+
|
|
37
|
+
if (patientData.userRef && patientData.userRef === requesterId) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
42
|
+
const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
|
|
43
|
+
if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
49
|
+
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
50
|
+
if (adminProfile && adminProfile.clinicsManaged) {
|
|
51
|
+
const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
|
|
52
|
+
patientData.clinicIds?.includes(managedClinicId)
|
|
53
|
+
);
|
|
54
|
+
if (hasAccess) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new AuthError(
|
|
61
|
+
'Unauthorized access to hair & scalp assessment.',
|
|
62
|
+
'AUTH/UNAUTHORIZED_ACCESS',
|
|
63
|
+
403
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const calculateHairScalpCompletionPercentage = (data: Partial<HairScalpAssessment>): number => {
|
|
68
|
+
let completed = 0;
|
|
69
|
+
const total = 4;
|
|
70
|
+
|
|
71
|
+
if (data.hairLossPattern && data.hairLossPattern.type) completed++;
|
|
72
|
+
if (data.hairCharacteristics && data.hairCharacteristics.type) completed++;
|
|
73
|
+
if (data.scalpCondition) completed++;
|
|
74
|
+
if (data.zoneAssessments && data.zoneAssessments.length > 0) completed++;
|
|
75
|
+
|
|
76
|
+
return Math.round((completed / total) * 100);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const determineHairScalpStatus = (completionPercentage: number): HairScalpAssessmentStatus => {
|
|
80
|
+
if (completionPercentage < 50) return 'incomplete';
|
|
81
|
+
if (completionPercentage >= 50) return 'ready_for_planning';
|
|
82
|
+
return 'incomplete';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const getHairScalpAssessmentUtil = async (
|
|
86
|
+
db: Firestore,
|
|
87
|
+
patientId: string,
|
|
88
|
+
requesterId: string,
|
|
89
|
+
requesterRoles: UserRole[]
|
|
90
|
+
): Promise<HairScalpAssessment | null> => {
|
|
91
|
+
await checkHairScalpAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
92
|
+
|
|
93
|
+
const docRef = getHairScalpAssessmentDocRef(db, patientId);
|
|
94
|
+
const snapshot = await getDoc(docRef);
|
|
95
|
+
|
|
96
|
+
if (!snapshot.exists()) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = snapshot.data();
|
|
101
|
+
return hairScalpAssessmentSchema.parse({
|
|
102
|
+
...data,
|
|
103
|
+
id: patientId,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const createOrUpdateHairScalpAssessmentUtil = async (
|
|
108
|
+
db: Firestore,
|
|
109
|
+
patientId: string,
|
|
110
|
+
data: CreateHairScalpAssessmentData | UpdateHairScalpAssessmentData,
|
|
111
|
+
requesterId: string,
|
|
112
|
+
requesterRoles: UserRole[],
|
|
113
|
+
isUpdate: boolean = false
|
|
114
|
+
): Promise<void> => {
|
|
115
|
+
await checkHairScalpAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
116
|
+
|
|
117
|
+
const validatedData = isUpdate
|
|
118
|
+
? updateHairScalpAssessmentSchema.parse(data)
|
|
119
|
+
: createHairScalpAssessmentSchema.parse(data);
|
|
120
|
+
|
|
121
|
+
const docRef = getHairScalpAssessmentDocRef(db, patientId);
|
|
122
|
+
const snapshot = await getDoc(docRef);
|
|
123
|
+
|
|
124
|
+
const requesterRole = requesterRoles.includes(UserRole.PRACTITIONER) ? 'PRACTITIONER' : 'PATIENT';
|
|
125
|
+
|
|
126
|
+
const existingData = snapshot.exists() ? snapshot.data() : null;
|
|
127
|
+
const mergedData: any = {
|
|
128
|
+
...(existingData || {}),
|
|
129
|
+
...validatedData,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const completionPercentage = calculateHairScalpCompletionPercentage(mergedData);
|
|
133
|
+
const status = determineHairScalpStatus(completionPercentage);
|
|
134
|
+
|
|
135
|
+
if (!snapshot.exists()) {
|
|
136
|
+
await setDoc(docRef, {
|
|
137
|
+
id: patientId,
|
|
138
|
+
patientId,
|
|
139
|
+
zoneAssessments: [],
|
|
140
|
+
...validatedData,
|
|
141
|
+
completionPercentage,
|
|
142
|
+
status,
|
|
143
|
+
lastUpdatedBy: requesterId,
|
|
144
|
+
lastUpdatedByRole: requesterRole,
|
|
145
|
+
createdAt: serverTimestamp(),
|
|
146
|
+
updatedAt: serverTimestamp(),
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
await updateDoc(docRef, {
|
|
150
|
+
...validatedData,
|
|
151
|
+
completionPercentage,
|
|
152
|
+
status,
|
|
153
|
+
lastUpdatedBy: requesterId,
|
|
154
|
+
lastUpdatedByRole: requesterRole,
|
|
155
|
+
updatedAt: serverTimestamp(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
|
|
2
|
+
import {
|
|
3
|
+
PreSurgicalAssessment,
|
|
4
|
+
CreatePreSurgicalAssessmentData,
|
|
5
|
+
UpdatePreSurgicalAssessmentData,
|
|
6
|
+
PRE_SURGICAL_ASSESSMENT_COLLECTION,
|
|
7
|
+
PATIENTS_COLLECTION,
|
|
8
|
+
PreSurgicalAssessmentStatus,
|
|
9
|
+
} from '../../../types/patient';
|
|
10
|
+
import { UserRole } from '../../../types';
|
|
11
|
+
import {
|
|
12
|
+
createPreSurgicalAssessmentSchema,
|
|
13
|
+
updatePreSurgicalAssessmentSchema,
|
|
14
|
+
preSurgicalAssessmentSchema,
|
|
15
|
+
} from '../../../validations/patient/pre-surgical-assessment.schema';
|
|
16
|
+
import { getPatientDocRef } from './docs.utils';
|
|
17
|
+
import { AuthError } from '../../../errors/auth.errors';
|
|
18
|
+
import { getPractitionerProfileByUserRef } from './practitioner.utils';
|
|
19
|
+
import { getClinicAdminByUserRef } from '../../clinic/utils/admin.utils';
|
|
20
|
+
|
|
21
|
+
export const getPreSurgicalAssessmentDocRef = (db: Firestore, patientId: string) => {
|
|
22
|
+
return doc(db, PATIENTS_COLLECTION, patientId, PRE_SURGICAL_ASSESSMENT_COLLECTION, patientId);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const checkPreSurgicalAssessmentAccessUtil = async (
|
|
26
|
+
db: Firestore,
|
|
27
|
+
patientId: string,
|
|
28
|
+
requesterId: string,
|
|
29
|
+
requesterRoles: UserRole[]
|
|
30
|
+
): Promise<void> => {
|
|
31
|
+
const patientDoc = await getDoc(getPatientDocRef(db, patientId));
|
|
32
|
+
if (!patientDoc.exists()) {
|
|
33
|
+
throw new Error('Patient profile not found');
|
|
34
|
+
}
|
|
35
|
+
const patientData = patientDoc.data() as any;
|
|
36
|
+
|
|
37
|
+
if (patientData.userRef && patientData.userRef === requesterId) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (requesterRoles.includes(UserRole.PRACTITIONER)) {
|
|
42
|
+
const practitionerProfile = await getPractitionerProfileByUserRef(db, requesterId);
|
|
43
|
+
if (practitionerProfile && patientData.doctorIds?.includes(practitionerProfile.id)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (requesterRoles.includes(UserRole.CLINIC_ADMIN)) {
|
|
49
|
+
const adminProfile = await getClinicAdminByUserRef(db, requesterId);
|
|
50
|
+
if (adminProfile && adminProfile.clinicsManaged) {
|
|
51
|
+
const hasAccess = adminProfile.clinicsManaged.some((managedClinicId) =>
|
|
52
|
+
patientData.clinicIds?.includes(managedClinicId)
|
|
53
|
+
);
|
|
54
|
+
if (hasAccess) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new AuthError(
|
|
61
|
+
'Unauthorized access to pre-surgical assessment.',
|
|
62
|
+
'AUTH/UNAUTHORIZED_ACCESS',
|
|
63
|
+
403
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const calculatePreSurgicalCompletionPercentage = (data: Partial<PreSurgicalAssessment>): number => {
|
|
68
|
+
let completed = 0;
|
|
69
|
+
const total = 5;
|
|
70
|
+
|
|
71
|
+
if (data.asaClassification) completed++;
|
|
72
|
+
if (data.anesthesiaHistory) completed++;
|
|
73
|
+
if (data.bleedingRisk) completed++;
|
|
74
|
+
if (data.surgicalSiteAssessments && data.surgicalSiteAssessments.length > 0) completed++;
|
|
75
|
+
if ((data.labResults && data.labResults.length > 0) || data.clearance === 'obtained') completed++;
|
|
76
|
+
|
|
77
|
+
return Math.round((completed / total) * 100);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const determinePreSurgicalStatus = (completionPercentage: number): PreSurgicalAssessmentStatus => {
|
|
81
|
+
if (completionPercentage < 40) return 'incomplete';
|
|
82
|
+
if (completionPercentage >= 40) return 'ready_for_planning';
|
|
83
|
+
return 'incomplete';
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const getPreSurgicalAssessmentUtil = async (
|
|
87
|
+
db: Firestore,
|
|
88
|
+
patientId: string,
|
|
89
|
+
requesterId: string,
|
|
90
|
+
requesterRoles: UserRole[]
|
|
91
|
+
): Promise<PreSurgicalAssessment | null> => {
|
|
92
|
+
await checkPreSurgicalAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
93
|
+
|
|
94
|
+
const docRef = getPreSurgicalAssessmentDocRef(db, patientId);
|
|
95
|
+
const snapshot = await getDoc(docRef);
|
|
96
|
+
|
|
97
|
+
if (!snapshot.exists()) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const data = snapshot.data();
|
|
102
|
+
return preSurgicalAssessmentSchema.parse({
|
|
103
|
+
...data,
|
|
104
|
+
id: patientId,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const createOrUpdatePreSurgicalAssessmentUtil = async (
|
|
109
|
+
db: Firestore,
|
|
110
|
+
patientId: string,
|
|
111
|
+
data: CreatePreSurgicalAssessmentData | UpdatePreSurgicalAssessmentData,
|
|
112
|
+
requesterId: string,
|
|
113
|
+
requesterRoles: UserRole[],
|
|
114
|
+
isUpdate: boolean = false
|
|
115
|
+
): Promise<void> => {
|
|
116
|
+
await checkPreSurgicalAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
117
|
+
|
|
118
|
+
const validatedData = isUpdate
|
|
119
|
+
? updatePreSurgicalAssessmentSchema.parse(data)
|
|
120
|
+
: createPreSurgicalAssessmentSchema.parse(data);
|
|
121
|
+
|
|
122
|
+
const docRef = getPreSurgicalAssessmentDocRef(db, patientId);
|
|
123
|
+
const snapshot = await getDoc(docRef);
|
|
124
|
+
|
|
125
|
+
const requesterRole = requesterRoles.includes(UserRole.PRACTITIONER) ? 'PRACTITIONER' : 'PATIENT';
|
|
126
|
+
|
|
127
|
+
const existingData = snapshot.exists() ? snapshot.data() : null;
|
|
128
|
+
const mergedData: any = {
|
|
129
|
+
...(existingData || {}),
|
|
130
|
+
...validatedData,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const completionPercentage = calculatePreSurgicalCompletionPercentage(mergedData);
|
|
134
|
+
const status = determinePreSurgicalStatus(completionPercentage);
|
|
135
|
+
|
|
136
|
+
if (!snapshot.exists()) {
|
|
137
|
+
await setDoc(docRef, {
|
|
138
|
+
id: patientId,
|
|
139
|
+
patientId,
|
|
140
|
+
surgicalSiteAssessments: [],
|
|
141
|
+
labResults: [],
|
|
142
|
+
clearance: 'not_required',
|
|
143
|
+
...validatedData,
|
|
144
|
+
completionPercentage,
|
|
145
|
+
status,
|
|
146
|
+
lastUpdatedBy: requesterId,
|
|
147
|
+
lastUpdatedByRole: requesterRole,
|
|
148
|
+
createdAt: serverTimestamp(),
|
|
149
|
+
updatedAt: serverTimestamp(),
|
|
150
|
+
});
|
|
151
|
+
} else {
|
|
152
|
+
await updateDoc(docRef, {
|
|
153
|
+
...validatedData,
|
|
154
|
+
completionPercentage,
|
|
155
|
+
status,
|
|
156
|
+
lastUpdatedBy: requesterId,
|
|
157
|
+
lastUpdatedByRole: requesterRole,
|
|
158
|
+
updatedAt: serverTimestamp(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
};
|