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": [
|
||||
"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"
|
||||
}
|
||||
@ -495,3 +495,13 @@ undefined
|
||||
- `05:15:45` **INFO** wrote 2176 chars in 18.3s (attempt 1)
|
||||
- `05:15:45` **INFO** Running tsc --noEmit on api…
|
||||
- `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 { useNavigate } from "@tanstack/react-router"
|
||||
import { useMemo } from "react"
|
||||
import { api } from "../lib/api"
|
||||
import { Clock, Calendar, FolderKanban, LogOut } from "lucide-react"
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigate = useNavigate()
|
||||
@ -16,7 +18,19 @@ export default function Dashboard() {
|
||||
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 last7DaysDates = getLast7Days()
|
||||
const oldestDate = last7DaysDates[0]
|
||||
|
||||
const { data: user, isLoading: userLoading } = useQuery({
|
||||
queryKey: ["me"],
|
||||
@ -33,12 +47,35 @@ export default function Dashboard() {
|
||||
queryFn: () => api.listTimeEntries({ from: weekStart })
|
||||
})
|
||||
|
||||
const { data: chartEntries, isLoading: chartLoading } = useQuery({
|
||||
queryKey: ["timeEntries", "chart"],
|
||||
queryFn: () => api.listTimeEntries({ from: oldestDate })
|
||||
})
|
||||
|
||||
const handleLogout = () => {
|
||||
api.logout()
|
||||
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 (
|
||||
<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" />
|
||||
@ -54,8 +91,6 @@ export default function Dashboard() {
|
||||
const activeProjects = new Set((weekEntries || []).map(e => e.projectId)).size
|
||||
const avgDailyHours = (weekHours / 5).toFixed(1)
|
||||
|
||||
const lastFiveEntries = (weekEntries || []).slice(-5).reverse()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-6">
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Heute</p>
|
||||
@ -88,7 +122,6 @@ export default function Dashboard() {
|
||||
</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>
|
||||
<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>
|
||||
|
||||
{/* Card 3: Aktive Projekte */}
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200 flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500 uppercase tracking-wider">Aktive Projekte</p>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<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 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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h2 className="text-lg font-semibold text-gray-800">Letzte 5 Einträge</h2>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead className="bg-gray-50 text-xs uppercase text-gray-500 font-medium">
|
||||
<tr>
|
||||
<th className="px-6 py-3">Datum</th>
|
||||
<th className="px-6 py-3">Projekt</th>
|
||||
<th className="px-6 py-3">Beschreibung</th>
|
||||
<th className="px-6 py-3 text-right">Dauer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{lastFiveEntries.length > 0 ? (
|
||||
lastFiveEntries.map((entry) => (
|
||||
<tr key={entry.id} className="hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4 text-sm text-gray-600">{entry.date}</td>
|
||||
<td className="px-6 py-4 text-sm font-medium text-gray-800">{entry.projectName || "Unbekannt"}</td>
|
||||
<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>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="px-6 py-8 text-center text-gray-400 text-sm">
|
||||
Keine aktuellen Einträge gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-6">Stunden der letzten 7 Tage</h2>
|
||||
<div className="h-80 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#9ca3af', fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: '#9ca3af', fontSize: 12 }}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ fill: '#f9fafb' }}
|
||||
contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="hours"
|
||||
fill="#4f46e5"
|
||||
radius={[4, 4, 0, 0]}
|
||||
barSize={40}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user