first commit

This commit is contained in:
2025-12-30 15:03:19 +08:00
commit c735c36009
902 changed files with 83591 additions and 0 deletions

View File

@@ -0,0 +1,347 @@
/**
* 倉庫對話框元件
* 重構後:加入驗證邏輯
*/
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "./ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Textarea } from "./ui/textarea";
import { Button } from "./ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { Trash2 } from "lucide-react";
import { Warehouse, WarehouseType, Store } from "../types/warehouse";
import { validateWarehouse } from "../utils/validation";
import { toast } from "sonner@2.0.3";
interface WarehouseDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
warehouse: Warehouse | null;
onSave: (warehouse: Omit<Warehouse, "id" | "createdAt">) => void;
onDelete?: (warehouseId: string) => void;
stores?: Store[]; // 門市列表
}
export default function WarehouseDialog({
open,
onOpenChange,
warehouse,
onSave,
onDelete,
stores = [], // 預設為空陣列
}: WarehouseDialogProps) {
const [formData, setFormData] = useState<{
name: string;
address: string;
manager: string;
phone: string;
description: string;
type: WarehouseType;
storeId?: string;
storeName?: string;
}>({
name: "",
address: "",
manager: "",
phone: "",
description: "",
type: "中央倉庫",
storeId: undefined,
storeName: undefined,
});
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
useEffect(() => {
if (warehouse) {
setFormData({
name: warehouse.name,
address: warehouse.address,
manager: warehouse.manager,
phone: warehouse.phone,
description: warehouse.description,
type: warehouse.type,
storeId: warehouse.storeId,
storeName: warehouse.storeName,
});
} else {
setFormData({
name: "",
address: "",
manager: "",
phone: "",
description: "",
type: "中央倉庫",
storeId: undefined,
storeName: undefined,
});
}
}, [warehouse, open]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const validation = validateWarehouse(formData);
if (!validation.isValid) {
toast.error(validation.error);
return;
}
onSave(formData);
};
const handleDelete = () => {
if (warehouse && onDelete) {
onDelete(warehouse.id);
setShowDeleteDialog(false);
onOpenChange(false);
}
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{warehouse ? "編輯倉庫" : "新增倉庫"}</DialogTitle>
<DialogDescription>
{warehouse ? "修改倉庫資訊" : "建立新的倉庫資訊"}
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-6 py-4">
{/* 區塊 A基本資訊 */}
<div className="space-y-4">
<div className="border-b pb-2">
<h4 className="text-sm text-gray-700"></h4>
</div>
{/* 倉庫名稱 */}
<div className="space-y-2">
<Label htmlFor="name">
<span className="text-red-500">*</span>
</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="例:中央倉庫"
required
/>
</div>
{/* 倉庫類型 */}
<div className="space-y-2">
<Label htmlFor="type">
<span className="text-red-500">*</span>
</Label>
<Select
value={formData.type}
onValueChange={(value) => {
const newType = value as WarehouseType;
setFormData({
...formData,
type: newType,
// 切換到中央倉庫時清空門市綁定
storeId: newType === "中央倉庫" ? undefined : formData.storeId,
storeName: newType === "中央倉庫" ? undefined : formData.storeName,
});
}}
>
<SelectTrigger id="type">
<SelectValue placeholder="選擇倉庫類型">{formData.type}</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="中央倉庫"></SelectItem>
<SelectItem value="門市"></SelectItem>
</SelectContent>
</Select>
</div>
{/* 綁定門市 - 僅當類型為門市時顯示 */}
{formData.type === "門市" && (
<div className="space-y-2">
<Label htmlFor="store"></Label>
<Select
value={formData.storeId || ""}
onValueChange={(value) => {
const selectedStore = stores.find((s) => s.id === value);
setFormData({
...formData,
storeId: value,
storeName: selectedStore?.name,
});
}}
>
<SelectTrigger id="store">
<SelectValue placeholder="選擇門市">
{formData.storeName || "請選擇門市"}
</SelectValue>
</SelectTrigger>
<SelectContent>
{stores.length === 0 ? (
<div className="px-2 py-1.5 text-sm text-gray-500">
</div>
) : (
stores.map((store) => (
<SelectItem key={store.id} value={store.id}>
{store.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
{formData.storeId && (
<p className="text-sm text-gray-500">
{stores.find((s) => s.id === formData.storeId)?.address}
</p>
)}
</div>
)}
</div>
{/* 區塊 B位置 */}
<div className="space-y-4">
<div className="border-b pb-2">
<h4 className="text-sm text-gray-700"></h4>
</div>
{/* 倉庫地址 */}
<div className="space-y-2">
<Label htmlFor="address">
<span className="text-red-500">*</span>
</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
placeholder="例台北市信義區信義路五段7號"
required
/>
</div>
</div>
{/* 區塊 C聯絡資訊 */}
<div className="space-y-4">
<div className="border-b pb-2">
<h4 className="text-sm text-gray-700"></h4>
</div>
{/* 負責人和聯絡電話 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="manager"></Label>
<Input
id="manager"
value={formData.manager}
onChange={(e) =>
setFormData({ ...formData, manager: e.target.value })
}
placeholder="例:張經理"
/>
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
placeholder="例02-1234-5678"
/>
</div>
</div>
{/* 備註說明 */}
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder="其他說明"
rows={2}
className="resize-none"
/>
</div>
</div>
</div>
<DialogFooter className="gap-2">
{warehouse && onDelete && (
<Button
type="button"
onClick={() => setShowDeleteDialog(true)}
variant="outline"
className="group mr-auto border-destructive text-destructive hover:bg-destructive hover:text-white"
>
<Trash2 className="mr-2 h-4 w-4 group-hover:text-white" />
</Button>
)}
<Button
type="button"
onClick={() => onOpenChange(false)}
className="button-outlined-primary"
>
</Button>
<Button type="submit" className="button-filled-primary">
{warehouse ? "更新" : "新增"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* 刪除確認對話框 */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
{warehouse?.name}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}