From 08ce8f8d90606a75efd3b68f27b3ff4c98649983 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Sat, 28 Mar 2026 16:59:19 +0100 Subject: [PATCH] Enhanced Dashboard Analytics & Reporting (#211) --- .../src/analytics/analytics.controller.ts | 22 +++++++++++++ .../src/analytics/analytics.repository.ts | 32 +++++++++++++++++++ .../src/analytics/analytics.service.ts | 21 ++++++++++++ .../analytics/tests/analytics.service.spec.ts | 26 +++++++++++++++ app/backend/src/utils/csv-export.ts | 6 ++++ app/backend/src/utils/pdf-export.ts | 14 ++++++++ 6 files changed, 121 insertions(+) create mode 100644 app/backend/src/analytics/analytics.controller.ts create mode 100644 app/backend/src/analytics/analytics.repository.ts create mode 100644 app/backend/src/analytics/analytics.service.ts create mode 100644 app/backend/src/analytics/tests/analytics.service.spec.ts create mode 100644 app/backend/src/utils/csv-export.ts create mode 100644 app/backend/src/utils/pdf-export.ts diff --git a/app/backend/src/analytics/analytics.controller.ts b/app/backend/src/analytics/analytics.controller.ts new file mode 100644 index 0000000..e2987cf --- /dev/null +++ b/app/backend/src/analytics/analytics.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Query } from "@nestjs/common"; +import { AnalyticsService } from "./analytics.service"; + +@Controller("analytics") +export class AnalyticsController { + constructor(private readonly service: AnalyticsService) {} + + @Get("dashboard") + async dashboard(@Query("userId") userId: string) { + return this.service.getDashboardData(userId); + } + + @Get("time-series") + async timeSeries(@Query("userId") userId: string, @Query("interval") interval: "day" | "week" | "month") { + return this.service.getTimeSeriesData(userId, interval); + } + + @Get("export") + async export(@Query("userId") userId: string, @Query("format") format: "csv" | "pdf") { + return this.service.exportReport(userId, format); + } +} diff --git a/app/backend/src/analytics/analytics.repository.ts b/app/backend/src/analytics/analytics.repository.ts new file mode 100644 index 0000000..13e3c88 --- /dev/null +++ b/app/backend/src/analytics/analytics.repository.ts @@ -0,0 +1,32 @@ +import { AppDataSource } from "../data-source"; +import { Transaction } from "../entities/transaction.entity"; + +export const transactionRepo = AppDataSource.getRepository(Transaction); + +export async function getTotalVolumeUSD(userId: string) { + return transactionRepo + .createQueryBuilder("t") + .select("SUM(t.amount_usd)", "total") + .where("t.userId = :userId", { userId }) + .getRawOne(); +} + +export async function getAssetDistribution(userId: string) { + return transactionRepo + .createQueryBuilder("t") + .select("t.asset, SUM(t.amount_usd)", "total") + .where("t.userId = :userId", { userId }) + .groupBy("t.asset") + .getRawMany(); +} + +export async function getTimeSeries(userId: string, interval: "day" | "week" | "month") { + return transactionRepo + .createQueryBuilder("t") + .select(`DATE_TRUNC('${interval}', t.createdAt)`, "period") + .addSelect("SUM(t.amount_usd)", "total") + .where("t.userId = :userId", { userId }) + .groupBy("period") + .orderBy("period", "ASC") + .getRawMany(); +} diff --git a/app/backend/src/analytics/analytics.service.ts b/app/backend/src/analytics/analytics.service.ts new file mode 100644 index 0000000..afbdbc5 --- /dev/null +++ b/app/backend/src/analytics/analytics.service.ts @@ -0,0 +1,21 @@ +import { getTotalVolumeUSD, getAssetDistribution, getTimeSeries } from "./analytics.repository"; +import { exportToCSV } from "../utils/csv-export"; +import { exportToPDF } from "../utils/pdf-export"; + +export class AnalyticsService { + async getDashboardData(userId: string) { + const totalVolume = await getTotalVolumeUSD(userId); + const distribution = await getAssetDistribution(userId); + return { totalVolume, distribution }; + } + + async getTimeSeriesData(userId: string, interval: "day" | "week" | "month") { + return getTimeSeries(userId, interval); + } + + async exportReport(userId: string, format: "csv" | "pdf") { + const data = await this.getDashboardData(userId); + if (format === "csv") return exportToCSV(data); + if (format === "pdf") return exportToPDF(data); + } +} diff --git a/app/backend/src/analytics/tests/analytics.service.spec.ts b/app/backend/src/analytics/tests/analytics.service.spec.ts new file mode 100644 index 0000000..7640db1 --- /dev/null +++ b/app/backend/src/analytics/tests/analytics.service.spec.ts @@ -0,0 +1,26 @@ +import { AnalyticsService } from "../analytics.service"; + +describe("AnalyticsService", () => { + const service = new AnalyticsService(); + + it("should return dashboard data", async () => { + const data = await service.getDashboardData("user1"); + expect(data).toHaveProperty("totalVolume"); + expect(data).toHaveProperty("distribution"); + }); + + it("should return time series data", async () => { + const series = await service.getTimeSeriesData("user1", "day"); + expect(Array.isArray(series)).toBe(true); + }); + + it("should export CSV", async () => { + const csv = await service.exportReport("user1", "csv"); + expect(typeof csv).toBe("string"); + }); + + it("should export PDF", async () => { + const pdf = await service.exportReport("user1", "pdf"); + expect(typeof pdf).toBe("string"); + }); +}); diff --git a/app/backend/src/utils/csv-export.ts b/app/backend/src/utils/csv-export.ts new file mode 100644 index 0000000..87c18b7 --- /dev/null +++ b/app/backend/src/utils/csv-export.ts @@ -0,0 +1,6 @@ +import { Parser } from "json2csv"; + +export function exportToCSV(data: any) { + const parser = new Parser(); + return parser.parse(data); +} diff --git a/app/backend/src/utils/pdf-export.ts b/app/backend/src/utils/pdf-export.ts new file mode 100644 index 0000000..e3ca053 --- /dev/null +++ b/app/backend/src/utils/pdf-export.ts @@ -0,0 +1,14 @@ +import PDFDocument from "pdfkit"; + +export function exportToPDF(data: any) { + const doc = new PDFDocument(); + let buffers: Buffer[] = []; + doc.on("data", buffers.push.bind(buffers)); + doc.on("end", () => Buffer.concat(buffers)); + + doc.text("Financial Report"); + doc.text(JSON.stringify(data, null, 2)); + doc.end(); + + return Buffer.concat(buffers).toString("base64"); +}