fix(filas): adiciona edição de fila após criação

Achado real reportado pelo usuário: "na fila nao tem opcao de editar a
fila apos a criacao". queues.controller.ts só tinha create/list/delete
— nunca existiu PATCH.

UpdateQueueDto (mesmos campos do create, todos opcionais) + PATCH
/queues/:id. Tela ganhou um botão "Editar" por linha, abrindo um
formulário inline pré-preenchido (reaproveita o mesmo componente do
"Nova fila", só muda entre createQueue/updateQueue).

Testado ponta a ponta com Playwright de verdade (não só a API): abrir o
form de edição confirmando que veio pré-preenchido com os valores reais
da fila, editar nome e espera máxima, salvar, e confirmar que a tabela
atualizou com os novos valores.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 20:13:03 -03:00
parent 5bbe9e6800
commit 26cc79cec2
4 changed files with 223 additions and 42 deletions

View File

@@ -40,6 +40,20 @@ export async function createQueue(input: CreateQueueInput): Promise<{ ok: true;
}
}
export async function updateQueue(
id: string,
input: CreateQueueInput,
): Promise<{ ok: true; queue: Queue } | { ok: false; error: string }> {
const session = await requireSession();
try {
const queue = await apiFetch<Queue>(`/queues/${id}`, session.accessToken, { method: "PATCH", body: JSON.stringify(input) });
revalidatePath("/app/callcenter/filas");
return { ok: true, queue };
} catch (err) {
return { ok: false, error: extractErrorMessage(err) };
}
}
export async function deleteQueue(id: string): Promise<{ ok: true } | { ok: false; error: string }> {
const session = await requireSession();
try {

View File

@@ -1,8 +1,8 @@
"use client";
import { useState, useTransition } from "react";
import { Fragment, useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { ListTree, Plus, Trash2, X } from "lucide-react";
import { ListTree, Pencil, Plus, Trash2, X } from "lucide-react";
import { Panel, PanelHeader } from "@/components/ui/panel";
import { Button } from "@/components/ui/button";
import { Input, Select, FieldLabel } from "@/components/ui/input";
@@ -10,10 +10,11 @@ import { Pill } from "@/components/ui/pill";
import { EmptyState, TBody, TD, TH, THead, TR, Table } from "@/components/ui/table";
import { formatDate } from "@/lib/format";
import { QUEUE_STRATEGIES, QUEUE_STRATEGY_LABELS, type Queue } from "@/lib/callcenter-types";
import { createQueue, deleteQueue } from "./actions";
import { createQueue, deleteQueue, updateQueue, type CreateQueueInput } from "./actions";
export function FilasView({ queues }: { queues: Queue[] }) {
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
return (
<div className="space-y-5">
@@ -25,13 +26,19 @@ export function FilasView({ queues }: { queues: Queue[] }) {
fila, que distribui pros agentes segundo a estratégia escolhida.
</p>
</div>
<Button type="button" onClick={() => setShowForm((s) => !s)}>
<Button
type="button"
onClick={() => {
setEditingId(null);
setShowForm((s) => !s);
}}
>
{showForm ? <X className="h-4 w-4" aria-hidden /> : <Plus className="h-4 w-4" aria-hidden />}
{showForm ? "Cancelar" : "Nova fila"}
</Button>
</div>
{showForm && <NewQueueForm onDone={() => setShowForm(false)} />}
{showForm && <QueueForm onDone={() => setShowForm(false)} />}
<Panel>
<PanelHeader title="Filas cadastradas" description={`${queues.length} fila(s) neste tenant`} />
@@ -54,25 +61,48 @@ export function FilasView({ queues }: { queues: Queue[] }) {
</THead>
<TBody>
{queues.map((q) => (
<TR key={q.id}>
<TD>
<span className="flex items-center gap-2 font-medium text-foreground">
<ListTree className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
{q.name}
</span>
{q.description && <span className="block pl-5 text-xs text-muted-foreground">{q.description}</span>}
</TD>
<TD className="text-muted-foreground">{QUEUE_STRATEGY_LABELS[q.strategy] ?? q.strategy}</TD>
<TD className="font-mono tabular-nums text-muted-foreground">{q.maxWaitTime > 0 ? `${q.maxWaitTime}s` : "sem limite"}</TD>
<TD className="font-mono tabular-nums text-muted-foreground">{q.discardAbandonedAfter}s</TD>
<TD>
<Pill tone={q.recordingEnabled ? "accent" : "neutral"}>{q.recordingEnabled ? "Habilitada" : "Desabilitada"}</Pill>
</TD>
<TD className="text-muted-foreground">{formatDate(q.createdAt)}</TD>
<TD>
<DeleteQueueButton queueId={q.id} queueName={q.name} />
</TD>
</TR>
<Fragment key={q.id}>
<TR>
<TD>
<span className="flex items-center gap-2 font-medium text-foreground">
<ListTree className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
{q.name}
</span>
{q.description && <span className="block pl-5 text-xs text-muted-foreground">{q.description}</span>}
</TD>
<TD className="text-muted-foreground">{QUEUE_STRATEGY_LABELS[q.strategy] ?? q.strategy}</TD>
<TD className="font-mono tabular-nums text-muted-foreground">{q.maxWaitTime > 0 ? `${q.maxWaitTime}s` : "sem limite"}</TD>
<TD className="font-mono tabular-nums text-muted-foreground">{q.discardAbandonedAfter}s</TD>
<TD>
<Pill tone={q.recordingEnabled ? "accent" : "neutral"}>{q.recordingEnabled ? "Habilitada" : "Desabilitada"}</Pill>
</TD>
<TD className="text-muted-foreground">{formatDate(q.createdAt)}</TD>
<TD>
<div className="flex items-center justify-end gap-1">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
setShowForm(false);
setEditingId((id) => (id === q.id ? null : q.id));
}}
aria-label={`Editar ${q.name}`}
>
<Pencil className="h-3.5 w-3.5" aria-hidden />
</Button>
<DeleteQueueButton queueId={q.id} queueName={q.name} />
</div>
</TD>
</TR>
{editingId === q.id && (
<tr>
<td colSpan={7} className="bg-muted/30 p-4">
<QueueForm queue={q} onDone={() => setEditingId(null)} />
</td>
</tr>
)}
</Fragment>
))}
</TBody>
</Table>
@@ -82,13 +112,14 @@ export function FilasView({ queues }: { queues: Queue[] }) {
);
}
function NewQueueForm({ onDone }: { onDone: () => void }) {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [strategy, setStrategy] = useState<string>("LONGEST_IDLE_AGENT");
const [maxWaitTime, setMaxWaitTime] = useState("120");
const [discardAbandonedAfter, setDiscardAbandonedAfter] = useState("60");
const [recordingEnabled, setRecordingEnabled] = useState(false);
function QueueForm({ queue, onDone }: { queue?: Queue; onDone: () => void }) {
const isEditing = !!queue;
const [name, setName] = useState(queue?.name ?? "");
const [description, setDescription] = useState(queue?.description ?? "");
const [strategy, setStrategy] = useState<string>(queue?.strategy ?? "LONGEST_IDLE_AGENT");
const [maxWaitTime, setMaxWaitTime] = useState(String(queue?.maxWaitTime ?? 120));
const [discardAbandonedAfter, setDiscardAbandonedAfter] = useState(String(queue?.discardAbandonedAfter ?? 60));
const [recordingEnabled, setRecordingEnabled] = useState(queue?.recordingEnabled ?? false);
const [error, setError] = useState<string | null>(null);
const [pending, startTransition] = useTransition();
@@ -99,15 +130,16 @@ function NewQueueForm({ onDone }: { onDone: () => void }) {
setError("Dê um nome à fila.");
return;
}
const input: CreateQueueInput = {
name: name.trim(),
description: description.trim() || undefined,
strategy,
maxWaitTime: Number(maxWaitTime) || 0,
discardAbandonedAfter: Number(discardAbandonedAfter) || 0,
recordingEnabled,
};
startTransition(async () => {
const result = await createQueue({
name: name.trim(),
description: description.trim() || undefined,
strategy,
maxWaitTime: Number(maxWaitTime) || undefined,
discardAbandonedAfter: Number(discardAbandonedAfter) || undefined,
recordingEnabled,
});
const result = isEditing ? await updateQueue(queue.id, input) : await createQueue(input);
if (!result.ok) {
setError(result.error);
return;
@@ -164,9 +196,14 @@ function NewQueueForm({ onDone }: { onDone: () => void }) {
{error}
</p>
)}
<div className="flex justify-end">
<div className="flex justify-end gap-2">
{isEditing && (
<Button type="button" variant="ghost" onClick={onDone} disabled={pending}>
Cancelar
</Button>
)}
<Button type="submit" disabled={pending}>
{pending ? "Criando…" : "Criar fila"}
{pending ? "Salvando…" : isEditing ? "Salvar alterações" : "Criar fila"}
</Button>
</div>
</form>