270 lines
14 KiB
TypeScript
270 lines
14 KiB
TypeScript
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 處理
|
||
},
|
||
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>
|
||
);
|
||
}
|