- #21: Add missing await in removeChoiceIfPresent() to prevent user appearing in two restaurants - #15: Add 1-hour TTL for menu refetching to avoid scraping on every page load - #9: Block stats API and UI navigation for future dates - #14: Add restaurant warnings (missing soup/prices, stale data) with warning icon - #12: Pre-fill restaurant/departure dropdowns from existing choices on page refresh - #10: Add voting statistics endpoint and table on stats page
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Footer from "../components/Footer";
|
||||
import Header from "../components/Header";
|
||||
import { useAuth } from "../context/auth";
|
||||
import Login from "../Login";
|
||||
import { formatDate, getFirstWorkDayOfWeek, getHumanDate, getLastWorkDayOfWeek } from "../Utils";
|
||||
import { WeeklyStats, LunchChoice, getStats } from "../../../types";
|
||||
import { WeeklyStats, LunchChoice, VotingStats, getStats, getVotingStats } from "../../../types";
|
||||
import Loader from "../components/Loader";
|
||||
import { faChevronLeft, faChevronRight, faGear } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Legend, Line, LineChart, Tooltip, XAxis, YAxis } from "recharts";
|
||||
@@ -32,6 +32,7 @@ export default function StatsPage() {
|
||||
const auth = useAuth();
|
||||
const [dateRange, setDateRange] = useState<Date[]>();
|
||||
const [data, setData] = useState<WeeklyStats>();
|
||||
const [votingStats, setVotingStats] = useState<VotingStats>();
|
||||
|
||||
// Prvotní nastavení aktuálního týdne
|
||||
useEffect(() => {
|
||||
@@ -48,6 +49,19 @@ export default function StatsPage() {
|
||||
}
|
||||
}, [dateRange]);
|
||||
|
||||
// Načtení statistik hlasování
|
||||
useEffect(() => {
|
||||
getVotingStats().then(response => {
|
||||
setVotingStats(response.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sortedVotingStats = useMemo(() => {
|
||||
if (!votingStats) return [];
|
||||
return Object.entries(votingStats)
|
||||
.sort((a, b) => (b[1] as number) - (a[1] as number));
|
||||
}, [votingStats]);
|
||||
|
||||
const renderLine = (location: LunchChoice) => {
|
||||
const index = Object.values(LunchChoice).indexOf(location);
|
||||
return <Line key={location} name={getLunchChoiceName(location)} type="monotone" dataKey={data => data.locations[location] ?? 0} stroke={COLORS[index]} strokeWidth={STROKE_WIDTH} />
|
||||
@@ -73,13 +87,20 @@ export default function StatsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const isCurrentOrFutureWeek = useMemo(() => {
|
||||
if (!dateRange) return true;
|
||||
const currentWeekEnd = getLastWorkDayOfWeek(new Date());
|
||||
currentWeekEnd.setHours(23, 59, 59, 999);
|
||||
return dateRange[1] >= currentWeekEnd;
|
||||
}, [dateRange]);
|
||||
|
||||
const handleKeyDown = useCallback((e: any) => {
|
||||
if (e.keyCode === 37) {
|
||||
handlePreviousWeek();
|
||||
} else if (e.keyCode === 39) {
|
||||
} else if (e.keyCode === 39 && !isCurrentOrFutureWeek) {
|
||||
handleNextWeek()
|
||||
}
|
||||
}, [dateRange]);
|
||||
}, [dateRange, isCurrentOrFutureWeek]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
@@ -111,7 +132,7 @@ export default function StatsPage() {
|
||||
</span>
|
||||
<h2 className="date-range">{getHumanDate(dateRange[0])} - {getHumanDate(dateRange[1])}</h2>
|
||||
<span title="Následující týden">
|
||||
<FontAwesomeIcon icon={faChevronRight} style={{ cursor: "pointer" }} onClick={handleNextWeek} />
|
||||
<FontAwesomeIcon icon={faChevronRight} style={{ cursor: "pointer", visibility: isCurrentOrFutureWeek ? "hidden" : "visible" }} onClick={handleNextWeek} />
|
||||
</span>
|
||||
</div>
|
||||
<LineChart width={CHART_WIDTH} height={CHART_HEIGHT} data={data}>
|
||||
@@ -121,6 +142,27 @@ export default function StatsPage() {
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
</LineChart>
|
||||
{sortedVotingStats.length > 0 && (
|
||||
<div className="voting-stats-section">
|
||||
<h2>Hlasování o funkcích</h2>
|
||||
<table className="voting-stats-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Funkce</th>
|
||||
<th>Počet hlasů</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sortedVotingStats.map(([feature, count]) => (
|
||||
<tr key={feature}>
|
||||
<td>{feature}</td>
|
||||
<td>{count as number}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user