import type { AdminProgramSummary } from './admin'; import type { Program } from '../types'; export type AdminControlStatus = 'ready' | 'needs_admin' | 'blocked' | 'live' | 'locked'; export interface AdminControlPlaneStep { key: 'sponsor' | 'app' | 'evidence' | 'methodology' | 'sme' | 'settlement' | 'report'; label: string; status: AdminControlStatus; owner: string; reason: string; action: string; route: string; } export interface AdminControlPlaneSnapshot { campaignId: string; title: string; source: 'db' | 'demo' | 'fallback'; nextAction: string; nextRoute: string; severity: 'ok' | 'watch' | 'blocker'; blockingReasons: string[]; steps: AdminControlPlaneStep[]; } const statusRank: Record = { blocked: 0, needs_admin: 1, ready: 2, live: 3, locked: 4, }; export const controlStatusLabel: Record = { ready: '준비', needs_admin: '확인', blocked: '차단', live: '운영', locked: '잠금', }; export const controlStatusClass: Record = { ready: 'border-status-info text-status-info bg-paper-white', needs_admin: 'border-status-warning text-status-warning bg-paper-cream', blocked: 'border-status-error text-status-error bg-paper-white', live: 'border-accent-teal text-accent-teal bg-paper-white', locked: 'border-ink-black text-ink-black bg-paper-gray', }; function classifySeverity(steps: AdminControlPlaneStep[]): AdminControlPlaneSnapshot['severity'] { if (steps.some((step) => step.status === 'blocked')) return 'blocker'; if (steps.some((step) => step.status === 'needs_admin')) return 'watch'; return 'ok'; } function firstAction(steps: AdminControlPlaneStep[]) { return [...steps].sort((a, b) => statusRank[a.status] - statusRank[b.status])[0]; } function buildSnapshot(input: { campaignId: string; title: string; source: AdminControlPlaneSnapshot['source']; status: string; sponsorName: string; budgetKrw: number; targetCo2Kg: number; currentCo2Kg: number; totalSubmissions: number; pendingSubmissions: number; rejectedSubmissions: number; participantCount: number; methodology?: string; }): AdminControlPlaneSnapshot { const progressPct = input.targetCo2Kg > 0 ? (input.currentCo2Kg / input.targetCo2Kg) * 100 : 0; const approvedSubmissions = Math.max( 0, input.totalSubmissions - input.pendingSubmissions - input.rejectedSubmissions, ); const verificationRate = input.totalSubmissions > 0 ? (approvedSubmissions / input.totalSubmissions) * 100 : 0; const hasMethodology = !!input.methodology && !/미등록|DB 캠페인/.test(input.methodology); const hasBudget = input.budgetKrw > 0; const hasAppExposure = input.status === 'active' || input.status === 'completed'; const settlementReady = input.status === 'completed' || (input.status === 'active' && progressPct >= 80); const reportReady = hasMethodology && approvedSubmissions > 0 && settlementReady; const smeReady = hasBudget && (input.status === 'active' || input.status === 'completed'); const steps: AdminControlPlaneStep[] = [ { key: 'sponsor', label: 'Sponsor', status: input.sponsorName && hasBudget ? 'ready' : 'needs_admin', owner: 'admin', reason: hasBudget ? `${input.sponsorName} 예산 구조 확인` : '스폰서 예산/담당자 확인 필요', action: hasBudget ? '스폰서 운영 큐 확인' : '예산과 담당자 보완', route: `/admin/programs/${input.campaignId}`, }, { key: 'app', label: 'App', status: hasAppExposure ? 'live' : input.status === 'draft' ? 'blocked' : 'needs_admin', owner: 'operations', reason: hasAppExposure ? '사용자앱 노출 가능 상태' : '앱 노출 전 승인/미션 점검 필요', action: hasAppExposure ? '앱 노출 문구 점검' : '미션/증빙 조건 확인', route: `/admin/programs/${input.campaignId}`, }, { key: 'evidence', label: 'Evidence', status: input.pendingSubmissions > 0 ? 'needs_admin' : input.totalSubmissions > 0 ? 'ready' : 'blocked', owner: 'verifier', reason: input.totalSubmissions === 0 ? '제출 데이터 없음' : input.pendingSubmissions > 0 ? `검증 대기 ${input.pendingSubmissions.toLocaleString('ko-KR')}건` : `승인율 ${verificationRate.toFixed(0)}%`, action: input.pendingSubmissions > 0 ? '검증 큐 처리' : '증빙 상태 확인', route: '/admin/submissions', }, { key: 'methodology', label: 'Method', status: hasMethodology ? 'locked' : 'needs_admin', owner: 'methodology', reason: hasMethodology ? '방법론 근거 연결됨' : '방법론/정책 버전 잠금 필요', action: hasMethodology ? '정책 버전 영향 확인' : '방법론 매칭 검토', route: '/admin/methodology', }, { key: 'sme', label: 'SME', status: smeReady ? 'ready' : 'needs_admin', owner: 'sme manager', reason: smeReady ? 'SME 배분 설계 가능' : 'SME 후보/배분 근거 확인 필요', action: 'SME 선정 근거 확인', route: '/admin/sme', }, { key: 'settlement', label: 'Settlement', status: settlementReady ? 'ready' : input.totalSubmissions > 0 ? 'needs_admin' : 'blocked', owner: 'finance', reason: settlementReady ? '정산 잠금 준비' : '검증 완료 수치 부족', action: settlementReady ? '정산 대조' : '검증/참여 수치 보강', route: '/admin/settlement', }, { key: 'report', label: 'Report', status: reportReady ? 'ready' : 'needs_admin', owner: 'report owner', reason: reportReady ? '보고 readiness 준비' : '방법론·검증·정산 중 누락 있음', action: reportReady ? '보고서 readiness 확인' : '보고 차단 사유 정리', route: `/admin/programs/${input.campaignId}`, }, ]; const severity = classifySeverity(steps); const next = firstAction(steps); return { campaignId: input.campaignId, title: input.title, source: input.source, nextAction: next.action, nextRoute: next.route, severity, blockingReasons: steps .filter((step) => step.status === 'blocked' || step.status === 'needs_admin') .map((step) => `${step.label}: ${step.reason}`), steps, }; } export function deriveAdminControlPlaneFromSummary(program: AdminProgramSummary): AdminControlPlaneSnapshot { return buildSnapshot({ campaignId: program.id, title: program.title, source: program.source, status: program.status, sponsorName: program.sponsorName, budgetKrw: program.budgetKrw, targetCo2Kg: program.targetCo2Kg, currentCo2Kg: program.currentCo2Kg, totalSubmissions: program.totalSubmissions, pendingSubmissions: program.pendingSubmissions, rejectedSubmissions: 0, participantCount: program.participantCount, }); } export function deriveAdminControlPlaneFromProgram(program: Program): AdminControlPlaneSnapshot { return buildSnapshot({ campaignId: program.id, title: program.title, source: 'fallback', status: program.status, sponsorName: program.organizationName, budgetKrw: program.budgetKRW, targetCo2Kg: program.co2ReductionKg, currentCo2Kg: program.co2ReductionKg, totalSubmissions: program.totalVerifications, pendingSubmissions: program.pendingVerifications, rejectedSubmissions: program.rejectedVerifications, participantCount: program.currentParticipants, methodology: program.methodology, }); }