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 <noreply@anthropic.com>

* 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<Deadline>
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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-25 12:59:41 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent d012d40b18
commit 731a57dd6e
4 changed files with 27 additions and 32 deletions
+10 -11
View File
@@ -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<Deadline, 'id' | 'user_id' | 'company_id' | 'created_at' | 'updated_at'>
) => {
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<Deadline, 'id' | 'user_id' | 'company_id' | 'created_at' | 'updated_at'>,
) => {
const handleFormSubmit = async (data: DeadlineFormValues) => {
if (editingDeadline) {
await handleDeadlineEdit({ ...editingDeadline, ...data })
await handleDeadlineEdit(editingDeadline.id, data)
} else {
await handleDeadlineCreate(data)
}
+12 -15
View File
@@ -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<Deadline, 'id' | 'user_id' | 'company_id' | 'created_at' | 'updated_at'>) => Promise<void>
onSubmit: (data: DeadlineFormValues) => Promise<void>
onDelete?: (deadline: Partial<Deadline>) => void
initialData?: Partial<Deadline>
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)
@@ -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<Deadline, 'id' | 'user_id' | 'company_id' | 'created_at' | 'updated_at'>
) => {
const handleDeadlineCreate = async (data: DeadlineFormValues) => {
try {
const { error } = await supabase.from('deadlines').insert([data])
@@ -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<Deadline, 'id' | 'user_id' | 'company_id' | 'created_at' | 'updated_at'>) => Promise<void>
onDeadlineCreate: (data: DeadlineFormValues) => Promise<void>
onDeadlineToggle: (deadline: Deadline) => Promise<void>
}
@@ -125,7 +125,7 @@ export function PaymentCalendar({
setDeadlineFormDate(null)
}, [])
const handleDeadlineSubmit = useCallback(async (data: Omit<Deadline, 'id' | 'user_id' | 'company_id' | 'created_at' | 'updated_at'>) => {
const handleDeadlineSubmit = useCallback(async (data: DeadlineFormValues) => {
await onDeadlineCreate(data)
handleDeadlineFormClose()
}, [onDeadlineCreate])