Files
star-erp/resources/js/Components/Warehouse/SafetyStock/AddSafetyStockDialog.tsx
sky121113 3ce96537b3
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 1m0s
feat: 標準化全系統數值輸入欄位與擴充商品價格功能
1. UI 標準化:
   - 針對全系統數值輸入欄位統一加上 step='any' 以支援小數點。
   - 表格形式 (Table) 的數值輸入欄位統一加上 text-right 靠右對齊。
   - 修正 Components 與 Pages 中所有涉及金額與數量的輸入框。

2. 功能擴充與修正:
   - 擴充 Product 模型與相關 Dialog 以支援多種價格設定。
   - 修正 Inventory/GoodsReceipt/Create.tsx 未使用的變數錯誤。
   - 優化庫存相關頁面的 UI 一致性。

3. 其他:
   - 更新相關的 Type 定義與 Controller 邏輯。
2026-02-05 11:45:08 +08:00

284 lines
12 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 } from "react";
import { Search, ChevronRight, ChevronLeft } from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Checkbox } from "@/Components/ui/checkbox";
import { SafetyStockSetting, Product } from "@/types/warehouse";
import { toast } from "sonner";
import { Badge } from "@/Components/ui/badge";
interface AddSafetyStockDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
warehouseId: string;
existingSettings: SafetyStockSetting[];
availableProducts: Product[];
onAdd: (settings: SafetyStockSetting[]) => void;
}
export default function AddSafetyStockDialog({
open,
onOpenChange,
warehouseId,
existingSettings,
availableProducts,
onAdd,
}: AddSafetyStockDialogProps) {
const [step, setStep] = useState<1 | 2>(1);
const [searchTerm, setSearchTerm] = useState("");
const [selectedProducts, setSelectedProducts] = useState<Set<string>>(new Set());
const [productQuantities, setProductQuantities] = useState<Map<string, number>>(new Map());
// 重置對話框
const resetDialog = () => {
setStep(1);
setSearchTerm("");
setSelectedProducts(new Set());
setProductQuantities(new Map());
};
// 關閉對話框
const handleClose = () => {
resetDialog();
onOpenChange(false);
};
// 已設定的商品 ID
const existingProductIds = new Set(existingSettings.map((s) => s.productId));
// 可選擇的商品(排除已設定的)
const selectableProducts = availableProducts.filter(
(p) => !existingProductIds.has(p.id)
);
// 篩選後的商品
const filteredProducts = selectableProducts.filter(
(p) =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.type.includes(searchTerm)
);
// 切換商品選擇
const toggleProduct = (productId: string) => {
const newSet = new Set(selectedProducts);
if (newSet.has(productId)) {
newSet.delete(productId);
// 同時移除數量設定
const newQuantities = new Map(productQuantities);
newQuantities.delete(productId);
setProductQuantities(newQuantities);
} else {
newSet.add(productId);
// 預設數量
const newQuantities = new Map(productQuantities);
if (!newQuantities.has(productId)) {
newQuantities.set(productId, 10);
}
setProductQuantities(newQuantities);
}
setSelectedProducts(newSet);
};
// 更新商品安全庫存量
const updateQuantity = (productId: string, value: number) => {
const newQuantities = new Map(productQuantities);
newQuantities.set(productId, value); // 允許為 0
setProductQuantities(newQuantities);
};
// 前往步驟 2
const goToStep2 = () => {
if (selectedProducts.size === 0) {
toast.error("請至少選擇一個商品");
return;
}
setStep(2);
};
// 提交
const handleSubmit = () => {
// 驗證所有商品都已輸入數量
const missingQuantity = Array.from(selectedProducts).some(
(productId) => !productQuantities.has(productId) || (productQuantities.get(productId) ?? -1) < 0
);
if (missingQuantity) {
toast.error("請為所有商品設定安全庫存量");
return;
}
// 創建安全庫存設定
const newSettings: SafetyStockSetting[] = Array.from(selectedProducts).map((productId) => {
const product = availableProducts.find((p) => p.id === productId)!;
return {
id: `ss-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
warehouseId,
productId,
productName: product.name,
productType: product.type,
safetyStock: productQuantities.get(productId) || 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
});
onAdd(newSettings);
// toast.success(`成功新增 ${newSettings.length} 項安全庫存設定`); // 父組件已處理
handleClose();
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>
- {step}/2
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{step === 1 ? (
// 步驟 1選擇商品
<div className="space-y-4">
<div className="space-y-2">
<Label></Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜尋商品名稱或類型..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 button-outlined-primary"
/>
</div>
</div>
<div className="border rounded-lg max-h-96 overflow-y-auto">
{filteredProducts.length === 0 ? (
<div className="p-8 text-center text-gray-400">
{selectableProducts.length === 0
? "所有商品都已設定安全庫存"
: "無符合條件的商品"}
</div>
) : (
<div className="divide-y">
{filteredProducts.map((product) => {
const isSelected = selectedProducts.has(product.id);
return (
<div
key={product.id}
className={`p-4 flex items-center gap-3 hover:bg-gray-50 cursor-pointer transition-colors ${isSelected ? "bg-blue-50" : ""
}`}
onClick={() => toggleProduct(product.id)}
>
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleProduct(product.id)}
/>
<div className="flex-1">
<div className="font-medium">{product.name}</div>
</div>
<Badge variant="outline">{product.type}</Badge>
</div>
);
})}
</div>
)}
</div>
<div className="text-sm text-gray-600">
{selectedProducts.size}
</div>
</div>
) : (
// 步驟 2設定安全庫存量
<div className="space-y-4">
<p className="text-sm text-gray-600">
{selectedProducts.size}
</p>
<div className="border rounded-lg max-h-96 overflow-y-auto">
<div className="divide-y">
{Array.from(selectedProducts).map((productId) => {
const product = availableProducts.find((p) => p.id === productId)!;
const quantity = productQuantities.get(productId) || 0;
return (
<div key={productId} className="p-4 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="font-medium">{product.name}</span>
<Badge variant="outline">{product.type}</Badge>
</div>
</div>
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center gap-2">
<Input
type="number"
min="0"
step="any"
value={quantity || ""}
onChange={(e) =>
updateQuantity(productId, parseFloat(e.target.value) || 0)
}
placeholder="請輸入數量"
className="flex-1 button-outlined-primary"
/>
<span className="text-sm text-gray-500 w-12">{product.unit || '個'}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
)}
<DialogFooter>
{step === 1 ? (
<>
<Button variant="outline" onClick={handleClose} className="button-outlined-primary">
</Button>
<Button
onClick={goToStep2}
disabled={selectedProducts.size === 0}
className="button-filled-primary"
>
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
</>
) : (
<>
<Button variant="outline" onClick={() => setStep(1)} className="button-outlined-primary">
<ChevronLeft className="mr-2 h-4 w-4" />
</Button>
<Button onClick={handleSubmit} className="button-filled-primary">
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}