feat: lessons station + technician workbench + polish
All checks were successful
CI / ci (pull_request) Successful in 26s
CI / e2e (pull_request) Successful in 1m8s

Phase 3: Technician workbench
- Focused single-ticket view with Work, Parts, Photos, Notes sections
- Template quick-add for line items, inline add/delete
- Ticket selector for multiple assigned tickets

Phase 4: Lessons desk view
- Today overview: all instructors' sessions, group by time/instructor
- Quick check-in (attended/missed/cancelled) buttons
- Highlights upcoming and overdue sessions
- Schedule view: weekly grid with instructor filter, open slots

Phase 5: Lessons instructor view
- My Sessions (today) + Week calendar sub-views
- Session detail with attendance, notes, plan items
- Week calendar with session blocks, click to open detail dialog

Phase 6: Polish
- Permission-based routing: desk vs tech/instructor views
- Build and lint clean

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-06 01:46:51 +00:00
parent 0411df57eb
commit 3b0daeae0c
7 changed files with 747 additions and 11 deletions

View File

@@ -0,0 +1,104 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { instructorListOptions, scheduleSlotListOptions } from '@/api/lessons'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Badge } from '@/components/ui/badge'
import type { ScheduleSlot, Instructor } from '@/types/lesson'
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
const HOURS = Array.from({ length: 14 }, (_, i) => i + 7) // 7 AM to 8 PM
export function LessonsScheduleView() {
const [selectedInstructorId, setSelectedInstructorId] = useState<string>('all')
const { data: instructorsData } = useQuery({
...instructorListOptions({ page: 1, limit: 100, q: undefined, sort: 'name', order: 'asc' }),
})
const instructors = (instructorsData?.data ?? []).filter((i: Instructor) => i.isActive !== false)
const { data: slotsData } = useQuery({
...scheduleSlotListOptions(
{ page: 1, limit: 500, q: undefined, sort: undefined, order: 'asc' },
selectedInstructorId !== 'all' ? { instructorId: selectedInstructorId } : undefined,
),
})
const slots = slotsData?.data ?? []
// Group slots by day
function getSlotsForDay(dayOfWeek: number) {
return slots.filter((s: ScheduleSlot) => s.dayOfWeek === dayOfWeek)
}
function formatTime(time: string) {
const [h, m] = time.split(':').map(Number)
const ampm = h >= 12 ? 'PM' : 'AM'
const hour = h > 12 ? h - 12 : h === 0 ? 12 : h
return `${hour}:${m.toString().padStart(2, '0')} ${ampm}`
}
return (
<div className="flex flex-col h-full">
{/* Controls */}
<div className="flex items-center gap-3 px-3 py-2 border-b border-border shrink-0 bg-card/50">
<span className="text-sm font-medium">Weekly Schedule</span>
<Select value={selectedInstructorId} onValueChange={setSelectedInstructorId}>
<SelectTrigger className="h-8 w-48">
<SelectValue placeholder="All Instructors" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Instructors</SelectItem>
{instructors.map((inst: Instructor) => (
<SelectItem key={inst.id} value={inst.id}>{inst.displayName}</SelectItem>
))}
</SelectContent>
</Select>
<Badge variant="outline" className="text-xs">{slots.length} slots</Badge>
</div>
{/* Weekly grid */}
<div className="flex-1 overflow-auto">
<div className="grid grid-cols-8 min-w-[800px]">
{/* Header row */}
<div className="sticky top-0 bg-card border-b border-r border-border p-2 text-xs font-medium text-muted-foreground z-10" />
{DAYS.map((day) => (
<div key={day} className="sticky top-0 bg-card border-b border-border p-2 text-xs font-medium text-center z-10">
{day.slice(0, 3)}
</div>
))}
{/* Time rows */}
{HOURS.map((hour) => (
<>
<div key={`h-${hour}`} className="border-r border-b border-border p-1 text-xs text-muted-foreground text-right pr-2">
{hour > 12 ? hour - 12 : hour}{hour >= 12 ? 'p' : 'a'}
</div>
{DAYS.map((_, dayIdx) => {
const daySlots = getSlotsForDay(dayIdx).filter((s: ScheduleSlot) => {
const slotHour = parseInt(s.startTime.split(':')[0])
return slotHour === hour
})
return (
<div key={`${hour}-${dayIdx}`} className="border-b border-border min-h-[48px] p-0.5">
{daySlots.map((slot: ScheduleSlot) => {
const instructor = instructors.find((i: Instructor) => i.id === slot.instructorId)
return (
<div
key={slot.id}
className="bg-primary/10 border border-primary/20 rounded px-1.5 py-0.5 text-xs mb-0.5"
>
<div className="font-medium truncate">{formatTime(slot.startTime)}</div>
{instructor && <div className="text-muted-foreground truncate">{instructor.displayName}</div>}
<Badge variant="outline" className="text-[9px] h-4">Open</Badge>
</div>
)
})}
</div>
)
})}
</>
))}
</div>
</div>
</div>
)
}