This repository has been archived on 2026-02-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
diarywhisper/utils/commands/report.ts
2025-01-19 20:50:08 +00:00

275 lines
7.5 KiB
TypeScript

import { Resend } from 'resend';
import { Prisma } from '@prisma/client';
import prisma from '@prisma/prisma';
import { ReportExpenseData, ReportDayLogsData } from '@utils/types';
export class ExpenseReporter {
private resend: Resend;
private senderEmail: string;
private recipientEmail: string;
constructor() {
if (!process.env.RESEND_API_KEY) {
throw new Error('RESEND_API_KEY environment variable is not set');
}
if (!process.env.SENDER_EMAIL) {
throw new Error('SENDER_EMAIL environment variable is not set');
}
if (!process.env.RECIPIENT_EMAIL) {
throw new Error('RECIPIENT_EMAIL environment variable is not set');
}
this.resend = new Resend(process.env.RESEND_API_KEY);
this.senderEmail = process.env.SENDER_EMAIL;
this.recipientEmail = process.env.RECIPIENT_EMAIL;
}
private async generateExpenses(
from: Date,
to: Date
): Promise<ReportExpenseData> {
const startDate = new Date(from);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(to);
endDate.setHours(23, 59, 59, 999);
const expenses = await prisma.expense.findMany({
where: {
deleted: false,
createdAt: {
gte: startDate,
lte: endDate
}
},
include: {
category: true
},
orderBy: {
createdAt: 'desc'
}
});
const totalExpenses = expenses.reduce((sum, exp) => sum + exp.cost, 0);
const categoryMap = new Map<string, { total: number; count: number }>();
expenses.forEach(exp => {
const current = categoryMap.get(exp.category.name) || {
total: 0,
count: 0
};
categoryMap.set(exp.category.name, {
total: current.total + exp.cost,
count: current.count + 1
});
});
const byCategory = Array.from(categoryMap.entries())
.map(([category, stats]) => ({
category,
total: stats.total,
count: stats.count
}))
.sort((a, b) => b.total - a.total);
return {
expenses,
summary: {
totalExpenses,
byCategory
},
dateRange: { from, to }
};
}
private async generateDayLogs(
from: Date,
to: Date
): Promise<ReportDayLogsData> {
const startDate = new Date(from);
startDate.setHours(0, 0, 0, 0);
const endDate = new Date(to);
endDate.setHours(23, 59, 59, 999);
const dayLogs = await prisma.dayLog.findMany({
where: {
createdAt: {
gte: startDate,
lte: endDate
}
},
orderBy: {
createdAt: 'desc'
}
});
return {
dayLogs,
dateRange: { from, to }
};
}
private generateHtmlReport(
expenses: ReportExpenseData,
dayLogs: ReportDayLogsData
): string {
const formatDate = (date: Date) =>
date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const formatCurrency = (amount: number) =>
amount.toLocaleString('en-US', {
style: 'currency',
currency: 'EUR'
});
return `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #f5f5f5; }
.summary { margin: 20px 0; padding: 20px; background: #f9f9f9; border-radius: 5px; }
.category-summary { margin-top: 10px; }
</style>
</head>
<body>
<h1>Diary Report</h1>
<p>From ${formatDate(expenses.dateRange.from)} to ${formatDate(expenses.dateRange.to)}</p>
<div class="summary">
<h2>Summary</h2>
<p><strong>Total Expenses:</strong> ${formatCurrency(expenses.summary.totalExpenses)}</p>
<div class="category-summary">
<h3>Expenses by Category</h3>
<table>
<tr>
<th>Category</th>
<th>Total</th>
<th>Count</th>
</tr>
${expenses.summary.byCategory
.map(
cat => `
<tr>
<td>${cat.category}</td>
<td>${formatCurrency(cat.total)}</td>
<td>${cat.count}</td>
</tr>
`
)
.join('')}
</table>
</div>
</div>
<h2>Detailed Expenses</h2>
<table>
<tr>
<th>ID</th>
<th>Date</th>
<th>Description</th>
<th>Category</th>
<th>Amount</th>
</tr>
${expenses.expenses
.map(
exp => `
<tr>
<td>${exp.id}</td>
<td>${formatDate(exp.createdAt)}</td>
<td>${exp.description}</td>
<td>${exp.category.name}</td>
<td>${formatCurrency(exp.cost)}</td>
</tr>
`
)
.join('')}
</table>
<h2>Day Logs Report</h2>
<p>From ${formatDate(dayLogs.dateRange.from)} to ${formatDate(dayLogs.dateRange.to)}</p>
<table>
<tr>
<th>ID</th>
<th>Date</th>
<th>Log</th>
</tr>
${dayLogs.dayLogs
.filter(
(dl): dl is typeof dl & { comments: any[] } =>
dl.comments !== null && Array.isArray(dl.comments)
)
.flatMap(dl =>
dl.comments.map(
comment => `
<tr>
<td>${dl.id}</td>
<td>${formatDate(dl.createdAt)}</td>
<td>${comment.text}</td>
</tr>
`
)
)
.join('')}
</table>
</body>
</html>
`;
}
async sendReport(
from: Date,
to: Date,
includeJson: boolean = false
): Promise<void> {
const reportExpenseData = await this.generateExpenses(from, to);
const reportDayLogData = await this.generateDayLogs(from, to);
const htmlContent = this.generateHtmlReport(
reportExpenseData,
reportDayLogData
);
const attachments = [];
if (includeJson) {
const jsonExpenseData = JSON.stringify(reportExpenseData, null, 2);
attachments.push({
filename: 'expenses.json',
content: Buffer.from(jsonExpenseData).toString('base64'),
contentType: 'application/json'
});
const jsonDayLogData = JSON.stringify(reportDayLogData, null, 2);
attachments.push({
filename: 'day-logs.json',
content: Buffer.from(jsonDayLogData).toString('base64'),
contentType: 'application/json'
});
}
try {
const response = await this.resend.emails.send({
from: this.senderEmail,
to: this.recipientEmail,
subject: `Diary Report: ${from.toLocaleDateString()} - ${to.toLocaleDateString()}`,
html: htmlContent,
attachments
});
if (response.error) {
throw new Error('Failed to send email: No id returned from Resend');
}
} catch (error) {
console.error('Failed to send email:', error);
throw new Error(`Email sending failed: ${error}`);
}
}
}