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:
co-authored by
Claude Fable 5
parent
d012d40b18
commit
731a57dd6e
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user