import { useActionMutation } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { zodResolver } from "@hookform/resolvers/zod"; import type { Exercise } from "@shared/types"; import { IconPlus } from "@tabler/icons-react"; import { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { formatLocalDate } from "@/lib/utils"; const formSchema = z.object({ name: z.string().min(1, "Exercise name is required"), calories_burned: z .string() .transform((val) => parseInt(val, 10)) .refine((val) => val > 0, "Must be > 0"), duration_minutes: z .string() .optional() .transform((val) => (val ? parseInt(val, 10) : undefined)), date: z.string(), }); type FormData = z.input; interface AddExerciseDialogProps { editingExercise?: Exercise | null; onOpenChange?: (open: boolean) => void; isOpen?: boolean; currentDate?: Date; } export function AddExerciseDialog({ editingExercise, onOpenChange, isOpen: controlledOpen, currentDate = new Date(), }: AddExerciseDialogProps) { const t = useT(); const [uncontrolledOpen, setUncontrolledOpen] = useState(false); const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen; const setOpen = controlledOpen !== undefined ? (v: boolean) => onOpenChange?.(v) : setUncontrolledOpen; const isEditing = !!editingExercise; const form = useForm({ // zod resolves at multiple minor versions across the workspace; cast the // schema so @hookform/resolvers v5's zod-v4 overload doesn't reject a // v4.3-internal schema under CI's frozen lockfile. Runtime is unaffected. resolver: zodResolver(formSchema as any) as any, defaultValues: { name: editingExercise?.name || "", calories_burned: editingExercise?.calories_burned.toString() || "", duration_minutes: editingExercise?.duration_minutes?.toString() || "", date: editingExercise?.date || formatLocalDate(currentDate), }, }); const createMutation = useActionMutation("log-exercise", { onSuccess: () => { toast.success(t("exercise.logged")); setOpen(false); form.reset(); }, onError: () => toast.error(t("exercise.logFailed")), }); const updateMutation = useActionMutation("update-exercise", { onSuccess: () => { toast.success(t("exercise.updated")); setOpen(false); form.reset(); }, onError: () => toast.error(t("exercise.updateFailed")), }); const handleOpenChange = (newOpen: boolean) => { setOpen(newOpen); onOpenChange?.(newOpen); if (!newOpen) form.reset(); }; useEffect(() => { if (editingExercise) { form.reset({ name: editingExercise.name, calories_burned: editingExercise.calories_burned.toString(), duration_minutes: editingExercise.duration_minutes?.toString() || "", date: editingExercise.date, }); } }, [editingExercise, form]); const onSubmit = (data: FormData) => { const date = isEditing ? editingExercise!.date : formatLocalDate(currentDate); if (isEditing) { updateMutation.mutate({ id: String(editingExercise!.id), name: data.name, calories_burned: String(data.calories_burned), duration_minutes: data.duration_minutes ? String(data.duration_minutes) : undefined, date, }); } else { createMutation.mutate({ name: data.name, calories_burned: String(data.calories_burned), duration_minutes: data.duration_minutes ? String(data.duration_minutes) : undefined, date, }); } }; return ( {!isEditing && ( )} {isEditing ? t("exercise.edit") : t("exercise.log")} {isEditing ? t("exercise.updateDescription") : t("exercise.logDescription")}
{form.formState.errors.name && (

{form.formState.errors.name.message}

)}
{form.formState.errors.calories_burned && (

{form.formState.errors.calories_burned.message}

)}
); }