275 lines
11 KiB
TypeScript
275 lines
11 KiB
TypeScript
import { Head, Link, router } from '@inertiajs/react';
|
|
import { format } from 'date-fns';
|
|
import { ClipboardCheck, Pencil, Plus, Trash2, X } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
|
|
import { PageHeader } from '@/components/page-header';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { destroy, session } from '@/routes/admin/academic-classes/attendances';
|
|
import type {
|
|
AttendanceCourseClass,
|
|
AttendanceSession,
|
|
} from '@/types/attendance';
|
|
|
|
type Props = {
|
|
sessions: AttendanceSession[];
|
|
courseClasses: AttendanceCourseClass[];
|
|
};
|
|
|
|
function courseClassLabel(courseClass: AttendanceCourseClass): string {
|
|
return `${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`;
|
|
}
|
|
|
|
export default function AttendanceIndex({ sessions, courseClasses }: Props) {
|
|
const [newSessionOpen, setNewSessionOpen] = useState(false);
|
|
const [deleting, setDeleting] = useState<AttendanceSession | null>(null);
|
|
|
|
function handleDelete() {
|
|
if (!deleting || deleting.meeting_number === null) {
|
|
return;
|
|
}
|
|
|
|
router.delete(
|
|
destroy([deleting.course_class_id, deleting.meeting_number]).url,
|
|
{ onSuccess: () => setDeleting(null) },
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head title="Kehadiran" />
|
|
|
|
<div className="flex h-full flex-1 flex-col gap-6 p-4 md:p-6">
|
|
<PageHeader
|
|
title="Kehadiran"
|
|
actions={
|
|
<Button onClick={() => setNewSessionOpen(true)}>
|
|
<Plus className="h-4 w-4" />
|
|
Ambil Kehadiran
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{sessions.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
Belum ada data kehadiran.
|
|
</p>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{sessions.map((item) => {
|
|
const key = `${item.course_class_id}-${item.meeting_number}-${item.date}`;
|
|
const canOpen = item.meeting_number !== null;
|
|
|
|
return (
|
|
<Card key={key}>
|
|
<CardHeader className="flex flex-row items-start justify-between gap-2">
|
|
<CardTitle className="text-sm leading-tight">
|
|
{item.course_class
|
|
? courseClassLabel(
|
|
item.course_class,
|
|
)
|
|
: '-'}
|
|
</CardTitle>
|
|
<Badge variant="secondary">
|
|
{item.present_count}/
|
|
{item.total_count} Hadir
|
|
</Badge>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col gap-2">
|
|
<p className="text-xs text-muted-foreground">
|
|
Pertemuan ke-
|
|
{item.meeting_number ?? '-'}{' '}
|
|
·{' '}
|
|
{format(
|
|
new Date(item.date),
|
|
'd MMM yyyy',
|
|
)}
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
asChild
|
|
disabled={!canOpen}
|
|
>
|
|
{canOpen ? (
|
|
<Link
|
|
href={
|
|
session([
|
|
item.course_class_id,
|
|
item.meeting_number as number,
|
|
]).url
|
|
}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
Lihat / Edit
|
|
</Link>
|
|
) : (
|
|
<span>
|
|
<Pencil className="h-4 w-4" />
|
|
Lihat / Edit
|
|
</span>
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={!canOpen}
|
|
onClick={() =>
|
|
setDeleting(item)
|
|
}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<NewSessionDialog
|
|
open={newSessionOpen}
|
|
onOpenChange={setNewSessionOpen}
|
|
courseClasses={courseClasses}
|
|
/>
|
|
|
|
<DeleteConfirmDialog
|
|
target={deleting}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setDeleting(null);
|
|
}
|
|
}}
|
|
title="Hapus Sesi Kehadiran"
|
|
description={(item) =>
|
|
`Apakah Anda yakin ingin menghapus seluruh data kehadiran pertemuan ke-${item.meeting_number} untuk kelas ini? Tindakan ini tidak dapat dibatalkan.`
|
|
}
|
|
onConfirm={handleDelete}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function NewSessionDialog({
|
|
open,
|
|
onOpenChange,
|
|
courseClasses,
|
|
}: {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
courseClasses: AttendanceCourseClass[];
|
|
}) {
|
|
const [courseClassId, setCourseClassId] = useState('');
|
|
const [meetingNumber, setMeetingNumber] = useState('');
|
|
|
|
function reset() {
|
|
setCourseClassId('');
|
|
setMeetingNumber('');
|
|
}
|
|
|
|
function handleOpenSession() {
|
|
if (!courseClassId || !meetingNumber) {
|
|
return;
|
|
}
|
|
|
|
router.get(session([Number(courseClassId), Number(meetingNumber)]).url);
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(value) => {
|
|
onOpenChange(value);
|
|
|
|
if (!value) {
|
|
reset();
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Ambil Kehadiran</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid gap-2">
|
|
<Label>
|
|
Kelas <span className="text-destructive">*</span>
|
|
</Label>
|
|
<Select
|
|
value={courseClassId}
|
|
onValueChange={setCourseClassId}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Pilih kelas" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{courseClasses.map((courseClass) => (
|
|
<SelectItem
|
|
key={courseClass.id}
|
|
value={String(courseClass.id)}
|
|
>
|
|
{courseClassLabel(courseClass)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="grid gap-2">
|
|
<Label htmlFor="meeting_number">
|
|
Pertemuan Ke-{' '}
|
|
<span className="text-destructive">*</span>
|
|
</Label>
|
|
<Input
|
|
id="meeting_number"
|
|
type="number"
|
|
min={1}
|
|
placeholder="Contoh: 1"
|
|
value={meetingNumber}
|
|
onChange={(e) => setMeetingNumber(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
Batal
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
disabled={!courseClassId || !meetingNumber}
|
|
onClick={handleOpenSession}
|
|
>
|
|
<ClipboardCheck className="h-4 w-4" />
|
|
Buka Kehadiran
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|