itmpwk.ac.id/resources/js/pages/admin/academic-classes/attendances/course-class.tsx

256 lines
9.8 KiB
TypeScript

import { Head, Link, router } from '@inertiajs/react';
import { format } from 'date-fns';
import { ArrowLeft, ClipboardCheck, Pencil, Plus, Trash2 } 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 { usePermissions } from '@/hooks/use-permissions';
import {
destroy,
index as attendanceIndex,
session,
} from '@/routes/admin/academic-classes/attendances';
import { formatAcademicTermLabel } from '@/types/academic-term';
import type {
AttendanceCourseClass,
AttendanceSession,
} from '@/types/attendance';
type Props = {
courseClass: AttendanceCourseClass;
sessions: AttendanceSession[];
nextMeetingNumber: number;
};
function courseClassLabel(courseClass: AttendanceCourseClass): string {
return `${courseClass.course?.code ?? ''} ${courseClass.course?.name ?? ''}`;
}
export default function AttendanceCourseClass({
courseClass,
sessions,
nextMeetingNumber,
}: Props) {
const [newSessionOpen, setNewSessionOpen] = useState(false);
const [deleting, setDeleting] = useState<AttendanceSession | null>(null);
const { hasPermission } = usePermissions();
const canCreate = hasPermission('create-attendances');
const canDelete = hasPermission('delete-attendances');
function handleDelete() {
if (!deleting) {
return;
}
router.delete(destroy([courseClass.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={courseClassLabel(courseClass)}
description={
courseClass.academic_term && (
<Badge variant="outline" className="mt-1 w-fit">
{formatAcademicTermLabel(
courseClass.academic_term,
)}
</Badge>
)
}
actions={
<div className="flex items-center gap-2">
<Button variant="outline" asChild>
<Link href={attendanceIndex.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
{canCreate && (
<Button onClick={() => setNewSessionOpen(true)}>
<Plus className="h-4 w-4" />
Tambah Pertemuan
</Button>
)}
</div>
}
/>
{sessions.length === 0 ? (
<p className="rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground">
Belum ada pertemuan yang tercatat.
</p>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{sessions.map((item) => (
<Card key={item.meeting_number}>
<CardHeader className="flex flex-row items-start justify-between gap-2">
<CardTitle className="text-sm leading-tight">
Pertemuan ke-{item.meeting_number}
</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">
{format(
new Date(item.date),
'd MMM yyyy',
)}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
asChild
>
<Link
href={
session([
courseClass.id,
item.meeting_number,
]).url
}
>
<Pencil className="h-4 w-4" />
Lihat / Edit
</Link>
</Button>
{canDelete && (
<Button
variant="ghost"
size="sm"
onClick={() =>
setDeleting(item)
}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
</CardContent>
</Card>
))}
</div>
)}
<NewSessionDialog
open={newSessionOpen}
onOpenChange={setNewSessionOpen}
courseClassId={courseClass.id}
defaultMeetingNumber={nextMeetingNumber}
/>
<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}? Tindakan ini tidak dapat dibatalkan.`
}
onConfirm={handleDelete}
/>
</div>
</>
);
}
function NewSessionDialog({
open,
onOpenChange,
courseClassId,
defaultMeetingNumber,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
courseClassId: number;
defaultMeetingNumber: number;
}) {
const [meetingNumber, setMeetingNumber] = useState(
String(defaultMeetingNumber),
);
function handleOpenSession() {
if (!meetingNumber) {
return;
}
router.get(session([courseClassId, Number(meetingNumber)]).url);
}
return (
<Dialog
open={open}
onOpenChange={(value) => {
onOpenChange(value);
if (!value) {
setMeetingNumber(String(defaultMeetingNumber));
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Tambah Pertemuan</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<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}
value={meetingNumber}
onChange={(e) => setMeetingNumber(e.target.value)}
autoFocus
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Batal
</Button>
<Button
type="button"
disabled={!meetingNumber}
onClick={handleOpenSession}
>
<ClipboardCheck className="h-4 w-4" />
Buka Kehadiran
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}