Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new components for managing admin settings, including AdminListControls, AdminSettingsCard, and TechBrandSelectLabel. - Implemented dynamic loading for admin settings sections to optimize performance. - Enhanced the layout of various admin settings sections for better user experience. - Updated the AiAssistantSection to include LLM provider management and improved model selection. - Refactored authentication settings to streamline configuration and improve accessibility.
214 lines
6.0 KiB
TypeScript
214 lines
6.0 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import { Pencil, Plus, Trash2, UsersRound } from "lucide-react"
|
|
import { useAdminUserGroups } from "@/lib/api/hooks/use-admin-queries"
|
|
import {
|
|
useCreateAdminUserGroup,
|
|
useDeleteAdminUserGroup,
|
|
useUpdateAdminUserGroup,
|
|
} from "@/lib/api/hooks/use-admin-mutations"
|
|
import type { AdminUserGroup } from "@/lib/api/admin-types"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
|
|
export function UsersGroupsDialog({
|
|
open,
|
|
onOpenChange,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}) {
|
|
const [q, setQ] = useState("")
|
|
const [editorOpen, setEditorOpen] = useState(false)
|
|
const [editing, setEditing] = useState<AdminUserGroup | null>(null)
|
|
|
|
const queryParams = useMemo(
|
|
() => ({ page: 1, page_size: 100, q: q.trim() || undefined }),
|
|
[q]
|
|
)
|
|
const { data, isFetching } = useAdminUserGroups(queryParams)
|
|
const groups = data?.groups ?? []
|
|
|
|
function openCreate() {
|
|
setEditing(null)
|
|
setEditorOpen(true)
|
|
}
|
|
|
|
function openEdit(group: AdminUserGroup) {
|
|
setEditing(group)
|
|
setEditorOpen(true)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>Groupes d'utilisateurs</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="Rechercher un groupe"
|
|
className="h-9"
|
|
/>
|
|
<Button className="h-9 shrink-0" onClick={openCreate}>
|
|
<Plus className="mr-2 size-4" />
|
|
Créer
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="max-h-80 space-y-2 overflow-y-auto rounded-lg border p-2">
|
|
{isFetching ? (
|
|
<p className="px-2 py-4 text-sm text-muted-foreground">Chargement…</p>
|
|
) : groups.length === 0 ? (
|
|
<p className="px-2 py-4 text-sm text-muted-foreground">Aucun groupe.</p>
|
|
) : (
|
|
groups.map((group) => (
|
|
<GroupRow key={group.id} group={group} onEdit={() => openEdit(group)} />
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<GroupEditorDialog
|
|
open={editorOpen}
|
|
onOpenChange={setEditorOpen}
|
|
group={editing}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function GroupRow({
|
|
group,
|
|
onEdit,
|
|
}: {
|
|
group: AdminUserGroup
|
|
onEdit: () => void
|
|
}) {
|
|
const deleteGroup = useDeleteAdminUserGroup()
|
|
|
|
return (
|
|
<div className="flex items-start gap-3 rounded-md px-2 py-2 hover:bg-muted/50">
|
|
<UsersRound className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
|
<div className="min-w-0 flex-1">
|
|
<div className="font-medium">{group.name}</div>
|
|
{group.description ? (
|
|
<div className="text-xs text-muted-foreground">{group.description}</div>
|
|
) : null}
|
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
{group.member_count.toLocaleString("fr-FR")} membre(s)
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 gap-1">
|
|
<Button variant="ghost" size="icon" className="size-8" onClick={onEdit}>
|
|
<Pencil className="size-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8 text-destructive"
|
|
onClick={() => {
|
|
if (confirm(`Supprimer le groupe « ${group.name} » ?`)) {
|
|
void deleteGroup.mutateAsync(group.id)
|
|
}
|
|
}}
|
|
>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function GroupEditorDialog({
|
|
open,
|
|
onOpenChange,
|
|
group,
|
|
}: {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
group: AdminUserGroup | null
|
|
}) {
|
|
const createGroup = useCreateAdminUserGroup()
|
|
const updateGroup = useUpdateAdminUserGroup(group?.id ?? "")
|
|
const [name, setName] = useState("")
|
|
const [description, setDescription] = useState("")
|
|
|
|
const isEdit = Boolean(group)
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setName(group?.name ?? "")
|
|
setDescription(group?.description ?? "")
|
|
}, [open, group])
|
|
|
|
async function submit() {
|
|
const trimmedName = name.trim()
|
|
if (!trimmedName) return
|
|
if (isEdit && group) {
|
|
await updateGroup.mutateAsync({
|
|
name: trimmedName,
|
|
description: description.trim(),
|
|
})
|
|
} else {
|
|
await createGroup.mutateAsync({
|
|
name: trimmedName,
|
|
description: description.trim() || undefined,
|
|
})
|
|
}
|
|
onOpenChange(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{isEdit ? "Modifier le groupe" : "Nouveau groupe"}</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<Label>Nom</Label>
|
|
<Input className="mt-1" value={name} onChange={(e) => setName(e.target.value)} />
|
|
</div>
|
|
<div>
|
|
<Label>Description (optionnel)</Label>
|
|
<Textarea
|
|
className="mt-1 min-h-20"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
disabled={!name.trim() || createGroup.isPending || updateGroup.isPending}
|
|
onClick={() => void submit()}
|
|
>
|
|
{isEdit ? "Enregistrer" : "Créer"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|