141 lines
6.6 KiB
TypeScript
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>
|
|
)
|
|
}
|