654 lines
33 KiB
Plaintext
654 lines
33 KiB
Plaintext
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Upload, FileText, Database, CheckCircle, XCircle,
|
||
AlertTriangle, BarChart2, PieChart, Activity,
|
||
ArrowRight, Search, Filter, Download, RefreshCw, FileSpreadsheet,
|
||
Info
|
||
} from 'lucide-react';
|
||
import {
|
||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer,
|
||
PieChart as RePieChart, Pie, Cell
|
||
} from 'recharts';
|
||
|
||
// --- 真實模擬資料 (源自使用者上傳檔案) ---
|
||
|
||
// 1. DIT Report (模擬解析後的結構)
|
||
// 來源: DIT report (by Focus Item)_app_樣本.xlsx
|
||
const PARSED_DIT_DATA = [
|
||
{ id: 'OP0000012868', customer: '光寶科技股份有限公司', pn: 'GBU610', eau: 20000, stage: 'Design Win', date: '2025-02-04' },
|
||
{ id: 'OP0000012869', customer: 'LITEON (PMS)', pn: 'PMSM808LL_R2', eau: 55000, stage: 'Design-In', date: '2025-07-28' },
|
||
{ id: 'OP0000012870', customer: '台達電子', pn: 'PEC3205ES-AU', eau: 100000, stage: 'Sample Provide', date: '2025-08-15' }, // 故意用簡稱測試模糊比對
|
||
{ id: 'OP0000012871', customer: '合美企業', pn: 'PJT7828', eau: 5000, stage: 'Negotiation', date: '2025-08-10' },
|
||
{ id: 'OP0000012872', customer: '申浦电子', pn: 'ER1604FCT', eau: 30000, stage: 'Design Win', date: '2025-07-30' },
|
||
];
|
||
|
||
// 2. Sample Data (模擬解析後的結構)
|
||
// 來源: 樣品申請紀錄_樣本.xlsx
|
||
const PARSED_SAMPLE_DATA = [
|
||
{ id: 'S202512490-02', order_no: 'S202512490', customer: '台達電子工業股份有限公司', pn: 'PEC3205ES-AU', qty: 20, date: '2025-12-19' },
|
||
{ id: 'S202512490-03', order_no: 'S202512490', customer: '台達電子工業股份有限公司', pn: 'PDZ9.1B-AU', qty: 20, date: '2025-12-19' },
|
||
];
|
||
|
||
// 3. Order Data (模擬解析後的結構)
|
||
// 來源: 訂單樣本_20251217.xlsx
|
||
const PARSED_ORDER_DATA = [
|
||
{ id: '1125025312-1', order_no: '1125025312', customer: '合美企業有限公司', pn: 'PJT7828', qty: 1200, status: 'Shipped', amount: 10800 },
|
||
{ id: '1125062105-1', order_no: '1125062105', customer: '申浦电子', pn: 'ER1604FCT', qty: 800, status: 'Backlog', amount: 3200 },
|
||
];
|
||
|
||
// --- 模糊比對模擬結果 ---
|
||
const SIMULATED_REVIEWS = [
|
||
{
|
||
id: 101,
|
||
score: 88,
|
||
dit: { id: 'OP0000012870', cust: '台達電子', pn: 'PEC3205ES-AU' },
|
||
match_target: { id: 'S202512490', cust: '台達電子工業股份有限公司', pn: 'PEC3205ES-AU', type: 'SAMPLE' },
|
||
type: 'SAMPLE',
|
||
reason: 'Customer Name Partial Match (88%)'
|
||
},
|
||
{
|
||
id: 102,
|
||
score: 92,
|
||
dit: { id: 'OP0000012871', cust: '合美企業', pn: 'PJT7828' },
|
||
match_target: { id: '1125025312', cust: '合美企業有限公司', pn: 'PJT7828', type: 'ORDER' },
|
||
type: 'ORDER',
|
||
reason: 'Corporate Suffix Mismatch'
|
||
}
|
||
];
|
||
|
||
// --- 輔助元件 ---
|
||
|
||
const Card = ({ children, className = "" }) => (
|
||
<div className={`bg-white rounded-lg border border-slate-200 shadow-sm ${className}`}>
|
||
{children}
|
||
</div>
|
||
);
|
||
|
||
const Badge = ({ children, type = "neutral" }) => {
|
||
const styles = {
|
||
neutral: "bg-slate-100 text-slate-600",
|
||
success: "bg-emerald-100 text-emerald-700",
|
||
warning: "bg-amber-100 text-amber-700",
|
||
danger: "bg-rose-100 text-rose-700",
|
||
info: "bg-blue-100 text-blue-700"
|
||
};
|
||
return (
|
||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${styles[type]}`}>
|
||
{children}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
// --- 主應用程式 ---
|
||
|
||
export default function App() {
|
||
const [activeTab, setActiveTab] = useState('import');
|
||
const [isProcessing, setIsProcessing] = useState(false);
|
||
const [processingStep, setProcessingStep] = useState('');
|
||
const [filesLoaded, setFilesLoaded] = useState(false);
|
||
|
||
// 狀態管理
|
||
const [reviews, setReviews] = useState([]);
|
||
const [processedCount, setProcessedCount] = useState(0);
|
||
|
||
// Dashboard 數據
|
||
const [dashboardData, setDashboardData] = useState({
|
||
totalDit: 5,
|
||
matchedSample: 0,
|
||
matchedOrder: 1, // '申浦电子' 是 Exact Match
|
||
conversionRate: 20.0
|
||
});
|
||
|
||
// 模擬載入使用者的真實檔案
|
||
const loadUserFiles = () => {
|
||
setFilesLoaded(true);
|
||
};
|
||
|
||
// 模擬 ETL 執行
|
||
const runEtl = () => {
|
||
if (!filesLoaded) {
|
||
alert("請先載入檔案!");
|
||
return;
|
||
}
|
||
setIsProcessing(true);
|
||
|
||
// 模擬後端處理步驟
|
||
const steps = [
|
||
"正在讀取 DIT Report... 偵測到表頭在第 16 行",
|
||
"正在讀取 樣品紀錄... 偵測到表頭在第 9 行",
|
||
"正在讀取 訂單明細... 編碼識別為 CP950",
|
||
"執行資料標準化 (Normalization)...",
|
||
"執行模糊比對 (Fuzzy Matching)...",
|
||
"運算完成!"
|
||
];
|
||
|
||
let stepIndex = 0;
|
||
const interval = setInterval(() => {
|
||
if (stepIndex >= steps.length) {
|
||
clearInterval(interval);
|
||
setIsProcessing(false);
|
||
setReviews(SIMULATED_REVIEWS); // 載入模擬的比對結果
|
||
setActiveTab('review');
|
||
} else {
|
||
setProcessingStep(steps[stepIndex]);
|
||
stepIndex++;
|
||
}
|
||
}, 800);
|
||
};
|
||
|
||
// 處理審核動作
|
||
const handleReviewAction = (id, action) => {
|
||
const reviewItem = reviews.find(r => r.id === id);
|
||
setReviews(prev => prev.filter(r => r.id !== id));
|
||
|
||
if (action === 'accept') {
|
||
setProcessedCount(prev => prev + 1);
|
||
|
||
// 更新 Dashboard 數據 (模擬)
|
||
if (reviewItem.type === 'ORDER') {
|
||
setDashboardData(prev => ({
|
||
...prev,
|
||
matchedOrder: prev.matchedOrder + 1,
|
||
conversionRate: ((prev.matchedOrder + 1) / prev.totalDit * 100).toFixed(1)
|
||
}));
|
||
} else if (reviewItem.type === 'SAMPLE') {
|
||
setDashboardData(prev => ({
|
||
...prev,
|
||
matchedSample: prev.matchedSample + 1
|
||
}));
|
||
}
|
||
}
|
||
};
|
||
|
||
// 漏斗圖資料
|
||
const funnelData = [
|
||
{ name: 'DIT 案件', value: dashboardData.totalDit, fill: '#6366f1' },
|
||
{ name: '成功送樣', value: dashboardData.matchedSample, fill: '#8b5cf6' },
|
||
{ name: '取得訂單', value: dashboardData.matchedOrder, fill: '#10b981' },
|
||
];
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 font-sans text-slate-800">
|
||
{/* Top Navigation */}
|
||
<header className="bg-white border-b border-slate-200 sticky top-0 z-10">
|
||
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center text-white font-bold">
|
||
S
|
||
</div>
|
||
<div>
|
||
<span className="font-bold text-lg text-slate-800 block leading-tight">SalesPipeline</span>
|
||
<span className="text-[10px] text-slate-500 font-medium">On-Premise Simulator</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg">
|
||
{[
|
||
{ id: 'import', icon: Database, label: '資料匯入' },
|
||
{ id: 'review', icon: CheckCircle, label: '比對審核', badge: reviews.length },
|
||
{ id: 'dashboard', icon: BarChart2, label: '分析儀表板' }
|
||
].map(tab => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`
|
||
flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-all
|
||
${activeTab === tab.id
|
||
? 'bg-white text-indigo-600 shadow-sm'
|
||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-200/50'}
|
||
`}
|
||
>
|
||
<tab.icon size={16} />
|
||
{tab.label}
|
||
{tab.badge > 0 && (
|
||
<span className="bg-rose-500 text-white text-[10px] px-1.5 py-0.5 rounded-full">
|
||
{tab.badge}
|
||
</span>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm text-emerald-600 bg-emerald-50 px-3 py-1 rounded-full border border-emerald-100">
|
||
<div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div>
|
||
系統運作中
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||
|
||
{/* --- View 1: Import --- */}
|
||
{activeTab === 'import' && (
|
||
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||
<div className="flex justify-between items-end">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-800">原始資料匯入中心</h1>
|
||
<p className="text-slate-500 mt-1">系統將自動偵測 Excel/CSV 檔頭位置並進行智慧欄位對應。</p>
|
||
</div>
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={loadUserFiles}
|
||
disabled={filesLoaded || isProcessing}
|
||
className={`flex items-center gap-2 px-4 py-3 rounded-lg font-medium border transition-all ${filesLoaded ? 'bg-slate-100 text-slate-400 border-slate-200' : 'bg-white text-slate-700 border-slate-300 hover:bg-slate-50'}`}
|
||
>
|
||
<FileSpreadsheet size={18} />
|
||
{filesLoaded ? '檔案已就緒' : '載入範本檔案'}
|
||
</button>
|
||
<button
|
||
onClick={runEtl}
|
||
disabled={isProcessing || !filesLoaded}
|
||
className={`flex items-center gap-2 px-6 py-3 rounded-lg text-white font-bold shadow-lg transition-all ${isProcessing || !filesLoaded ? 'bg-slate-400 cursor-not-allowed shadow-none' : 'bg-indigo-600 hover:bg-indigo-700 hover:scale-105 shadow-indigo-200'}`}
|
||
>
|
||
{isProcessing ? (
|
||
<>
|
||
<RefreshCw size={18} className="animate-spin" />
|
||
運算中...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Activity size={18} />
|
||
開始 ETL 運算
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Progress Log */}
|
||
{isProcessing && (
|
||
<div className="bg-slate-800 text-green-400 font-mono p-4 rounded-lg text-sm shadow-inner">
|
||
<p className="flex items-center gap-2">
|
||
<span className="animate-pulse">▶</span> {processingStep}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
{[
|
||
{ type: 'dit', title: '1. DIT Report', file: 'DIT report_app_樣本.xlsx', desc: '含 Metadata (前16行)', count: '5 筆 (測試)' },
|
||
{ type: 'sample', title: '2. 樣品紀錄', file: '樣品申請紀錄_樣本.xlsx', desc: '含檔頭 (前9行)', count: '2 筆 (測試)' },
|
||
{ type: 'order', title: '3. 訂單明細', file: '訂單樣本_20251217.xlsx', desc: '標準格式', count: '2 筆 (測試)' },
|
||
].map(file => (
|
||
<Card key={file.type} className={`p-6 border-dashed border-2 transition-colors ${filesLoaded ? 'border-emerald-300 bg-emerald-50/30' : 'border-slate-300'}`}>
|
||
<div className="flex justify-between items-start mb-4">
|
||
<div className={`p-3 rounded-lg ${filesLoaded ? 'bg-emerald-100 text-emerald-600' : 'bg-slate-100 text-slate-500'}`}>
|
||
{filesLoaded ? <CheckCircle size={24} /> : <FileText size={24} />}
|
||
</div>
|
||
{filesLoaded && <Badge type="success">Ready</Badge>}
|
||
</div>
|
||
<h3 className="text-lg font-bold text-slate-800">{file.title}</h3>
|
||
<p className="text-xs text-slate-500 font-mono mt-1 mb-3 truncate">{filesLoaded ? file.file : "等待上傳..."}</p>
|
||
<p className="text-sm text-slate-600">{file.desc}</p>
|
||
|
||
{filesLoaded && (
|
||
<div className="mt-4 pt-3 border-t border-slate-200/60 flex justify-between text-sm">
|
||
<span className="text-slate-500">預覽筆數</span>
|
||
<span className="font-bold font-mono text-slate-700">{file.count}</span>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
))}
|
||
</div>
|
||
|
||
{/* Data Preview (Mocked from User Files) */}
|
||
{filesLoaded && (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
||
<Card className="overflow-hidden">
|
||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||
<h3 className="font-bold text-slate-700 text-sm flex items-center gap-2">
|
||
<Database size={16} />
|
||
DIT 解析結果 (預覽)
|
||
</h3>
|
||
<Badge type="info">Auto-Skipped Header</Badge>
|
||
</div>
|
||
<table className="w-full text-xs text-left">
|
||
<thead className="bg-white text-slate-500 border-b border-slate-200">
|
||
<tr>
|
||
<th className="px-4 py-2">Customer</th>
|
||
<th className="px-4 py-2">Part No</th>
|
||
<th className="px-4 py-2">Stage</th>
|
||
<th className="px-4 py-2 text-right">EAU</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-100">
|
||
{PARSED_DIT_DATA.map((row, i) => (
|
||
<tr key={i} className="hover:bg-slate-50">
|
||
<td className="px-4 py-2 font-medium text-slate-700">{row.customer}</td>
|
||
<td className="px-4 py-2 font-mono text-slate-500">{row.pn}</td>
|
||
<td className="px-4 py-2 text-slate-600">{row.stage}</td>
|
||
<td className="px-4 py-2 text-right font-mono">{row.eau.toLocaleString()}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</Card>
|
||
|
||
<Card className="overflow-hidden">
|
||
<div className="bg-slate-50 px-4 py-3 border-b border-slate-200 flex justify-between items-center">
|
||
<h3 className="font-bold text-slate-700 text-sm flex items-center gap-2">
|
||
<Database size={16} />
|
||
訂單解析結果 (預覽)
|
||
</h3>
|
||
<Badge type="info">Detected CP950</Badge>
|
||
</div>
|
||
<table className="w-full text-xs text-left">
|
||
<thead className="bg-white text-slate-500 border-b border-slate-200">
|
||
<tr>
|
||
<th className="px-4 py-2">Customer</th>
|
||
<th className="px-4 py-2">Part No</th>
|
||
<th className="px-4 py-2">Status</th>
|
||
<th className="px-4 py-2 text-right">Qty</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-100">
|
||
{PARSED_ORDER_DATA.map((row, i) => (
|
||
<tr key={i} className="hover:bg-slate-50">
|
||
<td className="px-4 py-2 font-medium text-slate-700">{row.customer}</td>
|
||
<td className="px-4 py-2 font-mono text-slate-500">{row.pn}</td>
|
||
<td className="px-4 py-2">
|
||
<span className={`px-1.5 py-0.5 rounded text-[10px] ${row.status === 'Shipped' ? 'bg-green-100 text-green-700' : 'bg-pink-100 text-pink-700'}`}>
|
||
{row.status}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-2 text-right font-mono">{row.qty.toLocaleString()}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* --- View 2: Review --- */}
|
||
{activeTab === 'review' && (
|
||
<div className="space-y-6 animate-in fade-in zoom-in-95 duration-300">
|
||
<div className="flex justify-between items-center">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-800 flex items-center gap-2">
|
||
模糊比對審核工作台
|
||
<span className="text-sm bg-indigo-100 text-indigo-700 px-2 py-1 rounded-full font-normal">待審核: {reviews.length}</span>
|
||
</h1>
|
||
<p className="text-slate-500 mt-1">系統發現以下案件名稱相似,請人工確認關聯性。</p>
|
||
</div>
|
||
</div>
|
||
|
||
{reviews.length === 0 ? (
|
||
<div className="flex flex-col items-center justify-center py-20 bg-white rounded-lg border border-dashed border-slate-300">
|
||
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center mb-4">
|
||
<CheckCircle size={32} />
|
||
</div>
|
||
<h3 className="text-xl font-bold text-slate-800">所有案件已審核完畢!</h3>
|
||
<p className="text-slate-500 mt-2">您的資料比對已完成,請查看分析儀表板。</p>
|
||
<button
|
||
onClick={() => setActiveTab('dashboard')}
|
||
className="mt-6 px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
||
>
|
||
前往儀表板
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="grid gap-4">
|
||
{reviews.map(item => (
|
||
<Card key={item.id} className="p-0 overflow-hidden group hover:shadow-md transition-all border-l-4 border-l-amber-400">
|
||
<div className="flex flex-col md:flex-row">
|
||
{/* Left: DIT */}
|
||
<div className="flex-1 p-5 border-b md:border-b-0 md:border-r border-slate-100 bg-slate-50/50">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Badge type="info">DIT (設計導入)</Badge>
|
||
<span className="text-xs text-slate-400">OP編號: {item.dit.id}</span>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<div className="text-xs text-slate-400 uppercase">Customer Name</div>
|
||
<div className="font-bold text-slate-800 text-lg">{item.dit.cust}</div>
|
||
<div className="text-xs text-slate-400 uppercase mt-2">Part Number</div>
|
||
<div className="font-mono text-slate-700 bg-white border border-slate-200 px-2 py-1 rounded inline-block text-sm">
|
||
{item.dit.pn}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Middle: Score & Reason */}
|
||
<div className="w-full md:w-48 p-4 flex flex-col items-center justify-center bg-white z-10">
|
||
<div className="text-center">
|
||
<div className="text-2xl font-bold text-amber-500 mb-1">{item.score}%</div>
|
||
<div className="text-[10px] font-medium text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full mb-2">
|
||
相似度
|
||
</div>
|
||
<div className="text-xs text-slate-400 text-center px-2 leading-tight">
|
||
{item.reason}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right: Target (Sample/Order) */}
|
||
<div className="flex-1 p-5 border-t md:border-t-0 md:border-l border-slate-100 bg-indigo-50/30">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<Badge type={item.type === 'ORDER' ? 'warning' : 'success'}>
|
||
{item.type === 'ORDER' ? 'Order (訂單)' : 'Sample (樣品)'}
|
||
</Badge>
|
||
<span className="text-xs text-slate-400">來源單號: {item.match_target.id}</span>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<div className="text-xs text-slate-400 uppercase">Customer Name</div>
|
||
<div className="font-bold text-slate-800 text-lg">{item.match_target.cust}</div>
|
||
<div className="text-xs text-slate-400 uppercase mt-2">Part Number</div>
|
||
<div className="font-mono text-slate-700 bg-white border border-slate-200 px-2 py-1 rounded inline-block text-sm">
|
||
{item.match_target.pn}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="w-full md:w-40 p-4 bg-slate-50 flex flex-row md:flex-col gap-3 justify-center items-center border-t md:border-t-0 md:border-l border-slate-200">
|
||
<button
|
||
onClick={() => handleReviewAction(item.id, 'accept')}
|
||
className="flex-1 md:flex-none w-full py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors shadow-sm"
|
||
>
|
||
<CheckCircle size={16} /> 確認關聯
|
||
</button>
|
||
<button
|
||
onClick={() => handleReviewAction(item.id, 'reject')}
|
||
className="flex-1 md:flex-none w-full py-2 bg-white border border-slate-300 text-slate-600 hover:bg-slate-50 rounded-lg text-sm font-medium flex items-center justify-center gap-2 transition-colors"
|
||
>
|
||
<XCircle size={16} /> 駁回
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* --- View 3: Dashboard --- */}
|
||
{activeTab === 'dashboard' && (
|
||
<div className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-500">
|
||
<div className="flex justify-between items-center">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-800">業務轉換率戰情室</h1>
|
||
<p className="text-slate-500 mt-1">數據來源:DIT Report, 樣品紀錄, 訂單明細 (2025-12-17)</p>
|
||
</div>
|
||
<button className="flex items-center gap-2 px-4 py-2 border border-slate-300 rounded-lg text-slate-600 hover:bg-slate-50 text-sm font-medium">
|
||
<Download size={16} />
|
||
匯出報表
|
||
</button>
|
||
</div>
|
||
|
||
{/* KPI Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||
<Card className="p-5 border-l-4 border-l-indigo-500">
|
||
<div className="text-sm text-slate-500 mb-1">DIT 總案件數</div>
|
||
<div className="text-3xl font-bold text-slate-800">{dashboardData.totalDit}</div>
|
||
<div className="text-xs text-slate-400 mt-2">Raw Data Count</div>
|
||
</Card>
|
||
<Card className="p-5 border-l-4 border-l-purple-500">
|
||
<div className="text-sm text-slate-500 mb-1">成功送樣數</div>
|
||
<div className="text-3xl font-bold text-slate-800">{dashboardData.matchedSample}</div>
|
||
<div className="text-xs text-emerald-600 mt-2">
|
||
{processedCount > 0 ? `(含人工審核 +1)` : ''}
|
||
</div>
|
||
</Card>
|
||
<Card className="p-5 border-l-4 border-l-pink-500">
|
||
<div className="text-sm text-slate-500 mb-1">取得訂單 (Backlog)</div>
|
||
<div className="text-3xl font-bold text-slate-800">{dashboardData.matchedOrder}</div>
|
||
<div className="text-xs text-emerald-600 mt-2">
|
||
Match Rate: {dashboardData.conversionRate}%
|
||
</div>
|
||
</Card>
|
||
<Card className="p-5 border-l-4 border-l-emerald-500">
|
||
<div className="text-sm text-slate-500 mb-1">預估營收 (Revenue)</div>
|
||
<div className="text-3xl font-bold text-emerald-600">$14,000</div>
|
||
<div className="text-xs text-slate-400 mt-2">Based on matched orders</div>
|
||
</Card>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
{/* Funnel Chart */}
|
||
<Card className="lg:col-span-2 p-6">
|
||
<h3 className="font-bold text-slate-700 mb-6 flex items-center gap-2">
|
||
<Filter size={18} />
|
||
DIT 轉換漏斗 (Funnel Analysis)
|
||
</h3>
|
||
<div className="h-64 w-full">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart
|
||
layout="vertical"
|
||
data={funnelData}
|
||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||
>
|
||
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
|
||
<XAxis type="number" />
|
||
<YAxis dataKey="name" type="category" width={100} />
|
||
<RechartsTooltip cursor={{fill: '#f1f5f9'}} />
|
||
<Bar dataKey="value" barSize={30} radius={[0, 4, 4, 0]} label={{ position: 'right' }}>
|
||
{funnelData.map((entry, index) => (
|
||
<Cell key={`cell-${index}`} fill={entry.fill} />
|
||
))}
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* Status Pie Chart */}
|
||
<Card className="p-6">
|
||
<h3 className="font-bold text-slate-700 mb-6 flex items-center gap-2">
|
||
<PieChart size={18} />
|
||
訂單狀態佔比
|
||
</h3>
|
||
<div className="h-64 w-full flex justify-center">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<RePieChart>
|
||
<Pie
|
||
data={[
|
||
{ name: 'Backlog', value: 800 },
|
||
{ name: 'Shipped', value: 1200 },
|
||
]}
|
||
cx="50%"
|
||
cy="50%"
|
||
innerRadius={60}
|
||
outerRadius={80}
|
||
paddingAngle={5}
|
||
dataKey="value"
|
||
>
|
||
<Cell fill="#ec4899" />
|
||
<Cell fill="#10b981" />
|
||
</Pie>
|
||
<RechartsTooltip />
|
||
<Legend verticalAlign="bottom" height={36}/>
|
||
</RePieChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Attribution Table Preview */}
|
||
<Card className="overflow-hidden">
|
||
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200 flex justify-between items-center">
|
||
<h3 className="font-bold text-slate-700 flex items-center gap-2">
|
||
<Activity size={18} />
|
||
DIT 歸因明細表 (LIFO 邏輯)
|
||
</h3>
|
||
<div className="text-xs text-slate-500">
|
||
<span className="flex items-center gap-1">
|
||
<Info size={12} />
|
||
Hover to see order details
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<table className="w-full text-sm text-left">
|
||
<thead className="bg-white text-slate-500 border-b border-slate-200">
|
||
<tr>
|
||
<th className="px-6 py-3 w-32">OP編號</th>
|
||
<th className="px-6 py-3">Customer</th>
|
||
<th className="px-6 py-3">Part No.</th>
|
||
<th className="px-6 py-3 text-right">EAU</th>
|
||
<th className="px-6 py-3 text-center">Sample Order</th>
|
||
<th className="px-6 py-3 text-center">Order Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-100">
|
||
{PARSED_DIT_DATA.map((row, i) => {
|
||
// 簡單模擬關聯邏輯 (Find exact matches or partial matches)
|
||
const matchedOrder = PARSED_ORDER_DATA.find(o => o.customer.includes(row.customer) || row.customer.includes(o.customer.substring(0, 2)));
|
||
const matchedSample = PARSED_SAMPLE_DATA.find(s => s.customer.includes(row.customer) || row.customer.includes(s.customer.substring(0, 2)));
|
||
|
||
// 人工審核修正 (台達電子 & 合美)
|
||
const isReviewedSample = (row.customer === '台達電子' && processedCount > 0) ? {order_no: 'S202512490'} : null;
|
||
const isReviewedOrder = (row.customer === '合美企業' && processedCount > 1) ? {order_no: '1125025312'} : null;
|
||
|
||
// 決定顯示內容
|
||
const finalSample = matchedSample || isReviewedSample;
|
||
const finalOrder = matchedOrder || isReviewedOrder;
|
||
|
||
return (
|
||
<tr key={i} className="hover:bg-slate-50 group transition-colors">
|
||
<td className="px-6 py-3 font-mono text-xs text-slate-500 font-bold">
|
||
{row.id}
|
||
</td>
|
||
<td className="px-6 py-3 font-medium text-slate-800">
|
||
{row.customer}
|
||
<div className="text-[10px] text-slate-400 font-light">{row.stage}</div>
|
||
</td>
|
||
<td className="px-6 py-3 font-mono text-slate-600 text-xs">{row.pn}</td>
|
||
<td className="px-6 py-3 text-right font-mono text-slate-600">{row.eau.toLocaleString()}</td>
|
||
|
||
{/* Sample Column */}
|
||
<td className="px-6 py-3 text-center">
|
||
{finalSample ? (
|
||
<div className="inline-flex items-center gap-1 px-2 py-1 bg-purple-50 text-purple-700 rounded-md text-xs font-mono border border-purple-100 cursor-help" title={`送樣單號: ${finalSample.order_no}`}>
|
||
<CheckCircle size={12} />
|
||
{finalSample.order_no}
|
||
</div>
|
||
) : (
|
||
<span className="text-slate-300">-</span>
|
||
)}
|
||
</td>
|
||
|
||
{/* Order Column */}
|
||
<td className="px-6 py-3 text-center">
|
||
{finalOrder ? (
|
||
<div className="inline-flex items-center gap-1 px-2 py-1 bg-emerald-50 text-emerald-700 rounded-md text-xs font-mono border border-emerald-100 cursor-help" title={`訂單單號: ${finalOrder.order_no}`}>
|
||
<CheckCircle size={12} />
|
||
{finalOrder.order_no}
|
||
</div>
|
||
) : (
|
||
<span className="text-slate-300">-</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</Card>
|
||
|
||
</div>
|
||
)}
|
||
|
||
</main>
|
||
</div>
|
||
);
|
||
} |