- Add emprestimos (equipment loan) module in Convex with queries/mutations - Create emprestimos page with full CRUD and status tracking - Add USB bulk control to admin devices overview - Fix Portuguese accents in USB policy control component - Fix dead code warnings in Rust agent - Fix tiptap type error in rich text editor
834 lines
26 KiB
TypeScript
834 lines
26 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()),
|
|
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()),
|
|
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"]),
|
|
|
|
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"]),
|
|
|
|
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"]),
|
|
|
|
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"]),
|
|
|
|
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_auth_email", ["authEmail"]),
|
|
|
|
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"]),
|
|
|
|
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()),
|
|
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"]),
|
|
});
|