feat: CSV exports, PDF improvements, play internal/external with hour split, roles cleanup, admin companies with 'Cliente avulso', ticket list spacing/alignment fixes, status translations and mappings

This commit is contained in:
Esdras Renan 2025-10-07 13:42:45 -03:00
parent addd4ce6e8
commit 3bafcc5a0a
45 changed files with 1401 additions and 256 deletions

View file

@ -95,9 +95,26 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
const [lastInviteLink, setLastInviteLink] = useState<string | null>(null)
const [revokingId, setRevokingId] = useState<string | null>(null)
const [isPending, startTransition] = useTransition()
const [companies, setCompanies] = useState<Array<{ id: string; name: string }>>([])
const [linkEmail, setLinkEmail] = useState("")
const [linkCompanyId, setLinkCompanyId] = useState("")
const normalizedRoles = useMemo(() => roleOptions ?? ROLE_OPTIONS, [roleOptions])
// load companies for association
useMemo(() => {
void (async () => {
try {
const r = await fetch("/api/admin/companies", { credentials: "include" })
const j = await r.json()
const items = (j.companies ?? []).map((c: any) => ({ id: c.id as string, name: c.name as string }))
setCompanies(items)
} catch {
// noop
}
})()
}, [])
async function handleInviteSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
if (!email || !email.includes("@")) {
@ -238,9 +255,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
<SelectContent>
{normalizedRoles.map((item) => (
<SelectItem key={item} value={item}>
{item === "customer"
? "Cliente"
: item === "admin"
{item === "admin"
? "Administrador"
: item === "manager"
? "Gestor"
@ -294,6 +309,63 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Vincular usuário a empresa</CardTitle>
<CardDescription>Associe um colaborador à sua empresa (usado para escopo de gestores e relatórios).</CardDescription>
</CardHeader>
<CardContent>
<form
className="grid grid-cols-1 gap-4 md:grid-cols-[1fr_1fr_auto]"
onSubmit={(e) => {
e.preventDefault()
if (!linkEmail || !linkCompanyId) {
toast.error("Informe e-mail e empresa")
return
}
startTransition(async () => {
toast.loading("Vinculando...", { id: "assign-company" })
try {
const r = await fetch("/api/admin/users/assign-company", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: linkEmail, companyId: linkCompanyId }),
credentials: "include",
})
if (!r.ok) throw new Error("failed")
toast.success("Usuário vinculado à empresa!", { id: "assign-company" })
} catch {
toast.error("Não foi possível vincular", { id: "assign-company" })
}
})
}}
>
<div className="grid gap-2">
<Label>E-mail do usuário</Label>
<Input value={linkEmail} onChange={(e) => setLinkEmail(e.target.value)} placeholder="colaborador@empresa.com" />
</div>
<div className="grid gap-2">
<Label>Empresa</Label>
<Select value={linkCompanyId} onValueChange={setLinkCompanyId}>
<SelectTrigger>
<SelectValue placeholder="Selecionar" />
</SelectTrigger>
<SelectContent>
{companies.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button type="submit" disabled={isPending}>Vincular</Button>
</div>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Convites emitidos</CardTitle>