feat: improve ticket export and navigation
This commit is contained in:
parent
0731c5d1ea
commit
7d6f3bea01
28 changed files with 1612 additions and 609 deletions
|
|
@ -50,20 +50,24 @@ export const list = query({
|
|||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
kind: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { tenantId, viewerId }) => {
|
||||
handler: async (ctx, { tenantId, viewerId, kind }) => {
|
||||
await requireStaff(ctx, viewerId, tenantId)
|
||||
const normalizedKind = (kind ?? "comment").toLowerCase()
|
||||
const templates = await ctx.db
|
||||
.query("commentTemplates")
|
||||
.withIndex("by_tenant", (q) => q.eq("tenantId", tenantId))
|
||||
.collect()
|
||||
|
||||
return templates
|
||||
.filter((template) => (template.kind ?? "comment") === normalizedKind)
|
||||
.sort((a, b) => a.title.localeCompare(b.title, "pt-BR", { sensitivity: "base" }))
|
||||
.map((template) => ({
|
||||
id: template._id,
|
||||
title: template.title,
|
||||
body: template.body,
|
||||
kind: template.kind ?? "comment",
|
||||
createdAt: template.createdAt,
|
||||
updatedAt: template.updatedAt,
|
||||
createdBy: template.createdBy,
|
||||
|
|
@ -78,8 +82,9 @@ export const create = mutation({
|
|||
actorId: v.id("users"),
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
kind: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { tenantId, actorId, title, body }) => {
|
||||
handler: async (ctx, { tenantId, actorId, title, body, kind }) => {
|
||||
await requireStaff(ctx, actorId, tenantId)
|
||||
const normalizedTitle = normalizeTitle(title)
|
||||
if (!normalizedTitle || normalizedTitle.length < 3) {
|
||||
|
|
@ -89,19 +94,21 @@ export const create = mutation({
|
|||
if (!sanitizedBody) {
|
||||
throw new ConvexError("Informe o conteúdo do template")
|
||||
}
|
||||
const normalizedKind = (kind ?? "comment").toLowerCase()
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("commentTemplates")
|
||||
.withIndex("by_tenant_title", (q) => q.eq("tenantId", tenantId).eq("title", normalizedTitle))
|
||||
.first()
|
||||
|
||||
if (existing) {
|
||||
if (existing && (existing.kind ?? "comment") === normalizedKind) {
|
||||
throw new ConvexError("Já existe um template com este título")
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const id = await ctx.db.insert("commentTemplates", {
|
||||
tenantId,
|
||||
kind: normalizedKind,
|
||||
title: normalizedTitle,
|
||||
body: sanitizedBody,
|
||||
createdBy: actorId,
|
||||
|
|
@ -121,8 +128,9 @@ export const update = mutation({
|
|||
actorId: v.id("users"),
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
kind: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, { templateId, tenantId, actorId, title, body }) => {
|
||||
handler: async (ctx, { templateId, tenantId, actorId, title, body, kind }) => {
|
||||
await requireStaff(ctx, actorId, tenantId)
|
||||
const template = await ctx.db.get(templateId)
|
||||
if (!template || template.tenantId !== tenantId) {
|
||||
|
|
@ -137,17 +145,19 @@ export const update = mutation({
|
|||
if (!sanitizedBody) {
|
||||
throw new ConvexError("Informe o conteúdo do template")
|
||||
}
|
||||
const normalizedKind = (kind ?? "comment").toLowerCase()
|
||||
|
||||
const duplicate = await ctx.db
|
||||
.query("commentTemplates")
|
||||
.withIndex("by_tenant_title", (q) => q.eq("tenantId", tenantId).eq("title", normalizedTitle))
|
||||
.first()
|
||||
if (duplicate && duplicate._id !== templateId) {
|
||||
if (duplicate && duplicate._id !== templateId && (duplicate.kind ?? "comment") === normalizedKind) {
|
||||
throw new ConvexError("Já existe um template com este título")
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
await ctx.db.patch(templateId, {
|
||||
kind: normalizedKind,
|
||||
title: normalizedTitle,
|
||||
body: sanitizedBody,
|
||||
updatedBy: actorId,
|
||||
|
|
|
|||
|
|
@ -65,6 +65,24 @@ function computeFingerprint(tenantId: string, companySlug: string | undefined, h
|
|||
return toHex(sha256(payload))
|
||||
}
|
||||
|
||||
function extractCollaboratorEmail(metadata: unknown): string | null {
|
||||
if (!metadata || typeof metadata !== "object") return null
|
||||
const record = metadata as Record<string, unknown>
|
||||
const collaborator = record["collaborator"]
|
||||
if (!collaborator || typeof collaborator !== "object") return null
|
||||
const email = (collaborator as { email?: unknown }).email
|
||||
if (typeof email !== "string") return null
|
||||
const trimmed = email.trim().toLowerCase()
|
||||
return trimmed || null
|
||||
}
|
||||
|
||||
function matchesExistingHardware(existing: Doc<"machines">, identifiers: NormalizedIdentifiers, hostname: string): boolean {
|
||||
const intersectsMac = existing.macAddresses.some((mac) => identifiers.macs.includes(mac))
|
||||
const intersectsSerial = existing.serialNumbers.some((serial) => identifiers.serials.includes(serial))
|
||||
const sameHostname = existing.hostname.trim().toLowerCase() === hostname.trim().toLowerCase()
|
||||
return intersectsMac || intersectsSerial || sameHostname
|
||||
}
|
||||
|
||||
function hashToken(token: string) {
|
||||
return toHex(sha256(token))
|
||||
}
|
||||
|
|
@ -305,11 +323,24 @@ export const register = mutation({
|
|||
const now = Date.now()
|
||||
const metadataPatch = args.metadata && typeof args.metadata === "object" ? (args.metadata as Record<string, unknown>) : undefined
|
||||
|
||||
const existing = await ctx.db
|
||||
let existing = await ctx.db
|
||||
.query("machines")
|
||||
.withIndex("by_tenant_fingerprint", (q) => q.eq("tenantId", tenantId).eq("fingerprint", fingerprint))
|
||||
.first()
|
||||
|
||||
if (!existing) {
|
||||
const collaboratorEmail = extractCollaboratorEmail(metadataPatch ?? args.metadata)
|
||||
if (collaboratorEmail) {
|
||||
const candidate = await ctx.db
|
||||
.query("machines")
|
||||
.withIndex("by_tenant_assigned_email", (q) => q.eq("tenantId", tenantId).eq("assignedUserEmail", collaboratorEmail))
|
||||
.first()
|
||||
if (candidate && matchesExistingHardware(candidate, identifiers, args.hostname)) {
|
||||
existing = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let machineId: Id<"machines">
|
||||
|
||||
if (existing) {
|
||||
|
|
@ -323,6 +354,7 @@ export const register = mutation({
|
|||
architecture: args.os.architecture,
|
||||
macAddresses: identifiers.macs,
|
||||
serialNumbers: identifiers.serials,
|
||||
fingerprint,
|
||||
metadata: metadataPatch ? mergeMetadata(existing.metadata, metadataPatch) : existing.metadata,
|
||||
lastHeartbeatAt: now,
|
||||
updatedAt: now,
|
||||
|
|
@ -834,6 +866,28 @@ export const getContext = query({
|
|||
},
|
||||
})
|
||||
|
||||
export const findByAuthEmail = query({
|
||||
args: {
|
||||
authEmail: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const normalizedEmail = args.authEmail.trim().toLowerCase()
|
||||
|
||||
const machine = await ctx.db
|
||||
.query("machines")
|
||||
.withIndex("by_auth_email", (q) => q.eq("authEmail", normalizedEmail))
|
||||
.first()
|
||||
|
||||
if (!machine) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
id: machine._id,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const linkAuthAccount = mutation({
|
||||
args: {
|
||||
machineId: v.id("machines"),
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ export default defineSchema({
|
|||
|
||||
commentTemplates: defineTable({
|
||||
tenantId: v.string(),
|
||||
kind: v.optional(v.string()),
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
createdBy: v.id("users"),
|
||||
|
|
@ -154,7 +155,8 @@ export default defineSchema({
|
|||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_tenant", ["tenantId"])
|
||||
.index("by_tenant_title", ["tenantId", "title"]),
|
||||
.index("by_tenant_title", ["tenantId", "title"])
|
||||
.index("by_tenant_kind", ["tenantId", "kind"]),
|
||||
|
||||
ticketWorkSessions: defineTable({
|
||||
ticketId: v.id("tickets"),
|
||||
|
|
@ -267,6 +269,7 @@ export default defineSchema({
|
|||
.index("by_tenant", ["tenantId"])
|
||||
.index("by_tenant_company", ["tenantId", "companyId"])
|
||||
.index("by_tenant_fingerprint", ["tenantId", "fingerprint"])
|
||||
.index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"])
|
||||
.index("by_auth_email", ["authEmail"]),
|
||||
|
||||
machineTokens: defineTable({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { MutationCtx, QueryCtx } from "./_generated/server";
|
|||
import { ConvexError, v } from "convex/values";
|
||||
import { Id, type Doc } from "./_generated/dataModel";
|
||||
|
||||
import { requireStaff, requireUser } from "./rbac";
|
||||
import { requireAdmin, requireStaff, requireUser } from "./rbac";
|
||||
|
||||
const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]);
|
||||
const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]);
|
||||
|
|
@ -1343,8 +1343,13 @@ export const playNext = mutation({
|
|||
});
|
||||
|
||||
export const remove = mutation({
|
||||
args: { ticketId: v.id("tickets") },
|
||||
handler: async (ctx, { ticketId }) => {
|
||||
args: { ticketId: v.id("tickets"), actorId: v.id("users") },
|
||||
handler: async (ctx, { ticketId, actorId }) => {
|
||||
const ticket = await ctx.db.get(ticketId)
|
||||
if (!ticket) {
|
||||
throw new ConvexError("Ticket não encontrado")
|
||||
}
|
||||
await requireAdmin(ctx, actorId, ticket.tenantId)
|
||||
// delete comments (and attachments)
|
||||
const comments = await ctx.db
|
||||
.query("ticketComments")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue