- scheduling: update auto-schedule-panel, schedule-change-form, schedule-change-list, schedule-conflicts-view, scheduling-rules-form, data-access-class-schedule, data-access - school: update academic-year-view, departments-view, grade-form-dialog, data-access - settings: update admin-settings-view, ai-provider-settings-card, avatar-upload, brand-config-card, notification-preferences-form, password-change-form, profile-settings-form, security-recent-logins-section, security-two-factor-section - standards: update data-access - student: update student-courses-view - textbooks: update chapter-sidebar-list, create-chapter-dialog, force-graph, graph-kp-node, graph-prerequisite-edge, knowledge-graph-node, textbook-card, textbook-form-dialog, textbook-reader, textbook-settings-dialog, data-access-graph, data-access, use-kp-create, use-kp-delete, use-kp-update, types - users: update user-import-dialog
318 lines
13 KiB
TypeScript
318 lines
13 KiB
TypeScript
"use client"
|
||
|
||
import { useMemo, useState } from "react"
|
||
import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
||
import { notify } from "@/shared/lib/notify"
|
||
import { useRouter } from "next/navigation"
|
||
import { useTranslations } from "next-intl"
|
||
|
||
import type { AcademicYearListItem } from "../types"
|
||
import { createAcademicYearAction, deleteAcademicYearAction, updateAcademicYearAction } 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 { Checkbox } from "@/shared/components/ui/checkbox"
|
||
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"
|
||
|
||
const toDateInput = (iso: string) => iso.slice(0, 10)
|
||
|
||
export function AcademicYearClient({ years }: { years: AcademicYearListItem[] }) {
|
||
const t = useTranslations("school")
|
||
const router = useRouter()
|
||
const [isWorking, setIsWorking] = useState(false)
|
||
const [createOpen, setCreateOpen] = useState(false)
|
||
const [createActive, setCreateActive] = useState(true)
|
||
const [editItem, setEditItem] = useState<AcademicYearListItem | null>(null)
|
||
const [editActive, setEditActive] = useState(false)
|
||
const [deleteItem, setDeleteItem] = useState<AcademicYearListItem | null>(null)
|
||
|
||
const activeYear = useMemo(() => years.find((y) => y.isActive) ?? null, [years])
|
||
|
||
const handleCreate = async (formData: FormData) => {
|
||
setIsWorking(true)
|
||
try {
|
||
formData.set("isActive", createActive ? "true" : "false")
|
||
const res = await createAcademicYearAction(undefined, formData)
|
||
if (res.success) {
|
||
notify.success(res.message)
|
||
setCreateOpen(false)
|
||
router.refresh()
|
||
} else {
|
||
notify.error(res.message || t("academicYear.delete.title"))
|
||
}
|
||
} catch {
|
||
notify.error(t("academicYear.delete.title"))
|
||
} finally {
|
||
setIsWorking(false)
|
||
}
|
||
}
|
||
|
||
const handleUpdate = async (formData: FormData) => {
|
||
if (!editItem) return
|
||
setIsWorking(true)
|
||
try {
|
||
formData.set("isActive", editActive ? "true" : "false")
|
||
const res = await updateAcademicYearAction(editItem.id, undefined, formData)
|
||
if (res.success) {
|
||
notify.success(res.message)
|
||
setEditItem(null)
|
||
router.refresh()
|
||
} else {
|
||
notify.error(res.message || t("academicYear.delete.title"))
|
||
}
|
||
} catch {
|
||
notify.error(t("academicYear.delete.title"))
|
||
} finally {
|
||
setIsWorking(false)
|
||
}
|
||
}
|
||
|
||
const handleDelete = async () => {
|
||
if (!deleteItem) return
|
||
setIsWorking(true)
|
||
try {
|
||
const res = await deleteAcademicYearAction(deleteItem.id)
|
||
if (res.success) {
|
||
notify.success(res.message)
|
||
setDeleteItem(null)
|
||
router.refresh()
|
||
} else {
|
||
notify.error(res.message || t("academicYear.delete.title"))
|
||
}
|
||
} catch {
|
||
notify.error(t("academicYear.delete.title"))
|
||
} finally {
|
||
setIsWorking(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<div className="flex justify-end">
|
||
<Button
|
||
onClick={() => {
|
||
setCreateActive(activeYear === null)
|
||
setCreateOpen(true)
|
||
}}
|
||
disabled={isWorking}
|
||
>
|
||
<Plus className="mr-2 h-4 w-4" />
|
||
{t("academicYear.new")}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="grid gap-6 lg:grid-cols-3">
|
||
<Card className="lg:col-span-1 shadow-none">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{t("academicYear.active")}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{activeYear ? (
|
||
<div className="space-y-2">
|
||
<div className="text-lg font-semibold">{activeYear.name}</div>
|
||
<div className="text-sm text-muted-foreground">
|
||
{formatDate(activeYear.startDate)} – {formatDate(activeYear.endDate)}
|
||
</div>
|
||
<Badge variant="secondary">{t("academicYear.active")}</Badge>
|
||
</div>
|
||
) : (
|
||
<EmptyState
|
||
title={t("academicYear.empty.title")}
|
||
description={t("academicYear.empty.description")}
|
||
className="h-auto border-none shadow-none"
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="lg:col-span-2 shadow-none">
|
||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||
<CardTitle className="text-base">{t("academicYear.all")}</CardTitle>
|
||
<Badge variant="secondary" className="tabular-nums">
|
||
{years.length}
|
||
</Badge>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{years.length === 0 ? (
|
||
<EmptyState
|
||
title={t("academicYear.empty.title")}
|
||
description={t("academicYear.empty.description")}
|
||
className="h-auto border-none shadow-none"
|
||
/>
|
||
) : (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>{t("academicYear.column.name")}</TableHead>
|
||
<TableHead>{t("academicYear.column.startDate")}</TableHead>
|
||
<TableHead>{t("academicYear.column.status")}</TableHead>
|
||
<TableHead className="w-[60px]" />
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{years.map((y) => (
|
||
<TableRow key={y.id}>
|
||
<TableCell className="font-medium">{y.name}</TableCell>
|
||
<TableCell className="text-muted-foreground">
|
||
{formatDate(y.startDate)} – {formatDate(y.endDate)}
|
||
</TableCell>
|
||
<TableCell>{y.isActive ? <Badge variant="secondary">{t("academicYear.active")}</Badge> : <Badge variant="outline">-</Badge>}</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(y)
|
||
setEditActive(y.isActive)
|
||
}}
|
||
>
|
||
<Pencil className="mr-2 h-4 w-4" />
|
||
{t("academicYear.actions.edit")}
|
||
</DropdownMenuItem>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItem
|
||
className="text-destructive focus:text-destructive"
|
||
onClick={() => setDeleteItem(y)}
|
||
>
|
||
<Trash2 className="mr-2 h-4 w-4" />
|
||
{t("academicYear.actions.delete")}
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>{t("academicYear.form.createTitle")}</DialogTitle>
|
||
</DialogHeader>
|
||
<form action={handleCreate} className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="name">{t("academicYear.form.name")}</Label>
|
||
<Input id="name" name="name" placeholder={t("academicYear.form.namePlaceholder")} autoFocus />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="startDate">{t("academicYear.form.startDate")}</Label>
|
||
<Input id="startDate" name="startDate" type="date" />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="endDate">{t("academicYear.form.endDate")}</Label>
|
||
<Input id="endDate" name="endDate" type="date" />
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Checkbox checked={createActive} onCheckedChange={(v) => setCreateActive(Boolean(v))} />
|
||
<Label className="cursor-pointer" onClick={() => setCreateActive((v) => !v)}>
|
||
{t("academicYear.form.isActive")}
|
||
</Label>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)} disabled={isWorking}>
|
||
{t("academicYear.form.cancel")}
|
||
</Button>
|
||
<Button type="submit" disabled={isWorking}>
|
||
{t("academicYear.form.create")}
|
||
</Button>
|
||
</DialogFooter>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
<Dialog open={Boolean(editItem)} onOpenChange={(open) => {
|
||
if (!open) setEditItem(null)
|
||
}}>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>{t("academicYear.form.editTitle")}</DialogTitle>
|
||
</DialogHeader>
|
||
{editItem ? (
|
||
<form action={handleUpdate} className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-name">{t("academicYear.form.name")}</Label>
|
||
<Input id="edit-name" name="name" defaultValue={editItem.name} />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-startDate">{t("academicYear.form.startDate")}</Label>
|
||
<Input id="edit-startDate" name="startDate" type="date" defaultValue={toDateInput(editItem.startDate)} />
|
||
</div>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="edit-endDate">{t("academicYear.form.endDate")}</Label>
|
||
<Input id="edit-endDate" name="endDate" type="date" defaultValue={toDateInput(editItem.endDate)} />
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Checkbox checked={editActive} onCheckedChange={(v) => setEditActive(Boolean(v))} />
|
||
<Label className="cursor-pointer" onClick={() => setEditActive((v) => !v)}>
|
||
{t("academicYear.form.isActive")}
|
||
</Label>
|
||
</div>
|
||
<DialogFooter>
|
||
<Button type="button" variant="outline" onClick={() => setEditItem(null)} disabled={isWorking}>
|
||
{t("academicYear.form.cancel")}
|
||
</Button>
|
||
<Button type="submit" disabled={isWorking}>
|
||
{t("academicYear.form.save")}
|
||
</Button>
|
||
</DialogFooter>
|
||
</form>
|
||
) : null}
|
||
</DialogContent>
|
||
</Dialog>
|
||
|
||
<AlertDialog open={Boolean(deleteItem)} onOpenChange={(open) => {
|
||
if (!open) setDeleteItem(null)
|
||
}}>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>{t("academicYear.delete.title")}</AlertDialogTitle>
|
||
<AlertDialogDescription>{t("academicYear.delete.description", { name: deleteItem?.name || "" })}</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel disabled={isWorking}>{t("academicYear.delete.cancel")}</AlertDialogCancel>
|
||
<AlertDialogAction onClick={handleDelete} disabled={isWorking}>
|
||
{t("academicYear.delete.confirm")}
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</>
|
||
)
|
||
}
|