refactor: 重構 VendorProduct API 與新增進貨單重複檢查前端邏輯
1. 將 VendorProductController 中的 Eloquent 關聯操作改為透過 ProcurementService 使用 DB 操作,解除跨模組 Model 直接依賴。 2. ProcurementService 加入 vendor product 的資料存取方法。 3. 進貨單建立前端 (GoodsReceipt/Create.tsx) 新增重複進貨檢查與警告對話框邏輯。
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
import axios from 'axios';
|
||||
import { PurchaseOrderStatus } from '@/types/purchase-order';
|
||||
import { STATUS_CONFIG } from '@/constants/purchase-order';
|
||||
import { DuplicateWarningDialog } from './components/DuplicateWarningDialog';
|
||||
|
||||
|
||||
|
||||
@@ -89,6 +90,11 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
|
||||
const [productSearch, setProductSearch] = useState('');
|
||||
const [foundProducts, setFoundProducts] = useState<any[]>([]);
|
||||
|
||||
// Duplicate Check States
|
||||
const [warningOpen, setWarningOpen] = useState(false);
|
||||
const [warnings, setWarnings] = useState<any[]>([]);
|
||||
const [isCheckingDuplicate, setIsCheckingDuplicate] = useState(false);
|
||||
|
||||
const { data, setData, post, processing, errors } = useForm({
|
||||
type: 'standard', // 'standard', 'miscellaneous', 'other'
|
||||
warehouse_id: '',
|
||||
@@ -280,9 +286,30 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
|
||||
});
|
||||
}, [nextSequences, JSON.stringify(data.items.map(i => ({ m: i.batchMode, c: i.originCountry, s: i.product_code, p: i.product_id }))), data.received_date]);
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
post(route('goods-receipts.store'));
|
||||
const submit = async (e: React.FormEvent, force: boolean = false) => {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
// 如果不是強制提交,先檢查重複
|
||||
if (!force) {
|
||||
setIsCheckingDuplicate(true);
|
||||
try {
|
||||
const response = await axios.post(route('goods-receipts.check-duplicate'), data);
|
||||
if (response.data.has_warnings) {
|
||||
setWarnings(response.data.warnings);
|
||||
setWarningOpen(true);
|
||||
return; // 停止並顯示警告
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Duplicate check failed", error);
|
||||
// 檢查失敗則繼續,或視為阻擋?這裡選擇繼續
|
||||
} finally {
|
||||
setIsCheckingDuplicate(false);
|
||||
}
|
||||
}
|
||||
|
||||
post(route('goods-receipts.store'), {
|
||||
onSuccess: () => setWarningOpen(false),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -738,13 +765,21 @@ export default function GoodsReceiptCreate({ warehouses, pendingPurchaseOrders,
|
||||
size="lg"
|
||||
className="button-filled-primary px-12 h-14 rounded-xl shadow-lg text-lg font-bold transition-all hover:scale-[1.02] active:scale-[0.98]"
|
||||
onClick={submit}
|
||||
disabled={processing || (data.type === 'standard' ? !selectedPO : !selectedVendor)}
|
||||
disabled={processing || isCheckingDuplicate || (data.type === 'standard' ? !selectedPO : !selectedVendor)}
|
||||
>
|
||||
<Save className="mr-2 h-5 w-5" />
|
||||
{processing ? '處理中...' : '確認進貨'}
|
||||
{processing || isCheckingDuplicate ? '處理中...' : '確認進貨'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DuplicateWarningDialog
|
||||
open={warningOpen}
|
||||
onClose={() => setWarningOpen(false)}
|
||||
onConfirm={() => submit(null as any, true)}
|
||||
warnings={warnings}
|
||||
processing={processing}
|
||||
/>
|
||||
</AuthenticatedLayout >
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user