import { useState, useMemo } from "react"; import { Head, router } from "@inertiajs/react"; import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; import { Button } from "@/Components/ui/button"; import { Input } from "@/Components/ui/input"; import { Label } from "@/Components/ui/label"; import { SearchableSelect } from "@/Components/ui/searchable-select"; import { BarChart3, TrendingUp, DollarSign, Truck, Clock, CheckCircle, Package, Filter, RotateCcw, ShoppingCart, Info, } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/Components/ui/tooltip"; import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, AreaChart, Area, } from "recharts"; // ===== 型別定義 ===== interface KPIs { total_amount: number; total_orders: number; total_receipts: number; avg_delivery_days: number; on_time_rate: number; } interface VendorDelivery { vendor_id: number; vendor_name: string; total_count: number; avg_days: number; min_days: number; max_days: number; on_time_count: number; on_time_rate: number; } interface DelayDistribution { category: string; count: number; } interface MonthlyTrend { month: string; total_quantity: number; total_amount: number; receipt_count: number; } interface VendorShare { vendor_id: number; vendor_name: string; total_amount: number; total_quantity: number; } interface ProductRanking { product_id: number; product_name: string; product_code: string; total_quantity: number; total_amount: number; } interface PriceTrendItem { product_id: number; product_name: string; month: string; avg_price: number; min_price: number; max_price: number; } interface VendorComparison { product_id: number; product_name: string; vendor_id: number; vendor_name: string; avg_price: number; min_price: number; max_price: number; purchase_count: number; } interface FilterOption { id: number; name: string; code?: string; } interface Props { kpis: KPIs; deliveryAnalysis: { vendor_delivery: VendorDelivery[]; delay_distribution: DelayDistribution[]; }; quantityAnalysis: { monthly_trend: MonthlyTrend[]; vendor_share: VendorShare[]; product_ranking: ProductRanking[]; }; priceTrendAnalysis: { price_trend: PriceTrendItem[]; vendor_comparison: VendorComparison[]; }; vendors: FilterOption[]; warehouses: FilterOption[]; filters: { date_from?: string; date_to?: string; vendor_id?: string; warehouse_id?: string; }; } // 圓餅圖色彩 const PIE_COLORS = [ "#0ea5e9", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#ec4899", "#06b6d4", "#84cc16", "#f97316", "#6366f1", ]; // Tab 定義 const TABS = [ { key: "delivery", label: "供貨時效", icon: }, { key: "quantity", label: "數量分析", icon: }, { key: "price", label: "價格趨勢", icon: }, ] as const; type TabKey = (typeof TABS)[number]["key"]; export default function ProcurementAnalysisIndex({ kpis, deliveryAnalysis, quantityAnalysis, priceTrendAnalysis, vendors, warehouses, filters, }: Props) { const [activeTab, setActiveTab] = useState("delivery"); const [localFilters, setLocalFilters] = useState({ date_from: filters.date_from || "", date_to: filters.date_to || "", vendor_id: filters.vendor_id || "", warehouse_id: filters.warehouse_id || "", }); const handleApplyFilters = () => { const params: Record = {}; Object.entries(localFilters).forEach(([key, value]) => { if (value) params[key] = value; }); router.get(route("procurement.analysis.index"), params, { preserveState: true, preserveScroll: true, }); }; const handleClearFilters = () => { setLocalFilters({ date_from: "", date_to: "", vendor_id: "", warehouse_id: "" }); router.get(route("procurement.analysis.index"), {}, { preserveState: true, preserveScroll: true, }); }; // ===== KPI 卡片資料 ===== const kpiCards = [ { label: "採購總金額", tooltip: "期間內所有非草稿、非取消的採購單總金額", value: `NT$ ${Math.round(kpis.total_amount).toLocaleString()}`, description: `共 ${kpis.total_orders} 筆採購單`, icon: , color: "text-blue-600", bgColor: "bg-blue-50", borderColor: "border-blue-100", }, { label: "進貨筆數", tooltip: "期間內狀態為「已完成」的進貨單數量", value: kpis.total_receipts, description: "已完成進貨單", icon: , color: "text-emerald-600", bgColor: "bg-emerald-50", borderColor: "border-emerald-100", }, { label: "平均交期", tooltip: "真實進貨日期減去採購單日期的平均天數", value: `${kpis.avg_delivery_days} 天`, description: "下單到到貨平均天數", icon: , color: "text-amber-600", bgColor: "bg-amber-50", borderColor: "border-amber-100", }, { label: "準時到貨率", tooltip: "進貨單真實進貨日期早於或等於採購單預計到貨日的比例", value: `${kpis.on_time_rate}%`, description: "於預計交期內到貨", icon: , color: kpis.on_time_rate >= 80 ? "text-emerald-600" : "text-red-600", bgColor: kpis.on_time_rate >= 80 ? "bg-emerald-50" : "bg-red-50", borderColor: kpis.on_time_rate >= 80 ? "border-emerald-100" : "border-red-100", }, ]; return (
{/* 頁面標題 */}

