@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.
Files changed (42) hide show
  1. package/dist/admin/index.d.mts +5 -0
  2. package/dist/admin/index.d.ts +5 -0
  3. package/dist/admin/index.js +47 -5
  4. package/dist/admin/index.mjs +47 -5
  5. package/dist/index.d.mts +302 -1
  6. package/dist/index.d.ts +302 -1
  7. package/dist/index.js +2655 -1754
  8. package/dist/index.mjs +1880 -983
  9. package/package.json +1 -1
  10. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +59 -6
  11. package/src/services/__tests__/auth/auth.mock.test.ts +2 -2
  12. package/src/services/__tests__/auth/auth.setup.ts +6 -1
  13. package/src/services/__tests__/auth.service.test.ts +8 -44
  14. package/src/services/__tests__/base.service.test.ts +4 -45
  15. package/src/services/__tests__/user.service.test.ts +6 -4
  16. package/src/services/appointment/utils/appointment.utils.ts +0 -3
  17. package/src/services/appointment/utils/extended-procedure.utils.ts +1 -0
  18. package/src/services/auth/auth.v2.service.ts +7 -7
  19. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +11 -33
  20. package/src/services/clinic/__tests__/clinic-group.service.test.ts +21 -151
  21. package/src/services/clinic/__tests__/clinic.service.test.ts +17 -69
  22. package/src/services/clinic/utils/clinic-group.utils.ts +2 -2
  23. package/src/services/clinic/utils/clinic.utils.ts +28 -22
  24. package/src/services/clinic/utils/index.ts +0 -1
  25. package/src/services/notifications/__tests__/notification.service.test.ts +5 -5
  26. package/src/services/patient/__tests__/patient.service.test.ts +17 -25
  27. package/src/services/patient/patient.service.ts +136 -0
  28. package/src/services/patient/utils/body-assessment.utils.ts +159 -0
  29. package/src/services/patient/utils/docs.utils.ts +1 -1
  30. package/src/services/patient/utils/hair-scalp-assessment.utils.ts +158 -0
  31. package/src/services/patient/utils/pre-surgical-assessment.utils.ts +161 -0
  32. package/src/services/patient/utils/skin-quality-assessment.utils.ts +160 -0
  33. package/src/services/user/user.v2.service.ts +4 -3
  34. package/src/types/patient/body-assessment.types.ts +93 -0
  35. package/src/types/patient/hair-scalp-assessment.types.ts +98 -0
  36. package/src/types/patient/index.ts +4 -0
  37. package/src/types/patient/pre-surgical-assessment.types.ts +95 -0
  38. package/src/types/patient/skin-quality-assessment.types.ts +105 -0
  39. package/src/validations/patient/body-assessment.schema.ts +82 -0
  40. package/src/validations/patient/hair-scalp-assessment.schema.ts +70 -0
  41. package/src/validations/patient/pre-surgical-assessment.schema.ts +78 -0
  42. 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
+ });