新增單位管理以及一些功能修正
This commit is contained in:
309
resources/js/Components/Unit/UnitManagerDialog.tsx
Normal file
309
resources/js/Components/Unit/UnitManagerDialog.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user