183 lines
6.7 KiB
TypeScript
183 lines
6.7 KiB
TypeScript
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>
|
|
)
|
|
} |