diff --git a/frontend/src/components/layout/PortalHeader.tsx b/frontend/src/components/layout/PortalHeader.tsx index 8f2dd91..748bdc9 100644 --- a/frontend/src/components/layout/PortalHeader.tsx +++ b/frontend/src/components/layout/PortalHeader.tsx @@ -5,8 +5,9 @@ import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import { BaseHeader } from './BaseHeader'; import { Mono } from '@/components/common'; import { useThemeModeContext } from '@/context/ThemeModeContext'; -import { useMyPolicies } from '@/hooks/useMyPolicies'; +import { useMyPolicies, type MyPolicySummary } from '@/hooks/useMyPolicies'; import type { ParticipantInfo } from '@/hooks/useParticipantRole'; +import { MasterAgreementStatus } from '@/lib/idl/open_parametric'; const Controls = styled.div` display: flex; @@ -18,18 +19,18 @@ const PolicySelect = styled.select` background: ${p => p.theme.colors.card}; border: 1px solid ${p => p.theme.colors.border}; color: ${p => p.theme.colors.text}; - font-family: ${p => p.theme.fonts.sans}; - font-size: 11px; + font-family: ${p => p.theme.fonts.mono}; + font-size: 10px; font-weight: 600; padding: 5px 24px 5px 9px; - border-radius: ${p => p.theme.radii.sm}; + border-radius: 7px; outline: none; cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2394A3B8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; - max-width: 160px; + max-width: 220px; `; const ThemeToggle = styled.button` @@ -143,6 +144,29 @@ const ROLE_COLORS: Record = { rein: '#38BDF8', }; +const POLICY_ROLE_LABEL: Record = { + leader: '리더사', + partA: '참여사', + partB: '참여사', + rein: '재보험사', +}; + +function policyStatusLabel(p: MyPolicySummary): string { + if (p.track === 'B') return p.statusLabel || 'Unknown'; + if (p.status === MasterAgreementStatus.Active) return 'Active'; + if (p.status === MasterAgreementStatus.PendingConfirm) return 'Pending'; + if (p.status === MasterAgreementStatus.Closed) return 'Closed'; + if (p.status === MasterAgreementStatus.Cancelled) return 'Cancelled'; + return 'Draft'; +} + +function formatPolicyDate(ts?: number): string { + if (!ts) return ''; + return new Date(ts * 1000).toLocaleDateString('ko-KR', { + year: '2-digit', month: '2-digit', day: '2-digit', + }); +} + interface PortalHeaderProps { role: 'leader' | 'participant' | 'rein' | null; masterPDA: string | null; @@ -178,11 +202,22 @@ export function PortalHeader({ role, masterPDA, roles, hideBottomBar = false, pa {policies.length > 0 && ( - {policies.map(p => ( - - ))} + {policies.map(p => { + const role = p.roles[0]?.role; + const roleLabel = role ? (POLICY_ROLE_LABEL[role] || '') : ''; + const dateStr = formatPolicyDate(p.coverageEndTs); + const parts = [ + `${p.pda.slice(0, 8)}...`, + policyStatusLabel(p), + roleLabel, + dateStr, + ].filter(Boolean); + return ( + + ); + })} )} diff --git a/frontend/src/hooks/useMyPolicies.ts b/frontend/src/hooks/useMyPolicies.ts index cfee1e1..558251f 100644 --- a/frontend/src/hooks/useMyPolicies.ts +++ b/frontend/src/hooks/useMyPolicies.ts @@ -17,6 +17,7 @@ export interface MyPolicySummary { statusLabel: string; roles: MyPolicyRole[]; track: 'A' | 'B'; + /** Coverage end timestamp (Track A only) */ coverageEndTs?: number; /** Track B only fields */ flightNo?: string; diff --git a/frontend/src/pages/PortalPage.tsx b/frontend/src/pages/PortalPage.tsx index 1490123..4b58d8a 100644 --- a/frontend/src/pages/PortalPage.tsx +++ b/frontend/src/pages/PortalPage.tsx @@ -6,11 +6,11 @@ import { useWallet } from '@solana/wallet-adapter-react'; import { useTranslation } from 'react-i18next'; import { PageShell } from '@/components/layout/PageShell'; import { PortalHeader } from '@/components/layout/PortalHeader'; - +import { Tag, Mono } from '@/components/common'; import { useParticipantRole } from '@/hooks/useParticipantRole'; import { useMyPolicies, type MyPolicySummary } from '@/hooks/useMyPolicies'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; -import { MasterAgreementStatus, POLICY_STATE_LABELS } from '@/lib/idl/open_parametric'; +import { MasterAgreementStatus, POLICY_STATE_LABELS, PolicyState } from '@/lib/idl/open_parametric'; import { LeaderPortal } from './portal/LeaderPortal'; import { ParticipantPortal } from './portal/ParticipantPortal'; import { ReinPortal } from './portal/ReinPortal'; @@ -71,6 +71,13 @@ const PolicyCard = styled.div` } `; +const ROLE_COLORS: Record = { + leader: '#9945FF', + partA: '#22C55E', + partB: '#F59E0B', + rein: '#38BDF8', +}; + const STATUS_LABELS: Record = { [MasterAgreementStatus.Draft]: 'Draft', [MasterAgreementStatus.PendingConfirm]: 'Pending', @@ -79,40 +86,55 @@ const STATUS_LABELS: Record = { [MasterAgreementStatus.Cancelled]: 'Cancelled', }; - -const PORTAL_ROLE_LABEL: Record = { - leader: '리더사', - partA: '참여사', - partB: '참여사', - rein: '재보험사', +const STATUS_COLORS: Record = { + [MasterAgreementStatus.Draft]: '#94A3B8', + [MasterAgreementStatus.PendingConfirm]: '#F59E0B', + [MasterAgreementStatus.Active]: '#22C55E', + [MasterAgreementStatus.Closed]: '#64748B', + [MasterAgreementStatus.Cancelled]: '#EF4444', }; -function formatDate(ts: number): string { - return new Date(ts * 1000).toLocaleDateString('ko-KR', { - year: '2-digit', month: '2-digit', day: '2-digit', - }); -} +const TRACK_B_STATUS_COLORS: Record = { + [PolicyState.Draft]: '#94A3B8', + [PolicyState.Open]: '#38BDF8', + [PolicyState.Funded]: '#F59E0B', + [PolicyState.Active]: '#22C55E', + [PolicyState.Claimable]: '#EF4444', + [PolicyState.Approved]: '#9945FF', + [PolicyState.Settled]: '#64748B', + [PolicyState.Expired]: '#475569', +}; function PolicyListItem({ policy, onClick }: { policy: MyPolicySummary; onClick: () => void }) { + const { t } = useTranslation(); const isTrackB = policy.track === 'B'; + const statusColor = isTrackB + ? (TRACK_B_STATUS_COLORS[policy.status] || '#94A3B8') + : (STATUS_COLORS[policy.status] || '#94A3B8'); const statusLabel = isTrackB ? (POLICY_STATE_LABELS[policy.status] || 'Unknown') : (STATUS_LABELS[policy.status] || 'Unknown'); - const primaryRole = policy.roles[0]; - const roleLabel = primaryRole ? (PORTAL_ROLE_LABEL[primaryRole.role] || primaryRole.role) : ''; - const dateStr = policy.coverageEndTs ? formatDate(policy.coverageEndTs) : ''; - const label = [ - `${policy.pda.slice(0, 8)}...`, - statusLabel, - roleLabel, - dateStr, - ].filter(Boolean).join(' · '); return ( - - {label} - +
+ {policy.roles.map(r => ( + + {t(`portal.role.${r.role}`, r.role)} + + ))} +
+ + {isTrackB ? `Policy #${policy.masterId}` : `Master #${policy.masterId}`} + + + {isTrackB && policy.flightNo + ? `${policy.flightNo} · ${policy.route}` + : `${policy.pda.slice(0, 12)}...${policy.pda.slice(-8)}`} + +
+
+ {statusLabel}
); }