310 lines
17 KiB
TypeScript
310 lines
17 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";
|
||
|
||
export interface Unit {
|
||
id: number;
|
||
name: string;
|
||
code: string | null;
|
||
}
|
||
|
||
interface UnitManagerDialogProps {
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
units: Unit[];
|
||
}
|
||
|
||
export default function UnitManagerDialog({
|
||
open,
|
||
onOpenChange,
|
||
units,
|
||
}: UnitManagerDialogProps) {
|
||
const [editingId, setEditingId] = useState<number | null>(null);
|
||
const [editName, setEditName] = useState("");
|
||
const [editCode, setEditCode] = useState("");
|
||
|
||
const { data, setData, post, processing, reset, errors, clearErrors } = useForm({
|
||
name: "",
|
||
code: "",
|
||
});
|
||
|
||
useEffect(() => {
|
||
if (!open) {
|
||
reset();
|
||
clearErrors();
|
||
setEditingId(null);
|
||
}
|
||
}, [open]);
|
||
|
||
const handleAdd = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!data.name.trim()) return;
|
||
|
||
post(route("units.store"), {
|
||
onSuccess: () => {
|
||
reset();
|
||
},
|
||
onError: (errors) => {
|
||
toast.error("新增失敗: " + (errors.name || errors.code || "未知錯誤"));
|
||
}
|
||
});
|
||
};
|
||
|
||
const startEdit = (unit: Unit) => {
|
||
setEditingId(unit.id);
|
||
setEditName(unit.name);
|
||
setEditCode(unit.code || "");
|
||
};
|
||
|
||
const cancelEdit = () => {
|
||
setEditingId(null);
|
||
setEditName("");
|
||
setEditCode("");
|
||
};
|
||
|
||
const saveEdit = (id: number) => {
|
||
if (!editName.trim()) return;
|
||
|
||
router.put(route("units.update", id), { name: editName, code: editCode }, {
|
||
onSuccess: () => {
|
||
setEditingId(null);
|
||
},
|
||
onError: (errors) => {
|
||
toast.error("更新失敗: " + (errors.name || errors.code || "未知錯誤"));
|
||
}
|
||
});
|
||
};
|
||
|
||
const handleDelete = (id: number) => {
|
||
router.delete(route("units.destroy", id), {
|
||
onSuccess: () => {
|
||
// 由全域 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 grid grid-cols-2 gap-3">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="new-unit-name" className="text-xs text-gray-500">單位名稱</Label>
|
||
<Input
|
||
id="new-unit-name"
|
||
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>
|
||
<div className="space-y-2">
|
||
<Label htmlFor="new-unit-code" className="text-xs text-gray-500">代碼 (選填)</Label>
|
||
<Input
|
||
id="new-unit-code"
|
||
placeholder="例如: box, kg"
|
||
value={data.code}
|
||
onChange={(e) => setData("code", e.target.value)}
|
||
className={errors.code ? "border-red-500" : ""}
|
||
/>
|
||
{errors.code && <p className="text-xs text-red-500 mt-1">{errors.code}</p>}
|
||
</div>
|
||
</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">共 {units.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="font-medium text-gray-700">代碼</TableHead>
|
||
<TableHead className="w-[140px] text-right font-medium text-gray-700">操作</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{units.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={4} className="text-center py-12 text-gray-400">
|
||
目前尚無單位,請從上方新增。
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
units.map((unit, index) => (
|
||
<TableRow key={unit.id}>
|
||
<TableCell className="py-3 text-center text-gray-500 font-medium">
|
||
{index + 1}
|
||
</TableCell>
|
||
<TableCell className="py-3">
|
||
{editingId === unit.id ? (
|
||
<Input
|
||
value={editName}
|
||
onChange={(e) => setEditName(e.target.value)}
|
||
className="h-9 focus-visible:ring-1"
|
||
autoFocus
|
||
placeholder="單位名稱"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') saveEdit(unit.id);
|
||
if (e.key === 'Escape') cancelEdit();
|
||
}}
|
||
/>
|
||
) : (
|
||
<span className="font-medium text-gray-700">{unit.name}</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell className="py-3">
|
||
{editingId === unit.id ? (
|
||
<Input
|
||
value={editCode}
|
||
onChange={(e) => setEditCode(e.target.value)}
|
||
className="h-9 focus-visible:ring-1"
|
||
placeholder="代碼"
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') saveEdit(unit.id);
|
||
if (e.key === 'Escape') cancelEdit();
|
||
}}
|
||
/>
|
||
) : (
|
||
<span className="text-gray-500">{unit.code || '-'}</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell className="text-right py-3">
|
||
{editingId === unit.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(unit.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(unit)}
|
||
>
|
||
<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>
|
||
確定要刪除「{unit.name}」嗎?<br />
|
||
若該單位下仍有商品,系統將會拒絕刪除。
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||
<AlertDialogAction
|
||
onClick={() => handleDelete(unit.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>
|
||
);
|
||
}
|