chore: init project

This commit is contained in:
cesnimda
2026-06-30 15:53:32 +02:00
commit f43ef5f945
94 changed files with 4405 additions and 0 deletions
@@ -0,0 +1,112 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
using CsvHelper;
using InboxIntel.Application.Abstractions;
using DTOs = InboxIntel.Application.DTOs;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace InboxIntel.Infrastructure.Export;
/// <summary>
/// Builds inbox reports as PDF (QuestPDF), CSV (CsvHelper) or JSON. The report
/// covers the inbox summary, sender stats, cleanup suggestions and storage use.
/// </summary>
public class ExportService : IExportService
{
private readonly IAnalyticsService _analytics;
private readonly IAiService _ai;
public ExportService(IAnalyticsService analytics, IAiService ai)
{
_analytics = analytics;
_ai = ai;
QuestPDF.Settings.License = LicenseType.Community;
}
public async Task<(byte[] Content, string ContentType, string FileName)> ExportReportAsync(Guid userId, ExportFormat format, CancellationToken ct = default)
{
var health = await _analytics.GetInboxHealthAsync(userId, ct);
var topSenders = await _analytics.GetTopSendersAsync(userId, 25, ct);
var suggestions = await _ai.SuggestCleanupAsync(userId, ct);
var stamp = DateTime.UtcNow.ToString("yyyyMMdd-HHmm");
return format switch
{
ExportFormat.Json => (BuildJson(health, topSenders, suggestions), "application/json", $"inbox-report-{stamp}.json"),
ExportFormat.Csv => (BuildCsv(topSenders), "text/csv", $"sender-stats-{stamp}.csv"),
_ => (BuildPdf(health, topSenders, suggestions), "application/pdf", $"inbox-report-{stamp}.pdf")
};
}
private static byte[] BuildJson(object health, object senders, object suggestions)
{
var payload = new { generatedUtc = DateTimeOffset.UtcNow, health, topSenders = senders, cleanupSuggestions = suggestions };
return JsonSerializer.SerializeToUtf8Bytes(payload, new JsonSerializerOptions { WriteIndented = true });
}
private static byte[] BuildCsv(IEnumerable<DTOs.SenderStatDto> senders)
{
using var ms = new MemoryStream();
using (var writer = new StreamWriter(ms, Encoding.UTF8, leaveOpen: true))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(senders);
}
return ms.ToArray();
}
private byte[] BuildPdf(DTOs.InboxHealthDto health, IReadOnlyList<DTOs.SenderStatDto> senders, IReadOnlyList<DTOs.AiCleanupSuggestionDto> suggestions)
{
var doc = Document.Create(container =>
{
container.Page(page =>
{
page.Margin(40);
page.Size(PageSizes.A4);
page.DefaultTextStyle(t => t.FontSize(10));
page.Header().Text("InboxIntel — Inbox Report").FontSize(20).Bold();
page.Content().PaddingVertical(10).Column(col =>
{
col.Item().Text($"Generated: {DateTime.UtcNow:u}").FontColor(Colors.Grey.Medium);
col.Item().PaddingTop(10).Text($"Inbox Health: {health.Score}/100 (Grade {health.Grade})").FontSize(14).Bold();
col.Item().Text($"Total emails: {health.TotalEmails:N0} Unread: {health.UnreadEmails:N0} Newsletters: {health.NewsletterCount:N0}");
col.Item().Text($"Estimated storage: {health.EstimatedStorageBytes / 1_048_576.0:N1} MB");
col.Item().PaddingTop(14).Text("Recommendations").FontSize(13).Bold();
foreach (var rec in health.Recommendations)
col.Item().Text($"• {rec}");
col.Item().PaddingTop(14).Text("Top Senders").FontSize(13).Bold();
col.Item().Table(table =>
{
table.ColumnsDefinition(c => { c.RelativeColumn(3); c.RelativeColumn(1); c.RelativeColumn(1); });
table.Header(h =>
{
h.Cell().Text("Sender").Bold();
h.Cell().Text("Emails").Bold();
h.Cell().Text("Unread").Bold();
});
foreach (var s in senders)
{
table.Cell().Text(s.Address);
table.Cell().Text(s.EmailCount.ToString("N0"));
table.Cell().Text(s.UnreadCount.ToString("N0"));
}
});
col.Item().PaddingTop(14).Text("Cleanup Suggestions").FontSize(13).Bold();
foreach (var sug in suggestions)
col.Item().Text($"• {sug.Title} — {sug.Rationale}");
});
page.Footer().AlignCenter().Text(t => { t.Span("InboxIntel • "); t.CurrentPageNumber(); t.Span(" / "); t.TotalPages(); });
});
});
return doc.GeneratePdf();
}
}