feat(time-entry-templates-page): UI-Page für TimeEntry-Templates CRUD [tsc:fail]
This commit is contained in:
parent
f7cc7eb8fe
commit
e5877d05d6
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
183
apps/web/src/pages/TimeEntryTemplates.tsx
Normal file
183
apps/web/src/pages/TimeEntryTemplates.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user