@blackcode_sa/metaestetics-api 1.14.79 → 1.15.3

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.
@@ -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
+ };
@@ -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
+ };
@@ -196,6 +196,14 @@ export interface BillingPerZone {
196
196
  export interface FinalBilling {
197
197
  /** Total of all subtotals from all zones */
198
198
  subtotalAll: number;
199
+ /** Overall discount applied to the subtotal */
200
+ discount?: {
201
+ type: 'percentage' | 'fixed';
202
+ value: number; // e.g., 10 for 10% or 50 for CHF 50
203
+ amount: number; // calculated discount in currency
204
+ };
205
+ /** Subtotal after discount (subtotalAll - discount.amount) */
206
+ discountedSubtotal?: number;
199
207
  /** Tax rate as percentage (e.g., 0.20 for 20%) */
200
208
  taxRate: number;
201
209
  /** Calculated tax amount */
@@ -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 {