feat: add leadership history dialog and integrate into DepartmentIndex

This commit is contained in:
Yoga Pangestu 2026-09-03 13:25:02 +07:00
parent f00fab9520
commit 2b458e26fa
4 changed files with 99 additions and 4 deletions

View File

@ -14,7 +14,11 @@ class DepartmentService
public function paginated(int $perPage = 25, string $search = ''): LengthAwarePaginator
{
return Department::query()
->with(['currentLeader.lecturer.user.profile'])
->with([
'currentLeader.lecturer.user.profile',
'leaderships' => fn ($q) => $q->orderByRaw('ended_at IS NULL DESC')->orderByDesc('started_at'),
'leaderships.lecturer.user.profile',
])
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%")->orWhere('code', 'like', "%{$search}%"))
->latest()
->paginate($perPage, ['id', 'code', 'name', 'degree_level']);

View File

@ -1,5 +1,5 @@
import type { ColumnDef } from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
import { History, Pencil, Trash2 } from 'lucide-react';
import { RowActions } from '@/components/row-actions';
import { Badge } from '@/components/ui/badge';
import type { Department } from '@/types/department';
@ -9,6 +9,7 @@ export type { Department } from '@/types/department';
type CreateColumnsParams = {
handleEdit: (department: Department) => void;
handleDeleteClick: (department: Department) => void;
handleViewHistory: (department: Department) => void;
canUpdate: boolean;
canDelete: boolean;
};
@ -16,7 +17,13 @@ type CreateColumnsParams = {
export function createDepartmentColumns(
params: CreateColumnsParams,
): ColumnDef<Department>[] {
const { handleEdit, handleDeleteClick, canUpdate, canDelete } = params;
const {
handleEdit,
handleDeleteClick,
handleViewHistory,
canUpdate,
canDelete,
} = params;
const columns: ColumnDef<Department>[] = [
{
@ -72,7 +79,7 @@ export function createDepartmentColumns(
},
];
if (canUpdate || canDelete) {
{
columns.push({
id: 'actions',
header: () => <span className="block text-center">Aksi</span>,
@ -86,6 +93,11 @@ export function createDepartmentColumns(
return (
<RowActions
actions={[
{
label: 'Riwayat Kaprodi',
icon: <History className="h-4 w-4" />,
onClick: () => handleViewHistory(department),
},
{
label: 'Edit',
icon: <Pencil className="h-4 w-4" />,

View File

@ -9,6 +9,7 @@ import { DeleteConfirmDialog } from '@/components/delete-confirm-dialog';
import { FormDialog } from '@/components/form-dialog';
import InputError from '@/components/input-error';
import { PageHeader } from '@/components/page-header';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Combobox,
@ -18,6 +19,12 @@ import {
ComboboxItem,
ComboboxList,
} from '@/components/ui/combobox';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
@ -63,6 +70,8 @@ export default function DepartmentIndex({
const [createOpen, setCreateOpen] = useState(false);
const [editing, setEditing] = useState<Department | null>(null);
const [deleting, setDeleting] = useState<Department | null>(null);
const [historyDepartment, setHistoryDepartment] =
useState<Department | null>(null);
const { hasPermission } = usePermissions();
const canCreate = hasPermission('create-departments');
const canUpdate = hasPermission('update-departments');
@ -98,6 +107,7 @@ export default function DepartmentIndex({
const columns = createDepartmentColumns({
handleEdit: (department) => setEditing(department),
handleDeleteClick: (department) => setDeleting(department),
handleViewHistory: (department) => setHistoryDepartment(department),
canUpdate,
canDelete,
});
@ -188,11 +198,79 @@ export default function DepartmentIndex({
}
onConfirm={handleDelete}
/>
<LeadershipHistoryDialog
department={historyDepartment}
onOpenChange={(open) => {
if (!open) {
setHistoryDepartment(null);
}
}}
/>
</div>
</>
);
}
function LeadershipHistoryDialog({
department,
onOpenChange,
}: {
department: Department | null;
onOpenChange: (open: boolean) => void;
}) {
const entries = department?.leaderships ?? [];
return (
<Dialog open={department !== null} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[85vh] overflow-hidden sm:max-w-lg">
<DialogHeader>
<DialogTitle>
Riwayat Kaprodi
{department ? `${department.name}` : ''}
</DialogTitle>
</DialogHeader>
<div className="max-h-[60vh] space-y-3 overflow-y-auto">
{entries.length === 0 && (
<p className="text-sm text-muted-foreground">
Belum ada riwayat kaprodi untuk jurusan ini.
</p>
)}
{entries.map((entry) => (
<div
key={entry.id}
className="flex items-start justify-between gap-3 rounded-md border p-3"
>
<div>
<p className="font-medium">
{entry.lecturer?.user?.profile
?.full_name ?? 'N/A'}
</p>
<p className="text-sm text-muted-foreground">
{format(
new Date(entry.started_at),
'd MMM yyyy',
)}{' '}
-{' '}
{entry.ended_at
? format(
new Date(entry.ended_at),
'd MMM yyyy',
)
: 'Sekarang'}
</p>
</div>
{!entry.ended_at && <Badge>Sekarang</Badge>}
</div>
))}
</div>
</DialogContent>
</Dialog>
);
}
function KaprodiFields({
errors,
lecturers,

View File

@ -23,6 +23,7 @@ export type Department = {
name: string;
degree_level: DegreeLevel | null;
current_leader: DepartmentLeadership | null;
leaderships: DepartmentLeadership[];
created_at: string;
updated_at: string;
};