採購統計分析

分析廠商供貨時效、進貨數量、單價趨勢,支援採購決策與成本控制

{/* 篩選列 */}
setLocalFilters((f) => ({ ...f, date_from: e.target.value }))} className="h-9 bg-white" />
setLocalFilters((f) => ({ ...f, date_to: e.target.value }))} className="h-9 bg-white" />
setLocalFilters((f) => ({ ...f, vendor_id: v === "all" ? "" : v }))} options={[{ label: "全部廠商", value: "all" }, ...vendors.map((v) => ({ label: v.name, value: v.id.toString() }))]} className="w-full h-9" placeholder="選擇廠商..." />
setLocalFilters((f) => ({ ...f, warehouse_id: v === "all" ? "" : v }))} options={[{ label: "全部倉庫", value: "all" }, ...warehouses.map((w) => ({ label: w.name, value: w.id.toString() }))]} className="w-full h-9" placeholder="選擇倉庫..." />
{/* KPI 卡片列 */}
{kpiCards.map((card) => (
{card.icon}
{card.label}

{card.tooltip}

{card.value}
{card.description}
))}
{/* Tab 切換 */}
{TABS.map((tab) => ( ))}
{activeTab === "delivery" && ( )} {activeTab === "quantity" && ( )} {activeTab === "price" && ( )}
); } // ===== Tab 1: 供貨時效 ===== function DeliveryTab({ vendorDelivery, delayDistribution, }: { vendorDelivery: VendorDelivery[]; delayDistribution: DelayDistribution[]; }) { if (vendorDelivery.length === 0) { return ; } const parsedDelayDistribution = useMemo(() => { return delayDistribution.map(d => ({ ...d, count: Number(d.count) })); }, [delayDistribution]); return (
{/* 廠商平均交期長條圖 */}

廠商平均交期天數

將進貨資料依廠商分組,計算「進貨日 - 採購日」的平均天數、極端值與準時率。預設由快到慢排序。

{ const labels: Record = { avg_days: "平均交期", min_days: "最短", max_days: "最長", }; return [`${value} 天`, labels[name] || name]; }} /> ({ avg_days: "平均", min_days: "最短", max_days: "最長" }[v] || v)} />
{/* 交期分佈圓餅圖 */}

交期分佈

{parsedDelayDistribution.map((_, index) => ( ))} { const total = parsedDelayDistribution.reduce((sum, item) => sum + item.count, 0); const percent = total > 0 ? ((value / total) * 100).toFixed(0) : 0; return [`${value} 筆 (${percent}%)`, "數量"]; }} />
{/* 廠商準時率排行表格 */}

廠商準時到貨排行

準時率 = (該廠商準時進貨筆數 / 總進貨筆數) * 100%

