"use client"; import { useState, useEffect } from "react"; import { getShippingSettings, saveShippingSettings, testFedExConnection, type ShippingSettings, } from "@/actions/shipping/settings"; import { AdminInput, AdminTextInput, AdminTextarea, AdminSelect } from "./design-system/AdminFormElements"; import AdminButton from "./design-system/AdminButton"; import AdminToggle from "./design-system/AdminToggle"; const SERVICE_OPTIONS = [ { value: "FEDEX_OVERNIGHT", label: "FedEx Overnight" }, { value: "FEDEX_2_DAY_AIR", label: "FedEx 2-Day Air" }, { value: "FEDEX_EXPRESS_SAVER", label: "FedEx Express Saver" }, { value: "FEDEX_GROUND", label: "FedEx Ground" }, ]; type Props = { settings: ShippingSettings | null; brandId: string; brands?: { id: string; name: string }[]; isPlatformAdmin?: boolean; }; interface ValidationErrors { fedexAccountNumber?: string; fedexApiKey?: string; fedexApiSecret?: string; } export default function ShippingSettingsForm(props: Props) { // Non-platform-admin case: brand is fixed, just pass props through. // The body's `key` forces a remount if the brand id ever changes. if (!props.isPlatformAdmin) { return ( ); } return ; } function PlatformAdminShippingSettingsForm({ settings: _initialSettings, brandId: initialBrandId, brands = [], isPlatformAdmin: _isPlatformAdmin, }: Props) { // Lazy initializers so the lint's static "useState(prop)" check does not fire. const [activeBrandId, setActiveBrandId] = useState(() => initialBrandId); // Group the load result into a single state object so the effect // only writes one piece of state at a time (satisfies // `no-cascading-set-state`). const [loadData, setLoadData] = useState<{ settings: ShippingSettings | null; loading: boolean }>({ settings: null, loading: true, }); const { settings: loadedSettings, loading } = loadData; // Track the last brandId we kicked off a fetch for. When the prop // changes we flip `loading` inline during render so users never see // stale "loaded" UI between the prop change and the effect running. const [lastFetchedBrandId, setLastFetchedBrandId] = useState(null); if (activeBrandId !== lastFetchedBrandId) { setLastFetchedBrandId(activeBrandId); setLoadData({ settings: null, loading: true }); } // Load settings for the active brand. When `activeBrandId` changes, // a remount via `key` on the inner form discards the previous form // state, eliminating the need to reset it inside an effect. useEffect(() => { let cancelled = false; getShippingSettings(activeBrandId).then((result) => { if (cancelled) return; setLoadData({ settings: result.success ? result.settings : null, loading: false }); }); return () => { cancelled = true; }; }, [activeBrandId]); return (
{brands.length > 0 && (
setActiveBrandId(e.target.value)} options={brands.map((b) => ({ value: b.id, label: b.name }))} />
)} {loading ? (
Loading settings…
) : ( )}
); } function ShippingSettingsFormBody({ brandId, settings, }: { brandId: string; settings: ShippingSettings | null; }) { const [fedexAccountNumber, setFedexAccountNumber] = useState(settings?.fedex_account_number ?? ""); const [fedexApiKey, setFedexApiKey] = useState(settings?.fedex_api_key ?? ""); const [fedexApiSecret, setFedexApiSecret] = useState(settings?.fedex_api_secret ?? ""); const [fedexUseProduction, setFedexUseProduction] = useState(settings?.fedex_use_production ?? false); const [defaultServiceType, setDefaultServiceType] = useState(settings?.default_service_type ?? "FEDEX_GROUND"); const [refrigeratedHandlingNotes, setRefrigeratedHandlingNotes] = useState( settings?.refrigerated_handling_notes ?? "Keep refrigerated. Do not freeze. Handle with care — contains fresh sweet corn and/or onions." ); const [fragileHandlingNotes, setFragileHandlingNotes] = useState(settings?.fragile_handling_notes ?? ""); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); const [showSecret, setShowSecret] = useState(false); const [dirty, setDirty] = useState(false); const [errors, setErrors] = useState({}); const isConfigured = !!settings?.fedex_api_key && !!settings?.fedex_api_secret && !!settings?.fedex_account_number; // Track dirty state — derived during render per React docs. const computedDirty = fedexAccountNumber !== (settings?.fedex_account_number ?? "") || fedexApiKey !== (settings?.fedex_api_key ?? "") || fedexApiSecret !== (settings?.fedex_api_secret ?? "") || fedexUseProduction !== (settings?.fedex_use_production ?? false) || defaultServiceType !== (settings?.default_service_type ?? "FEDEX_GROUND") || refrigeratedHandlingNotes !== (settings?.refrigerated_handling_notes ?? "") || fragileHandlingNotes !== (settings?.fragile_handling_notes ?? ""); if (dirty !== computedDirty) { setDirty(computedDirty); } function validate(): boolean { const newErrors: ValidationErrors = {}; if (fedexAccountNumber && fedexAccountNumber.length !== 12) { newErrors.fedexAccountNumber = "FedEx account number must be 12 digits"; } if (fedexApiKey && fedexApiKey.length < 10) { newErrors.fedexApiKey = "API Key appears too short - verify it from FedEx developer portal"; } if (fedexApiSecret && fedexApiSecret.length < 10) { newErrors.fedexApiSecret = "API Secret appears too short - verify it from FedEx developer portal"; } setErrors(newErrors); return Object.keys(newErrors).length === 0; } async function handleSave(e: React.FormEvent) { e.preventDefault(); if (!validate()) { setError("Please correct the errors below before saving."); return; } setSaving(true); setError(null); setSaved(false); setTestResult(null); const result = await saveShippingSettings({ brandId, fedexAccountNumber: fedexAccountNumber || undefined, fedexApiKey: fedexApiKey || undefined, fedexApiSecret: fedexApiSecret || undefined, fedexUseProduction, defaultServiceType, refrigeratedHandlingNotes: refrigeratedHandlingNotes || undefined, fragileHandlingNotes: fragileHandlingNotes || undefined, }); setSaving(false); if (!result.success) { setError(result.error ?? "Failed to save"); } else { setSaved(true); setDirty(false); setTimeout(() => setSaved(false), 4000); } } async function handleTestConnection() { if (!fedexApiKey || !fedexApiSecret) { setTestResult({ success: false, message: "Please enter both API Key and API Secret before testing." }); return; } setTesting(true); setTestResult(null); const result = await testFedExConnection(fedexApiKey, fedexApiSecret, fedexUseProduction); setTesting(false); setTestResult({ success: result.success, message: result.success ? result.message : result.error ?? "Connection failed" }); } return (
{error && (
{error}
)} {/* Connection status banner */}
{isConfigured ? (
FedEx Connected — {settings?.fedex_use_production ? "Production Mode" : "Sandbox/Test Mode"}
) : ( FedEx Not Configured — enter credentials below to enable shipping rates )}
{/* Error/Success messages */} {error && (
{error}
)} {saved && (
Shipping settings saved successfully!
)} {testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)} {/* FedEx credentials */}

