feat(#41): implement widget system backend (closes #41)

This commit is contained in:
Jason Woltje
2026-01-29 21:30:01 -06:00
parent 0bd12b5751
commit 532f5a39a0
11 changed files with 927 additions and 6 deletions

View File

@@ -0,0 +1,34 @@
import {
IsString,
IsOptional,
IsIn,
IsBoolean,
IsNumber,
Min,
Max,
} from "class-validator";
/**
* DTO for querying calendar preview widget data
*/
export class CalendarPreviewQueryDto {
@IsString({ message: "view must be a string" })
@IsIn(["day", "week", "agenda"], {
message: "view must be one of: day, week, agenda",
})
view!: "day" | "week" | "agenda";
@IsOptional()
@IsBoolean({ message: "showTasks must be a boolean" })
showTasks?: boolean;
@IsOptional()
@IsBoolean({ message: "showEvents must be a boolean" })
showEvents?: boolean;
@IsOptional()
@IsNumber({}, { message: "daysAhead must be a number" })
@Min(1, { message: "daysAhead must be at least 1" })
@Max(30, { message: "daysAhead must not exceed 30" })
daysAhead?: number;
}

View File

@@ -0,0 +1,38 @@
import {
IsString,
IsOptional,
IsIn,
IsObject,
IsArray,
} from "class-validator";
/**
* DTO for querying chart widget data
*/
export class ChartQueryDto {
@IsString({ message: "chartType must be a string" })
@IsIn(["bar", "line", "pie", "donut"], {
message: "chartType must be one of: bar, line, pie, donut",
})
chartType!: "bar" | "line" | "pie" | "donut";
@IsString({ message: "dataSource must be a string" })
@IsIn(["tasks", "events", "projects"], {
message: "dataSource must be one of: tasks, events, projects",
})
dataSource!: "tasks" | "events" | "projects";
@IsString({ message: "groupBy must be a string" })
@IsIn(["status", "priority", "project", "day", "week", "month"], {
message: "groupBy must be one of: status, priority, project, day, week, month",
})
groupBy!: "status" | "priority" | "project" | "day" | "week" | "month";
@IsOptional()
@IsObject({ message: "filter must be an object" })
filter?: Record<string, unknown>;
@IsOptional()
@IsArray({ message: "colors must be an array" })
colors?: string[];
}

View File

@@ -0,0 +1,46 @@
import {
IsString,
IsOptional,
IsNumber,
IsObject,
MinLength,
MaxLength,
Min,
Max,
} from "class-validator";
/**
* DTO for creating a widget configuration in a layout
*/
export class CreateWidgetConfigDto {
@IsString({ message: "widgetType must be a string" })
@MinLength(1, { message: "widgetType must not be empty" })
widgetType!: string;
@IsNumber({}, { message: "x must be a number" })
@Min(0, { message: "x must be at least 0" })
x!: number;
@IsNumber({}, { message: "y must be a number" })
@Min(0, { message: "y must be at least 0" })
y!: number;
@IsNumber({}, { message: "w must be a number" })
@Min(1, { message: "w must be at least 1" })
@Max(12, { message: "w must not exceed 12" })
w!: number;
@IsNumber({}, { message: "h must be a number" })
@Min(1, { message: "h must be at least 1" })
@Max(12, { message: "h must not exceed 12" })
h!: number;
@IsOptional()
@IsString({ message: "title must be a string" })
@MaxLength(100, { message: "title must not exceed 100 characters" })
title?: string;
@IsOptional()
@IsObject({ message: "config must be an object" })
config?: Record<string, unknown>;
}

View File

@@ -0,0 +1,10 @@
/**
* Widget DTOs
*/
export { StatCardQueryDto } from "./stat-card-query.dto";
export { ChartQueryDto } from "./chart-query.dto";
export { ListQueryDto } from "./list-query.dto";
export { CalendarPreviewQueryDto } from "./calendar-preview-query.dto";
export { CreateWidgetConfigDto } from "./create-widget-config.dto";
export { UpdateWidgetConfigDto } from "./update-widget-config.dto";

View File

@@ -0,0 +1,48 @@
import {
IsString,
IsOptional,
IsIn,
IsObject,
IsNumber,
IsBoolean,
Min,
Max,
} from "class-validator";
/**
* DTO for querying list widget data
*/
export class ListQueryDto {
@IsString({ message: "dataSource must be a string" })
@IsIn(["tasks", "events", "projects"], {
message: "dataSource must be one of: tasks, events, projects",
})
dataSource!: "tasks" | "events" | "projects";
@IsOptional()
@IsString({ message: "sortBy must be a string" })
sortBy?: string;
@IsOptional()
@IsString({ message: "sortOrder must be a string" })
@IsIn(["asc", "desc"], { message: "sortOrder must be asc or desc" })
sortOrder?: "asc" | "desc";
@IsOptional()
@IsNumber({}, { message: "limit must be a number" })
@Min(1, { message: "limit must be at least 1" })
@Max(50, { message: "limit must not exceed 50" })
limit?: number;
@IsOptional()
@IsObject({ message: "filter must be an object" })
filter?: Record<string, unknown>;
@IsOptional()
@IsBoolean({ message: "showStatus must be a boolean" })
showStatus?: boolean;
@IsOptional()
@IsBoolean({ message: "showDueDate must be a boolean" })
showDueDate?: boolean;
}

View File

@@ -0,0 +1,27 @@
import {
IsString,
IsOptional,
IsIn,
IsObject,
} from "class-validator";
/**
* DTO for querying stat card widget data
*/
export class StatCardQueryDto {
@IsString({ message: "dataSource must be a string" })
@IsIn(["tasks", "events", "projects"], {
message: "dataSource must be one of: tasks, events, projects",
})
dataSource!: "tasks" | "events" | "projects";
@IsString({ message: "metric must be a string" })
@IsIn(["count", "completed", "overdue", "upcoming"], {
message: "metric must be one of: count, completed, overdue, upcoming",
})
metric!: "count" | "completed" | "overdue" | "upcoming";
@IsOptional()
@IsObject({ message: "filter must be an object" })
filter?: Record<string, unknown>;
}

View File

@@ -0,0 +1,7 @@
import { PartialType } from "@nestjs/mapped-types";
import { CreateWidgetConfigDto } from "./create-widget-config.dto";
/**
* DTO for updating a widget configuration
*/
export class UpdateWidgetConfigDto extends PartialType(CreateWidgetConfigDto) {}