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

202 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 },
})