{vendorDelivery.map((v, idx) => ( ))}
排名 廠商 進貨次數 平均交期 最短 最長 準時率
{idx + 1} {v.vendor_name} {v.total_count} {v.avg_days} 天 {v.min_days} 天 {v.max_days} 天 = 80 ? "bg-emerald-100 text-emerald-800" : v.on_time_rate >= 60 ? "bg-amber-100 text-amber-800" : "bg-red-100 text-red-800" }`}> {v.on_time_rate}%
); } // ===== Tab 2: 數量分析 ===== function QuantityTab({ monthlyTrend, vendorShare, productRanking, }: { monthlyTrend: MonthlyTrend[]; vendorShare: VendorShare[]; productRanking: ProductRanking[]; }) { if (monthlyTrend.length === 0 && vendorShare.length === 0) { return ; } const parsedVendorShare = useMemo(() => { return vendorShare.map(d => ({ ...d, total_amount: Number(d.total_amount) })); }, [vendorShare]); return (
{/* 月度趨勢 */}

月度進貨金額趨勢

將所有已完成的進貨單依月份分組,加總總進貨數量與金額。

`$${(v / 1000).toFixed(0)}k`} tick={{ fontSize: 12 }} /> { if (name === "total_amount") return [`NT$ ${value.toLocaleString()}`, "進貨金額"]; if (name === "receipt_count") return [`${value} 筆`, "進貨筆數"]; return [value, name]; }} />
{/* 廠商佔比 */}

廠商進貨金額佔比

{parsedVendorShare.map((_, index) => ( ))} { const total = parsedVendorShare.reduce((sum, item) => sum + item.total_amount, 0); const percent = total > 0 ? ((value / total) * 100).toFixed(0) : 0; return [`NT$ ${value.toLocaleString()} (${percent}%)`, "金額"]; }} /> value.length > 6 ? value.slice(0, 6) + '…' : value} />
{/* 商品進貨排行 */}

商品進貨排行 Top 10

依據商品分組,加總總進貨量與金額,並依金額高低反向排序出前 10 名。

{productRanking.map((p, idx) => ( ))}
排名 商品名稱 商品編號 進貨數量 進貨金額
{idx + 1} {p.product_name} {p.product_code} {Number(p.total_quantity).toLocaleString()} NT$ {Number(p.total_amount).toLocaleString()}
{productRanking.length === 0 && (
無符合條件的資料
)}
); } // ===== Tab 3: 價格趨勢 ===== function PriceTab({ priceTrend, vendorComparison, }: { priceTrend: PriceTrendItem[]; vendorComparison: VendorComparison[]; }) { if (priceTrend.length === 0 && vendorComparison.length === 0) { return ; } // 將 priceTrend 轉為折線圖資料(每個月為 X 軸,每個商品為一條線) const productNames = [...new Set(priceTrend.map((p) => p.product_name))]; const months = [...new Set(priceTrend.map((p) => p.month))].sort(); const chartData = months.map((month) => { const row: Record = { month }; productNames.forEach((name) => { const item = priceTrend.find((p) => p.month === month && p.product_name === name); row[name] = item ? item.avg_price : 0; }); return row; }); // 將 vendorComparison 按商品分組 const comparisonGrouped = vendorComparison.reduce>((acc, item) => { if (!acc[item.product_name]) acc[item.product_name] = []; acc[item.product_name].push(item); return acc; }, {}); return (
{/* 商品單價趨勢折線圖 */} {chartData.length > 0 && (

商品月均單價趨勢(Top 10 商品)

先從期間內找出「進貨總金額最高的前 10 項商品」作為追蹤目標,計算它們每月的平均進貨單價繪製成折線圖。

`$${v}`} tick={{ fontSize: 12 }} /> [ `NT$ ${value.toLocaleString()}`, name ]} /> {productNames.slice(0, 8).map((name, idx) => ( ))}
)} {/* 跨廠商比價表格 */} {Object.keys(comparisonGrouped).length > 0 && (

跨廠商比價分析

以進貨金額最高的前 10項商品為基準,列出各廠商過去期間的平均、最高、最低進貨單價以供比價,最低價將會標記「最優」。

{Object.entries(comparisonGrouped).map(([productName, items]) => (
{productName}
{items.map((item, idx) => { const isLowest = item.avg_price === Math.min(...items.map((i) => i.avg_price)); return ( ); })}
廠商 平均單價 最低價 最高價 交易次數
{item.vendor_name} {isLowest && ( 最優 )} NT$ {item.avg_price.toLocaleString()} NT$ {item.min_price.toLocaleString()} NT$ {item.max_price.toLocaleString()} {item.purchase_count}
))}
)}
); } // ===== 空狀態 ===== function EmptyState({ message }: { message: string }) { return (

{message}

); }