feat: přidána midnight snack
feat: přidány statistiky pohybu
This commit is contained in:
1 parent
8dac67bdd1
commit
f1ce3ff297
13 files changed
+833
-163
No files matched your search
@@ -33,6 +33,19 @@
|
||||
--ft-danger: #dc2626;
|
||||
--ft-info: #2563eb;
|
||||
|
||||
// Kategoriální řady grafů (šablony tréninků) — pevné pořadí, ověřené na
|
||||
// rozlišitelnost i pro barvoslepé. Mimo šablonu a přebytečné šablony jsou neutrální.
|
||||
--ft-series-1: #2a78d6;
|
||||
--ft-series-2: #eb6834;
|
||||
--ft-series-3: #1baf7a;
|
||||
--ft-series-4: #eda100;
|
||||
--ft-series-5: #e87ba4;
|
||||
--ft-series-6: #008300;
|
||||
--ft-series-7: #4a3aa7;
|
||||
--ft-series-8: #e34948;
|
||||
--ft-series-other-template: #7c7b76;
|
||||
--ft-series-other: #b8b7b1;
|
||||
|
||||
// Shadows
|
||||
--ft-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--ft-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
@@ -73,6 +86,17 @@
|
||||
--ft-danger: #f87171;
|
||||
--ft-info: #60a5fa;
|
||||
|
||||
--ft-series-1: #3987e5;
|
||||
--ft-series-2: #d95926;
|
||||
--ft-series-3: #199e70;
|
||||
--ft-series-4: #c98500;
|
||||
--ft-series-5: #d55181;
|
||||
--ft-series-6: #008300;
|
||||
--ft-series-7: #9085e9;
|
||||
--ft-series-8: #e66767;
|
||||
--ft-series-other-template: #8f8e88;
|
||||
--ft-series-other: #55544f;
|
||||
|
||||
--ft-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.5);
|
||||
--ft-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.5), 0 2px 4px -2px rgb(0 0 0 / 0.4);
|
||||
--ft-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5), 0 4px 6px -4px rgb(0 0 0 / 0.4);
|
||||
|
||||
+4
-1
@@ -7,6 +7,7 @@ export const MEAL_TYPE_NAMES: Record<MealType, string> = {
|
||||
OBED: 'Oběd',
|
||||
ODPOLEDNI_SVACINA: 'Odpolední svačina',
|
||||
VECERE: 'Večeře',
|
||||
PULNOCNI_SVACINA: 'Půlnoční svačina',
|
||||
JINE: 'Jiné',
|
||||
};
|
||||
|
||||
@@ -17,6 +18,7 @@ export const MEAL_TYPE_ICONS: Record<MealType, string> = {
|
||||
OBED: '🍲',
|
||||
ODPOLEDNI_SVACINA: '🥨',
|
||||
VECERE: '🍽',
|
||||
PULNOCNI_SVACINA: '🌙',
|
||||
JINE: '🍫',
|
||||
};
|
||||
|
||||
@@ -27,6 +29,7 @@ export const MEAL_TYPES_IN_ORDER: MealType[] = [
|
||||
MealType.OBED,
|
||||
MealType.ODPOLEDNI_SVACINA,
|
||||
MealType.VECERE,
|
||||
MealType.PULNOCNI_SVACINA,
|
||||
MealType.JINE,
|
||||
];
|
||||
|
||||
@@ -75,7 +78,7 @@ export const ACTIVITY_PRESETS: { name: string, unit: ActivityUnit, caloriesPer10
|
||||
{ name: 'Shyby', unit: ActivityUnit.OPAKOVANI, caloriesPer100Units: 100 },
|
||||
];
|
||||
|
||||
/** Záložky přehledu dne. */
|
||||
/** Záložky příjem/výdej — v přehledu dne i ve statistikách. */
|
||||
export enum DayTab {
|
||||
PRIJEM = 'PRIJEM',
|
||||
VYDEJ = 'VYDEJ',
|
||||
|
||||
@@ -35,9 +35,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.range-input {
|
||||
width: auto;
|
||||
min-width: 140px;
|
||||
.period-picker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
.form-select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,3 +211,106 @@
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
// Odchylka od šablon — vynechané je červeně, přidané navíc zeleně
|
||||
.deviation-minus,
|
||||
.summary-tile .summary-value.deviation-minus {
|
||||
color: var(--ft-danger);
|
||||
}
|
||||
|
||||
.deviation-plus,
|
||||
.summary-tile .summary-value.deviation-plus {
|
||||
color: var(--ft-success);
|
||||
}
|
||||
|
||||
.usage-table {
|
||||
.extras-toggle {
|
||||
cursor: pointer;
|
||||
|
||||
td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover td {
|
||||
background: var(--ft-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.extras-item td {
|
||||
color: var(--ft-text-secondary);
|
||||
|
||||
&:first-child {
|
||||
padding-left: 26px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bublina sloupce výdeje — hodnoty v barvě textu, identitu nese jen čtvereček
|
||||
.activity-tooltip {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.82rem;
|
||||
max-width: 280px;
|
||||
box-shadow: var(--ft-shadow);
|
||||
|
||||
.activity-tooltip-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-tooltip-empty {
|
||||
color: var(--ft-text-muted);
|
||||
}
|
||||
|
||||
.activity-tooltip-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.activity-tooltip-swatch {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.plan {
|
||||
height: 0;
|
||||
border-top: 2px dashed var(--ft-text-secondary);
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-tooltip-value {
|
||||
margin-left: auto;
|
||||
padding-left: 12px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.activity-tooltip-plan {
|
||||
color: var(--ft-text-secondary);
|
||||
}
|
||||
|
||||
.plan-note {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--ft-border-light);
|
||||
}
|
||||
|
||||
.plan-note-date {
|
||||
color: var(--ft-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.plan-note-icon {
|
||||
color: var(--ft-warning);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.usage-note {
|
||||
margin: 10px 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--ft-text-muted);
|
||||
}
|
||||
+138
-151
@@ -1,39 +1,36 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Spinner } from "react-bootstrap";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCoins, faFire, faListCheck, faCalendarDay, faFileImport, faChartColumn } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faCoins, faFire, faListCheck, faCalendarDay, faFileImport, faChartColumn, faUtensils, faPersonRunning,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, Legend,
|
||||
} from "recharts";
|
||||
import { FoodStats, MealType, getFoodStats } from "../../../types";
|
||||
import { MEAL_TYPE_NAMES, StatsRange, STATS_RANGE_NAMES } from "../enums";
|
||||
import { DAY_TAB_NAMES, DayTab, MEAL_TYPE_NAMES, StatsRange, STATS_RANGE_NAMES } from "../enums";
|
||||
import {
|
||||
formatCalories, formatDateString, formatMonth, formatPrice, formatPriceShort,
|
||||
getHumanMonth, getMonthEnd, getMonthStart,
|
||||
formatCalories, formatMonth, formatPrice, formatPriceShort, getMonthEnd, getMonthStart,
|
||||
} from "../Utils";
|
||||
import ImportModal from "../components/modals/ImportModal";
|
||||
import PeriodPicker, { EARLIEST_YEAR } from "../components/PeriodPicker";
|
||||
import ActivityStatsPanel from "./ActivityStatsPanel";
|
||||
import { TOOLTIP_STYLE, formatPeriodLabel, pickPeriods } from "./statsShared";
|
||||
import "./StatsPage.scss";
|
||||
|
||||
/** Nejstarší rok, do kterého sahá volba "Vše". Starší data nikdo evidovat nebude. */
|
||||
const EARLIEST_YEAR = 2015;
|
||||
|
||||
/** Barvy výsečí grafu dle typu jídla — laděné do zelené palety aplikace. */
|
||||
const PIE_COLORS = ['#16a34a', '#22c55e', '#4ade80', '#86efac', '#0ea5e9', '#a3a3a3'];
|
||||
|
||||
/** Vzhled bubliny s hodnotou — recharts ji kreslí inline stylem, ne přes CSS třídu. */
|
||||
const TOOLTIP_STYLE = {
|
||||
background: 'var(--ft-bg-card)',
|
||||
border: '1px solid var(--ft-border)',
|
||||
borderRadius: 'var(--ft-radius-sm)',
|
||||
color: 'var(--ft-text)',
|
||||
};
|
||||
const PIE_COLORS = ['#16a34a', '#22c55e', '#4ade80', '#86efac', '#0ea5e9', '#6366f1', '#a3a3a3'];
|
||||
|
||||
/** Naformátuje hodnotu v bublině grafu na koruny. Hodnoty chodí z rechartsu jako `unknown`. */
|
||||
function formatChartPrice(value: unknown): string {
|
||||
return `${Number(value).toLocaleString('cs-CZ')} Kč`;
|
||||
}
|
||||
|
||||
/** Ikony záložek statistik — stejné dělení jako přehled dne. */
|
||||
const TAB_ICONS = { [DayTab.PRIJEM]: faUtensils, [DayTab.VYDEJ]: faPersonRunning };
|
||||
|
||||
export default function StatsPage() {
|
||||
const [tab, setTab] = useState<DayTab>(DayTab.PRIJEM);
|
||||
const [mode, setMode] = useState<StatsRange>(StatsRange.MESIC);
|
||||
const [month, setMonth] = useState<string>(() => formatMonth(new Date()));
|
||||
const [year, setYear] = useState<number>(() => new Date().getFullYear());
|
||||
@@ -59,31 +56,15 @@ export default function StatsPage() {
|
||||
setLoading(false);
|
||||
}, [range]);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
// Výdej si data načítá sám ve svém panelu
|
||||
useEffect(() => { if (tab === DayTab.PRIJEM) load(); }, [load, tab]);
|
||||
|
||||
/** Řada pro sloupcový graf — dny u měsíce, měsíce u roku, roky u celého přehledu. */
|
||||
const series = useMemo(() => {
|
||||
if (!stats) return [];
|
||||
if (mode === StatsRange.MESIC) {
|
||||
return stats.byDay.map(total => ({
|
||||
label: formatDateString(total.period).slice(0, 5),
|
||||
price: total.price / 100,
|
||||
calories: total.calories,
|
||||
}));
|
||||
}
|
||||
if (mode === StatsRange.ROK) {
|
||||
return stats.byMonth.map(total => ({
|
||||
label: getHumanMonth(total.period).split(' ')[0],
|
||||
price: total.price / 100,
|
||||
calories: total.calories,
|
||||
}));
|
||||
}
|
||||
return stats.byYear.map(total => ({
|
||||
label: total.period,
|
||||
price: total.price / 100,
|
||||
calories: total.calories,
|
||||
}));
|
||||
}, [stats, mode]);
|
||||
const series = useMemo(() => stats ? pickPeriods(stats, mode).map(total => ({
|
||||
label: formatPeriodLabel(total.period, mode),
|
||||
price: total.price / 100,
|
||||
calories: total.calories,
|
||||
})) : [], [stats, mode]);
|
||||
|
||||
const pieData = useMemo(() => (stats?.byMealType ?? [])
|
||||
.filter(total => total.price > 0)
|
||||
@@ -99,14 +80,31 @@ export default function StatsPage() {
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1>Statistiky</h1>
|
||||
<p className="page-subtitle">Kolik jste utratili za jídlo a za co</p>
|
||||
<p className="page-subtitle">
|
||||
{tab === DayTab.PRIJEM
|
||||
? 'Kolik jste utratili za jídlo a za co'
|
||||
: 'Kolik jste spálili pohybem a jak se držíte šablon'}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setImportOpen(true)}>
|
||||
<FontAwesomeIcon icon={faFileImport} /> Import z Luncheru
|
||||
</Button>
|
||||
{tab === DayTab.PRIJEM && (
|
||||
<Button onClick={() => setImportOpen(true)}>
|
||||
<FontAwesomeIcon icon={faFileImport} /> Import z Luncheru
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="range-picker card-panel">
|
||||
<div className="range-modes">
|
||||
{Object.values(DayTab).map(value => (
|
||||
<button
|
||||
key={value}
|
||||
className={`range-mode ${tab === value ? 'active' : ''}`}
|
||||
onClick={() => setTab(value)}
|
||||
>
|
||||
<FontAwesomeIcon icon={TAB_ICONS[value]} /> {DAY_TAB_NAMES[value]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="range-modes">
|
||||
{Object.values(StatsRange).map(value => (
|
||||
<button
|
||||
@@ -118,121 +116,110 @@ export default function StatsPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{mode === StatsRange.MESIC && (
|
||||
<input
|
||||
type="month"
|
||||
className="form-control range-input"
|
||||
value={month}
|
||||
onChange={event => event.target.value && setMonth(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
{mode === StatsRange.ROK && (
|
||||
<input
|
||||
type="number"
|
||||
className="form-control range-input"
|
||||
value={year}
|
||||
min={EARLIEST_YEAR}
|
||||
max={new Date().getFullYear()}
|
||||
onChange={event => setYear(Number(event.target.value))}
|
||||
/>
|
||||
)}
|
||||
<PeriodPicker mode={mode} month={month} year={year} onMonthChange={setMonth} onYearChange={setYear} />
|
||||
</div>
|
||||
|
||||
<div className="summary-grid">
|
||||
<div className="summary-tile accent">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faCoins} /> Celkem utraceno</div>
|
||||
<div className="summary-value">{formatPrice(stats?.totalPrice ?? 0)}</div>
|
||||
</div>
|
||||
<div className="summary-tile">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faCalendarDay} /> Průměr na den</div>
|
||||
<div className="summary-value">{formatPrice(averagePerDay)}</div>
|
||||
<div className="summary-hint">z {stats?.dayCount ?? 0} vykázaných dní</div>
|
||||
</div>
|
||||
<div className="summary-tile">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faListCheck} /> Jídel</div>
|
||||
<div className="summary-value">{stats?.entryCount ?? 0}</div>
|
||||
</div>
|
||||
<div className="summary-tile">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faFire} /> Kalorie</div>
|
||||
<div className="summary-value">{formatCalories(stats?.totalCalories)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{tab === DayTab.VYDEJ && <ActivityStatsPanel range={range} mode={mode} />}
|
||||
|
||||
{loading && (
|
||||
<div className="empty-state">
|
||||
<Spinner animation="border" variant="success" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !stats?.entryCount && (
|
||||
<div className="card-panel empty-state">
|
||||
<div className="empty-icon"><FontAwesomeIcon icon={faChartColumn} /></div>
|
||||
<div className="empty-title">Za zvolené období nemáte žádná data</div>
|
||||
<p className="empty-description">
|
||||
Přidejte jídla v přehledu dne, nebo si sem naimportujte měsíční přehled z Luncheru.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !!stats?.entryCount && (
|
||||
{tab === DayTab.PRIJEM && (
|
||||
<>
|
||||
<div className="card-panel chart-panel">
|
||||
<h2 className="chart-title">Útrata v čase</h2>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<BarChart data={series} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--ft-border)" vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fontSize: 12 }} stroke="var(--ft-text-muted)" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="var(--ft-text-muted)" width={56} />
|
||||
<Tooltip
|
||||
formatter={value => [formatChartPrice(value), 'Útrata']}
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
/>
|
||||
<Bar dataKey="price" fill="var(--ft-primary)" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="chart-row">
|
||||
<div className="card-panel chart-panel">
|
||||
<h2 className="chart-title">Podle chodu</h2>
|
||||
<ResponsiveContainer width="100%" height={240}>
|
||||
<PieChart>
|
||||
<Pie data={pieData} dataKey="value" nameKey="name" innerRadius={50} outerRadius={85}>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell key={entry.name} fill={PIE_COLORS[index % PIE_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Legend />
|
||||
<Tooltip
|
||||
formatter={value => formatChartPrice(value)}
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="summary-grid">
|
||||
<div className="summary-tile accent">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faCoins} /> Celkem utraceno</div>
|
||||
<div className="summary-value">{formatPrice(stats?.totalPrice ?? 0)}</div>
|
||||
</div>
|
||||
|
||||
<div className="card-panel chart-panel">
|
||||
<h2 className="chart-title">Kde nejvíc utrácíte</h2>
|
||||
<table className="source-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zdroj</th>
|
||||
<th className="numeric">Jídel</th>
|
||||
<th className="numeric">Útrata</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.bySource.slice(0, 8).map(total => (
|
||||
<tr key={total.key}>
|
||||
<td>{total.key}</td>
|
||||
<td className="numeric">{total.count}</td>
|
||||
<td className="numeric">{formatPriceShort(total.price)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="summary-tile">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faCalendarDay} /> Průměr na den</div>
|
||||
<div className="summary-value">{formatPrice(averagePerDay)}</div>
|
||||
<div className="summary-hint">z {stats?.dayCount ?? 0} vykázaných dní</div>
|
||||
</div>
|
||||
<div className="summary-tile">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faListCheck} /> Jídel</div>
|
||||
<div className="summary-value">{stats?.entryCount ?? 0}</div>
|
||||
</div>
|
||||
<div className="summary-tile">
|
||||
<div className="summary-label"><FontAwesomeIcon icon={faFire} /> Kalorie</div>
|
||||
<div className="summary-value">{formatCalories(stats?.totalCalories)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="empty-state">
|
||||
<Spinner animation="border" variant="success" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !stats?.entryCount && (
|
||||
<div className="card-panel empty-state">
|
||||
<div className="empty-icon"><FontAwesomeIcon icon={faChartColumn} /></div>
|
||||
<div className="empty-title">Za zvolené období nemáte žádná data</div>
|
||||
<p className="empty-description">
|
||||
Přidejte jídla v přehledu dne, nebo si sem naimportujte měsíční přehled z Luncheru.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !!stats?.entryCount && (
|
||||
<>
|
||||
<div className="card-panel chart-panel">
|
||||
<h2 className="chart-title">Útrata v čase</h2>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<BarChart data={series} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--ft-border)" vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fontSize: 12 }} stroke="var(--ft-text-muted)" />
|
||||
<YAxis tick={{ fontSize: 12 }} stroke="var(--ft-text-muted)" width={56} />
|
||||
<Tooltip
|
||||
formatter={value => [formatChartPrice(value), 'Útrata']}
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
/>
|
||||
<Bar dataKey="price" fill="var(--ft-primary)" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="chart-row">
|
||||
<div className="card-panel chart-panel">
|
||||
<h2 className="chart-title">Podle chodu</h2>
|
||||
<ResponsiveContainer width="100%" height={240}>
|
||||
<PieChart>
|
||||
<Pie data={pieData} dataKey="value" nameKey="name" innerRadius={50} outerRadius={85}>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell key={entry.name} fill={PIE_COLORS[index % PIE_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Legend />
|
||||
<Tooltip
|
||||
formatter={value => formatChartPrice(value)}
|
||||
contentStyle={TOOLTIP_STYLE}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="card-panel chart-panel">
|
||||
<h2 className="chart-title">Kde nejvíc utrácíte</h2>
|
||||
<table className="source-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zdroj</th>
|
||||
<th className="numeric">Jídel</th>
|
||||
<th className="numeric">Útrata</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.bySource.slice(0, 8).map(total => (
|
||||
<tr key={total.key}>
|
||||
<td>{total.key}</td>
|
||||
<td className="numeric">{total.count}</td>
|
||||
<td className="numeric">{formatPriceShort(total.price)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user