107 lines
4.9 KiB
TypeScript
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 },
|
|
})
|