siakad-itm/resources/js/pages/admin/academic-classes/attendances/session.tsx
Yoga Pangestu 5710a57992
Some checks failed
tests / ci (pull_request) Has been cancelled
refactor: remove class_name from CourseClass and related components
2026-08-30 13:14:53 +07:00

216 lines
8.4 KiB
TypeScript

import { Head, router } from '@inertiajs/react';
import { format } from 'date-fns';
import { ArrowLeft, CheckCircle2, Save, XCircle } from 'lucide-react';
import { useState } from 'react';
import { DatePicker } from '@/components/date-picker';
import { PageHeader } from '@/components/page-header';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import {
index as attendanceIndex,
store,
} from '@/routes/admin/academic-classes/attendances';
import type {
AttendanceCourseClass,
AttendanceRosterEntry,
AttendanceStatus,
} from '@/types/attendance';
import { AttendanceStatusLabels } from '@/types/attendance';
type Props = {
courseClass: AttendanceCourseClass;
meetingNumber: number;
roster: AttendanceRosterEntry[];
date: string | null;
};
function courseClassLabel(courseClass: AttendanceCourseClass): string {
return `${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`;
}
export default function AttendanceSession({
courseClass,
meetingNumber,
roster,
date,
}: Props) {
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
date ? new Date(date) : new Date(),
);
const [statuses, setStatuses] = useState<Record<number, AttendanceStatus>>(
() =>
Object.fromEntries(
roster.map((entry) => [entry.student_id, entry.status]),
),
);
const [saving, setSaving] = useState(false);
function setAll(status: AttendanceStatus) {
setStatuses(
Object.fromEntries(
roster.map((entry) => [entry.student_id, status]),
),
);
}
function handleSave() {
if (!selectedDate) {
return;
}
setSaving(true);
router.post(
store([courseClass.id, meetingNumber]).url,
{
date: format(selectedDate, 'yyyy-MM-dd'),
records: roster.map((entry) => ({
student_id: entry.student_id,
status: statuses[entry.student_id],
})),
},
{
preserveScroll: true,
onFinish: () => setSaving(false),
},
);
}
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 variant="outline" asChild>
<a href={attendanceIndex.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</a>
</Button>
}
/>
<Card>
<CardHeader>
<CardTitle>{courseClassLabel(courseClass)}</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<p className="text-sm text-muted-foreground">
Pertemuan ke-{meetingNumber}
</p>
<div className="grid max-w-xs gap-2">
<Label>Tanggal</Label>
<DatePicker
value={selectedDate}
onChange={setSelectedDate}
placeholder="Pilih tanggal"
/>
</div>
</CardContent>
</Card>
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Daftar Mahasiswa</h2>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setAll('present')}
>
<CheckCircle2 className="h-4 w-4" />
Tandai Semua Hadir
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setAll('absent')}
>
<XCircle className="h-4 w-4" />
Tandai Semua Tidak Hadir
</Button>
</div>
</div>
{roster.length === 0 ? (
<p className="rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground">
Belum ada mahasiswa yang terdaftar di kelas ini.
</p>
) : (
<div className="flex flex-col divide-y rounded-md border">
{roster.map((entry) => (
<div
key={entry.student_id}
className="flex items-center justify-between gap-4 px-4 py-3"
>
<div>
<p className="text-sm font-medium">
{entry.student?.user?.profile
?.full_name ?? 'N/A'}
</p>
<p className="text-xs text-muted-foreground">
{entry.student?.student_number}
</p>
</div>
<div className="flex items-center gap-1 rounded-md border p-1">
<Button
type="button"
size="sm"
variant={
statuses[entry.student_id] ===
'present'
? 'default'
: 'ghost'
}
onClick={() =>
setStatuses((prev) => ({
...prev,
[entry.student_id]: 'present',
}))
}
>
{AttendanceStatusLabels.present}
</Button>
<Button
type="button"
size="sm"
variant={
statuses[entry.student_id] ===
'absent'
? 'default'
: 'ghost'
}
onClick={() =>
setStatuses((prev) => ({
...prev,
[entry.student_id]: 'absent',
}))
}
>
{AttendanceStatusLabels.absent}
</Button>
</div>
</div>
))}
</div>
)}
{roster.length > 0 && (
<div className="flex justify-end">
<Button
onClick={handleSave}
disabled={saving || !selectedDate}
>
<Save className="h-4 w-4" />
{saving ? 'Menyimpan...' : 'Simpan Kehadiran'}
</Button>
</div>
)}
</div>
</>
);
}