Hours by client: add search and CSV filtering; add alerts cron (BRT 08:00 guard) + alerts panel filters; admin companies shows last alert; PDF Inter font from public/fonts; fix Select empty value; type cleanups; tests for CSV/TZ; remove Knowledge Base nav
This commit is contained in:
parent
2cf399dcb1
commit
08cc8037d5
151 changed files with 1404 additions and 214 deletions
|
|
@ -128,10 +128,11 @@ function formatDateKey(timestamp: number) {
|
|||
}
|
||||
|
||||
export const slaOverview = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
|
||||
handler: async (ctx, { tenantId, viewerId }) => {
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
|
||||
handler: async (ctx, { tenantId, viewerId, companyId }) => {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
|
||||
let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
|
||||
if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
|
||||
const queues = await fetchQueues(ctx, tenantId);
|
||||
|
||||
const now = Date.now();
|
||||
|
|
@ -179,10 +180,11 @@ export const slaOverview = query({
|
|||
});
|
||||
|
||||
export const csatOverview = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
|
||||
handler: async (ctx, { tenantId, viewerId, range }) => {
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
|
||||
handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
|
||||
let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
|
||||
if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
|
||||
const surveysAll = await collectCsatSurveys(ctx, tickets);
|
||||
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
|
||||
const end = new Date();
|
||||
|
|
@ -415,7 +417,7 @@ export const hoursByClient = query({
|
|||
|
||||
// Accumulate by company
|
||||
type Acc = {
|
||||
companyId: string
|
||||
companyId: Id<"companies">
|
||||
name: string
|
||||
isAvulso: boolean
|
||||
internalMs: number
|
||||
|
|
@ -428,7 +430,7 @@ export const hoursByClient = query({
|
|||
for (const t of tickets) {
|
||||
// only consider tickets updated in range as a proxy for recent work
|
||||
if (t.updatedAt < startMs || t.updatedAt >= endMs) continue
|
||||
const companyId = (t as any).companyId ?? null
|
||||
const companyId = t.companyId ?? null
|
||||
if (!companyId) continue
|
||||
|
||||
let acc = map.get(companyId)
|
||||
|
|
@ -436,17 +438,82 @@ export const hoursByClient = query({
|
|||
const company = await ctx.db.get(companyId)
|
||||
acc = {
|
||||
companyId,
|
||||
name: (company as any)?.name ?? "Sem empresa",
|
||||
isAvulso: Boolean((company as any)?.isAvulso ?? false),
|
||||
name: company?.name ?? "Sem empresa",
|
||||
isAvulso: Boolean(company?.isAvulso ?? false),
|
||||
internalMs: 0,
|
||||
externalMs: 0,
|
||||
totalMs: 0,
|
||||
contractedHoursPerMonth: (company as any)?.contractedHoursPerMonth ?? null,
|
||||
contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null,
|
||||
}
|
||||
map.set(companyId, acc)
|
||||
}
|
||||
const internal = ((t as any).internalWorkedMs ?? 0) as number
|
||||
const external = ((t as any).externalWorkedMs ?? 0) as number
|
||||
const internal = t.internalWorkedMs ?? 0
|
||||
const external = t.externalWorkedMs ?? 0
|
||||
acc.internalMs += internal
|
||||
acc.externalMs += external
|
||||
acc.totalMs += internal + external
|
||||
}
|
||||
|
||||
const items = Array.from(map.values()).sort((a, b) => b.totalMs - a.totalMs)
|
||||
return {
|
||||
rangeDays: days,
|
||||
items: items.map((i) => ({
|
||||
companyId: i.companyId,
|
||||
name: i.name,
|
||||
isAvulso: i.isAvulso,
|
||||
internalMs: i.internalMs,
|
||||
externalMs: i.externalMs,
|
||||
totalMs: i.totalMs,
|
||||
contractedHoursPerMonth: i.contractedHoursPerMonth ?? null,
|
||||
})),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Internal variant used by scheduled jobs: skips viewer scoping and aggregates for the whole tenant
|
||||
export const hoursByClientInternal = query({
|
||||
args: { tenantId: v.string(), range: v.optional(v.string()) },
|
||||
handler: async (ctx, { tenantId, range }) => {
|
||||
const tickets = await fetchTickets(ctx, tenantId)
|
||||
|
||||
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90
|
||||
const end = new Date()
|
||||
end.setUTCHours(0, 0, 0, 0)
|
||||
const endMs = end.getTime() + ONE_DAY_MS
|
||||
const startMs = endMs - days * ONE_DAY_MS
|
||||
|
||||
type Acc = {
|
||||
companyId: Id<"companies">
|
||||
name: string
|
||||
isAvulso: boolean
|
||||
internalMs: number
|
||||
externalMs: number
|
||||
totalMs: number
|
||||
contractedHoursPerMonth?: number | null
|
||||
}
|
||||
const map = new Map<string, Acc>()
|
||||
|
||||
for (const t of tickets) {
|
||||
if (t.updatedAt < startMs || t.updatedAt >= endMs) continue
|
||||
const companyId = t.companyId ?? null
|
||||
if (!companyId) continue
|
||||
|
||||
let acc = map.get(companyId)
|
||||
if (!acc) {
|
||||
const company = await ctx.db.get(companyId)
|
||||
acc = {
|
||||
companyId,
|
||||
name: company?.name ?? "Sem empresa",
|
||||
isAvulso: Boolean(company?.isAvulso ?? false),
|
||||
internalMs: 0,
|
||||
externalMs: 0,
|
||||
totalMs: 0,
|
||||
contractedHoursPerMonth: company?.contractedHoursPerMonth ?? null,
|
||||
}
|
||||
map.set(companyId, acc)
|
||||
}
|
||||
const internal = t.internalWorkedMs ?? 0
|
||||
const external = t.externalWorkedMs ?? 0
|
||||
acc.internalMs += internal
|
||||
acc.externalMs += external
|
||||
acc.totalMs += internal + external
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue