202 lines
10 KiB
TypeScript
202 lines
10 KiB
TypeScript
import { Head, Link } from '@inertiajs/react';
|
|
import AppLayout from '@/layouts/app-layout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Users, Building2, Briefcase, UserPlus, Banknote } from 'lucide-react';
|
|
import {
|
|
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer,
|
|
PieChart, Pie, Cell, Legend
|
|
} from 'recharts';
|
|
|
|
interface DashboardProps {
|
|
stats: {
|
|
total_employees: number;
|
|
total_departments: number;
|
|
total_positions: number;
|
|
payroll_realisasi: number;
|
|
};
|
|
genderData: { name: string; value: number; fill: string }[];
|
|
deptData: { name: string; employees: number }[];
|
|
latestEmployees: {
|
|
id: number;
|
|
join_date: string;
|
|
user: { name: string };
|
|
position: { name: string };
|
|
department: { name: string };
|
|
}[];
|
|
currentPeriod: string;
|
|
}
|
|
|
|
function formatRupiah(value: number): string {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
minimumFractionDigits: 0,
|
|
}).format(value);
|
|
}
|
|
|
|
function formatPeriod(period: string): string {
|
|
const [year, month] = period.split('-');
|
|
return new Date(Number(year), Number(month) - 1, 1)
|
|
.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' });
|
|
}
|
|
|
|
export default function Dashboard({ stats, genderData, deptData, latestEmployees, currentPeriod }: DashboardProps) {
|
|
return (
|
|
<AppLayout breadcrumbs={[{ title: 'Dashboard', href: '/dashboard' }]}>
|
|
<Head title="Dashboard" />
|
|
|
|
<div className="flex flex-1 flex-col gap-4 p-4 md:p-8 pt-0">
|
|
|
|
{/* CARD STATISTIK */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Karyawan</CardTitle>
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.total_employees}</div>
|
|
<p className="text-xs text-muted-foreground">Pegawai aktif saat ini</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Departemen</CardTitle>
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.total_departments}</div>
|
|
<p className="text-xs text-muted-foreground">Divisi terdaftar</p>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Total Jabatan</CardTitle>
|
|
<Briefcase className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats.total_positions}</div>
|
|
<p className="text-xs text-muted-foreground">Posisi pekerjaan</p>
|
|
</CardContent>
|
|
</Card>
|
|
{/* Card Realisasi Gaji */}
|
|
<Card className="border-emerald-200 bg-emerald-50/50 dark:bg-emerald-950/20 dark:border-emerald-900">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium text-emerald-800 dark:text-emerald-300">
|
|
Realisasi Gaji Bulan Ini
|
|
</CardTitle>
|
|
<Banknote className="h-4 w-4 text-emerald-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-xl font-bold text-emerald-700 dark:text-emerald-400 leading-tight">
|
|
{formatRupiah(stats.payroll_realisasi)}
|
|
</div>
|
|
<p className="text-xs text-emerald-600/80 dark:text-emerald-500 mt-1">
|
|
Status "Telah Dikirim" · {formatPeriod(currentPeriod)}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
|
|
|
{/* Grafik Batang */}
|
|
<Card className="col-span-4">
|
|
<CardHeader>
|
|
<CardTitle>Karyawan per Departemen</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pl-2">
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<BarChart data={deptData}>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} />
|
|
<XAxis
|
|
dataKey="name"
|
|
stroke="#888888"
|
|
fontSize={12}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
/>
|
|
<YAxis
|
|
stroke="#888888"
|
|
fontSize={12}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value) => `${value}`}
|
|
/>
|
|
<RechartsTooltip
|
|
cursor={{ fill: 'transparent' }}
|
|
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
|
/>
|
|
<Bar dataKey="employees" fill="#0f172a" radius={[4, 4, 0, 0]} barSize={40} />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Grafik Donat*/}
|
|
<Card className="col-span-3">
|
|
<CardHeader>
|
|
<CardTitle>Komposisi Gender</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[300px] w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={genderData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={60}
|
|
outerRadius={80}
|
|
paddingAngle={5}
|
|
dataKey="value"
|
|
>
|
|
{genderData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.fill} />
|
|
))}
|
|
</Pie>
|
|
<RechartsTooltip />
|
|
<Legend verticalAlign="bottom" height={36}/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* TABEL KARYAWAN TERBARU */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>Karyawan Terbaru Bergabung</CardTitle>
|
|
<Link href="/admin/employees" className="text-sm text-blue-600 hover:underline">
|
|
Lihat Semua
|
|
</Link>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-8">
|
|
{latestEmployees.length === 0 ? (
|
|
<p className="text-sm text-center text-muted-foreground py-4">Belum ada data karyawan.</p>
|
|
) : latestEmployees.map((emp) => (
|
|
<div key={emp.id} className="flex items-center">
|
|
<div className="h-9 w-9 rounded-full bg-slate-100 flex items-center justify-center border">
|
|
<UserPlus className="h-5 w-5 text-slate-500" />
|
|
</div>
|
|
<div className="ml-4 space-y-1">
|
|
<p className="text-sm font-medium leading-none">{emp.user?.name ?? '-'}</p>
|
|
<p className="text-xs text-muted-foreground">{emp.position?.name ?? '-'} • {emp.department?.name ?? '-'}</p>
|
|
</div>
|
|
<div className="ml-auto font-medium text-xs text-muted-foreground">
|
|
{new Date(emp.join_date).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' })}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
} |