// src/pages/DashboardPage/BillingPage/InvoiceTermsSettingsSection.tsx // Billing workspace v2-4: the org "Default payment terms" control, living in the Overview's "Billing // settings" card next to the (moved) tax rate. Mirrors TaxSettingsSection's save pattern - seed from the // org, edit locally, PUT the dedicated billing:manage endpoint, then push the returned Organization back // into auth via setWpOrganization. A new invoice starts with these net days; issuing freezes its due date. // Radix ui/* + Tailwind, no @wordpress/components and no per-section CSS file (zero new WordPress). import { useEffect, useRef, useState } from 'react'; import { AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'; import { useOrganizationServiceContext } from '../../../services/OrganizationService.tsx'; import { Label } from '../../../components/ui/label.tsx'; import { Alert, AlertDescription } from '../../../components/ui/alert.tsx'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '../../../components/ui/select.tsx'; import { getDefaultNetDays } from '../../../utils/organizationInvoiceTerms'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import { READ_BACK_FAILED, useBillingDefaultSave } from './useBillingDefaultSave'; // The default-terms options as net days - byte-values match the server's accepted set (0/15/30/60/90). // Exported so the Overview's read-only viewer display can label a stored value without duplicating this. export const NET_DAYS_OPTIONS: { value: number; label: string }[] = [ { value: 0, label: 'Due on receipt' }, { value: 15, label: 'Net 15' }, { value: 30, label: 'Net 30' }, { value: 60, label: 'Net 60' }, { value: 90, label: 'Net 90' }, ]; // Label for a stored net-days value; falls back to "Net N" for any value not in the fixed option set. export const netDaysLabel = (netDays: number): string => NET_DAYS_OPTIONS.find((option) => option.value === netDays)?.label ?? `Net ${netDays}`; const InvoiceTermsSettingsSection = () => { const { updateInvoiceTermsSettings } = useOrganizationServiceContext(); const [netDays, setNetDays] = useState(30); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(); const [saveSuccess, setSaveSuccess] = useState(false); // Seeded from the practice on screen whenever it, or the person signed in, changes; whatever was saving is retired. const seed = useRef(() => undefined as void); const { organization, practiceId, save, readBack, unsettled, checksHere } = useBillingDefaultSave('organization-invoice-terms', 'terms', () => seed.current()); seed.current = () => { setNetDays(getDefaultNetDays(organization ?? undefined)); setSaving(false); setSaveError(undefined); setSaveSuccess(false); }; useEffect(() => { seed.current(); }, [practiceId]); const handleSave = async () => { setSaving(true); setSaveError(undefined); setSaveSuccess(false); const result = await save((transport) => updateInvoiceTermsSettings({ netDays }, transport)); if (result.outcome === 'retired') return; setSaving(false); if (result.outcome === 'saved') { setSaveSuccess(true); } else if (result.outcome !== 'unconfirmed' && result.outcome !== 'held') { console.error('Error saving invoice terms settings:', result.error); setSaveError(relayServerFailure(result.error, 'Failed to save payment terms. Please try again.')); } }; const checkSaved = async () => { setSaving(true); const fresh = await readBack(); if (fresh === 'retired') return; setSaving(false); if (!fresh) { setSaveError(READ_BACK_FAILED); return; } setNetDays(getDefaultNetDays(fresh)); setSaveError(undefined); }; return (

Default payment terms

The payment terms new invoices start with. A new invoice's due date is its issue date plus these net days; the due date freezes when the invoice is issued.

{(saveError || unsettled) && ( {saveError ?? unsettled} )} {saveSuccess && ( Payment terms saved. )}
{checksHere && ( )}
); }; export default InvoiceTermsSettingsSection;