- Lessons module: lesson types, instructors, schedule slots, enrollments, sessions (list + week grid view), lesson plans, grading scales, templates - Rate cycles: replace monthly_rate with billing_interval + billing_unit on enrollments; add weekly/monthly/quarterly rate presets to lesson types and schedule slots with auto-fill on enrollment form - Member detail page: tabbed layout for details, identity documents, enrollments - Sessions week view: custom 7-column grid replacing react-big-calendar - Music store seed: instructors, lesson types, slots, enrollments, sessions, grading scale, lesson plan template - Scrollbar styling: themed to match sidebar/app palette - deploy/: EC2 setup and redeploy scripts, nginx config, systemd service - Help: add Lessons category (overview, types, instructors, slots, enrollments, sessions, plans/grading); collapsible sidebar with independent scroll; remove POS/accounting references from docs
271 lines
9.4 KiB
TypeScript
271 lines
9.4 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
/** Coerce empty strings to undefined — solves HTML form inputs sending '' for blank optional fields */
|
|
function opt<T extends z.ZodTypeAny>(schema: T) {
|
|
return z.preprocess((v) => (v === '' ? undefined : v), schema.optional())
|
|
}
|
|
|
|
// --- Enums ---
|
|
|
|
export const LessonFormat = z.enum(['private', 'group'])
|
|
export type LessonFormat = z.infer<typeof LessonFormat>
|
|
|
|
// --- Instructor schemas ---
|
|
|
|
export const InstructorCreateSchema = z.object({
|
|
userId: opt(z.string().uuid()),
|
|
displayName: z.string().min(1).max(255),
|
|
bio: opt(z.string()),
|
|
instruments: z.array(z.string()).optional(),
|
|
})
|
|
export type InstructorCreateInput = z.infer<typeof InstructorCreateSchema>
|
|
|
|
export const InstructorUpdateSchema = InstructorCreateSchema.partial()
|
|
export type InstructorUpdateInput = z.infer<typeof InstructorUpdateSchema>
|
|
|
|
// --- Lesson Type schemas ---
|
|
|
|
export const LessonTypeCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
instrument: opt(z.string().max(100)),
|
|
durationMinutes: z.coerce.number().int().min(5).max(480),
|
|
lessonFormat: LessonFormat.default('private'),
|
|
rateWeekly: opt(z.coerce.number().min(0)),
|
|
rateMonthly: opt(z.coerce.number().min(0)),
|
|
rateQuarterly: opt(z.coerce.number().min(0)),
|
|
})
|
|
export type LessonTypeCreateInput = z.infer<typeof LessonTypeCreateSchema>
|
|
|
|
export const LessonTypeUpdateSchema = LessonTypeCreateSchema.partial()
|
|
export type LessonTypeUpdateInput = z.infer<typeof LessonTypeUpdateSchema>
|
|
|
|
// --- Schedule Slot schemas ---
|
|
|
|
export const ScheduleSlotCreateSchema = z.object({
|
|
instructorId: z.string().uuid(),
|
|
lessonTypeId: z.string().uuid(),
|
|
dayOfWeek: z.coerce.number().int().min(0).max(6),
|
|
startTime: z.string().regex(/^\d{2}:\d{2}$/, 'Must be HH:MM format'),
|
|
room: opt(z.string().max(100)),
|
|
maxStudents: z.coerce.number().int().min(1).default(1),
|
|
rateWeekly: opt(z.coerce.number().min(0)),
|
|
rateMonthly: opt(z.coerce.number().min(0)),
|
|
rateQuarterly: opt(z.coerce.number().min(0)),
|
|
})
|
|
export type ScheduleSlotCreateInput = z.infer<typeof ScheduleSlotCreateSchema>
|
|
|
|
export const ScheduleSlotUpdateSchema = ScheduleSlotCreateSchema.partial()
|
|
export type ScheduleSlotUpdateInput = z.infer<typeof ScheduleSlotUpdateSchema>
|
|
|
|
// --- Enrollment schemas ---
|
|
|
|
export const EnrollmentStatus = z.enum(['active', 'paused', 'cancelled', 'completed'])
|
|
export type EnrollmentStatus = z.infer<typeof EnrollmentStatus>
|
|
|
|
export const EnrollmentCreateSchema = z.object({
|
|
memberId: z.string().uuid(),
|
|
accountId: z.string().uuid(),
|
|
scheduleSlotId: z.string().uuid(),
|
|
instructorId: z.string().uuid(),
|
|
startDate: z.string().min(1),
|
|
endDate: opt(z.string()),
|
|
rate: opt(z.coerce.number().min(0)),
|
|
billingInterval: opt(z.coerce.number().int().min(1)),
|
|
billingUnit: opt(z.enum(['day', 'week', 'month', 'quarter', 'year'])),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type EnrollmentCreateInput = z.infer<typeof EnrollmentCreateSchema>
|
|
|
|
export const EnrollmentUpdateSchema = EnrollmentCreateSchema.omit({
|
|
memberId: true,
|
|
accountId: true,
|
|
scheduleSlotId: true,
|
|
instructorId: true,
|
|
}).partial()
|
|
export type EnrollmentUpdateInput = z.infer<typeof EnrollmentUpdateSchema>
|
|
|
|
export const EnrollmentStatusUpdateSchema = z.object({
|
|
status: EnrollmentStatus,
|
|
})
|
|
export type EnrollmentStatusUpdateInput = z.infer<typeof EnrollmentStatusUpdateSchema>
|
|
|
|
// --- Lesson Session schemas ---
|
|
|
|
export const LessonSessionStatus = z.enum(['scheduled', 'attended', 'missed', 'makeup', 'cancelled'])
|
|
export type LessonSessionStatus = z.infer<typeof LessonSessionStatus>
|
|
|
|
export const LessonSessionStatusUpdateSchema = z.object({
|
|
status: LessonSessionStatus,
|
|
})
|
|
export type LessonSessionStatusUpdateInput = z.infer<typeof LessonSessionStatusUpdateSchema>
|
|
|
|
export const LessonSessionNotesSchema = z.object({
|
|
instructorNotes: opt(z.string()),
|
|
memberNotes: opt(z.string()),
|
|
homeworkAssigned: opt(z.string()),
|
|
nextLessonGoals: opt(z.string()),
|
|
topicsCovered: z.array(z.string()).optional(),
|
|
})
|
|
export type LessonSessionNotesInput = z.infer<typeof LessonSessionNotesSchema>
|
|
|
|
export const LessonSessionUpdateSchema = z.object({
|
|
actualStartTime: opt(z.string().regex(/^\d{2}:\d{2}$/, 'Must be HH:MM format')),
|
|
actualEndTime: opt(z.string().regex(/^\d{2}:\d{2}$/, 'Must be HH:MM format')),
|
|
substituteInstructorId: opt(z.string().uuid()),
|
|
})
|
|
export type LessonSessionUpdateInput = z.infer<typeof LessonSessionUpdateSchema>
|
|
|
|
// --- Instructor Blocked Date schemas ---
|
|
|
|
export const InstructorBlockedDateCreateSchema = z.object({
|
|
startDate: z.string().min(1),
|
|
endDate: z.string().min(1),
|
|
reason: opt(z.string().max(255)),
|
|
})
|
|
export type InstructorBlockedDateCreateInput = z.infer<typeof InstructorBlockedDateCreateSchema>
|
|
|
|
// --- Store Closure schemas ---
|
|
|
|
export const StoreClosureCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
startDate: z.string().min(1),
|
|
endDate: z.string().min(1),
|
|
})
|
|
export type StoreClosureCreateInput = z.infer<typeof StoreClosureCreateSchema>
|
|
|
|
// --- Grading Scale schemas ---
|
|
|
|
export const GradingScaleLevelInput = z.object({
|
|
value: z.string().min(1).max(50),
|
|
label: z.string().min(1).max(255),
|
|
numericValue: z.coerce.number().int().min(0).max(100),
|
|
colorHex: opt(z.string().regex(/^#[0-9A-Fa-f]{6}$/, 'Must be #RRGGBB format')),
|
|
sortOrder: z.coerce.number().int(),
|
|
})
|
|
|
|
export const GradingScaleCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
isDefault: z.boolean().default(false),
|
|
levels: z.array(GradingScaleLevelInput).min(1),
|
|
})
|
|
export type GradingScaleCreateInput = z.infer<typeof GradingScaleCreateSchema>
|
|
|
|
export const GradingScaleUpdateSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
description: opt(z.string()),
|
|
isDefault: z.boolean().optional(),
|
|
})
|
|
export type GradingScaleUpdateInput = z.infer<typeof GradingScaleUpdateSchema>
|
|
|
|
// --- Lesson Plan schemas ---
|
|
|
|
export const LessonPlanItemStatus = z.enum(['not_started', 'in_progress', 'mastered', 'skipped'])
|
|
export type LessonPlanItemStatus = z.infer<typeof LessonPlanItemStatus>
|
|
|
|
const LessonPlanItemInput = z.object({
|
|
title: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
gradingScaleId: opt(z.string().uuid()),
|
|
targetGradeValue: opt(z.string().max(50)),
|
|
sortOrder: z.coerce.number().int(),
|
|
})
|
|
|
|
const LessonPlanSectionInput = z.object({
|
|
title: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
sortOrder: z.coerce.number().int(),
|
|
items: z.array(LessonPlanItemInput).default([]),
|
|
})
|
|
|
|
export const LessonPlanCreateSchema = z.object({
|
|
memberId: z.string().uuid(),
|
|
enrollmentId: z.string().uuid(),
|
|
title: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
startedDate: opt(z.string()),
|
|
sections: z.array(LessonPlanSectionInput).default([]),
|
|
})
|
|
export type LessonPlanCreateInput = z.infer<typeof LessonPlanCreateSchema>
|
|
|
|
export const LessonPlanUpdateSchema = z.object({
|
|
title: z.string().min(1).max(255).optional(),
|
|
description: opt(z.string()),
|
|
isActive: z.boolean().optional(),
|
|
})
|
|
export type LessonPlanUpdateInput = z.infer<typeof LessonPlanUpdateSchema>
|
|
|
|
export const LessonPlanItemUpdateSchema = z.object({
|
|
title: z.string().min(1).max(255).optional(),
|
|
description: opt(z.string()),
|
|
status: LessonPlanItemStatus.optional(),
|
|
gradingScaleId: opt(z.string().uuid()),
|
|
currentGradeValue: opt(z.string().max(50)),
|
|
targetGradeValue: opt(z.string().max(50)),
|
|
notes: opt(z.string()),
|
|
sortOrder: z.coerce.number().int().optional(),
|
|
})
|
|
export type LessonPlanItemUpdateInput = z.infer<typeof LessonPlanItemUpdateSchema>
|
|
|
|
// --- Lesson Plan Template schemas ---
|
|
|
|
export const SkillLevel = z.enum(['beginner', 'intermediate', 'advanced', 'all_levels'])
|
|
export type SkillLevel = z.infer<typeof SkillLevel>
|
|
|
|
const TemplateItemInput = z.object({
|
|
title: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
gradingScaleId: opt(z.string().uuid()),
|
|
targetGradeValue: opt(z.string().max(50)),
|
|
sortOrder: z.coerce.number().int(),
|
|
})
|
|
|
|
const TemplateSectionInput = z.object({
|
|
title: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
sortOrder: z.coerce.number().int(),
|
|
items: z.array(TemplateItemInput).default([]),
|
|
})
|
|
|
|
export const LessonPlanTemplateCreateSchema = z.object({
|
|
name: z.string().min(1).max(255),
|
|
description: opt(z.string()),
|
|
instrument: opt(z.string().max(100)),
|
|
skillLevel: SkillLevel.default('all_levels'),
|
|
sections: z.array(TemplateSectionInput).default([]),
|
|
})
|
|
export type LessonPlanTemplateCreateInput = z.infer<typeof LessonPlanTemplateCreateSchema>
|
|
|
|
export const LessonPlanTemplateUpdateSchema = z.object({
|
|
name: z.string().min(1).max(255).optional(),
|
|
description: opt(z.string()),
|
|
instrument: opt(z.string().max(100)),
|
|
skillLevel: SkillLevel.optional(),
|
|
isActive: z.boolean().optional(),
|
|
})
|
|
export type LessonPlanTemplateUpdateInput = z.infer<typeof LessonPlanTemplateUpdateSchema>
|
|
|
|
export const TemplateInstantiateSchema = z.object({
|
|
memberId: z.string().uuid(),
|
|
enrollmentId: z.string().uuid(),
|
|
title: opt(z.string().min(1).max(255)),
|
|
})
|
|
export type TemplateInstantiateInput = z.infer<typeof TemplateInstantiateSchema>
|
|
|
|
// --- Grade History schemas ---
|
|
|
|
export const GradeCreateSchema = z.object({
|
|
gradingScaleId: opt(z.string().uuid()),
|
|
gradeValue: z.string().min(1).max(50),
|
|
sessionId: opt(z.string().uuid()),
|
|
notes: opt(z.string()),
|
|
})
|
|
export type GradeCreateInput = z.infer<typeof GradeCreateSchema>
|
|
|
|
// --- Session Plan Items schemas ---
|
|
|
|
export const SessionPlanItemsSchema = z.object({
|
|
lessonPlanItemIds: z.array(z.string().uuid()).min(1),
|
|
})
|
|
export type SessionPlanItemsInput = z.infer<typeof SessionPlanItemsSchema>
|