feat(logistics): sort every table column
This commit is contained in:
parent
24e708901a
commit
63a59d42bb
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in New Issue