"use client"; import { useEffect, useMemo, useState } from "react"; import { Autocomplete, Chip, CircularProgress, TextField } from "@mui/material"; import { useTranslation } from "react-i18next"; import { searchItemCodes, type ItemCodeSearchHit } from "./itemCodeSearchApi"; type Props = { label: string; value: string[]; onChange: (codes: string[]) => void; placeholder?: string; disabled?: boolean; minChars?: number; }; const hitLabel = (hit: ItemCodeSearchHit) => hit.name ? `${hit.code} ${hit.name}` : hit.code; /** FP-MTMS Version Checklist | Functions Ref. No. 81 | v1.0.0 | 2026-09-10 */ const AsyncItemCodeAutocomplete: React.FC = ({ label, value, onChange, placeholder, disabled = false, minChars = 2, }) => { const { t } = useTranslation("report"); const [inputValue, setInputValue] = useState(""); const [suggestions, setSuggestions] = useState([]); const [labelByCode, setLabelByCode] = useState>({}); const [isSearching, setIsSearching] = useState(false); const trimmedInput = inputValue.trim(); const needsMoreChars = trimmedInput.length > 0 && trimmedInput.length < minChars; useEffect(() => { if (trimmedInput.length < minChars) { setSuggestions([]); setIsSearching(false); return; } const controller = new AbortController(); let cancelled = false; const timer = window.setTimeout(async () => { setIsSearching(true); try { const hits = await searchItemCodes(trimmedInput, controller.signal); if (cancelled) return; setSuggestions(hits); setLabelByCode((prev) => { const next = { ...prev }; hits.forEach((hit) => { next[hit.code] = hitLabel(hit); }); return next; }); } catch (error) { if (cancelled) return; if (error instanceof DOMException && error.name === "AbortError") return; setSuggestions([]); } finally { if (!cancelled) setIsSearching(false); } }, 300); return () => { cancelled = true; window.clearTimeout(timer); controller.abort(); }; }, [trimmedInput, minChars]); const options = useMemo(() => { const seen = new Set(); const codes: string[] = []; suggestions.forEach((hit) => { if (seen.has(hit.code)) return; seen.add(hit.code); codes.push(hit.code); }); value.forEach((code) => { if (seen.has(code)) return; seen.add(code); codes.push(code); }); return codes; }, [suggestions, value]); const noOptionsText = needsMoreChars ? t("typeToSearchItemCode", { min: minChars }) : isSearching ? t("searchingItemCodes") : trimmedInput.length < minChars ? t("typeToSearchItemCode", { min: minChars }) : t("noItemCodeMatches"); const hasSelection = value.length > 0; return ( trimmedInput.length < minChars ? [] : opts.filter((code) => !value.includes(code)) } isOptionEqualToValue={(option, selected) => option === selected} autoHighlight noOptionsText={noOptionsText} sx={{ '& .MuiOutlinedInput-root.MuiAutocomplete-inputRoot': hasSelection ? { alignItems: 'flex-start', alignContent: 'flex-start', flexWrap: 'wrap', minHeight: 56, paddingTop: '32px !important', paddingBottom: '8px !important', paddingLeft: '14px !important', } : { alignItems: 'center', height: 56, minHeight: 56, maxHeight: 56, boxSizing: 'border-box', paddingTop: '16.5px !important', paddingBottom: '16.5px !important', paddingLeft: '14px !important', }, '& .MuiOutlinedInput-root.MuiAutocomplete-inputRoot .MuiAutocomplete-input': { fontSize: '1rem', padding: '0 !important', }, '& .MuiOutlinedInput-root.MuiAutocomplete-inputRoot .MuiAutocomplete-input::placeholder': { color: 'text.disabled', opacity: 1, }, '& .MuiAutocomplete-tag': { flex: '1 0 100%', maxWidth: '100%', width: '100%', margin: '6px 0 4px', }, }} componentsProps={{ popper: { placement: "top-start", modifiers: [{ name: "flip", enabled: false }], }, }} onInputChange={(_, next, reason) => { if (reason === "reset") { setInputValue(""); return; } setInputValue(next); }} onChange={(_, newValue) => { const codes = (Array.isArray(newValue) ? newValue : []) .map((item) => (typeof item === "string" ? item.trim() : String(item).trim())) .filter(Boolean); onChange(Array.from(new Set(codes))); setInputValue(""); }} getOptionLabel={(option) => labelByCode[option] || option} renderTags={(selected, getTagProps) => selected.map((option, index) => ( )) } renderInput={(params) => ( {isSearching ? : null} {params.InputProps.endAdornment} ), }} /> )} /> ); }; export default AsyncItemCodeAutocomplete;