From 731a57dd6e2fd0d8404f116e7854c93f00038727 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:59:41 +0200 Subject: [PATCH] fix(deadlines): submit only form-managed fields from the deadline form (#1185) * fix(deadlines): submit only form-managed fields from the deadline form Fixes #1176. The form fabricated 11 system-field values on every submit (source: 'user', status: 'upcoming', reminder_offsets, tax_* nulls, ...) and the edit path PUT the entire merged Deadline row; only the route handlers' whitelists prevented editing a system-generated tax deadline from nuking those fields. The form now has an explicit DeadlineFormValues contract (the 7 fields it renders), create and edit send exactly that, and the edit handler takes (id, values) instead of a whole Deadline. No behavior change today; removes the latent data-loss dependency on the server whitelist. Co-Authored-By: Claude Fable 5 * fix(deadlines): migrate calendar DeadlineForm consumers to DeadlineFormValues The calendar extension's PaymentCalendar (and its CalendarWorkspace host) still typed the submit chain as the old full-row Omit shape, failing the core-only typecheck. Behavior unchanged: the raw insert already omitted ids, and the DB defaults cover system fields. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- app/(dashboard)/deadlines/page.tsx | 21 +++++++-------- components/deadlines/DeadlineForm.tsx | 27 +++++++++---------- .../extensions/general/CalendarWorkspace.tsx | 5 ++-- .../calendar/components/PaymentCalendar.tsx | 6 ++--- 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/app/(dashboard)/deadlines/page.tsx b/app/(dashboard)/deadlines/page.tsx index 1efcc1cf..16b1408a 100644 --- a/app/(dashboard)/deadlines/page.tsx +++ b/app/(dashboard)/deadlines/page.tsx @@ -8,7 +8,7 @@ import { fetchAllRows } from '@/lib/supabase/fetch-all' import { useToast } from '@/components/ui/use-toast' import { ToastAction } from '@/components/ui/toast' import { DeadlineList } from '@/components/deadlines/DeadlineList' -import { DeadlineForm } from '@/components/deadlines/DeadlineForm' +import { DeadlineForm, type DeadlineFormValues } from '@/components/deadlines/DeadlineForm' import { PageHeader } from '@/components/ui/page-header' import { HelpPopover } from '@/components/ui/help-popover' import { AttnLine } from '@/components/ui/attn-line' @@ -145,9 +145,7 @@ export default function DeadlinesPage() { } } - const handleDeadlineCreate = async ( - data: Omit - ) => { + const handleDeadlineCreate = async (data: DeadlineFormValues) => { try { const response = await fetch('/api/deadlines', { method: 'POST', @@ -228,12 +226,15 @@ export default function DeadlinesPage() { } } - const handleDeadlineEdit = async (deadline: Deadline) => { + // Sends ONLY the form-managed fields: the PUT route whitelists to the same + // set, and posting a whole Deadline row from here would fabricate system + // fields (source, status, reminder_offsets) that only the whitelist drops. + const handleDeadlineEdit = async (id: string, data: DeadlineFormValues) => { try { - const response = await fetch(`/api/deadlines/${deadline.id}`, { + const response = await fetch(`/api/deadlines/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(deadline), + body: JSON.stringify(data), }) if (!response.ok) { @@ -281,11 +282,9 @@ export default function DeadlinesPage() { } } - const handleFormSubmit = async ( - data: Omit, - ) => { + const handleFormSubmit = async (data: DeadlineFormValues) => { if (editingDeadline) { - await handleDeadlineEdit({ ...editingDeadline, ...data }) + await handleDeadlineEdit(editingDeadline.id, data) } else { await handleDeadlineCreate(data) } diff --git a/components/deadlines/DeadlineForm.tsx b/components/deadlines/DeadlineForm.tsx index 2210d4b0..ef09c478 100644 --- a/components/deadlines/DeadlineForm.tsx +++ b/components/deadlines/DeadlineForm.tsx @@ -24,10 +24,21 @@ import { formatDateISO, DEADLINE_TYPE_LABELS, PRIORITY_LABELS } from '@/lib/cale import { useCanWrite } from '@/lib/hooks/use-can-write' import { Lock } from 'lucide-react' +/** + * Only the fields the form actually manages. Both API routes whitelist to + * this set; the form must never fabricate values for system fields (source, + * status, reminder_offsets, tax_*), or editing a system-generated tax + * deadline would depend on the server whitelist alone to avoid data loss. + */ +export type DeadlineFormValues = Pick< + Deadline, + 'title' | 'due_date' | 'due_time' | 'deadline_type' | 'priority' | 'customer_id' | 'notes' +> + interface DeadlineFormProps { open: boolean onOpenChange: (open: boolean) => void - onSubmit: (data: Omit) => Promise + onSubmit: (data: DeadlineFormValues) => Promise onDelete?: (deadline: Partial) => void initialData?: Partial initialDate?: Date | null @@ -107,20 +118,6 @@ export function DeadlineForm({ priority: formData.priority, customer_id: formData.customer_id || null, notes: formData.notes || null, - is_completed: initialData?.is_completed || false, - completed_at: initialData?.completed_at || null, - is_auto_generated: false, - // New tax deadline fields with defaults for user-created deadlines - tax_deadline_type: null, - tax_period: null, - source: 'user', - reminder_offsets: [14, 7, 1, 0], - status: 'upcoming', - status_changed_at: new Date().toISOString(), - linked_report_type: null, - linked_report_period: null, - dismissed_at: null, - tax_assessment_notice_id: null, }) } finally { setIsLoading(false) diff --git a/components/extensions/general/CalendarWorkspace.tsx b/components/extensions/general/CalendarWorkspace.tsx index 56654470..b9002c7c 100644 --- a/components/extensions/general/CalendarWorkspace.tsx +++ b/components/extensions/general/CalendarWorkspace.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react' import { createClient } from '@/lib/supabase/client' import { useToast } from '@/components/ui/use-toast' import { PaymentCalendar } from '@/extensions/general/calendar/components/PaymentCalendar' +import type { DeadlineFormValues } from '@/components/deadlines/DeadlineForm' import type { WorkspaceComponentProps } from '@/lib/extensions/workspace-registry' import type { Invoice, Deadline } from '@/types' @@ -58,9 +59,7 @@ export default function CalendarWorkspace({ userId }: WorkspaceComponentProps) { fetchData() }, [fetchData]) - const handleDeadlineCreate = async ( - data: Omit - ) => { + const handleDeadlineCreate = async (data: DeadlineFormValues) => { try { const { error } = await supabase.from('deadlines').insert([data]) diff --git a/extensions/general/calendar/components/PaymentCalendar.tsx b/extensions/general/calendar/components/PaymentCalendar.tsx index 36c6672f..4337616e 100644 --- a/extensions/general/calendar/components/PaymentCalendar.tsx +++ b/extensions/general/calendar/components/PaymentCalendar.tsx @@ -10,13 +10,13 @@ import { CalendarGrid } from './CalendarGrid' import { CalendarWeekView } from './CalendarWeekView' import { CalendarDayView } from './CalendarDayView' import { DayDetailModal } from './DayDetailModal' -import { DeadlineForm } from '@/components/deadlines/DeadlineForm' +import { DeadlineForm, type DeadlineFormValues } from '@/components/deadlines/DeadlineForm' interface PaymentCalendarProps { invoices: Invoice[] deadlines: Deadline[] customers: { id: string; name: string }[] - onDeadlineCreate: (data: Omit) => Promise + onDeadlineCreate: (data: DeadlineFormValues) => Promise onDeadlineToggle: (deadline: Deadline) => Promise } @@ -125,7 +125,7 @@ export function PaymentCalendar({ setDeadlineFormDate(null) }, []) - const handleDeadlineSubmit = useCallback(async (data: Omit) => { + const handleDeadlineSubmit = useCallback(async (data: DeadlineFormValues) => { await onDeadlineCreate(data) handleDeadlineFormClose() }, [onDeadlineCreate])