diff --git a/.phase20-state.json b/.phase20-state.json index 99e9b7d..78b52bc 100644 --- a/.phase20-state.json +++ b/.phase20-state.json @@ -1,5 +1,8 @@ { "completed_features": [], - "current_feature": "time-budget-per-project", - "started_at": "2026-05-23T07:57:43.412201" + "current_feature": "recurring-time-entries", + "started_at": "2026-05-23T07:57:43.412201", + "attempted_features": [ + "time-budget-per-project" + ] } \ No newline at end of file diff --git a/GENERATION_LOG.md b/GENERATION_LOG.md index 462bab7..615bfd9 100644 --- a/GENERATION_LOG.md +++ b/GENERATION_LOG.md @@ -2388,3 +2388,20 @@ 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 +- `08:00:25` **INFO** Committed feature time-budget-per-project +- `08:00:26` **INFO** Pushed: rc=0 + +## Phase-3 Feature: recurring-time-entries (2026-05-23 08:00:26) + +- `08:00:26` **INFO** Description: Template-System für wiederkehrende Entries (z.B. Daily-Standup 30min) +- `08:00:26` **INFO** Generating apps/web/src/pages/TimeEntries.tsx (ERWEITERT — füge 'Aus Template' Dropdown im Create-Form, lädt api.list…) +- `08:02:16` **INFO** wrote 14977 chars in 110.3s (attempt 1) +- `08:02:16` **INFO** Running tsc --noEmit on api… +- `08:02:18` **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/TimeEntries.tsx b/apps/web/src/pages/TimeEntries.tsx index 2a0462e..b2721b2 100644 --- a/apps/web/src/pages/TimeEntries.tsx +++ b/apps/web/src/pages/TimeEntries.tsx @@ -5,7 +5,7 @@ import { EmptyState } from "../components/EmptyState" import { LoadingSpinner } from "../components/LoadingSpinner" import { SuggestionInput } from "../components/SuggestionInput" import { SmartFilters } from "../components/SmartFilters" -import type { TimeEntryInsert, SavedView, TimeEntry } from "@emberclone/shared" +import type { TimeEntryInsert, SavedView, TimeEntry, TimeEntryTemplate } from "@emberclone/shared" function renderSimpleMarkdown(text: string | null) { if (!text) return null @@ -56,11 +56,32 @@ export default function TimeEntries() { queryFn: () => api.listSavedViews({ entityType: 'time-entries' }) }) + const { data: templates } = useQuery({ + queryKey: ["time-entry-templates"], + queryFn: () => api.listTimeEntryTemplates() + }) + const descriptionSuggestions = useMemo(() => { if (!entries) return [] return Array.from(new Set(entries.map(e => e.description).filter(Boolean))).slice(0, 50) as string[] }, [entries]) + const handleTemplateChange = (templateId: string) => { + const template = templates?.find(t => t.id === templateId) + if (!template) return + + const now = new Date() + const end = new Date(now.getTime() + (template.durationMinutes || 0) * 60000) + + setFormData({ + ...formData, + description: template.description || "", + projectId: template.projectId || "", + startTime: now.toISOString(), + endTime: end.toISOString() + }) + } + const createMutation = useMutation({ mutationFn: (data: Partial) => api.createTimeEntry(data), onSuccess: () => { @@ -119,40 +140,13 @@ export default function TimeEntries() { const filteredEntries = useMemo(() => { if (!entries) return [] - return entries.filter(entry => - entry.description?.toLowerCase().includes(filters.search.toLowerCase()) - ) - }, [entries, filters.search]) - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - createMutation.mutate({ - description: formData.description, - startTime: new Date(formData.startTime) as any, - endTime: new Date(formData.endTime) as any, - projectId: formData.projectId || undefined, - notes: formData.notes + return entries.filter(e => { + const matchesSearch = !filters.search || + e.description.toLowerCase().includes(filters.search.toLowerCase()) || + e.notes?.toLowerCase().includes(filters.search.toLowerCase()) + return matchesSearch }) - } - - const handleStartEditing = (entry: TimeEntry) => { - setEditingId(entry.id) - setEditValue(entry.description || "") - } - - const handleEditBlur = () => { - if (editingId) { - updateMutation.mutate({ id: editingId, data: { description: editValue } }) - } - } - - const handleEditKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - handleEditBlur() - } else if (e.key === 'Escape') { - setEditingId(null) - } - } + }, [entries, filters.search]) if (isLoading) return
if (isError) return
Error loading time entries.
@@ -178,168 +172,210 @@ export default function TimeEntries() { -
- setFormData({...formData, description: v})} - suggestions={descriptionSuggestions} - placeholder="Description" - /> - setFormData({...formData, startTime: e.target.value})} - required - /> - setFormData({...formData, endTime: e.target.value})} - required - /> - setFormData({...formData, projectId: e.target.value})} - /> - -
-