feat: consumption page date filter — pick a day, see its detail

- HTML <input type=date> in a GET form, zero client JS (same server-component model)
- selected day detail card with tiles + rows above the period summary
- day-by-day list entries link to /consumption?date=YYYY-MM-DD and open+highlight the selected one
- 'Temizle' clears the filter

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TCFDONMEZ
2026-10-09 19:18:47 +03:00
co-authored by Claude
parent ea8032a813
commit e6ac3307ed
2 changed files with 174 additions and 29 deletions
+108 -29
View File
@@ -1,3 +1,4 @@
import Link from "next/link";
import { requireSession } from "@/lib/auth"; import { requireSession } from "@/lib/auth";
import { loadOverview, sumDays } from "@/lib/data"; import { loadOverview, sumDays } from "@/lib/data";
import { duration, efficiency, isoDate, num, precise } from "@/lib/format"; import { duration, efficiency, isoDate, num, precise } from "@/lib/format";
@@ -6,8 +7,13 @@ import { Row, Shell, Tile } from "@/components/shell";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function ConsumptionPage() { export default async function ConsumptionPage({
searchParams,
}: {
searchParams: Promise<{ date?: string }>;
}) {
await requireSession(); await requireSession();
const { date: selectedDate } = await searchParams;
const { installationId, days } = await loadOverview(90, 1); const { installationId, days } = await loadOverview(90, 1);
if (!installationId || days.length === 0) { if (!installationId || days.length === 0) {
@@ -26,8 +32,74 @@ export default async function ConsumptionPage() {
// Newest first for reading; the charts above keep chronological order. // Newest first for reading; the charts above keep chronological order.
const table = [...days].reverse(); const table = [...days].reverse();
// Find the selected day's record (if any). table is newest-first; a Map would be overkill
// for ≤90 entries.
const selected = selectedDate ? table.find((d) => d.date === selectedDate) : null;
const hasFilter = !!selectedDate;
const validFilter = hasFilter && !!selected;
// Date input bounds: oldest record to today.
const minDate = days[0].date; // days is chronological (oldest first after loadOverview reverse)
const maxDate = new Date().toISOString().slice(0, 10);
return ( return (
<Shell active="/consumption" title="Tüketim" subtitle={`${days.length} günlük kayıt`}> <Shell
active="/consumption"
title="Tüketim"
subtitle={hasFilter ? (validFilter ? isoDate(selectedDate!) : "Seçili gün kaydı yok") : `${days.length} günlük kayıt`}
>
{/* Date filter */}
<section className="card">
<form className="filter-form" method="get" action="/consumption">
<label className="filter-label" htmlFor="date-picker">
Tarihe göre filtrele
</label>
<div className="filter-row">
<input
id="date-picker"
type="date"
name="date"
defaultValue={selectedDate ?? ""}
min={minDate}
max={maxDate}
className="field date-input"
/>
<button className="button filter-button" type="submit">Filtrele</button>
{hasFilter && (
<Link className="filter-clear" href="/consumption">Temizle</Link>
)}
</div>
</form>
</section>
{/* Selected day detail — shown above the period tiles when a filter is active */}
{hasFilter && (
<section className="card">
<h2 className="card-title">{isoDate(selectedDate!)}</h2>
{validFilter && selected ? (
<>
<div className="tiles">
<Tile label="Mesafe" value={num(selected.day.km)} unit="km" tone="teal" />
<Tile label="Enerji" value={precise(selected.day.kwh)} unit="kWh" tone="gold" />
<Tile label="Ortalama" value={efficiency(selected.day.kwh, selected.day.km)} tone="teal" />
<Tile
label="Ortalama hız"
value={selected.day.hours > 0 ? num(selected.day.km / selected.day.hours) : "—"}
unit={selected.day.hours > 0 ? "km/h" : undefined}
/>
</div>
<div className="rows">
<Row label="Sürüş süresi" value={duration(selected.day.hours * 3600)} />
<Row label="Doluluk düşüşü" value={`%${num(selected.day.socDrop)}`} />
<Row label="O gün sonu ömür boyu" value={`${num(selected.lifetime.km)} km`} />
</div>
</>
) : (
<p className="empty">Bu tarih için tüketim kaydı yok.</p>
)}
</section>
)}
<div className="tiles"> <div className="tiles">
<Tile label="Toplam mesafe" value={num(total.km)} unit="km" tone="teal" /> <Tile label="Toplam mesafe" value={num(total.km)} unit="km" tone="teal" />
<Tile label="Toplam enerji" value={precise(total.kwh)} unit="kWh" tone="gold" /> <Tile label="Toplam enerji" value={precise(total.kwh)} unit="kWh" tone="gold" />
@@ -62,35 +134,42 @@ export default async function ConsumptionPage() {
<section className="card"> <section className="card">
<h2 className="card-title">Gün gün</h2> <h2 className="card-title">Gün gün</h2>
<ul className="list"> <ul className="list">
{table.map((day) => ( {table.map((day) => {
<li key={day.date}> const isSelected = hasFilter && day.date === selectedDate;
<details className="session"> return (
<summary> <li key={day.date}>
<div> <details className={`session${isSelected ? " selected" : ""}`} open={isSelected}>
<div className="session-when">{isoDate(day.date)}</div> <summary>
<div className="session-meta"> <div>
{num(day.day.km)} km · {efficiency(day.day.kwh, day.day.km)} <div className="session-when">
<Link href={`/consumption?date=${day.date}`} className="day-link">
{isoDate(day.date)}
</Link>
</div>
<div className="session-meta">
{num(day.day.km)} km · {efficiency(day.day.kwh, day.day.km)}
</div>
</div>
<div className="session-energy">
{precise(day.day.kwh)}
<span className="session-cost"> kWh</span>
</div>
</summary>
<div className="session-body">
<div className="rows">
<Row label="Sürüş süresi" value={duration(day.day.hours * 3600)} />
<Row
label="Ortalama hız"
value={day.day.hours > 0 ? `${num(day.day.km / day.day.hours)} km/h` : "—"}
/>
<Row label="Doluluk düşüşü" value={`%${num(day.day.socDrop)}`} />
<Row label="O gün sonu ömür boyu" value={`${num(day.lifetime.km)} km`} />
</div> </div>
</div> </div>
<div className="session-energy"> </details>
{precise(day.day.kwh)} </li>
<span className="session-cost"> kWh</span> );
</div> })}
</summary>
<div className="session-body">
<div className="rows">
<Row label="Sürüş süresi" value={duration(day.day.hours * 3600)} />
<Row
label="Ortalama hız"
value={day.day.hours > 0 ? `${num(day.day.km / day.day.hours)} km/h` : "—"}
/>
<Row label="Doluluk düşüşü" value={`%${num(day.day.socDrop)}`} />
<Row label="O gün sonu ömür boyu" value={`${num(day.lifetime.km)} km`} />
</div>
</div>
</details>
</li>
))}
</ul> </ul>
</section> </section>
</Shell> </Shell>
+66
View File
@@ -362,3 +362,69 @@ a {
padding: 5px 12px; padding: 5px 12px;
cursor: pointer; cursor: pointer;
} }
/* ---------- Consumption date filter ---------- */
.filter-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.filter-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--muted);
}
.filter-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.date-input {
width: auto;
min-width: 0;
flex: 1 1 150px;
margin-top: 0;
color-scheme: dark;
}
.filter-button {
width: auto;
margin-top: 0;
padding: 12px 18px;
flex: 0 0 auto;
}
.filter-clear {
font: inherit;
font-size: 13px;
color: var(--muted);
text-decoration: none;
border: 1px solid var(--outline);
border-radius: 12px;
padding: 11px 14px;
flex: 0 0 auto;
}
.filter-clear:active {
border-color: var(--accent);
}
/* Highlighted selected day in the day-by-day list */
.session.selected {
border-color: var(--accent);
}
.day-link {
color: var(--text);
text-decoration: none;
}
.day-link:active {
color: var(--accent);
}