feat(time-entry-templates-page): UI-Page für TimeEntry-Templates CRUD [tsc:fail]

This commit is contained in:
Dennis (via Claude+Gemma) 2026-05-23 08:14:55 +02:00
parent f7cc7eb8fe
commit e5877d05d6
3 changed files with 203 additions and 2 deletions

View File

@ -1,10 +1,11 @@
{ {
"completed_features": [], "completed_features": [],
"current_feature": "color-coded-customer-rows", "current_feature": "time-entry-templates-page",
"started_at": "2026-05-23T08:09:40.135892", "started_at": "2026-05-23T08:09:40.135892",
"attempted_features": [ "attempted_features": [
"keyboard-undo-stack", "keyboard-undo-stack",
"snippet-shortcuts", "snippet-shortcuts",
"smart-rounding-on-input" "smart-rounding-on-input",
"color-coded-customer-rows"
] ]
} }

View File

@ -2550,3 +2550,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. 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
- `08:13:58` **INFO** Committed feature color-coded-customer-rows
- `08:13:58` **INFO** Pushed: rc=0
## Phase-3 Feature: time-entry-templates-page (2026-05-23 08:13:58)
- `08:13:58` **INFO** Description: UI-Page für TimeEntry-Templates CRUD
- `08:13:58` **INFO** Generating apps/web/src/pages/TimeEntryTemplates.tsx (TimeEntryTemplates-Page. Liste + Create-Form (name, description, proje…)
- `08:14:53` **INFO** wrote 6864 chars in 55.1s (attempt 1)
- `08:14:53` **INFO** Running tsc --noEmit on api…
- `08:14:55` **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

View File

@ -0,0 +1,183 @@
import { useState, useMemo } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Plus, Trash2, Clock, FileText, Tag } from "lucide-react"
import { api } from "../lib/api"
export default function TimeEntryTemplates() {
const queryClient = useQueryClient()
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [projectId, setProjectId] = useState("")
const [duration, setDuration] = useState("")
const { data: templates, isLoading, isError } = useQuery({
queryKey: ["timeEntryTemplates"],
queryFn: () => api.listTimeEntryTemplates()
})
const { data: projects } = useQuery({
queryKey: ["projects"],
queryFn: () => api.listProjects()
})
const createMutation = useMutation({
mutationFn: (payload: {
name: string;
description: string;
projectId: string | null;
defaultDurationMinutes: number | null
}) => api.createTimeEntryTemplate(payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["timeEntryTemplates"] })
setName("")
setDescription("")
setProjectId("")
setDuration("")
}
})
const deleteMutation = useMutation({
mutationFn: (id: string) => api.deleteTimeEntryTemplate(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["timeEntryTemplates"] })
}
})
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (!name.trim()) return
createMutation.mutate({
name,
description,
projectId: projectId || null,
defaultDurationMinutes: duration ? parseInt(duration, 10) : null
})
}
if (isLoading) return <div className="p-6 text-gray-500">Loading templates...</div>
if (isError) return <div className="p-6 text-red-500">Error loading templates.</div>
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8">
<h1 className="text-2xl font-bold flex items-center gap-2">
<Clock className="w-6 h-6" />
Time Entry Templates
</h1>
</div>
<form onSubmit={handleSubmit} className="bg-white border rounded-lg p-6 mb-8 shadow-sm grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2 flex items-center gap-2 mb-2 text-gray-600 font-medium">
<Plus className="w-4 h-4" /> Create New Template
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-semibold text-gray-500 uppercase">Template Name *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Weekly Sync"
className="border rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-semibold text-gray-500 uppercase">Default Duration (Min)</label>
<input
type="number"
value={duration}
onChange={(e) => setDuration(e.target.value)}
placeholder="60"
className="border rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div className="flex flex-col gap-1 md:col-span-1">
<label className="text-xs font-semibold text-gray-500 uppercase">Project (Optional)</label>
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="border rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none bg-white"
>
<option value="">No Project</option>
{projects?.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
<div className="flex flex-col gap-1 md:col-span-1">
<label className="text-xs font-semibold text-gray-500 uppercase">Description</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description..."
className="border rounded px-3 py-2 focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
<div className="md:col-span-2 flex justify-end mt-2">
<button
type="submit"
disabled={createMutation.isPending}
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center gap-2"
>
{createMutation.isPending ? "Saving..." : "Save Template"}
</button>
</div>
</form>
<div className="grid gap-3">
{templates?.length === 0 && (
<div className="text-center py-12 text-gray-400 border-2 border-dashed rounded-lg">
No templates found. Create one above to speed up your time tracking.
</div>
)}
{templates?.map(template => (
<div key={template.id} className="bg-white border rounded-lg p-4 flex items-center justify-between hover:border-blue-200 transition-colors shadow-sm">
<div className="flex items-start gap-4">
<div className="p-2 bg-blue-50 text-blue-600 rounded">
<FileText className="w-5 h-5" />
</div>
<div>
<div className="font-semibold text-gray-800">{template.name}</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-gray-500 mt-1">
{template.defaultDurationMinutes && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" /> {template.defaultDurationMinutes} min
</span>
)}
{template.projectId && (
<span className="flex items-center gap-1">
<Tag className="w-3 h-3" /> Project ID: {template.projectId}
</span>
)}
{template.description && (
<span className="italic">{template.description}</span>
)}
</div>
</div>
</div>
<button
onClick={() => {
if (confirm("Delete this template?")) {
deleteMutation.mutate(template.id)
}
}}
className="p-2 text-gray-400 hover:text-red-500 transition-colors"
title="Delete Template"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
))}
</div>
</div>
)
}