feat(dashboard-charts): Dashboard mit Stunden-Chart (recharts) [tsc:ok]
This commit is contained in:
parent
ec7476f23d
commit
bbf058acbe
@ -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"
|
||||||
}
|
}
|
||||||
@ -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 ✓
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user