import { useEffect, useState } from 'react' import { View, Text, StyleSheet, ActivityIndicator } from 'react-native' import { COLORS } from '../constants/Config' import { getApprovalStages } from '../services/api' /** * 기능 #65 — 다단계 승인 진행 시각화 * - GET /api/approvals/{id}/stages * - 단계별 타임라인: 단계명, 승인자, 상태, 시각 * - 완료(초록 체크) / 현재(파란 점, 하이라이트) / 대기(회색 원) / 반려(빨강) */ export interface ApprovalStage { level: number name?: string approver: string status: 'approved' | 'pending' | 'rejected' | 'current' | 'waiting' acted_at?: string | null } interface Props { approvalId: string | number /** 이미 보유한 단계 데이터가 있으면 fetch 생략 */ stages?: ApprovalStage[] } const STATUS_META: Record = { approved: { color: COLORS.success, mark: '✓', label: '승인' }, rejected: { color: COLORS.danger, mark: '✕', label: '반려' }, current: { color: COLORS.accent, mark: '●', label: '진행중' }, pending: { color: '#cbd5e1', mark: '○', label: '대기' }, waiting: { color: '#cbd5e1', mark: '○', label: '대기' }, } export default function ApprovalStages({ approvalId, stages: preset }: Props) { const [stages, setStages] = useState(preset ?? []) const [loading, setLoading] = useState(!preset) useEffect(() => { if (preset) { setStages(preset); return } let alive = true ;(async () => { setLoading(true) try { const r = await getApprovalStages(approvalId) if (alive) setStages(r.data?.stages ?? r.data ?? []) } catch { /* 무시 */ } finally { if (alive) setLoading(false) } })() return () => { alive = false } }, [approvalId, preset]) if (loading) return if (!stages.length) return 승인 단계 정보가 없습니다. return ( {stages.map((st, idx) => { const meta = STATUS_META[st.status] ?? STATUS_META.pending const isCurrent = st.status === 'current' const isLast = idx === stages.length - 1 return ( {meta.mark} {!isLast && } {st.level}단계 · {st.name ?? `승인 ${st.level}`} {meta.label} 승인자: {st.approver} {!!st.acted_at && {formatTime(st.acted_at)}} ) })} ) } function formatTime(ts: string): string { try { const d = new Date(ts) return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}` } catch { return ts } } const s = StyleSheet.create({ empty: { textAlign: 'center', color: COLORS.muted, paddingVertical: 16, fontSize: 12 }, row: { flexDirection: 'row' }, gutter: { width: 30, alignItems: 'center' }, node: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff' }, nodeMark: { fontSize: 12, fontWeight: '800', color: '#fff' }, line: { flex: 1, width: 2, backgroundColor: COLORS.border, marginVertical: 2 }, content: { flex: 1, paddingBottom: 16, paddingLeft: 8 }, contentCurrent: { backgroundColor: COLORS.light, borderRadius: 8, padding: 8, marginLeft: 4, marginBottom: 12 }, head: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, stageName: { fontSize: 13, fontWeight: '700', color: COLORS.text, flex: 1 }, badge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 9 }, badgeText: { fontSize: 10, fontWeight: '700', color: '#fff' }, approver: { fontSize: 12, color: COLORS.blue, marginTop: 3 }, time: { fontSize: 11, color: COLORS.muted, marginTop: 2 }, })