first commit
This commit is contained in:
156
api/src/controllers/report.controller.ts
Normal file
156
api/src/controllers/report.controller.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import type { Response } from 'express';
|
||||
import { createRequire } from 'node:module';
|
||||
import { z } from 'zod';
|
||||
import { AppDataSource } from '../config/data-source.js';
|
||||
import { AppSetting } from '../entities/AppSetting.js';
|
||||
import { User } from '../entities/User.js';
|
||||
import { getStatistics } from '../services/statistics.service.js';
|
||||
import type { AuthenticatedRequest } from '../types/express.js';
|
||||
|
||||
const preferencesSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
frequency: z.enum(['monthly', 'yearly', 'threshold']),
|
||||
thresholdAmount: z.number().min(0).default(0),
|
||||
sendToEmail: z.email().nullable().optional(),
|
||||
categoryIds: z.array(z.string().uuid()).default([])
|
||||
});
|
||||
|
||||
const previewOverrideSchema = preferencesSchema.partial();
|
||||
|
||||
const userRepo = () => AppDataSource.getRepository(User);
|
||||
const require = createRequire(import.meta.url);
|
||||
const settingsRepo = () => AppDataSource.getRepository(AppSetting);
|
||||
|
||||
const defaultPrefs = (email: string) => ({
|
||||
enabled: false,
|
||||
frequency: 'monthly' as const,
|
||||
thresholdAmount: 0,
|
||||
sendToEmail: email,
|
||||
categoryIds: [] as string[]
|
||||
});
|
||||
|
||||
const periodRange = (frequency: 'monthly' | 'yearly' | 'threshold') => {
|
||||
const now = new Date();
|
||||
const endDate = now.toISOString().slice(0, 10);
|
||||
|
||||
if (frequency === 'yearly') {
|
||||
return { startDate: `${now.getUTCFullYear()}-01-01`, endDate, bucket: 'month' as const, label: 'Year to date' };
|
||||
}
|
||||
|
||||
const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
||||
return { startDate: start.toISOString().slice(0, 10), endDate, bucket: 'month' as const, label: 'Current month' };
|
||||
};
|
||||
|
||||
const buildReportHtml = (title: string, summary: Awaited<ReturnType<typeof getStatistics>>) => {
|
||||
const categoryRows = summary.byCategory
|
||||
.slice(0, 6)
|
||||
.map((item) => `<tr><td style="padding:8px 0;border-bottom:1px solid #e5e7eb">${item.categoryName}</td><td style="padding:8px 0;border-bottom:1px solid #e5e7eb;text-align:right">${item.total.toFixed(2)}</td></tr>`)
|
||||
.join('');
|
||||
|
||||
const timelineRows = summary.timeline
|
||||
.slice(-6)
|
||||
.map((item) => `<tr><td style="padding:8px 0;border-bottom:1px solid #e5e7eb">${item.label}</td><td style="padding:8px 0;border-bottom:1px solid #e5e7eb;text-align:right">${item.total.toFixed(2)}</td></tr>`)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<div style="font-family:Arial,sans-serif;color:#111827;max-width:760px;margin:0 auto">
|
||||
<h1 style="margin-bottom:8px">${title}</h1>
|
||||
<p style="color:#4b5563;margin-top:0">Total: <strong>${summary.total.toFixed(2)}</strong> | Count: <strong>${summary.count}</strong> | Average: <strong>${summary.average.toFixed(2)}</strong></p>
|
||||
<h2 style="margin-top:32px">Top categories</h2>
|
||||
<table style="width:100%;border-collapse:collapse">${categoryRows || '<tr><td>No data</td></tr>'}</table>
|
||||
<h2 style="margin-top:32px">Timeline</h2>
|
||||
<table style="width:100%;border-collapse:collapse">${timelineRows || '<tr><td>No data</td></tr>'}</table>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
export const getPreferences = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const user = await userRepo().findOne({ where: { id: req.user!.id } });
|
||||
if (!user) return res.status(404).json({ message: 'User not found' });
|
||||
return res.json({ item: user.reportPreferences ?? defaultPrefs(user.email) });
|
||||
};
|
||||
|
||||
export const updatePreferences = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const parsed = preferencesSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ message: 'Invalid report preferences payload', issues: parsed.error.issues });
|
||||
}
|
||||
|
||||
const user = await userRepo().findOne({ where: { id: req.user!.id } });
|
||||
if (!user) return res.status(404).json({ message: 'User not found' });
|
||||
|
||||
user.reportPreferences = parsed.data;
|
||||
await userRepo().save(user);
|
||||
return res.json({ item: user.reportPreferences });
|
||||
};
|
||||
|
||||
export const previewReport = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const user = await userRepo().findOne({ where: { id: req.user!.id } });
|
||||
if (!user) return res.status(404).json({ message: 'User not found' });
|
||||
|
||||
const overridesParsed = previewOverrideSchema.safeParse(req.body ?? {});
|
||||
if (!overridesParsed.success) {
|
||||
return res.status(400).json({ message: 'Invalid report preview payload', issues: overridesParsed.error.issues });
|
||||
}
|
||||
|
||||
const prefs = { ...defaultPrefs(user.email), ...(user.reportPreferences ?? {}), ...overridesParsed.data };
|
||||
const range = periodRange(prefs.frequency ?? 'monthly');
|
||||
const summary = await getStatistics(
|
||||
{
|
||||
userId: user.id,
|
||||
startDate: range.startDate,
|
||||
endDate: range.endDate,
|
||||
categoryIds: prefs.categoryIds?.length ? prefs.categoryIds : undefined
|
||||
},
|
||||
range.bucket
|
||||
);
|
||||
|
||||
return res.json({
|
||||
range,
|
||||
summary,
|
||||
html: buildReportHtml(`${range.label} report`, summary)
|
||||
});
|
||||
};
|
||||
|
||||
export const sendReport = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const user = await userRepo().findOne({ where: { id: req.user!.id } });
|
||||
if (!user) return res.status(404).json({ message: 'User not found' });
|
||||
|
||||
const settings = await settingsRepo().find({ take: 1, order: { createdAt: 'ASC' } });
|
||||
const appSettings = settings[0];
|
||||
if (!appSettings || !appSettings.smtpEnabled || !appSettings.smtpHost || !appSettings.smtpFromEmail) {
|
||||
return res.status(400).json({ message: 'SMTP is not configured' });
|
||||
}
|
||||
|
||||
const prefs = user.reportPreferences ?? defaultPrefs(user.email);
|
||||
const range = periodRange(prefs.frequency ?? 'monthly');
|
||||
const summary = await getStatistics(
|
||||
{
|
||||
userId: user.id,
|
||||
startDate: range.startDate,
|
||||
endDate: range.endDate,
|
||||
categoryIds: prefs.categoryIds?.length ? prefs.categoryIds : undefined
|
||||
},
|
||||
range.bucket
|
||||
);
|
||||
|
||||
const nodemailer = require('nodemailer') as { createTransport: (options: unknown) => { sendMail: (message: unknown) => Promise<unknown> } };
|
||||
const transport = nodemailer.createTransport({
|
||||
host: appSettings.smtpHost,
|
||||
port: appSettings.smtpPort,
|
||||
secure: appSettings.smtpSecure,
|
||||
auth: appSettings.smtpUser ? { user: appSettings.smtpUser, pass: appSettings.smtpPassword ?? '' } : undefined
|
||||
});
|
||||
|
||||
const to = prefs.sendToEmail || user.email;
|
||||
await transport.sendMail({
|
||||
from: appSettings.smtpFromName
|
||||
? `"${appSettings.smtpFromName}" <${appSettings.smtpFromEmail}>`
|
||||
: appSettings.smtpFromEmail,
|
||||
to,
|
||||
subject: `${appSettings.appName} - ${range.label} report`,
|
||||
html: buildReportHtml(`${range.label} report`, summary)
|
||||
});
|
||||
|
||||
return res.json({ message: 'Report email was sent', sentTo: to });
|
||||
};
|
||||
Reference in New Issue
Block a user