EmberClone/apps/web/src/pages/TimeEntryTemplates.tsx

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>
)
}