"use client"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useCreateStory, useUpdateStory } from "@/lib/hooks/use-stories"; import { useEpics } from "@/lib/hooks/use-epics"; import type { Story } from "@/types/project"; import { toast } from "sonner"; import { Loader2 } from "lucide-react"; import { useAuthStore } from "@/stores/authStore"; import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor"; import { TagsInput } from "./tags-input"; const storySchema = z.object({ epicId: z.string().min(1, "Parent Epic is required"), title: z.string().min(1, "Title is required").max(200, "Title must be less than 200 characters"), description: z.string().max(2000, "Description must be less than 2000 characters").optional(), priority: z.enum(["Low", "Medium", "High", "Critical"]), estimatedHours: z .number() .min(0, "Estimated hours must be positive") .optional() .or(z.literal("")), // Sprint 4 Story 3: New fields acceptanceCriteria: z.array(z.string()).optional(), assigneeId: z.string().optional(), tags: z.array(z.string()).optional(), storyPoints: z .number() .min(0, "Story points must be positive") .max(100, "Story points must be less than 100") .optional() .or(z.literal("")), }); type StoryFormValues = z.infer; interface StoryFormProps { story?: Story; epicId?: string; projectId?: string; onSuccess?: () => void; onCancel?: () => void; } export function StoryForm({ story, epicId, projectId, onSuccess, onCancel }: StoryFormProps) { const isEditing = !!story; const user = useAuthStore((state) => state.user); const createStory = useCreateStory(); const updateStory = useUpdateStory(); // Fetch epics for parent epic selection const { data: epics = [], isLoading: epicsLoading } = useEpics(projectId); const form = useForm({ resolver: zodResolver(storySchema), defaultValues: { epicId: story?.epicId || epicId || "", title: story?.title || "", description: story?.description || "", priority: story?.priority || "Medium", estimatedHours: story?.estimatedHours || ("" as const), // Sprint 4 Story 3: New field defaults acceptanceCriteria: story?.acceptanceCriteria || [], assigneeId: story?.assigneeId || "", tags: story?.tags || [], storyPoints: story?.storyPoints || ("" as const), }, }); async function onSubmit(data: StoryFormValues) { try { if (isEditing && story) { await updateStory.mutateAsync({ id: story.id, data: { title: data.title, description: data.description, priority: data.priority, estimatedHours: typeof data.estimatedHours === "number" ? data.estimatedHours : undefined, // Sprint 4 Story 3: New fields acceptanceCriteria: data.acceptanceCriteria, assigneeId: data.assigneeId || undefined, tags: data.tags, storyPoints: typeof data.storyPoints === "number" ? data.storyPoints : undefined, }, }); toast.success("Story updated successfully"); } else { if (!user?.id) { toast.error("User not authenticated"); return; } if (!projectId) { toast.error("Project ID is required"); return; } await createStory.mutateAsync({ epicId: data.epicId, projectId, title: data.title, description: data.description, priority: data.priority, estimatedHours: typeof data.estimatedHours === "number" ? data.estimatedHours : undefined, createdBy: user.id, // Sprint 4 Story 3: New fields acceptanceCriteria: data.acceptanceCriteria, assigneeId: data.assigneeId || undefined, tags: data.tags, storyPoints: typeof data.storyPoints === "number" ? data.storyPoints : undefined, }); toast.success("Story created successfully"); } onSuccess?.(); } catch (error) { const message = error instanceof Error ? error.message : "Operation failed"; toast.error(message); } } const isLoading = createStory.isPending || updateStory.isPending; return (
( Parent Epic * {isEditing ? "Parent epic cannot be changed" : "Select the parent epic"} )} /> ( Story Title * A clear, concise title for this story )} /> ( Description