Files
CICD/src/modules/classes/components/schedule-view.tsx
SpecialX 57807def37
Some checks failed
CI / build-and-test (push) Failing after 3m50s
CI / deploy (push) Has been skipped
完整性更新
现在已经实现了大部分基础功能
2026-01-08 11:14:03 +08:00

466 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useMemo, useState } from "react"
import { useRouter } from "next/navigation"
import { Clock, MapPin, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
import { toast } from "sonner"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { cn } from "@/shared/lib/utils"
import { Button } from "@/shared/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/shared/components/ui/alert-dialog"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select"
import type { ClassScheduleItem, TeacherClass } from "../types"
import {
createClassScheduleItemAction,
deleteClassScheduleItemAction,
updateClassScheduleItemAction,
} from "../actions"
const WEEKDAYS: Array<{ key: ClassScheduleItem["weekday"]; label: string }> = [
{ key: 1, label: "Mon" },
{ key: 2, label: "Tue" },
{ key: 3, label: "Wed" },
{ key: 4, label: "Thu" },
{ key: 5, label: "Fri" },
{ key: 6, label: "Sat" },
{ key: 7, label: "Sun" },
]
export function ScheduleView({
schedule,
classes,
}: {
schedule: ClassScheduleItem[]
classes: TeacherClass[]
}) {
const router = useRouter()
const [isWorking, setIsWorking] = useState(false)
const [editItem, setEditItem] = useState<ClassScheduleItem | null>(null)
const [deleteItem, setDeleteItem] = useState<ClassScheduleItem | null>(null)
const [createWeekday, setCreateWeekday] = useState<ClassScheduleItem["weekday"]>(1)
const [createOpen, setCreateOpen] = useState(false)
const [createClassId, setCreateClassId] = useState<string>("")
const [editClassId, setEditClassId] = useState<string>("")
const [editWeekday, setEditWeekday] = useState<string>("1")
const classNameById = useMemo(() => new Map(classes.map((c) => [c.id, c.name] as const)), [classes])
const defaultClassId = useMemo(() => classes[0]?.id ?? "", [classes])
useEffect(() => {
if (!editItem) return
setEditClassId(editItem.classId)
setEditWeekday(String(editItem.weekday))
}, [editItem])
useEffect(() => {
if (!createOpen) return
setCreateClassId(defaultClassId)
}, [createOpen, defaultClassId])
const byDay = new Map<ClassScheduleItem["weekday"], ClassScheduleItem[]>()
for (const d of WEEKDAYS) byDay.set(d.key, [])
for (const item of schedule) byDay.get(item.weekday)?.push(item)
const handleCreate = async (formData: FormData) => {
setIsWorking(true)
try {
formData.set("classId", createClassId || defaultClassId)
formData.set("weekday", String(createWeekday))
const res = await createClassScheduleItemAction(null, formData)
if (res.success) {
toast.success(res.message)
setCreateOpen(false)
router.refresh()
} else {
toast.error(res.message || "Failed to create schedule item")
}
} catch {
toast.error("Failed to create schedule item")
} finally {
setIsWorking(false)
}
}
const handleUpdate = async (formData: FormData) => {
if (!editItem) return
setIsWorking(true)
try {
formData.set("classId", editClassId)
formData.set("weekday", editWeekday)
const res = await updateClassScheduleItemAction(editItem.id, null, formData)
if (res.success) {
toast.success(res.message)
setEditItem(null)
router.refresh()
} else {
toast.error(res.message || "Failed to update schedule item")
}
} catch {
toast.error("Failed to update schedule item")
} finally {
setIsWorking(false)
}
}
const handleDelete = async () => {
if (!deleteItem) return
setIsWorking(true)
try {
const res = await deleteClassScheduleItemAction(deleteItem.id)
if (res.success) {
toast.success(res.message)
setDeleteItem(null)
router.refresh()
} else {
toast.error(res.message || "Failed to delete schedule item")
}
} catch {
toast.error("Failed to delete schedule item")
} finally {
setIsWorking(false)
}
}
return (
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
{WEEKDAYS.map((d) => {
const items = byDay.get(d.key) ?? []
return (
<Card key={d.key} className="shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div className="flex items-center gap-2">
<CardTitle className="text-base">{d.label}</CardTitle>
<Badge variant="secondary" className={cn(items.length === 0 && "opacity-60")}>
{items.length} items
</Badge>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
disabled={classes.length === 0}
onClick={() => {
setCreateWeekday(d.key)
setCreateOpen(true)
}}
>
<Plus className="size-4" />
</Button>
</CardHeader>
<CardContent>
{items.length === 0 ? (
<div className="text-muted-foreground text-sm">No classes scheduled.</div>
) : (
<div className="space-y-4">
{items.map((item) => (
<div key={item.id} className="space-y-1 border-b pb-4 last:border-0 last:pb-0">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="font-medium leading-none">{item.course}</div>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline">{classNameById.get(item.classId) ?? "Class"}</Badge>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={isWorking}>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setEditItem(item)}>
<Pencil className="mr-2 size-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setDeleteItem(item)}
>
<Trash2 className="mr-2 size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="text-muted-foreground flex items-center gap-3 text-sm">
<span className="inline-flex items-center gap-1 tabular-nums">
<Clock className="h-3.5 w-3.5" />
{item.startTime}{item.endTime}
</span>
{item.location ? (
<span className="inline-flex items-center gap-1">
<MapPin className="h-3.5 w-3.5" />
{item.location}
</span>
) : null}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
)
})}
<Dialog
open={createOpen}
onOpenChange={(v) => {
if (isWorking) return
setCreateOpen(v)
}}
>
<DialogContent className="sm:max-w-[560px]">
<DialogHeader>
<DialogTitle>Add schedule item</DialogTitle>
<DialogDescription>Create a class schedule entry.</DialogDescription>
</DialogHeader>
<form action={handleCreate}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Class</Label>
<div className="col-span-3">
<Select value={createClassId} onValueChange={setCreateClassId}>
<SelectTrigger>
<SelectValue placeholder="Select a class" />
</SelectTrigger>
<SelectContent>
{classes.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input type="hidden" name="classId" value={createClassId} />
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Weekday</Label>
<Input value={WEEKDAYS.find((w) => w.key === createWeekday)?.label ?? ""} readOnly className="col-span-3" />
<input type="hidden" name="weekday" value={String(createWeekday)} />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="create-startTime" className="text-right">
Start
</Label>
<Input id="create-startTime" name="startTime" type="time" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="create-endTime" className="text-right">
End
</Label>
<Input id="create-endTime" name="endTime" type="time" className="col-span-3" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="create-course" className="text-right">
Course
</Label>
<Input id="create-course" name="course" className="col-span-3" placeholder="e.g. Math" required />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="create-location" className="text-right">
Location
</Label>
<Input id="create-location" name="location" className="col-span-3" placeholder="Optional" />
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={isWorking || !createClassId}>
{isWorking ? "Creating..." : "Create"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog
open={Boolean(editItem)}
onOpenChange={(v) => {
if (isWorking) return
if (!v) setEditItem(null)
}}
>
<DialogContent className="sm:max-w-[560px]">
<DialogHeader>
<DialogTitle>Edit schedule item</DialogTitle>
<DialogDescription>Update this schedule entry.</DialogDescription>
</DialogHeader>
{editItem ? (
<form action={handleUpdate}>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Class</Label>
<div className="col-span-3">
<Select value={editClassId} onValueChange={setEditClassId}>
<SelectTrigger>
<SelectValue placeholder="Select a class" />
</SelectTrigger>
<SelectContent>
{classes.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
<input type="hidden" name="classId" value={editClassId} />
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Weekday</Label>
<div className="col-span-3">
<Select value={editWeekday} onValueChange={setEditWeekday}>
<SelectTrigger>
<SelectValue placeholder="Select weekday" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Mon</SelectItem>
<SelectItem value="2">Tue</SelectItem>
<SelectItem value="3">Wed</SelectItem>
<SelectItem value="4">Thu</SelectItem>
<SelectItem value="5">Fri</SelectItem>
<SelectItem value="6">Sat</SelectItem>
<SelectItem value="7">Sun</SelectItem>
</SelectContent>
</Select>
<input type="hidden" name="weekday" value={editWeekday} />
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="edit-startTime" className="text-right">
Start
</Label>
<Input
id="edit-startTime"
name="startTime"
type="time"
className="col-span-3"
defaultValue={editItem.startTime}
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="edit-endTime" className="text-right">
End
</Label>
<Input
id="edit-endTime"
name="endTime"
type="time"
className="col-span-3"
defaultValue={editItem.endTime}
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="edit-course" className="text-right">
Course
</Label>
<Input
id="edit-course"
name="course"
className="col-span-3"
defaultValue={editItem.course}
required
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="edit-location" className="text-right">
Location
</Label>
<Input
id="edit-location"
name="location"
className="col-span-3"
defaultValue={editItem.location ?? ""}
/>
</div>
</div>
<DialogFooter>
<Button type="submit" disabled={isWorking}>
{isWorking ? "Saving..." : "Save"}
</Button>
</DialogFooter>
</form>
) : null}
</DialogContent>
</Dialog>
<AlertDialog
open={Boolean(deleteItem)}
onOpenChange={(v) => {
if (isWorking) return
if (!v) setDeleteItem(null)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete schedule item?</AlertDialogTitle>
<AlertDialogDescription>
{deleteItem ? (
<>
This will permanently delete <span className="font-medium text-foreground">{deleteItem.course}</span>{" "}
({deleteItem.startTime}{deleteItem.endTime}).
</>
) : null}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isWorking}>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={handleDelete}
disabled={isWorking}
>
{isWorking ? "Deleting..." : "Delete"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}