admin: remove 'Espaço (ID interno)' from Convites and improve filter bar alignment; tickets: fix running timer by optimistic updating start/pause times

This commit is contained in:
Esdras Renan 2025-10-19 16:27:12 -03:00
parent a325d612cb
commit 7c3bf00790
2 changed files with 41 additions and 20 deletions

View file

@ -884,8 +884,8 @@ async function handleDeleteUser() {
Novo usuário Novo usuário
</Button> </Button>
</div> </div>
<div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between"> <div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm md:grid md:grid-cols-[minmax(0,1fr)_auto_auto_auto_auto] md:items-center md:gap-3">
<div className="relative w-full sm:max-w-xs"> <div className="relative w-full md:max-w-sm">
<IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" /> <IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
<Input <Input
value={teamSearch} value={teamSearch}
@ -894,7 +894,7 @@ async function handleDeleteUser() {
className="h-9 pl-9" className="h-9 pl-9"
/> />
</div> </div>
<div className="flex flex-wrap items-center gap-3"> <div className="mt-3 flex flex-wrap items-center gap-3 md:mt-0">
<Select value={teamRoleFilter} onValueChange={(value) => setTeamRoleFilter(value as "all" | RoleOption)}> <Select value={teamRoleFilter} onValueChange={(value) => setTeamRoleFilter(value as "all" | RoleOption)}>
<SelectTrigger className="h-9 w-full sm:w-48"> <SelectTrigger className="h-9 w-full sm:w-48">
<SelectValue placeholder="Todos os papéis" /> <SelectValue placeholder="Todos os papéis" />
@ -1102,8 +1102,8 @@ async function handleDeleteUser() {
Novo usuário Novo usuário
</Button> </Button>
</div> </div>
<div className="flex flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm sm:flex-row sm:items-center sm:justify-between"> <div className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm md:grid md:grid-cols-[minmax(0,1fr)_auto_auto_auto_auto] md:items-center md:gap-3">
<div className="relative w-full sm:max-w-xs"> <div className="relative w-full md:max-w-sm">
<IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" /> <IconSearch className="text-muted-foreground pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2" />
<Input <Input
value={peopleSearch} value={peopleSearch}
@ -1112,7 +1112,7 @@ async function handleDeleteUser() {
className="h-9 pl-9" className="h-9 pl-9"
/> />
</div> </div>
<div className="flex flex-wrap items-center gap-3"> <div className="mt-3 flex flex-wrap items-center gap-3 md:mt-0">
<Select value={peopleRoleFilter} onValueChange={(value) => setPeopleRoleFilter(value as "all" | "manager" | "collaborator")}> <Select value={peopleRoleFilter} onValueChange={(value) => setPeopleRoleFilter(value as "all" | "manager" | "collaborator")}>
<SelectTrigger className="h-9 w-full sm:w-48"> <SelectTrigger className="h-9 w-full sm:w-48">
<SelectValue placeholder="Perfil" /> <SelectValue placeholder="Perfil" />
@ -1423,7 +1423,7 @@ async function handleDeleteUser() {
<CardContent> <CardContent>
<form <form
onSubmit={handleInviteSubmit} onSubmit={handleInviteSubmit}
className="grid gap-4 md:grid-cols-2 xl:grid-cols-[minmax(0,2.4fr)_minmax(0,2fr)_minmax(0,1.2fr)_minmax(0,1.6fr)_minmax(0,1.2fr)_auto]" className="grid gap-4 md:grid-cols-2 xl:grid-cols-[minmax(0,2.4fr)_minmax(0,2fr)_minmax(0,1.2fr)_minmax(0,1.2fr)_auto]"
> >
<div className="grid gap-2 md:col-span-2 xl:col-auto"> <div className="grid gap-2 md:col-span-2 xl:col-auto">
<Label htmlFor="invite-email">E-mail corporativo</Label> <Label htmlFor="invite-email">E-mail corporativo</Label>
@ -1463,19 +1463,6 @@ async function handleDeleteUser() {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid gap-2 md:col-span-2 xl:col-auto">
<Label htmlFor="invite-tenant">Espaço (ID interno)</Label>
<Input
id="invite-tenant"
value={tenantId}
onChange={(event) => setTenantId(event.target.value)}
placeholder="ex.: principal"
className="w-full"
/>
<p className="text-xs text-neutral-500">
Use este campo apenas se trabalhar com múltiplos espaços de clientes. Caso contrário, mantenha o valor padrão.
</p>
</div>
<div className="grid gap-2 md:col-span-1 xl:col-auto"> <div className="grid gap-2 md:col-span-1 xl:col-auto">
<Label>Expira em</Label> <Label>Expira em</Label>
<Select value={expiresInDays} onValueChange={setExpiresInDays}> <Select value={expiresInDays} onValueChange={setExpiresInDays}>

View file

@ -430,6 +430,27 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
} else { } else {
toast.success("Atendimento iniciado", { id: "work" }) toast.success("Atendimento iniciado", { id: "work" })
} }
// Otimização local: garantir startedAt correto imediatamente
const startedAtMs = typeof result?.startedAt === "number" ? result.startedAt : Date.now()
const sessionId = (result as { sessionId?: unknown })?.sessionId as Id<"ticketWorkSessions"> | undefined
setWorkSummary((prev) => {
const base: WorkSummarySnapshot = prev ?? {
ticketId: ticket.id as Id<"tickets">,
totalWorkedMs: 0,
internalWorkedMs: 0,
externalWorkedMs: 0,
activeSession: null,
}
return {
...base,
activeSession: {
id: (sessionId as Id<"ticketWorkSessions">) ?? (base.activeSession?.id as Id<"ticketWorkSessions">),
agentId: convexUserId as Id<"users">,
startedAt: startedAtMs,
workType,
},
}
})
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Não foi possível atualizar o atendimento" const message = error instanceof Error ? error.message : "Não foi possível atualizar o atendimento"
toast.error(message, { id: "work" }) toast.error(message, { id: "work" })
@ -454,6 +475,19 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
toast.success("Atendimento pausado", { id: "work" }) toast.success("Atendimento pausado", { id: "work" })
} }
setPauseDialogOpen(false) setPauseDialogOpen(false)
// Otimização local: aplicar duração retornada no total e limpar sessão ativa
const delta = typeof (result as { durationMs?: unknown })?.durationMs === "number" ? (result as { durationMs?: number }).durationMs! : 0
setWorkSummary((prev) => {
if (!prev) return prev
const workType = prev.activeSession?.workType ?? "INTERNAL"
return {
...prev,
totalWorkedMs: prev.totalWorkedMs + delta,
internalWorkedMs: prev.internalWorkedMs + (workType === "INTERNAL" ? delta : 0),
externalWorkedMs: prev.externalWorkedMs + (workType === "EXTERNAL" ? delta : 0),
activeSession: null,
}
})
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Não foi possível atualizar o atendimento" const message = error instanceof Error ? error.message : "Não foi possível atualizar o atendimento"
toast.error(message, { id: "work" }) toast.error(message, { id: "work" })