fix(reports): remove truncation cap in range collectors to avoid dropped records
feat(calendar): migrate to react-day-picker v9 and polish UI - Update classNames and CSS import (style.css) - Custom Dropdown via shadcn Select - Nav arrows aligned with caption (around) - Today highlight with cyan tone, weekdays in sentence case - Wider layout to avoid overflow; remove inner wrapper chore(tickets): make 'Patrimônio do computador (se houver)' optional - Backend hotfix to enforce optional + label on existing tenants - Hide required asterisk for this field in portal/new-ticket refactor(new-ticket): remove channel dropdown from admin/agent flow - Keep default channel as MANUAL feat(ux): simplify requester section and enlarge combobox trigger - Remove RequesterPreview redundancy; show company badge in trigger
This commit is contained in:
parent
e0ef66555d
commit
a8333c010f
28 changed files with 1752 additions and 455 deletions
|
|
@ -7,7 +7,7 @@ import { requireStaff } from "./rbac";
|
|||
import { getOfflineThresholdMs, getStaleThresholdMs } from "./machines";
|
||||
|
||||
type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED";
|
||||
|
||||
type QueryFilterBuilder = { lt: (field: unknown, value: number) => unknown; field: (name: string) => unknown };
|
||||
const STATUS_NORMALIZE_MAP: Record<string, TicketStatusNormalized> = {
|
||||
NEW: "PENDING",
|
||||
PENDING: "PENDING",
|
||||
|
|
@ -122,34 +122,61 @@ async function fetchScopedTicketsByCreatedRange(
|
|||
endMs: number,
|
||||
companyId?: Id<"companies">,
|
||||
) {
|
||||
const collectRange = async (buildQuery: (chunkStart: number) => unknown) => {
|
||||
const results: Doc<"tickets">[] = [];
|
||||
const chunkSize = 7 * ONE_DAY_MS;
|
||||
for (let chunkStart = startMs; chunkStart < endMs; chunkStart += chunkSize) {
|
||||
const chunkEnd = Math.min(chunkStart + chunkSize, endMs);
|
||||
const baseQuery = buildQuery(chunkStart);
|
||||
const filterFn = (baseQuery as { filter?: (fn: (q: QueryFilterBuilder) => unknown) => unknown }).filter;
|
||||
const queryForChunk =
|
||||
typeof filterFn === "function"
|
||||
? filterFn.call(baseQuery, (q: QueryFilterBuilder) => q.lt(q.field("createdAt"), chunkEnd))
|
||||
: baseQuery;
|
||||
const collectFn = (queryForChunk as { collect?: () => Promise<Doc<"tickets">[]> }).collect;
|
||||
if (typeof collectFn !== "function") {
|
||||
throw new ConvexError("Indexed query does not support collect (createdAt)");
|
||||
}
|
||||
const page = await collectFn.call(queryForChunk);
|
||||
if (!page || page.length === 0) continue;
|
||||
for (const ticket of page) {
|
||||
const createdAt = typeof ticket.createdAt === "number" ? ticket.createdAt : null;
|
||||
if (createdAt === null) continue;
|
||||
if (createdAt < chunkStart || createdAt >= endMs) continue;
|
||||
results.push(ticket);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
if (viewer.role === "MANAGER") {
|
||||
if (!viewer.user.companyId) {
|
||||
throw new ConvexError("Gestor não possui empresa vinculada");
|
||||
}
|
||||
return ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_created", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", viewer.user.companyId!).gte("createdAt", startMs),
|
||||
)
|
||||
.filter((q) => q.lt(q.field("createdAt"), endMs))
|
||||
.collect();
|
||||
return collectRange((chunkStart) =>
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_created", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", viewer.user.companyId!).gte("createdAt", chunkStart)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
return ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_created", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", companyId).gte("createdAt", startMs),
|
||||
)
|
||||
.filter((q) => q.lt(q.field("createdAt"), endMs))
|
||||
.collect();
|
||||
return collectRange((chunkStart) =>
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_created", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", companyId).gte("createdAt", chunkStart)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_created", (q) => q.eq("tenantId", tenantId).gte("createdAt", startMs))
|
||||
.filter((q) => q.lt(q.field("createdAt"), endMs))
|
||||
.collect();
|
||||
return collectRange((chunkStart) =>
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_created", (q) => q.eq("tenantId", tenantId).gte("createdAt", chunkStart))
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchScopedTicketsByResolvedRange(
|
||||
|
|
@ -160,34 +187,61 @@ async function fetchScopedTicketsByResolvedRange(
|
|||
endMs: number,
|
||||
companyId?: Id<"companies">,
|
||||
) {
|
||||
const collectRange = async (buildQuery: (chunkStart: number) => unknown) => {
|
||||
const results: Doc<"tickets">[] = [];
|
||||
const chunkSize = 7 * ONE_DAY_MS;
|
||||
for (let chunkStart = startMs; chunkStart < endMs; chunkStart += chunkSize) {
|
||||
const chunkEnd = Math.min(chunkStart + chunkSize, endMs);
|
||||
const baseQuery = buildQuery(chunkStart);
|
||||
const filterFn = (baseQuery as { filter?: (fn: (q: QueryFilterBuilder) => unknown) => unknown }).filter;
|
||||
const queryForChunk =
|
||||
typeof filterFn === "function"
|
||||
? filterFn.call(baseQuery, (q: QueryFilterBuilder) => q.lt(q.field("resolvedAt"), chunkEnd))
|
||||
: baseQuery;
|
||||
const collectFn = (queryForChunk as { collect?: () => Promise<Doc<"tickets">[]> }).collect;
|
||||
if (typeof collectFn !== "function") {
|
||||
throw new ConvexError("Indexed query does not support collect (resolvedAt)");
|
||||
}
|
||||
const page = await collectFn.call(queryForChunk);
|
||||
if (!page || page.length === 0) continue;
|
||||
for (const ticket of page) {
|
||||
const resolvedAt = typeof ticket.resolvedAt === "number" ? ticket.resolvedAt : null;
|
||||
if (resolvedAt === null) continue;
|
||||
if (resolvedAt < chunkStart || resolvedAt >= endMs) continue;
|
||||
results.push(ticket);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
if (viewer.role === "MANAGER") {
|
||||
if (!viewer.user.companyId) {
|
||||
throw new ConvexError("Gestor não possui empresa vinculada");
|
||||
}
|
||||
return ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_resolved", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", viewer.user.companyId!).gte("resolvedAt", startMs),
|
||||
)
|
||||
.filter((q) => q.lt(q.field("resolvedAt"), endMs))
|
||||
.collect();
|
||||
return collectRange((chunkStart) =>
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_resolved", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", viewer.user.companyId!).gte("resolvedAt", chunkStart)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
return ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_resolved", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", companyId).gte("resolvedAt", startMs),
|
||||
)
|
||||
.filter((q) => q.lt(q.field("resolvedAt"), endMs))
|
||||
.collect();
|
||||
return collectRange((chunkStart) =>
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_company_resolved", (q) =>
|
||||
q.eq("tenantId", tenantId).eq("companyId", companyId).gte("resolvedAt", chunkStart)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_resolved", (q) => q.eq("tenantId", tenantId).gte("resolvedAt", startMs))
|
||||
.filter((q) => q.lt(q.field("resolvedAt"), endMs))
|
||||
.collect();
|
||||
return collectRange((chunkStart) =>
|
||||
ctx.db
|
||||
.query("tickets")
|
||||
.withIndex("by_tenant_resolved", (q) => q.eq("tenantId", tenantId).gte("resolvedAt", chunkStart))
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchQueues(ctx: QueryCtx, tenantId: string) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue