@blackcode_sa/metaestetics-api 1.14.79 → 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.
@@ -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
+ };
@@ -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
+ });