CarboAny / src /lib /approvalStore.ts
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
5.9 kB
// Unified approval queue store (Supabase-backed)
import { supabase } from './supabase';
import type {
ApprovalType as DbApprovalType,
ApprovalStatus as DbApprovalStatus,
} from './database.types';
export type ApprovalType = DbApprovalType;
export type ApprovalStatus = DbApprovalStatus;
// UI-facing camelCase interface (mapped from DB snake_case)
export interface ApprovalItem {
id: string;
type: ApprovalType;
status: ApprovalStatus;
requesterId: string | null;
requesterName: string;
title: string;
description: string | null;
payload: Record<string, unknown>;
reviewerId?: string | null;
reviewerName?: string | null;
reviewedAt?: string | null;
rejectReason?: string | null;
createdAt: string;
}
export interface ApprovalFilters {
type?: ApprovalType;
status?: ApprovalStatus;
requesterId?: string;
}
export interface ApprovalStats {
pending: number;
approved: number;
rejected: number;
byType: Record<ApprovalType, number>;
}
// --- Mapping helpers ---
function dbToItem(row: Record<string, unknown>): ApprovalItem {
return {
id: row.id as string,
type: row.type as ApprovalType,
status: row.status as ApprovalStatus,
requesterId: (row.requester_id as string) ?? null,
requesterName: row.requester_name as string,
title: row.title as string,
description: (row.description as string) ?? null,
payload: (row.payload as Record<string, unknown>) ?? {},
reviewerId: (row.reviewer_id as string) ?? null,
reviewerName: (row.reviewer_name as string) ?? null,
reviewedAt: (row.reviewed_at as string) ?? null,
rejectReason: (row.reject_reason as string) ?? null,
createdAt: row.created_at as string,
};
}
// --- CRUD ---
export async function getApprovalQueue(filters?: ApprovalFilters): Promise<ApprovalItem[]> {
let query = supabase
.from('approval_queue')
.select('*')
.order('created_at', { ascending: false });
if (filters?.type) query = query.eq('type', filters.type);
if (filters?.status) query = query.eq('status', filters.status);
if (filters?.requesterId) query = query.eq('requester_id', filters.requesterId);
const { data, error } = await query;
if (error) {
console.error('getApprovalQueue error:', error);
return [];
}
return (data ?? []).map(dbToItem);
}
export async function getApprovalById(id: string): Promise<ApprovalItem | undefined> {
const { data, error } = await supabase
.from('approval_queue')
.select('*')
.eq('id', id)
.maybeSingle();
if (error) {
console.error('getApprovalById error:', error);
return undefined;
}
return data ? dbToItem(data) : undefined;
}
export async function createApproval(
item: Omit<ApprovalItem, 'id' | 'createdAt'>,
): Promise<ApprovalItem | null> {
const row = {
type: item.type,
status: item.status,
requester_id: item.requesterId,
requester_name: item.requesterName,
title: item.title,
description: item.description,
payload: item.payload,
reviewer_id: item.reviewerId ?? null,
reviewer_name: item.reviewerName ?? null,
reviewed_at: item.reviewedAt ?? null,
reject_reason: item.rejectReason ?? null,
};
const { data, error } = await supabase
.from('approval_queue')
.insert(row)
.select()
.single();
if (error) {
console.error('createApproval error:', error);
return null;
}
return dbToItem(data);
}
export async function updateApprovalStatus(
id: string,
status: ApprovalStatus,
reviewerId?: string,
reviewerName?: string,
rejectReason?: string,
): Promise<ApprovalItem | null> {
const updates: Record<string, unknown> = {
status,
reviewer_id: reviewerId ?? null,
reviewer_name: reviewerName ?? null,
reviewed_at: new Date().toISOString(),
};
if (status === 'rejected' && rejectReason) {
updates.reject_reason = rejectReason;
}
const { data, error } = await supabase
.from('approval_queue')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) {
console.error('updateApprovalStatus error:', error);
return null;
}
return dbToItem(data);
}
export async function getApprovalsByRequester(requesterId: string): Promise<ApprovalItem[]> {
return getApprovalQueue({ requesterId });
}
export async function getApprovalStats(): Promise<ApprovalStats> {
const { data, error } = await supabase
.from('approval_queue')
.select('type, status');
if (error) {
console.error('getApprovalStats error:', error);
return {
pending: 0, approved: 0, rejected: 0,
byType: {
campaign_review: 0, sme_application: 0, sme_selection: 0,
trade_request: 0, point_conversion: 0, finance_registration: 0,
},
};
}
const items = (data ?? []) as { type: ApprovalType; status: ApprovalStatus }[];
const allTypes: ApprovalType[] = [
'campaign_review', 'sme_application', 'sme_selection',
'trade_request', 'point_conversion', 'finance_registration',
];
const byType = Object.fromEntries(
allTypes.map((t) => [t, items.filter((i) => i.type === t && i.status === 'pending').length]),
) as Record<ApprovalType, number>;
return {
pending: items.filter((i) => i.status === 'pending').length,
approved: items.filter((i) => i.status === 'approved').length,
rejected: items.filter((i) => i.status === 'rejected').length,
byType,
};
}
// --- Type labels (ํ•œ๊ตญ์–ด) ---
export const approvalTypeLabels: Record<ApprovalType, string> = {
campaign_review: '์บ ํŽ˜์ธ ์Šน์ธ',
sme_application: 'SME ์ˆ˜ํ˜œ ์‹ ์ฒญ',
sme_selection: 'SME ์„ ์ •',
trade_request: '๊ฑฐ๋ž˜ ์Šน์ธ',
point_conversion: 'ํฌ์ธํŠธ ์ „ํ™˜',
finance_registration: '๊ธˆ์œต์‚ฌ ๊ฐ€์ž…',
};
export const approvalStatusLabels: Record<ApprovalStatus, string> = {
pending: '๋Œ€๊ธฐ',
approved: '์Šน์ธ',
rejected: '๋ฐ˜๋ ค',
cancelled: '์ทจ์†Œ',
};