Add interactive citizen page with sidebar, display settings, and adaptive CSS

Major rework of the citizen-facing page:
- Chart + sidebar layout (auth/vote/countdown in right sidebar)
- DisplaySettings component (font size, chart density, color palettes)
- Adaptive CSS with clamp() throughout, responsive breakpoints at 480/768/1024
- Baseline charts zoomed on first tier for small consumption detail
- Marginal price chart with dual Y-axes (foyers left, €/m³ right)
- Key metrics banner (5 columns: recettes, palier, prix palier, prix médian, mon prix)
- Client-side p0/impacts computation, draggable median price bar
- Household dots toggle, vote overlay curves
- Auth returns volume_m3, vote captures submitted_at
- Cleaned header nav (removed Accueil/Super Admin for public visitors)
- Terminology: foyer for bills, électeur for votes
- 600m³ added to impact reference volumes
- Realistic seed votes (50 votes, 3 profiles)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-23 21:00:22 +01:00
parent 6caea1b809
commit 5dc42af33e
19 changed files with 2109 additions and 416 deletions

View File

@@ -16,6 +16,59 @@ from app.services.import_service import parse_import_file, import_households, ge
router = APIRouter()
@router.get("/communes/{slug}/households/distribution")
async def household_distribution(
slug: str,
bucket_size: int = 50,
db: AsyncSession = Depends(get_db),
):
"""Public: returns consumption distribution as histogram buckets."""
result = await db.execute(select(Commune).where(Commune.slug == slug))
commune = result.scalar_one_or_none()
if not commune:
raise HTTPException(status_code=404, detail="Commune introuvable")
hh_result = await db.execute(
select(Household).where(Household.commune_id == commune.id)
)
households = hh_result.scalars().all()
if not households:
return {"buckets": [], "total": 0, "bucket_size": bucket_size}
volumes = [h.volume_m3 for h in households]
max_vol = max(volumes)
num_buckets = int(max_vol // bucket_size) + 1
buckets = []
for i in range(num_buckets):
low = i * bucket_size
high = (i + 1) * bucket_size
count = sum(1 for v in volumes if low <= v < high)
if count > 0 or i < num_buckets:
buckets.append({"low": low, "high": high, "count": count})
return {"buckets": buckets, "total": len(households), "bucket_size": bucket_size}
@router.get("/communes/{slug}/households/volumes")
async def household_volumes(
slug: str,
db: AsyncSession = Depends(get_db),
):
"""Public: returns anonymous list of household volumes and statuses for chart display."""
result = await db.execute(select(Commune).where(Commune.slug == slug))
commune = result.scalar_one_or_none()
if not commune:
raise HTTPException(status_code=404, detail="Commune introuvable")
hh_result = await db.execute(
select(Household.volume_m3, Household.status).where(Household.commune_id == commune.id)
)
rows = hh_result.all()
return [{"volume_m3": r.volume_m3, "status": r.status} for r in rows]
@router.get("/communes/{slug}/households/template")
async def download_template():
content = generate_template_csv()