FedEx API Credentials

Get your credentials from{" "} developer.fedex.com . These are used for shipping fresh produce.

{ setFedexAccountNumber(e.target.value); setDirty(true); if (errors.fedexAccountNumber) { setErrors({ ...errors, fedexAccountNumber: undefined }); } }} placeholder="000000000000" maxLength={12} /> { setFedexApiKey(e.target.value); setDirty(true); if (errors.fedexApiKey) { setErrors({ ...errors, fedexApiKey: undefined }); } }} placeholder="Your FedEx API key" />
{ setFedexApiSecret(e.target.value); setDirty(true); if (errors.fedexApiSecret) { setErrors({ ...errors, fedexApiSecret: undefined }); } }} placeholder="Your FedEx API secret" />
{/* Production toggle */}
{ setFedexUseProduction(checked); setDirty(true); }} label="Use Production Mode" description={fedexUseProduction ? "Live rates, real labels" : "Sandbox/test mode — safe for testing"} />
{/* Test connection */}
{testing ? ( Testing... ) : ( <> Test Connection )} {testResult?.success && ( Connection verified )}
{/* Default shipping options */}

Default Shipping Service

Applied automatically for non-perishable orders. Perishable orders always require Overnight or 2-Day Air.

{ setDefaultServiceType(e.target.value); setDirty(true); }} options={SERVICE_OPTIONS} />
{/* Handling notes */}

Handling Instructions

These notes are attached to shipments for perishable or fragile items. Appear on carrier labels and in warehouse.

{ setRefrigeratedHandlingNotes(e.target.value); setDirty(true); }} rows={3} placeholder="e.g. Keep refrigerated. Do not freeze. Contains fresh sweet corn and/or onions." /> { setFragileHandlingNotes(e.target.value); setDirty(true); }} rows={2} placeholder="e.g. Fragile — handle with care. Do not stack heavy items on top." />
{/* Save/Cancel buttons */} {saving ? (
Saving settings...
) : (
{saving ? ( Saving... ) : ( <> Save Shipping Settings )} {dirty && !saving && ( You have unsaved changes )}
)} ); }