feat(scheduling): rename 完成→年度达标, add sort by 客户日均/年度达标 to list
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kkfluous
2026-04-16 23:03:59 +08:00
parent 8664317852
commit 9f781c766a

View File

@@ -1,4 +1,5 @@
import { ArrowRightLeft, ChevronRight } from 'lucide-react';
import { useState, useMemo } from 'react';
import { ArrowRightLeft, ChevronRight, ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
import { motion } from 'motion/react';
import type { SchedulingSuggestion } from './types';
import Blur from '../../components/Blur';
@@ -12,7 +13,27 @@ function fmtRate(rate: number): string {
return (rate * 100).toFixed(1) + '%';
}
type SortKey = 'default' | 'avgDaily' | 'completion';
type SortDir = 'asc' | 'desc';
export default function SuggestionList({ suggestions, onSelect }: Props) {
const [sortKey, setSortKey] = useState<SortKey>('default');
const [sortDir, setSortDir] = useState<SortDir>('desc');
const toggleSort = (key: SortKey) => {
if (sortKey === key) { setSortDir(d => d === 'desc' ? 'asc' : 'desc'); }
else { setSortKey(key); setSortDir('desc'); }
};
const sorted = useMemo(() => {
if (sortKey === 'default') return suggestions;
return [...suggestions].sort((a, b) => {
const va = sortKey === 'avgDaily' ? a.currentVehicle.customerAvgDaily : a.currentVehicle.completionRate;
const vb = sortKey === 'avgDaily' ? b.currentVehicle.customerAvgDaily : b.currentVehicle.completionRate;
return sortDir === 'desc' ? vb - va : va - vb;
});
}, [suggestions, sortKey, sortDir]);
if (suggestions.length === 0) {
return (
<div className="py-16 text-center">
@@ -23,8 +44,33 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
}
return (
<div>
{/* Sort controls */}
<div className="px-4 py-2 border-b border-slate-50 flex items-center gap-2">
<button
onClick={() => toggleSort('avgDaily')}
className={`text-[10px] px-2 py-1 rounded-lg border flex items-center gap-1 cursor-pointer transition-colors ${
sortKey === 'avgDaily' ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-500'
}`}
>
{sortKey === 'avgDaily' && (sortDir === 'desc' ? <ArrowDown size={10} /> : <ArrowUp size={10} />)}
{sortKey !== 'avgDaily' && <ArrowUpDown size={10} />}
</button>
<button
onClick={() => toggleSort('completion')}
className={`text-[10px] px-2 py-1 rounded-lg border flex items-center gap-1 cursor-pointer transition-colors ${
sortKey === 'completion' ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-500'
}`}
>
{sortKey === 'completion' && (sortDir === 'desc' ? <ArrowDown size={10} /> : <ArrowUp size={10} />)}
{sortKey !== 'completion' && <ArrowUpDown size={10} />}
</button>
</div>
<div className="divide-y divide-slate-50">
{suggestions.map((s, idx) => {
{sorted.map((s, idx) => {
const isRescue = s.type === 'rescue_hopeless';
const v = s.currentVehicle;
@@ -60,7 +106,7 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
{v.manager && <span className="flex-shrink-0 text-slate-500">{v.manager}</span>}
<span className="truncate max-w-[35%] flex-shrink"><Blur>{v.customer || '-'}</Blur></span>
<span className="flex-shrink-0"> <span className="text-slate-600 font-medium">{Math.round(v.customerAvgDaily)}</span></span>
<span className="flex-shrink-0"> <span className={`font-medium ${v.completionRate >= 1 ? 'text-emerald-600' : v.completionRate >= 0.5 ? 'text-amber-600' : 'text-rose-500'}`}>{fmtRate(v.completionRate)}</span></span>
<span className="flex-shrink-0"> <span className={`font-medium ${v.completionRate >= 1 ? 'text-emerald-600' : v.completionRate >= 0.5 ? 'text-amber-600' : 'text-rose-500'}`}>{fmtRate(v.completionRate)}</span></span>
</div>
</div>
@@ -73,5 +119,6 @@ export default function SuggestionList({ suggestions, onSelect }: Props) {
);
})}
</div>
</div>
);
}