/** * Payroll Jurisdiction Onboarding Step * * Used during organisation creation to set up the payroll tax jurisdiction. * Three modes: * 1. Native (AU, US, GB, CA, NZ, SG) — "Supported" badge + jurisdiction-specific fields * 2. EU countries — "Manual tax rate" mode with note about progressive support * 3. Other countries — same manual flat rate flow */ "use client"; import React, { useState, useMemo } from "react"; import { Check, ChevronDown, Search, BadgeCheck, AlertTriangle, Settings2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent } from "@/components/ui/card"; import { NATIVE_COUNTRIES, EU_COUNTRIES, US_STATES, type CountryOption, } from "@/lib/payroll-settings-types"; // ── Currency map for display ───────────────────────────────── const COUNTRY_CURRENCY: Record = { AU: "AUD", US: "USD", GB: "GBP", CA: "CAD", NZ: "NZD", SG: "SGD", DE: "EUR", FR: "EUR", ES: "EUR", IT: "EUR", IE: "EUR", NL: "EUR", BE: "EUR", AT: "EUR", PT: "EUR", FI: "EUR", SE: "SEK", DK: "DKK", NO: "NOK", PL: "PLN", }; // ── Country Dropdown with search ───────────────────────────── function CountryDropdown({ value, onChange, disabled, }: { value: string; onChange: (code: string) => void; disabled?: boolean; }) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const searchInputRef = React.useRef(null); const allCountries: CountryOption[] = useMemo(() => { // Merge native + EU + a catch-all for other countries const seen = new Set(); const merged: CountryOption[] = []; for (const c of [...NATIVE_COUNTRIES, ...EU_COUNTRIES]) { if (!seen.has(c.code)) { seen.add(c.code); merged.push(c); } } return merged; }, []); const filtered = useMemo(() => { if (!search) return allCountries; const q = search.toLowerCase(); return allCountries.filter( (c) => c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q) ); }, [search, allCountries]); const selected = allCountries.find((c) => c.code === value); return (
{open && (
setSearch(e.target.value)} placeholder="Search countries..." className="w-full pl-8 pr-3 py-1.5 text-sm border-0 focus:outline-none" autoFocus />
{/* Native supported section */} {filtered.filter((c) => c.mode === "native").length > 0 && (
Fully supported
{filtered .filter((c) => c.mode === "native") .map((c) => ( ))}
)} {/* EU section */} {filtered.filter((c) => c.mode === "eu").length > 0 && (
Manual configuration
{filtered .filter((c) => c.mode === "eu") .map((c) => ( ))}
)} {filtered.length === 0 && (
No countries found
)}
)}
); } // ── Main Form Component ────────────────────────────────────── export interface PayrollOnboardingValues { country_code: string; country_name: string; currency: string; is_natively_supported: boolean; state_code?: string; state_name?: string; state_tax_rate?: number; is_singapore_citizen_or_pr?: boolean; flat_income_tax_rate?: number; employer_pension_rate?: number; employer_tax_rate?: number; tax_free_allowance?: number; } interface PayrollJurisdictionStepProps { onComplete: (values: PayrollOnboardingValues) => void; onBack?: () => void; isLoading?: boolean; } export default function PayrollJurisdictionStep({ onComplete, onBack, isLoading, }: PayrollJurisdictionStepProps) { const [countryCode, setCountryCode] = useState(""); const [showAdvanced, setShowAdvanced] = useState(false); const [formError, setFormError] = useState(null); // Form state const [stateCode, setStateCode] = useState(""); const [stateName, setStateName] = useState(""); const [stateTaxRate, setStateTaxRate] = useState(0); const [isSgCitizenOrPr, setIsSgCitizenOrPr] = useState(true); const [flatTaxRate, setFlatTaxRate] = useState("0.2"); const [employerPensionRate, setEmployerPensionRate] = useState("0"); const [employerTaxRate, setEmployerTaxRate] = useState("0"); const [taxFreeAllowance, setTaxFreeAllowance] = useState("0"); const isNative = NATIVE_COUNTRIES.some((c) => c.code === countryCode); const isUS = countryCode === "US"; const isSG = countryCode === "SG"; const usState = US_STATES.find((s) => s.code === stateCode); const handleCountrySelect = (code: string) => { setCountryCode(code); // Reset US state when country changes if (code !== "US") { setStateCode(""); setStateName(""); setStateTaxRate(0); } }; const handleSubmit = () => { setFormError(null); if (!countryCode) { setFormError("Please select a country"); return; } if (isUS && !stateCode) { setFormError("Please select a state"); return; } if (!isNative) { const rate = parseFloat(flatTaxRate); if (isNaN(rate) || rate < 0 || rate > 1) { setFormError("Income tax rate must be between 0 and 1 (e.g. 0.25 for 25%)"); return; } } const country = NATIVE_COUNTRIES.find((c) => c.code === countryCode) ?? EU_COUNTRIES.find((c) => c.code === countryCode); onComplete({ country_code: countryCode, country_name: country?.name ?? countryCode, currency: COUNTRY_CURRENCY[countryCode] ?? "USD", is_natively_supported: isNative, state_code: isUS ? stateCode : undefined, state_name: isUS ? stateName : undefined, state_tax_rate: isUS ? stateTaxRate : undefined, is_singapore_citizen_or_pr: isSG ? isSgCitizenOrPr : undefined, flat_income_tax_rate: !isNative ? parseFloat(flatTaxRate) : undefined, employer_pension_rate: !isNative ? parseFloat(employerPensionRate) || 0 : undefined, employer_tax_rate: !isNative ? parseFloat(employerTaxRate) || 0 : undefined, tax_free_allowance: !isNative ? parseFloat(taxFreeAllowance) || 0 : undefined, }); }; const selectedCountry = NATIVE_COUNTRIES.find((c) => c.code === countryCode) ?? EU_COUNTRIES.find((c) => c.code === countryCode); return (

