161 lines
7.9 KiB
TypeScript
161 lines
7.9 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, RefreshControl } from 'react-native'
|
|
import { router, useFocusEffect } from 'expo-router'
|
|
import { Feather } from '@expo/vector-icons'
|
|
import { useTheme } from '../../constants/ThemeContext'
|
|
import { type ThemeTokens } from '../../constants/ItmsTheme'
|
|
import { StatusPill, SectionTitle } from '../../components/ui'
|
|
import { rowTitle, rowStatus, rowDate, reqIncidentNum } from '../../constants/rows'
|
|
import {
|
|
itmsProfile, itmsMyIncidents, itmsMyReports, itmsMyTodo, itmsResourceStat, listRows,
|
|
type ItmsProfile, type RowMap,
|
|
} from '../itmsApi'
|
|
|
|
export default function Home() {
|
|
const { t } = useTheme()
|
|
const s = makeStyles(t)
|
|
const [me, setMe] = useState<ItmsProfile | null>(null)
|
|
const [incidents, setIncidents] = useState<RowMap[]>([])
|
|
const [reports, setReports] = useState<RowMap[]>([])
|
|
const [todo, setTodo] = useState<RowMap[]>([])
|
|
const [statRows, setStatRows] = useState<RowMap[]>([])
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
// 각 호출 독립 — 하나 실패해도 나머지 표시(서버 미기동 시 빈 상태).
|
|
try { setMe(await itmsProfile()) } catch { /* graceful */ }
|
|
try { setIncidents(listRows(await itmsMyIncidents())) } catch { setIncidents([]) }
|
|
try { setReports(listRows(await itmsMyReports())) } catch { setReports([]) }
|
|
try { setTodo(listRows(await itmsMyTodo())) } catch { setTodo([]) }
|
|
try { setStatRows(listRows(await itmsResourceStat())) } catch { setStatRows([]) }
|
|
}, [])
|
|
|
|
useFocusEffect(useCallback(() => { load() }, [load]))
|
|
const onRefresh = useCallback(async () => { setRefreshing(true); await load(); setRefreshing(false) }, [load])
|
|
|
|
const kpis = [
|
|
{ key: 'todo', label: '내 할 일', count: todo.length, color: t.brand, icon: 'check-square' as const },
|
|
{ key: 'inc', label: '내 인시던트', count: incidents.length, color: t.info, icon: 'alert-circle' as const },
|
|
{ key: 'rep', label: '내 보고', count: reports.length, color: t.warning, icon: 'file-text' as const },
|
|
]
|
|
|
|
const quick = [
|
|
{ key: 'new', icon: 'plus-circle' as const, label: '요청 등록', go: () => router.push('/request-new') },
|
|
{ key: 'req', icon: 'file-text' as const, label: '내 요청', go: () => router.push('/requests') },
|
|
{ key: 'inc', icon: 'alert-circle' as const, label: '인시던트', go: () => router.push('/incidents') },
|
|
{ key: 'asset', icon: 'server' as const, label: '자산 조회', go: () => router.push('/assets') },
|
|
]
|
|
|
|
return (
|
|
<ScrollView
|
|
style={s.wrap} contentContainerStyle={s.content}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={t.accent} />}
|
|
>
|
|
<View style={s.header}>
|
|
<View>
|
|
<Text style={s.greeting}>안녕하세요,</Text>
|
|
<Text style={s.userName}>{me?.userNm || 'ITMS'} 님</Text>
|
|
</View>
|
|
<TouchableOpacity onPress={() => router.push('/notices')} style={s.iconBtn}>
|
|
<Feather name="bell" size={20} color={t.text} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* KPI 3분할 */}
|
|
<View style={s.kpiRow}>
|
|
{kpis.map((k) => (
|
|
<View key={k.key} style={s.kpiTile}>
|
|
<View style={[s.kpiIcon, { backgroundColor: k.color + '1e' }]}>
|
|
<Feather name={k.icon} size={16} color={k.color} />
|
|
</View>
|
|
<Text style={[s.kpiCount, { color: k.color }]}>{k.count}</Text>
|
|
<Text style={s.kpiLbl} numberOfLines={1}>{k.label}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
|
|
{/* 자산 요약 통계(있을 때만) */}
|
|
{statRows.length > 0 && (
|
|
<View style={s.statCard}>
|
|
<SectionTitle>자산 현황</SectionTitle>
|
|
{statRows.slice(0, 6).map((r, i) => (
|
|
<View key={i} style={s.statRow}>
|
|
<Text style={s.statLbl} numberOfLines={1}>{rowTitle(r)}</Text>
|
|
<Text style={s.statVal}>{r.cnt ?? r.totCnt ?? r.count ?? r.value ?? '-'}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
{/* 퀵액션 */}
|
|
<View style={s.quickGrid}>
|
|
{quick.map((q) => (
|
|
<TouchableOpacity key={q.key} style={s.quickCell} onPress={q.go}>
|
|
<View style={s.quickIcon}><Feather name={q.icon} size={22} color={t.brand} /></View>
|
|
<Text style={s.quickLbl}>{q.label}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
|
|
{/* 내 할 일 */}
|
|
<SectionTitle style={s.mt}>내 할 일 ({todo.length})</SectionTitle>
|
|
{todo.length === 0 ? (
|
|
<Text style={s.empty}>표시할 항목이 없습니다.</Text>
|
|
) : todo.slice(0, 5).map((r, i) => (
|
|
<View key={i} style={s.rowCard}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={s.rowTitle} numberOfLines={1}>{rowTitle(r)}</Text>
|
|
<Text style={s.rowSub}>{rowDate(r)}</Text>
|
|
</View>
|
|
<StatusPill label={rowStatus(r)} />
|
|
</View>
|
|
))}
|
|
|
|
{/* 내 인시던트 */}
|
|
<SectionTitle style={s.mt}>내 인시던트 ({incidents.length})</SectionTitle>
|
|
{incidents.length === 0 ? (
|
|
<Text style={s.empty}>표시할 항목이 없습니다.</Text>
|
|
) : incidents.slice(0, 5).map((r, i) => (
|
|
<TouchableOpacity
|
|
key={i} style={s.rowCard}
|
|
onPress={() => router.push({ pathname: '/request-detail', params: { num: reqIncidentNum(r), raw: JSON.stringify(r) } })}
|
|
>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={s.rowTitle} numberOfLines={1}>{rowTitle(r)}</Text>
|
|
<Text style={s.rowSub}>{rowDate(r)}</Text>
|
|
</View>
|
|
<StatusPill label={rowStatus(r)} />
|
|
</TouchableOpacity>
|
|
))}
|
|
<View style={{ height: 24 }} />
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
const makeStyles = (t: ThemeTokens) => StyleSheet.create({
|
|
wrap: { flex: 1, backgroundColor: t.bg },
|
|
content: { padding: 16 },
|
|
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, marginTop: 8 },
|
|
greeting: { color: t.muted, fontSize: 13 },
|
|
userName: { color: t.text, fontSize: 20, fontWeight: '900', marginTop: 2 },
|
|
iconBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: t.card, borderWidth: 1, borderColor: t.border, justifyContent: 'center', alignItems: 'center' },
|
|
kpiRow: { flexDirection: 'row', gap: 10, marginBottom: 14 },
|
|
kpiTile: { flex: 1, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, paddingVertical: 14, alignItems: 'center', gap: 4 },
|
|
kpiIcon: { width: 30, height: 30, borderRadius: 15, justifyContent: 'center', alignItems: 'center', marginBottom: 2 },
|
|
kpiCount: { fontSize: 22, fontWeight: '900' },
|
|
kpiLbl: { color: t.muted, fontSize: 11, fontWeight: '600' },
|
|
statCard: { backgroundColor: t.card, borderRadius: 14, borderWidth: 1, borderColor: t.border, padding: 16, marginBottom: 14 },
|
|
statRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 6 },
|
|
statLbl: { color: t.muted, fontSize: 13, flex: 1, marginRight: 12 },
|
|
statVal: { color: t.text, fontSize: 14, fontWeight: '800' },
|
|
quickGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
|
|
quickCell: { width: '47.6%', backgroundColor: t.card, borderRadius: 14, borderWidth: 1, borderColor: t.border, paddingVertical: 18, alignItems: 'center', gap: 8 },
|
|
quickIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: t.brand + '18', justifyContent: 'center', alignItems: 'center' },
|
|
quickLbl: { color: t.text, fontSize: 13, fontWeight: '700' },
|
|
mt: { marginTop: 22 },
|
|
empty: { color: t.muted, fontSize: 13, paddingVertical: 12 },
|
|
rowCard: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: t.card, borderRadius: 12, borderWidth: 1, borderColor: t.border, padding: 13, marginBottom: 8 },
|
|
rowTitle: { color: t.text, fontSize: 14, fontWeight: '700' },
|
|
rowSub: { color: t.muted, fontSize: 12, marginTop: 2 },
|
|
})
|