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 COLLAPSED_KEY = "logistics-section-collapsed";
const PAGE_KEY = "logistics-board-page";
const SORT_KEY = "logistics-board-created-sort";
const SORT_KEY = "logistics-board-sort";
const PAGE_SIZE = 20;
// 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 MIN_W = "min-w-[1160px]";
const TableHeader = ({ createdSort, onToggleCreatedSort }) => (
<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)]`}>
<div className="px-3 py-1.5 font-medium">Клиент</div>
<div className="px-3 py-1.5 font-medium">Город</div>
<div className="px-3 py-1.5 font-medium">Тип</div>
<div className="px-3 py-1.5 font-medium">Дата доставки</div>
<div className="px-3 py-1.5 font-medium">Водитель</div>
<div className="px-3 py-1.5 font-medium">Статус</div>
<div className="px-3 py-1.5 font-medium">Обновлён</div>
<button
type="button"
onClick={onToggleCreatedSort}
className="flex items-center gap-1 px-3 py-1.5 text-left font-medium transition hover:text-[var(--color-text)]"
title={createdSort === "asc" ? "Сейчас старые сверху. Нажмите: свежие сверху" : "Сейчас свежие сверху. Нажмите: старые сверху"}
aria-label={createdSort === "asc" ? "Добавлен в базу: старые сверху" : "Добавлен в базу: свежие сверху"}
>
<span>Добавлен в базу</span>
<span aria-hidden="true">{createdSort === "asc" ? "↑" : "↓"}</span>
</button>
const TABLE_COLUMNS = [
{ key: "customer", label: "Клиент" },
{ key: "city", label: "Город" },
{ key: "deliveryType", label: "Тип" },
{ key: "deliveryDate", label: "Дата доставки" },
{ key: "driver", label: "Водитель" },
{ key: "status", label: "Статус" },
{ key: "updatedAt", label: "Обновлён" },
{ key: "createdAt", label: "Добавлен в базу" },
];
const TableHeader = ({ sort, onSort }) => (
<div className={`grid ${COLS} gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-[11px] text-[var(--color-text-muted)]`}>
{TABLE_COLUMNS.map((column) => {
const active = sort.key === column.key;
return (
<button
key={column.key}
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>
);
@ -173,7 +183,7 @@ const renderRow = (g, onSelectSet) => {
</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">
<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"
? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]"
: "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 style = {
@ -236,7 +246,7 @@ const SortableSection = ({ statusValue, label, groups, isCollapsed, onToggle, on
{!isCollapsed && (
<div className="overflow-x-auto">
<div className={MIN_W}>
<TableHeader createdSort={createdSort} onToggleCreatedSort={onToggleCreatedSort} />
<TableHeader sort={sort} onSort={onSort} />
{groups.map((g) => renderRow(g, onSelectSet))}
</div>
</div>
@ -275,17 +285,20 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
const custom = loadCustomOrder();
return custom || [...DEFAULT_FUNNEL_ORDER];
});
const [createdSort, setCreatedSort] = React.useState(() => {
const [sort, setSort] = React.useState(() => {
try {
return localStorage.getItem(SORT_KEY) === "asc" ? "asc" : "desc";
} catch {
return "desc";
}
const saved = JSON.parse(localStorage.getItem(SORT_KEY) || "null");
if (saved?.key && ["asc", "desc"].includes(saved.direction)) return saved;
} catch {}
return { key: "createdAt", direction: "desc" };
});
const toggleCreatedSort = React.useCallback(() => {
setCreatedSort((current) => {
const next = current === "desc" ? "asc" : "desc";
try { localStorage.setItem(SORT_KEY, next); } catch {}
const handleSort = React.useCallback((key) => {
setSort((current) => {
const next = {
key,
direction: current.key === key && current.direction === "asc" ? "desc" : "asc",
};
try { localStorage.setItem(SORT_KEY, JSON.stringify(next)); } catch {}
return next;
});
setPage(1);
@ -320,13 +333,34 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
[filters, orderGroups],
);
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) => {
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return (aTime - bTime) * direction;
const aValue = valueFor(a);
const bValue = valueFor(b);
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)
const hasActiveFilters = isFilterActive(filters);
@ -474,8 +508,8 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
});
}}
onSelectSet={onSelectSet}
createdSort={createdSort}
onToggleCreatedSort={toggleCreatedSort}
sort={sort}
onSort={handleSort}
/>
);
})}