feat: mail phase 3 - sorting (date/from/subject, asc/desc)

- Add sort_by and sort_order query params to list_mails endpoint
- Add deleted_at IS NULL filter to exclude soft-deleted mails
- Frontend: sort state in Mail.tsx, sort header bar in MailList
- Sort dropdown (Datum/Absender/Betreff) + asc/desc toggle
- fetchMails API accepts optional sortBy/sortOrder params
- i18n: 6 new sort keys (de/en)
This commit is contained in:
Agent Zero
2026-07-15 20:17:27 +02:00
parent ed1eec87dc
commit 4e100e9d33
6 changed files with 101 additions and 9 deletions
+14 -2
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import StreamingResponse
from sqlalchemy import and_, desc, func, or_, select
from sqlalchemy import and_, asc, desc, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
@@ -1488,6 +1488,8 @@ async def list_mails(
account_id: str | None = None,
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
sort_by: str = Query("date", pattern="^(date|from|subject)$"),
sort_order: str = Query("desc", pattern="^(asc|desc)$"),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
@@ -1499,8 +1501,18 @@ async def list_mails(
if account_id:
a_id = _parse_uuid(account_id, "account_id")
stmt = stmt.where(Mail.account_id == a_id)
# Exclude soft-deleted mails
stmt = stmt.where(Mail.deleted_at.is_(None))
total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar()
stmt = stmt.order_by(desc(Mail.received_at)).offset((page - 1) * page_size).limit(page_size)
# Dynamic sorting
sort_columns = {
"date": Mail.received_at,
"from": Mail.from_address,
"subject": Mail.subject,
}
sort_col = sort_columns.get(sort_by, Mail.received_at)
order_func = desc if sort_order == "desc" else asc
stmt = stmt.order_by(order_func(sort_col)).offset((page - 1) * page_size).limit(page_size)
mails = (await db.execute(stmt)).scalars().all()
return {
"mails": [mail_to_response(m) for m in mails],