Files
lunarfront-app/packages/admin/src/components/station-lessons/lessons-week-view.tsx
ryan 3b0daeae0c
All checks were successful
CI / ci (pull_request) Successful in 26s
CI / e2e (pull_request) Successful in 1m8s
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>
2026-04-06 01:46:51 +00:00

141 lines
5.6 KiB
TypeScript

import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { sessionListOptions } from '@/api/lessons'
import type { LessonSession } from '@/types/lesson'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { SessionDetailPanel } from './session-detail-panel'
import { ChevronLeft, ChevronRight } from 'lucide-react'
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const STATUS_COLORS: Record<string, string> = {
scheduled: 'bg-blue-500/15 border-blue-500/30 text-blue-700 dark:text-blue-300',
attended: 'bg-green-500/15 border-green-500/30 text-green-700 dark:text-green-300',
missed: 'bg-red-500/15 border-red-500/30 text-red-700 dark:text-red-300',
makeup: 'bg-purple-500/15 border-purple-500/30 text-purple-700 dark:text-purple-300',
cancelled: 'bg-muted border-border text-muted-foreground',
}
interface LessonsWeekViewProps {
canEdit: boolean
instructorId?: string
}
export function LessonsWeekView({ canEdit, instructorId }: LessonsWeekViewProps) {
const [weekOffset, setWeekOffset] = useState(0)
const [selectedSessionId, setSelectedSessionId] = useState<string | null>(null)
// Get week start (Monday) and end (Sunday)
const now = new Date()
const monday = new Date(now)
monday.setDate(now.getDate() - ((now.getDay() + 6) % 7) + weekOffset * 7)
monday.setHours(0, 0, 0, 0)
const sunday = new Date(monday)
sunday.setDate(monday.getDate() + 6)
const weekStart = monday.toISOString().slice(0, 10)
const weekEnd = sunday.toISOString().slice(0, 10)
const { data } = useQuery({
...sessionListOptions({
page: 1,
limit: 200,
sort: 'scheduled_time',
order: 'asc',
scheduledDateFrom: weekStart,
scheduledDateTo: weekEnd,
...(instructorId ? { instructorId } : {}),
}),
staleTime: 30_000,
})
const sessions = data?.data ?? []
// Group sessions by date
const byDate = new Map<string, LessonSession[]>()
for (const s of sessions) {
const existing = byDate.get(s.scheduledDate) ?? []
existing.push(s)
byDate.set(s.scheduledDate, existing)
}
// Generate week dates
const weekDates = Array.from({ length: 7 }, (_, i) => {
const d = new Date(monday)
d.setDate(monday.getDate() + i)
return d.toISOString().slice(0, 10)
})
const todayStr = new Date().toISOString().slice(0, 10)
return (
<div className="flex flex-col h-full">
{/* Week navigation */}
<div className="flex items-center justify-between px-3 py-2 border-b border-border shrink-0 bg-card/50">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setWeekOffset(weekOffset - 1)}>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="text-sm font-medium">
{monday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} {sunday.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</div>
<div className="flex gap-1">
{weekOffset !== 0 && (
<Button variant="ghost" size="sm" className="h-8 text-xs" onClick={() => setWeekOffset(0)}>Today</Button>
)}
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setWeekOffset(weekOffset + 1)}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
{/* Calendar grid */}
<div className="flex-1 overflow-auto">
<div className="grid grid-cols-7 min-h-full">
{weekDates.map((dateStr, i) => {
const daySessions = byDate.get(dateStr) ?? []
const isToday = dateStr === todayStr
const dayDate = new Date(dateStr + 'T00:00:00')
return (
<div key={dateStr} className={`border-r border-border ${i === 6 ? 'border-r-0' : ''}`}>
{/* Day header */}
<div className={`sticky top-0 z-10 p-2 border-b border-border text-center ${isToday ? 'bg-primary/5' : 'bg-card'}`}>
<div className="text-xs text-muted-foreground">{DAYS[dayDate.getDay()]}</div>
<div className={`text-sm font-medium ${isToday ? 'text-primary' : ''}`}>
{dayDate.getDate()}
</div>
</div>
{/* Sessions */}
<div className="p-1 space-y-1">
{daySessions.map((session) => (
<button
key={session.id}
className={`w-full text-left p-2 rounded border text-xs transition-colors hover:opacity-80 ${STATUS_COLORS[session.status] ?? ''}`}
onClick={() => setSelectedSessionId(session.id)}
>
<div className="font-medium">{session.scheduledTime}</div>
<div className="truncate">{session.memberName ?? 'Unknown'}</div>
<div className="truncate text-muted-foreground">{session.lessonTypeName ?? ''}</div>
</button>
))}
{daySessions.length === 0 && (
<div className="text-xs text-muted-foreground text-center py-4 opacity-30"></div>
)}
</div>
</div>
)
})}
</div>
</div>
{/* Session detail dialog */}
<Dialog open={!!selectedSessionId} onOpenChange={(open) => { if (!open) setSelectedSessionId(null) }}>
<DialogContent className="max-w-lg max-h-[80vh] overflow-hidden p-0">
<SessionDetailPanel sessionId={selectedSessionId} canEdit={canEdit} />
</DialogContent>
</Dialog>
</div>
)
}