Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions frontend/src/components/layout/PortalHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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`
Expand Down Expand Up @@ -143,6 +144,29 @@ const ROLE_COLORS: Record<string, string> = {
rein: '#38BDF8',
};

const POLICY_ROLE_LABEL: Record<string, string> = {
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;
Expand Down Expand Up @@ -178,11 +202,22 @@ export function PortalHeader({ role, masterPDA, roles, hideBottomBar = false, pa
{policies.length > 0 && (
<PolicySelect value={masterPDA ?? ''} onChange={handlePolicyChange}>
<option value="">{t('portal.selectPolicy')}</option>
{policies.map(p => (
<option key={p.pda} value={p.pda}>
{p.track === 'B' ? `Policy #${p.masterId}` : `Master #${p.masterId}`}
</option>
))}
{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 (
<option key={p.pda} value={p.pda}>
{parts.join(' · ')}
</option>
);
})}
</PolicySelect>
)}
<ThemeToggle onClick={toggle} aria-label="테마 전환">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/useMyPolicies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
72 changes: 47 additions & 25 deletions frontend/src/pages/PortalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,6 +71,13 @@ const PolicyCard = styled.div`
}
`;

const ROLE_COLORS: Record<string, string> = {
leader: '#9945FF',
partA: '#22C55E',
partB: '#F59E0B',
rein: '#38BDF8',
};

const STATUS_LABELS: Record<number, string> = {
[MasterAgreementStatus.Draft]: 'Draft',
[MasterAgreementStatus.PendingConfirm]: 'Pending',
Expand All @@ -79,40 +86,55 @@ const STATUS_LABELS: Record<number, string> = {
[MasterAgreementStatus.Cancelled]: 'Cancelled',
};


const PORTAL_ROLE_LABEL: Record<string, string> = {
leader: '리더사',
partA: '참여사',
partB: '참여사',
rein: '재보험사',
const STATUS_COLORS: Record<number, string> = {
[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<number, string> = {
[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 (
<PolicyCard onClick={onClick}>
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600 }}>
{label}
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{policy.roles.map(r => (
<Tag key={r.role} variant="subtle" style={{ color: ROLE_COLORS[r.role] || '#94A3B8', fontSize: 9, minWidth: 48, textAlign: 'center' }}>
{t(`portal.role.${r.role}`, r.role)}
</Tag>
))}
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<span style={{ fontFamily: 'DM Mono, monospace', fontSize: 11, fontWeight: 600 }}>
{isTrackB ? `Policy #${policy.masterId}` : `Master #${policy.masterId}`}
</span>
<Mono style={{ fontSize: 9, color: 'var(--sub)' }}>
{isTrackB && policy.flightNo
? `${policy.flightNo} · ${policy.route}`
: `${policy.pda.slice(0, 12)}...${policy.pda.slice(-8)}`}
</Mono>
</div>
</div>
<Tag variant="subtle" style={{ color: statusColor, fontSize: 8 }}>{statusLabel}</Tag>
</PolicyCard>
);
}
Expand Down
Loading