feat(logistics): sort every table column

This commit is contained in:
root 2026-08-06 07:01:15 +00:00
parent 24e708901a
commit 63a59d42bb
1 changed files with 73 additions and 39 deletions

View File

@ -55,7 +55,7 @@ const DEFAULT_FUNNEL_ORDER = [
const STORAGE_KEY = "logistics-section-order"; const STORAGE_KEY = "logistics-section-order";
const COLLAPSED_KEY = "logistics-section-collapsed"; const COLLAPSED_KEY = "logistics-section-collapsed";
const PAGE_KEY = "logistics-board-page"; const PAGE_KEY = "logistics-board-page";
const SORT_KEY = "logistics-board-created-sort"; const SORT_KEY = "logistics-board-sort";
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
// Load custom order from localStorage, merge with defaults // Load custom order from localStorage, merge with defaults
@ -102,25 +102,35 @@ const saveCollapsedSections = (collapsedSet) => {
const COLS = "grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(100px,0.8fr)_minmax(100px,1fr)_minmax(90px,1fr)_minmax(100px,1fr)_minmax(90px,0.8fr)_minmax(120px,1fr)]"; const COLS = "grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(100px,0.8fr)_minmax(100px,1fr)_minmax(90px,1fr)_minmax(100px,1fr)_minmax(90px,0.8fr)_minmax(120px,1fr)]";
const MIN_W = "min-w-[1160px]"; const MIN_W = "min-w-[1160px]";
const TableHeader = ({ createdSort, onToggleCreatedSort }) => ( const TABLE_COLUMNS = [
<div className={`grid ${COLS} gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.12em] text-[var(--color-text-muted)]`}> { key: "customer", label: "Клиент" },
<div className="px-3 py-1.5 font-medium">Клиент</div> { key: "city", label: "Город" },
<div className="px-3 py-1.5 font-medium">Город</div> { key: "deliveryType", label: "Тип" },
<div className="px-3 py-1.5 font-medium">Тип</div> { key: "deliveryDate", label: "Дата доставки" },
<div className="px-3 py-1.5 font-medium">Дата доставки</div> { key: "driver", label: "Водитель" },
<div className="px-3 py-1.5 font-medium">Водитель</div> { key: "status", label: "Статус" },
<div className="px-3 py-1.5 font-medium">Статус</div> { key: "updatedAt", label: "Обновлён" },
<div className="px-3 py-1.5 font-medium">Обновлён</div> { key: "createdAt", label: "Добавлен в базу" },
<button ];
type="button"
onClick={onToggleCreatedSort} const TableHeader = ({ sort, onSort }) => (
className="flex items-center gap-1 px-3 py-1.5 text-left font-medium transition hover:text-[var(--color-text)]" <div className={`grid ${COLS} gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-[11px] text-[var(--color-text-muted)]`}>
title={createdSort === "asc" ? "Сейчас старые сверху. Нажмите: свежие сверху" : "Сейчас свежие сверху. Нажмите: старые сверху"} {TABLE_COLUMNS.map((column) => {
aria-label={createdSort === "asc" ? "Добавлен в базу: старые сверху" : "Добавлен в базу: свежие сверху"} const active = sort.key === column.key;
> return (
<span>Добавлен в базу</span> <button
<span aria-hidden="true">{createdSort === "asc" ? "↑" : "↓"}</span> key={column.key}
</button> type="button"
onClick={() => onSort(column.key)}
className={`flex min-w-0 items-center gap-1 whitespace-nowrap px-3 py-1.5 text-left font-medium transition hover:text-[var(--color-text)] ${active ? "text-[var(--color-text)]" : ""}`}
title={`Сортировать: ${column.label.toLowerCase()}`}
aria-label={`Сортировать по колонке «${column.label}»`}
>
<span className="truncate">{column.label}</span>
<span className="flex-shrink-0" aria-hidden="true">{active ? (sort.direction === "asc" ? "↑" : "↓") : "↕"}</span>
</button>
);
})}
</div> </div>
); );
@ -173,7 +183,7 @@ const renderRow = (g, onSelectSet) => {
</div> </div>
<div className="px-3 py-2.5 text-sm text-[var(--color-text-muted)]">{g.city || "—"}</div> <div className="px-3 py-2.5 text-sm text-[var(--color-text-muted)]">{g.city || "—"}</div>
<div className="px-3 py-2.5 text-xs"> <div className="px-3 py-2.5 text-xs">
<span className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium ${ <span className={`inline-flex items-center gap-1 whitespace-nowrap rounded-full px-2 py-0.5 font-medium ${
g.deliveryType === "pickup" g.deliveryType === "pickup"
? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]" ? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]"
: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]" : "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]"
@ -198,7 +208,7 @@ const renderRow = (g, onSelectSet) => {
); );
}; };
const SortableSection = ({ statusValue, label, groups, isCollapsed, onToggle, onSelectSet, createdSort, onToggleCreatedSort }) => { const SortableSection = ({ statusValue, label, groups, isCollapsed, onToggle, onSelectSet, sort, onSort }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: statusValue }); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: statusValue });
const style = { const style = {
@ -236,7 +246,7 @@ const SortableSection = ({ statusValue, label, groups, isCollapsed, onToggle, on
{!isCollapsed && ( {!isCollapsed && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className={MIN_W}> <div className={MIN_W}>
<TableHeader createdSort={createdSort} onToggleCreatedSort={onToggleCreatedSort} /> <TableHeader sort={sort} onSort={onSort} />
{groups.map((g) => renderRow(g, onSelectSet))} {groups.map((g) => renderRow(g, onSelectSet))}
</div> </div>
</div> </div>
@ -275,17 +285,20 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
const custom = loadCustomOrder(); const custom = loadCustomOrder();
return custom || [...DEFAULT_FUNNEL_ORDER]; return custom || [...DEFAULT_FUNNEL_ORDER];
}); });
const [createdSort, setCreatedSort] = React.useState(() => { const [sort, setSort] = React.useState(() => {
try { try {
return localStorage.getItem(SORT_KEY) === "asc" ? "asc" : "desc"; const saved = JSON.parse(localStorage.getItem(SORT_KEY) || "null");
} catch { if (saved?.key && ["asc", "desc"].includes(saved.direction)) return saved;
return "desc"; } catch {}
} return { key: "createdAt", direction: "desc" };
}); });
const toggleCreatedSort = React.useCallback(() => { const handleSort = React.useCallback((key) => {
setCreatedSort((current) => { setSort((current) => {
const next = current === "desc" ? "asc" : "desc"; const next = {
try { localStorage.setItem(SORT_KEY, next); } catch {} key,
direction: current.key === key && current.direction === "asc" ? "desc" : "asc",
};
try { localStorage.setItem(SORT_KEY, JSON.stringify(next)); } catch {}
return next; return next;
}); });
setPage(1); setPage(1);
@ -320,13 +333,34 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
[filters, orderGroups], [filters, orderGroups],
); );
const rankedGroups = React.useMemo(() => { const rankedGroups = React.useMemo(() => {
const direction = createdSort === "asc" ? 1 : -1; const direction = sort.direction === "asc" ? 1 : -1;
const text = (value) => String(value || "").trim().toLocaleLowerCase("ru");
const time = (value) => {
const parsed = value ? new Date(value).getTime() : 0;
return Number.isFinite(parsed) ? parsed : 0;
};
const valueFor = (group) => {
switch (sort.key) {
case "customer": return text(group.customerName || group.groupKey);
case "city": return text(group.city);
case "deliveryType": return text(group.deliveryType === "pickup" ? "Самовывоз" : "Доставка");
case "deliveryDate": return time(group.deliveryDate);
case "driver": return text(group.assignedDriverName);
case "status": return text(getOrderGroupDisplayStatusLabel(group));
case "updatedAt": return time(group.updatedAt);
case "createdAt":
default: return time(group.createdAt);
}
};
return [...filteredGroups].sort((a, b) => { return [...filteredGroups].sort((a, b) => {
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0; const aValue = valueFor(a);
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0; const bValue = valueFor(b);
return (aTime - bTime) * direction; const result = typeof aValue === "string"
? aValue.localeCompare(bValue, "ru", { numeric: true, sensitivity: "base" })
: aValue - bValue;
return result * direction;
}); });
}, [filteredGroups, createdSort]); }, [filteredGroups, sort]);
// Determine if filters are active if so, show all results (no pagination) // Determine if filters are active if so, show all results (no pagination)
const hasActiveFilters = isFilterActive(filters); const hasActiveFilters = isFilterActive(filters);
@ -474,8 +508,8 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
}); });
}} }}
onSelectSet={onSelectSet} onSelectSet={onSelectSet}
createdSort={createdSort} sort={sort}
onToggleCreatedSort={toggleCreatedSort} onSort={handleSort}
/> />
); );
})} })}