223 lines
13 KiB
TypeScript
223 lines
13 KiB
TypeScript
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
|
|
import { Head, Link, router, usePage } from '@inertiajs/react';
|
|
import { Badge } from '@/Components/ui/badge';
|
|
import { Button } from '@/Components/ui/button';
|
|
import { Card, CardContent } from '@/Components/ui/card';
|
|
import { DataTableToolbar } from '@/Components/DataTableToolbar';
|
|
import {
|
|
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
} from '@/Components/ui/select';
|
|
import {
|
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
} from '@/Components/ui/table';
|
|
import { PaginatedData, User, PageProps } from '@/types';
|
|
import { Plus, Eye, Pencil, Trash2, ShieldCheck, ShieldOff, Upload, KeyRound, Building2 } from 'lucide-react';
|
|
import { FormEvent, useState } from 'react';
|
|
|
|
interface Props extends PageProps {
|
|
users: PaginatedData<User>;
|
|
filters: {
|
|
search?: string;
|
|
type?: string;
|
|
status?: string;
|
|
};
|
|
}
|
|
|
|
const statusVariant = (status: string) => {
|
|
switch (status) {
|
|
case 'active': return 'default';
|
|
case 'inactive': return 'secondary';
|
|
case 'suspended': return 'destructive';
|
|
default: return 'outline';
|
|
}
|
|
};
|
|
|
|
const typeLabel = (type: string) => {
|
|
switch (type) {
|
|
case 'admin': return 'Admin';
|
|
case 'employee': return 'Employee';
|
|
case 'customer': return 'Customer';
|
|
default: return type;
|
|
}
|
|
};
|
|
|
|
export default function Index({ users, filters }: Props) {
|
|
const { flash } = usePage<PageProps>().props;
|
|
const [search, setSearch] = useState(filters.search || '');
|
|
const [typeFilter, setTypeFilter] = useState(filters.type || 'all');
|
|
const [statusFilter, setStatusFilter] = useState(filters.status || 'all');
|
|
|
|
const applyFilters = (e?: FormEvent) => {
|
|
e?.preventDefault();
|
|
router.get(route('users.index'), {
|
|
search: search || undefined,
|
|
type: typeFilter !== 'all' ? typeFilter : undefined,
|
|
status: statusFilter !== 'all' ? statusFilter : undefined,
|
|
}, { preserveState: true, replace: true });
|
|
};
|
|
|
|
const handleDelete = (user: User) => {
|
|
if (confirm(`Are you sure you want to delete "${user.name}"?`)) {
|
|
router.delete(route('users.destroy', user.ulid));
|
|
}
|
|
};
|
|
|
|
const handleToggleStatus = (user: User) => {
|
|
router.patch(route('users.toggle-status', user.ulid));
|
|
};
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
header={
|
|
<div className="flex items-center gap-2">
|
|
<h2 className="text-xl font-semibold leading-tight text-gray-800">User Management</h2>
|
|
</div>
|
|
}
|
|
>
|
|
<Head title="Users" />
|
|
|
|
<div className="py-6">
|
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
{flash?.success && (
|
|
<div className="mb-4 rounded-md bg-green-50 p-4 text-sm text-green-700">{flash.success}</div>
|
|
)}
|
|
{flash?.error && (
|
|
<div className="mb-4 rounded-md bg-red-50 p-4 text-sm text-red-700">{flash.error}</div>
|
|
)}
|
|
|
|
<Card>
|
|
<DataTableToolbar
|
|
searchValue={search}
|
|
searchPlaceholder="Search by name or email..."
|
|
onSearchChange={setSearch}
|
|
onSearchSubmit={applyFilters}
|
|
filters={
|
|
<>
|
|
<Select value={typeFilter} onValueChange={(v) => { if (v) setTypeFilter(v); }} items={[{ value: 'all', label: 'All Types' }, { value: 'admin', label: 'Admin' }, { value: 'employee', label: 'Employee' }, { value: 'customer', label: 'Customer' }]}>
|
|
<SelectTrigger id="filter-type" className="w-[150px]">
|
|
<SelectValue placeholder="User Type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Types</SelectItem>
|
|
<SelectItem value="admin">Admin</SelectItem>
|
|
<SelectItem value="employee">Employee</SelectItem>
|
|
<SelectItem value="customer">Customer</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Select value={statusFilter} onValueChange={(v) => { if (v) setStatusFilter(v); }} items={[{ value: 'all', label: 'All Status' }, { value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' }, { value: 'suspended', label: 'Suspended' }]}>
|
|
<SelectTrigger id="filter-status" className="w-[150px]">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Status</SelectItem>
|
|
<SelectItem value="active">Active</SelectItem>
|
|
<SelectItem value="inactive">Inactive</SelectItem>
|
|
<SelectItem value="suspended">Suspended</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</>
|
|
}
|
|
actions={
|
|
<div className="flex items-center gap-2">
|
|
<Link href={route('users.bulk-import.form')}>
|
|
<Button variant="outline" size="sm">
|
|
<Upload className="mr-2 h-4 w-4" /> Bulk Import
|
|
</Button>
|
|
</Link>
|
|
<Link href={route('users.create')}>
|
|
<Button size="sm"><Plus className="mr-2 h-4 w-4" /> Add User</Button>
|
|
</Link>
|
|
</div>
|
|
}
|
|
/>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Contractor</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{users.data.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center text-gray-500 py-8">No users found.</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
users.data.map((user) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell className="font-medium">
|
|
<div className="flex items-center gap-2">
|
|
{user.must_change_password && (
|
|
<span title="Pending password change">
|
|
<KeyRound className="h-3.5 w-3.5 text-amber-400" />
|
|
</span>
|
|
)}
|
|
{user.name}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{user.email}</TableCell>
|
|
<TableCell>
|
|
{user.contractor ? (
|
|
<span className="flex items-center gap-1.5 text-sm">
|
|
<Building2 className="h-3.5 w-3.5 shrink-0 text-gray-400" />
|
|
{user.contractor.company_name}
|
|
</span>
|
|
) : (
|
|
<span className="text-xs text-gray-400 italic">Platform</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">{typeLabel(user.user_type)}</Badge>
|
|
{user.roles && user.roles.length > 0 && (
|
|
<div className="mt-1 text-[10px] uppercase text-gray-500">
|
|
{user.roles[0].name.replace(/-/g, ' ')}
|
|
</div>
|
|
)}
|
|
</TableCell>
|
|
<TableCell><Badge variant={statusVariant(user.status)}>{user.status}</Badge></TableCell>
|
|
<TableCell>{new Date(user.created_at).toLocaleDateString()}</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex items-center justify-end gap-1">
|
|
<Link href={route('users.show', user.ulid)}>
|
|
<Button variant="ghost" size="icon-sm" title="View"><Eye className="h-4 w-4" /></Button>
|
|
</Link>
|
|
<Link href={route('users.edit', user.ulid)}>
|
|
<Button variant="ghost" size="icon-sm" title="Edit"><Pencil className="h-4 w-4" /></Button>
|
|
</Link>
|
|
<Button variant="ghost" size="icon-sm" title={user.status === 'active' ? 'Suspend' : 'Activate'} onClick={() => handleToggleStatus(user)}>
|
|
{user.status === 'active' ? <ShieldOff className="h-4 w-4 text-amber-500" /> : <ShieldCheck className="h-4 w-4 text-green-500" />}
|
|
</Button>
|
|
<Button variant="ghost" size="icon-sm" title="Delete" onClick={() => handleDelete(user)}>
|
|
<Trash2 className="h-4 w-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
{users.last_page > 1 && (
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<p className="text-sm text-gray-600">Showing {users.from} to {users.to} of {users.total} users</p>
|
|
<div className="flex gap-1">
|
|
{users.prev_page_url && <Link href={users.prev_page_url}><Button variant="outline" size="sm">Previous</Button></Link>}
|
|
{users.next_page_url && <Link href={users.next_page_url}><Button variant="outline" size="sm">Next</Button></Link>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
}
|