import { addMinutes, endOfDay, endOfMonth, endOfWeek, isAfter, isBefore, isWithinInterval, startOfDay, startOfMonth, startOfWeek, } from "date-fns" import type { Ticket, TicketPriority } from "@/lib/schemas/ticket" import type { AgendaFilterState, AgendaPeriod } from "@/components/agenda/agenda-filters" import { getSlaDisplayStatus, getSlaDueDate } from "@/lib/sla-utils" import { isVisitTicket } from "@/lib/ticket-matchers" export type AgendaSlaStatus = "on_track" | "at_risk" | "breached" | "met" export type AgendaTicketSummary = { id: string reference: number subject: string queue: string | null company: string | null priority: TicketPriority location?: string | null startAt: Date | null endAt: Date | null slaStatus: AgendaSlaStatus completedAt?: Date | null href: string } export type AgendaCalendarEvent = { id: string ticketId: string reference: number title: string queue: string | null priority: TicketPriority start: Date end: Date slaStatus: AgendaSlaStatus href: string } export type AgendaDataset = { range: { start: Date; end: Date } availableQueues: string[] kpis: { pending: number inProgress: number paused: number outsideSla: string } sections: { upcoming: AgendaTicketSummary[] overdue: AgendaTicketSummary[] unscheduled: AgendaTicketSummary[] completed: AgendaTicketSummary[] } calendarEvents: AgendaCalendarEvent[] } const DEFAULT_EVENT_DURATION_MINUTES = 60 export function buildAgendaDataset(tickets: Ticket[], filters: AgendaFilterState): AgendaDataset { const now = new Date() const range = computeRange(filters.period, now) const availableQueues = Array.from( new Set( tickets .map((ticket) => ticket.queue?.trim()) .filter((queue): queue is string => Boolean(queue)) ) ).sort((a, b) => a.localeCompare(b, "pt-BR")) const filteredTickets = tickets .filter((ticket) => matchesFilters(ticket, filters)) .filter((ticket) => isVisitTicket(ticket)) const enriched = filteredTickets.map((ticket) => { const schedule = deriveScheduleWindow(ticket) const slaStatus = computeSlaStatus(ticket, now) return { ticket, schedule, slaStatus } }) const summarySections = { upcoming: [] as AgendaTicketSummary[], overdue: [] as AgendaTicketSummary[], unscheduled: [] as AgendaTicketSummary[], completed: [] as AgendaTicketSummary[], } for (const entry of enriched) { const summary = buildSummary(entry.ticket, entry.schedule, entry.slaStatus) const dueDate = entry.schedule.startAt const createdAt = entry.ticket.createdAt const resolvedAt = entry.ticket.resolvedAt if (dueDate && isWithinRange(dueDate, range)) { if (!entry.ticket.resolvedAt && isAfter(dueDate, now)) { summarySections.upcoming.push(summary) } if (!entry.ticket.resolvedAt && isBefore(dueDate, now)) { summarySections.overdue.push(summary) } } if (!dueDate && entry.ticket.status !== "RESOLVED" && isWithinRange(createdAt, range)) { summarySections.unscheduled.push(summary) } if (resolvedAt && isWithinRange(resolvedAt, range)) { summarySections.completed.push(summary) } } summarySections.upcoming.sort((a, b) => compareNullableDate(a.startAt, b.startAt, 1)) summarySections.overdue.sort((a, b) => compareNullableDate(a.startAt, b.startAt, -1)) summarySections.unscheduled.sort((a, b) => compareByPriorityThenReference(a, b)) summarySections.completed.sort((a, b) => compareNullableDate(a.completedAt ?? null, b.completedAt ?? null, -1)) const calendarEvents = enriched .filter((entry): entry is typeof entry & { schedule: { startAt: Date; endAt: Date } } => Boolean(entry.schedule.startAt && entry.schedule.endAt)) .map((entry) => ({ id: `${entry.ticket.id}-event`, ticketId: entry.ticket.id, reference: entry.ticket.reference, title: entry.ticket.subject, queue: entry.ticket.queue ?? null, priority: entry.ticket.priority, start: entry.schedule.startAt!, end: entry.schedule.endAt!, slaStatus: entry.slaStatus, href: `/tickets/${entry.ticket.id}`, })) .sort((a, b) => a.start.getTime() - b.start.getTime()) const outsideSlaCount = enriched.filter((entry) => entry.slaStatus === "breached" || entry.slaStatus === "at_risk").length const outsideSlaPct = filteredTickets.length ? Math.round((outsideSlaCount / filteredTickets.length) * 100) : 0 const dataset: AgendaDataset = { range, availableQueues, kpis: { pending: countByStatus(filteredTickets, ["PENDING"]), inProgress: countByStatus(filteredTickets, ["AWAITING_ATTENDANCE"]), paused: countByStatus(filteredTickets, ["PAUSED"]), outsideSla: `${outsideSlaPct}%`, }, sections: summarySections, calendarEvents, } return dataset } function matchesFilters(ticket: Ticket, filters: AgendaFilterState) { if (filters.queues.length > 0) { if (!ticket.queue) return false const normalizedQueue = ticket.queue.toLowerCase() const matchesQueue = filters.queues.some((queue) => queue.toLowerCase() === normalizedQueue) if (!matchesQueue) return false } if (filters.priorities.length > 0 && !filters.priorities.includes(ticket.priority)) { return false } if (filters.focusVisits && !isVisitTicket(ticket)) { return false } return true } function computeRange(period: AgendaPeriod, pivot: Date) { if (period === "today") { return { start: startOfDay(pivot), end: endOfDay(pivot), } } if (period === "month") { return { start: startOfMonth(pivot), end: endOfMonth(pivot), } } return { start: startOfWeek(pivot, { weekStartsOn: 1 }), end: endOfWeek(pivot, { weekStartsOn: 1 }), } } function deriveScheduleWindow(ticket: Ticket) { const due = getSlaDueDate(ticket, "solution") if (!due) { return { startAt: null, endAt: null } } const startAt = due const endAt = addMinutes(startAt, DEFAULT_EVENT_DURATION_MINUTES) return { startAt, endAt } } function computeSlaStatus(ticket: Ticket, now: Date): AgendaSlaStatus { const status = getSlaDisplayStatus(ticket, "solution", now) if (status === "n/a") { return "on_track" } return status } function buildSummary(ticket: Ticket, schedule: { startAt: Date | null; endAt: Date | null }, slaStatus: AgendaSlaStatus): AgendaTicketSummary { return { id: ticket.id, reference: ticket.reference, subject: ticket.subject, queue: ticket.queue ?? null, company: ticket.company?.name ?? null, priority: ticket.priority, location: null, startAt: schedule.startAt, endAt: ticket.resolvedAt ?? schedule.endAt, slaStatus, completedAt: ticket.resolvedAt ?? null, href: `/tickets/${ticket.id}`, } } function isWithinRange(date: Date, range: { start: Date; end: Date }) { return isWithinInterval(date, range) } function countByStatus(tickets: Ticket[], statuses: Ticket["status"][]): number { const set = new Set(statuses) return tickets.filter((ticket) => set.has(ticket.status)).length } function compareNullableDate(a: Date | null, b: Date | null, direction: 1 | -1) { const aTime = a ? a.getTime() : Number.MAX_SAFE_INTEGER const bTime = b ? b.getTime() : Number.MAX_SAFE_INTEGER return (aTime - bTime) * direction } function compareByPriorityThenReference(a: AgendaTicketSummary, b: AgendaTicketSummary) { const rank: Record = { URGENT: 1, HIGH: 2, MEDIUM: 3, LOW: 4 } const diff = (rank[a.priority] ?? 5) - (rank[b.priority] ?? 5) if (diff !== 0) return diff return a.reference - b.reference }