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
This commit is contained in:
2026-08-31 14:01:03 -03:00
parent e75872faec
commit 3fe571bfb0
9 changed files with 296 additions and 95 deletions

View File

@@ -1,5 +1,13 @@
import { IsIn, IsInt, IsOptional, IsString, Matches, Max, Min, MaxLength } from "class-validator";
/** Codecs de áudio suportados pelo perfil Sofia `internal` (mesma lista
* usada em `packages/telephony/src/directory-xml.ts::absolute_codec_string`
* — manter as duas em sincronia). */
export const AVAILABLE_CODECS = ["PCMU", "PCMA", "OPUS", "G722", "G729", "GSM"] as const;
const CODEC_ALTERNATION = AVAILABLE_CODECS.join("|");
const CODECS_REGEX = new RegExp(`^(${CODEC_ALTERNATION})(,(${CODEC_ALTERNATION}))*$`);
export class CreateExtensionDto {
@IsString()
@Matches(/^[0-9]{2,10}$/, { message: "number deve ter só dígitos (2 a 10)" })
@@ -45,6 +53,11 @@ export class CreateExtensionDto {
}
export class UpdateExtensionDto {
@IsOptional()
@IsString()
@MaxLength(120)
name?: string;
@IsOptional()
@IsString()
@MaxLength(80)
@@ -55,6 +68,16 @@ export class UpdateExtensionDto {
@Matches(/^[0-9]{2,20}$/)
callerIdNumber?: string;
@IsOptional()
@IsString()
@MaxLength(80)
context?: string;
@IsOptional()
@IsString()
@Matches(CODECS_REGEX, { message: `codecs deve ser uma lista separada por vírgula entre: ${AVAILABLE_CODECS.join(", ")}` })
codecs?: string;
@IsOptional()
@IsInt()
@Min(1)

View File

@@ -209,8 +209,11 @@ export class ExtensionsController {
tx.extension.updateMany({
where: { id, tenantId, deletedAt: null },
data: {
...(dto.name !== undefined ? { name: dto.name } : {}),
...(dto.callerIdName !== undefined ? { callerIdName: dto.callerIdName } : {}),
...(dto.callerIdNumber !== undefined ? { callerIdNumber: dto.callerIdNumber } : {}),
...(dto.context !== undefined ? { context: dto.context } : {}),
...(dto.codecs !== undefined ? { codecs: dto.codecs } : {}),
...(dto.maxRegistrations !== undefined ? { maxRegistrations: dto.maxRegistrations } : {}),
...(dto.callGroup !== undefined ? { callGroup: dto.callGroup } : {}),
},