202 lines
8.3 KiB
TypeScript
202 lines
8.3 KiB
TypeScript
import { useEffect, useState } from 'react'
|
||
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, RefreshControl, ActivityIndicator } from 'react-native'
|
||
import { COLORS, PRIORITY_COLOR } from '../../constants/Config'
|
||
import { getDashboard, getLicenseStatus } from '../../services/api'
|
||
import { useAuth } from '../../hooks/useAuth'
|
||
import LineIcon from '../../components/LineIcon'
|
||
|
||
interface Stats {
|
||
total_tasks: number
|
||
open_tasks: number
|
||
in_progress_tasks: number
|
||
completed_today: number
|
||
critical_count: number
|
||
pending_approvals: number
|
||
recent_tasks?: any[]
|
||
}
|
||
|
||
type IconName = Parameters<typeof LineIcon>[0]['name']
|
||
|
||
/* Variant 스타일 StatCard — screenshot9 패턴 */
|
||
function StatCard({ iconName, label, value, color }: { iconName: IconName; label: string; value: number | string; color: string }) {
|
||
return (
|
||
<View style={[s.statCard, { borderTopColor: color, borderTopWidth: 3 }]}>
|
||
{/* 아이콘 박스 — 연파랑 컨테이너 */}
|
||
<View style={[s.statIconBox, { backgroundColor: color + '18' }]}>
|
||
<LineIcon name={iconName} size={22} color={color} />
|
||
</View>
|
||
{/* 라벨 (위) */}
|
||
<Text style={s.statLabel}>{label}</Text>
|
||
{/* 수치 (아래) */}
|
||
<Text style={[s.statValue, { color }]}>{value}</Text>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
export default function Dashboard() {
|
||
const { user } = useAuth()
|
||
const [stats, setStats] = useState<Stats | null>(null)
|
||
const [license, setLicense] = useState<any>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
const [refresh, setRefresh] = useState(false)
|
||
|
||
const load = async (isRefresh = false) => {
|
||
if (isRefresh) setRefresh(true)
|
||
else setLoading(true)
|
||
try {
|
||
const [d, l] = await Promise.allSettled([getDashboard(), getLicenseStatus()])
|
||
if (d.status === 'fulfilled') setStats(d.value.data)
|
||
if (l.status === 'fulfilled') setLicense(l.value.data)
|
||
} catch {}
|
||
setLoading(false)
|
||
setRefresh(false)
|
||
}
|
||
|
||
useEffect(() => { load() }, [])
|
||
|
||
if (loading) return (
|
||
<View style={s.center}>
|
||
<ActivityIndicator size="large" color={COLORS.accent} />
|
||
<Text style={{ color: COLORS.muted, marginTop: 12 }}>로딩 중...</Text>
|
||
</View>
|
||
)
|
||
|
||
return (
|
||
<ScrollView
|
||
style={s.scroll}
|
||
refreshControl={<RefreshControl refreshing={refresh} onRefresh={() => load(true)} />}
|
||
>
|
||
{/* 인사말 */}
|
||
<View style={s.header}>
|
||
<Text style={s.greeting}>안녕하세요, {user?.display_name ?? user?.username}님 👋</Text>
|
||
<Text style={s.subGreet}>GUARDiA ITSM 현황을 확인하세요</Text>
|
||
</View>
|
||
|
||
{/* 라이선스 배너 */}
|
||
{license?.upgrade_banner?.show && (
|
||
<View style={s.licenseBanner}>
|
||
<Text style={s.licenseBannerText}>⚠️ {license.upgrade_banner.message}</Text>
|
||
</View>
|
||
)}
|
||
{license?.valid && (
|
||
<View style={s.licenseBar}>
|
||
<Text style={s.licenseEdition}>{license.edition}</Text>
|
||
<Text style={s.licenseDays}>{license.days_remaining}일 남음</Text>
|
||
</View>
|
||
)}
|
||
|
||
{/* 통계 카드 */}
|
||
<View style={s.statsGrid}>
|
||
<StatCard iconName="sr" label="전체 SR" value={stats?.total_tasks ?? '-'} color={COLORS.accent} />
|
||
<StatCard iconName="sync" label="진행 중" value={stats?.in_progress_tasks ?? '-'} color={COLORS.warning} />
|
||
<StatCard iconName="alert" label="긴급" value={stats?.critical_count ?? '-'} color={COLORS.danger} />
|
||
<StatCard iconName="check" label="오늘 완료" value={stats?.completed_today ?? '-'} color={COLORS.success} />
|
||
</View>
|
||
|
||
{/* 최근 SR */}
|
||
{stats?.recent_tasks && stats.recent_tasks.length > 0 && (
|
||
<View style={s.section}>
|
||
<Text style={s.sectionTitle}>최근 서비스 요청</Text>
|
||
{stats.recent_tasks.slice(0, 5).map((sr: any) => (
|
||
<View key={sr.id} style={s.srItem}>
|
||
<View style={[s.priorityDot, { backgroundColor: PRIORITY_COLOR[sr.priority] ?? COLORS.muted }]} />
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={s.srTitle} numberOfLines={1}>{sr.title}</Text>
|
||
<Text style={s.srMeta}>{sr.sr_id} · {sr.status}</Text>
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
)}
|
||
|
||
{/* 빠른 실행 */}
|
||
<View style={s.section}>
|
||
<Text style={s.sectionTitle}>빠른 실행</Text>
|
||
<View style={s.quickRow}>
|
||
{[
|
||
{ iconName: 'sr' as const, label: 'SR 등록' },
|
||
{ iconName: 'ai' as const, label: 'AI 질문' },
|
||
{ iconName: 'dashboard'as const, label: '리포트' },
|
||
{ iconName: 'lock' as const, label: '감사로그' },
|
||
].map(q => (
|
||
<TouchableOpacity key={q.label} style={s.quickBtn}>
|
||
<View style={s.quickIconBox}>
|
||
<LineIcon name={q.iconName} size={20} color={COLORS.accent} />
|
||
</View>
|
||
<Text style={s.quickLabel}>{q.label}</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
<View style={{ height: 24 }} />
|
||
</ScrollView>
|
||
)
|
||
}
|
||
|
||
const s = StyleSheet.create({
|
||
scroll: { flex: 1, backgroundColor: COLORS.bg },
|
||
center: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: COLORS.bg },
|
||
/* 헤더 — Variant 딥네이비 그라디언트 */
|
||
header: {
|
||
backgroundColor: '#001530',
|
||
padding: 24, paddingTop: 20,
|
||
borderBottomLeftRadius: 20, borderBottomRightRadius: 20,
|
||
},
|
||
greeting: { fontSize: 20, fontWeight: '800', color: '#fff', letterSpacing: -0.5 },
|
||
subGreet: { fontSize: 13, color: 'rgba(0,160,200,.85)', marginTop: 4 },
|
||
licenseBanner: { backgroundColor: '#fff3cd', padding: 12, marginHorizontal: 16, marginTop: 12,
|
||
borderRadius: 10, borderLeftWidth: 3, borderLeftColor: COLORS.warning },
|
||
licenseBannerText: { fontSize: 12, color: '#856404' },
|
||
licenseBar: {
|
||
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
|
||
backgroundColor: COLORS.light, marginHorizontal: 16, marginTop: 10,
|
||
paddingHorizontal: 14, paddingVertical: 8, borderRadius: 10,
|
||
borderWidth: 1, borderColor: 'rgba(0,160,200,.2)',
|
||
},
|
||
licenseEdition: { fontSize: 12, fontWeight: '700', color: COLORS.accent },
|
||
licenseDays: { fontSize: 12, color: COLORS.muted },
|
||
/* 통계 그리드 */
|
||
statsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 14, gap: 10 },
|
||
/* Variant StatCard — 상단 컬러 바 + 아이콘 박스 */
|
||
statCard: {
|
||
width: '47%', backgroundColor: '#fff', borderRadius: 12, padding: 14,
|
||
shadowColor: '#003366', shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: .08, shadowRadius: 8, elevation: 3,
|
||
},
|
||
statIconBox: { width: 40, height: 40, borderRadius: 10, marginBottom: 10,
|
||
alignItems: 'center', justifyContent: 'center' },
|
||
statIcon: { fontSize: 20 },
|
||
statValue: { fontSize: 28, fontWeight: '900', letterSpacing: -0.5 },
|
||
statLabel: { fontSize: 11, fontWeight: '600', color: COLORS.muted,
|
||
letterSpacing: 0.3, textTransform: 'uppercase', marginBottom: 4 },
|
||
/* 섹션 */
|
||
section: {
|
||
backgroundColor: '#fff', marginHorizontal: 16, marginTop: 12,
|
||
borderRadius: 14, padding: 16,
|
||
shadowColor: '#003366', shadowOffset: { width: 0, height: 2 },
|
||
shadowOpacity: .05, shadowRadius: 6, elevation: 2,
|
||
},
|
||
sectionTitle: {
|
||
fontSize: 14, fontWeight: '800', color: COLORS.primary,
|
||
marginBottom: 12, letterSpacing: -0.3,
|
||
},
|
||
srItem: { flexDirection: 'row', alignItems: 'center', gap: 10,
|
||
paddingVertical: 9, borderBottomWidth: 1, borderBottomColor: '#f1f5f9' },
|
||
priorityDot: { width: 8, height: 8, borderRadius: 4 },
|
||
srTitle: { fontSize: 13, fontWeight: '600', color: COLORS.text },
|
||
srMeta: { fontSize: 11, color: COLORS.muted, marginTop: 2 },
|
||
/* 빠른 실행 */
|
||
quickRow: { flexDirection: 'row', justifyContent: 'space-around' },
|
||
quickBtn: {
|
||
alignItems: 'center', padding: 12,
|
||
backgroundColor: COLORS.bg, borderRadius: 12,
|
||
flex: 1, marginHorizontal: 3,
|
||
},
|
||
quickIconBox: { width: 36, height: 36, borderRadius: 10, marginBottom: 5,
|
||
backgroundColor: 'rgba(0,160,200,.1)',
|
||
alignItems: 'center', justifyContent: 'center' },
|
||
quickIcon: { fontSize: 26, marginBottom: 5 },
|
||
quickLabel: { fontSize: 11, fontWeight: '600', color: COLORS.primary },
|
||
})
|