import { useState, useRef, useEffect, useMemo } from 'react'; import { Scale } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useToast } from '../../contexts/ToastContext'; import type { AdditionalSectionProps } from './types'; function SpoolWeightPicker({ catalog, value, onChange, catalogId, onCatalogIdChange, }: { catalog: { id: number; name: string; weight: number }[]; value: number; onChange: (weight: number) => void; catalogId: number | null; onCatalogIdChange: (id: number | null) => void; }) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(''); const dropdownRef = useRef(null); const inputRef = useRef(null); useEffect(() => { const handleClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setIsOpen(false); } }; document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, []); // When value changes, auto-select if there's only one matching entry or keep selection if it still matches useEffect(() => { // If no catalog loaded yet, skip matching logic if (catalog.length === 0) { return; } const matches = catalog.filter(e => e.weight === value); // If currently selected entry still matches the weight, keep it selected if (catalogId) { const selected = catalog.find(e => e.id === catalogId); if (selected && selected.weight === value) { return; // Keep current selection } } // If exactly one match, auto-select it if (matches.length === 1) { onCatalogIdChange(matches[0].id); } else if (matches.length === 0) { // No matches, clear selection to prevent stale catalog ID if (catalogId !== null) { onCatalogIdChange(null); } } // If multiple matches, don't auto-select - let user choose }, [value, catalog, catalogId, onCatalogIdChange]); const filtered = useMemo(() => { if (!search) return catalog; const s = search.toLowerCase(); return catalog.filter(e => e.name.toLowerCase().includes(s) || e.weight.toString().includes(s), ); }, [catalog, search]); // Find all entries matching the current weight const matchingEntries = useMemo(() => { return catalog.filter(e => e.weight === value); }, [catalog, value]); // Display value: show catalog name if selected by ID, otherwise show first match const displayValue = useMemo(() => { if (isOpen) return search; // If a catalog ID is explicitly selected, use that if (catalogId) { const entry = catalog.find(e => e.id === catalogId); if (entry) return entry.name; } // Otherwise, show the first matching entry as a suggestion if (matchingEntries.length > 0) { return matchingEntries[0].name; } // Leave empty if there are no matches return ''; }, [isOpen, search, catalogId, catalog, matchingEntries]); return (
{ setIsOpen(true); setSearch(''); }} onChange={(e) => { setSearch(e.target.value); setIsOpen(true); }} /> {isOpen && (
{filtered.length === 0 ? (
{t('inventory.noResults')}
) : ( filtered.map(entry => ( )) )}
)}
{ const val = parseInt(e.target.value); if (!isNaN(val) && val >= 0) onChange(val); }} /> g
); } export function AdditionalSection({ formData, updateField, spoolCatalog, currencySymbol, availableCategories, globalLowStockThreshold, spoolmanMode = false, }: AdditionalSectionProps) { const { t } = useTranslation(); const { showToast } = useToast(); const [measuredInput, setMeasuredInput] = useState(''); const [isMeasuredFocused, setIsMeasuredFocused] = useState(false); const [remainingInput, setRemainingInput] = useState(''); const [isRemainingFocused, setIsRemainingFocused] = useState(false); const remainingWeight = Math.max(0, formData.label_weight - formData.weight_used); const measuredDefault = formData.core_weight + remainingWeight; useEffect(() => { if (!isMeasuredFocused) { setMeasuredInput(String(measuredDefault)); } }, [isMeasuredFocused, measuredDefault]); useEffect(() => { if (!isRemainingFocused) { setRemainingInput(String(remainingWeight)); } }, [isRemainingFocused, remainingWeight]); return (
{/* Empty Spool Weight — hidden in Spoolman mode (managed per filament type in Spoolman) */} {spoolmanMode ? (

{t('inventory.spoolWeightManagedBySpoolman')}

) : ( updateField('core_weight', weight)} catalogId={formData.core_weight_catalog_id} onCatalogIdChange={(id) => updateField('core_weight_catalog_id', id)} /> )} {/* Current Weight (remaining filament) */}
setIsRemainingFocused(true)} onChange={(e) => { setRemainingInput(e.target.value); }} onBlur={() => { setIsRemainingFocused(false); const raw = remainingInput.trim(); const remaining = Number(raw); if (!raw || !Number.isFinite(remaining) || remaining < 0 || remaining > formData.label_weight) { setRemainingInput(String(remainingWeight)); return; } const rounded = Math.round(remaining); updateField('weight_used', Math.max(0, formData.label_weight - rounded)); setRemainingInput(String(rounded)); }} className="w-full px-3 py-2 pr-7 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green" /> g
/ {formData.label_weight}g
{/* Measured Weight (empty spool + remaining filament) */}
setIsMeasuredFocused(true)} onChange={(e) => { setMeasuredInput(e.target.value); }} onBlur={() => { setIsMeasuredFocused(false); const raw = measuredInput.trim(); const measured = Number(raw); const minAllowed = formData.core_weight; const maxAllowed = formData.core_weight + formData.label_weight; if (!raw || !Number.isFinite(measured) || measured < minAllowed || measured > maxAllowed) { showToast(t('inventory.measuredWeightError', { min: minAllowed, max: maxAllowed }), 'error'); setMeasuredInput(String(measuredDefault)); return; } const rounded = Math.round(measured); const remaining = Math.max(0, Math.min(formData.label_weight, rounded - formData.core_weight)); updateField('weight_used', Math.max(0, formData.label_weight - remaining)); setMeasuredInput(String(rounded)); }} className="w-full px-3 py-2 pr-7 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green" /> g
/ {formData.core_weight + formData.label_weight}g
{/* Cost per kg */}
{currencySymbol} { const value = e.target.value === '' ? null : parseFloat(e.target.value); updateField('cost_per_kg', value); }} style={{ paddingLeft: `${Math.max(2, currencySymbol.length * 0.6 + 1)}rem` }} className="w-full py-2 pr-3 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white text-sm focus:outline-none focus:border-bambu-green" />
{/* Category (#729) */}
updateField('category', e.target.value)} /> {availableCategories.length > 0 && ( {availableCategories.map((c) => )}
{/* Per-spool low-stock threshold override (#729) */}
{ const raw = e.target.value; if (raw === '') { updateField('low_stock_threshold_pct', null); return; } const n = Number(raw); if (Number.isFinite(n)) { updateField('low_stock_threshold_pct', Math.min(99, Math.max(1, Math.round(n)))); } }} /> %

{t('inventory.lowStockThresholdOverrideHelp', { global: globalLowStockThreshold })}

{/* Note */}