Files
CICD/src/modules/school/components/schools-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

254 lines
8.8 KiB
TypeScript

"use client"
import { useState } from "react"
import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
import type { SchoolListItem } from "../types"
import { createSchoolAction, deleteSchoolAction, updateSchoolAction } from "../actions"
import { Button } from "@/shared/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog"
import { Input } from "@/shared/components/ui/input"
import { Label } from "@/shared/components/ui/label"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/shared/components/ui/table"
import { EmptyState } from "@/shared/components/ui/empty-state"
import { Badge } from "@/shared/components/ui/badge"
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 { formatDate } from "@/shared/lib/utils"
export function SchoolsClient({ schools }: { schools: SchoolListItem[] }) {
const router = useRouter()
const [isWorking, setIsWorking] = useState(false)
const [createOpen, setCreateOpen] = useState(false)
const [editItem, setEditItem] = useState<SchoolListItem | null>(null)
const [deleteItem, setDeleteItem] = useState<SchoolListItem | null>(null)
const handleCreate = async (formData: FormData) => {
setIsWorking(true)
try {
const res = await createSchoolAction(undefined, formData)
if (res.success) {
toast.success(res.message)
setCreateOpen(false)
router.refresh()
} else {
toast.error(res.message || "Failed to create school")
}
} catch {
toast.error("Failed to create school")
} finally {
setIsWorking(false)
}
}
const handleUpdate = async (formData: FormData) => {
if (!editItem) return
setIsWorking(true)
try {
const res = await updateSchoolAction(editItem.id, undefined, formData)
if (res.success) {
toast.success(res.message)
setEditItem(null)
router.refresh()
} else {
toast.error(res.message || "Failed to update school")
}
} catch {
toast.error("Failed to update school")
} finally {
setIsWorking(false)
}
}
const handleDelete = async () => {
if (!deleteItem) return
setIsWorking(true)
try {
const res = await deleteSchoolAction(deleteItem.id)
if (res.success) {
toast.success(res.message)
setDeleteItem(null)
router.refresh()
} else {
toast.error(res.message || "Failed to delete school")
}
} catch {
toast.error("Failed to delete school")
} finally {
setIsWorking(false)
}
}
return (
<>
<div className="flex justify-end">
<Button onClick={() => setCreateOpen(true)} disabled={isWorking}>
<Plus className="mr-2 h-4 w-4" />
New school
</Button>
</div>
<Card className="shadow-none">
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<CardTitle className="text-base">All schools</CardTitle>
<Badge variant="secondary" className="tabular-nums">
{schools.length}
</Badge>
</CardHeader>
<CardContent>
{schools.length === 0 ? (
<EmptyState
title="No schools"
description="Create your first school to get started."
className="h-auto border-none shadow-none"
/>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Code</TableHead>
<TableHead>Updated</TableHead>
<TableHead className="w-[60px]" />
</TableRow>
</TableHeader>
<TableBody>
{schools.map((s) => (
<TableRow key={s.id}>
<TableCell className="font-medium">{s.name}</TableCell>
<TableCell className="text-muted-foreground">{s.code || "-"}</TableCell>
<TableCell className="text-muted-foreground">{formatDate(s.updatedAt)}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" disabled={isWorking}>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setEditItem(s)}>
<Pencil className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setDeleteItem(s)}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>New school</DialogTitle>
</DialogHeader>
<form action={handleCreate} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Name</Label>
<Input id="name" name="name" placeholder="e.g. First Primary School" autoFocus />
</div>
<div className="space-y-2">
<Label htmlFor="code">Code</Label>
<Input id="code" name="code" placeholder="Optional" />
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)} disabled={isWorking}>
Cancel
</Button>
<Button type="submit" disabled={isWorking}>
Create
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog
open={Boolean(editItem)}
onOpenChange={(open) => {
if (!open) setEditItem(null)
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit school</DialogTitle>
</DialogHeader>
{editItem ? (
<form action={handleUpdate} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-name">Name</Label>
<Input id="edit-name" name="name" defaultValue={editItem.name} />
</div>
<div className="space-y-2">
<Label htmlFor="edit-code">Code</Label>
<Input id="edit-code" name="code" defaultValue={editItem.code || ""} />
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setEditItem(null)} disabled={isWorking}>
Cancel
</Button>
<Button type="submit" disabled={isWorking}>
Save
</Button>
</DialogFooter>
</form>
) : null}
</DialogContent>
</Dialog>
<AlertDialog
open={Boolean(deleteItem)}
onOpenChange={(open) => {
if (!open) setDeleteItem(null)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete school</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete {deleteItem?.name || "this school"} and its grades.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isWorking}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} disabled={isWorking}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}