2025-11-19 16:30:16 +01:00

400 lines
14 KiB
TypeScript

"use client";
import {
Box,
TextField,
MenuItem,
Button,
Drawer,
FormControl,
Select,
Typography,
Stack,
debounce,
} from "@mui/material";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import SearchIcon from "@mui/icons-material/Search";
import RefreshIcon from "@mui/icons-material/Refresh";
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect, useMemo } from "react";
import { ISearchLabel } from "../DataTable/types";
import { AppDispatch } from "@/app/redux/store";
import {
updateFilter,
clearFilters,
FilterValue,
} from "@/app/redux/advanedSearch/advancedSearchSlice";
import { selectFilters } from "@/app/redux/advanedSearch/selectors";
import { normalizeValue, defaultOperatorForField } from "./utils/utils";
import { selectConditionOperators } from "@/app/redux/metadata/selectors";
// -----------------------------------------------------
// COMPONENT
// -----------------------------------------------------
export default function AdvancedSearch({ labels }: { labels: ISearchLabel[] }) {
const dispatch = useDispatch<AppDispatch>();
const filters = useSelector(selectFilters);
const [open, setOpen] = useState(false);
// Local form state for UI (synced with Redux)
const [formValues, setFormValues] = useState<Record<string, string>>({});
const [operators, setOperators] = useState<Record<string, string>>({});
const conditionOperators = useSelector(selectConditionOperators);
console.log("[conditionOperators]", conditionOperators);
// -----------------------------------------------------
// SYNC REDUX FILTERS TO LOCAL STATE ON LOAD
// -----------------------------------------------------
useEffect(() => {
const values: Record<string, string> = {};
const ops: Record<string, string> = {};
labels.forEach(({ field, type }) => {
const filter = filters[field];
if (filter) {
if (typeof filter === "string") {
// Simple string filter
values[field] = filter;
ops[field] = defaultOperatorForField(field, type);
} else {
// FilterValue object with operator and value
values[field] = filter.value;
ops[field] = filter.operator;
}
}
// Handle date ranges
const startKey = `${field}_start`;
const endKey = `${field}_end`;
const startFilter = filters[startKey];
const endFilter = filters[endKey];
if (startFilter && typeof startFilter === "string") {
values[startKey] = startFilter;
}
if (endFilter && typeof endFilter === "string") {
values[endKey] = endFilter;
}
});
setFormValues(values);
setOperators(ops);
}, [filters, labels]);
// -----------------------------------------------------
// DEBOUNCED FILTER UPDATE
// -----------------------------------------------------
const debouncedUpdateFilter = useMemo(
() =>
debounce(
(field: string, value: string | undefined, operator?: string) => {
if (!value || value === "") {
dispatch(updateFilter({ field, value: undefined }));
return;
}
const safeValue = normalizeValue(value);
if (!safeValue) {
dispatch(updateFilter({ field, value: undefined }));
return;
}
// For text/select fields, use FilterValue with operator
const filterValue: FilterValue = {
operator: operator ?? defaultOperatorForField(field, "text"),
value: safeValue,
};
dispatch(updateFilter({ field, value: filterValue }));
},
300
),
[dispatch]
);
// -----------------------------------------------------
// handlers
// -----------------------------------------------------
const updateField = (field: string, value: string) => {
setFormValues(prev => ({ ...prev, [field]: value }));
const operator = operators[field] ?? defaultOperatorForField(field, "text");
debouncedUpdateFilter(field, value, operator);
};
const updateOperator = (field: string, op: string) => {
setOperators(prev => ({ ...prev, [field]: op }));
// If value exists, update filter immediately with new operator
const currentValue = formValues[field];
if (currentValue) {
const safeValue = normalizeValue(currentValue);
if (safeValue) {
dispatch(
updateFilter({
field,
value: { operator: op, value: safeValue },
})
);
}
}
};
const updateDateRange = (
field: string,
start: string | undefined,
end: string | undefined
) => {
if (start) {
dispatch(updateFilter({ field: `${field}_start`, value: start }));
} else {
dispatch(updateFilter({ field: `${field}_start`, value: undefined }));
}
if (end) {
dispatch(updateFilter({ field: `${field}_end`, value: end }));
} else {
dispatch(updateFilter({ field: `${field}_end`, value: undefined }));
}
};
const resetForm = () => {
setFormValues({});
setOperators({});
dispatch(clearFilters());
};
// -----------------------------------------------------
// render
// -----------------------------------------------------
return (
<Box sx={{ width: "185px" }}>
<Button
sx={{
borderRadius: "8px",
textTransform: "none",
backgroundColor: "#f5f5f5",
color: "#555",
padding: "6px 12px",
boxShadow: "inset 0 0 0 1px #ddd",
fontWeight: 400,
fontSize: "16px",
"&:hover": { backgroundColor: "#e0e0e0" },
}}
startIcon={<SearchIcon />}
onClick={() => setOpen(true)}
>
Advanced Search
</Button>
<Drawer anchor="right" open={open} onClose={() => setOpen(false)}>
<Box sx={{ width: 400 }} p={2}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Box
sx={{ display: "flex", justifyContent: "space-between", mb: 2 }}
>
<Typography variant="h6">Search</Typography>
<Box display="flex" gap={2}>
<Button
variant="contained"
startIcon={<SearchIcon />}
onClick={() => {
// Apply all current form values to Redux
labels.forEach(({ field, type }) => {
const val = formValues[field];
if (!val) return;
if (type === "select" || type === "text") {
const operator =
operators[field] ??
defaultOperatorForField(field, type);
const safeValue = normalizeValue(val);
if (safeValue) {
dispatch(
updateFilter({
field,
value: { operator, value: safeValue },
})
);
}
}
});
}}
>
Apply
</Button>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={resetForm}
/>
</Box>
</Box>
<Stack spacing={2}>
{labels.map(({ label, field, type, options }) => (
<Box key={field}>
<Typography variant="body2" fontWeight={600} mb={0.5}>
{label}
</Typography>
{/* TEXT FIELDS */}
{type === "text" && (
<>
{/* AMOUNT WITH OPERATOR */}
{field === "Amount" ? (
<Box sx={{ display: "flex", gap: 1 }}>
<FormControl size="small" sx={{ minWidth: 130 }}>
<Select
value={operators[field] ?? ">="}
onChange={e =>
updateOperator(field, e.target.value)
}
>
{Object.entries(conditionOperators ?? {}).map(
([key, value]) => (
<MenuItem key={key} value={value}>
{key.replace(/_/g, " ")}{" "}
{/* Optional: make it readable */}
</MenuItem>
)
)}
</Select>
</FormControl>
<TextField
fullWidth
size="small"
value={formValues[field] ?? ""}
onChange={e => updateField(field, e.target.value)}
placeholder="Enter amount"
/>
</Box>
) : (
<TextField
fullWidth
size="small"
value={formValues[field] ?? ""}
onChange={e => updateField(field, e.target.value)}
/>
)}
</>
)}
{/* SELECTS */}
{type === "select" && (
<FormControl fullWidth size="small">
<Select
value={formValues[field] ?? ""}
onChange={e => updateField(field, e.target.value)}
displayEmpty
>
<MenuItem value="">
<em>{label}</em>
</MenuItem>
{options?.map(opt => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
)}
{/* DATE RANGE */}
{type === "date" && (
<Stack spacing={2}>
<DatePicker
label="Start Date"
value={
formValues[`${field}_start`]
? new Date(formValues[`${field}_start`])
: null
}
onChange={value => {
if (!value) {
updateDateRange(
field,
undefined,
formValues[`${field}_end`]
);
setFormValues(prev => ({
...prev,
[`${field}_start`]: "",
}));
return;
}
const v = new Date(value);
v.setHours(0, 0, 0, 0);
const isoString = v.toISOString();
setFormValues(prev => ({
...prev,
[`${field}_start`]: isoString,
}));
updateDateRange(
field,
isoString,
formValues[`${field}_end`]
);
}}
slotProps={{
textField: { fullWidth: true, size: "small" },
}}
/>
<DatePicker
label="End Date"
value={
formValues[`${field}_end`]
? new Date(formValues[`${field}_end`])
: null
}
onChange={value => {
if (!value) {
updateDateRange(
field,
formValues[`${field}_start`],
undefined
);
setFormValues(prev => ({
...prev,
[`${field}_end`]: "",
}));
return;
}
const v = new Date(value);
v.setHours(23, 59, 59, 999);
const isoString = v.toISOString();
setFormValues(prev => ({
...prev,
[`${field}_end`]: isoString,
}));
updateDateRange(
field,
formValues[`${field}_start`],
isoString
);
}}
slotProps={{
textField: { fullWidth: true, size: "small" },
}}
/>
</Stack>
)}
</Box>
))}
</Stack>
</LocalizationProvider>
</Box>
</Drawer>
</Box>
);
}