/** * Package settings form: owns state, validation (Zod), and errors. * Notifies page via onChange(formId, value) and onInteraction(). */ import { __ } from '@wordpress/i18n'; import { memo, useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { Card, CardBody, Flex, Notice } from '@wordpress/components'; import { Spinner } from '@woocommerce/components'; import { SettingsField } from './settings-field'; import { formatDisplayNumber, getDimensionBounds } from '../../shared/utils'; import type { PackagePreset, NewPackageForm, PackagesFormValue, UpdateSettingsPayload, UpdateSettingsResponse, } from '../../types/settings'; import { packagePresetArraySchema } from './utils/schemas'; import type { ZodError } from 'zod'; export const PACKAGES_FORM_ID = 'packages' as const; function buildDimensionRangeMessage(dimensionUnit: string, min: number, max: number): string { return __( `Dimension must be between ${formatDisplayNumber(min)} and ${formatDisplayNumber(max)} ${dimensionUnit} (1-150cm equivalent).`, 'parcel2go-shipping' ); } function formatPackageLabel(pkg: PackagePreset, dimensionUnit: string): string { return `${pkg.name} - ${formatDisplayNumber(pkg.length)}x${formatDisplayNumber(pkg.width)}x${formatDisplayNumber(pkg.height)}${dimensionUnit}`; } /** Map packages array Zod errors; when newPackageIndex is set, map that index to "new.*". */ function mapPackagesErrors(error: ZodError, newPackageIndex?: number): Record { const flat = error.flatten(); const fieldErrors = flat.fieldErrors as Record; const out: Record = {}; for (const key of Object.keys(fieldErrors)) { const msg = fieldErrors[key]?.[0]; if (!msg) continue; // Keys are like "0", "1.name", "2.length" if (newPackageIndex !== undefined && key.startsWith(String(newPackageIndex) + '.')) { out['new.' + key.slice(String(newPackageIndex).length + 1)] = msg; } out[key] = msg; } return out; } export interface PackagePresetsFormProps { defaultValue: PackagesFormValue; dimensionUnit?: string; onChange: (formId: string, value: PackagesFormValue) => void; onInteraction?: () => void; resetTrigger?: number; submitErrors?: Record; handleSubmit?: (payload: UpdateSettingsPayload) => Promise; } function PackagePresetsFormInner({ defaultValue, dimensionUnit = 'cm', onChange, onInteraction, resetTrigger = 0, submitErrors, handleSubmit, }: PackagePresetsFormProps) { const [value, setValue] = useState(() => ({ packages: defaultValue.packages.map((p) => ({ ...p })), defaultPackageId: defaultValue.defaultPackageId, newPackage: { ...defaultValue.newPackage }, })); const [errors, setErrors] = useState>({}); const [defaultPackageFeedback, setDefaultPackageFeedback] = useState< { status: 'success' | 'error'; message: string } | null >(null); const [isDefaultPackageSaving, setIsDefaultPackageSaving] = useState(false); const hasInteracted = useRef(false); const dimensionBounds = getDimensionBounds(dimensionUnit); useEffect(() => { setValue({ packages: defaultValue.packages.map((p) => ({ ...p })), defaultPackageId: defaultValue.defaultPackageId, newPackage: { ...defaultValue.newPackage }, }); setErrors({}); hasInteracted.current = false; }, [defaultValue]); useEffect(() => { setDefaultPackageFeedback(null); hasInteracted.current = false; }, [resetTrigger]); useEffect(() => { const packagesToValidate = [...value.packages]; const hasNewData = value.newPackage.name.trim() !== '' || value.newPackage.length !== 0 || value.newPackage.width !== 0 || value.newPackage.height !== 0; if (hasNewData) { packagesToValidate.push({ id: 'pkg_new', name: value.newPackage.name.trim(), length: value.newPackage.length, width: value.newPackage.width, height: value.newPackage.height, weight: 0, }); } const result = packagePresetArraySchema.safeParse(packagesToValidate); const nextErrors: Record = result.success ? {} : mapPackagesErrors(result.error, hasNewData ? value.packages.length : undefined); if (hasNewData) { const rangeMessage = buildDimensionRangeMessage( dimensionUnit, dimensionBounds.min, dimensionBounds.max ); const fields: Array = ['length', 'width', 'height']; for (const field of fields) { const fieldValue = Number(value.newPackage[field]); if ( fieldValue > 0 && (fieldValue < dimensionBounds.min || fieldValue > dimensionBounds.max) ) { nextErrors[`new.${field}`] = rangeMessage; } } } if (result.success) { setErrors(nextErrors); } else { setErrors(nextErrors); } }, [value, dimensionBounds.max, dimensionBounds.min, dimensionUnit]); const displayErrors = submitErrors ?? errors; const notify = useCallback( (next: PackagesFormValue) => { if (!hasInteracted.current) { hasInteracted.current = true; onInteraction?.(); } onChange(PACKAGES_FORM_ID, next); }, [onChange, onInteraction] ); const updateDefaultId = useCallback( async (id: string) => { if (!handleSubmit || isDefaultPackageSaving) return; const previousId = value.defaultPackageId; const payload: UpdateSettingsPayload = { type: 'App', data: { packages: { defaultPackage: id } }, }; const next = { ...value, defaultPackageId: id }; setDefaultPackageFeedback(null); setIsDefaultPackageSaving(true); setValue(next); try { const result = await handleSubmit(payload); if (result?.success) { setDefaultPackageFeedback({ status: 'success', message: result.message ?? __('Default package updated successfully.', 'parcel2go-shipping'), }); } else { setValue({ ...value, defaultPackageId: previousId }); setDefaultPackageFeedback({ status: 'error', message: result?.message ?? __('Failed to update default package', 'parcel2go-shipping'), }); } } catch { setValue({ ...value, defaultPackageId: previousId }); setDefaultPackageFeedback({ status: 'error', message: __('Failed to update default package', 'parcel2go-shipping'), }); } finally { setIsDefaultPackageSaving(false); } }, [value, handleSubmit, isDefaultPackageSaving] ); const updateNewPackage = useCallback( (pkg: NewPackageForm) => { const next = { ...value, newPackage: pkg }; setValue(next); notify(next); }, [value, notify] ); const defaultOptions = [ { value: '', label: __('— Select —', 'parcel2go-shipping') }, ...value.packages.map((pkg) => ({ value: pkg.id, label: formatPackageLabel(pkg, dimensionUnit), })), ]; return (
{__('Default package', 'parcel2go-shipping')}
{isDefaultPackageSaving && ( {__('Updating default package...', 'parcel2go-shipping')} )} {defaultPackageFeedback && ( setDefaultPackageFeedback(null)} > {defaultPackageFeedback.message} )}
{__('Add a new package', 'parcel2go-shipping')}
updateNewPackage({ ...value.newPackage, name: v }) } error={displayErrors['new.name']} description={__( 'Enter a unique name for this package.', 'parcel2go-shipping' )} />
{__('Dimensions', 'parcel2go-shipping')} ({dimensionUnit})
updateNewPackage({ ...value.newPackage, length: parseFloat(v || '0') || 0, }) } type="number" min={dimensionBounds.min} max={dimensionBounds.max} error={displayErrors['new.length']} /> updateNewPackage({ ...value.newPackage, width: parseFloat(v || '0') || 0, }) } type="number" min={dimensionBounds.min} max={dimensionBounds.max} error={displayErrors['new.width']} /> updateNewPackage({ ...value.newPackage, height: parseFloat(v || '0') || 0, }) } type="number" min={dimensionBounds.min} max={dimensionBounds.max} error={displayErrors['new.height']} />

{__( `Allowed range: ${formatDisplayNumber(dimensionBounds.min)}-${formatDisplayNumber(dimensionBounds.max)} ${dimensionUnit} (1-150cm equivalent).`, 'parcel2go-shipping' )}

); } export const PackagePresetsForm = memo(PackagePresetsFormInner);