feat(reports): hours by client (CSV + UI), company contracted hours, UI to manage companies; adjust ticket list spacing

This commit is contained in:
Esdras Renan 2025-10-07 14:04:36 -03:00
parent 3bafcc5a0a
commit 70f91f5bbd
10 changed files with 294 additions and 4 deletions

View file

@ -397,3 +397,70 @@ export const ticketsByChannel = query({
};
},
});
export const hoursByClient = query({
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
handler: async (ctx, { tenantId, viewerId, range }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId)
const tickets = await fetchScopedTickets(ctx, tenantId, viewer)
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
// Accumulate by company
type Acc = {
companyId: string
name: string
isAvulso: boolean
internalMs: number
externalMs: number
totalMs: number
contractedHoursPerMonth?: number | null
}
const map = new Map<string, Acc>()
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
if (!companyId) continue
let acc = map.get(companyId)
if (!acc) {
const company = await ctx.db.get(companyId)
acc = {
companyId,
name: (company as any)?.name ?? "Sem empresa",
isAvulso: Boolean((company as any)?.isAvulso ?? false),
internalMs: 0,
externalMs: 0,
totalMs: 0,
contractedHoursPerMonth: (company as any)?.contractedHoursPerMonth ?? null,
}
map.set(companyId, acc)
}
const internal = ((t as any).internalWorkedMs ?? 0) as number
const external = ((t as any).externalWorkedMs ?? 0) as number
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,
})),
}
},
})

View file

@ -21,6 +21,7 @@ export default defineSchema({
name: v.string(),
slug: 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()),