feat(calendar-month-view): Monatsansicht für TimeEntries (Grid 6 weeks × 7 days) [tsc:fail]
This commit is contained in:
parent
a3452bda75
commit
fb6adcf85a
@ -8,6 +8,7 @@
|
|||||||
"recent-projects-quick-access",
|
"recent-projects-quick-access",
|
||||||
"time-entry-templates",
|
"time-entry-templates",
|
||||||
"dark-mode-improvements",
|
"dark-mode-improvements",
|
||||||
"api-client-phase16"
|
"api-client-phase16",
|
||||||
|
"router-phase16"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
5
.phase17-state.json
Normal file
5
.phase17-state.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"completed_features": [],
|
||||||
|
"current_feature": "calendar-month-view",
|
||||||
|
"started_at": "2026-05-23T07:18:43.778897"
|
||||||
|
}
|
||||||
@ -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.
|
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<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, 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<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||||
|
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, 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<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||||
|
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||||
|
|||||||
@ -1,107 +1,174 @@
|
|||||||
import React, { useState, useMemo } from "react"
|
import React, { useState, useMemo } from "react"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { api } from "../lib/api"
|
import { api } from "../lib/api"
|
||||||
import { format, startOfWeek, addDays, endOfWeek, eachDayOfInterval, isSameDay } from "date-fns"
|
import {
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
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() {
|
export default function CalendarPage() {
|
||||||
const [currentWeekStart, setCurrentWeekStart] = useState(startOfWeek(new Date(), { weekStartsOn: 1 }))
|
const [viewMode, setViewMode] = useState<ViewMode>("week")
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date())
|
||||||
|
|
||||||
const weekEnd = endOfWeek(currentWeekStart, { weekStartsOn: 1 })
|
// Week View Logic
|
||||||
const days = eachDayOfInterval({ start: currentWeekStart, end: weekEnd })
|
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({
|
const { data: entries = [], isLoading } = useQuery({
|
||||||
queryKey: ["time-entries", currentWeekStart.toISOString()],
|
queryKey: ["time-entries", viewMode, currentDate.toISOString()],
|
||||||
queryFn: () => api.listTimeEntries({
|
queryFn: () => {
|
||||||
from: currentWeekStart.toISOString(),
|
const from = viewMode === "week" ? weekStart.toISOString() : monthStart.toISOString()
|
||||||
to: weekEnd.toISOString()
|
const to = viewMode === "week" ? weekEnd.toISOString() : monthEnd.toISOString()
|
||||||
}),
|
return api.listTimeEntries({ from, to })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const navigateWeek = (direction: "prev" | "next") => {
|
const navigate = (direction: "prev" | "next") => {
|
||||||
const offset = direction === "prev" ? -7 : 7
|
if (viewMode === "week") {
|
||||||
setCurrentWeekStart(prev => addDays(prev, offset))
|
setCurrentDate(prev => addDays(prev, direction === "prev" ? -7 : 7))
|
||||||
|
} else {
|
||||||
|
setCurrentDate(prev => direction === "prev" ? subMonths(prev, 1) : addMonths(prev, 1))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayData = useMemo(() => {
|
const calculateDayStats = (day: Date) => {
|
||||||
return days.map(day => {
|
const dayEntries = entries.filter(e => isSameDay(new Date(e.startTime), day))
|
||||||
const dayEntries = entries.filter(e => {
|
const totalHours = dayEntries.reduce((sum, e) => {
|
||||||
const start = new Date(e.startTime)
|
const start = new Date(e.startTime)
|
||||||
return isSameDay(start, day)
|
const end = e.endTime ? new Date(e.endTime) : new Date()
|
||||||
})
|
return sum + (end.getTime() - start.getTime()) / (1000 * 60 * 60)
|
||||||
|
}, 0)
|
||||||
const totalHours = dayEntries.reduce((sum, e) => {
|
return { dayEntries, totalHours }
|
||||||
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>
|
if (isLoading) return <div className="p-8 text-center">Loading calendar...</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full p-4 gap-4">
|
<div className="flex flex-col h-full p-4 gap-4">
|
||||||
<div className="flex items-center justify-between mb-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="flex items-center gap-4">
|
||||||
<div className="text-sm font-medium">
|
<h1 className="text-2xl font-bold">Calendar</h1>
|
||||||
{format(currentWeekStart, "MMM d")} - {format(weekEnd, "MMM d, yyyy")}
|
<div className="flex bg-gray-100 p-1 rounded-lg border">
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateWeek("prev")}
|
onClick={() => setViewMode("week")}
|
||||||
className="p-2 hover:bg-gray-100 rounded-full border transition-colors"
|
className={`px-3 py-1 text-sm rounded-md transition-all ${viewMode === "week" ? "bg-white shadow-sm font-bold" : "text-gray-500"}`}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="w-5 h-5" />
|
Week
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigateWeek("next")}
|
onClick={() => setViewMode("month")}
|
||||||
className="p-2 hover:bg-gray-100 rounded-full border transition-colors"
|
className={`px-3 py-1 text-sm rounded-md transition-all ${viewMode === "month" ? "bg-white shadow-sm font-bold" : "text-gray-500"}`}
|
||||||
>
|
>
|
||||||
|
Month
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{viewMode === "week"
|
||||||
|
? `${format(weekStart, "MMM d")} - ${format(weekEnd, "MMM d, yyyy")}`
|
||||||
|
: format(currentDate, "MMMM yyyy")
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button onClick={() => navigate("prev")} className="p-2 hover:bg-gray-100 rounded-full border transition-colors">
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => navigate("next")} className="p-2 hover:bg-gray-100 rounded-full border transition-colors">
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronRight className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-2 flex-1 min-h-0">
|
{viewMode === "week" ? (
|
||||||
{dayData.map(({ day, entries, totalHours }) => (
|
<div className="grid grid-cols-7 gap-2 flex-1 min-h-0">
|
||||||
<div key={day.toISOString()} className="flex flex-col bg-gray-50 rounded-lg border border-gray-200 overflow-hidden">
|
{weekDays.map(day => {
|
||||||
<div className="p-2 text-center border-b bg-white">
|
const { dayEntries, totalHours } = calculateDayStats(day)
|
||||||
<div className="text-xs uppercase text-gray-500 font-semibold">
|
return (
|
||||||
{format(day, "EEE")}
|
<div key={day.toISOString()} className="flex flex-col bg-gray-50 rounded-lg border border-gray-200 overflow-hidden">
|
||||||
</div>
|
<div className="p-2 text-center border-b bg-white">
|
||||||
<div className="text-lg font-bold">
|
<div className="text-xs uppercase text-gray-500 font-semibold">{format(day, "EEE")}</div>
|
||||||
{format(day, "d")}
|
<div className="text-lg font-bold">{format(day, "d")}</div>
|
||||||
</div>
|
<div className="text-xs font-medium text-blue-600">{totalHours.toFixed(2)}h</div>
|
||||||
<div className="text-xs font-medium text-blue-600">
|
</div>
|
||||||
{totalHours.toFixed(2)}h
|
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
||||||
|
{dayEntries.length === 0 ? (
|
||||||
|
<div className="text-center text-gray-400 text-xs py-4 italic">No entries</div>
|
||||||
|
) : (
|
||||||
|
dayEntries.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>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden">
|
||||||
|
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => (
|
||||||
|
<div key={d} className="bg-gray-50 p-2 text-center text-xs font-bold text-gray-500 uppercase">
|
||||||
|
{d}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
{monthDays.map(day => {
|
||||||
{entries.length === 0 ? (
|
const { totalHours } = calculateDayStats(day)
|
||||||
<div className="text-center text-gray-400 text-xs py-4 italic">No entries</div>
|
const isCurrentMonth = day.getMonth() === currentDate.getMonth()
|
||||||
) : (
|
return (
|
||||||
entries.map(entry => (
|
<button
|
||||||
<div key={entry.id} className="p-2 bg-white border rounded text-xs shadow-sm hover:border-blue-300 transition-colors">
|
key={day.toISOString()}
|
||||||
<div className="font-bold truncate" title={entry.description}>
|
onClick={() => {
|
||||||
{entry.description}
|
setCurrentDate(day)
|
||||||
</div>
|
setViewMode("week")
|
||||||
<div className="text-gray-500 mt-1">
|
}}
|
||||||
{format(new Date(entry.startTime), "HH:mm")} -
|
className={`h-24 p-2 transition-colors text-left relative ${
|
||||||
{entry.endTime ? format(new Date(entry.endTime), " HH:mm") : " ..."}
|
isCurrentMonth ? "bg-white hover:bg-blue-50" : "bg-gray-50 text-gray-400 hover:bg-gray-100"
|
||||||
</div>
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`text-sm font-medium ${isCurrentMonth ? "text-gray-700" : "text-gray-400"}`}>
|
||||||
|
{format(day, "d")}
|
||||||
|
</span>
|
||||||
|
{totalHours > 0 && (
|
||||||
|
<div className="absolute bottom-2 right-2 px-1.5 py-0.5 bg-blue-100 text-blue-700 text-[10px] font-bold rounded">
|
||||||
|
{totalHours.toFixed(1)}h
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
)}
|
</button>
|
||||||
</div>
|
)
|
||||||
</div>
|
})}
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
166
scripts/phase17_features.py
Normal file
166
scripts/phase17_features.py
Normal file
@ -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 <SmartFilters onApply={setFilters} /> 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()))
|
||||||
Loading…
Reference in New Issue
Block a user