emr-types
Version:
Comprehensive TypeScript Types Library for Electronic Medical Record (EMR) Applications - Domain-Driven Design with Zod Validation
615 lines • 24.5 kB
JavaScript
import { z } from 'zod';
import { AppointmentStatus, AppointmentType } from '../domains/appointment';
// ============================================================================
// BASE SCHEMAS
// ============================================================================
export const AppointmentIdSchema = z.string().uuid().brand();
export const TimeSlotSchema = z.object({
startTime: z.date(),
endTime: z.date()
}).refine((data) => data.startTime < data.endTime, {
message: 'Start time must be before end time',
path: ['endTime']
});
export const DurationSchema = z.object({
minutes: z.number().positive().max(1440), // max 24 hours
hours: z.number().nonnegative(),
days: z.number().nonnegative()
}).refine((data) => data.minutes > 0 || data.hours > 0 || data.days > 0, {
message: 'Duration must be greater than zero',
path: ['minutes']
});
// ============================================================================
// ENUM SCHEMAS
// ============================================================================
export const AppointmentStatusSchema = z.nativeEnum(AppointmentStatus);
export const AppointmentTypeSchema = z.nativeEnum(AppointmentType);
// ============================================================================
// VALUE OBJECT SCHEMAS
// ============================================================================
export const AppointmentIdValueObjectSchema = z.object({
value: AppointmentIdSchema,
format: z.enum(['uuid', 'auto-increment', 'custom']),
createdAt: z.date(),
metadata: z.record(z.unknown()).optional()
});
export const TimeSlotValueObjectSchema = z.object({
value: TimeSlotSchema,
isAvailable: z.boolean(),
isBooked: z.boolean(),
isBlocked: z.boolean(),
blockReason: z.string().max(200).optional(),
createdAt: z.date()
});
export const DurationValueObjectSchema = z.object({
value: DurationSchema,
isStandard: z.boolean(),
isCustom: z.boolean(),
createdAt: z.date()
});
// ============================================================================
// ENTITY SCHEMAS
// ============================================================================
export const AppointmentSlotSchema = z.object({
id: z.string().uuid(),
appointmentId: AppointmentIdSchema.optional(),
timeSlot: TimeSlotValueObjectSchema,
duration: DurationValueObjectSchema,
isAvailable: z.boolean(),
isBooked: z.boolean(),
isBlocked: z.boolean(),
blockReason: z.string().max(200).optional(),
recurringPattern: z.object({
frequency: z.enum(['daily', 'weekly', 'monthly', 'yearly']),
interval: z.number().positive(),
endDate: z.date().optional(),
maxOccurrences: z.number().positive().optional(),
daysOfWeek: z.array(z.number().min(0).max(6)).optional(), // 0 = Sunday
dayOfMonth: z.number().min(1).max(31).optional(),
monthOfYear: z.number().min(1).max(12).optional()
}).optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export const AppointmentSchema = z.object({
id: AppointmentIdSchema,
patientId: z.string().uuid(),
providerId: z.string().uuid(),
timeSlot: TimeSlotValueObjectSchema,
duration: DurationValueObjectSchema,
type: AppointmentTypeSchema,
status: AppointmentStatusSchema,
reason: z.string().min(1).max(500),
notes: z.string().max(1000).optional(),
symptoms: z.array(z.string().max(100)).optional(),
diagnosis: z.array(z.object({
condition: z.string().min(1).max(100),
icd10Code: z.string().max(20).optional(),
severity: z.enum(['mild', 'moderate', 'severe']),
notes: z.string().max(500).optional()
})).optional(),
prescriptions: z.array(z.object({
medication: z.string().min(1).max(100),
dosage: z.string().max(50),
frequency: z.string().max(50),
duration: z.string().max(50),
instructions: z.string().max(500).optional(),
prescribedBy: z.string().uuid(),
prescribedAt: z.date()
})).optional(),
procedures: z.array(z.object({
name: z.string().min(1).max(100),
cptCode: z.string().max(20).optional(),
description: z.string().max(500).optional(),
performedBy: z.string().uuid(),
performedAt: z.date().optional(),
notes: z.string().max(500).optional()
})).optional(),
labOrders: z.array(z.object({
testName: z.string().min(1).max(100),
loincCode: z.string().max(20).optional(),
labName: z.string().max(100),
orderedBy: z.string().uuid(),
orderedAt: z.date(),
results: z.array(z.object({
testName: z.string().min(1).max(100),
value: z.string().max(100),
unit: z.string().max(20).optional(),
referenceRange: z.string().max(100).optional(),
isAbnormal: z.boolean(),
notes: z.string().max(500).optional(),
reportedAt: z.date()
})).optional()
})).optional(),
imagingOrders: z.array(z.object({
studyName: z.string().min(1).max(100),
modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']),
bodyPart: z.string().max(100),
facility: z.string().max(100),
orderedBy: z.string().uuid(),
orderedAt: z.date(),
results: z.object({
report: z.string().max(1000),
images: z.array(z.string().url()),
radiologist: z.string().max(100).optional(),
reportedAt: z.date()
}).optional()
})).optional(),
referrals: z.array(z.object({
specialist: z.string().max(100),
specialty: z.string().max(100),
reason: z.string().max(500),
urgency: z.enum(['routine', 'urgent', 'emergency']),
referredBy: z.string().uuid(),
referredAt: z.date(),
notes: z.string().max(500).optional()
})).optional(),
followUp: z.object({
isRequired: z.boolean(),
recommendedDate: z.date().optional(),
type: AppointmentTypeSchema.optional(),
reason: z.string().max(500).optional(),
notes: z.string().max(500).optional()
}).optional(),
billing: z.object({
totalAmount: z.number().nonnegative(),
insuranceAmount: z.number().nonnegative(),
patientAmount: z.number().nonnegative(),
copay: z.number().nonnegative(),
deductible: z.number().nonnegative(),
status: z.enum(['pending', 'billed', 'paid', 'denied', 'appealed']),
invoiceNumber: z.string().max(50).optional(),
billedAt: z.date().optional(),
paidAt: z.date().optional()
}).optional(),
reminders: z.array(z.object({
type: z.enum(['email', 'sms', 'phone', 'push']),
scheduledAt: z.date(),
sentAt: z.date().optional(),
status: z.enum(['scheduled', 'sent', 'failed', 'cancelled']),
message: z.string().max(500).optional()
})).optional(),
attachments: z.array(z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
type: z.enum(['document', 'image', 'video', 'audio']),
url: z.string().url(),
size: z.number().positive(),
mimeType: z.string(),
uploadedBy: z.string().uuid(),
uploadedAt: z.date()
})).optional(),
tags: z.array(z.string().max(50)).optional(),
createdAt: z.date(),
updatedAt: z.date(),
createdBy: z.string().uuid().optional(),
updatedBy: z.string().uuid().optional(),
cancelledAt: z.date().optional(),
cancelledBy: z.string().uuid().optional(),
cancellationReason: z.string().max(500).optional()
});
// ============================================================================
// REQUEST/INPUT SCHEMAS
// ============================================================================
export const CreateAppointmentRequestSchema = z.object({
patientId: z.string().uuid(),
providerId: z.string().uuid(),
startTime: z.date(),
endTime: z.date(),
type: AppointmentTypeSchema,
reason: z.string().min(1).max(500),
notes: z.string().max(1000).optional(),
symptoms: z.array(z.string().max(100)).optional(),
isRecurring: z.boolean().optional(),
recurringPattern: z.object({
frequency: z.enum(['daily', 'weekly', 'monthly', 'yearly']),
interval: z.number().positive(),
endDate: z.date().optional(),
maxOccurrences: z.number().positive().optional(),
daysOfWeek: z.array(z.number().min(0).max(6)).optional(),
dayOfMonth: z.number().min(1).max(31).optional(),
monthOfYear: z.number().min(1).max(12).optional()
}).optional(),
reminders: z.array(z.object({
type: z.enum(['email', 'sms', 'phone', 'push']),
minutesBefore: z.number().positive(),
message: z.string().max(500).optional()
})).optional(),
tags: z.array(z.string().max(50)).optional()
}).refine((data) => data.startTime < data.endTime, {
message: 'Start time must be before end time',
path: ['endTime']
});
export const UpdateAppointmentRequestSchema = z.object({
startTime: z.date().optional(),
endTime: z.date().optional(),
type: AppointmentTypeSchema.optional(),
status: AppointmentStatusSchema.optional(),
reason: z.string().min(1).max(500).optional(),
notes: z.string().max(1000).optional(),
symptoms: z.array(z.string().max(100)).optional(),
diagnosis: z.array(z.object({
condition: z.string().min(1).max(100),
icd10Code: z.string().max(20).optional(),
severity: z.enum(['mild', 'moderate', 'severe']),
notes: z.string().max(500).optional()
})).optional(),
prescriptions: z.array(z.object({
medication: z.string().min(1).max(100),
dosage: z.string().max(50),
frequency: z.string().max(50),
duration: z.string().max(50),
instructions: z.string().max(500).optional()
})).optional(),
procedures: z.array(z.object({
name: z.string().min(1).max(100),
cptCode: z.string().max(20).optional(),
description: z.string().max(500).optional(),
notes: z.string().max(500).optional()
})).optional(),
labOrders: z.array(z.object({
testName: z.string().min(1).max(100),
loincCode: z.string().max(20).optional(),
labName: z.string().max(100)
})).optional(),
imagingOrders: z.array(z.object({
studyName: z.string().min(1).max(100),
modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']),
bodyPart: z.string().max(100),
facility: z.string().max(100)
})).optional(),
referrals: z.array(z.object({
specialist: z.string().max(100),
specialty: z.string().max(100),
reason: z.string().max(500),
urgency: z.enum(['routine', 'urgent', 'emergency']),
notes: z.string().max(500).optional()
})).optional(),
followUp: z.object({
isRequired: z.boolean(),
recommendedDate: z.date().optional(),
type: AppointmentTypeSchema.optional(),
reason: z.string().max(500).optional(),
notes: z.string().max(500).optional()
}).optional(),
tags: z.array(z.string().max(50)).optional()
});
export const CancelAppointmentRequestSchema = z.object({
reason: z.string().min(1).max(500),
sendNotification: z.boolean().optional()
});
export const RescheduleAppointmentRequestSchema = z.object({
newStartTime: z.date(),
newEndTime: z.date(),
reason: z.string().max(500).optional(),
sendNotification: z.boolean().optional()
}).refine((data) => data.newStartTime < data.newEndTime, {
message: 'New start time must be before new end time',
path: ['newEndTime']
});
export const AddDiagnosisRequestSchema = z.object({
condition: z.string().min(1).max(100),
icd10Code: z.string().max(20).optional(),
severity: z.enum(['mild', 'moderate', 'severe']),
notes: z.string().max(500).optional()
});
export const AddPrescriptionRequestSchema = z.object({
medication: z.string().min(1).max(100),
dosage: z.string().max(50),
frequency: z.string().max(50),
duration: z.string().max(50),
instructions: z.string().max(500).optional()
});
export const AddProcedureRequestSchema = z.object({
name: z.string().min(1).max(100),
cptCode: z.string().max(20).optional(),
description: z.string().max(500).optional(),
notes: z.string().max(500).optional()
});
export const AddLabOrderRequestSchema = z.object({
testName: z.string().min(1).max(100),
loincCode: z.string().max(20).optional(),
labName: z.string().max(100)
});
export const AddImagingOrderRequestSchema = z.object({
studyName: z.string().min(1).max(100),
modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']),
bodyPart: z.string().max(100),
facility: z.string().max(100)
});
export const AddReferralRequestSchema = z.object({
specialist: z.string().max(100),
specialty: z.string().max(100),
reason: z.string().max(500),
urgency: z.enum(['routine', 'urgent', 'emergency']),
notes: z.string().max(500).optional()
});
export const ScheduleFollowUpRequestSchema = z.object({
recommendedDate: z.date(),
type: AppointmentTypeSchema,
reason: z.string().max(500),
notes: z.string().max(500).optional()
});
// ============================================================================
// RESPONSE SCHEMAS
// ============================================================================
export const AppointmentResponseSchema = z.object({
id: AppointmentIdSchema,
patientId: z.string().uuid(),
providerId: z.string().uuid(),
startTime: z.date(),
endTime: z.date(),
type: AppointmentTypeSchema,
status: AppointmentStatusSchema,
reason: z.string(),
notes: z.string().optional(),
symptoms: z.array(z.string()).optional(),
diagnosis: z.array(z.object({
condition: z.string(),
icd10Code: z.string().optional(),
severity: z.enum(['mild', 'moderate', 'severe']),
notes: z.string().optional()
})).optional(),
prescriptions: z.array(z.object({
medication: z.string(),
dosage: z.string(),
frequency: z.string(),
duration: z.string(),
instructions: z.string().optional()
})).optional(),
procedures: z.array(z.object({
name: z.string(),
cptCode: z.string().optional(),
description: z.string().optional(),
performedAt: z.date().optional()
})).optional(),
labOrders: z.array(z.object({
testName: z.string(),
loincCode: z.string().optional(),
labName: z.string(),
status: z.enum(['ordered', 'in-progress', 'completed', 'cancelled'])
})).optional(),
imagingOrders: z.array(z.object({
studyName: z.string(),
modality: z.enum(['x-ray', 'ct', 'mri', 'ultrasound', 'mammography', 'other']),
bodyPart: z.string(),
facility: z.string(),
status: z.enum(['ordered', 'in-progress', 'completed', 'cancelled'])
})).optional(),
referrals: z.array(z.object({
specialist: z.string(),
specialty: z.string(),
reason: z.string(),
urgency: z.enum(['routine', 'urgent', 'emergency']),
notes: z.string().optional()
})).optional(),
followUp: z.object({
isRequired: z.boolean(),
recommendedDate: z.date().optional(),
type: AppointmentTypeSchema.optional(),
reason: z.string().optional()
}).optional(),
billing: z.object({
totalAmount: z.number().nonnegative(),
insuranceAmount: z.number().nonnegative(),
patientAmount: z.number().nonnegative(),
status: z.enum(['pending', 'billed', 'paid', 'denied', 'appealed'])
}).optional(),
createdAt: z.date(),
updatedAt: z.date(),
cancelledAt: z.date().optional(),
cancellationReason: z.string().optional()
});
export const AppointmentListResponseSchema = z.object({
appointments: z.array(AppointmentResponseSchema),
pagination: z.object({
page: z.number().positive(),
limit: z.number().positive(),
total: z.number().nonnegative(),
totalPages: z.number().nonnegative()
})
});
export const AppointmentSlotResponseSchema = z.object({
id: z.string().uuid(),
startTime: z.date(),
endTime: z.date(),
isAvailable: z.boolean(),
isBooked: z.boolean(),
isBlocked: z.boolean(),
blockReason: z.string().optional(),
appointmentId: AppointmentIdSchema.optional()
});
export const AppointmentSlotListResponseSchema = z.object({
slots: z.array(AppointmentSlotResponseSchema),
pagination: z.object({
page: z.number().positive(),
limit: z.number().positive(),
total: z.number().nonnegative(),
totalPages: z.number().nonnegative()
})
});
export const AppointmentDetailResponseSchema = z.object({
appointment: AppointmentResponseSchema,
patient: z.object({
id: z.string().uuid(),
firstName: z.string(),
lastName: z.string(),
dateOfBirth: z.date(),
phoneNumber: z.string().optional(),
email: z.string().email().optional()
}),
provider: z.object({
id: z.string().uuid(),
firstName: z.string(),
lastName: z.string(),
specialty: z.string().optional(),
phoneNumber: z.string().optional(),
email: z.string().email().optional()
}),
reminders: z.array(z.object({
type: z.enum(['email', 'sms', 'phone', 'push']),
scheduledAt: z.date(),
sentAt: z.date().optional(),
status: z.enum(['scheduled', 'sent', 'failed', 'cancelled'])
})).optional(),
attachments: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
type: z.enum(['document', 'image', 'video', 'audio']),
url: z.string().url(),
size: z.number().positive(),
uploadedAt: z.date()
})).optional(),
tags: z.array(z.string()).optional()
});
// ============================================================================
// FILTER/SEARCH SCHEMAS
// ============================================================================
export const AppointmentSearchFiltersSchema = z.object({
query: z.string().optional(),
patientId: z.string().uuid().optional(),
providerId: z.string().uuid().optional(),
type: AppointmentTypeSchema.optional(),
status: AppointmentStatusSchema.optional(),
startDate: z.date().optional(),
endDate: z.date().optional(),
hasDiagnosis: z.boolean().optional(),
hasPrescriptions: z.boolean().optional(),
hasProcedures: z.boolean().optional(),
hasLabOrders: z.boolean().optional(),
hasImagingOrders: z.boolean().optional(),
hasReferrals: z.boolean().optional(),
hasFollowUp: z.boolean().optional(),
isCancelled: z.boolean().optional(),
tags: z.array(z.string()).optional(),
createdAfter: z.date().optional(),
createdBefore: z.date().optional()
});
export const AppointmentSlotSearchFiltersSchema = z.object({
providerId: z.string().uuid(),
startDate: z.date(),
endDate: z.date(),
type: AppointmentTypeSchema.optional(),
duration: z.number().positive().optional(), // in minutes
isAvailable: z.boolean().optional()
});
export const AppointmentSortOptionsSchema = z.enum([
'startTime',
'endTime',
'createdAt',
'updatedAt',
'status',
'type',
'patientName',
'providerName'
]);
// ============================================================================
// VALIDATION UTILITIES
// ============================================================================
export const AppointmentValidationUtils = {
isValidTimeSlot: (startTime, endTime) => {
return startTime < endTime;
},
calculateDuration: (startTime, endTime) => {
const diffMs = endTime.getTime() - startTime.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
return {
minutes: diffMinutes % 60,
hours: diffHours % 24,
days: diffDays
};
},
isBusinessHours: (date) => {
const hour = date.getHours();
const day = date.getDay();
// Monday to Friday, 8 AM to 6 PM
return day >= 1 && day <= 5 && hour >= 8 && hour < 18;
},
isAvailableTimeSlot: (startTime, endTime, existingAppointments) => {
for (const appointment of existingAppointments) {
if ((startTime >= appointment.startTime && startTime < appointment.endTime) ||
(endTime > appointment.startTime && endTime <= appointment.endTime) ||
(startTime <= appointment.startTime && endTime >= appointment.endTime)) {
return false;
}
}
return true;
},
validateAppointmentData: (data) => {
const errors = [];
if (data.startTime && data.endTime && data.startTime >= data.endTime) {
errors.push('Start time must be before end time');
}
if (data.startTime && data.startTime < new Date()) {
errors.push('Start time cannot be in the past');
}
if (data.recurringPattern) {
if (data.recurringPattern.frequency === 'weekly' && (!data.recurringPattern.daysOfWeek || data.recurringPattern.daysOfWeek.length === 0)) {
errors.push('Weekly recurring appointments must specify days of week');
}
if (data.recurringPattern.frequency === 'monthly' && !data.recurringPattern.dayOfMonth) {
errors.push('Monthly recurring appointments must specify day of month');
}
if (data.recurringPattern.frequency === 'yearly' && !data.recurringPattern.monthOfYear) {
errors.push('Yearly recurring appointments must specify month of year');
}
}
if (data.reminders) {
data.reminders.forEach((reminder, index) => {
if (reminder.minutesBefore <= 0) {
errors.push(`Reminder ${index + 1}: minutes before must be positive`);
}
});
}
return {
isValid: errors.length === 0,
errors
};
}
};
// ============================================================================
// SCHEMA EXPORTS
// ============================================================================
export const AppointmentSchemas = {
// Base schemas
AppointmentId: AppointmentIdSchema,
TimeSlot: TimeSlotSchema,
Duration: DurationSchema,
// Enum schemas
AppointmentStatus: AppointmentStatusSchema,
AppointmentType: AppointmentTypeSchema,
// Value object schemas
AppointmentIdValueObject: AppointmentIdValueObjectSchema,
TimeSlotValueObject: TimeSlotValueObjectSchema,
DurationValueObject: DurationValueObjectSchema,
// Entity schemas
Appointment: AppointmentSchema,
AppointmentSlot: AppointmentSlotSchema,
// Request schemas
CreateAppointmentRequest: CreateAppointmentRequestSchema,
UpdateAppointmentRequest: UpdateAppointmentRequestSchema,
CancelAppointmentRequest: CancelAppointmentRequestSchema,
RescheduleAppointmentRequest: RescheduleAppointmentRequestSchema,
AddDiagnosisRequest: AddDiagnosisRequestSchema,
AddPrescriptionRequest: AddPrescriptionRequestSchema,
AddProcedureRequest: AddProcedureRequestSchema,
AddLabOrderRequest: AddLabOrderRequestSchema,
AddImagingOrderRequest: AddImagingOrderRequestSchema,
AddReferralRequest: AddReferralRequestSchema,
ScheduleFollowUpRequest: ScheduleFollowUpRequestSchema,
// Response schemas
AppointmentResponse: AppointmentResponseSchema,
AppointmentListResponse: AppointmentListResponseSchema,
AppointmentSlotResponse: AppointmentSlotResponseSchema,
AppointmentSlotListResponse: AppointmentSlotListResponseSchema,
AppointmentDetailResponse: AppointmentDetailResponseSchema,
// Filter schemas
AppointmentSearchFilters: AppointmentSearchFiltersSchema,
AppointmentSlotSearchFilters: AppointmentSlotSearchFiltersSchema,
AppointmentSortOptions: AppointmentSortOptionsSchema,
// Utilities
ValidationUtils: AppointmentValidationUtils
};
//# sourceMappingURL=appointment-schemas.js.map