285 lines
15 KiB
TypeScript
285 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { t } from '@/i18n/locales';
|
|
import { useApi } from '@/lib/use-api';
|
|
|
|
/**
|
|
* D8.4 IPO准备度检查清单 - 独立页面
|
|
*/
|
|
|
|
interface CheckItem {
|
|
id: string;
|
|
item: string;
|
|
category: string;
|
|
status: 'done' | 'progress' | 'blocked' | 'pending';
|
|
owner: string;
|
|
deadline: string;
|
|
dependency?: string;
|
|
note?: string;
|
|
}
|
|
|
|
interface IpoData {
|
|
overallProgress: { total: number; done: number; inProgress: number; blocked: number; pending: number; percent: number };
|
|
milestones: { name: string; date: string; status: 'done' | 'progress' | 'pending' }[];
|
|
checklistItems: CheckItem[];
|
|
keyContacts: { role: string; name: string; status: string }[];
|
|
}
|
|
|
|
const loadingBox: React.CSSProperties = {
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
padding: 40, color: 'var(--color-text-tertiary)', font: 'var(--text-body)',
|
|
};
|
|
|
|
const categories = [
|
|
{ key: 'legal', label: () => t('ipo_cat_legal'), icon: '§', color: 'var(--color-primary)' },
|
|
{ key: 'financial', label: () => t('ipo_cat_financial'), icon: '$', color: 'var(--color-success)' },
|
|
{ key: 'sox', label: () => t('ipo_cat_sox'), icon: '✓', color: 'var(--color-info)' },
|
|
{ key: 'governance', label: () => t('ipo_cat_governance'), icon: '◆', color: 'var(--color-warning)' },
|
|
{ key: 'insurance', label: () => t('ipo_cat_insurance'), icon: '☂', color: '#FF6B6B' },
|
|
];
|
|
|
|
const statusConfig: Record<string, { label: () => string; bg: string; fg: string }> = {
|
|
done: { label: () => t('completed'), bg: 'var(--color-success-light)', fg: 'var(--color-success)' },
|
|
progress: { label: () => t('in_progress'), bg: 'var(--color-warning-light)', fg: 'var(--color-warning)' },
|
|
blocked: { label: () => t('blocked'), bg: 'var(--color-error-light)', fg: 'var(--color-error)' },
|
|
pending: { label: () => t('pending'), bg: 'var(--color-gray-100)', fg: 'var(--color-text-tertiary)' },
|
|
};
|
|
|
|
export const IpoReadinessPage: React.FC = () => {
|
|
const { data: ipoData, isLoading, error } = useApi<IpoData>('/api/v1/admin/compliance/reports');
|
|
const { data: insuranceData } = useApi<{ ipoReadiness: number }>('/api/v1/admin/insurance/stats');
|
|
|
|
if (error) return <div style={loadingBox}>Error: {error.message}</div>;
|
|
if (isLoading) return <div style={loadingBox}>Loading...</div>;
|
|
|
|
const overallProgress = ipoData?.overallProgress ?? { total: 0, done: 0, inProgress: 0, blocked: 0, pending: 0, percent: 0 };
|
|
const milestones = ipoData?.milestones ?? [];
|
|
const checklistItems = ipoData?.checklistItems ?? [];
|
|
const keyContacts = ipoData?.keyContacts ?? [];
|
|
|
|
return (
|
|
<div>
|
|
<h1 style={{ font: 'var(--text-h1)', color: 'var(--color-text-primary)', marginBottom: 8 }}>{t('ipo_title')}</h1>
|
|
<p style={{ font: 'var(--text-body)', color: 'var(--color-text-secondary)', marginBottom: 24 }}>
|
|
{t('ipo_subtitle')}
|
|
</p>
|
|
|
|
{/* Summary Stats */}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16, marginBottom: 24 }}>
|
|
{[
|
|
{ label: t('ipo_total_items'), value: overallProgress.total, color: 'var(--color-text-primary)' },
|
|
{ label: t('completed'), value: overallProgress.done, color: 'var(--color-success)' },
|
|
{ label: t('in_progress'), value: overallProgress.inProgress, color: 'var(--color-warning)' },
|
|
{ label: t('blocked'), value: overallProgress.blocked, color: 'var(--color-error)' },
|
|
{ label: t('pending'), value: overallProgress.pending, color: 'var(--color-text-tertiary)' },
|
|
].map(s => (
|
|
<div key={s.label} style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-border-light)', padding: 20, textAlign: 'center',
|
|
}}>
|
|
<div style={{ font: 'var(--text-body-sm)', color: 'var(--color-text-tertiary)', marginBottom: 8 }}>{s.label}</div>
|
|
<div style={{ font: 'var(--text-display)', color: s.color }}>{s.value}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Overall Progress Bar */}
|
|
<div style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-border-light)', padding: 20, marginBottom: 24,
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
<span style={{ font: 'var(--text-label)', color: 'var(--color-text-primary)' }}>{t('ipo_overall_progress')}</span>
|
|
<span style={{ font: 'var(--text-h2)', color: 'var(--color-primary)' }}>{overallProgress.percent}%</span>
|
|
</div>
|
|
<div style={{ height: 12, background: 'var(--color-gray-100)', borderRadius: 'var(--radius-full)', overflow: 'hidden', display: 'flex' }}>
|
|
<div style={{ width: `${overallProgress.total > 0 ? (overallProgress.done / overallProgress.total * 100).toFixed(0) : 0}%`, height: '100%', background: 'var(--color-success)' }} />
|
|
<div style={{ width: `${overallProgress.total > 0 ? (overallProgress.inProgress / overallProgress.total * 100).toFixed(0) : 0}%`, height: '100%', background: 'var(--color-warning)' }} />
|
|
<div style={{ width: `${overallProgress.total > 0 ? (overallProgress.blocked / overallProgress.total * 100).toFixed(0) : 0}%`, height: '100%', background: 'var(--color-error)' }} />
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 16, marginTop: 8 }}>
|
|
{[
|
|
{ label: t('completed'), color: 'var(--color-success)' },
|
|
{ label: t('in_progress'), color: 'var(--color-warning)' },
|
|
{ label: t('blocked'), color: 'var(--color-error)' },
|
|
{ label: t('pending'), color: 'var(--color-gray-200)' },
|
|
].map(l => (
|
|
<div key={l.label} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<div style={{ width: 8, height: 8, borderRadius: '50%', background: l.color }} />
|
|
<span style={{ font: 'var(--text-caption)', color: 'var(--color-text-tertiary)' }}>{l.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 24 }}>
|
|
{/* Left: Checklist by Category */}
|
|
<div>
|
|
{categories.map(cat => {
|
|
const items = checklistItems.filter(i => i.category === cat.key);
|
|
const catDone = items.filter(i => i.status === 'done').length;
|
|
if (items.length === 0) return null;
|
|
return (
|
|
<div key={cat.key} style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-border-light)', padding: 20, marginBottom: 16,
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<div style={{
|
|
width: 28, height: 28, borderRadius: 'var(--radius-sm)', display: 'flex',
|
|
alignItems: 'center', justifyContent: 'center', background: cat.color, color: 'white',
|
|
font: 'var(--text-label)', fontWeight: 700,
|
|
}}>
|
|
{cat.icon}
|
|
</div>
|
|
<span style={{ font: 'var(--text-h3)', color: 'var(--color-text-primary)' }}>{cat.label()}</span>
|
|
</div>
|
|
<span style={{ font: 'var(--text-label-sm)', color: 'var(--color-text-secondary)' }}>
|
|
{catDone}/{items.length} {t('ipo_unit_done')}
|
|
</span>
|
|
</div>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr>
|
|
{[t('ipo_th_id'), t('ipo_th_item'), t('ipo_th_owner'), t('ipo_th_deadline'), t('ipo_th_status')].map(h => (
|
|
<th key={h} style={{
|
|
padding: '6px 0', textAlign: 'left', font: 'var(--text-label-sm)',
|
|
color: 'var(--color-text-tertiary)', borderBottom: '1px solid var(--color-border-light)',
|
|
}}>{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items.map(item => {
|
|
const st = statusConfig[item.status];
|
|
return (
|
|
<tr key={item.id} style={{ borderBottom: '1px solid var(--color-border-light)' }}>
|
|
<td style={{ padding: '10px 0', fontFamily: 'var(--font-family-mono)', font: 'var(--text-body-sm)' }}>{item.id}</td>
|
|
<td style={{ padding: '10px 0' }}>
|
|
<div style={{ font: 'var(--text-body)' }}>{item.item}</div>
|
|
{item.note && <div style={{ font: 'var(--text-caption)', color: 'var(--color-text-tertiary)', marginTop: 2 }}>{item.note}</div>}
|
|
{item.dependency && (
|
|
<div style={{ font: 'var(--text-caption)', color: 'var(--color-info)', marginTop: 2 }}>
|
|
{t('ipo_dependency')}: {item.dependency}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td style={{ padding: '10px 0', font: 'var(--text-body-sm)' }}>{item.owner}</td>
|
|
<td style={{ padding: '10px 0', font: 'var(--text-body-sm)', fontFamily: 'var(--font-family-mono)' }}>{item.deadline}</td>
|
|
<td style={{ padding: '10px 0' }}>
|
|
<span style={{
|
|
padding: '2px 8px', borderRadius: 'var(--radius-full)', font: 'var(--text-caption)',
|
|
fontWeight: 600, background: st.bg, color: st.fg,
|
|
}}>{st.label()}</span>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Right: Timeline & Blockers */}
|
|
<div>
|
|
{/* IPO Timeline */}
|
|
<div style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-border-light)', padding: 20, marginBottom: 16,
|
|
}}>
|
|
<h2 style={{ font: 'var(--text-h2)', marginBottom: 16 }}>{t('ipo_timeline')}</h2>
|
|
{milestones.map((m, i) => (
|
|
<div key={m.name} style={{ display: 'flex', gap: 12 }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
|
<div style={{
|
|
width: 12, height: 12, borderRadius: '50%',
|
|
background: m.status === 'progress' ? 'var(--color-warning)' : m.status === 'done' ? 'var(--color-success)' : 'var(--color-gray-200)',
|
|
border: m.status === 'progress' ? '2px solid var(--color-warning)' : 'none',
|
|
}} />
|
|
{i < milestones.length - 1 && (
|
|
<div style={{ width: 2, height: 40, background: 'var(--color-border-light)' }} />
|
|
)}
|
|
</div>
|
|
<div style={{ paddingBottom: 16 }}>
|
|
<div style={{ font: 'var(--text-label)', color: 'var(--color-text-primary)' }}>{m.name}</div>
|
|
<div style={{ font: 'var(--text-caption)', color: 'var(--color-text-tertiary)' }}>{m.date}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Blockers */}
|
|
<div style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-error)', padding: 20, marginBottom: 16,
|
|
}}>
|
|
<h2 style={{ font: 'var(--text-h2)', color: 'var(--color-error)', marginBottom: 16 }}>{t('ipo_blockers')}</h2>
|
|
{checklistItems.filter(i => i.status === 'blocked').map(item => (
|
|
<div key={item.id} style={{
|
|
padding: 12, background: 'var(--color-error-light)', borderRadius: 'var(--radius-sm)', marginBottom: 8,
|
|
}}>
|
|
<div style={{ font: 'var(--text-label)', color: 'var(--color-error)' }}>{item.id}: {item.item}</div>
|
|
<div style={{ font: 'var(--text-caption)', color: 'var(--color-text-secondary)', marginTop: 4 }}>
|
|
{t('ipo_owner')}: {item.owner} · {t('ipo_deadline')}: {item.deadline}
|
|
</div>
|
|
{item.note && (
|
|
<div style={{ font: 'var(--text-caption)', color: 'var(--color-text-secondary)', marginTop: 2 }}>{item.note}</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Category Progress */}
|
|
<div style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-border-light)', padding: 20, marginBottom: 16,
|
|
}}>
|
|
<h2 style={{ font: 'var(--text-h2)', marginBottom: 16 }}>{t('ipo_category_progress')}</h2>
|
|
{categories.map(cat => {
|
|
const items = checklistItems.filter(i => i.category === cat.key);
|
|
if (items.length === 0) return null;
|
|
const catDone = items.filter(i => i.status === 'done').length;
|
|
const pct = Math.round(catDone / items.length * 100);
|
|
return (
|
|
<div key={cat.key} style={{ marginBottom: 14 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
|
<span style={{ font: 'var(--text-body-sm)', color: 'var(--color-text-primary)' }}>{cat.label()}</span>
|
|
<span style={{ font: 'var(--text-label-sm)', color: cat.color }}>{pct}%</span>
|
|
</div>
|
|
<div style={{ height: 6, background: 'var(--color-gray-100)', borderRadius: 'var(--radius-full)', overflow: 'hidden' }}>
|
|
<div style={{ width: `${pct}%`, height: '100%', background: cat.color, borderRadius: 'var(--radius-full)' }} />
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Key Contacts */}
|
|
<div style={{
|
|
background: 'var(--color-surface)', borderRadius: 'var(--radius-md)',
|
|
border: '1px solid var(--color-border-light)', padding: 20,
|
|
}}>
|
|
<h2 style={{ font: 'var(--text-h2)', marginBottom: 16 }}>{t('ipo_key_contacts')}</h2>
|
|
{keyContacts.map(c => (
|
|
<div key={c.role} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 0', borderBottom: '1px solid var(--color-border-light)' }}>
|
|
<div>
|
|
<div style={{ font: 'var(--text-label-sm)', color: 'var(--color-text-primary)' }}>{c.role}</div>
|
|
<div style={{ font: 'var(--text-body-sm)', color: 'var(--color-text-secondary)' }}>{c.name}</div>
|
|
</div>
|
|
<span style={{
|
|
padding: '2px 8px', borderRadius: 'var(--radius-full)', font: 'var(--text-caption)',
|
|
background: 'var(--color-primary-surface)', color: 'var(--color-primary)', fontWeight: 600,
|
|
}}>{c.status}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|