Ajusta placeholders, formulários e widgets
This commit is contained in:
parent
343f0c8c64
commit
b94cea2f9a
33 changed files with 2122 additions and 462 deletions
280
convex/ticketFormTemplates.ts
Normal file
280
convex/ticketFormTemplates.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
"use server";
|
||||
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import type { MutationCtx, QueryCtx } from "./_generated/server";
|
||||
import { ConvexError, v } from "convex/values";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
|
||||
import { requireAdmin, requireStaff } from "./rbac";
|
||||
import { TICKET_FORM_CONFIG } from "./ticketForms.config";
|
||||
|
||||
type AnyCtx = MutationCtx | QueryCtx;
|
||||
|
||||
function slugify(input: string) {
|
||||
return input
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/[^\w\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
export function normalizeFormTemplateKey(input: string | null | undefined): string | null {
|
||||
if (!input) return null;
|
||||
const normalized = slugify(input);
|
||||
return normalized || null;
|
||||
}
|
||||
|
||||
async function templateKeyExists(ctx: AnyCtx, tenantId: string, key: string) {
|
||||
const existing = await ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant_key", (q) => q.eq("tenantId", tenantId).eq("key", key))
|
||||
.first();
|
||||
return Boolean(existing);
|
||||
}
|
||||
|
||||
export async function ensureTicketFormTemplatesForTenant(ctx: MutationCtx, tenantId: string) {
|
||||
const existing = await ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect();
|
||||
let order = existing.reduce((max, tpl) => Math.max(max, tpl.order ?? 0), 0);
|
||||
const now = Date.now();
|
||||
for (const template of TICKET_FORM_CONFIG) {
|
||||
const match = existing.find((tpl) => tpl.key === template.key);
|
||||
if (match) {
|
||||
const updates: Partial<Doc<"ticketFormTemplates">> = {};
|
||||
if (!match.baseTemplateKey) {
|
||||
updates.baseTemplateKey = template.key;
|
||||
}
|
||||
if (match.isSystem !== true) {
|
||||
updates.isSystem = true;
|
||||
}
|
||||
if (typeof match.defaultEnabled === "undefined") {
|
||||
updates.defaultEnabled = template.defaultEnabled;
|
||||
}
|
||||
if (Object.keys(updates).length) {
|
||||
await ctx.db.patch(match._id, {
|
||||
...updates,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
order += 1;
|
||||
await ctx.db.insert("ticketFormTemplates", {
|
||||
tenantId,
|
||||
key: template.key,
|
||||
label: template.label,
|
||||
description: template.description ?? undefined,
|
||||
defaultEnabled: template.defaultEnabled,
|
||||
baseTemplateKey: template.key,
|
||||
isSystem: true,
|
||||
isArchived: false,
|
||||
order,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTemplateByKey(ctx: AnyCtx, tenantId: string, key: string): Promise<Doc<"ticketFormTemplates"> | null> {
|
||||
return ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant_key", (q) => q.eq("tenantId", tenantId).eq("key", key))
|
||||
.first();
|
||||
}
|
||||
|
||||
async function generateTemplateKey(ctx: MutationCtx, tenantId: string, label: string) {
|
||||
const base = slugify(label) || `template-${Date.now()}`;
|
||||
let candidate = base;
|
||||
let suffix = 1;
|
||||
while (await templateKeyExists(ctx, tenantId, candidate)) {
|
||||
candidate = `${base}-${suffix}`;
|
||||
suffix += 1;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
async function cloneFieldsFromTemplate(ctx: MutationCtx, tenantId: string, sourceKey: string, targetKey: string) {
|
||||
const sourceFields = await ctx.db
|
||||
.query("ticketFields")
|
||||
.withIndex("by_tenant_scope", (q) => q.eq("tenantId", tenantId).eq("scope", sourceKey))
|
||||
.collect();
|
||||
if (sourceFields.length === 0) return;
|
||||
const ordered = await ctx.db
|
||||
.query("ticketFields")
|
||||
.withIndex("by_tenant_order", (q) => q.eq("tenantId", tenantId))
|
||||
.collect();
|
||||
let order = ordered.reduce((max, field) => Math.max(max, field.order ?? 0), 0);
|
||||
const now = Date.now();
|
||||
for (const field of sourceFields.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))) {
|
||||
order += 1;
|
||||
await ctx.db.insert("ticketFields", {
|
||||
tenantId,
|
||||
key: field.key,
|
||||
label: field.label,
|
||||
description: field.description ?? undefined,
|
||||
type: field.type,
|
||||
required: field.required,
|
||||
options: field.options ?? undefined,
|
||||
scope: targetKey,
|
||||
order,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function mapTemplate(template: Doc<"ticketFormTemplates">) {
|
||||
return {
|
||||
id: template._id,
|
||||
key: template.key,
|
||||
label: template.label,
|
||||
description: template.description ?? "",
|
||||
defaultEnabled: template.defaultEnabled ?? true,
|
||||
baseTemplateKey: template.baseTemplateKey ?? null,
|
||||
isSystem: Boolean(template.isSystem),
|
||||
isArchived: Boolean(template.isArchived),
|
||||
order: template.order ?? 0,
|
||||
createdAt: template.createdAt,
|
||||
updatedAt: template.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export const list = query({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
includeArchived: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, { tenantId, viewerId, includeArchived }) => {
|
||||
await requireAdmin(ctx, viewerId, tenantId);
|
||||
const templates = await ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect();
|
||||
return templates
|
||||
.filter((tpl) => includeArchived || tpl.isArchived !== true)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR"))
|
||||
.map(mapTemplate);
|
||||
},
|
||||
});
|
||||
|
||||
export const listActive = query({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
},
|
||||
handler: async (ctx, { tenantId, viewerId }) => {
|
||||
await requireStaff(ctx, viewerId, tenantId);
|
||||
const templates = await ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect();
|
||||
return templates
|
||||
.filter((tpl) => tpl.isArchived !== true)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.label.localeCompare(b.label, "pt-BR"))
|
||||
.map(mapTemplate);
|
||||
},
|
||||
});
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
actorId: v.id("users"),
|
||||
label: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
baseTemplateKey: v.optional(v.string()),
|
||||
cloneFields: v.optional(v.boolean()),
|
||||
},
|
||||
handler: async (ctx, { tenantId, actorId, label, description, baseTemplateKey, cloneFields }) => {
|
||||
await requireAdmin(ctx, actorId, tenantId);
|
||||
const trimmedLabel = label.trim();
|
||||
if (trimmedLabel.length < 3) {
|
||||
throw new ConvexError("Informe um nome com pelo menos 3 caracteres");
|
||||
}
|
||||
const key = await generateTemplateKey(ctx, tenantId, trimmedLabel);
|
||||
const templates = await ctx.db
|
||||
.query("ticketFormTemplates")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect();
|
||||
const order = (templates.reduce((max, tpl) => Math.max(max, tpl.order ?? 0), 0) ?? 0) + 1;
|
||||
const now = Date.now();
|
||||
const templateId = await ctx.db.insert("ticketFormTemplates", {
|
||||
tenantId,
|
||||
key,
|
||||
label: trimmedLabel,
|
||||
description: description?.trim() || undefined,
|
||||
defaultEnabled: true,
|
||||
baseTemplateKey: baseTemplateKey ?? undefined,
|
||||
isSystem: false,
|
||||
isArchived: false,
|
||||
order,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
createdBy: actorId,
|
||||
updatedBy: actorId,
|
||||
});
|
||||
if (baseTemplateKey && cloneFields) {
|
||||
await cloneFieldsFromTemplate(ctx, tenantId, baseTemplateKey, key);
|
||||
}
|
||||
return templateId;
|
||||
},
|
||||
});
|
||||
|
||||
export const update = mutation({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
actorId: v.id("users"),
|
||||
templateId: v.id("ticketFormTemplates"),
|
||||
label: v.string(),
|
||||
description: v.optional(v.string()),
|
||||
isArchived: v.optional(v.boolean()),
|
||||
defaultEnabled: v.optional(v.boolean()),
|
||||
order: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, { tenantId, actorId, templateId, label, description, isArchived, defaultEnabled, order }) => {
|
||||
await requireAdmin(ctx, actorId, tenantId);
|
||||
const template = await ctx.db.get(templateId);
|
||||
if (!template || template.tenantId !== tenantId) {
|
||||
throw new ConvexError("Template não encontrado");
|
||||
}
|
||||
const trimmedLabel = label.trim();
|
||||
if (trimmedLabel.length < 3) {
|
||||
throw new ConvexError("Informe um nome com pelo menos 3 caracteres");
|
||||
}
|
||||
await ctx.db.patch(templateId, {
|
||||
label: trimmedLabel,
|
||||
description: description?.trim() || undefined,
|
||||
isArchived: typeof isArchived === "boolean" ? isArchived : template.isArchived ?? false,
|
||||
defaultEnabled: typeof defaultEnabled === "boolean" ? defaultEnabled : template.defaultEnabled ?? true,
|
||||
order: typeof order === "number" ? order : template.order ?? 0,
|
||||
updatedAt: Date.now(),
|
||||
updatedBy: actorId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const archive = mutation({
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
actorId: v.id("users"),
|
||||
templateId: v.id("ticketFormTemplates"),
|
||||
archived: v.boolean(),
|
||||
},
|
||||
handler: async (ctx, { tenantId, actorId, templateId, archived }) => {
|
||||
await requireAdmin(ctx, actorId, tenantId);
|
||||
const template = await ctx.db.get(templateId);
|
||||
if (!template || template.tenantId !== tenantId) {
|
||||
throw new ConvexError("Template não encontrado");
|
||||
}
|
||||
await ctx.db.patch(templateId, {
|
||||
isArchived: archived,
|
||||
updatedAt: Date.now(),
|
||||
updatedBy: actorId,
|
||||
});
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue