feat: lessons station + technician workbench + polish
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user