119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
import { useState } from 'react'
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
import { useConfirm } from '../../components/shared/ConfirmDialog'
|
|
import { useToast } from '../../components/shared/Toast/ToastProvider'
|
|
import { usePermission } from '../../hooks/usePermission'
|
|
import { fetchUsers, deleteUser } from './usersApi'
|
|
import { useUsersRealtime } from './useUsersRealtime'
|
|
import { UserFormModal } from './UserFormModal'
|
|
import { AssignRoleModal } from './AssignRoleModal'
|
|
import type { User } from '../../types/user'
|
|
|
|
export function UsersListPageMobile() {
|
|
const { can } = usePermission()
|
|
const confirm = useConfirm()
|
|
const toast = useToast()
|
|
const queryClient = useQueryClient()
|
|
useUsersRealtime()
|
|
|
|
const [page, setPage] = useState(1)
|
|
const [formUser, setFormUser] = useState<User | null | undefined>(undefined)
|
|
const [roleUser, setRoleUser] = useState<User | null>(null)
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['users', { page, sort: 'created_at', direction: 'desc' as const }],
|
|
queryFn: () => fetchUsers({ page, sort: 'created_at', direction: 'desc' }),
|
|
})
|
|
|
|
async function handleDelete(user: User) {
|
|
const ok = await confirm({
|
|
title: 'Delete user',
|
|
message: `Delete ${user.name}? This cannot be undone.`,
|
|
confirmLabel: 'Delete',
|
|
danger: true,
|
|
})
|
|
if (!ok) return
|
|
|
|
await deleteUser(user.id)
|
|
queryClient.invalidateQueries({ queryKey: ['users'] })
|
|
toast.success(`${user.name} was deleted.`)
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h1 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Users</h1>
|
|
{can('users.create') && (
|
|
<button
|
|
onClick={() => setFormUser(null)}
|
|
className="rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
>
|
|
Add
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<p className="text-sm text-gray-500">Loading…</p>
|
|
) : (
|
|
<div className="flex flex-col gap-3">
|
|
{(data?.data ?? []).map((u) => (
|
|
<div
|
|
key={u.id}
|
|
className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900"
|
|
>
|
|
<p className="font-medium text-gray-900 dark:text-gray-100">{u.name}</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-500">{u.email}</p>
|
|
<p className="mt-1 text-xs text-gray-400 dark:text-gray-600">{u.roles.join(', ')}</p>
|
|
<div className="mt-3 flex gap-4 text-sm">
|
|
{u.can_edit && (
|
|
<button onClick={() => setFormUser(u)} className="text-blue-600 dark:text-blue-400">
|
|
Edit
|
|
</button>
|
|
)}
|
|
{can('roles.assign') && (
|
|
<button onClick={() => setRoleUser(u)} className="text-blue-600 dark:text-blue-400">
|
|
Role
|
|
</button>
|
|
)}
|
|
{u.can_delete && (
|
|
<button onClick={() => handleDelete(u)} className="text-red-600 dark:text-red-400">
|
|
Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{data && data.meta.last_page > 1 && (
|
|
<div className="mt-4 flex items-center justify-between text-sm">
|
|
<button
|
|
disabled={page <= 1}
|
|
onClick={() => setPage((p) => p - 1)}
|
|
className="text-gray-600 disabled:opacity-40 dark:text-gray-400"
|
|
>
|
|
Previous
|
|
</button>
|
|
<span className="text-gray-500">
|
|
Page {page} of {data.meta.last_page}
|
|
</span>
|
|
<button
|
|
disabled={page >= data.meta.last_page}
|
|
onClick={() => setPage((p) => p + 1)}
|
|
className="text-gray-600 disabled:opacity-40 dark:text-gray-400"
|
|
>
|
|
Next
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{formUser !== undefined && (
|
|
<UserFormModal open onClose={() => setFormUser(undefined)} user={formUser ?? undefined} />
|
|
)}
|
|
{roleUser && <AssignRoleModal open onClose={() => setRoleUser(null)} user={roleUser} />}
|
|
</div>
|
|
)
|
|
}
|