230 lines
9.7 KiB
TypeScript
230 lines
9.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { Head, Link, router, usePage } from '@inertiajs/react';
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useDebounce } from 'use-debounce';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import AppLayout from '@/layouts/app-layout';
|
|
|
|
interface Attendance {
|
|
id: number;
|
|
date: string;
|
|
shift: 'Pagi' | 'Siang' | 'Malam';
|
|
status: 'hadir' | 'izin' | 'sakit' | 'alpha';
|
|
check_in: string | null;
|
|
check_out: string | null;
|
|
notes: string | null;
|
|
employee: {
|
|
id: number;
|
|
name: string;
|
|
nip: string;
|
|
department: {
|
|
id: number;
|
|
name: string;
|
|
} | null;
|
|
};
|
|
}
|
|
|
|
interface PageProps {
|
|
attendances: {
|
|
data: Attendance[];
|
|
links: any[];
|
|
};
|
|
departments: { id: number; name: string }[];
|
|
filters: {
|
|
search?: string;
|
|
status?: string;
|
|
date?: string;
|
|
department_id?: string;
|
|
};
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface SharedData {
|
|
flash: {
|
|
success?: string;
|
|
error?: string;
|
|
};
|
|
}
|
|
|
|
const statusClass: Record<Attendance['status'], string> = {
|
|
hadir: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-100',
|
|
izin: 'bg-blue-100 text-blue-700 hover:bg-blue-100',
|
|
sakit: 'bg-amber-100 text-amber-700 hover:bg-amber-100',
|
|
alpha: 'bg-rose-100 text-rose-700 hover:bg-rose-100',
|
|
};
|
|
|
|
function formatDate(value: string): string {
|
|
return new Date(value).toLocaleDateString('id-ID', {
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
});
|
|
}
|
|
|
|
function formatTime(value: string | null): string {
|
|
if (!value) return '-';
|
|
return new Date(value).toLocaleTimeString('id-ID', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
export default function Index({ attendances, departments, filters }: PageProps) {
|
|
const { flash } = usePage<any>().props as SharedData;
|
|
|
|
const [search, setSearch] = useState(filters.search || '');
|
|
const [status, setStatus] = useState(filters.status || 'all');
|
|
const [departmentId, setDepartmentId] = useState(filters.department_id || 'all');
|
|
const [date, setDate] = useState(filters.date || '');
|
|
const [debouncedSearch] = useDebounce(search, 500);
|
|
|
|
useEffect(() => {
|
|
router.get(
|
|
'/admin/attendance',
|
|
{
|
|
search: debouncedSearch || '',
|
|
status: status === 'all' ? '' : status,
|
|
department_id: departmentId === 'all' ? '' : departmentId,
|
|
date,
|
|
},
|
|
{ preserveState: true, replace: true }
|
|
);
|
|
}, [debouncedSearch, status, departmentId, date]);
|
|
|
|
return (
|
|
<AppLayout>
|
|
<Head title="Manajemen Absensi" />
|
|
|
|
<div className="p-4 md:p-8 w-full space-y-4">
|
|
<div className="space-y-1">
|
|
<h2 className="text-2xl font-bold tracking-tight">Manajemen Absensi</h2>
|
|
<p className="text-muted-foreground">Pantau absensi harian karyawan berdasarkan tanggal, status, dan departemen.</p>
|
|
</div>
|
|
|
|
{flash?.success && (
|
|
<div className="p-4 bg-green-50 text-green-700 border border-green-200 rounded-md text-sm">
|
|
{flash.success}
|
|
</div>
|
|
)}
|
|
|
|
{flash?.error && (
|
|
<div className="p-4 bg-red-50 text-red-700 border border-red-200 rounded-md text-sm">
|
|
{flash.error}
|
|
</div>
|
|
)}
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
|
<CardTitle>Daftar Absensi</CardTitle>
|
|
<Button asChild>
|
|
<Link href="/admin/attendance/create">+ Input Absensi</Link>
|
|
</Button>
|
|
</CardHeader>
|
|
|
|
<Separator />
|
|
|
|
<div className="p-4 grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<Input
|
|
placeholder="Cari nama / NIP"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
|
|
<Input
|
|
type="date"
|
|
value={date}
|
|
onChange={(e) => setDate(e.target.value)}
|
|
/>
|
|
|
|
<Select value={status} onValueChange={setStatus}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Filter status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Semua Status</SelectItem>
|
|
<SelectItem value="hadir">Hadir</SelectItem>
|
|
<SelectItem value="izin">Izin</SelectItem>
|
|
<SelectItem value="sakit">Sakit</SelectItem>
|
|
<SelectItem value="alpha">Alpha</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={departmentId} onValueChange={setDepartmentId}>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Filter departemen" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Semua Departemen</SelectItem>
|
|
{departments.map((dept) => (
|
|
<SelectItem key={dept.id} value={String(dept.id)}>
|
|
{dept.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<CardContent className="pt-0">
|
|
<div className="relative w-full overflow-auto">
|
|
<table className="w-full text-sm text-left">
|
|
<thead className="bg-zinc-50/50 text-muted-foreground">
|
|
<tr className="border-b">
|
|
<th className="h-10 px-4 font-medium">Tanggal</th>
|
|
<th className="h-10 px-4 font-medium">Karyawan</th>
|
|
<th className="h-10 px-4 font-medium">Shift</th>
|
|
<th className="h-10 px-4 font-medium">Masuk</th>
|
|
<th className="h-10 px-4 font-medium">Pulang</th>
|
|
<th className="h-10 px-4 font-medium">Status</th>
|
|
<th className="h-10 px-4 font-medium">Catatan</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{attendances.data.length > 0 ? (
|
|
attendances.data.map((attendance) => (
|
|
<tr key={attendance.id} className="border-b hover:bg-zinc-50">
|
|
<td className="p-4">{formatDate(attendance.date)}</td>
|
|
<td className="p-4">
|
|
<div className="font-semibold">{attendance.employee.name}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
NIP: {attendance.employee.nip} | {attendance.employee.department?.name || '-'}
|
|
</div>
|
|
</td>
|
|
<td className="p-4">{attendance.shift}</td>
|
|
<td className="p-4">{formatTime(attendance.check_in)}</td>
|
|
<td className="p-4">{formatTime(attendance.check_out)}</td>
|
|
<td className="p-4">
|
|
<Badge className={statusClass[attendance.status]}>
|
|
{attendance.status.toUpperCase()}
|
|
</Badge>
|
|
</td>
|
|
<td className="p-4 text-muted-foreground">{attendance.notes || '-'}</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td colSpan={7} className="p-8 text-center text-muted-foreground">
|
|
Belum ada data absensi.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|