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} /> ); })}
)}
); };