Restore client user editing sheet and tweak pagination copy
This commit is contained in:
parent
c7aaa60d9a
commit
b51d0770d3
4 changed files with 333 additions and 6 deletions
|
|
@ -1320,7 +1320,7 @@ async function handleDeleteUser() {
|
||||||
<div className="flex flex-col items-center justify-between gap-3 border-t border-slate-200 pt-2 text-xs text-neutral-500 md:flex-row md:text-sm">
|
<div className="flex flex-col items-center justify-between gap-3 border-t border-slate-200 pt-2 text-xs text-neutral-500 md:flex-row md:text-sm">
|
||||||
<div>{teamTotal === 0 ? "Nenhum registro" : `Mostrando ${teamStart}-${teamEnd} de ${teamTotal}`}</div>
|
<div>{teamTotal === 0 ? "Nenhum registro" : `Mostrando ${teamStart}-${teamEnd} de ${teamTotal}`}</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-neutral-500 md:text-sm">
|
<div className="flex items-center gap-2 text-xs tracking-wide text-neutral-500 md:text-sm">
|
||||||
<span>Itens por página</span>
|
<span>Itens por página</span>
|
||||||
<Select
|
<Select
|
||||||
value={`${teamPageSize}`}
|
value={`${teamPageSize}`}
|
||||||
|
|
@ -1629,7 +1629,7 @@ async function handleDeleteUser() {
|
||||||
<div className="flex flex-col items-center justify-between gap-3 border-t border-slate-200 pt-2 text-xs text-neutral-500 md:flex-row md:text-sm">
|
<div className="flex flex-col items-center justify-between gap-3 border-t border-slate-200 pt-2 text-xs text-neutral-500 md:flex-row md:text-sm">
|
||||||
<div>{usersTotal === 0 ? "Nenhum registro" : `Mostrando ${usersStart}-${usersEnd} de ${usersTotal}`}</div>
|
<div>{usersTotal === 0 ? "Nenhum registro" : `Mostrando ${usersStart}-${usersEnd} de ${usersTotal}`}</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-neutral-500 md:text-sm">
|
<div className="flex items-center gap-2 text-xs tracking-wide text-neutral-500 md:text-sm">
|
||||||
<span>Itens por página</span>
|
<span>Itens por página</span>
|
||||||
<Select
|
<Select
|
||||||
value={`${usersPageSize}`}
|
value={`${usersPageSize}`}
|
||||||
|
|
@ -1887,7 +1887,7 @@ async function handleDeleteUser() {
|
||||||
<IconTrash className="size-4" /> Revogar selecionados
|
<IconTrash className="size-4" /> Revogar selecionados
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-neutral-500 md:text-sm">
|
<div className="flex items-center gap-2 text-xs tracking-wide text-neutral-500 md:text-sm">
|
||||||
<span>Itens por página</span>
|
<span>Itens por página</span>
|
||||||
<Select
|
<Select
|
||||||
value={`${invitesPageSize}`}
|
value={`${invitesPageSize}`}
|
||||||
|
|
|
||||||
|
|
@ -895,7 +895,7 @@ function TableView({ companies, machineCountsBySlug, onEdit, onDelete }: TableVi
|
||||||
{total === 0 ? "Nenhum registro" : `Mostrando ${start}-${end} de ${total}`}
|
{total === 0 ? "Nenhum registro" : `Mostrando ${start}-${end} de ${total}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-neutral-500 md:text-sm">
|
<div className="flex items-center gap-2 text-xs tracking-wide text-neutral-500 md:text-sm">
|
||||||
<span>Itens por página</span>
|
<span>Itens por página</span>
|
||||||
<Select value={`${pageSize}`} onValueChange={(v) => { setPageSize(Number(v)); setPageIndex(0) }}>
|
<Select value={`${pageSize}`} onValueChange={(v) => { setPageSize(Number(v)); setPageIndex(0) }}>
|
||||||
<SelectTrigger className="h-8 w-20">
|
<SelectTrigger className="h-8 w-20">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useMemo, useState, useTransition } from "react"
|
import { useCallback, useEffect, useMemo, useState, useTransition } from "react"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { ptBR } from "date-fns/locale"
|
import { ptBR } from "date-fns/locale"
|
||||||
import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form"
|
import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form"
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
} from "@/lib/schemas/company"
|
} from "@/lib/schemas/company"
|
||||||
import type { NormalizedCompany } from "@/server/company-service"
|
import type { NormalizedCompany } from "@/server/company-service"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import type { RoleOption } from "@/lib/authz"
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
|
@ -91,6 +92,18 @@ const ROLE_LABEL: Record<AdminAccount["role"], string> = {
|
||||||
COLLABORATOR: "Colaborador",
|
COLLABORATOR: "Colaborador",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROLE_TO_OPTION: Record<AdminAccount["role"], RoleOption> = {
|
||||||
|
MANAGER: "manager",
|
||||||
|
COLLABORATOR: "collaborator",
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROLE_OPTIONS_DISPLAY: ReadonlyArray<{ value: AdminAccount["role"]; label: string }> = [
|
||||||
|
{ value: "MANAGER", label: "Gestor" },
|
||||||
|
{ value: "COLLABORATOR", label: "Colaborador" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const NO_COMPANY_SELECT_VALUE = "__none__"
|
||||||
|
|
||||||
const NO_CONTACT_VALUE = "__none__"
|
const NO_CONTACT_VALUE = "__none__"
|
||||||
|
|
||||||
function createId(prefix: string) {
|
function createId(prefix: string) {
|
||||||
|
|
@ -137,6 +150,17 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({})
|
||||||
const [isPending, startTransition] = useTransition()
|
const [isPending, startTransition] = useTransition()
|
||||||
const [deleteDialogIds, setDeleteDialogIds] = useState<string[]>([])
|
const [deleteDialogIds, setDeleteDialogIds] = useState<string[]>([])
|
||||||
|
const [editAccountId, setEditAccountId] = useState<string | null>(null)
|
||||||
|
const [editAuthUserId, setEditAuthUserId] = useState<string | null>(null)
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
name: "",
|
||||||
|
email: "",
|
||||||
|
role: "COLLABORATOR" as AdminAccount["role"],
|
||||||
|
companyId: "",
|
||||||
|
})
|
||||||
|
const [isSavingAccount, setIsSavingAccount] = useState(false)
|
||||||
|
const [isResettingPassword, setIsResettingPassword] = useState(false)
|
||||||
|
const [passwordPreview, setPasswordPreview] = useState<string | null>(null)
|
||||||
|
|
||||||
const filteredAccounts = useMemo(() => {
|
const filteredAccounts = useMemo(() => {
|
||||||
const term = search.trim().toLowerCase()
|
const term = search.trim().toLowerCase()
|
||||||
|
|
@ -157,6 +181,11 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
|
||||||
[rowSelection]
|
[rowSelection]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const editAccount = useMemo(
|
||||||
|
() => (editAccountId ? accounts.find((account) => account.id === editAccountId) ?? null : null),
|
||||||
|
[accounts, editAccountId]
|
||||||
|
)
|
||||||
|
|
||||||
const companies = useMemo(() => {
|
const companies = useMemo(() => {
|
||||||
const map = new Map<string, string>()
|
const map = new Map<string, string>()
|
||||||
accounts.forEach((account) => {
|
accounts.forEach((account) => {
|
||||||
|
|
@ -173,6 +202,131 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
|
||||||
|
|
||||||
const closeDeleteDialog = useCallback(() => setDeleteDialogIds([]), [])
|
const closeDeleteDialog = useCallback(() => setDeleteDialogIds([]), [])
|
||||||
|
|
||||||
|
const closeEditor = useCallback(() => {
|
||||||
|
setEditAccountId(null)
|
||||||
|
setEditAuthUserId(null)
|
||||||
|
setPasswordPreview(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editAccount) {
|
||||||
|
setEditForm({
|
||||||
|
name: editAccount.name,
|
||||||
|
email: editAccount.email,
|
||||||
|
role: editAccount.role,
|
||||||
|
companyId: editAccount.companyId ?? "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [editAccount])
|
||||||
|
|
||||||
|
const refreshAccounts = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/admin/users", { credentials: "include", cache: "no-store" })
|
||||||
|
if (!response.ok) {
|
||||||
|
const payload = await response.json().catch(() => null)
|
||||||
|
throw new Error(payload?.error ?? "Não foi possível atualizar a lista de usuários.")
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as { items: Array<AdminAccount & { name: string | null }> }
|
||||||
|
setAccounts(
|
||||||
|
data.items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
name: item.name ?? item.email,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Falha ao atualizar usuários."
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleOpenEditor = useCallback((account: AdminAccount) => {
|
||||||
|
if (!account.authUserId) {
|
||||||
|
toast.error("Não foi possível localizar o perfil de acesso para este usuário.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setEditAccountId(account.id)
|
||||||
|
setEditAuthUserId(account.authUserId)
|
||||||
|
setPasswordPreview(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSaveAccount = useCallback(
|
||||||
|
async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (!editAccount || !editAuthUserId) {
|
||||||
|
toast.error("Nenhum usuário selecionado.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: editForm.name.trim(),
|
||||||
|
email: editForm.email.trim().toLowerCase(),
|
||||||
|
role: ROLE_TO_OPTION[editForm.role] ?? "collaborator",
|
||||||
|
tenantId: editAccount.tenantId,
|
||||||
|
companyId: editForm.companyId ? editForm.companyId : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payload.name) {
|
||||||
|
toast.error("Informe o nome do usuário.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!payload.email || !payload.email.includes("@")) {
|
||||||
|
toast.error("Informe um e-mail válido.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSavingAccount(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/users/${editAuthUserId}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => null)
|
||||||
|
throw new Error(data?.error ?? "Não foi possível salvar as alterações.")
|
||||||
|
}
|
||||||
|
await refreshAccounts()
|
||||||
|
toast.success("Usuário atualizado com sucesso.")
|
||||||
|
closeEditor()
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Erro ao salvar usuário."
|
||||||
|
toast.error(message)
|
||||||
|
} finally {
|
||||||
|
setIsSavingAccount(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[editAccount, editAuthUserId, editForm, refreshAccounts, closeEditor]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleResetPassword = useCallback(async () => {
|
||||||
|
if (!editAuthUserId) {
|
||||||
|
toast.error("Usuário inválido para gerar senha.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsResettingPassword(true)
|
||||||
|
toast.loading("Gerando nova senha...", { id: "admin-users-reset-password" })
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/users/${editAuthUserId}/reset-password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => null)
|
||||||
|
throw new Error(data?.error ?? "Falha ao gerar senha temporária.")
|
||||||
|
}
|
||||||
|
const data = (await response.json()) as { temporaryPassword: string }
|
||||||
|
setPasswordPreview(data.temporaryPassword)
|
||||||
|
toast.success("Senha temporária criada com sucesso.", { id: "admin-users-reset-password" })
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Erro ao gerar senha."
|
||||||
|
toast.error(message, { id: "admin-users-reset-password" })
|
||||||
|
} finally {
|
||||||
|
setIsResettingPassword(false)
|
||||||
|
}
|
||||||
|
}, [editAuthUserId])
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(ids: string[]) => {
|
(ids: string[]) => {
|
||||||
if (ids.length === 0) return
|
if (ids.length === 0) return
|
||||||
|
|
@ -274,6 +428,7 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
|
||||||
<TableHead>Empresa</TableHead>
|
<TableHead>Empresa</TableHead>
|
||||||
<TableHead>Papel</TableHead>
|
<TableHead>Papel</TableHead>
|
||||||
<TableHead>Último acesso</TableHead>
|
<TableHead>Último acesso</TableHead>
|
||||||
|
<TableHead className="text-right">Ações</TableHead>
|
||||||
<TableHead className="text-right">Selecionar</TableHead>
|
<TableHead className="text-right">Selecionar</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|
@ -312,6 +467,29 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
|
||||||
<Badge variant="secondary">{ROLE_LABEL[account.role]}</Badge>
|
<Badge variant="secondary">{ROLE_LABEL[account.role]}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs text-muted-foreground">{formatDate(account.lastSeenAt)}</TableCell>
|
<TableCell className="text-xs text-muted-foreground">{formatDate(account.lastSeenAt)}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!account.authUserId || isPending}
|
||||||
|
onClick={() => handleOpenEditor(account)}
|
||||||
|
>
|
||||||
|
<IconPencil className="mr-1 size-4" />
|
||||||
|
<span className="hidden sm:inline">Editar</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-rose-600 hover:bg-rose-50 hover:text-rose-700"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={() => openDeleteDialog([account.id])}
|
||||||
|
>
|
||||||
|
<IconTrash className="mr-1 size-4" />
|
||||||
|
<span className="hidden sm:inline">Remover</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={rowSelection[account.id] ?? false}
|
checked={rowSelection[account.id] ?? false}
|
||||||
|
|
@ -348,6 +526,155 @@ function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] })
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Sheet open={Boolean(editAccountId)} onOpenChange={(open) => (!open ? closeEditor() : null)}>
|
||||||
|
<SheetContent side="right" className="space-y-6 overflow-y-auto px-6 pb-10 sm:max-w-lg">
|
||||||
|
<SheetHeader className="px-0 pt-6">
|
||||||
|
<SheetTitle>Editar usuário</SheetTitle>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Atualize os dados cadastrais, papel e vínculo do colaborador.
|
||||||
|
</p>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
{editAccount ? (
|
||||||
|
<form className="space-y-6" onSubmit={handleSaveAccount}>
|
||||||
|
<div className="rounded-lg border border-border/60 bg-muted/40 p-4">
|
||||||
|
<p className="text-sm font-semibold text-foreground">{editAccount.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{editAccount.email}</p>
|
||||||
|
<div className="mt-3 text-xs text-muted-foreground">
|
||||||
|
Essa seleção substitui o vínculo atual no portal do cliente.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-name">Nome</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-name"
|
||||||
|
value={editForm.name}
|
||||||
|
onChange={(event) => setEditForm((prev) => ({ ...prev, name: event.target.value }))}
|
||||||
|
placeholder="Nome completo"
|
||||||
|
disabled={isSavingAccount}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-email">E-mail</Label>
|
||||||
|
<Input
|
||||||
|
id="edit-email"
|
||||||
|
type="email"
|
||||||
|
value={editForm.email}
|
||||||
|
onChange={(event) => setEditForm((prev) => ({ ...prev, email: event.target.value }))}
|
||||||
|
placeholder="usuario@empresa.com"
|
||||||
|
disabled={isSavingAccount}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-role">Papel</Label>
|
||||||
|
<Select
|
||||||
|
value={editForm.role}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setEditForm((prev) => ({ ...prev, role: value as AdminAccount["role"] }))
|
||||||
|
}
|
||||||
|
disabled={isSavingAccount}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="edit-role">
|
||||||
|
<SelectValue placeholder="Selecione" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{ROLE_OPTIONS_DISPLAY.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="edit-company">Empresa vinculada</Label>
|
||||||
|
<Select
|
||||||
|
value={editForm.companyId || NO_COMPANY_SELECT_VALUE}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setEditForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
companyId: value === NO_COMPANY_SELECT_VALUE ? "" : value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={isSavingAccount}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="edit-company">
|
||||||
|
<SelectValue placeholder="Sem empresa vinculada" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={NO_COMPANY_SELECT_VALUE}>Sem empresa vinculada</SelectItem>
|
||||||
|
{companies.map((company) => (
|
||||||
|
<SelectItem key={company.id} value={company.id}>
|
||||||
|
{company.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-lg border border-border/60 bg-muted/20 p-4">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-foreground">Gerar nova senha</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Uma senha temporária será exibida abaixo. Compartilhe com o usuário e peça para trocá-la no primeiro acesso.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
disabled={isResettingPassword}
|
||||||
|
>
|
||||||
|
{isResettingPassword ? "Gerando..." : "Gerar senha"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{passwordPreview ? (
|
||||||
|
<div className="mt-4 flex flex-wrap items-center gap-3 rounded-md border border-dashed border-border/60 bg-background p-3">
|
||||||
|
<code className="text-sm font-semibold text-foreground">{passwordPreview}</code>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (!passwordPreview) return
|
||||||
|
const promise = navigator.clipboard?.writeText(passwordPreview)
|
||||||
|
if (!promise) {
|
||||||
|
toast.error("Não foi possível copiar a senha.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
promise
|
||||||
|
.then(() => toast.success("Senha copiada para a área de transferência."))
|
||||||
|
.catch(() => toast.error("Não foi possível copiar a senha."))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copiar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SheetFooter className="flex flex-col gap-2 sm:flex-row sm:justify-end">
|
||||||
|
<Button type="button" variant="outline" onClick={closeEditor} disabled={isSavingAccount}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSavingAccount}>
|
||||||
|
{isSavingAccount ? "Salvando..." : "Salvar alterações"}
|
||||||
|
</Button>
|
||||||
|
</SheetFooter>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">Selecione um usuário para visualizar os detalhes.</p>
|
||||||
|
)}
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export function TablePagination<TData>({
|
||||||
|
|
||||||
<div className="flex flex-col-reverse items-center gap-3 md:flex-row md:gap-4 md:justify-end md:flex-1">
|
<div className="flex flex-col-reverse items-center gap-3 md:flex-row md:gap-4 md:justify-end md:flex-1">
|
||||||
{pageSizeOptions.length > 0 ? (
|
{pageSizeOptions.length > 0 ? (
|
||||||
<div className="flex items-center gap-2 text-xs uppercase tracking-wide text-neutral-500 md:text-sm">
|
<div className="flex items-center gap-2 text-xs tracking-wide text-neutral-500 md:text-sm">
|
||||||
<span>{rowsPerPageLabel}</span>
|
<span>{rowsPerPageLabel}</span>
|
||||||
<Select
|
<Select
|
||||||
value={`${pageSize}`}
|
value={`${pageSize}`}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue