From fb6adcf85ac82f5df389264b6944ab638ac88e15 Mon Sep 17 00:00:00 2001 From: "Dennis (via Claude+Gemma)" Date: Sat, 23 May 2026 07:19:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(calendar-month-view):=20Monatsansicht=20f?= =?UTF-8?q?=C3=BCr=20TimeEntries=20(Grid=206=20weeks=20=C3=97=207=20days)?= =?UTF-8?q?=20[tsc:fail]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .phase16-state.json | 3 +- .phase17-state.json | 5 + GENERATION_LOG.md | 24 ++++ apps/web/src/pages/Calendar.tsx | 209 +++++++++++++++++++++----------- scripts/phase17_features.py | 166 +++++++++++++++++++++++++ 5 files changed, 335 insertions(+), 72 deletions(-) create mode 100644 .phase17-state.json create mode 100644 scripts/phase17_features.py diff --git a/.phase16-state.json b/.phase16-state.json index ecd72f9..8774143 100644 --- a/.phase16-state.json +++ b/.phase16-state.json @@ -8,6 +8,7 @@ "recent-projects-quick-access", "time-entry-templates", "dark-mode-improvements", - "api-client-phase16" + "api-client-phase16", + "router-phase16" ] } \ No newline at end of file diff --git a/.phase17-state.json b/.phase17-state.json new file mode 100644 index 0000000..7233770 --- /dev/null +++ b/.phase17-state.json @@ -0,0 +1,5 @@ +{ + "completed_features": [], + "current_feature": "calendar-month-view", + "started_at": "2026-05-23T07:18:43.778897" +} \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index ad02d00..d1586f6 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2009,3 +2009,27 @@ src/index.ts(27,25): error TS2769: No overload matches this call. Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy +- `07:17:23` **INFO** Committed feature router-phase16 +- `07:17:23` **INFO** Pushed: rc=0 + +## Phase-16 Run beendet (2026-05-23 07:17:23) + +- `07:17:23` **INFO** OK: 0, Attempted: 7, Total: 7 + +## 🚀 Phase-17 Codegen-Run gestartet (2026-05-23 07:18:43) + + +## Phase-3 Feature: calendar-month-view (2026-05-23 07:18:43) + +- `07:18:43` **INFO** Description: Monatsansicht für TimeEntries (Grid 6 weeks × 7 days) +- `07:18:43` **INFO** Generating apps/web/src/pages/Calendar.tsx (ERWEITERT — behalte Week-View. Füge View-Toggle (Week / Month). Month-…) +- `07:19:47` **INFO** wrote 7506 chars in 63.7s (attempt 1) +- `07:19:47` **INFO** Running tsc --noEmit on api… +- `07:19:49` **WARN** tsc errors: +src/index.ts(27,25): error TS2769: No overload matches this call. + Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'. + Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error. + Argument of type 'Promise' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'. + Type 'Promise' provides no match for the signature '(instance: FastifyInstance, FastifyBaseLogger, FastifyTy diff --git a/apps/web/src/pages/Calendar.tsx b/apps/web/src/pages/Calendar.tsx index 024064b..702b1d0 100644 --- a/apps/web/src/pages/Calendar.tsx +++ b/apps/web/src/pages/Calendar.tsx @@ -1,107 +1,174 @@ 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" +import { + format, + startOfWeek, + addDays, + endOfWeek, + eachDayOfInterval, + isSameDay, + startOfMonth, + endOfMonth, + startOfmonth, + addMonths, + subMonths, + getDaysInMonth, + startOfDay +} from "date-fns" +import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, List } from "lucide-react" + +type ViewMode = "week" | "month" export default function CalendarPage() { - const [currentWeekStart, setCurrentWeekStart] = useState(startOfWeek(new Date(), { weekStartsOn: 1 })) + const [viewMode, setViewMode] = useState("week") + const [currentDate, setCurrentDate] = useState(new Date()) - const weekEnd = endOfWeek(currentWeekStart, { weekStartsOn: 1 }) - const days = eachDayOfInterval({ start: currentWeekStart, end: weekEnd }) + // Week View Logic + const weekStart = useMemo(() => startOfWeek(currentDate, { weekStartsOn: 1 }), [currentDate]) + const weekEnd = useMemo(() => endOfWeek(weekStart, { weekStartsOn: 1 }), [weekStart]) + const weekDays = useMemo(() => eachDayOfInterval({ start: weekStart, end: weekEnd }), [weekStart, weekEnd]) + + // Month View Logic + const monthStart = useMemo(() => startOfMonth(currentDate), [currentDate]) + const monthEnd = useMemo(() => endOfMonth(currentDate), [currentDate]) + + // To make a perfect grid, we find the start of the first week of the month + const monthGridStart = useMemo(() => startOfWeek(monthStart, { weekStartsOn: 1 }), [monthStart]) + const monthGridEnd = useMemo(() => addDays(endOfWeek(monthEnd, { weekStartsOn: 1 }), 0), [monthEnd]) + const monthDays = useMemo(() => eachDayOfInterval({ start: monthGridStart, end: monthGridEnd }), [monthGridStart, monthGridEnd]) const { data: entries = [], isLoading } = useQuery({ - queryKey: ["time-entries", currentWeekStart.toISOString()], - queryFn: () => api.listTimeEntries({ - from: currentWeekStart.toISOString(), - to: weekEnd.toISOString() - }), + queryKey: ["time-entries", viewMode, currentDate.toISOString()], + queryFn: () => { + const from = viewMode === "week" ? weekStart.toISOString() : monthStart.toISOString() + const to = viewMode === "week" ? weekEnd.toISOString() : monthEnd.toISOString() + return api.listTimeEntries({ from, to }) + }, }) - const navigateWeek = (direction: "prev" | "next") => { - const offset = direction === "prev" ? -7 : 7 - setCurrentWeekStart(prev => addDays(prev, offset)) + const navigate = (direction: "prev" | "next") => { + if (viewMode === "week") { + setCurrentDate(prev => addDays(prev, direction === "prev" ? -7 : 7)) + } else { + setCurrentDate(prev => direction === "prev" ? subMonths(prev, 1) : addMonths(prev, 1)) + } } - 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]) + const calculateDayStats = (day: Date) => { + const dayEntries = entries.filter(e => isSameDay(new Date(e.startTime), 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 { dayEntries, totalHours } + } if (isLoading) return
Loading calendar...
return (
-

Calendar

-
- {format(currentWeekStart, "MMM d")} - {format(weekEnd, "MMM d, yyyy")} -
-
+

Calendar

+
+
+
+ +
+
+ {viewMode === "week" + ? `${format(weekStart, "MMM d")} - ${format(weekEnd, "MMM d, yyyy")}` + : format(currentDate, "MMMM yyyy") + } +
+
+ +
-
- {dayData.map(({ day, entries, totalHours }) => ( -
-
-
- {format(day, "EEE")} -
-
- {format(day, "d")} -
-
- {totalHours.toFixed(2)}h + {viewMode === "week" ? ( +
+ {weekDays.map(day => { + const { dayEntries, totalHours } = calculateDayStats(day) + return ( +
+
+
{format(day, "EEE")}
+
{format(day, "d")}
+
{totalHours.toFixed(2)}h
+
+
+ {dayEntries.length === 0 ? ( +
No entries
+ ) : ( + dayEntries.map(entry => ( +
+
{entry.description}
+
+ {format(new Date(entry.startTime), "HH:mm")} - {entry.endTime ? format(new Date(entry.endTime), "HH:mm") : "..."} +
+
+ )) + )} +
+ ) + })} +
+ ) : ( +
+ {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => ( +
+ {d}
- -
- {entries.length === 0 ? ( -
No entries
- ) : ( - entries.map(entry => ( -
-
- {entry.description} -
-
- {format(new Date(entry.startTime), "HH:mm")} - - {entry.endTime ? format(new Date(entry.endTime), " HH:mm") : " ..."} -
+ ))} + {monthDays.map(day => { + const { totalHours } = calculateDayStats(day) + const isCurrentMonth = day.getMonth() === currentDate.getMonth() + return ( +
-
- ))} -
+ )} + + ) + })} +
+ )}
) } \ No newline at end of file diff --git a/scripts/phase17_features.py b/scripts/phase17_features.py new file mode 100644 index 0000000..ab6182f --- /dev/null +++ b/scripts/phase17_features.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Phase-17: drag-drop-reorder, calendar-month, batch-rename, customer-merge, smart-filter-suggestions.""" + +from __future__ import annotations + +import asyncio +import datetime +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from phase2_features import Feature, FileGen, ROOT, log, log_section # noqa: E402 +from phase3_features import run_feature_v2 # noqa: E402 + +PHASE17_STATE = ROOT / ".phase17-state.json" + +FEATURES: list[Feature] = [ + Feature( + name="calendar-month-view", + description="Monatsansicht für TimeEntries (Grid 6 weeks × 7 days)", + files=[ + FileGen( + path="apps/web/src/pages/Calendar.tsx", + purpose=( + "ERWEITERT — behalte Week-View. Füge View-Toggle (Week / Month). " + "Month-View: Grid 7 Spalten, 6 Reihen, jeder Cell zeigt Tagesnummer + total-hours Badge. " + "Vor/zurück-Buttons für Monat-Navigation. Klick auf Day-Cell wechselt zu Week-View dieser Woche." + ), + refs=["apps/web/src/pages/Calendar.tsx"], + ), + ], + ), + Feature( + name="batch-rename-projects", + description="Bulk-Select + rename Projects via Mutation", + files=[ + FileGen( + path="apps/web/src/pages/Projects.tsx", + purpose=( + "ERWEITERT — füge Checkbox-Spalte. Wenn min 1 selektiert, Action-Bar oben: " + "'Rename mit Prefix...' Input → bei Submit ruft api.bulkRenameProjects(ids, prefix). " + "Plus Bulk-Delete-Button." + ), + refs=["apps/web/src/pages/Projects.tsx"], + ), + FileGen( + path="apps/api/src/routes/projects.ts", + purpose=( + "ERWEITERT — füge POST /bulk-rename (body: {ids, prefix}): updated jedes Project's name = prefix + ' ' + currentName. " + "Plus POST /bulk-delete (ids[]) → delete all. Behalte alles." + ), + refs=["apps/api/src/routes/projects.ts"], + ), + ], + ), + Feature( + name="customer-merge", + description="Merge zwei Customers: source-Projects auf target umhängen, dann source löschen", + files=[ + FileGen( + path="apps/api/src/routes/customers.ts", + purpose=( + "ERWEITERT — füge POST /:id/merge (body: {targetId}): " + "alle projects vom source-customer (id) bekommen targetId als customerId, dann delete source-customer. " + "Admin-only. Behalte alles." + ), + refs=["apps/api/src/routes/customers.ts"], + ), + FileGen( + path="apps/web/src/pages/Customers.tsx", + purpose=( + "ERWEITERT — Merge-Button pro Row (admin-only). Modal mit Target-Customer-Picker, Submit → " + "api.mergeCustomers(sourceId, targetId), refetch." + ), + refs=["apps/web/src/pages/Customers.tsx"], + ), + ], + ), + Feature( + name="smart-filter-suggestions", + description="Saved-Views-Vorschläge basierend auf häufig benutzten Filters", + files=[ + FileGen( + path="apps/web/src/components/SmartFilters.tsx", + purpose=( + "SmartFilters-Component. Zeigt 3-4 vorgeschlagene Filter-Buttons: " + "'Diese Woche', 'Letzter Monat', 'Heute', 'Nur ohne Projekt'. " + "Props: onApply(filterObject). Klick wendet Filter sofort an." + ), + ), + FileGen( + path="apps/web/src/pages/TimeEntries.tsx", + purpose=( + "ERWEITERT — behalte alles. Füge oberhalb Filter-Bar." + ), + refs=["apps/web/src/pages/TimeEntries.tsx"], + ), + ], + ), + Feature( + name="time-entry-quick-edit", + description="Inline-Edit für TimeEntry-Description (Klick auf Description = Input)", + files=[ + FileGen( + path="apps/web/src/pages/TimeEntries.tsx", + purpose=( + "ERWEITERT — in der Liste: Description-Zelle wird beim Klick zu Input (useState editingId). " + "On Blur oder Enter: api.updateTimeEntry(id, {description}). Cancel via Escape." + ), + refs=["apps/web/src/pages/TimeEntries.tsx"], + ), + ], + ), + Feature( + name="api-client-phase17", + description="API um bulk-rename, customer-merge erweitert", + files=[ + FileGen( + path="apps/web/src/lib/api.ts", + purpose=( + "ERWEITERT — behalte ALLES. Füge: bulkRenameProjects(ids, prefix), bulkDeleteProjects(ids), " + "mergeCustomers(sourceId, targetId)." + ), + refs=["apps/web/src/lib/api.ts"], + ), + ], + ), +] + + +def load_state() -> dict: + if PHASE17_STATE.exists(): + return json.loads(PHASE17_STATE.read_text()) + return {"completed_features": [], "current_feature": None, "started_at": datetime.datetime.now().isoformat()} + + +def save_state(state: dict) -> None: + PHASE17_STATE.write_text(json.dumps(state, indent=2)) + + +async def main() -> int: + log_section("🚀 Phase-17 Codegen-Run gestartet") + state = load_state() + for feature in FEATURES: + if feature.name in state.get("completed_features", []): + continue + state["current_feature"] = feature.name; save_state(state) + try: + success = await run_feature_v2(feature) + if success: + state.setdefault("completed_features", []).append(feature.name) + else: + state.setdefault("attempted_features", []).append(feature.name) + save_state(state) + except Exception as e: + log(f"❌ {feature.name} crashed: {e}", level="ERROR") + state.setdefault("attempted_features", []).append(feature.name); save_state(state) + + log_section("Phase-17 Run beendet") + log(f"OK: {len(state.get('completed_features', []))}, Attempted: {len(state.get('attempted_features', []))}, Total: {len(FEATURES)}") + return 0 + + +if __name__ == "__main__": + sys.exit(asyncio.run(main()))