feat(dashboard-charts): Dashboard mit Stunden-Chart (recharts) [tsc:ok]

This commit is contained in:
Dennis (via Claude+Gemma) 2026-05-23 05:16:50 +02:00
parent ec7476f23d
commit bbf058acbe
3 changed files with 82 additions and 43 deletions

View File

@ -1,8 +1,9 @@
{ {
"completed_features": [ "completed_features": [
"admin-user-management", "admin-user-management",
"csv-export-time-entries" "csv-export-time-entries",
"error-boundary"
], ],
"current_feature": "error-boundary", "current_feature": "dashboard-charts",
"started_at": "2026-05-23T05:10:51.482879" "started_at": "2026-05-23T05:10:51.482879"
} }

View File

@ -495,3 +495,13 @@ undefined
- `05:15:45` **INFO** wrote 2176 chars in 18.3s (attempt 1) - `05:15:45` **INFO** wrote 2176 chars in 18.3s (attempt 1)
- `05:15:45` **INFO** Running tsc --noEmit on api… - `05:15:45` **INFO** Running tsc --noEmit on api…
- `05:15:46` **INFO** tsc clean ✓ - `05:15:46` **INFO** tsc clean ✓
- `05:15:46` **INFO** Committed feature error-boundary
- `05:15:47` **INFO** Pushed: rc=0
## Phase-3 Feature: dashboard-charts (2026-05-23 05:15:47)
- `05:15:47` **INFO** Description: Dashboard mit Stunden-Chart (recharts)
- `05:15:47` **INFO** Generating apps/web/src/pages/Dashboard.tsx (ÜBERARBEITETER Dashboard. Behalte die 3 Karten (Heute/Woche/Aktive Pro…)
- `05:16:48` **INFO** wrote 7018 chars in 61.7s (attempt 1)
- `05:16:48` **INFO** Running tsc --noEmit on api…
- `05:16:50` **INFO** tsc clean ✓

View File

@ -1,7 +1,9 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { useNavigate } from "@tanstack/react-router" import { useNavigate } from "@tanstack/react-router"
import { useMemo } from "react"
import { api } from "../lib/api" import { api } from "../lib/api"
import { Clock, Calendar, FolderKanban, LogOut } from "lucide-react" import { Clock, Calendar, FolderKanban, LogOut } from "lucide-react"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
export default function Dashboard() { export default function Dashboard() {
const navigate = useNavigate() const navigate = useNavigate()
@ -16,7 +18,19 @@ export default function Dashboard() {
return start.toISOString().split("T")[0] return start.toISOString().split("T")[0]
} }
const getLast7Days = () => {
const dates = []
for (let i = 6; i >= 0; i--) {
const d = new Date()
d.setDate(d.getDate() - i)
dates.push(d.toISOString().split("T")[0])
}
return dates
}
const weekStart = getStartOfWeek() const weekStart = getStartOfWeek()
const last7DaysDates = getLast7Days()
const oldestDate = last7DaysDates[0]
const { data: user, isLoading: userLoading } = useQuery({ const { data: user, isLoading: userLoading } = useQuery({
queryKey: ["me"], queryKey: ["me"],
@ -33,12 +47,35 @@ export default function Dashboard() {
queryFn: () => api.listTimeEntries({ from: weekStart }) queryFn: () => api.listTimeEntries({ from: weekStart })
}) })
const { data: chartEntries, isLoading: chartLoading } = useQuery({
queryKey: ["timeEntries", "chart"],
queryFn: () => api.listTimeEntries({ from: oldestDate })
})
const handleLogout = () => { const handleLogout = () => {
api.logout() api.logout()
navigate({ to: "/login" }) navigate({ to: "/login" })
} }
if (userLoading || todayLoading || weekLoading) { const chartData = useMemo(() => {
if (!chartEntries) return []
return last7DaysDates.map(date => {
const dayEntries = chartEntries.filter(e => e.date === date)
const totalHours = dayEntries.reduce((acc, curr) => acc + (parseFloat(curr.duration) || 0), 0)
const d = new Date(date)
const dayName = d.toLocaleDateString('de-DE', { weekday: 'short' })
return {
name: dayName,
hours: parseFloat(totalHours.toFixed(2)),
date
}
})
}, [chartEntries])
if (userLoading || todayLoading || weekLoading || chartLoading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600" /> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600" />
@ -54,8 +91,6 @@ export default function Dashboard() {
const activeProjects = new Set((weekEntries || []).map(e => e.projectId)).size const activeProjects = new Set((weekEntries || []).map(e => e.projectId)).size
const avgDailyHours = (weekHours / 5).toFixed(1) const avgDailyHours = (weekHours / 5).toFixed(1)
const lastFiveEntries = (weekEntries || []).slice(-5).reverse()
return ( return (
<div className="min-h-screen bg-gray-50 p-6"> <div className="min-h-screen bg-gray-50 p-6">
<header className="flex justify-between items-center mb-8 max-w-7xl mx-auto"> <header className="flex justify-between items-center mb-8 max-w-7xl mx-auto">
@ -74,7 +109,6 @@ export default function Dashboard() {
<main className="max-w-7xl mx-auto space-y-8"> <main className="max-w-7xl mx-auto space-y-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Card 1: Heute */}
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between"> <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Heute</p> <p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Heute</p>
@ -88,7 +122,6 @@ export default function Dashboard() {
</div> </div>
</div> </div>
{/* Card 2: Diese Woche */}
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between"> <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Diese Woche</p> <p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Diese Woche</p>
@ -102,54 +135,49 @@ export default function Dashboard() {
</div> </div>
</div> </div>
{/* Card 3: Aktive Projekte */}
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between"> <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Aktive Projekte</p> <p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Aktive Projekte</p>
<div className="mt-2 flex items-baseline gap-2"> <div className="mt-2 flex items-baseline gap-2">
<span className="text-3xl font-bold text-gray-900">{activeProjects}</span> <span className="text-3xl font-bold text-gray-900">{activeProjects}</span>
<span className="text-sm text-gray-400">diese Woche</span> <span className="text-sm text-gray-400">in dieser Woche</span>
</div> </div>
</div> </div>
<div className="p-3 bg-blue-50 text-blue-600 rounded-lg"> <div className="p-3 bg-amber-50 text-amber-600 rounded-lg">
<FolderKanban size={24} /> <FolderKanban size={24} />
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
<div className="px-6 py-4 border-b border-gray-100"> <h2 className="text-lg font-semibold text-gray-800 mb-6">Stunden der letzten 7 Tage</h2>
<h2 className="text-lg font-semibold text-gray-800">Letzte 5 Einträge</h2> <div className="h-80 w-full">
</div> <ResponsiveContainer width="100%" height="100%">
<div className="overflow-x-auto"> <BarChart data={chartData}>
<table className="w-full text-left"> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
<thead className="bg-gray-50 text-xs uppercase text-gray-500 font-medium"> <XAxis
<tr> dataKey="name"
<th className="px-6 py-3">Datum</th> axisLine={false}
<th className="px-6 py-3">Projekt</th> tickLine={false}
<th className="px-6 py-3">Beschreibung</th> tick={{ fill: '#9ca3af', fontSize: 12 }}
<th className="px-6 py-3 text-right">Dauer</th> />
</tr> <YAxis
</thead> axisLine={false}
<tbody className="divide-y divide-gray-100"> tickLine={false}
{lastFiveEntries.length > 0 ? ( tick={{ fill: '#9ca3af', fontSize: 12 }}
lastFiveEntries.map((entry) => ( />
<tr key={entry.id} className="hover:bg-gray-50 transition-colors"> <Tooltip
<td className="px-6 py-4 text-sm text-gray-600">{entry.date}</td> cursor={{ fill: '#f9fafb' }}
<td className="px-6 py-4 text-sm font-medium text-gray-800">{entry.projectName || "Unbekannt"}</td> contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
<td className="px-6 py-4 text-sm text-gray-500">{entry.description}</td> />
<td className="px-6 py-4 text-sm text-right font-semibold text-gray-700">{entry.duration}h</td> <Bar
</tr> dataKey="hours"
)) fill="#4f46e5"
) : ( radius={[4, 4, 0, 0]}
<tr> barSize={40}
<td colSpan={4} className="px-6 py-8 text-center text-gray-400 text-sm"> />
Keine aktuellen Einträge gefunden. </BarChart>
</td> </ResponsiveContainer>
</tr>
)}
</tbody>
</table>
</div> </div>
</div> </div>
</main> </main>