Payroll Jurisdiction

Where is your organisation based? We'll configure the correct tax rules.

{ e.preventDefault(); handleSubmit(); }} className="space-y-5"> {/* Country selector */}
{/* Native supported: jurisdiction-specific fields */} {isNative && (
Fully supported — {selectedCountry?.name}
{selectedCountry?.badge && (

{selectedCountry.badge}

)} {/* US: state selector */} {isUS && (
{usState && (
State income tax rate (auto-filled) {usState.hasStateTax ? `${(usState.defaultTaxRate * 100).toFixed(2)}%` : "None"}
)} {!usState && stateCode && (
State tax rate will be set automatically.
)}
)} {/* Singapore: employee type */} {isSG && (
)}
)} {/* Manual mode: EU + other countries */} {(countryCode && !isNative) && (
Manual tax configuration

{selectedCountry?.name} is not yet fully supported. Enter your local tax rates below. Vela will add full support progressively based on demand.

setFlatTaxRate(e.target.value)} type="number" step="0.01" min="0" max="1" placeholder="0.25" className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm pr-10 focus:outline-none focus:ring-2 focus:ring-[#1D9E75]/20 focus:border-[#1D9E75]" /> %

Enter as decimal: 0.25 = 25%, 0.30 = 30%

{showAdvanced && (
setEmployerPensionRate(e.target.value)} type="number" step="0.01" min="0" max="1" placeholder="0" className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm pr-10 focus:outline-none focus:ring-2 focus:ring-[#1D9E75]/20 focus:border-[#1D9E75]" /> %
setEmployerTaxRate(e.target.value)} type="number" step="0.01" min="0" max="1" placeholder="0" className="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm pr-10 focus:outline-none focus:ring-2 focus:ring-[#1D9E75]/20 focus:border-[#1D9E75]" /> %
setTaxFreeAllowance(e.target.value)} type="number" min="0" placeholder="0" className="mt-1 w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-[#1D9E75]/20 focus:border-[#1D9E75]" />
)}
)} {/* Error display */} {formError && (
{formError}
)} {/* Actions */}
{onBack ? ( ) : (
)}
); }