Files
B2BCall-dialer/apps/frontend/src/app/app/telefonia/ramais/actions.ts
Matheus 3fe571bfb0 feat(telefonia): edição de ramal (nome/caller ID/contexto/codecs/grupo) junto da senha SIP
Pedido do usuário: abrir a edição completa do ramal no mesmo lugar de
ver/redefinir a senha, em vez de só um campo (grupo de captura) editável
isoladamente. UpdateExtensionDto ganha name/context/codecs (só callGroup
e caller ID eram editáveis antes); codecs valida contra uma lista fixa
e agora realmente vira absolute_codec_string no directory XML — antes
a coluna existia no schema mas não tinha efeito nenhum no FreeSWITCH.

Grupo de captura vira um input com datalist alimentado pelos grupos já
usados em outros ramais do tenant, em vez de digitar de novo do zero.

Testado ponta a ponta com Playwright (tenant/ramais descartáveis):
grupo reaproveitado de outro ramal, edição de nome/caller ID/contexto,
remoção de um codec — tudo persistido e refletido na tela depois de
salvar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-31 14:01:03 -03:00

111 lines
3.8 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { requireSession } from "@/lib/session";
import { apiFetch, ApiError } from "@/lib/api";
import type { Extension } from "@/lib/extension-types";
function extractErrorMessage(err: unknown): string {
if (err instanceof ApiError) {
try {
const parsed = JSON.parse(err.message);
if (Array.isArray(parsed.message)) return parsed.message.join(" ");
if (typeof parsed.message === "string") return parsed.message;
} catch {
// corpo não era JSON — usa a mensagem crua
}
return err.message || "Falha inesperada na API.";
}
return "Falha inesperada. Tente novamente.";
}
export interface CreateExtensionInput {
number: string;
name: string;
callerIdName?: string;
callerIdNumber?: string;
context?: string;
maxRegistrations?: number;
callGroup?: string;
}
export async function createExtension(
input: CreateExtensionInput,
): Promise<{ ok: true; extension: Extension; sipPassword: string } | { ok: false; error: string }> {
const session = await requireSession();
try {
const result = await apiFetch<Extension & { sipPassword: string }>("/extensions", session.accessToken, {
method: "POST",
body: JSON.stringify(input),
});
const { sipPassword, ...extension } = result;
revalidatePath("/app/telefonia/ramais");
return { ok: true, extension, sipPassword };
} catch (err) {
return { ok: false, error: extractErrorMessage(err) };
}
}
export async function resetExtensionPassword(id: string): Promise<{ ok: true; sipPassword: string } | { ok: false; error: string }> {
const session = await requireSession();
try {
const { sipPassword } = await apiFetch<{ sipPassword: string }>(`/extensions/${id}/reset-password`, session.accessToken, {
method: "POST",
});
return { ok: true, sipPassword };
} catch (err) {
return { ok: false, error: extractErrorMessage(err) };
}
}
/** Diferente de `resetExtensionPassword`: não troca nada, só mostra a
* senha atual de novo — pra reconfigurar um telefone/softphone sem
* derrubar o registro de outro aparelho já usando essa senha. */
export async function revealExtensionPassword(id: string): Promise<{ ok: true; sipPassword: string } | { ok: false; error: string }> {
const session = await requireSession();
try {
const { sipPassword } = await apiFetch<{ sipPassword: string }>(`/extensions/${id}/reveal-password`, session.accessToken, {
method: "POST",
});
return { ok: true, sipPassword };
} catch (err) {
return { ok: false, error: extractErrorMessage(err) };
}
}
export interface UpdateExtensionInput {
name?: string;
callerIdName?: string | null;
callerIdNumber?: string | null;
context?: string;
codecs?: string;
maxRegistrations?: number;
callGroup?: string | null;
}
export async function updateExtension(id: string, input: UpdateExtensionInput): Promise<{ ok: true; extension: Extension } | { ok: false; error: string }> {
const session = await requireSession();
try {
const extension = await apiFetch<Extension>(`/extensions/${id}`, session.accessToken, {
method: "PATCH",
body: JSON.stringify(input),
});
revalidatePath(`/app/telefonia/ramais/${id}`);
revalidatePath("/app/telefonia/ramais");
return { ok: true, extension };
} catch (err) {
return { ok: false, error: extractErrorMessage(err) };
}
}
export async function deleteExtension(id: string): Promise<{ ok: true } | { ok: false; error: string }> {
const session = await requireSession();
try {
await apiFetch<void>(`/extensions/${id}`, session.accessToken, { method: "DELETE" });
revalidatePath("/app/telefonia/ramais");
return { ok: true };
} catch (err) {
return { ok: false, error: extractErrorMessage(err) };
}
}