Ajusta placeholders, formulários e widgets
This commit is contained in:
parent
343f0c8c64
commit
b94cea2f9a
33 changed files with 2122 additions and 462 deletions
|
|
@ -7,6 +7,17 @@ import { Id, type Doc, type DataModel } from "./_generated/dataModel";
|
|||
import type { NamedTableInfo, Query as ConvexQuery } from "convex/server";
|
||||
|
||||
import { requireAdmin, requireStaff, requireUser } from "./rbac";
|
||||
import {
|
||||
OPTIONAL_ADMISSION_FIELD_KEYS,
|
||||
TICKET_FORM_CONFIG,
|
||||
TICKET_FORM_DEFAULT_FIELDS,
|
||||
type TicketFormFieldSeed,
|
||||
} from "./ticketForms.config";
|
||||
import {
|
||||
ensureTicketFormTemplatesForTenant,
|
||||
getTemplateByKey,
|
||||
normalizeFormTemplateKey,
|
||||
} from "./ticketFormTemplates";
|
||||
|
||||
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]);
|
||||
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
|
||||
|
|
@ -45,120 +56,13 @@ const MAX_COMMENT_CHARS = 20000;
|
|||
const DEFAULT_REOPEN_DAYS = 7;
|
||||
const MAX_REOPEN_DAYS = 14;
|
||||
|
||||
const TICKET_FORM_CONFIG = [
|
||||
{
|
||||
key: "admissao" as const,
|
||||
label: "Admissão de colaborador",
|
||||
description: "Coleta dados completos para novos colaboradores, incluindo informações pessoais e provisionamento de acesso.",
|
||||
defaultEnabled: true,
|
||||
},
|
||||
{
|
||||
key: "desligamento" as const,
|
||||
label: "Desligamento de colaborador",
|
||||
description: "Checklist de desligamento com orientações para revogar acessos e coletar equipamentos.",
|
||||
defaultEnabled: true,
|
||||
},
|
||||
];
|
||||
type AnyCtx = QueryCtx | MutationCtx;
|
||||
|
||||
type TicketFormFieldSeed = {
|
||||
type TemplateSummary = {
|
||||
key: string;
|
||||
label: string;
|
||||
type: "text" | "number" | "date" | "select" | "boolean";
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
options?: Array<{ value: string; label: string }>;
|
||||
};
|
||||
|
||||
const TICKET_FORM_DEFAULT_FIELDS: Record<string, TicketFormFieldSeed[]> = {
|
||||
admissao: [
|
||||
{ key: "solicitante_nome", label: "Nome do solicitante", type: "text", required: true, description: "Quem está solicitando a admissão." },
|
||||
{ key: "solicitante_telefone", label: "Telefone do solicitante", type: "text", required: true },
|
||||
{ key: "solicitante_ramal", label: "Ramal", type: "text" },
|
||||
{
|
||||
key: "solicitante_email",
|
||||
label: "E-mail do solicitante",
|
||||
type: "text",
|
||||
required: true,
|
||||
description: "Informe um e-mail válido para retornarmos atualizações.",
|
||||
},
|
||||
{ key: "colaborador_nome", label: "Nome do colaborador", type: "text", required: true },
|
||||
{ key: "colaborador_email_desejado", label: "E-mail do colaborador", type: "text", required: true, description: "Endereço de e-mail que deverá ser criado." },
|
||||
{ key: "colaborador_data_nascimento", label: "Data de nascimento", type: "date", required: true },
|
||||
{ key: "colaborador_rg", label: "RG", type: "text", required: true },
|
||||
{ key: "colaborador_cpf", label: "CPF", type: "text", required: true },
|
||||
{ key: "colaborador_data_inicio", label: "Data de início", type: "date", required: true },
|
||||
{ key: "colaborador_departamento", label: "Departamento", type: "text", required: true },
|
||||
{
|
||||
key: "colaborador_nova_contratacao",
|
||||
label: "O colaborador é uma nova contratação?",
|
||||
type: "select",
|
||||
required: true,
|
||||
description: "Informe se é uma nova contratação ou substituição.",
|
||||
options: [
|
||||
{ value: "nova", label: "Sim, nova contratação" },
|
||||
{ value: "substituicao", label: "Não, irá substituir alguém" },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "colaborador_substituicao",
|
||||
label: "Quem será substituído?",
|
||||
type: "text",
|
||||
description: "Preencha somente se for uma substituição.",
|
||||
},
|
||||
{
|
||||
key: "colaborador_grupos_email",
|
||||
label: "Grupos de e-mail necessários",
|
||||
type: "text",
|
||||
required: true,
|
||||
description: "Liste os grupos ou escreva 'Não se aplica'.",
|
||||
},
|
||||
{
|
||||
key: "colaborador_equipamento",
|
||||
label: "Equipamento disponível",
|
||||
type: "text",
|
||||
required: true,
|
||||
description: "Informe se já existe equipamento ou qual deverá ser disponibilizado.",
|
||||
},
|
||||
{
|
||||
key: "colaborador_permissoes_pasta",
|
||||
label: "Permissões de pastas",
|
||||
type: "text",
|
||||
required: true,
|
||||
description: "Indique quais pastas ou qual colaborador servirá de referência.",
|
||||
},
|
||||
{
|
||||
key: "colaborador_observacoes",
|
||||
label: "Observações adicionais",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "colaborador_patrimonio",
|
||||
label: "Patrimônio do computador (se houver)",
|
||||
type: "text",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
desligamento: [
|
||||
{ key: "contato_nome", label: "Contato responsável", type: "text", required: true },
|
||||
{ key: "contato_email", label: "E-mail do contato", type: "text", required: true },
|
||||
{ key: "contato_telefone", label: "Telefone do contato", type: "text" },
|
||||
{ key: "colaborador_nome", label: "Nome do colaborador", type: "text", required: true },
|
||||
{ key: "colaborador_departamento", label: "Departamento do colaborador", type: "text", required: true },
|
||||
{
|
||||
key: "colaborador_email",
|
||||
label: "E-mail do colaborador",
|
||||
type: "text",
|
||||
required: true,
|
||||
description: "Informe o e-mail que deve ser desativado.",
|
||||
},
|
||||
{
|
||||
key: "colaborador_patrimonio",
|
||||
label: "Patrimônio do computador",
|
||||
type: "text",
|
||||
description: "Informe o patrimônio se houver equipamento vinculado.",
|
||||
},
|
||||
],
|
||||
description: string;
|
||||
defaultEnabled: boolean;
|
||||
};
|
||||
|
||||
function plainTextLength(html: string): number {
|
||||
|
|
@ -182,20 +86,6 @@ function escapeHtml(input: string): string {
|
|||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function normalizeFormTemplateKey(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) return null;
|
||||
const normalized = trimmed
|
||||
.normalize("NFD")
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
function resolveReopenWindowDays(input?: number | null): number {
|
||||
if (typeof input !== "number" || !Number.isFinite(input)) {
|
||||
return DEFAULT_REOPEN_DAYS;
|
||||
|
|
@ -286,7 +176,32 @@ function resolveFormEnabled(
|
|||
return baseEnabled
|
||||
}
|
||||
|
||||
async function fetchTemplateSummaries(ctx: AnyCtx, tenantId: string): Promise<TemplateSummary[]> {
|
||||
const templates = await ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect();
|
||||
if (!templates.length) {
|
||||
return TICKET_FORM_CONFIG.map((template) => ({
|
||||
key: template.key,
|
||||
label: template.label,
|
||||
description: template.description,
|
||||
defaultEnabled: template.defaultEnabled,
|
||||
}));
|
||||
}
|
||||
return templates
|
||||
.filter((tpl) => tpl.isArchived !== true)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR"))
|
||||
.map((tpl) => ({
|
||||
key: tpl.key,
|
||||
label: tpl.label,
|
||||
description: tpl.description ?? "",
|
||||
defaultEnabled: tpl.defaultEnabled ?? true,
|
||||
}));
|
||||
}
|
||||
|
||||
async function ensureTicketFormDefaultsForTenant(ctx: MutationCtx, tenantId: string) {
|
||||
await ensureTicketFormTemplatesForTenant(ctx, tenantId);
|
||||
const now = Date.now();
|
||||
for (const template of TICKET_FORM_CONFIG) {
|
||||
const defaults = TICKET_FORM_DEFAULT_FIELDS[template.key] ?? [];
|
||||
|
|
@ -297,18 +212,23 @@ async function ensureTicketFormDefaultsForTenant(ctx: MutationCtx, tenantId: str
|
|||
.query("ticketFields")
|
||||
.withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", template.key))
|
||||
.collect();
|
||||
// Hotfix: garantir que "Patrimônio do computador (se houver)" seja opcional na admissão
|
||||
if (template.key === "admissao") {
|
||||
const patrimonio = existing.find((f) => f.key === "colaborador_patrimonio");
|
||||
if (patrimonio) {
|
||||
const shouldBeOptional = false;
|
||||
const needsRequiredFix = Boolean(patrimonio.required) !== shouldBeOptional;
|
||||
const desiredLabel = "Patrimônio do computador (se houver)";
|
||||
const needsLabelFix = (patrimonio.label ?? "").trim() !== desiredLabel;
|
||||
if (needsRequiredFix || needsLabelFix) {
|
||||
await ctx.db.patch(patrimonio._id, {
|
||||
required: shouldBeOptional,
|
||||
label: desiredLabel,
|
||||
for (const key of OPTIONAL_ADMISSION_FIELD_KEYS) {
|
||||
const field = existing.find((f) => f.key === key);
|
||||
if (!field) continue;
|
||||
const updates: Partial<Doc<"ticketFields">> = {};
|
||||
if (field.required) {
|
||||
updates.required = false;
|
||||
}
|
||||
if (key === "colaborador_patrimonio") {
|
||||
const desiredLabel = "Patrimônio do computador (se houver)";
|
||||
if ((field.label ?? "").trim() !== desiredLabel) {
|
||||
updates.label = desiredLabel;
|
||||
}
|
||||
}
|
||||
if (Object.keys(updates).length) {
|
||||
await ctx.db.patch(field._id, {
|
||||
...updates,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
|
@ -1242,6 +1162,7 @@ export const list = query({
|
|||
csatRatedAt: t.csatRatedAt ?? null,
|
||||
csatRatedBy: t.csatRatedBy ? String(t.csatRatedBy) : null,
|
||||
formTemplate: t.formTemplate ?? null,
|
||||
formTemplateLabel: t.formTemplateLabel ?? null,
|
||||
company: company
|
||||
? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false }
|
||||
: t.companyId || t.companySnapshot
|
||||
|
|
@ -1551,6 +1472,7 @@ export const getById = query({
|
|||
})),
|
||||
},
|
||||
formTemplate: t.formTemplate ?? null,
|
||||
formTemplateLabel: t.formTemplateLabel ?? null,
|
||||
chatEnabled: Boolean(t.chatEnabled),
|
||||
relatedTicketIds: Array.isArray(t.relatedTicketIds) ? t.relatedTicketIds.map((id) => String(id)) : [],
|
||||
resolvedWithTicketId: t.resolvedWithTicketId ? String(t.resolvedWithTicketId) : null,
|
||||
|
|
@ -1668,7 +1590,21 @@ export const create = mutation({
|
|||
machineDoc = machine
|
||||
}
|
||||
|
||||
const formTemplateKey = normalizeFormTemplateKey(args.formTemplate ?? null);
|
||||
let formTemplateKey = normalizeFormTemplateKey(args.formTemplate ?? null);
|
||||
let formTemplateLabel: string | null = null;
|
||||
if (formTemplateKey) {
|
||||
const templateDoc = await getTemplateByKey(ctx, args.tenantId, formTemplateKey);
|
||||
if (templateDoc && templateDoc.isArchived !== true) {
|
||||
formTemplateLabel = templateDoc.label;
|
||||
} else {
|
||||
const fallbackTemplate = TICKET_FORM_CONFIG.find((tpl) => tpl.key === formTemplateKey);
|
||||
if (fallbackTemplate) {
|
||||
formTemplateLabel = fallbackTemplate.label;
|
||||
} else {
|
||||
formTemplateKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
const chatEnabled = typeof args.chatEnabled === "boolean" ? args.chatEnabled : true;
|
||||
const normalizedCustomFields = await normalizeCustomFieldValues(
|
||||
ctx,
|
||||
|
|
@ -1752,6 +1688,7 @@ export const create = mutation({
|
|||
}
|
||||
: undefined,
|
||||
formTemplate: formTemplateKey ?? undefined,
|
||||
formTemplateLabel: formTemplateLabel ?? undefined,
|
||||
chatEnabled,
|
||||
working: false,
|
||||
activeSessionId: undefined,
|
||||
|
|
@ -2470,6 +2407,7 @@ export const listTicketForms = query({
|
|||
fieldsByScope.get(scope)!.push(definition)
|
||||
}
|
||||
|
||||
const templates = await fetchTemplateSummaries(ctx, tenantId)
|
||||
const forms = [] as Array<{
|
||||
key: string
|
||||
label: string
|
||||
|
|
@ -2485,7 +2423,7 @@ export const listTicketForms = query({
|
|||
}>
|
||||
}>
|
||||
|
||||
for (const template of TICKET_FORM_CONFIG) {
|
||||
for (const template of templates) {
|
||||
let enabled = resolveFormEnabled(template.key, template.defaultEnabled, settings as Doc<"ticketFormSettings">[], {
|
||||
companyId: viewerCompanyId,
|
||||
userId: viewer.user._id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue