import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { TaskStatus, TaskPriority, ProjectStatus } from "@prisma/client"; import type { StatCardQueryDto, ChartQueryDto, ListQueryDto, CalendarPreviewQueryDto } from "./dto"; /** * Widget data response types */ export interface WidgetStatData { value: number; change?: number; changePercent?: number; previousValue?: number; } export interface WidgetChartData { labels: string[]; datasets: { label: string; data: number[]; backgroundColor?: string[]; }[]; } export interface WidgetListItem { id: string; title: string; subtitle?: string; status?: string; priority?: string; dueDate?: string; startTime?: string; color?: string; } export interface WidgetCalendarItem { id: string; title: string; startTime: string; endTime?: string; allDay?: boolean; type: "task" | "event"; color?: string; } export interface WidgetProjectItem { id: string; name: string; status: string; lastActivity: string; taskCount: number; eventCount: number; color: string | null; } export interface WidgetAgentSessionItem { id: string; sessionKey: string; label: string | null; channel: string | null; agentName: string | null; agentStatus: string | null; status: "active" | "ended"; startedAt: string; lastMessageAt: string | null; runtimeMs: number; messageCount: number; contextSummary: string | null; } /** * Service for fetching widget data from various sources */ @Injectable() export class WidgetDataService { constructor(private readonly prisma: PrismaService) {} /** * Get stat card data based on configuration */ async getStatCardData(workspaceId: string, query: StatCardQueryDto): Promise { const { dataSource, metric, filter } = query; switch (dataSource) { case "tasks": return this.getTaskStatData(workspaceId, metric, filter); case "events": return this.getEventStatData(workspaceId, metric, filter); case "projects": return this.getProjectStatData(workspaceId, metric, filter); default: return { value: 0 }; } } /** * Get chart data based on configuration */ async getChartData(workspaceId: string, query: ChartQueryDto): Promise { const { dataSource, groupBy, filter, colors } = query; switch (dataSource) { case "tasks": return this.getTaskChartData(workspaceId, groupBy, filter, colors); case "events": return this.getEventChartData(workspaceId, groupBy, filter, colors); case "projects": return this.getProjectChartData(workspaceId, groupBy, filter, colors); default: return { labels: [], datasets: [] }; } } /** * Get list data based on configuration */ async getListData(workspaceId: string, query: ListQueryDto): Promise { const { dataSource, sortBy, sortOrder, limit, filter } = query; switch (dataSource) { case "tasks": return this.getTaskListData(workspaceId, sortBy, sortOrder, limit, filter); case "events": return this.getEventListData(workspaceId, sortBy, sortOrder, limit, filter); case "projects": return this.getProjectListData(workspaceId, sortBy, sortOrder, limit, filter); default: return []; } } /** * Get calendar preview data */ async getCalendarPreviewData( workspaceId: string, query: CalendarPreviewQueryDto ): Promise { const { showTasks = true, showEvents = true, daysAhead = 7 } = query; const items: WidgetCalendarItem[] = []; const startDate = new Date(); startDate.setHours(0, 0, 0, 0); const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + daysAhead); if (showEvents) { const events = await this.prisma.event.findMany({ where: { workspaceId, startTime: { gte: startDate, lte: endDate, }, }, include: { project: { select: { color: true }, }, }, orderBy: { startTime: "asc" }, take: 20, }); items.push( ...events.map((event) => { const item: WidgetCalendarItem = { id: event.id, title: event.title, startTime: event.startTime.toISOString(), allDay: event.allDay, type: "event" as const, color: event.project?.color ?? "#3B82F6", }; if (event.endTime !== null) { item.endTime = event.endTime.toISOString(); } return item; }) ); } if (showTasks) { const tasks = await this.prisma.task.findMany({ where: { workspaceId, dueDate: { gte: startDate, lte: endDate, }, status: { not: TaskStatus.COMPLETED, }, }, include: { project: { select: { color: true }, }, }, orderBy: { dueDate: "asc" }, take: 20, }); items.push( ...tasks .filter((task): task is typeof task & { dueDate: Date } => task.dueDate !== null) .map((task) => ({ id: task.id, title: task.title, startTime: task.dueDate.toISOString(), allDay: true, type: "task" as const, color: task.project?.color ?? "#10B981", })) ); } // Sort by start time items.sort((a, b) => new Date(a.startTime).getTime() - new Date(b.startTime).getTime()); return items; } // Private helper methods private async getTaskStatData( workspaceId: string, metric: string, filter?: Record ): Promise { const where: Record = { workspaceId, ...filter }; switch (metric) { case "count": { const count = await this.prisma.task.count({ where }); return { value: count }; } case "completed": { const completed = await this.prisma.task.count({ where: { ...where, status: TaskStatus.COMPLETED }, }); return { value: completed }; } case "overdue": { const overdue = await this.prisma.task.count({ where: { ...where, status: { not: TaskStatus.COMPLETED }, dueDate: { lt: new Date() }, }, }); return { value: overdue }; } case "upcoming": { const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); const upcoming = await this.prisma.task.count({ where: { ...where, status: { not: TaskStatus.COMPLETED }, dueDate: { gte: new Date(), lte: nextWeek }, }, }); return { value: upcoming }; } default: return { value: 0 }; } } private async getEventStatData( workspaceId: string, metric: string, filter?: Record ): Promise { const where: Record = { workspaceId, ...filter }; switch (metric) { case "count": { const count = await this.prisma.event.count({ where }); return { value: count }; } case "upcoming": { const nextWeek = new Date(); nextWeek.setDate(nextWeek.getDate() + 7); const upcoming = await this.prisma.event.count({ where: { ...where, startTime: { gte: new Date(), lte: nextWeek }, }, }); return { value: upcoming }; } default: return { value: 0 }; } } private async getProjectStatData( workspaceId: string, metric: string, filter?: Record ): Promise { const where: Record = { workspaceId, ...filter }; switch (metric) { case "count": { const count = await this.prisma.project.count({ where }); return { value: count }; } case "completed": { const completed = await this.prisma.project.count({ where: { ...where, status: ProjectStatus.COMPLETED }, }); return { value: completed }; } default: return { value: 0 }; } } private async getTaskChartData( workspaceId: string, groupBy: string, filter?: Record, colors?: string[] ): Promise { const where: Record = { workspaceId, ...filter }; const defaultColors = ["#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6"]; switch (groupBy) { case "status": { const statusCounts = await this.prisma.task.groupBy({ by: ["status"], where, _count: { id: true }, }); const statusLabels = Object.values(TaskStatus); const statusData = statusLabels.map((status) => { const found = statusCounts.find((s) => s.status === status); return found ? found._count.id : 0; }); return { labels: statusLabels.map((s) => s.replace("_", " ")), datasets: [ { label: "Tasks by Status", data: statusData, backgroundColor: colors ?? defaultColors, }, ], }; } case "priority": { const priorityCounts = await this.prisma.task.groupBy({ by: ["priority"], where, _count: { id: true }, }); const priorityLabels = Object.values(TaskPriority); const priorityData = priorityLabels.map((priority) => { const found = priorityCounts.find((p) => p.priority === priority); return found ? found._count.id : 0; }); return { labels: priorityLabels, datasets: [ { label: "Tasks by Priority", data: priorityData, backgroundColor: colors ?? ["#EF4444", "#F59E0B", "#3B82F6", "#10B981"], }, ], }; } case "project": { const projectCounts = await this.prisma.task.groupBy({ by: ["projectId"], where: { ...where, projectId: { not: null } }, _count: { id: true }, }); const projectIds = projectCounts.map((p) => { if (p.projectId === null) { throw new Error("Unexpected null projectId"); } return p.projectId; }); const projects = await this.prisma.project.findMany({ where: { id: { in: projectIds } }, select: { id: true, name: true, color: true }, }); return { labels: projects.map((p) => p.name), datasets: [ { label: "Tasks by Project", data: projectCounts.map((p) => p._count.id), backgroundColor: projects.map((p) => p.color ?? "#3B82F6"), }, ], }; } default: return { labels: [], datasets: [] }; } } private async getEventChartData( workspaceId: string, groupBy: string, filter?: Record, colors?: string[] ): Promise { const where: Record = { workspaceId, ...filter }; const defaultColors = ["#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6"]; switch (groupBy) { case "project": { const projectCounts = await this.prisma.event.groupBy({ by: ["projectId"], where: { ...where, projectId: { not: null } }, _count: { id: true }, }); const projectIds = projectCounts.map((p) => { if (p.projectId === null) { throw new Error("Unexpected null projectId"); } return p.projectId; }); const projects = await this.prisma.project.findMany({ where: { id: { in: projectIds } }, select: { id: true, name: true, color: true }, }); return { labels: projects.map((p) => p.name), datasets: [ { label: "Events by Project", data: projectCounts.map((p) => p._count.id), backgroundColor: projects.map((p) => p.color ?? "#3B82F6"), }, ], }; } default: return { labels: [], datasets: [{ label: "Events", data: [], backgroundColor: colors ?? defaultColors }], }; } } private async getProjectChartData( workspaceId: string, groupBy: string, filter?: Record, colors?: string[] ): Promise { const where: Record = { workspaceId, ...filter }; const defaultColors = ["#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6"]; switch (groupBy) { case "status": { const statusCounts = await this.prisma.project.groupBy({ by: ["status"], where, _count: { id: true }, }); const statusLabels = Object.values(ProjectStatus); const statusData = statusLabels.map((status) => { const found = statusCounts.find((s) => s.status === status); return found ? found._count.id : 0; }); return { labels: statusLabels.map((s) => s.replace("_", " ")), datasets: [ { label: "Projects by Status", data: statusData, backgroundColor: colors ?? defaultColors, }, ], }; } default: return { labels: [], datasets: [] }; } } private async getTaskListData( workspaceId: string, sortBy?: string, sortOrder?: "asc" | "desc", limit?: number, filter?: Record ): Promise { const where: Record = { workspaceId, ...filter }; const orderBy: Record = {}; if (sortBy) { orderBy[sortBy] = sortOrder ?? "desc"; } else { orderBy.createdAt = "desc"; } const tasks = await this.prisma.task.findMany({ where, include: { project: { select: { name: true, color: true } }, }, orderBy, take: limit ?? 10, }); return tasks.map((task) => { const item: WidgetListItem = { id: task.id, title: task.title, status: task.status, priority: task.priority, }; if (task.project?.name) { item.subtitle = task.project.name; } if (task.dueDate) { item.dueDate = task.dueDate.toISOString(); } if (task.project?.color) { item.color = task.project.color; } return item; }); } private async getEventListData( workspaceId: string, sortBy?: string, sortOrder?: "asc" | "desc", limit?: number, filter?: Record ): Promise { const where: Record = { workspaceId, ...filter }; const orderBy: Record = {}; if (sortBy) { orderBy[sortBy] = sortOrder ?? "asc"; } else { orderBy.startTime = "asc"; } const events = await this.prisma.event.findMany({ where, include: { project: { select: { name: true, color: true } }, }, orderBy, take: limit ?? 10, }); return events.map((event) => { const item: WidgetListItem = { id: event.id, title: event.title, startTime: event.startTime.toISOString(), }; if (event.project?.name) { item.subtitle = event.project.name; } if (event.project?.color) { item.color = event.project.color; } return item; }); } private async getProjectListData( workspaceId: string, sortBy?: string, sortOrder?: "asc" | "desc", limit?: number, filter?: Record ): Promise { const where: Record = { workspaceId, ...filter }; const orderBy: Record = {}; if (sortBy) { orderBy[sortBy] = sortOrder ?? "desc"; } else { orderBy.createdAt = "desc"; } const projects = await this.prisma.project.findMany({ where, orderBy, take: limit ?? 10, }); return projects.map((project) => { const item: WidgetListItem = { id: project.id, title: project.name, status: project.status, }; if (project.description) { item.subtitle = project.description; } if (project.color) { item.color = project.color; } return item; }); } /** * Get active projects data */ async getActiveProjectsData(workspaceId: string): Promise { const projects = await this.prisma.project.findMany({ where: { workspaceId, status: ProjectStatus.ACTIVE, }, include: { _count: { select: { tasks: true, events: true }, }, }, orderBy: { updatedAt: "desc", }, take: 20, }); return projects.map((project) => ({ id: project.id, name: project.name, status: project.status, lastActivity: project.updatedAt.toISOString(), taskCount: project._count.tasks, eventCount: project._count.events, color: project.color, })); } /** * Get agent chains data (active agent sessions) */ async getAgentChainsData(workspaceId: string): Promise { const sessions = await this.prisma.agentSession.findMany({ where: { workspaceId, isActive: true, }, include: { agent: { select: { name: true, status: true, }, }, }, orderBy: { startedAt: "desc", }, take: 20, }); const now = new Date(); return sessions.map((session) => ({ id: session.id, sessionKey: session.sessionKey, label: session.label, channel: session.channel, agentName: session.agent?.name ?? null, agentStatus: session.agent?.status ?? null, status: session.isActive ? ("active" as const) : ("ended" as const), startedAt: session.startedAt.toISOString(), lastMessageAt: session.lastMessageAt ? session.lastMessageAt.toISOString() : null, runtimeMs: now.getTime() - session.startedAt.getTime(), messageCount: session.messageCount, contextSummary: session.contextSummary, })); } }