fix(reports): remove truncation cap in range collectors to avoid dropped records
feat(calendar): migrate to react-day-picker v9 and polish UI - Update classNames and CSS import (style.css) - Custom Dropdown via shadcn Select - Nav arrows aligned with caption (around) - Today highlight with cyan tone, weekdays in sentence case - Wider layout to avoid overflow; remove inner wrapper chore(tickets): make 'Patrimônio do computador (se houver)' optional - Backend hotfix to enforce optional + label on existing tenants - Hide required asterisk for this field in portal/new-ticket refactor(new-ticket): remove channel dropdown from admin/agent flow - Keep default channel as MANUAL feat(ux): simplify requester section and enlarge combobox trigger - Remove RequesterPreview redundancy; show company badge in trigger
This commit is contained in:
parent
e0ef66555d
commit
a8333c010f
28 changed files with 1752 additions and 455 deletions
|
|
@ -59,6 +59,107 @@ const TICKET_FORM_CONFIG = [
|
|||
},
|
||||
];
|
||||
|
||||
type TicketFormFieldSeed = {
|
||||
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.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function plainTextLength(html: string): number {
|
||||
try {
|
||||
const text = String(html)
|
||||
|
|
@ -184,6 +285,62 @@ function resolveFormEnabled(
|
|||
return baseEnabled
|
||||
}
|
||||
|
||||
async function ensureTicketFormDefaultsForTenant(ctx: MutationCtx, tenantId: string) {
|
||||
const now = Date.now();
|
||||
for (const template of TICKET_FORM_CONFIG) {
|
||||
const defaults = TICKET_FORM_DEFAULT_FIELDS[template.key] ?? [];
|
||||
if (!defaults.length) {
|
||||
continue;
|
||||
}
|
||||
const existing = await ctx.db
|
||||
.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,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const existingKeys = new Set(existing.map((field) => field.key));
|
||||
let order = existing.reduce((max, field) => Math.max(max, field.order ?? 0), 0);
|
||||
for (const field of defaults) {
|
||||
if (existingKeys.has(field.key)) {
|
||||
// Campo já existe: não sobrescrevemos personalizações do cliente, exceto hotfix acima
|
||||
continue;
|
||||
}
|
||||
order += 1;
|
||||
await ctx.db.insert("ticketFields", {
|
||||
tenantId,
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
description: field.description ?? "",
|
||||
type: field.type,
|
||||
required: Boolean(field.required),
|
||||
options: field.options?.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
})),
|
||||
scope: template.key,
|
||||
order,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAssigneeChangeComment(
|
||||
reason: string,
|
||||
context: { previousName: string; nextName: string },
|
||||
|
|
@ -2472,6 +2629,18 @@ export const markChatRead = mutation({
|
|||
},
|
||||
})
|
||||
|
||||
export const ensureTicketFormDefaults = mutation({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
actorId: v.id("users"),
|
||||
},
|
||||
handler: async (ctx, { tenantId, actorId }) => {
|
||||
await requireUser(ctx, actorId, tenantId);
|
||||
await ensureTicketFormDefaultsForTenant(ctx, tenantId);
|
||||
return { ok: true };
|
||||
},
|
||||
});
|
||||
|
||||
export async function submitCsatHandler(
|
||||
ctx: MutationCtx,
|
||||
{ ticketId, actorId, score, maxScore, comment }: { ticketId: Id<"tickets">; actorId: Id<"users">; score: number; maxScore?: number | null; comment?: string | null }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue