Files
star-erp/resources/js/Components/Unit/UnitManagerDialog.tsx
sky121113 106de4e945
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 53s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped
feat: 修正庫存與撥補單邏輯並整合文件
1. 修復倉庫統計數據加總與樣式。
2. 修正可用庫存計算邏輯(排除不可銷售倉庫)。
3. 撥補單商品列表加入批號與效期顯示。
4. 修正撥補單儲存邏輯以支援精確批號轉移。
5. 整合 FEATURES.md 至 README.md。
2026-01-26 14:59:24 +08:00

310 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: string;
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<string | 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: string) => {
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: string) => {
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>
);
}