Files
star-erp/resources/js/Components/Category/CategoryManagerDialog.tsx

270 lines
14 KiB
TypeScript
Raw Normal View History

2025-12-30 15:03:19 +08:00
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/Components/ui/alert-dialog";
import { router, useForm } from "@inertiajs/react";
import { toast } from "sonner";
import { Trash2, Edit2, Check, X, Plus, Loader2 } from "lucide-react";
import { Category } from "@/Pages/Product/Index";
interface CategoryManagerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
categories: Category[];
}
export default function CategoryManagerDialog({
open,
onOpenChange,
categories,
}: CategoryManagerDialogProps) {
const [editingId, setEditingId] = useState<number | null>(null);
const [editName, setEditName] = useState("");
const { data, setData, post, processing, reset, errors, clearErrors } = useForm({
name: "",
});
useEffect(() => {
if (!open) {
reset();
clearErrors();
setEditingId(null);
}
}, [open]);
const handleAdd = (e: React.FormEvent) => {
e.preventDefault();
if (!data.name.trim()) return;
post(route("categories.store"), {
onSuccess: () => {
reset();
},
onError: (errors) => {
toast.error("新增失敗: " + (errors.name || "未知錯誤"));
}
});
};
const startEdit = (category: Category) => {
setEditingId(category.id);
setEditName(category.name);
};
const cancelEdit = () => {
setEditingId(null);
setEditName("");
};
const saveEdit = (id: number) => {
if (!editName.trim()) return;
router.put(route("categories.update", id), { name: editName }, {
onSuccess: () => {
setEditingId(null);
},
onError: (errors) => {
toast.error("更新失敗: " + (errors.name || "未知錯誤"));
}
});
};
const handleDelete = (id: number) => {
router.delete(route("categories.destroy", id), {
onSuccess: () => {
// 不在此處理 toast交由全域 flash 處理
2025-12-30 15:03:19 +08:00
},
onError: () => {
toast.error("刪除失敗,請確認該分類下無商品");
}
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto py-4 space-y-6">
{/* Add New Section */}
<div className="space-y-4">
<h3 className="text-sm font-medium border-l-4 border-primary pl-2"></h3>
<form onSubmit={handleAdd} className="flex items-end gap-3 p-4 bg-white border rounded-lg shadow-sm">
<div className="flex-1 space-y-2">
<Label htmlFor="new-category" className="text-xs text-gray-500"></Label>
<Input
id="new-category"
placeholder="輸入新分類名稱..."
value={data.name}
onChange={(e) => setData("name", e.target.value)}
className={errors.name ? "border-red-500" : ""}
/>
{errors.name && <p className="text-xs text-red-500 mt-1">{errors.name}</p>}
</div>
<Button type="submit" disabled={processing} className="button-filled-primary h-10 px-6">
{processing ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
</Button>
</form>
</div>
{/* List Section */}
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium border-l-4 border-primary pl-2"></h3>
<span className="text-xs text-gray-400"> {categories.length} </span>
</div>
<div className="bg-white border rounded-lg shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="w-[50px] font-medium text-gray-700">#</TableHead>
<TableHead className="font-medium text-gray-700"></TableHead>
<TableHead className="w-[140px] text-right font-medium text-gray-700"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{categories.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="text-center py-12 text-gray-400">
</TableCell>
</TableRow>
) : (
categories.map((category, index) => (
<TableRow key={category.id}>
<TableCell className="py-3 text-center text-gray-500 font-medium">
{index + 1}
</TableCell>
<TableCell className="py-3">
{editingId === category.id ? (
<Input
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="h-9 focus-visible:ring-1"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') saveEdit(category.id);
if (e.key === 'Escape') cancelEdit();
}}
/>
) : (
<span className="font-medium text-gray-700">{category.name}</span>
)}
</TableCell>
<TableCell className="text-right py-3">
{editingId === category.id ? (
<div className="flex justify-end gap-1">
<Button
size="sm"
variant="ghost"
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-50"
onClick={() => saveEdit(category.id)}
>
<Check className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-8 w-8 text-gray-400 hover:text-gray-600 hover:bg-gray-100"
onClick={cancelEdit}
>
<X className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex justify-end gap-1">
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 button-outlined-primary"
onClick={() => startEdit(category)}
>
<Edit2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="h-8 w-8 p-0 button-outlined-error"
>
<Trash2 className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{category.name}<br />
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(category.id)}
className="bg-red-600 hover:bg-red-700"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
</div>
<div className="flex justify-end gap-2 pt-4 border-t mt-auto">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="button-outlined-primary px-8"
>
</Button>
</div>
</DialogContent>
</Dialog>
);
}