guardia-hrm/frontend/src/pages/DashboardPage.tsx
DESKTOP-TKLFCPR\ython f7aa51c33f feat(hrm): GUARDiA HRM v1.0 AI 인사관리 플랫폼 (사원/조직/급여/근태/성과/채용/교육/AI분석)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 13:52:49 +09:00

141 lines
6.6 KiB
TypeScript

import React, { useEffect, useState } from 'react'
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'
import api from '../api/client'
const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6']
export default function DashboardPage() {
const [data, setData] = useState<any>(null)
const [insights, setInsights] = useState<any>(null)
useEffect(() => {
api.get('/dashboard').then(r => setData(r.data.data)).catch(() => {})
api.get('/ai/insights').then(r => setInsights(r.data.data)).catch(() => {})
}, [])
const kpi = data?.kpi || {}
const trend = data?.headcountTrend || []
const deptData = data?.deptHeadcount || []
const att = data?.attendance || {}
const kpiCards = [
{ label: '전체 사원', value: kpi.total_employees || 0, icon: '👥', color: 'text-blue-600', bg: 'bg-blue-50' },
{ label: '금년 신규입사', value: kpi.new_hires_ytd || 0, icon: '✅', color: 'text-green-600', bg: 'bg-green-50' },
{ label: '채용 진행중', value: kpi.open_positions || 0, icon: '🔍', color: 'text-orange-600', bg: 'bg-orange-50' },
{ label: '승인 대기', value: (kpi.pending_leaves || 0) + (kpi.pending_reviews || 0), icon: '⏳', color: 'text-red-600', bg: 'bg-red-50' },
]
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-slate-800"> </h1>
<span className="text-sm text-slate-500">{new Date().toLocaleDateString('ko-KR', { year:'numeric', month:'long', day:'numeric' })}</span>
</div>
{/* KPI */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{kpiCards.map(c => (
<div key={c.label} className="card">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl ${c.bg} flex items-center justify-center text-xl`}>{c.icon}</div>
<div>
<p className="text-xs text-slate-500">{c.label}</p>
<p className={`text-2xl font-bold ${c.color}`}>{c.value.toLocaleString()}</p>
</div>
</div>
</div>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* 인원 추이 */}
<div className="card">
<h2 className="text-base font-semibold text-slate-700 mb-4"> (6)</h2>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={trend}>
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip />
<Bar dataKey="headcount" fill="#3b82f6" radius={[4,4,0,0]} />
</BarChart>
</ResponsiveContainer>
</div>
{/* 부서별 인원 */}
<div className="card">
<h2 className="text-base font-semibold text-slate-700 mb-4"> </h2>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie data={deptData} dataKey="headcount" nameKey="dept_name" cx="50%" cy="50%" outerRadius={80} label>
{deptData.map((_: any, i: number) => <Cell key={i} fill={COLORS[i % COLORS.length]} />)}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
{/* 오늘 출결 */}
<div className="card">
<h2 className="text-base font-semibold text-slate-700 mb-4"> </h2>
<div className="space-y-3">
{[
{ label: '출근', value: att.present_count || 0, color: 'bg-green-500' },
{ label: '결근', value: att.absent_count || 0, color: 'bg-red-500' },
{ label: '휴가', value: att.leave_count || 0, color: 'bg-yellow-500' },
].map(r => (
<div key={r.label} className="flex items-center gap-3">
<span className="w-12 text-sm text-slate-600">{r.label}</span>
<div className="flex-1 bg-slate-100 rounded-full h-3 overflow-hidden">
<div className={`${r.color} h-3 rounded-full`} style={{ width: `${Math.min((r.value / Math.max(kpi.total_employees||1,1))*100, 100)}%` }} />
</div>
<span className="text-sm font-semibold text-slate-700 w-8 text-right">{r.value}</span>
</div>
))}
<p className="text-sm text-slate-500 mt-2">: <span className="font-bold text-green-600">{att.attendance_rate || 0}%</span></p>
</div>
</div>
{/* AI 인사이트 */}
<div className="card">
<h2 className="text-base font-semibold text-slate-700 mb-4">🤖 AI </h2>
<div className="space-y-3">
{(insights?.insights || []).map((ins: any, i: number) => (
<div key={i} className="flex items-start gap-3 p-3 bg-slate-50 rounded-lg">
<span className={`mt-0.5 px-2 py-0.5 rounded text-xs font-bold ${ins.priority==='HIGH'?'bg-red-100 text-red-700':ins.priority==='MEDIUM'?'bg-yellow-100 text-yellow-700':'bg-green-100 text-green-700'}`}>
{ins.priority}
</span>
<div>
<p className="text-sm font-medium text-slate-700">{ins.title}</p>
<p className="text-xs text-slate-500 mt-0.5">{ins.desc}</p>
</div>
</div>
))}
{!insights && <p className="text-sm text-slate-400">AI ...</p>}
</div>
</div>
</div>
{/* 승인 대기 */}
{(data?.pendingApprovals || []).length > 0 && (
<div className="card">
<h2 className="text-base font-semibold text-slate-700 mb-4"> </h2>
<div className="space-y-2">
{(data.pendingApprovals || []).map((a: any, i: number) => (
<div key={i} className="flex items-center justify-between py-2 border-b border-slate-100 last:border-0">
<div className="flex items-center gap-3">
<span className={`badge ${a.type==='leave'?'bg-blue-100 text-blue-700':'bg-purple-100 text-purple-700'}`}>
{a.type === 'leave' ? '휴가' : '성과평가'}
</span>
<span className="text-sm text-slate-700">{a.title}</span>
</div>
<span className="text-xs text-slate-400">{a.created_at?.slice(0,10)}</span>
</div>
))}
</div>
</div>
)}
</div>
)
}