feat(scheduling): batch filter as multi-select pills instead of dropdown
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-04-16 22:54:01 +08:00
parent 9012a955b8
commit a52a77f3a2

View File

@@ -28,7 +28,7 @@ function fmtRate(rate: number): string {
export default function SuggestionDetail({ suggestion: s, onClose, onNotifySuccess }: Props) { export default function SuggestionDetail({ suggestion: s, onClose, onNotifySuccess }: Props) {
const [previewCandidate, setPreviewCandidate] = useState<CandidateVehicle | null>(null); const [previewCandidate, setPreviewCandidate] = useState<CandidateVehicle | null>(null);
const [sentPlates, setSentPlates] = useState<Set<string>>(new Set()); const [sentPlates, setSentPlates] = useState<Set<string>>(new Set());
const [batchFilter, setBatchFilter] = useState<string>(''); const [batchFilter, setBatchFilter] = useState<Set<string>>(new Set());
const [sortKey, setSortKey] = useState<SortKey>('predicted'); const [sortKey, setSortKey] = useState<SortKey>('predicted');
const [sortDir, setSortDir] = useState<SortDir>('desc'); const [sortDir, setSortDir] = useState<SortDir>('desc');
@@ -45,7 +45,7 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
// Filtered + sorted candidates // Filtered + sorted candidates
const displayCandidates = useMemo(() => { const displayCandidates = useMemo(() => {
let list = s.candidates; let list = s.candidates;
if (batchFilter) list = list.filter(c => c.targetName === batchFilter); if (batchFilter.size > 0) list = list.filter(c => c.targetName != null && batchFilter.has(c.targetName));
return [...list].sort((a, b) => { return [...list].sort((a, b) => {
const va = sortKey === 'predicted' ? a.predictedAfterSwap : a.totalMileage; const va = sortKey === 'predicted' ? a.predictedAfterSwap : a.totalMileage;
const vb = sortKey === 'predicted' ? b.predictedAfterSwap : b.totalMileage; const vb = sortKey === 'predicted' ? b.predictedAfterSwap : b.totalMileage;
@@ -164,15 +164,35 @@ export default function SuggestionDetail({ suggestion: s, onClose, onNotifySucce
{/* Filter + Sort controls */} {/* Filter + Sort controls */}
<div className="flex items-center gap-2 mb-2.5 flex-wrap"> <div className="flex items-center gap-2 mb-2.5 flex-wrap">
{/* Batch filter */} {/* Batch multi-select pills */}
<select <div className="flex items-center gap-1.5 flex-wrap">
value={batchFilter} <button
onChange={e => setBatchFilter(e.target.value)} onClick={() => setBatchFilter(new Set())}
className="text-[10px] px-2 py-1 rounded-lg border border-slate-200 bg-white text-slate-600 cursor-pointer outline-none" className={`text-[10px] px-2 py-1 rounded-lg border cursor-pointer transition-colors ${
batchFilter.size === 0 ? 'border-blue-300 bg-blue-50 text-blue-700 font-bold' : 'border-slate-200 text-slate-500'
}`}
> >
<option value=""></option>
{batchOptions.map(b => <option key={b} value={b}>{b}</option>)} </button>
</select> {batchOptions.map(b => {
const active = batchFilter.has(b);
return (
<button
key={b}
onClick={() => setBatchFilter(prev => {
const next = new Set(prev);
if (active) next.delete(b); else next.add(b);
return next;
})}
className={`text-[10px] px-2 py-1 rounded-lg border cursor-pointer transition-colors ${
active ? 'border-blue-300 bg-blue-50 text-blue-700 font-bold' : 'border-slate-200 text-slate-500'
}`}
>
{b}
</button>
);
})}
</div>
{/* Sort buttons */} {/* Sort buttons */}
<button <button