Yaltopia-Ticket-Admin/src/pages/admin/system-members/index.tsx
2026-06-11 10:48:11 +03:00

289 lines
9.9 KiB
TypeScript

import { useState } from "react"
import { Navigate } from "react-router-dom"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Search, UserPlus } from "lucide-react"
import { systemMemberService } from "@/services"
import { useAdminRole } from "@/hooks/use-admin-role"
import { AdminRole, type AdminRoleValue } from "@/lib/admin-roles"
import { toast } from "sonner"
export default function SystemMembersPage() {
const { canAccessSystemMembers, canManageSystem } = useAdminRole()
const queryClient = useQueryClient()
const [page, setPage] = useState(1)
const [search, setSearch] = useState("")
const [open, setOpen] = useState(false)
const [form, setForm] = useState<{
email: string
firstName: string
lastName: string
password: string
role: AdminRoleValue
}>({
email: "",
firstName: "",
lastName: "",
password: "",
role: AdminRole.CUSTOMER_SUPPORT,
})
const { data, isLoading, error } = useQuery({
queryKey: ["admin", "system-members", page, search],
queryFn: () =>
systemMemberService.list({
page,
limit: 10,
search: search.trim() || undefined,
}),
enabled: canAccessSystemMembers,
})
const createMutation = useMutation({
mutationFn: () => systemMemberService.create(form),
onSuccess: () => {
toast.success("System user created")
queryClient.invalidateQueries({ queryKey: ["admin", "system-members"] })
setOpen(false)
setForm({
email: "",
firstName: "",
lastName: "",
password: "",
role: AdminRole.CUSTOMER_SUPPORT,
})
},
onError: () => toast.error("Failed to create user"),
})
if (!canAccessSystemMembers) {
return <Navigate to="/admin/dashboard" replace />
}
return (
<div className="space-y-8 max-w-7xl mx-auto bg-white p-4 min-h-screen">
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
System users
</h1>
<p className="text-gray-500 mt-1 max-w-xl">
Internal staff who can access this panel. Actors: System Admin (full
access), Admin (view &amp; edit), Customer Support (view-only on most
areas; cannot manage this list).
</p>
</div>
{canManageSystem && (
<Button
className="rounded-none gap-2"
onClick={() => setOpen(true)}
>
<UserPlus className="h-4 w-4" />
Add system user
</Button>
)}
</div>
<Card className="border shadow-none rounded-none">
<CardHeader className="border-b flex flex-row items-center justify-between space-y-0 pb-4">
<CardTitle className="text-xs font-bold uppercase tracking-widest text-gray-400">
Directory
</CardTitle>
<div className="relative w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
className="pl-10 h-9 rounded-none border-gray-200 text-xs"
placeholder="Search name or email…"
value={search}
onChange={(e) => {
setSearch(e.target.value)
setPage(1)
}}
/>
</div>
</CardHeader>
<CardContent className="p-0">
{error && (
<p className="p-6 text-sm text-amber-700 bg-amber-50 border-b">
Could not reach{" "}
<code className="text-xs">GET /admin/system-members</code>. Add this
route on your API to populate the table.
</p>
)}
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Name
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Email
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Panel role
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest text-right">
Status
</th>
</tr>
</thead>
<tbody className="divide-y">
{isLoading ? (
<tr>
<td
colSpan={4}
className="px-6 py-16 text-center text-gray-400 animate-pulse"
>
Loading
</td>
</tr>
) : data?.data?.length ? (
data.data.map((m) => (
<tr key={m.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm font-semibold text-gray-900">
{m.firstName} {m.lastName}
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{m.email}
</td>
<td className="px-6 py-4">
<Badge variant="outline" className="rounded-none text-[10px]">
{m.role}
</Badge>
</td>
<td className="px-6 py-4 text-right text-xs text-gray-600">
{m.isActive ? "Active" : "Disabled"}
</td>
</tr>
))
) : (
<tr>
<td
colSpan={4}
className="px-6 py-16 text-center text-gray-400 italic text-sm"
>
No system users loaded.
</td>
</tr>
)}
</tbody>
</table>
</div>
</CardContent>
</Card>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="rounded-none max-w-md">
<DialogHeader>
<DialogTitle>Add system user</DialogTitle>
</DialogHeader>
<div className="grid gap-3 py-2">
<div className="grid gap-1">
<Label htmlFor="sm-email">Email</Label>
<Input
id="sm-email"
value={form.email}
onChange={(e) =>
setForm((f) => ({ ...f, email: e.target.value }))
}
className="rounded-none"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div className="grid gap-1">
<Label htmlFor="sm-fn">First name</Label>
<Input
id="sm-fn"
value={form.firstName}
onChange={(e) =>
setForm((f) => ({ ...f, firstName: e.target.value }))
}
className="rounded-none"
/>
</div>
<div className="grid gap-1">
<Label htmlFor="sm-ln">Last name</Label>
<Input
id="sm-ln"
value={form.lastName}
onChange={(e) =>
setForm((f) => ({ ...f, lastName: e.target.value }))
}
className="rounded-none"
/>
</div>
</div>
<div className="grid gap-1">
<Label htmlFor="sm-pw">Temporary password</Label>
<Input
id="sm-pw"
type="password"
value={form.password}
onChange={(e) =>
setForm((f) => ({ ...f, password: e.target.value }))
}
className="rounded-none"
/>
</div>
<div className="grid gap-1">
<Label>Role</Label>
<Select
value={form.role}
onValueChange={(role) =>
setForm((f) => ({ ...f, role: role as AdminRoleValue }))
}
>
<SelectTrigger className="rounded-none">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={AdminRole.SUPER_ADMIN}>
System Admin full access
</SelectItem>
<SelectItem value={AdminRole.ADMIN}>
Admin view &amp; edit
</SelectItem>
<SelectItem value={AdminRole.CUSTOMER_SUPPORT}>
Customer Support view (no member management)
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button variant="outline" className="rounded-none" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
className="rounded-none"
disabled={createMutation.isPending}
onClick={() => createMutation.mutate()}
>
Create
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}