import React, { useState, useCallback } from 'react' import { View, Text, FlatList, TouchableOpacity, StyleSheet, RefreshControl, Alert, Animated, } from 'react-native' import { GestureHandlerRootView, PanGestureHandler, State } from 'react-native-gesture-handler' import { COLORS } from '../../constants/Config' import { getApprovals, approveRequest, rejectRequest, cancelApproval } from '../../services/api' import { useFocusEffect } from 'expo-router' import RejectReason from '../../components/RejectReason' import ApprovalStages from '../../components/ApprovalStages' type Tab = 'pending' | 'approved' | 'rejected' interface ApprovalItem { id: number title: string requester: string created_at: string status: string type?: string } export default function ApprovalScreen() { const [tab, setTab] = useState('pending') const [items, setItems] = useState([]) const [loading, setLoading] = useState(false) const [selected, setSelected] = useState>(new Set()) const [rejectId, setRejectId] = useState(null) const [stagesId, setStagesId] = useState(null) const [undoId, setUndoId] = useState(null) const load = useCallback(async () => { setLoading(true) try { const r = await getApprovals(tab) setItems(r.data?.items ?? r.data ?? []) } catch { setItems([]) } finally { setLoading(false) } }, [tab]) useFocusEffect(useCallback(() => { load() }, [load])) const doApprove = async (id: number) => { try { await approveRequest(id, '') setUndoId(id) setTimeout(() => setUndoId(null), 3000) load() } catch { Alert.alert('오류', '승인 처리 중 오류가 발생했습니다.') } } const doUndo = async () => { if (!undoId) return try { await cancelApproval(undoId); setUndoId(null); load() } catch {} } const bulkApprove = async () => { const ids = [...selected] if (!ids.length) return Alert.alert('일괄 승인', `${ids.length}건을 승인하시겠습니까?`, [ { text: '취소', style: 'cancel' }, { text: '승인', onPress: async () => { await Promise.all(ids.map(id => approveRequest(id, ''))) setSelected(new Set()) load() }}, ]) } const toggleSelect = (id: number) => { const next = new Set(selected) next.has(id) ? next.delete(id) : next.add(id) setSelected(next) } const renderItem = ({ item }: { item: ApprovalItem }) => ( doApprove(item.id)} onReject={() => setRejectId(item.id)} onDetail={() => setStagesId(item.id)} selected={selected.has(item.id)} onSelect={() => toggleSelect(item.id)} /> ) return ( {/* 탭 */} {(['pending','approved','rejected'] as Tab[]).map(t => ( setTab(t)}> {t === 'pending' ? '대기' : t === 'approved' ? '승인' : '반려'} ))} {/* 일괄 승인 버튼 */} {selected.size > 0 && ( {selected.size}건 일괄 승인 )} String(i.id)} renderItem={renderItem} refreshControl={} ListEmptyComponent={항목이 없습니다.} contentContainerStyle={{ paddingBottom: 80 }} /> {/* 언두 스낵바 */} {undoId && ( 승인됐습니다 실행취소 )} {/* 반려 사유 모달 */} {rejectId && ( { await rejectRequest(rejectId, reason) setRejectId(null) load() }} onClose={() => setRejectId(null)} /> )} {/* 다단계 승인 */} {stagesId && ( )} ) } function SwipeCard({ item, onApprove, onReject, onDetail, selected, onSelect }: any) { const x = React.useRef(new Animated.Value(0)).current const onGesture = ({ nativeEvent }: any) => { if (nativeEvent.state === State.END) { if (nativeEvent.translationX > 80) { Animated.spring(x, { toValue: 0, useNativeDriver: true }).start() onApprove() } else if (nativeEvent.translationX < -80) { Animated.spring(x, { toValue: 0, useNativeDriver: true }).start() onReject() } else { Animated.spring(x, { toValue: 0, useNativeDriver: true }).start() } } else { x.setValue(nativeEvent.translationX) } } return ( ✓ 승인 ✕ 반려 x.setValue(nativeEvent.translationX)}> {item.title} {item.requester} · {item.created_at?.slice(0, 10)} ) } const s = StyleSheet.create({ container: { flex: 1, backgroundColor: COLORS.bg }, tabs: { flexDirection: 'row', backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: COLORS.border }, tab: { flex: 1, paddingVertical: 12, alignItems: 'center' }, tabActive: { borderBottomWidth: 2, borderBottomColor: COLORS.accent }, tabText: { fontSize: 14, color: COLORS.muted }, tabTextActive: { color: COLORS.accent, fontWeight: '700' }, bulkBtn: { margin: 8, backgroundColor: COLORS.accent, borderRadius: 8, padding: 10, alignItems: 'center' }, bulkBtnText: { color: '#fff', fontWeight: '700' }, cardWrap: { marginHorizontal: 12, marginVertical: 4, borderRadius: 10, overflow: 'hidden', height: 80 }, swipeBg: { position: 'absolute', top: 0, bottom: 0, left: 0, width: 80, justifyContent: 'center', alignItems: 'center' }, swipeHint: { color: '#fff', fontWeight: '700', fontSize: 12 }, card: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', padding: 14, borderRadius: 10, height: 80, elevation: 1 }, selectBox: { marginRight: 10 }, checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, borderColor: COLORS.border }, checkboxSelected: { backgroundColor: COLORS.accent, borderColor: COLORS.accent }, title: { fontSize: 14, fontWeight: '600', color: COLORS.text }, meta: { fontSize: 12, color: COLORS.muted, marginTop: 2 }, empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 }, snack: { position: 'absolute', bottom: 20, left: 20, right: 20, backgroundColor: '#1E293B', borderRadius: 8, padding: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, snackText: { color: '#fff', fontSize: 14 }, snackUndo: { color: COLORS.accent, fontWeight: '700' }, })