feat(devices,custom-fields,csat,portal):\n- Editor de campos personalizados (inclui multiselect) e exibição no detalhe\n- Campos personalizados disponíveis nas colunas/templates de exportação\n- Move cópia de e-mail para ícone inline abaixo do nome do dispositivo\n- Portal: banner para avaliar último chamado e CSAT no detalhe\n- Tickets list inclui campos de CSAT para detectar pendências

This commit is contained in:
codex-bot 2025-11-04 14:12:21 -03:00
parent 06deb99bcd
commit c2c5707a97
7 changed files with 299 additions and 14 deletions

View file

@ -5,7 +5,7 @@ import type { Id } from "./_generated/dataModel"
import { requireAdmin, requireUser } from "./rbac"
const FIELD_TYPES = ["text", "number", "select", "date", "boolean"] as const
const FIELD_TYPES = ["text", "number", "select", "multiselect", "date", "boolean"] as const
type FieldType = (typeof FIELD_TYPES)[number]
type AnyCtx = MutationCtx | QueryCtx
@ -31,7 +31,7 @@ async function ensureUniqueKey(ctx: AnyCtx, tenantId: string, key: string, exclu
}
function validateOptions(type: FieldType, options: { value: string; label: string }[] | undefined) {
if (type === "select" && (!options || options.length === 0)) {
if ((type === "select" || type === "multiselect") && (!options || options.length === 0)) {
throw new ConvexError("Campos de seleção precisam de pelo menos uma opção")
}
}

View file

@ -138,6 +138,19 @@ function formatDeviceCustomFieldDisplay(
const option = options?.find((opt) => opt.value === raw || opt.label === raw)
return option?.label ?? raw
}
case "multiselect": {
const arr = Array.isArray(value)
? value
: typeof value === "string"
? value.split(",").map((s) => s.trim()).filter(Boolean)
: []
if (arr.length === 0) return null
const labels = arr.map((raw) => {
const opt = options?.find((o) => o.value === raw || o.label === raw)
return opt?.label ?? String(raw)
})
return labels.join(", ")
}
default:
try {
return JSON.stringify(value)

View file

@ -1236,6 +1236,11 @@ export const list = query({
priority: t.priority,
channel: t.channel,
queue: queueName,
csatScore: typeof t.csatScore === "number" ? t.csatScore : null,
csatMaxScore: typeof t.csatMaxScore === "number" ? t.csatMaxScore : null,
csatComment: typeof t.csatComment === "string" && t.csatComment.trim().length > 0 ? t.csatComment.trim() : null,
csatRatedAt: t.csatRatedAt ?? null,
csatRatedBy: t.csatRatedBy ? String(t.csatRatedBy) : null,
formTemplate: t.formTemplate ?? null,
company: company
? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false }