feat(calendar-week-view): Wochen-Kalender für Time-Entries [tsc:fail]
This commit is contained in:
parent
d146a68174
commit
276ed8c798
@ -2,6 +2,9 @@
|
|||||||
"completed_features": [
|
"completed_features": [
|
||||||
"password-change"
|
"password-change"
|
||||||
],
|
],
|
||||||
"current_feature": "audit-log",
|
"current_feature": "calendar-week-view",
|
||||||
"started_at": "2026-05-23T05:30:16.203066"
|
"started_at": "2026-05-23T05:30:16.203066",
|
||||||
|
"attempted_features": [
|
||||||
|
"audit-log"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@ -678,3 +678,17 @@ src/routes/audit-log.ts(3,10): error TS2724: '"../db/schema"' has no exported me
|
|||||||
undefined
|
undefined
|
||||||
/home/dark/Developer/EmberClone/apps/api:
|
/home/dark/Developer/EmberClone/apps/api:
|
||||||
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
|
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
|
||||||
|
- `05:33:03` **INFO** Committed feature audit-log
|
||||||
|
- `05:33:03` **INFO** Pushed: rc=0
|
||||||
|
|
||||||
|
## Phase-3 Feature: calendar-week-view (2026-05-23 05:33:03)
|
||||||
|
|
||||||
|
- `05:33:03` **INFO** Description: Wochen-Kalender für Time-Entries
|
||||||
|
- `05:33:03` **INFO** Generating apps/web/src/pages/Calendar.tsx (Calendar-Page mit Week-View. 7-Spalten-Grid (Mon-Sun mit aktueller Woc…)
|
||||||
|
- `05:33:38` **INFO** wrote 4226 chars in 34.7s (attempt 1)
|
||||||
|
- `05:33:38` **INFO** Running tsc --noEmit on api…
|
||||||
|
- `05:33:40` **WARN** tsc errors:
|
||||||
|
src/routes/audit-log.ts(3,10): error TS2724: '"../db/schema"' has no exported member named 'auditLogs'. Did you mean 'auditLog'?
|
||||||
|
undefined
|
||||||
|
/home/dark/Developer/EmberClone/apps/api:
|
||||||
|
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command failed with exit code 2: tsc --noEmit -p tsconfig.json
|
||||||
|
|||||||
107
apps/web/src/pages/Calendar.tsx
Normal file
107
apps/web/src/pages/Calendar.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useState, useMemo } from "react"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { api } from "../lib/api"
|
||||||
|
import { format, startOfWeek, addDays, endOfWeek, eachDayOfInterval, isSameDay } from "date-fns"
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||||
|
|
||||||
|
export default function CalendarPage() {
|
||||||
|
const [currentWeekStart, setCurrentWeekStart] = useState(startOfWeek(new Date(), { weekStartsOn: 1 }))
|
||||||
|
|
||||||
|
const weekEnd = endOfWeek(currentWeekStart, { weekStartsOn: 1 })
|
||||||
|
const days = eachDayOfInterval({ start: currentWeekStart, end: weekEnd })
|
||||||
|
|
||||||
|
const { data: entries = [], isLoading } = useQuery({
|
||||||
|
queryKey: ["time-entries", currentWeekStart.toISOString()],
|
||||||
|
queryFn: () => api.listTimeEntries({
|
||||||
|
from: currentWeekStart.toISOString(),
|
||||||
|
to: weekEnd.toISOString()
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const navigateWeek = (direction: "prev" | "next") => {
|
||||||
|
const offset = direction === "prev" ? -7 : 7
|
||||||
|
setCurrentWeekStart(prev => addDays(prev, offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayData = useMemo(() => {
|
||||||
|
return days.map(day => {
|
||||||
|
const dayEntries = entries.filter(e => {
|
||||||
|
const start = new Date(e.startTime)
|
||||||
|
return isSameDay(start, day)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalHours = dayEntries.reduce((sum, e) => {
|
||||||
|
const start = new Date(e.startTime)
|
||||||
|
const end = e.endTime ? new Date(e.endTime) : new Date()
|
||||||
|
return sum + (end.getTime() - start.getTime()) / (1000 * 60 * 60)
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return { day, entries: dayEntries, totalHours }
|
||||||
|
})
|
||||||
|
}, [days, entries])
|
||||||
|
|
||||||
|
if (isLoading) return <div className="p-8 text-center">Loading calendar...</div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full p-4 gap-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h1 className="text-2xl font-bold">Calendar</h1>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{format(currentWeekStart, "MMM d")} - {format(weekEnd, "MMM d, yyyy")}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => navigateWeek("prev")}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full border transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => navigateWeek("next")}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full border transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-2 flex-1 min-h-0">
|
||||||
|
{dayData.map(({ day, entries, totalHours }) => (
|
||||||
|
<div key={day.toISOString()} className="flex flex-col bg-gray-50 rounded-lg border border-gray-200 overflow-hidden">
|
||||||
|
<div className="p-2 text-center border-b bg-white">
|
||||||
|
<div className="text-xs uppercase text-gray-500 font-semibold">
|
||||||
|
{format(day, "EEE")}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold">
|
||||||
|
{format(day, "d")}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs font-medium text-blue-600">
|
||||||
|
{totalHours.toFixed(2)}h
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
||||||
|
{entries.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-400 text-xs py-4 italic">No entries</div>
|
||||||
|
) : (
|
||||||
|
entries.map(entry => (
|
||||||
|
<div key={entry.id} className="p-2 bg-white border rounded text-xs shadow-sm hover:border-blue-300 transition-colors">
|
||||||
|
<div className="font-bold truncate" title={entry.description}>
|
||||||
|
{entry.description}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500 mt-1">
|
||||||
|
{format(new Date(entry.startTime), "HH:mm")} -
|
||||||
|
{entry.endTime ? format(new Date(entry.endTime), " HH:mm") : " ..."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user