sistema-de-chamados/convex/schema.ts
rever-tecnologia 33f0cc2e13
Some checks failed
CI/CD Web + Desktop / Detect changes (push) Successful in 8s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m45s
Quality Checks / Lint, Test and Build (push) Successful in 3m58s
CI/CD Web + Desktop / Deploy Convex functions (push) Failing after 1m17s
feat: adiciona SLA por empresa e modal de exclusao de automacoes
## SLA por Empresa
- Adiciona tabela companySlaSettings no schema
- Cria convex/companySlas.ts com queries e mutations
- Modifica resolveTicketSlaSnapshot para verificar SLA da empresa primeiro
- Fallback: empresa > categoria > padrao

## Modal de Exclusao de Automacoes
- Substitui confirm() nativo por Dialog gracioso
- Segue padrao do delete-ticket-dialog

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 18:44:05 -03:00

977 lines
32 KiB
TypeScript

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
const gridLayoutItem = v.object({
i: v.string(),
x: v.number(),
y: v.number(),
w: v.number(),
h: v.number(),
minW: v.optional(v.number()),
minH: v.optional(v.number()),
static: v.optional(v.boolean()),
});
const widgetLayout = v.object({
x: v.number(),
y: v.number(),
w: v.number(),
h: v.number(),
minW: v.optional(v.number()),
minH: v.optional(v.number()),
static: v.optional(v.boolean()),
});
const tvSection = v.object({
id: v.string(),
title: v.optional(v.string()),
description: v.optional(v.string()),
widgetKeys: v.array(v.string()),
durationSeconds: v.optional(v.number()),
});
export default defineSchema({
users: defineTable({
tenantId: v.string(),
name: v.string(),
email: v.string(),
role: v.optional(v.string()),
jobTitle: v.optional(v.string()),
managerId: v.optional(v.id("users")),
avatarUrl: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
companyId: v.optional(v.id("companies")),
})
.index("by_tenant_email", ["tenantId", "email"])
.index("by_tenant_role", ["tenantId", "role"])
.index("by_tenant", ["tenantId"])
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant_manager", ["tenantId", "managerId"]),
companies: defineTable({
tenantId: v.string(),
name: v.string(),
slug: v.string(),
provisioningCode: v.optional(v.string()),
isAvulso: v.optional(v.boolean()),
contractedHoursPerMonth: v.optional(v.number()),
cnpj: v.optional(v.string()),
domain: v.optional(v.string()),
phone: v.optional(v.string()),
description: v.optional(v.string()),
address: v.optional(v.string()),
legalName: v.optional(v.string()),
tradeName: v.optional(v.string()),
stateRegistration: v.optional(v.string()),
stateRegistrationType: v.optional(v.string()),
primaryCnae: v.optional(v.string()),
timezone: v.optional(v.string()),
businessHours: v.optional(v.any()),
supportEmail: v.optional(v.string()),
billingEmail: v.optional(v.string()),
contactPreferences: v.optional(v.any()),
clientDomains: v.optional(v.array(v.string())),
communicationChannels: v.optional(v.any()),
fiscalAddress: v.optional(v.any()),
hasBranches: v.optional(v.boolean()),
regulatedEnvironments: v.optional(v.array(v.string())),
privacyPolicyAccepted: v.optional(v.boolean()),
privacyPolicyReference: v.optional(v.string()),
privacyPolicyMetadata: v.optional(v.any()),
contracts: v.optional(v.any()),
contacts: v.optional(v.any()),
locations: v.optional(v.any()),
sla: v.optional(v.any()),
reopenWindowDays: v.optional(v.number()),
tags: v.optional(v.array(v.string())),
customFields: v.optional(v.any()),
notes: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_tenant_slug", ["tenantId", "slug"])
.index("by_tenant", ["tenantId"])
.index("by_provisioning_code", ["provisioningCode"]),
alerts: defineTable({
tenantId: v.string(),
companyId: v.optional(v.id("companies")),
companyName: v.string(),
usagePct: v.number(),
threshold: v.number(),
range: v.string(),
recipients: v.array(v.string()),
createdAt: v.number(),
deliveredCount: v.number(),
})
.index("by_tenant_created", ["tenantId", "createdAt"])
.index("by_tenant", ["tenantId"]),
dashboards: defineTable({
tenantId: v.string(),
name: v.string(),
description: v.optional(v.string()),
aspectRatio: v.optional(v.string()),
theme: v.optional(v.string()),
filters: v.optional(v.any()),
layout: v.optional(v.array(gridLayoutItem)),
sections: v.optional(v.array(tvSection)),
tvIntervalSeconds: v.optional(v.number()),
readySelector: v.optional(v.string()),
createdBy: v.id("users"),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
isArchived: v.optional(v.boolean()),
})
.index("by_tenant", ["tenantId"])
.index("by_tenant_created", ["tenantId", "createdAt"]),
dashboardWidgets: defineTable({
tenantId: v.string(),
dashboardId: v.id("dashboards"),
widgetKey: v.string(),
title: v.optional(v.string()),
type: v.string(),
config: v.any(),
layout: v.optional(widgetLayout),
order: v.number(),
createdBy: v.id("users"),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
isHidden: v.optional(v.boolean()),
})
.index("by_dashboard", ["dashboardId"])
.index("by_dashboard_order", ["dashboardId", "order"])
.index("by_dashboard_key", ["dashboardId", "widgetKey"])
.index("by_tenant", ["tenantId"]),
metricDefinitions: defineTable({
tenantId: v.string(),
key: v.string(),
name: v.string(),
description: v.optional(v.string()),
version: v.number(),
definition: v.optional(v.any()),
createdBy: v.id("users"),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
tags: v.optional(v.array(v.string())),
})
.index("by_tenant_key", ["tenantId", "key"])
.index("by_tenant", ["tenantId"]),
dashboardShares: defineTable({
tenantId: v.string(),
dashboardId: v.id("dashboards"),
audience: v.string(),
token: v.optional(v.string()),
expiresAt: v.optional(v.number()),
canEdit: v.boolean(),
createdBy: v.id("users"),
createdAt: v.number(),
lastAccessAt: v.optional(v.number()),
})
.index("by_dashboard", ["dashboardId"])
.index("by_token", ["token"])
.index("by_tenant", ["tenantId"]),
queues: defineTable({
tenantId: v.string(),
name: v.string(),
slug: v.string(),
teamId: v.optional(v.id("teams")),
})
.index("by_tenant_slug", ["tenantId", "slug"])
.index("by_tenant_team", ["tenantId", "teamId"])
.index("by_tenant_name", ["tenantId", "name"])
.index("by_tenant", ["tenantId"]),
teams: defineTable({
tenantId: v.string(),
name: v.string(),
description: v.optional(v.string()),
}).index("by_tenant_name", ["tenantId", "name"]),
slaPolicies: defineTable({
tenantId: v.string(),
name: v.string(),
description: v.optional(v.string()),
timeToFirstResponse: v.optional(v.number()), // minutes
timeToResolution: v.optional(v.number()), // minutes
}).index("by_tenant_name", ["tenantId", "name"]),
tickets: defineTable({
tenantId: v.string(),
reference: v.number(),
subject: v.string(),
summary: v.optional(v.string()),
description: v.optional(v.string()),
status: v.string(),
priority: v.string(),
channel: v.string(),
queueId: v.optional(v.id("queues")),
categoryId: v.optional(v.id("ticketCategories")),
subcategoryId: v.optional(v.id("ticketSubcategories")),
requesterId: v.id("users"),
requesterSnapshot: v.optional(
v.object({
name: v.string(),
email: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
})
),
assigneeId: v.optional(v.id("users")),
assigneeSnapshot: v.optional(
v.object({
name: v.string(),
email: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
})
),
companyId: v.optional(v.id("companies")),
companySnapshot: v.optional(
v.object({
name: v.string(),
slug: v.optional(v.string()),
isAvulso: v.optional(v.boolean()),
})
),
machineId: v.optional(v.id("machines")),
machineSnapshot: v.optional(
v.object({
hostname: v.optional(v.string()),
persona: v.optional(v.string()),
assignedUserName: v.optional(v.string()),
assignedUserEmail: v.optional(v.string()),
status: v.optional(v.string()),
})
),
working: v.optional(v.boolean()),
slaPolicyId: v.optional(v.id("slaPolicies")),
slaSnapshot: v.optional(
v.object({
categoryId: v.optional(v.id("ticketCategories")),
categoryName: v.optional(v.string()),
priority: v.optional(v.string()),
responseTargetMinutes: v.optional(v.number()),
responseMode: v.optional(v.string()),
solutionTargetMinutes: v.optional(v.number()),
solutionMode: v.optional(v.string()),
alertThreshold: v.optional(v.number()),
pauseStatuses: v.optional(v.array(v.string())),
})
),
slaResponseDueAt: v.optional(v.number()),
slaSolutionDueAt: v.optional(v.number()),
slaResponseStatus: v.optional(v.string()),
slaSolutionStatus: v.optional(v.string()),
slaPausedAt: v.optional(v.number()),
slaPausedBy: v.optional(v.string()),
slaPausedMs: v.optional(v.number()),
dueAt: v.optional(v.number()), // ms since epoch
visitStatus: v.optional(v.string()),
visitPerformedAt: v.optional(v.number()),
firstResponseAt: v.optional(v.number()),
resolvedAt: v.optional(v.number()),
closedAt: v.optional(v.number()),
updatedAt: v.number(),
createdAt: v.number(),
tags: v.optional(v.array(v.string())),
customFields: v.optional(
v.array(
v.object({
fieldId: v.id("ticketFields"),
fieldKey: v.string(),
label: v.string(),
type: v.string(),
value: v.any(),
displayValue: v.optional(v.string()),
})
)
),
csatScore: v.optional(v.number()),
csatMaxScore: v.optional(v.number()),
csatComment: v.optional(v.string()),
csatRatedAt: v.optional(v.number()),
csatRatedBy: v.optional(v.id("users")),
csatAssigneeId: v.optional(v.id("users")),
csatAssigneeSnapshot: v.optional(
v.object({
name: v.string(),
email: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
})
),
formTemplate: v.optional(v.string()),
formTemplateLabel: v.optional(v.string()),
checklist: v.optional(
v.array(
v.object({
id: v.string(),
text: v.string(),
description: v.optional(v.string()),
type: v.optional(v.string()), // "checkbox" | "question"
options: v.optional(v.array(v.string())), // Para tipo "question": ["Sim", "Nao", ...]
answer: v.optional(v.string()), // Resposta selecionada para tipo "question"
done: v.boolean(),
required: v.optional(v.boolean()),
templateId: v.optional(v.id("ticketChecklistTemplates")),
templateItemId: v.optional(v.string()),
templateDescription: v.optional(v.string()), // Descricao do template (copiada ao aplicar)
createdAt: v.optional(v.number()),
createdBy: v.optional(v.id("users")),
doneAt: v.optional(v.number()),
doneBy: v.optional(v.id("users")),
})
)
),
relatedTicketIds: v.optional(v.array(v.id("tickets"))),
resolvedWithTicketId: v.optional(v.id("tickets")),
reopenDeadline: v.optional(v.number()),
reopenedAt: v.optional(v.number()),
chatEnabled: v.optional(v.boolean()),
totalWorkedMs: v.optional(v.number()),
internalWorkedMs: v.optional(v.number()),
externalWorkedMs: v.optional(v.number()),
activeSessionId: v.optional(v.id("ticketWorkSessions")),
})
.index("by_tenant_status", ["tenantId", "status"])
.index("by_tenant_queue", ["tenantId", "queueId"])
.index("by_tenant_assignee", ["tenantId", "assigneeId"])
.index("by_tenant_reference", ["tenantId", "reference"])
.index("by_tenant_requester", ["tenantId", "requesterId"])
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant_machine", ["tenantId", "machineId"])
.index("by_tenant_category", ["tenantId", "categoryId"])
.index("by_tenant_subcategory", ["tenantId", "subcategoryId"])
.index("by_tenant_sla_policy", ["tenantId", "slaPolicyId"])
.index("by_tenant", ["tenantId"])
.index("by_tenant_created", ["tenantId", "createdAt"])
.index("by_tenant_resolved", ["tenantId", "resolvedAt"])
.index("by_tenant_company_created", ["tenantId", "companyId", "createdAt"])
.index("by_tenant_company_resolved", ["tenantId", "companyId", "resolvedAt"]),
ticketComments: defineTable({
ticketId: v.id("tickets"),
authorId: v.id("users"),
visibility: v.string(), // PUBLIC | INTERNAL
body: v.string(),
authorSnapshot: v.optional(
v.object({
name: v.string(),
email: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
})
),
attachments: v.optional(
v.array(
v.object({
storageId: v.id("_storage"),
name: v.string(),
size: v.optional(v.number()),
type: v.optional(v.string()),
})
)
),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_ticket", ["ticketId"])
.index("by_author", ["authorId"]),
ticketEvents: defineTable({
ticketId: v.id("tickets"),
type: v.string(),
payload: v.optional(v.any()),
createdAt: v.number(),
}).index("by_ticket", ["ticketId"]),
ticketAutomations: defineTable({
tenantId: v.string(),
name: v.string(),
enabled: v.boolean(),
trigger: v.string(),
timing: v.string(), // IMMEDIATE | DELAYED
delayMs: v.optional(v.number()),
conditions: v.optional(v.any()),
actions: v.any(),
createdBy: v.id("users"),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
runCount: v.optional(v.number()),
lastRunAt: v.optional(v.number()),
})
.index("by_tenant", ["tenantId"])
.index("by_tenant_enabled", ["tenantId", "enabled"])
.index("by_tenant_trigger", ["tenantId", "trigger"]),
ticketAutomationRuns: defineTable({
tenantId: v.string(),
automationId: v.id("ticketAutomations"),
ticketId: v.id("tickets"),
eventType: v.string(),
status: v.string(), // SUCCESS | SKIPPED | ERROR
matched: v.boolean(),
error: v.optional(v.string()),
actionsApplied: v.optional(v.any()),
createdAt: v.number(),
})
.index("by_tenant_created", ["tenantId", "createdAt"])
.index("by_automation_created", ["automationId", "createdAt"])
.index("by_ticket", ["ticketId"]),
ticketChatMessages: defineTable({
ticketId: v.id("tickets"),
authorId: v.id("users"),
authorSnapshot: v.optional(
v.object({
name: v.string(),
email: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
teams: v.optional(v.array(v.string())),
})
),
body: v.string(),
attachments: v.optional(
v.array(
v.object({
storageId: v.id("_storage"),
name: v.string(),
size: v.optional(v.number()),
type: v.optional(v.string()),
})
)
),
notifiedAt: v.optional(v.number()),
createdAt: v.number(),
updatedAt: v.number(),
tenantId: v.string(),
companyId: v.optional(v.id("companies")),
readBy: v.optional(
v.array(
v.object({
userId: v.id("users"),
readAt: v.number(),
})
)
),
})
.index("by_ticket_created", ["ticketId", "createdAt"])
.index("by_tenant_created", ["tenantId", "createdAt"]),
// Sessoes de chat ao vivo entre agente (dashboard) e cliente (Raven desktop)
liveChatSessions: defineTable({
tenantId: v.string(),
ticketId: v.id("tickets"),
machineId: v.id("machines"),
agentId: v.id("users"),
agentSnapshot: v.optional(
v.object({
name: v.string(),
email: v.optional(v.string()),
avatarUrl: v.optional(v.string()),
})
),
status: v.string(), // ACTIVE | ENDED
startedAt: v.number(),
endedAt: v.optional(v.number()),
lastActivityAt: v.number(),
lastAgentMessageAt: v.optional(v.number()), // Timestamp da ultima mensagem do agente (para deteccao confiavel)
unreadByMachine: v.optional(v.number()),
unreadByAgent: v.optional(v.number()),
})
.index("by_ticket", ["ticketId"])
.index("by_machine_status", ["machineId", "status"])
.index("by_tenant_machine", ["tenantId", "machineId"])
.index("by_tenant_status", ["tenantId", "status"])
.index("by_status_lastActivity", ["status", "lastActivityAt"]),
commentTemplates: defineTable({
tenantId: v.string(),
kind: v.optional(v.string()),
title: v.string(),
body: v.string(),
createdBy: v.id("users"),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_tenant", ["tenantId"])
.index("by_tenant_title", ["tenantId", "title"])
.index("by_tenant_kind", ["tenantId", "kind"]),
ticketWorkSessions: defineTable({
ticketId: v.id("tickets"),
agentId: v.id("users"),
workType: v.optional(v.string()), // INTERNAL | EXTERNAL
startedAt: v.number(),
stoppedAt: v.optional(v.number()),
durationMs: v.optional(v.number()),
pauseReason: v.optional(v.string()),
pauseNote: v.optional(v.string()),
})
.index("by_ticket", ["ticketId"])
.index("by_ticket_agent", ["ticketId", "agentId"])
.index("by_agent", ["agentId"]),
incidents: defineTable({
tenantId: v.string(),
title: v.string(),
status: v.string(),
severity: v.string(),
impactSummary: v.optional(v.string()),
affectedQueues: v.array(v.string()),
ownerId: v.optional(v.id("users")),
ownerName: v.optional(v.string()),
ownerEmail: v.optional(v.string()),
startedAt: v.number(),
updatedAt: v.number(),
resolvedAt: v.optional(v.number()),
timeline: v.array(
v.object({
id: v.string(),
authorId: v.id("users"),
authorName: v.optional(v.string()),
message: v.string(),
type: v.optional(v.string()),
createdAt: v.number(),
})
),
})
.index("by_tenant_status", ["tenantId", "status"])
.index("by_tenant_updated", ["tenantId", "updatedAt"])
.index("by_tenant", ["tenantId"]),
ticketCategories: defineTable({
tenantId: v.string(),
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
order: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_tenant_slug", ["tenantId", "slug"])
.index("by_tenant_order", ["tenantId", "order"])
.index("by_tenant", ["tenantId"]),
ticketSubcategories: defineTable({
tenantId: v.string(),
categoryId: v.id("ticketCategories"),
name: v.string(),
slug: v.string(),
order: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_category_order", ["categoryId", "order"])
.index("by_category_slug", ["categoryId", "slug"])
.index("by_tenant_slug", ["tenantId", "slug"]),
categorySlaSettings: defineTable({
tenantId: v.string(),
categoryId: v.id("ticketCategories"),
priority: v.string(),
responseTargetMinutes: v.optional(v.number()),
responseMode: v.optional(v.string()),
solutionTargetMinutes: v.optional(v.number()),
solutionMode: v.optional(v.string()),
alertThreshold: v.optional(v.number()),
pauseStatuses: v.optional(v.array(v.string())),
calendarType: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
actorId: v.optional(v.id("users")),
})
.index("by_tenant_category_priority", ["tenantId", "categoryId", "priority"])
.index("by_tenant_category", ["tenantId", "categoryId"]),
// SLA por empresa - permite configurar políticas de SLA específicas por cliente
// Quando um ticket é criado, o sistema busca primeiro aqui antes de usar categorySlaSettings
companySlaSettings: defineTable({
tenantId: v.string(),
companyId: v.id("companies"),
// Se categoryId for null, aplica-se a todas as categorias da empresa
categoryId: v.optional(v.id("ticketCategories")),
priority: v.string(), // URGENT, HIGH, MEDIUM, LOW, DEFAULT
responseTargetMinutes: v.optional(v.number()),
responseMode: v.optional(v.string()), // "business" | "calendar"
solutionTargetMinutes: v.optional(v.number()),
solutionMode: v.optional(v.string()), // "business" | "calendar"
alertThreshold: v.optional(v.number()), // 0.1 a 0.95 (ex: 0.8 = 80%)
pauseStatuses: v.optional(v.array(v.string())),
calendarType: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
actorId: v.optional(v.id("users")),
})
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant_company_category", ["tenantId", "companyId", "categoryId"])
.index("by_tenant_company_category_priority", ["tenantId", "companyId", "categoryId", "priority"]),
ticketFields: defineTable({
tenantId: v.string(),
key: v.string(),
label: v.string(),
type: v.string(),
companyId: v.optional(v.id("companies")),
description: v.optional(v.string()),
required: v.boolean(),
order: v.number(),
options: v.optional(
v.array(
v.object({
value: v.string(),
label: v.string(),
})
)
),
scope: v.optional(v.string()),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_tenant_key", ["tenantId", "key"])
.index("by_tenant_order", ["tenantId", "order"])
.index("by_tenant_scope", ["tenantId", "scope"])
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant", ["tenantId"]),
ticketFormSettings: defineTable({
tenantId: v.string(),
template: v.string(),
scope: v.string(), // tenant | company | user
companyId: v.optional(v.id("companies")),
userId: v.optional(v.id("users")),
enabled: v.boolean(),
createdAt: v.number(),
updatedAt: v.number(),
actorId: v.optional(v.id("users")),
})
.index("by_tenant_template_scope", ["tenantId", "template", "scope"])
.index("by_tenant_template_company", ["tenantId", "template", "companyId"])
.index("by_tenant_template_user", ["tenantId", "template", "userId"])
.index("by_tenant", ["tenantId"]),
ticketFormTemplates: defineTable({
tenantId: v.string(),
key: v.string(),
label: v.string(),
description: v.optional(v.string()),
defaultEnabled: v.optional(v.boolean()),
baseTemplateKey: v.optional(v.string()),
isSystem: v.optional(v.boolean()),
isArchived: v.optional(v.boolean()),
order: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
createdBy: v.optional(v.id("users")),
updatedBy: v.optional(v.id("users")),
})
.index("by_tenant", ["tenantId"])
.index("by_tenant_key", ["tenantId", "key"])
.index("by_tenant_active", ["tenantId", "isArchived"]),
ticketChecklistTemplates: defineTable({
tenantId: v.string(),
name: v.string(),
description: v.optional(v.string()),
companyId: v.optional(v.id("companies")),
items: v.array(
v.object({
id: v.string(),
text: v.string(),
description: v.optional(v.string()),
type: v.optional(v.string()), // "checkbox" | "question"
options: v.optional(v.array(v.string())), // Para tipo "question": ["Sim", "Nao", ...]
required: v.optional(v.boolean()),
})
),
isArchived: v.optional(v.boolean()),
createdAt: v.number(),
updatedAt: v.number(),
createdBy: v.optional(v.id("users")),
updatedBy: v.optional(v.id("users")),
})
.index("by_tenant", ["tenantId"])
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant_active", ["tenantId", "isArchived"]),
userInvites: defineTable({
tenantId: v.string(),
inviteId: v.string(),
email: v.string(),
name: v.optional(v.string()),
role: v.string(),
status: v.string(),
token: v.string(),
expiresAt: v.number(),
createdAt: v.number(),
createdById: v.optional(v.string()),
acceptedAt: v.optional(v.number()),
acceptedById: v.optional(v.string()),
revokedAt: v.optional(v.number()),
revokedById: v.optional(v.string()),
revokedReason: v.optional(v.string()),
})
.index("by_tenant", ["tenantId"])
.index("by_token", ["tenantId", "token"])
.index("by_invite", ["tenantId", "inviteId"]),
machines: defineTable({
tenantId: v.string(),
companyId: v.optional(v.id("companies")),
companySlug: v.optional(v.string()),
authUserId: v.optional(v.string()),
authEmail: v.optional(v.string()),
persona: v.optional(v.string()),
assignedUserId: v.optional(v.id("users")),
assignedUserEmail: v.optional(v.string()),
assignedUserName: v.optional(v.string()),
assignedUserRole: v.optional(v.string()),
linkedUserIds: v.optional(v.array(v.id("users"))),
hostname: v.string(),
osName: v.string(),
osVersion: v.optional(v.string()),
architecture: v.optional(v.string()),
macAddresses: v.array(v.string()),
serialNumbers: v.array(v.string()),
fingerprint: v.string(),
metadata: v.optional(v.any()),
displayName: v.optional(v.string()),
deviceType: v.optional(v.string()),
devicePlatform: v.optional(v.string()),
deviceProfile: v.optional(v.any()),
managementMode: v.optional(v.string()),
customFields: v.optional(
v.array(
v.object({
fieldId: v.id("deviceFields"),
fieldKey: v.string(),
label: v.string(),
type: v.string(),
value: v.any(),
displayValue: v.optional(v.string()),
})
)
),
lastHeartbeatAt: v.optional(v.number()),
status: v.optional(v.string()),
isActive: v.optional(v.boolean()),
createdAt: v.number(),
updatedAt: v.number(),
registeredBy: v.optional(v.string()),
remoteAccess: v.optional(v.any()),
usbPolicy: v.optional(v.string()), // ALLOW | BLOCK_ALL | READONLY
usbPolicyAppliedAt: v.optional(v.number()),
usbPolicyStatus: v.optional(v.string()), // PENDING | APPLIED | FAILED
usbPolicyError: v.optional(v.string()),
usbPolicyReportedAt: v.optional(v.number()),
})
.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_tenant_hostname", ["tenantId", "hostname"])
.index("by_auth_email", ["authEmail"])
.index("by_usbPolicyStatus", ["usbPolicyStatus"]),
usbPolicyEvents: defineTable({
tenantId: v.string(),
machineId: v.id("machines"),
actorId: v.optional(v.id("users")),
actorEmail: v.optional(v.string()),
actorName: v.optional(v.string()),
oldPolicy: v.optional(v.string()),
newPolicy: v.string(),
status: v.string(), // PENDING | APPLIED | FAILED
error: v.optional(v.string()),
appliedAt: v.optional(v.number()),
createdAt: v.number(),
})
.index("by_machine", ["machineId"])
.index("by_machine_created", ["machineId", "createdAt"])
.index("by_tenant_created", ["tenantId", "createdAt"]),
machineAlerts: defineTable({
tenantId: v.string(),
machineId: v.id("machines"),
companyId: v.optional(v.id("companies")),
kind: v.string(),
message: v.string(),
severity: v.string(),
createdAt: v.number(),
})
.index("by_machine_created", ["machineId", "createdAt"])
.index("by_tenant_created", ["tenantId", "createdAt"])
.index("by_tenant_machine", ["tenantId", "machineId"]),
// Tabela separada para heartbeats - evita criar versoes do documento machines a cada heartbeat
// O documento machines so e atualizado quando ha mudancas reais nos dados (metadata, inventory, etc)
machineHeartbeats: defineTable({
machineId: v.id("machines"),
lastHeartbeatAt: v.number(),
})
.index("by_machine", ["machineId"]),
machineTokens: defineTable({
tenantId: v.string(),
machineId: v.id("machines"),
tokenHash: v.string(),
expiresAt: v.number(),
revoked: v.boolean(),
revokedAt: v.optional(v.number()),
createdAt: v.number(),
lastUsedAt: v.optional(v.number()),
usageCount: v.optional(v.number()),
type: v.optional(v.string()),
})
.index("by_token_hash", ["tokenHash"])
.index("by_machine", ["machineId"])
.index("by_tenant_machine", ["tenantId", "machineId"])
.index("by_machine_created", ["machineId", "createdAt"])
.index("by_machine_revoked_expires", ["machineId", "revoked", "expiresAt"]),
deviceFields: defineTable({
tenantId: v.string(),
key: v.string(),
label: v.string(),
description: v.optional(v.string()),
type: v.string(),
required: v.optional(v.boolean()),
options: v.optional(
v.array(
v.object({
value: v.string(),
label: v.string(),
})
)
),
scope: v.optional(v.string()),
companyId: v.optional(v.id("companies")),
order: v.number(),
createdAt: v.number(),
updatedAt: v.number(),
createdBy: v.optional(v.id("users")),
updatedBy: v.optional(v.id("users")),
})
.index("by_tenant_order", ["tenantId", "order"])
.index("by_tenant_key", ["tenantId", "key"])
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant_scope", ["tenantId", "scope"])
.index("by_tenant", ["tenantId"]),
deviceExportTemplates: defineTable({
tenantId: v.string(),
name: v.string(),
slug: v.string(),
description: v.optional(v.string()),
columns: v.array(
v.object({
key: v.string(),
label: v.optional(v.string()),
})
),
filters: v.optional(v.any()),
companyId: v.optional(v.id("companies")),
isDefault: v.optional(v.boolean()),
isActive: v.optional(v.boolean()),
createdBy: v.optional(v.id("users")),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_tenant_slug", ["tenantId", "slug"])
.index("by_tenant_company", ["tenantId", "companyId"])
.index("by_tenant_default", ["tenantId", "isDefault"])
.index("by_tenant", ["tenantId"]),
analyticsCache: defineTable({
tenantId: v.string(),
cacheKey: v.string(),
payload: v.any(),
expiresAt: v.number(),
_ttl: v.optional(v.number()),
})
.index("by_key", ["tenantId", "cacheKey"]),
analyticsLocks: defineTable({
tenantId: v.string(),
cacheKey: v.string(),
expiresAt: v.number(),
_ttl: v.optional(v.number()),
})
.index("by_key", ["tenantId", "cacheKey"]),
// ================================
// Emprestimo de Equipamentos
// ================================
emprestimos: defineTable({
tenantId: v.string(),
reference: v.number(),
clienteId: v.id("companies"),
clienteSnapshot: v.object({
name: v.string(),
slug: v.optional(v.string()),
}),
responsavelNome: v.string(),
responsavelContato: v.optional(v.string()),
tecnicoId: v.id("users"),
tecnicoSnapshot: v.object({
name: v.string(),
email: v.optional(v.string()),
}),
equipamentos: v.array(v.object({
id: v.string(),
tipo: v.string(),
marca: v.string(),
modelo: v.string(),
serialNumber: v.optional(v.string()),
patrimonio: v.optional(v.string()),
})),
quantidade: v.number(),
valor: v.optional(v.number()),
dataEmprestimo: v.number(),
dataFimPrevisto: v.number(),
dataDevolucao: v.optional(v.number()),
status: v.string(),
observacoes: v.optional(v.string()),
observacoesDevolucao: v.optional(v.string()),
multaDiaria: v.optional(v.number()),
multaCalculada: v.optional(v.number()),
createdBy: v.id("users"),
updatedBy: v.optional(v.id("users")),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_tenant", ["tenantId"])
.index("by_tenant_status", ["tenantId", "status"])
.index("by_tenant_cliente", ["tenantId", "clienteId"])
.index("by_tenant_tecnico", ["tenantId", "tecnicoId"])
.index("by_tenant_reference", ["tenantId", "reference"])
.index("by_tenant_created", ["tenantId", "createdAt"])
.index("by_tenant_data_fim", ["tenantId", "dataFimPrevisto"]),
emprestimoHistorico: defineTable({
tenantId: v.string(),
emprestimoId: v.id("emprestimos"),
tipo: v.string(),
descricao: v.string(),
alteracoes: v.optional(v.any()),
autorId: v.id("users"),
autorSnapshot: v.object({
name: v.string(),
email: v.optional(v.string()),
}),
createdAt: v.number(),
})
.index("by_emprestimo", ["emprestimoId"])
.index("by_emprestimo_created", ["emprestimoId", "createdAt"]),
});