feat: add day logs to report
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { Resend } from 'resend';
|
import { Resend } from 'resend';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import prisma from '@prisma/prisma';
|
import prisma from '@prisma/prisma';
|
||||||
import { ReportData } from '@utils/types';
|
import { ReportExpenseData, ReportDayLogsData } from '@utils/types';
|
||||||
|
|
||||||
export class ExpenseReporter {
|
export class ExpenseReporter {
|
||||||
private resend: Resend;
|
private resend: Resend;
|
||||||
@@ -22,7 +23,10 @@ export class ExpenseReporter {
|
|||||||
this.recipientEmail = process.env.RECIPIENT_EMAIL;
|
this.recipientEmail = process.env.RECIPIENT_EMAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateReport(from: Date, to: Date): Promise<ReportData> {
|
private async generateExpenses(
|
||||||
|
from: Date,
|
||||||
|
to: Date
|
||||||
|
): Promise<ReportExpenseData> {
|
||||||
const startDate = new Date(from);
|
const startDate = new Date(from);
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
@@ -77,7 +81,38 @@ export class ExpenseReporter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateHtmlReport(data: ReportData): string {
|
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) =>
|
const formatDate = (date: Date) =>
|
||||||
date.toLocaleDateString('en-US', {
|
date.toLocaleDateString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -105,12 +140,12 @@ export class ExpenseReporter {
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Expense Report</h1>
|
<h1>Diary Report</h1>
|
||||||
<p>From ${formatDate(data.dateRange.from)} to ${formatDate(data.dateRange.to)}</p>
|
<p>From ${formatDate(expenses.dateRange.from)} to ${formatDate(expenses.dateRange.to)}</p>
|
||||||
|
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<h2>Summary</h2>
|
<h2>Summary</h2>
|
||||||
<p><strong>Total Expenses:</strong> ${formatCurrency(data.summary.totalExpenses)}</p>
|
<p><strong>Total Expenses:</strong> ${formatCurrency(expenses.summary.totalExpenses)}</p>
|
||||||
|
|
||||||
<div class="category-summary">
|
<div class="category-summary">
|
||||||
<h3>Expenses by Category</h3>
|
<h3>Expenses by Category</h3>
|
||||||
@@ -120,7 +155,7 @@ export class ExpenseReporter {
|
|||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
<th>Count</th>
|
<th>Count</th>
|
||||||
</tr>
|
</tr>
|
||||||
${data.summary.byCategory
|
${expenses.summary.byCategory
|
||||||
.map(
|
.map(
|
||||||
cat => `
|
cat => `
|
||||||
<tr>
|
<tr>
|
||||||
@@ -144,7 +179,7 @@ export class ExpenseReporter {
|
|||||||
<th>Category</th>
|
<th>Category</th>
|
||||||
<th>Amount</th>
|
<th>Amount</th>
|
||||||
</tr>
|
</tr>
|
||||||
${data.expenses
|
${expenses.expenses
|
||||||
.map(
|
.map(
|
||||||
exp => `
|
exp => `
|
||||||
<tr>
|
<tr>
|
||||||
@@ -158,6 +193,33 @@ export class ExpenseReporter {
|
|||||||
)
|
)
|
||||||
.join('')}
|
.join('')}
|
||||||
</table>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
@@ -168,16 +230,27 @@ export class ExpenseReporter {
|
|||||||
to: Date,
|
to: Date,
|
||||||
includeJson: boolean = false
|
includeJson: boolean = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reportData = await this.generateReport(from, to);
|
const reportExpenseData = await this.generateExpenses(from, to);
|
||||||
const htmlContent = this.generateHtmlReport(reportData);
|
const reportDayLogData = await this.generateDayLogs(from, to);
|
||||||
|
const htmlContent = this.generateHtmlReport(
|
||||||
|
reportExpenseData,
|
||||||
|
reportDayLogData
|
||||||
|
);
|
||||||
|
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
if (includeJson) {
|
if (includeJson) {
|
||||||
const jsonData = JSON.stringify(reportData, null, 2);
|
const jsonExpenseData = JSON.stringify(reportExpenseData, null, 2);
|
||||||
attachments.push({
|
attachments.push({
|
||||||
filename: 'expense-report.json',
|
filename: 'expenses.json',
|
||||||
content: Buffer.from(jsonData).toString('base64'),
|
content: Buffer.from(jsonExpenseData).toString('base64'),
|
||||||
contentType: 'application/json' // Added MIME type
|
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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +258,7 @@ export class ExpenseReporter {
|
|||||||
const response = await this.resend.emails.send({
|
const response = await this.resend.emails.send({
|
||||||
from: this.senderEmail,
|
from: this.senderEmail,
|
||||||
to: this.recipientEmail,
|
to: this.recipientEmail,
|
||||||
subject: `Expense Report: ${from.toLocaleDateString()} - ${to.toLocaleDateString()}`,
|
subject: `Diary Report: ${from.toLocaleDateString()} - ${to.toLocaleDateString()}`,
|
||||||
html: htmlContent,
|
html: htmlContent,
|
||||||
attachments
|
attachments
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Category, Expense } from '@prisma/client';
|
import { Category, DayLog, Expense } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface Flag {
|
interface Flag {
|
||||||
@@ -40,7 +40,7 @@ const ExpenseSchema = z.object({
|
|||||||
|
|
||||||
export type ExpenseType = z.infer<typeof ExpenseSchema>;
|
export type ExpenseType = z.infer<typeof ExpenseSchema>;
|
||||||
|
|
||||||
export interface ReportData {
|
export interface ReportExpenseData {
|
||||||
expenses: (Expense & { category: Category })[];
|
expenses: (Expense & { category: Category })[];
|
||||||
summary: {
|
summary: {
|
||||||
totalExpenses: number;
|
totalExpenses: number;
|
||||||
@@ -55,3 +55,11 @@ export interface ReportData {
|
|||||||
to: Date;
|
to: Date;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReportDayLogsData {
|
||||||
|
dayLogs: DayLog[];
|
||||||
|
dateRange: {
|
||||||
|
from: Date;
|
||||||
|
to: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user