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": [
|
||||
"password-change"
|
||||
],
|
||||
"current_feature": "audit-log",
|
||||
"started_at": "2026-05-23T05:30:16.203066"
|
||||
"current_feature": "calendar-week-view",
|
||||
"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
|
||||
/home/dark/Developer/EmberClone/apps/api:
|
||||
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