Kyle Pope b04854a488 Default date/time fields to today/now on create forms
Todo, reminder, project, and task forms now pre-fill date/time
fields with today's date and current time when creating new items.
Edit mode still uses stored values. DOB fields excluded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:08:55 +08:00

217 lines
7.4 KiB
TypeScript

import { useState, FormEvent } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import api, { getErrorMessage } from '@/lib/api';
import type { ProjectTask, Person } from '@/types';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetFooter,
SheetClose,
} from '@/components/ui/sheet';
import { Input } from '@/components/ui/input';
import { DatePicker } from '@/components/ui/date-picker';
import { Textarea } from '@/components/ui/textarea';
import { Select } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
function todayLocal(): string {
const d = new Date();
const pad = (n: number) => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
interface TaskFormProps {
projectId: number;
task: ProjectTask | null;
parentTaskId?: number | null;
defaultDueDate?: string;
onClose: () => void;
}
export default function TaskForm({ projectId, task, parentTaskId, defaultDueDate, onClose }: TaskFormProps) {
const queryClient = useQueryClient();
const [formData, setFormData] = useState({
title: task?.title || '',
description: task?.description || '',
status: task?.status || 'pending',
priority: task?.priority || 'medium',
due_date: task?.due_date ? task.due_date.slice(0, 10) : (!task && defaultDueDate ? defaultDueDate.slice(0, 10) : todayLocal()),
person_id: task?.person_id?.toString() || '',
});
const { data: people = [] } = useQuery({
queryKey: ['people'],
queryFn: async () => {
const { data } = await api.get<Person[]>('/people');
return data;
},
});
const mutation = useMutation({
mutationFn: async (data: typeof formData) => {
const payload: Record<string, unknown> = {
...data,
person_id: data.person_id ? parseInt(data.person_id) : null,
};
if (task) {
const response = await api.put(`/projects/${projectId}/tasks/${task.id}`, payload);
return response.data;
} else {
if (parentTaskId) {
payload.parent_task_id = parentTaskId;
}
const response = await api.post(`/projects/${projectId}/tasks`, payload);
return response.data;
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] });
toast.success(task ? 'Task updated' : 'Task created');
onClose();
},
onError: (error) => {
toast.error(getErrorMessage(error, task ? 'Failed to update task' : 'Failed to create task'));
},
});
const deleteMutation = useMutation({
mutationFn: async () => {
await api.delete(`/projects/${projectId}/tasks/${task!.id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] });
toast.success('Task deleted');
onClose();
},
onError: () => {
toast.error('Failed to delete task');
},
});
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
mutation.mutate(formData);
};
return (
<Sheet open={true} onOpenChange={onClose}>
<SheetContent>
<SheetClose onClick={onClose} />
<SheetHeader>
<SheetTitle>{task ? 'Edit Task' : parentTaskId ? 'New Subtask' : 'New Task'}</SheetTitle>
</SheetHeader>
<form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-hidden">
<div className="flex-1 overflow-y-auto px-6 py-5 space-y-4">
<div className="space-y-2">
<Label htmlFor="title" required>Title</Label>
<Input
id="title"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="min-h-[80px] flex-1"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select
id="status"
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value as ProjectTask['status'] })}
>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="blocked">Blocked</option>
<option value="review">Review</option>
<option value="on_hold">On Hold</option>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="priority">Priority</Label>
<Select
id="priority"
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: e.target.value as ProjectTask['priority'] })}
>
<option value="none">None</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</Select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="due_date">Due Date</Label>
<DatePicker
variant="input"
id="due_date"
value={formData.due_date}
onChange={(v) => setFormData({ ...formData, due_date: v })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="person">Assign To</Label>
<Select
id="person"
value={formData.person_id}
onChange={(e) => setFormData({ ...formData, person_id: e.target.value })}
>
<option value="">Unassigned</option>
{people.map((person) => (
<option key={person.id} value={person.id}>
{person.name}
</option>
))}
</Select>
</div>
</div>
</div>
<SheetFooter>
{task && (
<Button
type="button"
variant="destructive"
className="mr-auto"
onClick={() => {
if (!window.confirm('Delete this task?')) return;
deleteMutation.mutate();
}}
disabled={deleteMutation.isPending}
>
Delete
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Saving...' : task ? 'Update' : 'Create'}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}