refactor: 重構 VendorProduct API 與新增進貨單重複檢查前端邏輯
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m9s
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m10s

1. 將 VendorProductController 中的 Eloquent 關聯操作改為透過 ProcurementService 使用 DB 操作,解除跨模組 Model 直接依賴。
2. ProcurementService 加入 vendor product 的資料存取方法。
3. 進貨單建立前端 (GoodsReceipt/Create.tsx) 新增重複進貨檢查與警告對話框邏輯。
This commit is contained in:
2026-02-25 11:11:28 +08:00
parent e406ecd63d
commit ad91b08dbc
11 changed files with 689 additions and 23 deletions

View File

@@ -0,0 +1,176 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/Components/ui/dialog";
import { Button } from "@/Components/ui/button";
import { AlertTriangle, ArrowRight } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/Components/ui/table";
import { StatusBadge } from "@/Components/shared/StatusBadge";
interface DuplicateWarningDialogProps {
open: boolean;
onClose: () => void;
onConfirm: () => void;
warnings: any[];
processing?: boolean;
}
export function DuplicateWarningDialog({ open, onClose, onConfirm, warnings, processing }: DuplicateWarningDialogProps) {
return (
<Dialog open={open} onOpenChange={(val) => !val && onClose()}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center gap-2 text-amber-600 mb-2">
<AlertTriangle className="h-6 w-6" />
<DialogTitle className="text-xl font-bold"></DialogTitle>
</div>
<p className="text-gray-500">
</p>
</DialogHeader>
<div className="py-4 space-y-6">
{warnings.map((warning, idx) => (
<div key={idx} className={`p-4 rounded-lg border ${warning.level === 'high' ? 'bg-red-50 border-red-100' : 'bg-amber-50 border-amber-100'}`}>
<div className="flex items-center gap-2 mb-3">
<div className={`w-2 h-2 rounded-full ${warning.level === 'high' ? 'bg-red-500' : 'bg-amber-500'}`} />
<h4 className={`font-bold ${warning.level === 'high' ? 'text-red-900' : 'text-amber-900'}`}>
{warning.title}
</h4>
</div>
<p className="text-sm text-gray-700 mb-4">{warning.message}</p>
{/* Same PO Warning Details */}
{warning.type === 'same_po' && warning.related_receipts && (
<div className="bg-white rounded border overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs text-center"></TableHead>
</TableRow>
</TableHeader>
<TableBody className="text-xs">
{warning.related_receipts.map((r: any) => (
<TableRow key={r.id}>
<TableCell className="font-medium text-blue-600">{r.code}</TableCell>
<TableCell>{r.received_date}</TableCell>
<TableCell>
<StatusBadge variant="neutral">{r.status}</StatusBadge>
</TableCell>
<TableCell className="text-center">{r.item_count} </TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* Recent Products Warning Details */}
{warning.type === 'recent_duplicate_product' && warning.duplicated_items && (
<div className="bg-white rounded border overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs"> / </TableHead>
<TableHead className="text-xs text-right"></TableHead>
<TableHead className="text-xs text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody className="text-xs">
{warning.duplicated_items.map((item: any, i: number) => (
<TableRow key={i}>
<TableCell>
<div>{item.product_name}</div>
<div className="text-[10px] text-gray-400">{item.product_id}</div>
</TableCell>
<TableCell>
<div>{item.last_receipt_date}</div>
<div className="text-[10px] text-gray-400">{item.last_receipt_code}</div>
</TableCell>
<TableCell className="text-right">{item.last_quantity}</TableCell>
<TableCell className="text-right font-bold flex items-center justify-end gap-1">
<ArrowRight className="h-3 w-3 text-gray-300" />
{item.current_quantity}
{item.is_high_risk && <span className="text-[10px] bg-red-100 text-red-600 px-1 rounded"></span>}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* Stale Price Warning Details */}
{warning.type === 'stale_price' && warning.stale_items && (
<div className="bg-white rounded border overflow-hidden">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="text-xs"></TableHead>
<TableHead className="text-xs text-right"></TableHead>
<TableHead className="text-xs text-center"></TableHead>
<TableHead className="text-xs"></TableHead>
</TableRow>
</TableHeader>
<TableBody className="text-xs">
{warning.stale_items.map((item: any, i: number) => (
<TableRow key={i}>
<TableCell>
<div>{item.product_name}</div>
<div className="text-[10px] text-gray-400">{item.product_id}</div>
</TableCell>
<TableCell className="text-right font-mono font-bold text-amber-700">
${item.unit_price.toLocaleString()}
</TableCell>
<TableCell className="text-center">
{item.record_count}
</TableCell>
<TableCell>
<div>{item.earliest_date} ~ {item.latest_date}</div>
<div className="text-[10px] text-gray-400">{item.latest_code}</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
))}
</div>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={onClose}
disabled={processing}
className="button-outlined-primary"
>
</Button>
<Button
onClick={onConfirm}
className="button-filled-primary"
disabled={processing}
>
{processing ? '處理中...' : '確認無重複,繼續建立'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}