CarboAny / src /lib /adminControlPlane.ts
Esketch's picture
Claude Fable 5
deploy: Campaign Control Plane + Security Hardening Release (Orphan Clean Build v3)
5c98911
Raw History Blame Contribute Delete
7.66 kB
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<AdminControlStatus, number> = {
blocked: 0,
needs_admin: 1,
ready: 2,
live: 3,
locked: 4,
};
export const controlStatusLabel: Record<AdminControlStatus, string> = {
ready: '์ค€๋น„',
needs_admin: 'ํ™•์ธ',
blocked: '์ฐจ๋‹จ',
live: '์šด์˜',
locked: '์ž ๊ธˆ',
};
export const controlStatusClass: Record<AdminControlStatus, string> = {
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,
});
}