177 lines
8.7 KiB
TypeScript
177 lines
8.7 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { Head, Link, router, usePage } from '@inertiajs/react';
|
|
import React, { useState } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import AppLayout from '@/layouts/app-layout';
|
|
import { type BreadcrumbItem } from '@/types';
|
|
import { Search } from 'lucide-react';
|
|
import { useEffect } from 'react';
|
|
import { useDebounce } from 'use-debounce';
|
|
import * as XLSX from 'xlsx';
|
|
|
|
interface User {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
role: 'admin' | 'employee';
|
|
created_at: string;
|
|
}
|
|
|
|
interface PageProps {
|
|
users: User[];
|
|
filters: { search?: string; role?: string };
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
{ title: 'Manajemen Pengguna', href: '/admin/users' },
|
|
];
|
|
|
|
export default function Index({ users, filters }: PageProps) {
|
|
const [search, setSearch] = useState(filters.search || '');
|
|
const [roleFilter, setRoleFilter] = useState(filters.role || 'all');
|
|
const [debouncedSearch] = useDebounce(search, 500);
|
|
const [processingId, setProcessingId] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (debouncedSearch !== filters.search || (roleFilter !== 'all' && roleFilter !== filters.role)) {
|
|
router.get(
|
|
'/admin/users',
|
|
{ search: debouncedSearch, role: roleFilter === 'all' ? '' : roleFilter },
|
|
{ preserveState: true, replace: true }
|
|
);
|
|
}
|
|
}, [debouncedSearch, roleFilter]);
|
|
|
|
const exportExcel = () => {
|
|
const rows = users.map((u, i) => ({
|
|
No: i + 1,
|
|
Nama: u.name,
|
|
Email: u.email,
|
|
Role: u.role.toUpperCase(),
|
|
}));
|
|
const ws = XLSX.utils.json_to_sheet(rows);
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Pengguna');
|
|
XLSX.writeFile(wb, `Data_Pengguna_${new Date().toISOString().slice(0, 10)}.xlsx`);
|
|
};
|
|
|
|
const updateRole = (id: number, role: string) => {
|
|
setProcessingId(id);
|
|
router.patch(
|
|
`/admin/users/${id}`,
|
|
{ role },
|
|
{
|
|
preserveScroll: true,
|
|
onFinish: () => setProcessingId(null),
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<AppLayout breadcrumbs={breadcrumbs}>
|
|
<Head title="Manajemen Pengguna" />
|
|
|
|
<div className="p-4 md:p-8 w-full space-y-4">
|
|
|
|
{/* Page Header */}
|
|
<div className="space-y-1">
|
|
<h2 className="text-2xl font-bold tracking-tight">Manajemen Akses User</h2>
|
|
<p className="text-muted-foreground">Kelola akun dan hak akses pengguna sistem.</p>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-4">
|
|
<CardTitle>Daftar Pengguna</CardTitle>
|
|
<div className="flex gap-2">
|
|
<Button variant="outline" onClick={exportExcel}>Export Excel</Button>
|
|
<Button asChild>
|
|
<Link href="/admin/users/create">+ Tambah User Baru</Link>
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<Separator />
|
|
|
|
<div className="p-4 flex flex-col md:flex-row gap-4">
|
|
<div className="w-full md:w-1/3">
|
|
<div className="relative">
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
className="pl-8"
|
|
placeholder="Cari pengguna..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="w-full md:w-1/4">
|
|
<Select value={roleFilter} onValueChange={(val) => setRoleFilter(val)}>
|
|
<SelectTrigger><SelectValue placeholder="Filter Role" /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">Semua Role</SelectItem>
|
|
<SelectItem value="admin">Admin</SelectItem>
|
|
<SelectItem value="employee">Employee</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<CardContent className="pt-0">
|
|
<div className="relative w-full overflow-auto">
|
|
<table className="w-full text-sm text-left">
|
|
<thead className="text-muted-foreground bg-zinc-50/50">
|
|
<tr className="border-b">
|
|
<th className="h-12 px-4 font-medium">Nama</th>
|
|
<th className="h-12 px-4 font-medium">Email</th>
|
|
<th className="h-12 px-4 font-medium text-center">Role Saat Ini</th>
|
|
<th className="h-12 px-4 font-medium text-right">Ubah Role</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.length > 0 ? (
|
|
users.map((user) => (
|
|
<tr key={user.id} className="border-b hover:bg-zinc-50">
|
|
<td className="p-4 font-medium">{user.name}</td>
|
|
<td className="p-4 text-muted-foreground">{user.email}</td>
|
|
<td className="p-4 text-center">
|
|
<span className="bg-zinc-100 px-2.5 py-1 rounded-full text-[11px] font-semibold tracking-wide">
|
|
{user.role.toUpperCase()}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 text-right">
|
|
<Select
|
|
defaultValue={user.role}
|
|
onValueChange={(val) => updateRole(user.id, val)}
|
|
disabled={processingId === user.id}
|
|
>
|
|
<SelectTrigger className="w-[130px] ml-auto">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="admin">Admin</SelectItem>
|
|
<SelectItem value="employee">Employee</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td colSpan={4} className="p-8 text-center text-muted-foreground">
|
|
Belum ada data pengguna.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|