Files
honeyDueWeb/src/components/tasks/task-form.tsx
T
Trey t 7884ebbfd4 feat: Phase 4-5 — demo mode, polish, deploy, and bug fixes
Add demo mode with mock data provider, Docker deployment, Playwright
tests, PostHog analytics, error boundaries, and SEO metadata. Fix
residences API response unwrapping, kanban drag-and-drop with optimistic
updates, trailing slash proxy redirects, and column name mismatches with
Go API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 11:37:41 -06:00

200 lines
6.3 KiB
TypeScript

"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { FormField } from "@/components/shared/form-field";
import { LookupSelect } from "@/components/shared/lookup-select";
import { CurrencyInput } from "@/components/shared/currency-input";
import { TemplateSearch } from "./template-search";
import { useResidences } from "@/lib/hooks/use-residences";
import { useContractors } from "@/lib/hooks/use-contractors";
import {
useTaskCategories,
useTaskPriorities,
useTaskFrequencies,
} from "@/lib/hooks/use-lookups";
import type { TaskResponse } from "@/lib/api/tasks";
import type { TaskTemplateResponse } from "@/lib/api/lookups";
const taskSchema = z.object({
title: z.string().min(1, "Title is required"),
residence_id: z.number({ error: "Residence is required" }),
description: z.string().optional(),
category_id: z.number().optional(),
priority_id: z.number().optional(),
frequency_id: z.number().optional(),
due_date: z.string().optional(),
estimated_cost: z.number().optional(),
contractor_id: z.number().optional(),
});
type TaskFormValues = z.infer<typeof taskSchema>;
interface TaskFormProps {
task?: TaskResponse;
onSubmit: (data: TaskFormValues) => void;
isSubmitting?: boolean;
}
export function TaskForm({ task, onSubmit, isSubmitting }: TaskFormProps) {
const isEdit = !!task;
const { data: residences } = useResidences();
const { data: contractors } = useContractors();
const { data: categories } = useTaskCategories();
const { data: priorities } = useTaskPriorities();
const { data: frequencies } = useTaskFrequencies();
const residenceItems = (Array.isArray(residences) ? residences : []).map((r) => ({
id: r.residence.id,
name: r.residence.name,
}));
const contractorItems = (Array.isArray(contractors) ? contractors : []).map((c) => ({
id: c.id,
name: c.company ? `${c.name} (${c.company})` : c.name,
}));
const categoryItems = categories.map((c) => ({
id: c.id,
name: c.name,
icon: c.icon,
}));
const priorityItems = priorities.map((p) => ({
id: p.id,
name: p.name,
icon: p.icon,
}));
const frequencyItems = frequencies.map((f) => ({
id: f.id,
name: f.name,
}));
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<TaskFormValues>({
resolver: zodResolver(taskSchema),
defaultValues: {
title: task?.title ?? "",
residence_id: task?.residence_id,
description: task?.description ?? "",
category_id: task?.category_id,
priority_id: task?.priority_id,
frequency_id: task?.frequency_id,
due_date: task?.due_date ? task.due_date.split("T")[0] : undefined,
estimated_cost: task?.estimated_cost,
contractor_id: task?.contractor_id,
},
});
const handleTemplateSelect = (template: TaskTemplateResponse) => {
if (template.category_id) setValue("category_id", template.category_id);
if (template.priority_id) setValue("priority_id", template.priority_id);
if (template.frequency_id) setValue("frequency_id", template.frequency_id);
if (template.estimated_cost) setValue("estimated_cost", template.estimated_cost);
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<FormField
label="Title"
htmlFor="title"
error={errors.title?.message}
required
>
{isEdit ? (
<Input id="title" {...register("title")} />
) : (
<TemplateSearch
onTitleChange={(value) => setValue("title", value, { shouldValidate: true })}
onSelect={handleTemplateSelect}
/>
)}
</FormField>
<FormField
label="Residence"
htmlFor="residence_id"
error={errors.residence_id?.message}
required
>
<LookupSelect
items={residenceItems}
value={watch("residence_id")}
onValueChange={(v) => setValue("residence_id", v as number, { shouldValidate: true })}
placeholder="Select residence..."
/>
</FormField>
<FormField label="Description" htmlFor="description">
<Textarea id="description" rows={3} {...register("description")} />
</FormField>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField label="Category" htmlFor="category_id">
<LookupSelect
items={categoryItems}
value={watch("category_id")}
onValueChange={(v) => setValue("category_id", v)}
placeholder="Select category..."
/>
</FormField>
<FormField label="Priority" htmlFor="priority_id">
<LookupSelect
items={priorityItems}
value={watch("priority_id")}
onValueChange={(v) => setValue("priority_id", v)}
placeholder="Select priority..."
/>
</FormField>
<FormField label="Frequency" htmlFor="frequency_id">
<LookupSelect
items={frequencyItems}
value={watch("frequency_id")}
onValueChange={(v) => setValue("frequency_id", v)}
placeholder="Select frequency..."
/>
</FormField>
<FormField label="Due Date" htmlFor="due_date">
<Input id="due_date" type="date" {...register("due_date")} />
</FormField>
<FormField label="Estimated Cost" htmlFor="estimated_cost">
<CurrencyInput
value={watch("estimated_cost")}
onChange={(v) => setValue("estimated_cost", v)}
/>
</FormField>
<FormField label="Contractor" htmlFor="contractor_id">
<LookupSelect
items={contractorItems}
value={watch("contractor_id")}
onValueChange={(v) => setValue("contractor_id", v)}
placeholder="Select contractor..."
/>
</FormField>
</div>
<div className="flex justify-end gap-2">
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Saving..." : isEdit ? "Update Task" : "Create Task"}
</Button>
</div>
</form>
);
}