| import { supabase } from './supabase'; |
| import type { CampaignStatus } from './database.types'; |
| import type { Program } from '../types'; |
| import { mockPrograms, resolveMockProgramId } from '../data/mockPrograms'; |
|
|
| export interface AdminProgramSummary { |
| id: string; |
| title: string; |
| sponsorName: string; |
| status: string; |
| startDate: string; |
| endDate?: string; |
| budgetKrw: number; |
| targetCo2Kg: number; |
| currentCo2Kg: number; |
| totalSubmissions: number; |
| pendingSubmissions: number; |
| participantCount: number; |
| achievementRate: number; |
| source: 'db' | 'demo'; |
| } |
|
|
| export interface AdminSettlementStat { |
| asset: 'KRW' | 'POINT' | 'TON'; |
| category: string; |
| totalAmount: number; |
| txCount: number; |
| lastTxAt: string; |
| } |
|
|
| function toSettlementAsset(value: unknown): AdminSettlementStat['asset'] { |
| return value === 'POINT' || value === 'TON' ? value : 'KRW'; |
| } |
|
|
| function isMissingSchemaError(error: { code?: string } | null) { |
| return error?.code === 'PGRST205' || error?.code === '42P01'; |
| } |
|
|
| function normalizeStatus(value: unknown): CampaignStatus { |
| if ( |
| value === 'draft' || |
| value === 'pending_review' || |
| value === 'active' || |
| value === 'paused' || |
| value === 'completed' |
| ) { |
| return value; |
| } |
| return 'draft'; |
| } |
|
|
| function inferOrganizationType( |
| sponsorName: string |
| ): Program['organizationType'] { |
| if (/๊ตฐ์ฒญ|์์ฒญ|๋์ฒญ|๊ตฌ์ฒญ|ํน๋ณ์|๊ด์ญ์/.test(sponsorName)) return 'municipality'; |
| if (/๊ณต์ฌ|๊ณต๋จ|์ฌ๋จ|๊ณต๊ณต|์ ๋ถ|๋ถ์ฒ|์ฒญ$/.test(sponsorName)) return 'public_corp'; |
| return 'enterprise'; |
| } |
|
|
| function dbSummaryToProgram(row: AdminProgramSummary): Program { |
| const approvedVerifications = Math.max( |
| 0, |
| row.totalSubmissions - row.pendingSubmissions |
| ); |
|
|
| return { |
| id: row.id, |
| title: row.title, |
| programType: inferOrganizationType(row.sponsorName) === 'enterprise' |
| ? 'corporate_event' |
| : 'public_caas', |
| organizationName: row.sponsorName || '๋ฏธ๋ฑ๋ก ์คํฐ์', |
| organizationType: inferOrganizationType(row.sponsorName || ''), |
| status: normalizeStatus(row.status), |
| startDate: row.startDate, |
| endDate: row.endDate || '์งํ ์ค', |
| targetParticipants: row.participantCount, |
| currentParticipants: row.participantCount, |
| totalVerifications: row.totalSubmissions, |
| approvedVerifications, |
| rejectedVerifications: 0, |
| pendingVerifications: row.pendingSubmissions, |
| co2ReductionKg: row.currentCo2Kg, |
| budgetKRW: row.budgetKrw, |
| pointsDistributed: 0, |
| methodology: 'DB ์บ ํ์ธ โ ์์ธ ๋ฐฉ๋ฒ๋ก ์ ๋ณด ๋ฏธ๋ฑ๋ก', |
| methodologyAutoAssigned: false, |
| apiCallsTotal: row.totalSubmissions, |
| apiCallsToday: 0, |
| fraudDetected: 0, |
| verifications: [], |
| }; |
| } |
|
|
| function simulatedAdminPrograms(): AdminProgramSummary[] { |
| return mockPrograms.map(p => ({ |
| id: p.id, |
| title: p.title, |
| sponsorName: p.organizationName, |
| status: p.status, |
| startDate: p.startDate, |
| endDate: p.endDate, |
| budgetKrw: p.budgetKRW || 0, |
| targetCo2Kg: p.co2ReductionKg || 0, |
| currentCo2Kg: p.co2ReductionKg || 0, |
| totalSubmissions: p.totalVerifications || 0, |
| pendingSubmissions: p.pendingVerifications || 0, |
| participantCount: p.currentParticipants || 0, |
| achievementRate: 0, |
| source: 'demo', |
| })); |
| } |
|
|
| function mapAdminProgramRow(row: Record<string, unknown>): AdminProgramSummary { |
| const campaignId = String(row.campaign_id ?? ''); |
|
|
| return { |
| id: resolveMockProgramId(campaignId) ?? campaignId, |
| title: String(row.title ?? '์ ๋ชฉ ๋ฏธ๋ฑ๋ก ์บ ํ์ธ'), |
| sponsorName: String(row.sponsor_name ?? '๋ฏธ๋ฑ๋ก ์คํฐ์'), |
| status: String(row.status ?? 'draft'), |
| startDate: String(row.start_date ?? ''), |
| endDate: row.end_date ? String(row.end_date) : undefined, |
| budgetKrw: Number(row.budget_krw ?? 0), |
| targetCo2Kg: Number(row.target_co2_kg ?? 0), |
| currentCo2Kg: Number(row.current_co2_kg ?? 0), |
| totalSubmissions: Number(row.total_submissions ?? 0), |
| pendingSubmissions: Number(row.pending_submissions ?? 0), |
| participantCount: Number(row.participant_count ?? 0), |
| achievementRate: Number(row.achievement_rate ?? 0), |
| source: 'db', |
| }; |
| } |
|
|
| |
| |
| |
| export async function getAdminPrograms(): Promise<AdminProgramSummary[]> { |
| const { data, error } = await supabase |
| .from('admin_program_summary') |
| .select('*') |
| .order('start_date', { ascending: false }); |
|
|
| if (error) { |
| if (!isMissingSchemaError(error)) console.error('getAdminPrograms error:', error); |
| return simulatedAdminPrograms(); |
| } |
|
|
| const dbPrograms = (data || []).map(mapAdminProgramRow); |
|
|
| |
| const dbProgramIds = new Set(dbPrograms.map((program) => program.id)); |
| return [ |
| ...dbPrograms, |
| ...simulatedAdminPrograms().filter((program) => !dbProgramIds.has(program.id)), |
| ]; |
| } |
|
|
| export async function getAdminProgram(id: string): Promise<Program | null> { |
| const canonicalId = resolveMockProgramId(id) ?? id; |
| const mockProgram = mockPrograms.find((program) => program.id === canonicalId); |
| if (mockProgram) return mockProgram; |
|
|
| const { data, error } = await supabase |
| .from('admin_program_summary') |
| .select('*') |
| .eq('campaign_id', id) |
| .maybeSingle(); |
|
|
| if (error) { |
| if (!isMissingSchemaError(error)) console.error('getAdminProgram error:', error); |
| return null; |
| } |
|
|
| return data ? dbSummaryToProgram(mapAdminProgramRow(data)) : null; |
| } |
|
|
| |
| |
| |
| export async function getGlobalSettlementStats(): Promise<AdminSettlementStat[]> { |
| const { data, error } = await supabase |
| .from('admin_global_settlement_stats') |
| .select('*'); |
|
|
| if (error) { |
| if (!isMissingSchemaError(error)) console.error('getGlobalSettlementStats error:', error); |
| return []; |
| } |
|
|
| return (data || []).map((row) => ({ |
| asset: toSettlementAsset(row.asset), |
| category: row.category, |
| totalAmount: Number(row.total_amount), |
| txCount: row.tx_count, |
| lastTxAt: row.last_tx_at, |
| })); |
| } |
|
|
| export interface AdminVerificationProject { |
| id: string; |
| title: string; |
| sponsorName: string; |
| registry: string; |
| methodology: string; |
| reductionTons: number; |
| stage: 'pending' | 'submitted' | 'reviewing' | 'approved' | 'rejected'; |
| verifier?: string; |
| slaRemainingDays?: number; |
| lastUpdatedAt: string; |
| } |
|
|
| |
| |
| |
| export async function getVerificationProjects(): Promise<AdminVerificationProject[]> { |
| |
| |
| const { data, error } = await supabase |
| .from('admin_program_summary') |
| .select('*'); |
|
|
| if (error) return []; |
|
|
| return (data || []).map((row) => ({ |
| id: row.campaign_id, |
| title: row.title, |
| sponsorName: row.sponsor_name, |
| registry: 'VCS', |
| methodology: 'VM0042', |
| reductionTons: Number(row.current_co2_kg) / 1000, |
| stage: row.status === 'completed' ? 'approved' : 'reviewing', |
| verifier: 'KQA (ํ๊ตญํ์ง์ฌ๋จ)', |
| slaRemainingDays: 14, |
| lastUpdatedAt: new Date().toISOString(), |
| })); |
| } |
|
|
| |
| |
| |
| export async function getVerificationStats(campaignId?: string) { |
| let query = supabase.from('admin_verification_stats').select('*'); |
| if (campaignId) query = query.eq('campaign_id', campaignId); |
|
|
| const { data, error } = await query; |
| if (error) { |
| console.error('getVerificationStats error:', error); |
| return []; |
| } |
| return data; |
| } |
|
|