diff --git a/convex/companies.ts b/convex/companies.ts
index 116eaa8..51bff83 100644
--- a/convex/companies.ts
+++ b/convex/companies.ts
@@ -85,3 +85,24 @@ export const ensureProvisioned = mutation({
}
},
})
+
+export const removeBySlug = mutation({
+ args: {
+ tenantId: v.string(),
+ slug: v.string(),
+ },
+ handler: async (ctx, { tenantId, slug }) => {
+ const normalizedSlug = normalizeSlug(slug) ?? slug
+ const existing = await ctx.db
+ .query("companies")
+ .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", normalizedSlug))
+ .unique()
+
+ if (!existing) {
+ return { removed: false }
+ }
+
+ await ctx.db.delete(existing._id)
+ return { removed: true }
+ },
+})
diff --git a/convex/reports.ts b/convex/reports.ts
index 59a64e7..330d55a 100644
--- a/convex/reports.ts
+++ b/convex/reports.ts
@@ -347,6 +347,22 @@ export const dashboardOverview = query({
const trend = percentageChange(newTickets.length, previousTickets.length);
+ const inProgressCurrent = tickets.filter((ticket) => {
+ if (!ticket.firstResponseAt) return false;
+ const status = normalizeStatus(ticket.status);
+ if (status === "RESOLVED") return false;
+ return !ticket.resolvedAt;
+ });
+
+ const inProgressPrevious = tickets.filter((ticket) => {
+ if (!ticket.firstResponseAt || ticket.firstResponseAt >= lastDayStart) return false;
+ if (ticket.resolvedAt && ticket.resolvedAt < lastDayStart) return false;
+ const status = normalizeStatus(ticket.status);
+ return status !== "RESOLVED" || !ticket.resolvedAt;
+ });
+
+ const inProgressTrend = percentageChange(inProgressCurrent.length, inProgressPrevious.length);
+
const lastWindowStart = now - 7 * ONE_DAY_MS;
const previousWindowStart = now - 14 * ONE_DAY_MS;
@@ -396,6 +412,11 @@ export const dashboardOverview = query({
previous24h: previousTickets.length,
trendPercentage: trend,
},
+ inProgress: {
+ current: inProgressCurrent.length,
+ previousSnapshot: inProgressPrevious.length,
+ trendPercentage: inProgressTrend,
+ },
firstResponse: {
averageMinutes: averageWindow,
previousAverageMinutes: averagePrevious,
diff --git a/scripts/prune-convex-companies.mjs b/scripts/prune-convex-companies.mjs
new file mode 100644
index 0000000..a3647bf
--- /dev/null
+++ b/scripts/prune-convex-companies.mjs
@@ -0,0 +1,54 @@
+import "dotenv/config"
+import { PrismaClient } from "@prisma/client"
+import { ConvexHttpClient } from "convex/browser"
+
+const prisma = new PrismaClient()
+
+const tenantId = process.env.SYNC_TENANT_ID || "tenant-atlas"
+const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || "http://127.0.0.1:3210"
+const secret = process.env.CONVEX_SYNC_SECRET
+
+if (!secret) {
+ console.error("CONVEX_SYNC_SECRET não configurado. Configure-o para executar o prune.")
+ process.exit(1)
+}
+
+async function main() {
+ const client = new ConvexHttpClient(convexUrl)
+
+ const prismaCompanies = await prisma.company.findMany({
+ where: { tenantId },
+ select: { slug: true },
+ })
+ const validSlugs = new Set(prismaCompanies.map((company) => company.slug))
+
+ const snapshot = await client.query("migrations:exportTenantSnapshot", {
+ tenantId,
+ secret,
+ })
+
+ const extraCompanies = snapshot.companies.filter((company) => !validSlugs.has(company.slug))
+ if (extraCompanies.length === 0) {
+ console.log("Nenhuma empresa extra encontrada no Convex.")
+ return
+ }
+
+ console.log(`Removendo ${extraCompanies.length} empresa(s) do Convex: ${extraCompanies.map((c) => c.slug).join(", ")}`)
+
+ for (const company of extraCompanies) {
+ await client.mutation("companies:removeBySlug", {
+ tenantId,
+ slug: company.slug,
+ })
+ }
+ console.log("Concluído.")
+}
+
+main()
+ .catch((error) => {
+ console.error("Falha ao remover empresas extras do Convex", error)
+ process.exitCode = 1
+ })
+ .finally(async () => {
+ await prisma.$disconnect()
+ })
diff --git a/src/app/api/admin/companies/[id]/route.ts b/src/app/api/admin/companies/[id]/route.ts
index 6ddcd04..29ffcc7 100644
--- a/src/app/api/admin/companies/[id]/route.ts
+++ b/src/app/api/admin/companies/[id]/route.ts
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/prisma"
import { assertStaffSession } from "@/lib/auth-server"
import { isAdmin } from "@/lib/authz"
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"
+import { removeConvexCompany, syncConvexCompany } from "@/server/companies-sync"
export const runtime = "nodejs"
@@ -45,6 +46,19 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
// Atualize o client quando possível; por ora liberamos o shape dinamicamente.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const company = await prisma.company.update({ where: { id }, data: updates as any })
+
+ if (company.provisioningCode) {
+ const synced = await syncConvexCompany({
+ tenantId: company.tenantId,
+ slug: company.slug,
+ name: company.name,
+ provisioningCode: company.provisioningCode,
+ })
+ if (!synced) {
+ console.warn("[admin.companies] Convex não configurado; atualização aplicada apenas no Prisma.")
+ }
+ }
+
return NextResponse.json({ company })
} catch (error) {
console.error("Failed to update company", error)
@@ -65,7 +79,7 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str
const company = await prisma.company.findUnique({
where: { id },
- select: { id: true, tenantId: true, name: true },
+ select: { id: true, tenantId: true, name: true, slug: true },
})
if (!company) {
@@ -90,6 +104,16 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str
return { detachedUsers: users.count, detachedTickets: tickets.count }
})
+ if (company.slug) {
+ const removed = await removeConvexCompany({
+ tenantId: company.tenantId,
+ slug: company.slug,
+ })
+ if (!removed) {
+ console.warn("[admin.companies] Convex não configurado; empresa removida apenas no Prisma.")
+ }
+ }
+
return NextResponse.json({ ok: true, ...result })
} catch (error) {
if (error instanceof PrismaClientKnownRequestError && error.code === "P2003") {
diff --git a/src/app/api/admin/companies/route.ts b/src/app/api/admin/companies/route.ts
index fba703f..e4286a8 100644
--- a/src/app/api/admin/companies/route.ts
+++ b/src/app/api/admin/companies/route.ts
@@ -5,6 +5,7 @@ import { prisma } from "@/lib/prisma"
import { assertStaffSession } from "@/lib/auth-server"
import { isAdmin } from "@/lib/authz"
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"
+import { syncConvexCompany } from "@/server/companies-sync"
export const runtime = "nodejs"
@@ -64,6 +65,19 @@ export async function POST(request: Request) {
address: address ? String(address) : null,
},
})
+
+ if (company.provisioningCode) {
+ const synced = await syncConvexCompany({
+ tenantId: company.tenantId,
+ slug: company.slug,
+ name: company.name,
+ provisioningCode: company.provisioningCode,
+ })
+ if (!synced) {
+ console.warn("[admin.companies] Convex não configurado; empresa criada apenas no Prisma.")
+ }
+ }
+
return NextResponse.json({ company })
} catch (error) {
console.error("Failed to create company", error)
diff --git a/src/app/api/machines/companies/route.ts b/src/app/api/machines/companies/route.ts
index fd8022e..addb01a 100644
--- a/src/app/api/machines/companies/route.ts
+++ b/src/app/api/machines/companies/route.ts
@@ -1,12 +1,12 @@
import { randomBytes } from "crypto"
import { Prisma } from "@prisma/client"
-import { api } from "@/convex/_generated/api"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { env } from "@/lib/env"
import { normalizeSlug, slugify } from "@/lib/slug"
import { prisma } from "@/lib/prisma"
import { createCorsPreflight, jsonWithCors } from "@/server/cors"
-import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client"
+import { ConvexConfigurationError } from "@/server/convex-client"
+import { syncConvexCompany } from "@/server/companies-sync"
export const runtime = "nodejs"
@@ -31,11 +31,6 @@ function extractSecret(request: Request, url: URL): string | null {
return null
}
-async function ensureConvexCompany(params: { tenantId: string; slug: string; name: string; provisioningCode: string }) {
- const client = createConvexClient()
- await client.mutation(api.companies.ensureProvisioned, params)
-}
-
export async function OPTIONS(request: Request) {
return createCorsPreflight(request.headers.get("origin"), CORS_METHODS)
}
@@ -158,12 +153,15 @@ export async function POST(request: Request) {
}))
try {
- await ensureConvexCompany({
+ const synced = await syncConvexCompany({
tenantId,
slug: company.slug,
name: company.name,
provisioningCode: company.provisioningCode,
})
+ if (!synced) {
+ throw new ConvexConfigurationError()
+ }
} catch (error) {
if (error instanceof ConvexConfigurationError) {
return jsonWithCors({ error: error.message }, 500, origin, CORS_METHODS)
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index c5a51e2..c4800cb 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -4,6 +4,7 @@ import { SiteHeader } from "@/components/site-header"
import { RecentTicketsPanel } from "@/components/tickets/recent-tickets-panel"
import { TicketQueueSummaryCards } from "@/components/tickets/ticket-queue-summary"
import { ChartOpenedResolved } from "@/components/charts/chart-opened-resolved"
+import { ChartOpenByPriority } from "@/components/charts/chart-open-priority"
import { NewTicketDialogDeferred } from "@/components/tickets/new-ticket-dialog.client"
import { requireAuthenticatedSession } from "@/lib/auth-server"
@@ -20,9 +21,12 @@ export default async function Dashboard() {
/>
}
>
-