import React from "react";
import {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
filterOrderGroups,
getOrderGroupDisplayStatusLabel,
getOrderGroupDisplayStatusValue,
getOrderGroupStatusTone,
ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
} from "../../services/orderGroupViews";
import { Badge } from "../UI/Badge";
import { Panel } from "../UI/Panel";
import { SkeletonPage } from "../UI/Loading";
import { OrderFilters } from "../orders/OrderFilters";
import { formatDate, formatDateTime } from "../../utils/formatters";
const fmtDate = (d) => {
if (!d) return "";
const [y, m, day] = d.split("-");
if (!y || !m || !day) return d;
return `${day}.${m}.${y}`;
};
// Default priority: agreed first, manual_required second, then funnel
const DEFAULT_FUNNEL_ORDER = [
"delivery:agreed",
"status:manual_required",
"status:ready_for_notification",
"delivery:pending_confirmation",
"status:first_sms_sent",
"status:second_sms_sent",
"delivery:driver_assigned",
"delivery:loaded",
"delivery:on_route",
"delivery:delivered",
"delivery:picked_up",
"delivery:paid_storage",
"delivery:problem",
"delivery:cancelled",
];
const STORAGE_KEY = "logistics-section-order";
const COLLAPSED_KEY = "logistics-section-collapsed";
// Load custom order from localStorage, merge with defaults
const loadCustomOrder = () => {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
};
const saveCustomOrder = (order) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(order));
} catch {
// ignore
}
};
// Load collapsed sections from localStorage
const loadCollapsedSections = () => {
try {
const raw = localStorage.getItem(COLLAPSED_KEY);
if (!raw) return new Set();
const parsed = JSON.parse(raw);
return new Set(Array.isArray(parsed) ? parsed : []);
} catch {
return new Set();
}
};
const saveCollapsedSections = (collapsedSet) => {
try {
localStorage.setItem(COLLAPSED_KEY, JSON.stringify([...collapsedSet]));
} catch {
// ignore
}
};
// 7 columns: Клиент | Город | Тип | Дата | Водитель | Статус | Обновлён
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)]";
const MIN_W = "min-w-[1080px]";
const TableHeader = () => (
Клиент
Город
Тип
Дата доставки
Водитель
Статус
Обновлён
);
const renderRow = (group, onSelectSet) => (
);
// Sortable section wrapper
const SortableSection = ({ statusValue, label, groups, isCollapsed, onToggle, onSelectSet }) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: statusValue });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
{/* Section header — drag handle + collapse toggle */}
{/* Drag handle */}
{/* Collapse toggle */}
{!isCollapsed && (
{groups.map((g) => renderRow(g, onSelectSet))}
)}
);
};
export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS, isLoading = false }) => {
const [filters, setFilters] = React.useState({ query: "", displayStatus: "all", city: "" });
const [collapsedSections, setCollapsedSections] = React.useState(() => loadCollapsedSections());
const [sectionOrder, setSectionOrder] = React.useState(() => {
const custom = loadCustomOrder();
return custom || [...DEFAULT_FUNNEL_ORDER];
});
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
const cities = React.useMemo(() => {
const set = new Set();
for (const g of orderGroups) {
if (g.city) set.add(g.city);
}
return [...set].sort();
}, [orderGroups]);
const filteredGroups = React.useMemo(
() => filterOrderGroups(orderGroups, filters),
[filters, orderGroups],
);
const statusGroups = React.useMemo(() => {
const map = new Map();
for (const group of filteredGroups) {
const statusValue = getOrderGroupDisplayStatusValue(group);
if (!map.has(statusValue)) {
const label = getOrderGroupDisplayStatusLabel(group);
map.set(statusValue, { label, groups: [] });
}
map.get(statusValue).groups.push(group);
}
return map;
}, [filteredGroups]);
const totalGroups = filteredGroups.length;
// Build sorted list: use sectionOrder for known statuses, append unknown ones at end
const sortedEntries = React.useMemo(() => {
const present = new Set(statusGroups.keys());
const result = [];
// First: statuses in custom order that are present
for (const statusValue of sectionOrder) {
if (present.has(statusValue)) {
const data = statusGroups.get(statusValue);
result.push([statusValue, data]);
}
}
// Then: any statuses not in sectionOrder (new statuses), sorted alphabetically
for (const [statusValue, data] of statusGroups.entries()) {
if (!sectionOrder.includes(statusValue)) {
result.push([statusValue, data]);
}
}
return result;
}, [statusGroups, sectionOrder]);
const handleDragEnd = (event) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
setSectionOrder((prevOrder) => {
// Build the full order including any new statuses
const allIds = sortedEntries.map(([id]) => id);
const oldIndex = allIds.indexOf(active.id);
const newIndex = allIds.indexOf(over.id);
if (oldIndex === -1 || newIndex === -1) return prevOrder;
const newAllOrder = arrayMove(allIds, oldIndex, newIndex);
// Merge: replace positions of known statuses, keep unknown at end
// Save the full new order so it persists
saveCustomOrder(newAllOrder);
return newAllOrder;
});
};
if (isLoading) {
return ;
}
return (
Наборы доставки
Перетаскивайте секции за ручку слева, чтобы изменить порядок отображения.
{totalGroups} групп
{!totalGroups ? (
По этому поиску ничего не найдено.
) : (
id)}
strategy={verticalListSortingStrategy}
>
{sortedEntries.map(([statusValue, { label, groups }]) => {
const isCollapsed = collapsedSections.has(statusValue);
return (
{
setCollapsedSections((prev) => {
const next = new Set(prev);
if (next.has(statusValue)) {
next.delete(statusValue);
} else {
next.add(statusValue);
}
saveCollapsedSections(next);
return next;
});
}}
onSelectSet={onSelectSet}
/>
);
})}
)}
);
};