import React, { useState, useEffect } from 'react'; import { ScatterChart, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Label } from 'recharts'; import { FlaskConical, Calendar, Clock, Target, AlertTriangle, Copy, Check, TrendingUp, Info, HelpCircle } from 'lucide-react'; import { Card } from './common/Card'; import { labApi } from '../services/api'; import type { LabKPI, ScatterPoint, OrphanSample } from '../types'; export const LabView: React.FC = () => { const [kpi, setKpi] = useState({ converted_count: 0, avg_velocity: 0, conversion_rate: 0, orphan_count: 0 }); const [scatterData, setScatterData] = useState([]); const [orphans, setOrphans] = useState([]); const [conversions, setConversions] = useState([]); const [loading, setLoading] = useState(true); const [dateRange, setDateRange] = useState<'all' | '12m' | '6m' | '3m'>('all'); const [useLogScale, setUseLogScale] = useState(false); const [copiedId, setCopiedId] = useState(null); const [viewMode, setViewMode] = useState<'orphans' | 'conversions'>('orphans'); useEffect(() => { loadLabData(); }, [dateRange]); const loadLabData = async () => { setLoading(true); try { let start_date = ''; const now = new Date(); if (dateRange === '12m') { start_date = new Date(now.setFullYear(now.getFullYear() - 1)).toISOString().split('T')[0]; } else if (dateRange === '6m') { start_date = new Date(now.setMonth(now.getMonth() - 6)).toISOString().split('T')[0]; } else if (dateRange === '3m') { start_date = new Date(now.setMonth(now.getMonth() - 3)).toISOString().split('T')[0]; } const params = start_date ? { start_date } : {}; const [kpiData, scatterRes, orphanRes, conversionRes] = await Promise.all([ labApi.getKPI(params), labApi.getScatter(params), labApi.getOrphans(), labApi.getConversions() ]); setKpi(kpiData); setScatterData(scatterRes); setOrphans(orphanRes); setConversions(conversionRes); } catch (error) { console.error('Error loading lab data:', error); } finally { setLoading(false); } }; const handleCopy = (orphan: OrphanSample, index: number) => { const text = `Customer: ${orphan.customer}\nPart No: ${orphan.pn}\nSent Date: ${orphan.date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')}\nDays Ago: ${orphan.days_since_sent}`; navigator.clipboard.writeText(text); setCopiedId(index); setTimeout(() => setCopiedId(null), 2000); }; const [selectedGroup, setSelectedGroup] = useState(null); // Calculate grouping info const groupInfo = React.useMemo(() => { const counts: Record = {}; orphans.forEach(o => { const key = `${o.customer}|${o.pn}`; counts[key] = (counts[key] || 0) + 1; }); return counts; }, [orphans]); if (loading) { return (
); } return (

送樣成效分析戰情室 (Sample Conversion Lab)

焦點分析:樣品投資報酬率 (ROI) 與 轉換速度 | 邏輯:ERP Code + PN 直接比對

{(['all', '12m', '6m', '3m'] as const).map((r) => ( ))}
{/* KPI Cards */}
setViewMode('conversions')} className={`p-6 border-b-4 border-b-blue-500 bg-gradient-to-br from-white to-blue-50/30 cursor-pointer transition-all hover:shadow-md ${viewMode === 'conversions' ? 'ring-2 ring-blue-500 ring-offset-2' : ''}`} >
成功收單總數
{kpi.converted_count} 筆
Converted Samples
平均轉換速度
{kpi.avg_velocity} 天
Conversion Velocity
整體轉換倍率 (ROI)
{kpi.conversion_rate}%
Sample to Order Ratio
setViewMode('orphans')} className={`p-6 border-b-4 border-b-rose-500 bg-gradient-to-br from-white to-rose-50/30 cursor-pointer transition-all hover:shadow-md ${viewMode === 'orphans' ? 'ring-2 ring-rose-500 ring-offset-2' : ''}`} >
無訂單樣品總數
{kpi.orphan_count} 筆
Wait-time > 90 Days
{/* Scatter Matrix */}

送樣量與訂單量分佈矩陣 (Sample ROI Matrix)

{ if (data && data.activePayload && data.activePayload[0]) { const point = data.activePayload[0].payload as ScatterPoint; if (point.order_qty > 0) { setViewMode('conversions'); } else { setViewMode('orphans'); } } }} > { if (active && payload && payload.length) { const data = payload[0].payload as ScatterPoint; return (

{data.customer}

{data.pn}

送樣量: {data.sample_qty.toLocaleString()}

訂單量: {data.order_qty.toLocaleString()}

{data.order_qty > data.sample_qty ? '✨ 高效轉換 (High ROI)' : data.order_qty > 0 ? '穩定轉換' : '尚無訂單 (No-Order)'}

); } return null; }} />
案件資料 (Customer/PN Group)
{/* Insight Card */}

戰情室洞察 (Lab Insights)

高效轉換客戶

識別散佈圖中「左上角」點位,代表投入少量樣品即獲得大量訂單。

風險警示

右下角點位代表送樣頻繁但轉換效率低,需檢視應用場景或產品適配度。

"本模組直接比對 ERP 編號,確保不因專案名稱模糊而漏失任何實際營收數據。"

{/* Dynamic Table Section */}

{viewMode === 'conversions' ? ( <> Successful Conversions List ) : ( <> No-Order Sample Alert Table )}

{viewMode === 'orphans' && (
重複送樣 (Repeated)
)}
{viewMode === 'conversions' ? `共 ${conversions.length} 筆成功轉換` : `共 ${orphans.length} 筆待追蹤案件`}
{viewMode === 'conversions' ? ( <> ) : ( <> )} {viewMode === 'conversions' ? ( conversions.map((row, i) => ( )) ) : ( orphans.map((row, i) => { const groupKey = `${row.customer}|${row.pn}`; const isRepeated = (groupInfo[groupKey] || 0) > 1; const isSelected = selectedGroup === groupKey; return ( setSelectedGroup(isSelected ? null : groupKey)} className={` transition-all cursor-pointer border-l-4 ${isSelected ? 'bg-indigo-50 border-l-indigo-500 shadow-inner' : 'hover:bg-slate-50 border-l-transparent'} `} > ) }) )} {viewMode === 'orphans' && orphans.length === 0 && ( )} {viewMode === 'conversions' && conversions.length === 0 && ( )}
客戶名稱 產品料號 (Part No)送樣資訊 (Date/Qty) 首張訂單 (Date/Qty) 轉換天數送樣日期 滯留天數 狀態 操作
{row.customer} {row.pn}
{row.sample_date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')} {row.sample_qty} pcs
{row.order_date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')} {row.order_qty.toLocaleString()} pcs
{row.days_to_convert} 天
{row.customer} {isRepeated && ( x{groupInfo[groupKey]} )}
{row.pn} {row.date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')} 180 ? 'text-rose-600' : 'text-amber-600'}`}> {row.days_since_sent} 天 180 ? 'bg-rose-100 text-rose-700' : 'bg-amber-100 text-amber-700' }`}> {row.days_since_sent > 180 ? '呆滯庫存 (Dead Stock)' : '需採取行動'}
目前沒有無需跟進的無訂單樣品,所有送樣皆在有效時限內或已轉化。
目前沒有成功轉換的紀錄。
); };