571
apps/api/src/widgets/widget-data.service.ts
Normal file
571
apps/api/src/widgets/widget-data.service.ts
Normal file
@@ -0,0 +1,571 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<WidgetStatData> {
|
||||
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<WidgetChartData> {
|
||||
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<WidgetListItem[]> {
|
||||
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<WidgetCalendarItem[]> {
|
||||
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) => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
startTime: event.startTime.toISOString(),
|
||||
endTime: event.endTime?.toISOString(),
|
||||
allDay: event.allDay,
|
||||
type: "event" as const,
|
||||
color: event.project?.color || "#3B82F6",
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
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.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<string, unknown>
|
||||
): Promise<WidgetStatData> {
|
||||
const where: Record<string, unknown> = { 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<string, unknown>
|
||||
): Promise<WidgetStatData> {
|
||||
const where: Record<string, unknown> = { 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<string, unknown>
|
||||
): Promise<WidgetStatData> {
|
||||
const where: Record<string, unknown> = { 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<string, unknown>,
|
||||
colors?: string[]
|
||||
): Promise<WidgetChartData> {
|
||||
const where: Record<string, unknown> = { 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) => 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<string, unknown>,
|
||||
colors?: string[]
|
||||
): Promise<WidgetChartData> {
|
||||
const where: Record<string, unknown> = { 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) => 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<string, unknown>,
|
||||
colors?: string[]
|
||||
): Promise<WidgetChartData> {
|
||||
const where: Record<string, unknown> = { 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<string, unknown>
|
||||
): Promise<WidgetListItem[]> {
|
||||
const where: Record<string, unknown> = { workspaceId, ...filter };
|
||||
const orderBy: Record<string, "asc" | "desc"> = {};
|
||||
|
||||
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) => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
subtitle: task.project?.name,
|
||||
status: task.status,
|
||||
priority: task.priority,
|
||||
dueDate: task.dueDate?.toISOString(),
|
||||
color: task.project?.color || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getEventListData(
|
||||
workspaceId: string,
|
||||
sortBy?: string,
|
||||
sortOrder?: "asc" | "desc",
|
||||
limit?: number,
|
||||
filter?: Record<string, unknown>
|
||||
): Promise<WidgetListItem[]> {
|
||||
const where: Record<string, unknown> = { workspaceId, ...filter };
|
||||
const orderBy: Record<string, "asc" | "desc"> = {};
|
||||
|
||||
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) => ({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
subtitle: event.project?.name,
|
||||
startTime: event.startTime.toISOString(),
|
||||
color: event.project?.color || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getProjectListData(
|
||||
workspaceId: string,
|
||||
sortBy?: string,
|
||||
sortOrder?: "asc" | "desc",
|
||||
limit?: number,
|
||||
filter?: Record<string, unknown>
|
||||
): Promise<WidgetListItem[]> {
|
||||
const where: Record<string, unknown> = { workspaceId, ...filter };
|
||||
const orderBy: Record<string, "asc" | "desc"> = {};
|
||||
|
||||
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) => ({
|
||||
id: project.id,
|
||||
title: project.name,
|
||||
subtitle: project.description || undefined,
|
||||
status: project.status,
|
||||
color: project.color || undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user