@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getDoc, updateDoc, setDoc, serverTimestamp, Firestore, doc } from 'firebase/firestore';
|
|
2
|
+
import {
|
|
3
|
+
SkinQualityAssessment,
|
|
4
|
+
CreateSkinQualityAssessmentData,
|
|
5
|
+
UpdateSkinQualityAssessmentData,
|
|
6
|
+
SKIN_QUALITY_ASSESSMENT_COLLECTION,
|
|
7
|
+
PATIENTS_COLLECTION,
|
|
8
|
+
SkinQualityAssessmentStatus,
|
|
9
|
+
} from '../../../types/patient';
|
|
10
|
+
import { UserRole } from '../../../types';
|
|
11
|
+
import {
|
|
12
|
+
createSkinQualityAssessmentSchema,
|
|
13
|
+
updateSkinQualityAssessmentSchema,
|
|
14
|
+
skinQualityAssessmentSchema,
|
|
15
|
+
} from '../../../validations/patient/skin-quality-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 getSkinQualityAssessmentDocRef = (db: Firestore, patientId: string) => {
|
|
22
|
+
return doc(db, PATIENTS_COLLECTION, patientId, SKIN_QUALITY_ASSESSMENT_COLLECTION, patientId);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const checkSkinQualityAssessmentAccessUtil = 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 skin quality assessment.',
|
|
62
|
+
'AUTH/UNAUTHORIZED_ACCESS',
|
|
63
|
+
403
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const calculateSkinQualityCompletionPercentage = (data: Partial<SkinQualityAssessment>): number => {
|
|
68
|
+
let completed = 0;
|
|
69
|
+
const total = 4;
|
|
70
|
+
|
|
71
|
+
if (data.characteristics) completed++;
|
|
72
|
+
if (data.conditions && data.conditions.length > 0) completed++;
|
|
73
|
+
if (data.zoneAssessments && data.zoneAssessments.length > 0) completed++;
|
|
74
|
+
if (data.scales && (data.scales.fitzpatrick || data.scales.glogau || data.scales.elastosis)) completed++;
|
|
75
|
+
|
|
76
|
+
return Math.round((completed / total) * 100);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const determineSkinQualityStatus = (completionPercentage: number): SkinQualityAssessmentStatus => {
|
|
80
|
+
if (completionPercentage < 50) return 'incomplete';
|
|
81
|
+
if (completionPercentage >= 50) return 'ready_for_planning';
|
|
82
|
+
return 'incomplete';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const getSkinQualityAssessmentUtil = async (
|
|
86
|
+
db: Firestore,
|
|
87
|
+
patientId: string,
|
|
88
|
+
requesterId: string,
|
|
89
|
+
requesterRoles: UserRole[]
|
|
90
|
+
): Promise<SkinQualityAssessment | null> => {
|
|
91
|
+
await checkSkinQualityAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
92
|
+
|
|
93
|
+
const docRef = getSkinQualityAssessmentDocRef(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 skinQualityAssessmentSchema.parse({
|
|
102
|
+
...data,
|
|
103
|
+
id: patientId,
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const createOrUpdateSkinQualityAssessmentUtil = async (
|
|
108
|
+
db: Firestore,
|
|
109
|
+
patientId: string,
|
|
110
|
+
data: CreateSkinQualityAssessmentData | UpdateSkinQualityAssessmentData,
|
|
111
|
+
requesterId: string,
|
|
112
|
+
requesterRoles: UserRole[],
|
|
113
|
+
isUpdate: boolean = false
|
|
114
|
+
): Promise<void> => {
|
|
115
|
+
await checkSkinQualityAssessmentAccessUtil(db, patientId, requesterId, requesterRoles);
|
|
116
|
+
|
|
117
|
+
const validatedData = isUpdate
|
|
118
|
+
? updateSkinQualityAssessmentSchema.parse(data)
|
|
119
|
+
: createSkinQualityAssessmentSchema.parse(data);
|
|
120
|
+
|
|
121
|
+
const docRef = getSkinQualityAssessmentDocRef(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 = calculateSkinQualityCompletionPercentage(mergedData);
|
|
133
|
+
const status = determineSkinQualityStatus(completionPercentage);
|
|
134
|
+
|
|
135
|
+
if (!snapshot.exists()) {
|
|
136
|
+
await setDoc(docRef, {
|
|
137
|
+
id: patientId,
|
|
138
|
+
patientId,
|
|
139
|
+
conditions: [],
|
|
140
|
+
zoneAssessments: [],
|
|
141
|
+
scales: {},
|
|
142
|
+
...validatedData,
|
|
143
|
+
completionPercentage,
|
|
144
|
+
status,
|
|
145
|
+
lastUpdatedBy: requesterId,
|
|
146
|
+
lastUpdatedByRole: requesterRole,
|
|
147
|
+
createdAt: serverTimestamp(),
|
|
148
|
+
updatedAt: serverTimestamp(),
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
await updateDoc(docRef, {
|
|
152
|
+
...validatedData,
|
|
153
|
+
completionPercentage,
|
|
154
|
+
status,
|
|
155
|
+
lastUpdatedBy: requesterId,
|
|
156
|
+
lastUpdatedByRole: requesterRole,
|
|
157
|
+
updatedAt: serverTimestamp(),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
@@ -170,6 +170,7 @@ export class UserServiceV2 extends BaseService {
|
|
|
170
170
|
},
|
|
171
171
|
isActive: true,
|
|
172
172
|
isVerified: false,
|
|
173
|
+
isManual: false,
|
|
173
174
|
});
|
|
174
175
|
profiles.patientProfile = patientProfile.id;
|
|
175
176
|
break;
|
|
@@ -260,7 +261,7 @@ export class UserServiceV2 extends BaseService {
|
|
|
260
261
|
}
|
|
261
262
|
|
|
262
263
|
const userData = userDoc.data();
|
|
263
|
-
return userSchema.parse(userData);
|
|
264
|
+
return userSchema.parse(userData) as User;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
/**
|
|
@@ -274,7 +275,7 @@ export class UserServiceV2 extends BaseService {
|
|
|
274
275
|
if (querySnapshot.empty) return null;
|
|
275
276
|
|
|
276
277
|
const userData = querySnapshot.docs[0].data();
|
|
277
|
-
return userSchema.parse(userData);
|
|
278
|
+
return userSchema.parse(userData) as User;
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
async getUsersByRole(role: UserRole): Promise<User[]> {
|
|
@@ -285,7 +286,7 @@ export class UserServiceV2 extends BaseService {
|
|
|
285
286
|
const querySnapshot = await getDocs(q);
|
|
286
287
|
|
|
287
288
|
const users = querySnapshot.docs.map((doc) => doc.data());
|
|
288
|
-
return Promise.all(users.map((userData) => userSchema.parse(userData)));
|
|
289
|
+
return Promise.all(users.map((userData) => userSchema.parse(userData) as User));
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
/**
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Timestamp } from 'firebase/firestore';
|
|
2
|
+
|
|
3
|
+
export const BODY_ASSESSMENT_COLLECTION = 'body-assessment';
|
|
4
|
+
|
|
5
|
+
export type BodyAssessmentStatus = 'incomplete' | 'ready_for_planning' | 'treatment_planned';
|
|
6
|
+
|
|
7
|
+
export type BodyZone =
|
|
8
|
+
| 'abdomen'
|
|
9
|
+
| 'flanks'
|
|
10
|
+
| 'back'
|
|
11
|
+
| 'upper_arms'
|
|
12
|
+
| 'thighs_inner'
|
|
13
|
+
| 'thighs_outer'
|
|
14
|
+
| 'buttocks'
|
|
15
|
+
| 'chest'
|
|
16
|
+
| 'knees'
|
|
17
|
+
| 'submental';
|
|
18
|
+
|
|
19
|
+
export type SeverityLevel = 'none' | 'mild' | 'moderate' | 'severe';
|
|
20
|
+
export type MuscleDefinitionLevel = 'well_defined' | 'moderate' | 'poor' | 'none';
|
|
21
|
+
|
|
22
|
+
export interface BodyZoneAssessment {
|
|
23
|
+
zone: BodyZone;
|
|
24
|
+
fatDeposit: SeverityLevel;
|
|
25
|
+
skinLaxity: SeverityLevel;
|
|
26
|
+
stretchMarks: SeverityLevel;
|
|
27
|
+
cellulite: SeverityLevel;
|
|
28
|
+
muscleDefinition: MuscleDefinitionLevel;
|
|
29
|
+
notes?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BodyCompositionData {
|
|
33
|
+
bmi?: number;
|
|
34
|
+
bodyFatPercentage?: number;
|
|
35
|
+
waistHipRatio?: number;
|
|
36
|
+
weight?: number;
|
|
37
|
+
height?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BodyMeasurementsData {
|
|
41
|
+
waist?: number;
|
|
42
|
+
hips?: number;
|
|
43
|
+
chest?: number;
|
|
44
|
+
upperArm?: number;
|
|
45
|
+
thigh?: number;
|
|
46
|
+
units: 'cm' | 'inches';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BodySymmetryData {
|
|
50
|
+
overallSymmetry: 'symmetric' | 'mild_asymmetry' | 'moderate_asymmetry' | 'significant_asymmetry';
|
|
51
|
+
notes?: string;
|
|
52
|
+
affectedAreas?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BodyAssessment {
|
|
56
|
+
id: string;
|
|
57
|
+
patientId: string;
|
|
58
|
+
appointmentId?: string;
|
|
59
|
+
|
|
60
|
+
selectedZones: BodyZone[];
|
|
61
|
+
zoneAssessments: BodyZoneAssessment[];
|
|
62
|
+
composition?: BodyCompositionData;
|
|
63
|
+
measurements?: BodyMeasurementsData;
|
|
64
|
+
symmetry?: BodySymmetryData;
|
|
65
|
+
|
|
66
|
+
completionPercentage: number;
|
|
67
|
+
status: BodyAssessmentStatus;
|
|
68
|
+
|
|
69
|
+
lastUpdatedBy: string;
|
|
70
|
+
lastUpdatedByRole: 'PATIENT' | 'PRACTITIONER';
|
|
71
|
+
|
|
72
|
+
createdAt?: Timestamp | any;
|
|
73
|
+
updatedAt?: Timestamp | any;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CreateBodyAssessmentData {
|
|
77
|
+
patientId: string;
|
|
78
|
+
appointmentId?: string;
|
|
79
|
+
selectedZones?: BodyZone[];
|
|
80
|
+
zoneAssessments?: BodyZoneAssessment[];
|
|
81
|
+
composition?: BodyCompositionData;
|
|
82
|
+
measurements?: BodyMeasurementsData;
|
|
83
|
+
symmetry?: BodySymmetryData;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface UpdateBodyAssessmentData {
|
|
87
|
+
appointmentId?: string;
|
|
88
|
+
selectedZones?: BodyZone[];
|
|
89
|
+
zoneAssessments?: BodyZoneAssessment[];
|
|
90
|
+
composition?: BodyCompositionData;
|
|
91
|
+
measurements?: BodyMeasurementsData;
|
|
92
|
+
symmetry?: BodySymmetryData;
|
|
93
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Timestamp } from 'firebase/firestore';
|
|
2
|
+
|
|
3
|
+
export const HAIR_SCALP_ASSESSMENT_COLLECTION = 'hair-scalp-assessment';
|
|
4
|
+
|
|
5
|
+
export type HairScalpAssessmentStatus = 'incomplete' | 'ready_for_planning' | 'treatment_planned';
|
|
6
|
+
|
|
7
|
+
export type NorwoodStage = 'I' | 'II' | 'IIa' | 'III' | 'IIIa' | 'III_vertex' | 'IV' | 'IVa' | 'V' | 'Va' | 'VI' | 'VII';
|
|
8
|
+
export type LudwigStage = 'I' | 'II' | 'III';
|
|
9
|
+
export type HairLossType = 'androgenetic' | 'diffuse' | 'areata' | 'traction' | 'scarring' | 'telogen_effluvium';
|
|
10
|
+
|
|
11
|
+
export interface HairLossPattern {
|
|
12
|
+
type: HairLossType;
|
|
13
|
+
norwoodStage?: NorwoodStage;
|
|
14
|
+
ludwigStage?: LudwigStage;
|
|
15
|
+
onsetDuration?: string;
|
|
16
|
+
familyHistory?: boolean;
|
|
17
|
+
notes?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type HairType = 'straight' | 'wavy' | 'curly' | 'coily';
|
|
21
|
+
export type HairDensity = 'high' | 'medium' | 'low' | 'very_low';
|
|
22
|
+
export type HairColor = 'black' | 'dark_brown' | 'brown' | 'light_brown' | 'blonde' | 'red' | 'grey' | 'white';
|
|
23
|
+
export type HairTextureGrade = 'fine' | 'medium' | 'coarse';
|
|
24
|
+
|
|
25
|
+
export interface HairCharacteristics {
|
|
26
|
+
type: HairType;
|
|
27
|
+
density: HairDensity;
|
|
28
|
+
color: HairColor;
|
|
29
|
+
texture: HairTextureGrade;
|
|
30
|
+
diameter?: 'thin' | 'medium' | 'thick';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ScalpScalinessLevel = 'none' | 'mild' | 'moderate' | 'severe';
|
|
34
|
+
export type ScalpRednessLevel = 'none' | 'mild' | 'moderate' | 'severe';
|
|
35
|
+
export type ScalpSebumLevel = 'dry' | 'normal' | 'oily' | 'very_oily';
|
|
36
|
+
|
|
37
|
+
export interface ScalpCondition {
|
|
38
|
+
scaliness: ScalpScalinessLevel;
|
|
39
|
+
redness: ScalpRednessLevel;
|
|
40
|
+
sebum: ScalpSebumLevel;
|
|
41
|
+
folliculitis: boolean;
|
|
42
|
+
scarring: boolean;
|
|
43
|
+
notes?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type HairLossZone =
|
|
47
|
+
| 'frontal'
|
|
48
|
+
| 'temporal_left'
|
|
49
|
+
| 'temporal_right'
|
|
50
|
+
| 'mid_scalp'
|
|
51
|
+
| 'vertex'
|
|
52
|
+
| 'occipital'
|
|
53
|
+
| 'parietal_left'
|
|
54
|
+
| 'parietal_right';
|
|
55
|
+
|
|
56
|
+
export interface HairLossZoneAssessment {
|
|
57
|
+
zone: HairLossZone;
|
|
58
|
+
miniaturization: 'none' | 'mild' | 'moderate' | 'severe';
|
|
59
|
+
density: HairDensity;
|
|
60
|
+
notes?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface HairScalpAssessment {
|
|
64
|
+
id: string;
|
|
65
|
+
patientId: string;
|
|
66
|
+
appointmentId?: string;
|
|
67
|
+
|
|
68
|
+
hairLossPattern?: HairLossPattern;
|
|
69
|
+
hairCharacteristics?: HairCharacteristics;
|
|
70
|
+
scalpCondition?: ScalpCondition;
|
|
71
|
+
zoneAssessments: HairLossZoneAssessment[];
|
|
72
|
+
|
|
73
|
+
completionPercentage: number;
|
|
74
|
+
status: HairScalpAssessmentStatus;
|
|
75
|
+
|
|
76
|
+
lastUpdatedBy: string;
|
|
77
|
+
lastUpdatedByRole: 'PATIENT' | 'PRACTITIONER';
|
|
78
|
+
|
|
79
|
+
createdAt?: Timestamp | any;
|
|
80
|
+
updatedAt?: Timestamp | any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CreateHairScalpAssessmentData {
|
|
84
|
+
patientId: string;
|
|
85
|
+
appointmentId?: string;
|
|
86
|
+
hairLossPattern?: HairLossPattern;
|
|
87
|
+
hairCharacteristics?: HairCharacteristics;
|
|
88
|
+
scalpCondition?: ScalpCondition;
|
|
89
|
+
zoneAssessments?: HairLossZoneAssessment[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface UpdateHairScalpAssessmentData {
|
|
93
|
+
appointmentId?: string;
|
|
94
|
+
hairLossPattern?: HairLossPattern;
|
|
95
|
+
hairCharacteristics?: HairCharacteristics;
|
|
96
|
+
scalpCondition?: ScalpCondition;
|
|
97
|
+
zoneAssessments?: HairLossZoneAssessment[];
|
|
98
|
+
}
|
|
@@ -259,6 +259,10 @@ export interface RequesterInfo {
|
|
|
259
259
|
|
|
260
260
|
export * from "./medical-info.types";
|
|
261
261
|
export * from "./aesthetic-analysis.types";
|
|
262
|
+
export * from "./skin-quality-assessment.types";
|
|
263
|
+
export * from "./body-assessment.types";
|
|
264
|
+
export * from "./hair-scalp-assessment.types";
|
|
265
|
+
export * from "./pre-surgical-assessment.types";
|
|
262
266
|
|
|
263
267
|
// This is a type that combines all the patient data - used only in UI Frontend App
|
|
264
268
|
export interface PatientProfileComplete {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Timestamp } from 'firebase/firestore';
|
|
2
|
+
|
|
3
|
+
export const PRE_SURGICAL_ASSESSMENT_COLLECTION = 'pre-surgical-assessment';
|
|
4
|
+
|
|
5
|
+
export type PreSurgicalAssessmentStatus = 'incomplete' | 'ready_for_planning' | 'treatment_planned';
|
|
6
|
+
|
|
7
|
+
export type ASAClassification = 'I' | 'II' | 'III' | 'IV';
|
|
8
|
+
|
|
9
|
+
export interface AnesthesiaHistory {
|
|
10
|
+
previousAnesthesia: boolean;
|
|
11
|
+
adverseReactions?: boolean;
|
|
12
|
+
reactionDetails?: string;
|
|
13
|
+
preferredType?: 'local' | 'regional' | 'general' | 'sedation';
|
|
14
|
+
malignantHyperthermiaRisk?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type BleedingRiskLevel = 'low' | 'moderate' | 'high';
|
|
18
|
+
|
|
19
|
+
export interface BleedingRisk {
|
|
20
|
+
level: BleedingRiskLevel;
|
|
21
|
+
anticoagulantUse: boolean;
|
|
22
|
+
anticoagulantDetails?: string;
|
|
23
|
+
bleedingDisorderHistory: boolean;
|
|
24
|
+
bleedingDisorderDetails?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type TissueQualityLevel = 'excellent' | 'good' | 'fair' | 'poor';
|
|
28
|
+
|
|
29
|
+
export interface SurgicalSiteAssessment {
|
|
30
|
+
site: string;
|
|
31
|
+
skinQuality: TissueQualityLevel;
|
|
32
|
+
scarringHistory: 'none' | 'normal' | 'hypertrophic' | 'keloid';
|
|
33
|
+
tissueQuality: TissueQualityLevel;
|
|
34
|
+
previousSurgery: boolean;
|
|
35
|
+
previousSurgeryDetails?: string;
|
|
36
|
+
notes?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface LabResult {
|
|
40
|
+
name: string;
|
|
41
|
+
value: string;
|
|
42
|
+
unit?: string;
|
|
43
|
+
date?: string;
|
|
44
|
+
isNormal?: boolean;
|
|
45
|
+
notes?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SmokingStatus = 'never' | 'former' | 'current' | 'quit_recently';
|
|
49
|
+
export type ClearanceStatus = 'not_required' | 'pending' | 'obtained' | 'denied';
|
|
50
|
+
|
|
51
|
+
export interface PreSurgicalAssessment {
|
|
52
|
+
id: string;
|
|
53
|
+
patientId: string;
|
|
54
|
+
appointmentId?: string;
|
|
55
|
+
|
|
56
|
+
asaClassification?: ASAClassification;
|
|
57
|
+
anesthesiaHistory?: AnesthesiaHistory;
|
|
58
|
+
bleedingRisk?: BleedingRisk;
|
|
59
|
+
surgicalSiteAssessments: SurgicalSiteAssessment[];
|
|
60
|
+
labResults: LabResult[];
|
|
61
|
+
smokingStatus?: SmokingStatus;
|
|
62
|
+
clearance: ClearanceStatus;
|
|
63
|
+
|
|
64
|
+
completionPercentage: number;
|
|
65
|
+
status: PreSurgicalAssessmentStatus;
|
|
66
|
+
|
|
67
|
+
lastUpdatedBy: string;
|
|
68
|
+
lastUpdatedByRole: 'PATIENT' | 'PRACTITIONER';
|
|
69
|
+
|
|
70
|
+
createdAt?: Timestamp | any;
|
|
71
|
+
updatedAt?: Timestamp | any;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CreatePreSurgicalAssessmentData {
|
|
75
|
+
patientId: string;
|
|
76
|
+
appointmentId?: string;
|
|
77
|
+
asaClassification?: ASAClassification;
|
|
78
|
+
anesthesiaHistory?: AnesthesiaHistory;
|
|
79
|
+
bleedingRisk?: BleedingRisk;
|
|
80
|
+
surgicalSiteAssessments?: SurgicalSiteAssessment[];
|
|
81
|
+
labResults?: LabResult[];
|
|
82
|
+
smokingStatus?: SmokingStatus;
|
|
83
|
+
clearance?: ClearanceStatus;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface UpdatePreSurgicalAssessmentData {
|
|
87
|
+
appointmentId?: string;
|
|
88
|
+
asaClassification?: ASAClassification;
|
|
89
|
+
anesthesiaHistory?: AnesthesiaHistory;
|
|
90
|
+
bleedingRisk?: BleedingRisk;
|
|
91
|
+
surgicalSiteAssessments?: SurgicalSiteAssessment[];
|
|
92
|
+
labResults?: LabResult[];
|
|
93
|
+
smokingStatus?: SmokingStatus;
|
|
94
|
+
clearance?: ClearanceStatus;
|
|
95
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Timestamp } from 'firebase/firestore';
|
|
2
|
+
|
|
3
|
+
export const SKIN_QUALITY_ASSESSMENT_COLLECTION = 'skin-quality-assessment';
|
|
4
|
+
|
|
5
|
+
export type SkinQualityAssessmentStatus = 'incomplete' | 'ready_for_planning' | 'treatment_planned';
|
|
6
|
+
|
|
7
|
+
export type SkinHydrationLevel = 'very_dry' | 'dry' | 'normal' | 'oily' | 'very_oily';
|
|
8
|
+
export type SkinSensitivityLevel = 'none' | 'mild' | 'moderate' | 'severe';
|
|
9
|
+
export type SkinElasticityLevel = 'excellent' | 'good' | 'fair' | 'poor';
|
|
10
|
+
export type PoreSizeLevel = 'fine' | 'small' | 'medium' | 'large' | 'very_large';
|
|
11
|
+
export type SkinTextureLevel = 'smooth' | 'slightly_rough' | 'rough' | 'very_rough';
|
|
12
|
+
|
|
13
|
+
export interface SkinCharacteristics {
|
|
14
|
+
hydration: SkinHydrationLevel;
|
|
15
|
+
oiliness: SkinHydrationLevel;
|
|
16
|
+
sensitivity: SkinSensitivityLevel;
|
|
17
|
+
elasticity: SkinElasticityLevel;
|
|
18
|
+
poreSize: PoreSizeLevel;
|
|
19
|
+
texture: SkinTextureLevel;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SkinConditionType =
|
|
23
|
+
| 'acne'
|
|
24
|
+
| 'rosacea'
|
|
25
|
+
| 'melasma'
|
|
26
|
+
| 'hyperpigmentation'
|
|
27
|
+
| 'hypopigmentation'
|
|
28
|
+
| 'eczema'
|
|
29
|
+
| 'psoriasis'
|
|
30
|
+
| 'sun_damage'
|
|
31
|
+
| 'scarring'
|
|
32
|
+
| 'keratosis';
|
|
33
|
+
|
|
34
|
+
export interface SkinConditionEntry {
|
|
35
|
+
type: SkinConditionType;
|
|
36
|
+
severity: string;
|
|
37
|
+
location?: string;
|
|
38
|
+
notes?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SkinZone =
|
|
42
|
+
| 'forehead'
|
|
43
|
+
| 'periorbital'
|
|
44
|
+
| 'cheeks'
|
|
45
|
+
| 'nose'
|
|
46
|
+
| 'perioral'
|
|
47
|
+
| 'chin'
|
|
48
|
+
| 'neck'
|
|
49
|
+
| 'decolletage';
|
|
50
|
+
|
|
51
|
+
export interface SkinZoneAssessment {
|
|
52
|
+
zone: SkinZone;
|
|
53
|
+
pigmentation: string;
|
|
54
|
+
texture: string;
|
|
55
|
+
vascularity: string;
|
|
56
|
+
photoaging: string;
|
|
57
|
+
notes?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type FitzpatrickType = 'I' | 'II' | 'III' | 'IV' | 'V' | 'VI';
|
|
61
|
+
export type GlogauClassification = 'I' | 'II' | 'III' | 'IV';
|
|
62
|
+
export type ElastosisGrade = 'none' | 'mild' | 'moderate' | 'severe';
|
|
63
|
+
|
|
64
|
+
export interface SkinQualityScales {
|
|
65
|
+
fitzpatrick?: FitzpatrickType;
|
|
66
|
+
glogau?: GlogauClassification;
|
|
67
|
+
elastosis?: ElastosisGrade;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SkinQualityAssessment {
|
|
71
|
+
id: string;
|
|
72
|
+
patientId: string;
|
|
73
|
+
appointmentId?: string;
|
|
74
|
+
|
|
75
|
+
characteristics?: SkinCharacteristics;
|
|
76
|
+
conditions: SkinConditionEntry[];
|
|
77
|
+
zoneAssessments: SkinZoneAssessment[];
|
|
78
|
+
scales: SkinQualityScales;
|
|
79
|
+
|
|
80
|
+
completionPercentage: number;
|
|
81
|
+
status: SkinQualityAssessmentStatus;
|
|
82
|
+
|
|
83
|
+
lastUpdatedBy: string;
|
|
84
|
+
lastUpdatedByRole: 'PATIENT' | 'PRACTITIONER';
|
|
85
|
+
|
|
86
|
+
createdAt?: Timestamp | any;
|
|
87
|
+
updatedAt?: Timestamp | any;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CreateSkinQualityAssessmentData {
|
|
91
|
+
patientId: string;
|
|
92
|
+
appointmentId?: string;
|
|
93
|
+
characteristics?: SkinCharacteristics;
|
|
94
|
+
conditions?: SkinConditionEntry[];
|
|
95
|
+
zoneAssessments?: SkinZoneAssessment[];
|
|
96
|
+
scales?: SkinQualityScales;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface UpdateSkinQualityAssessmentData {
|
|
100
|
+
appointmentId?: string;
|
|
101
|
+
characteristics?: SkinCharacteristics;
|
|
102
|
+
conditions?: SkinConditionEntry[];
|
|
103
|
+
zoneAssessments?: SkinZoneAssessment[];
|
|
104
|
+
scales?: SkinQualityScales;
|
|
105
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const bodyZoneAssessmentSchema = z.object({
|
|
4
|
+
zone: z.enum([
|
|
5
|
+
'abdomen', 'flanks', 'back', 'upper_arms', 'thighs_inner',
|
|
6
|
+
'thighs_outer', 'buttocks', 'chest', 'knees', 'submental',
|
|
7
|
+
]),
|
|
8
|
+
fatDeposit: z.enum(['none', 'mild', 'moderate', 'severe']),
|
|
9
|
+
skinLaxity: z.enum(['none', 'mild', 'moderate', 'severe']),
|
|
10
|
+
stretchMarks: z.enum(['none', 'mild', 'moderate', 'severe']),
|
|
11
|
+
cellulite: z.enum(['none', 'mild', 'moderate', 'severe']),
|
|
12
|
+
muscleDefinition: z.enum(['well_defined', 'moderate', 'poor', 'none']),
|
|
13
|
+
notes: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const bodyCompositionSchema = z.object({
|
|
17
|
+
bmi: z.number().optional(),
|
|
18
|
+
bodyFatPercentage: z.number().min(0).max(100).optional(),
|
|
19
|
+
waistHipRatio: z.number().optional(),
|
|
20
|
+
weight: z.number().optional(),
|
|
21
|
+
height: z.number().optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const bodyMeasurementsSchema = z.object({
|
|
25
|
+
waist: z.number().optional(),
|
|
26
|
+
hips: z.number().optional(),
|
|
27
|
+
chest: z.number().optional(),
|
|
28
|
+
upperArm: z.number().optional(),
|
|
29
|
+
thigh: z.number().optional(),
|
|
30
|
+
units: z.enum(['cm', 'inches']),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const bodySymmetrySchema = z.object({
|
|
34
|
+
overallSymmetry: z.enum(['symmetric', 'mild_asymmetry', 'moderate_asymmetry', 'significant_asymmetry']),
|
|
35
|
+
notes: z.string().optional(),
|
|
36
|
+
affectedAreas: z.array(z.string()).optional(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const createBodyAssessmentSchema = z.object({
|
|
40
|
+
patientId: z.string().min(1, 'Patient ID is required'),
|
|
41
|
+
appointmentId: z.string().optional(),
|
|
42
|
+
selectedZones: z.array(z.enum([
|
|
43
|
+
'abdomen', 'flanks', 'back', 'upper_arms', 'thighs_inner',
|
|
44
|
+
'thighs_outer', 'buttocks', 'chest', 'knees', 'submental',
|
|
45
|
+
])).optional(),
|
|
46
|
+
zoneAssessments: z.array(bodyZoneAssessmentSchema).optional(),
|
|
47
|
+
composition: bodyCompositionSchema.optional(),
|
|
48
|
+
measurements: bodyMeasurementsSchema.optional(),
|
|
49
|
+
symmetry: bodySymmetrySchema.optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const updateBodyAssessmentSchema = z.object({
|
|
53
|
+
appointmentId: z.string().optional(),
|
|
54
|
+
selectedZones: z.array(z.enum([
|
|
55
|
+
'abdomen', 'flanks', 'back', 'upper_arms', 'thighs_inner',
|
|
56
|
+
'thighs_outer', 'buttocks', 'chest', 'knees', 'submental',
|
|
57
|
+
])).optional(),
|
|
58
|
+
zoneAssessments: z.array(bodyZoneAssessmentSchema).optional(),
|
|
59
|
+
composition: bodyCompositionSchema.optional(),
|
|
60
|
+
measurements: bodyMeasurementsSchema.optional(),
|
|
61
|
+
symmetry: bodySymmetrySchema.optional(),
|
|
62
|
+
}).partial();
|
|
63
|
+
|
|
64
|
+
export const bodyAssessmentSchema = z.object({
|
|
65
|
+
id: z.string(),
|
|
66
|
+
patientId: z.string(),
|
|
67
|
+
appointmentId: z.string().optional(),
|
|
68
|
+
selectedZones: z.array(z.enum([
|
|
69
|
+
'abdomen', 'flanks', 'back', 'upper_arms', 'thighs_inner',
|
|
70
|
+
'thighs_outer', 'buttocks', 'chest', 'knees', 'submental',
|
|
71
|
+
])),
|
|
72
|
+
zoneAssessments: z.array(bodyZoneAssessmentSchema),
|
|
73
|
+
composition: bodyCompositionSchema.optional(),
|
|
74
|
+
measurements: bodyMeasurementsSchema.optional(),
|
|
75
|
+
symmetry: bodySymmetrySchema.optional(),
|
|
76
|
+
completionPercentage: z.number().min(0).max(100),
|
|
77
|
+
status: z.enum(['incomplete', 'ready_for_planning', 'treatment_planned']),
|
|
78
|
+
lastUpdatedBy: z.string(),
|
|
79
|
+
lastUpdatedByRole: z.enum(['PATIENT', 'PRACTITIONER']),
|
|
80
|
+
createdAt: z.any(),
|
|
81
|
+
updatedAt: z.any(),
|
|
82
|
+
});
|