feat: Smart Folders sidebar, category heatmap, sidebar counts API
- Replace plain activity heatmap with CategoryHeatmapWidget on dashboard - Add collapsible Smart Folders sidebar with Favorites, Mailbox, Smart Folders sections - Favorites: customisable pinned shortcuts (default: Inbox, Unread, Large, Old, Read Later); pin/unpin smart folders via buttons; persists to localStorage - Mailbox section: 11 Gmail mailbox items with icons and live count badges - Smart Folders section: 10 category folders sorted by count desc - Live count badges via new GET /api/v1/analytics/sidebar-counts endpoint - Backend: SidebarCountsDto, GetSidebarCountsAsync with label lookups for SENT/DRAFT/SPAM, size/age filters, EmailCategory mapping - Sidebar collapses to icon-only; section states persist to localStorage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,19 @@ public class AnalyticsService : IAnalyticsService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CategoryHeatmapCellDto>> GetCategoryHeatmapAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var raw = await _db.Emails
|
||||
.Where(e => e.UserId == userId)
|
||||
.Select(e => new { e.SentAtUtc, e.Category })
|
||||
.ToListAsync(ct);
|
||||
|
||||
return raw
|
||||
.GroupBy(x => new { x.Category, Dow = (int)x.SentAtUtc.DayOfWeek })
|
||||
.Select(g => new CategoryHeatmapCellDto(g.Key.Category.ToString(), g.Key.Dow, g.Count()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AttachmentBreakdownDto>> GetAttachmentBreakdownAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var raw = await _db.Attachments
|
||||
@@ -115,6 +128,60 @@ public class AnalyticsService : IAnalyticsService
|
||||
_ => "other"
|
||||
};
|
||||
|
||||
public async Task<SidebarCountsDto> GetSidebarCountsAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var emails = _db.Emails.Where(e => e.UserId == userId);
|
||||
|
||||
var allMail = await emails.CountAsync(ct);
|
||||
var inbox = await emails.CountAsync(e => e.IsInInbox, ct);
|
||||
var unread = await emails.CountAsync(e => e.IsUnread, ct);
|
||||
var starred = await emails.CountAsync(e => e.IsStarred, ct);
|
||||
var trash = await emails.CountAsync(e => e.IsTrashed, ct);
|
||||
var large = await emails.CountAsync(e => e.SizeEstimateBytes > 5_000_000, ct);
|
||||
var cutoff = DateTimeOffset.UtcNow.AddYears(-1);
|
||||
var old = await emails.CountAsync(e => e.SentAtUtc < cutoff, ct);
|
||||
|
||||
// Label-backed counts (SENT / DRAFT / SPAM are system Gmail labels)
|
||||
async Task<int> LabelCount(string gmailId)
|
||||
{
|
||||
var labelId = await _db.Labels
|
||||
.Where(l => l.UserId == userId && l.GmailLabelId == gmailId)
|
||||
.Select(l => (Guid?)l.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
return labelId.HasValue
|
||||
? await _db.EmailLabels.CountAsync(el => el.LabelId == labelId.Value, ct)
|
||||
: 0;
|
||||
}
|
||||
|
||||
var sent = await LabelCount("SENT");
|
||||
var drafts = await LabelCount("DRAFT");
|
||||
var spam = await LabelCount("SPAM");
|
||||
|
||||
// Category → smart-folder slug mapping
|
||||
var catCounts = await emails
|
||||
.GroupBy(e => e.Category)
|
||||
.Select(g => new { Cat = g.Key, N = g.Count() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
int Cat(EmailCategory c) => catCounts.FirstOrDefault(x => x.Cat == c)?.N ?? 0;
|
||||
|
||||
var smartFolders = new Dictionary<string, int>
|
||||
{
|
||||
["automated"] = Cat(EmailCategory.Notification),
|
||||
["finance"] = Cat(EmailCategory.Finance),
|
||||
["social"] = Cat(EmailCategory.Social),
|
||||
["shopping"] = Cat(EmailCategory.Promotional),
|
||||
["noreply"] = 0,
|
||||
["gaming"] = 0,
|
||||
["sales"] = 0,
|
||||
["ridesharing"] = 0,
|
||||
["food"] = 0,
|
||||
["wellness"] = 0,
|
||||
};
|
||||
|
||||
return new SidebarCountsDto(inbox, allMail, unread, starred, sent, drafts, trash, spam, large, old, smartFolders);
|
||||
}
|
||||
|
||||
public async Task RefreshAggregatesAsync(Guid userId, CancellationToken ct = default)
|
||||
{
|
||||
var since = DateTimeOffset.UtcNow.AddDays(-365);
|
||||
|
||||
Reference in New Issue
Block a user