import { useState, useEffect, useMemo, useCallback } from "react"; import { TextField, Dialog, DialogTitle, DialogContent, DialogActions, Button, List, ListItemButton, ListItemIcon, ListItemText, Typography, InputAdornment, IconButton, Checkbox, CircularProgress, } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; import FilterTableBuilder from "../utils/filterTableBuilder"; import { apiRequest } from "../api/request"; import { useApiHeaders } from "./ApiContext"; export default function ListVirtual({ id, label, name, url, lengthData = 100, getData, defaultValue, value, required, disabled, error = false, size = 'medium', helperText, filter, isApiGo = false, multiple, customSxTextField = {}, joins = [], warningCheck, }) { const headersRequest = useApiHeaders(); const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const [rows, setRows] = useState([]); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(false); const [selectAllLoading, setSelectAllLoading] = useState(false); const [offset, setOffset] = useState(0); const [totalRecord, setTotalRecord] = useState(0); const debounce = (fn, delay = 400) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => { fn(...args); }, delay); }; }; const fetchData = useCallback(async (keyword = "", currentOffset = 0, isAppend = false) => { setLoading(true); const payload = new FilterTableBuilder() .setPaging(currentOffset, lengthData) .setOrder([name], true); if (keyword) payload.like(name, keyword); if (filter && filter.length > 0) { filter.forEach(f => payload.where(f.logic_operator || "=", f.name, f.value, f.operator || "AND", f.value1 || null, f.table_name || null)); } if (Array.isArray(filter) && filter.some(f => f.name === "company_id")) { payload.addJoin("company", ["name"]); } joins.forEach(join => { payload.addJoin(join.table, join.columns); }); try { const res = await apiRequest.search( url, payload.build(), { headers: headersRequest }, isApiGo, ); const incomingData = res.data || []; setRows(prev => isAppend ? [...prev, ...incomingData] : incomingData); setTotalRecord(res.totalRecord || 0); setOffset(currentOffset); } catch (err) { console.error("Fetch Error:", err); } finally { setLoading(false); } }, [url, name, lengthData, filter]); const handleScroll = (e) => { const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const isAtBottom = scrollHeight - scrollTop <= clientHeight + 20; if (isAtBottom && !loading && rows.length < totalRecord) { const nextOffset = offset + lengthData; fetchData(search, nextOffset, true); } }; const debouncedFetch = useMemo( () => debounce((keyword) => { setOffset(0); fetchData(keyword, 0, false); }, 400), [fetchData] ); useEffect(() => { if (open) { setOffset(0); fetchData(search, 0, false); } }, [open]); const fetchDefaultValue = async (value) => { setLoading(true); const payload = new FilterTableBuilder().setPaging(0, lengthData); if (multiple) { payload.in("id", value); // value = [1,2,3] } else { payload.equal("id", value); // value = 1 } try { const res = await apiRequest.search( url, payload.build(), { headers: headersRequest, }, isApiGo, ); const data = res.data || []; setSelected(multiple ? data : data[0] || null); } catch (err) { console.error(err); } finally { setLoading(false); } }; useEffect(() => { if (defaultValue == null) return; if (multiple) { // kosong, jangan request if (Array.isArray(defaultValue) && defaultValue.length === 0) { setSelected([]); return; } // object if ( Array.isArray(defaultValue) && typeof defaultValue[0] === "object" ) { setSelected(defaultValue); return; } } else { if (typeof defaultValue === "object" && !Array.isArray(defaultValue)) { setSelected(defaultValue); return; } } fetchDefaultValue(defaultValue); }, [defaultValue, multiple]); const handleSelect = (row) => { if (!multiple) { setSelected(row); setOpen(false); setSearch(""); getData?.(row); return; } setSelected((prev) => { const current = Array.isArray(prev) ? prev : []; const exists = current.some((x) => x.id === row.id); const result = exists ? current.filter((x) => x.id !== row.id) : [...current, row]; getData?.(result); return result; }); }; const handleClear = () => { setSelected(multiple ? [] : null); setSearch(""); setRows([]); getData?.(multiple ? [] : null); }; // NEW: helper untuk memanggil warningCheck dengan aman const getRowWarning = useCallback( (row) => { if (typeof warningCheck !== "function") return null; return warningCheck(row) || null; }, [warningCheck] ); const displayValue = multiple ? (selected?.length > 1 ? `${selected.length} ${label} selected` : selected?.length === 1 ? selected[0][name] : "") : selected?.[name] || ""; const isSelected = (row) => { if (!multiple) { return selected?.id === row.id; } return (selected || []).some((x) => x.id === row.id); }; // Whether every record matching the current search/filter is currently selected const isAllSelected = multiple && totalRecord > 0 && (selected?.length || 0) === totalRecord; const isPartiallySelected = multiple && (selected?.length || 0) > 0 && !isAllSelected; const handleSelectAll = async () => { // Toggle off: if everything is already selected, clicking again clears selection if (isAllSelected) { setSelected([]); getData?.([]); return; } setSelectAllLoading(true); try { const pageSize = 1000; const requests = []; for (let pageOffset = 0; pageOffset < totalRecord; pageOffset += pageSize) { const payload = new FilterTableBuilder() .setPaging(pageOffset, pageSize) .setOrder([name], true); if (search) { payload.like(name, search); } if (filter?.length) { filter.forEach(f => payload.where( f.logic_operator || "=", f.name, f.value, f.operator || "AND", f.value1 || null, f.table_name || null ) ); } joins.forEach(join => { payload.addJoin(join.table, join.columns); }); requests.push( apiRequest.search( url, payload.build(), { headers: headersRequest, }, isApiGo ) ); } const responses = await Promise.all(requests); const allRows = responses.flatMap(res => res.data || []); setSelected(allRows); getData?.(allRows); } catch (err) { console.error("Select All Error:", err); } finally { setSelectAllLoading(false); } }; return ( <> !disabled && setOpen(true)} sx={{ ...customSxTextField, '& .MuiOutlinedInput-root.Mui-error .MuiOutlinedInput-notchedOutline': { borderColor: 'error.main', } }} slotProps={{ input: { endAdornment: selected && ( { e.stopPropagation(); handleClear(); }}> ) }, formHelperText: { sx: { marginLeft: 0, } } }} /> setOpen(false)} maxWidth="sm" fullWidth> Search {label} { const val = e.target.value; setSearch(val); debouncedFetch(val); }} /> <> {multiple && rows.length > 0 && ( {selectAllLoading ? ( ) : ( )} )} {rows.map((row, i) => { const warning = getRowWarning(row); return ( handleSelect(row)} disabled={selectAllLoading} > {multiple && ( )} {row[name]} {warning && ( {" "}| {warning} )} } secondary={row.join?.company_name} /> ); })} {loading && ( Loading more... )} {!loading && rows.length === 0 && ( No Data )} ); }