20160116
This commit is contained in:
@@ -14,13 +14,14 @@ export const DashboardView: React.FC = () => {
|
||||
sample_rate: 0,
|
||||
hit_rate: 0,
|
||||
fulfillment_rate: 0,
|
||||
orphan_sample_rate: 0,
|
||||
no_order_sample_rate: 0,
|
||||
total_revenue: 0,
|
||||
});
|
||||
const [funnelData, setFunnelData] = useState<FunnelData[]>([]);
|
||||
const [attribution, setAttribution] = useState<AttributionRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filterType, setFilterType] = useState<'all' | 'sample' | 'order'>('all');
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
@@ -44,11 +45,23 @@ export const DashboardView: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleExport = async (format: 'xlsx' | 'pdf') => {
|
||||
if (isExporting) return;
|
||||
setIsExporting(true);
|
||||
console.log(`Starting export for ${format}`);
|
||||
// alert(`Starting export for ${format}...`); // Debugging
|
||||
try {
|
||||
const blob = format === 'xlsx'
|
||||
const response = format === 'xlsx'
|
||||
? await reportApi.exportExcel()
|
||||
: await reportApi.exportPdf();
|
||||
|
||||
console.log('Response received', response);
|
||||
|
||||
const blob = response; // response is already a blob from api.ts
|
||||
if (blob.size === 0) {
|
||||
alert('Export failed: Received empty file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -57,9 +70,12 @@ export const DashboardView: React.FC = () => {
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
console.log('Download triggered');
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
alert('匯出失敗,請稍後再試');
|
||||
alert(`匯出失敗: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,10 +104,18 @@ export const DashboardView: React.FC = () => {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleExport('xlsx')}
|
||||
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"
|
||||
disabled={isExporting}
|
||||
className={`flex items-center gap-2 px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium transition-all ${isExporting
|
||||
? 'bg-slate-100 text-slate-400 cursor-not-allowed'
|
||||
: 'text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<Download size={16} />
|
||||
匯出報表
|
||||
{isExporting ? (
|
||||
<div className="w-4 h-4 border-2 border-slate-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
) : (
|
||||
<Download size={16} />
|
||||
)}
|
||||
{isExporting ? '匯出中...' : '匯出報表'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -118,9 +142,9 @@ export const DashboardView: React.FC = () => {
|
||||
<div className="text-[10px] text-amber-600 mt-1">Fulfillment (LIFO)</div>
|
||||
</Card>
|
||||
<Card className="p-4 border-l-4 border-l-rose-500">
|
||||
<div className="text-xs text-slate-500 mb-1">無效送樣率</div>
|
||||
<div className="text-2xl font-bold text-rose-600">{kpi.orphan_sample_rate}%</div>
|
||||
<div className="text-[10px] text-rose-400 mt-1">Orphan Sample</div>
|
||||
<div className="text-xs text-slate-500 mb-1">無訂單樣品率</div>
|
||||
<div className="text-2xl font-bold text-rose-600">{kpi.no_order_sample_rate}%</div>
|
||||
<div className="text-[10px] text-rose-400 mt-1">No-Order Sample</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -220,7 +244,12 @@ export const DashboardView: React.FC = () => {
|
||||
{filteredAttribution.map((row, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50 group transition-colors">
|
||||
<td className="px-6 py-3">
|
||||
<div className="font-mono text-xs text-slate-500 font-bold">{row.dit.op_id}</div>
|
||||
<div className="font-mono text-xs text-slate-700 font-bold">{row.dit.op_id}</div>
|
||||
{row.dit.op_name && (
|
||||
<div className="text-[10px] text-indigo-800 truncate max-w-[150px] my-0.5" title={row.dit.op_name}>
|
||||
{row.dit.op_name}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-400">{row.dit.date}</div>
|
||||
</td>
|
||||
<td className="px-6 py-3">
|
||||
|
||||
@@ -119,6 +119,35 @@ export const ImportView: React.FC<ImportViewProps> = ({ onEtlComplete }) => {
|
||||
<p className="text-slate-500 mt-1">系統將自動偵測 Excel/CSV 檔頭位置並進行智慧欄位對應。</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (confirm('確定要清除所有已上傳的資料與分析結果嗎?')) {
|
||||
try {
|
||||
await etlApi.clearData();
|
||||
setFiles({
|
||||
dit: { file: null, parsed: null, loading: false },
|
||||
sample: { file: null, parsed: null, loading: false },
|
||||
order: { file: null, parsed: null, loading: false },
|
||||
});
|
||||
// Reset file inputs
|
||||
Object.values(fileInputRefs).forEach(ref => {
|
||||
if (ref.current) ref.current.value = '';
|
||||
});
|
||||
setError(null);
|
||||
setProcessingStep('');
|
||||
// Reload page to refresh other components if needed, or just notify parent
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError('清除資料失敗');
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-3 rounded-lg text-red-600 bg-red-50 hover:bg-red-100 font-bold border border-red-200 transition-all"
|
||||
>
|
||||
<RefreshCw size={18} />
|
||||
清除 Data
|
||||
</button>
|
||||
<button
|
||||
onClick={runEtl}
|
||||
disabled={isProcessing || !allFilesReady}
|
||||
@@ -259,7 +288,7 @@ export const ImportView: React.FC<ImportViewProps> = ({ onEtlComplete }) => {
|
||||
<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>
|
||||
<th className="px-4 py-2 text-right">Qty / Kpcs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
|
||||
@@ -13,16 +13,19 @@ import type { LabKPI, ScatterPoint, OrphanSample } from '../types';
|
||||
|
||||
export const LabView: React.FC = () => {
|
||||
const [kpi, setKpi] = useState<LabKPI>({
|
||||
converted_count: 0,
|
||||
avg_velocity: 0,
|
||||
conversion_rate: 0,
|
||||
orphan_count: 0
|
||||
});
|
||||
const [scatterData, setScatterData] = useState<ScatterPoint[]>([]);
|
||||
const [orphans, setOrphans] = useState<OrphanSample[]>([]);
|
||||
const [conversions, setConversions] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dateRange, setDateRange] = useState<'all' | '12m' | '6m' | '3m'>('all');
|
||||
const [useLogScale, setUseLogScale] = useState(false);
|
||||
const [copiedId, setCopiedId] = useState<number | null>(null);
|
||||
const [viewMode, setViewMode] = useState<'orphans' | 'conversions'>('orphans');
|
||||
|
||||
useEffect(() => {
|
||||
loadLabData();
|
||||
@@ -43,15 +46,17 @@ export const LabView: React.FC = () => {
|
||||
|
||||
const params = start_date ? { start_date } : {};
|
||||
|
||||
const [kpiData, scatterRes, orphanRes] = await Promise.all([
|
||||
const [kpiData, scatterRes, orphanRes, conversionRes] = await Promise.all([
|
||||
labApi.getKPI(params),
|
||||
labApi.getScatter(params),
|
||||
labApi.getOrphans()
|
||||
labApi.getOrphans(),
|
||||
labApi.getConversions()
|
||||
]);
|
||||
|
||||
setKpi(kpiData);
|
||||
setScatterData(scatterRes);
|
||||
setOrphans(orphanRes);
|
||||
setConversions(conversionRes);
|
||||
} catch (error) {
|
||||
console.error('Error loading lab data:', error);
|
||||
} finally {
|
||||
@@ -60,12 +65,24 @@ export const LabView: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleCopy = (orphan: OrphanSample, index: number) => {
|
||||
const text = `Customer: ${orphan.customer}\nPart No: ${orphan.pn}\nSent Date: ${orphan.date}\nDays Ago: ${orphan.days_since_sent}`;
|
||||
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<string | null>(null);
|
||||
|
||||
// Calculate grouping info
|
||||
const groupInfo = React.useMemo(() => {
|
||||
const counts: Record<string, number> = {};
|
||||
orphans.forEach(o => {
|
||||
const key = `${o.customer}|${o.pn}`;
|
||||
counts[key] = (counts[key] || 0) + 1;
|
||||
});
|
||||
return counts;
|
||||
}, [orphans]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
@@ -104,7 +121,26 @@ export const LabView: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card
|
||||
onClick={() => 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' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 font-medium mb-1">成功收單總數</div>
|
||||
<div className="text-3xl font-bold text-slate-800">{kpi.converted_count} 筆</div>
|
||||
<div className="text-xs text-blue-600 mt-2 flex items-center gap-1 font-bold">
|
||||
<Check size={12} />
|
||||
Converted Samples
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-blue-100 text-blue-600 rounded-xl">
|
||||
<TrendingUp size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-b-4 border-b-indigo-500 bg-gradient-to-br from-white to-indigo-50/30">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
@@ -116,7 +152,7 @@ export const LabView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-indigo-100 text-indigo-600 rounded-xl">
|
||||
<TrendingUp size={24} />
|
||||
<Clock size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -137,10 +173,13 @@ export const LabView: React.FC = () => {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="p-6 border-b-4 border-b-rose-500 bg-gradient-to-br from-white to-rose-50/30">
|
||||
<Card
|
||||
onClick={() => 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' : ''}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 font-medium mb-1">孤兒樣品總數</div>
|
||||
<div className="text-sm text-slate-500 font-medium mb-1">無訂單樣品總數</div>
|
||||
<div className="text-3xl font-bold text-rose-600">{kpi.orphan_count} 筆</div>
|
||||
<div className="text-xs text-rose-400 mt-2 flex items-center gap-1 font-bold">
|
||||
<AlertTriangle size={12} />
|
||||
@@ -175,7 +214,19 @@ export const LabView: React.FC = () => {
|
||||
|
||||
<div className="h-[400px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
||||
<ScatterChart
|
||||
margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
|
||||
onClick={(data: any) => {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f1f5f9" />
|
||||
<XAxis
|
||||
type="number"
|
||||
@@ -215,7 +266,7 @@ export const LabView: React.FC = () => {
|
||||
<span className="font-bold text-emerald-600">{data.order_qty.toLocaleString()}</span>
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400 mt-2 italic">
|
||||
{data.order_qty > data.sample_qty ? '✨ 高效轉換 (High ROI)' : data.order_qty > 0 ? '穩定轉換' : '尚無訂單 (Orphan?)'}
|
||||
{data.order_qty > data.sample_qty ? '✨ 高效轉換 (High ROI)' : data.order_qty > 0 ? '穩定轉換' : '尚無訂單 (No-Order)'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,6 +282,7 @@ export const LabView: React.FC = () => {
|
||||
fillOpacity={0.6}
|
||||
stroke="#4338ca"
|
||||
strokeWidth={1}
|
||||
cursor="pointer"
|
||||
/>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -270,61 +322,149 @@ export const LabView: React.FC = () => {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Orphan Samples Table */}
|
||||
{/* Dynamic Table Section */}
|
||||
<Card className="overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
|
||||
<h3 className="font-bold text-slate-700 flex items-center gap-2">
|
||||
<AlertTriangle size={18} className="text-rose-500" />
|
||||
Orphan Alert Table - > 90 Days
|
||||
<div className={`px-6 py-4 border-b flex justify-between items-center ${viewMode === 'conversions' ? 'bg-blue-50 border-blue-200' : 'bg-rose-50 border-rose-200'}`}>
|
||||
<h3 className={`font-bold flex items-center gap-2 ${viewMode === 'conversions' ? 'text-blue-700' : 'text-rose-700'}`}>
|
||||
{viewMode === 'conversions' ? (
|
||||
<>
|
||||
<Check size={18} />
|
||||
Successful Conversions List
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertTriangle size={18} />
|
||||
No-Order Sample Alert Table
|
||||
</>
|
||||
)}
|
||||
</h3>
|
||||
<div className="text-[10px] text-slate-400 font-medium">
|
||||
共 {orphans.length} 筆待追蹤案件
|
||||
<div className="flex items-center gap-4">
|
||||
{viewMode === 'orphans' && (
|
||||
<div className="text-[10px] text-slate-400 flex items-center gap-2">
|
||||
<span className="flex items-center gap-1"><span className="w-2 h-2 bg-indigo-600 rounded-full"></span> 重複送樣 (Repeated)</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[10px] text-slate-400 font-medium">
|
||||
{viewMode === 'conversions' ? `共 ${conversions.length} 筆成功轉換` : `共 ${orphans.length} 筆待追蹤案件`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<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">客戶名稱</th>
|
||||
<th className="px-6 py-3">產品料號 (Part No)</th>
|
||||
<th className="px-6 py-3">送樣日期</th>
|
||||
<th className="px-6 py-3 text-center">滯留天數</th>
|
||||
<th className="px-6 py-3 text-center">狀態</th>
|
||||
<th className="px-6 py-3 text-right">操作</th>
|
||||
{viewMode === 'conversions' ? (
|
||||
<>
|
||||
<th className="px-6 py-3">送樣資訊 (Date/Qty)</th>
|
||||
<th className="px-6 py-3">首張訂單 (Date/Qty)</th>
|
||||
<th className="px-6 py-3 text-center">轉換天數</th>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<th className="px-6 py-3">送樣日期</th>
|
||||
<th className="px-6 py-3 text-center">滯留天數</th>
|
||||
<th className="px-6 py-3 text-center">狀態</th>
|
||||
<th className="px-6 py-3 text-right">操作</th>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{orphans.map((row, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50 transition-colors group">
|
||||
<td className="px-6 py-4 font-medium text-slate-800">{row.customer}</td>
|
||||
<td className="px-6 py-4 font-mono text-xs text-slate-600">{row.pn}</td>
|
||||
<td className="px-6 py-4 text-slate-500">{row.date}</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className={`font-bold ${row.days_since_sent > 180 ? 'text-rose-600' : 'text-amber-600'}`}>
|
||||
{row.days_since_sent} 天
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold ${row.days_since_sent > 180 ? 'bg-rose-100 text-rose-700' : 'bg-amber-100 text-amber-700'
|
||||
}`}>
|
||||
{row.days_since_sent > 180 ? '呆滯庫存 (Dead Stock)' : '需採取行動'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button
|
||||
onClick={() => handleCopy(row, i)}
|
||||
className="inline-flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-800 font-medium bg-indigo-50 px-2 py-1 rounded"
|
||||
{viewMode === 'conversions' ? (
|
||||
conversions.map((row, i) => (
|
||||
<tr key={i} className="hover:bg-slate-50">
|
||||
<td className="px-6 py-4 font-medium text-slate-800">{row.customer}</td>
|
||||
<td className="px-6 py-4 font-mono text-xs text-slate-600">{row.pn}</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-slate-500 text-xs">{row.sample_date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')}</span>
|
||||
<span className="font-bold text-slate-700">{row.sample_qty} pcs</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-slate-500 text-xs">{row.order_date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')}</span>
|
||||
<span className="font-bold text-emerald-600">{row.order_qty.toLocaleString()} pcs</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-bold bg-blue-100 text-blue-700">
|
||||
{row.days_to_convert} 天
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
orphans.map((row, i) => {
|
||||
const groupKey = `${row.customer}|${row.pn}`;
|
||||
const isRepeated = (groupInfo[groupKey] || 0) > 1;
|
||||
const isSelected = selectedGroup === groupKey;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={i}
|
||||
onClick={() => 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'}
|
||||
`}
|
||||
>
|
||||
{copiedId === i ? <Check size={12} /> : <Copy size={12} />}
|
||||
{copiedId === i ? '已複製' : '複製詳情'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{orphans.length === 0 && (
|
||||
<td className="px-6 py-4">
|
||||
<div className={`font-medium ${isRepeated ? 'text-indigo-700' : 'text-slate-800'}`}>
|
||||
{row.customer}
|
||||
{isRepeated && (
|
||||
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] bg-indigo-100 text-indigo-700 font-bold">
|
||||
x{groupInfo[groupKey]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono text-xs text-slate-600">
|
||||
{row.pn}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-500">{row.date?.replace(/(\d{4})(\d{2})(\d{2})/, '$1/$2/$3')}</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className={`font-bold ${row.days_since_sent > 180 ? 'text-rose-600' : 'text-amber-600'}`}>
|
||||
{row.days_since_sent} 天
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold ${row.days_since_sent > 180 ? 'bg-rose-100 text-rose-700' : 'bg-amber-100 text-amber-700'
|
||||
}`}>
|
||||
{row.days_since_sent > 180 ? '呆滯庫存 (Dead Stock)' : '需採取行動'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopy(row, i);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 text-xs text-indigo-600 hover:text-indigo-800 font-medium bg-indigo-50 px-2 py-1 rounded"
|
||||
>
|
||||
{copiedId === i ? <Check size={12} /> : <Copy size={12} />}
|
||||
{copiedId === i ? '已複製' : '複製詳情'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
|
||||
{viewMode === 'orphans' && orphans.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-10 text-center text-slate-400">
|
||||
目前沒有孤兒樣品,所有送樣皆在有效時限內或已轉化。
|
||||
目前沒有無需跟進的無訂單樣品,所有送樣皆在有效時限內或已轉化。
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{viewMode === 'conversions' && conversions.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-10 text-center text-slate-400">
|
||||
目前沒有成功轉換的紀錄。
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
@@ -83,9 +83,16 @@ export const ReviewView: React.FC<ReviewViewProps> = ({ onReviewComplete }) => {
|
||||
<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?.op_id}</span>
|
||||
<div className="flex flex-col gap-1 mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge type="info">DIT (設計導入)</Badge>
|
||||
<span className="text-xs text-slate-500 font-mono">OP NO: {item.dit?.op_id}</span>
|
||||
</div>
|
||||
{item.dit?.op_name && (
|
||||
<div className="text-sm font-medium text-indigo-900 line-clamp-2" title={item.dit.op_name}>
|
||||
{item.dit.op_name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-slate-400 uppercase">Customer Name</div>
|
||||
|
||||
Reference in New Issue
Block a user