197 lines
10 KiB
TypeScript
197 lines
10 KiB
TypeScript
import { Head, router } from '@inertiajs/react';
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
import AppLayout from '@/layouts/app-layout';
|
|
import { Search } from 'lucide-react';
|
|
import { useDebounce } from 'use-debounce';
|
|
import * as XLSX from 'xlsx';
|
|
import { AttendanceMapModal } from '@/components/AttendanceMapModal';
|
|
|
|
interface Attendance {
|
|
id: number;
|
|
employee_name: string;
|
|
employee_nik: string;
|
|
date: string;
|
|
check_in: string | null;
|
|
check_out: string | null;
|
|
status: 'present' | 'leave' | 'dispensation';
|
|
notes: string | null;
|
|
total_hours: string | null;
|
|
latitude_in?: string | null;
|
|
longitude_in?: string | null;
|
|
latitude_out?: string | null;
|
|
longitude_out?: string | null;
|
|
}
|
|
|
|
interface PageProps {
|
|
attendances: Attendance[];
|
|
filters: { search?: string; date?: string; status?: string };
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export default function Index({ attendances, filters }: PageProps) {
|
|
const [search, setSearch] = useState(filters.search || '');
|
|
const [date, setDate] = useState(filters.date || '');
|
|
const [status, setStatus] = useState(filters.status || 'all');
|
|
const [debouncedSearch] = useDebounce(search, 500);
|
|
|
|
useEffect(() => {
|
|
if (debouncedSearch !== filters.search || date !== filters.date || (status !== 'all' && status !== filters.status)) {
|
|
router.get(
|
|
'/admin/attendances',
|
|
{ search: debouncedSearch, date, status: status === 'all' ? '' : status },
|
|
{ preserveState: true, replace: true }
|
|
);
|
|
}
|
|
}, [debouncedSearch, date, status]);
|
|
|
|
const exportExcel = () => {
|
|
const rows = attendances.map((a, i) => ({
|
|
No: i + 1,
|
|
Nama: a.employee_name,
|
|
NIK: a.employee_nik,
|
|
Tanggal: a.date,
|
|
'Jam Masuk': a.check_in || '-',
|
|
'Jam Keluar': a.check_out || '-',
|
|
Status: a.status === 'present' ? 'Hadir' : a.status === 'leave' ? 'Izin/Cuti' : 'Dispensasi',
|
|
'Total Jam': a.total_hours ? `${a.total_hours} Jam` : '-',
|
|
Catatan: a.notes || '-',
|
|
}));
|
|
const ws = XLSX.utils.json_to_sheet(rows);
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Absensi');
|
|
XLSX.writeFile(wb, `Data_Absensi_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
};
|
|
const formatStatus = (status: string) => {
|
|
switch (status) {
|
|
case 'present': return <span className="text-green-600 font-medium">Hadir</span>;
|
|
case 'leave': return <span className="text-yellow-600 font-medium">Izin/Cuti</span>;
|
|
case 'dispensation': return <span className="text-blue-600 font-medium">Dispensasi</span>;
|
|
default: return <span>{status}</span>;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AppLayout>
|
|
<Head title="Rekap 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">Rekap Absensi Karyawan</h2>
|
|
<p className="text-muted-foreground">Monitoring kehadiran, izin, dan dispensasi karyawan.</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
|
<CardTitle>Daftar Absensi</CardTitle>
|
|
<Button variant="outline" onClick={exportExcel}>Export Excel</Button>
|
|
</CardHeader>
|
|
<Separator />
|
|
|
|
<div className="p-4 flex flex-col md:flex-row gap-4 items-end">
|
|
<div className="w-full md:w-1/3 space-y-1">
|
|
<label className="text-xs text-muted-foreground font-medium">Cari Karyawan</label>
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
className="pl-8"
|
|
placeholder="Nama / NIK..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="w-full md:w-1/4 space-y-1">
|
|
<label className="text-xs text-muted-foreground font-medium">Tanggal</label>
|
|
<Input
|
|
type="date"
|
|
value={date}
|
|
onChange={(e) => setDate(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="w-full md:w-1/4 space-y-1">
|
|
<label className="text-xs text-muted-foreground font-medium">Status</label>
|
|
<Select value={status} onValueChange={(val) => setStatus(val)}>
|
|
<SelectTrigger><SelectValue placeholder="Semua Status" /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Semua Status</SelectItem>
|
|
<SelectItem value="present">Hadir</SelectItem>
|
|
<SelectItem value="leave">Izin / Cuti</SelectItem>
|
|
<SelectItem value="dispensation">Dispensasi</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="md:ml-auto">
|
|
<Button variant="ghost" onClick={() => { setSearch(''); setDate(''); setStatus('all'); }}>Reset</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<CardContent className="pt-0">
|
|
<div className="relative w-full overflow-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Nama</TableHead>
|
|
<TableHead>NIK</TableHead>
|
|
<TableHead>Tanggal</TableHead>
|
|
<TableHead>Jam Masuk</TableHead>
|
|
<TableHead>Jam Keluar</TableHead>
|
|
<TableHead>Lokasi Peta</TableHead>
|
|
<TableHead>Keterangan</TableHead>
|
|
<TableHead className="text-right">Total Jam Kerja</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{attendances.length > 0 ? (
|
|
attendances.map((attendance) => (
|
|
<TableRow key={attendance.id}>
|
|
<TableCell className="font-medium text-gray-900">{attendance.employee_name}</TableCell>
|
|
<TableCell className="text-gray-600">{attendance.employee_nik}</TableCell>
|
|
<TableCell className="text-gray-600">{attendance.date}</TableCell>
|
|
<TableCell>{attendance.check_in || '-'}</TableCell>
|
|
<TableCell>{attendance.check_out || '-'}</TableCell>
|
|
<TableCell>
|
|
<AttendanceMapModal
|
|
latitudeIn={attendance.latitude_in}
|
|
longitudeIn={attendance.longitude_in}
|
|
latitudeOut={attendance.latitude_out}
|
|
longitudeOut={attendance.longitude_out}
|
|
employeeName={attendance.employee_name}
|
|
date={attendance.date}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex flex-col">
|
|
{formatStatus(attendance.status)}
|
|
{attendance.notes && (
|
|
<span className="text-xs text-gray-500 mt-1">{attendance.notes}</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-right font-medium">
|
|
{attendance.total_hours ? `${attendance.total_hours} Jam` : '-'}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={8} className="h-24 text-center text-muted-foreground">
|
|
Belum ada data absensi.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|