guardia-messenger/app/(tabs)/csap_dashboard.tsx

107 lines
4.9 KiB
TypeScript

import React, { useState, useCallback } from 'react'
import {
View, Text, ScrollView, TouchableOpacity, StyleSheet, Alert, RefreshControl,
} from 'react-native'
import { useFocusEffect } from 'expo-router'
import { COLORS } from '../../constants/Config'
import { getCSAPDashboard, getCSAPItems, createSR } from '../../services/api'
function DonutGauge({ score }: { score: number }) {
const color = score >= 85 ? COLORS.success : score >= 70 ? COLORS.warning : COLORS.danger
return (
<View style={g.wrap}>
<View style={[g.ring, { borderColor: COLORS.border }]}>
<View style={[g.progress, { borderColor: color, transform: [{ rotate: `${score * 3.6}deg` }] }]} />
<View style={g.inner}>
<Text style={[g.score, { color }]}>{score}%</Text>
<Text style={g.label}>CSAP</Text>
</View>
</View>
</View>
)
}
const g = StyleSheet.create({
wrap: { alignItems: 'center', marginVertical: 20 },
ring: { width: 120, height: 120, borderRadius: 60, borderWidth: 12, justifyContent: 'center', alignItems: 'center' },
progress: { position: 'absolute', width: 120, height: 120, borderRadius: 60, borderWidth: 12, borderTopColor: 'transparent', borderRightColor: 'transparent' },
inner: { alignItems: 'center' },
score: { fontSize: 26, fontWeight: '800' },
label: { fontSize: 11, color: COLORS.muted, fontWeight: '600' },
})
export default function CSAPDashboardScreen() {
const [dashboard, setDashboard] = useState<any>(null)
const [items, setItems] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const load = useCallback(async () => {
setLoading(true)
try {
const [d, i] = await Promise.all([getCSAPDashboard(), getCSAPItems()])
setDashboard(d.data)
setItems((i.data?.items ?? i.data ?? []).filter((x: any) => x.status === 'non_compliant' || x.result === 'FAIL'))
} catch {} finally { setLoading(false) }
}, [])
useFocusEffect(useCallback(() => { load() }, [load]))
const createActionSR = async (item: any) => {
Alert.alert('즉시 조치 SR 등록', `"${item.title ?? item.check_item}" 미준수 항목으로 SR을 등록하시겠습니까?`, [
{ text: '취소', style: 'cancel' },
{ text: '등록', onPress: async () => {
try {
await createSR({ title: `[CSAP] ${item.title ?? item.check_item}`, description: item.description ?? '미준수 항목 조치 필요', priority: 'HIGH', sr_type: 'OTHER' })
Alert.alert('완료', 'SR이 등록됐습니다.')
} catch { Alert.alert('오류', 'SR 등록에 실패했습니다.') }
}},
])
}
const score = dashboard?.overall_score ?? dashboard?.compliance_rate ?? 0
return (
<ScrollView style={s.container} refreshControl={<RefreshControl refreshing={loading} onRefresh={load} />}>
<DonutGauge score={Math.round(score)} />
{/* 영역별 바 */}
{(dashboard?.domains ?? []).map((d: any, i: number) => (
<View key={i} style={s.domainRow}>
<Text style={s.domainName}>{d.name}</Text>
<View style={s.barBg}>
<View style={[s.barFill, { width: `${d.rate ?? 0}%`, backgroundColor: d.rate >= 80 ? COLORS.success : COLORS.warning }]} />
</View>
<Text style={s.domainRate}>{d.rate ?? 0}%</Text>
</View>
))}
{/* 미준수 항목 */}
<Text style={s.sectionTitle}> ({items.length})</Text>
{items.map((item, i) => (
<View key={i} style={s.card}>
<Text style={s.itemTitle} numberOfLines={2}>{item.title ?? item.check_item}</Text>
<Text style={s.itemDesc} numberOfLines={2}>{item.description ?? item.detail ?? '-'}</Text>
<TouchableOpacity style={s.srBtn} onPress={() => createActionSR(item)}>
<Text style={s.srBtnText}> SR </Text>
</TouchableOpacity>
</View>
))}
</ScrollView>
)
}
const s = StyleSheet.create({
container: { flex: 1, backgroundColor: COLORS.bg },
domainRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 6, gap: 8 },
domainName: { width: 80, fontSize: 12, color: COLORS.text },
barBg: { flex: 1, height: 8, backgroundColor: COLORS.border, borderRadius: 4 },
barFill: { height: 8, borderRadius: 4 },
domainRate: { width: 40, fontSize: 12, color: COLORS.muted, textAlign: 'right' },
sectionTitle: { fontSize: 15, fontWeight: '700', color: COLORS.text, padding: 16, paddingBottom: 8 },
card: { backgroundColor: '#fff', borderRadius: 10, margin: 12, marginTop: 0, padding: 14, elevation: 1 },
itemTitle: { fontSize: 14, fontWeight: '600', color: COLORS.text, marginBottom: 4 },
itemDesc: { fontSize: 12, color: COLORS.muted, marginBottom: 10 },
srBtn: { backgroundColor: COLORS.danger, borderRadius: 6, padding: 8, alignItems: 'center' },
srBtnText: { color: '#fff', fontWeight: '700', fontSize: 12 },
})