import React from "react"; import { Navigate } from "react-router-dom"; import { ORDER_STATUSES } from "../constants/orderStatuses"; import { ROLE_LABELS, ROLE_PERMISSIONS } from "../constants/roles"; import { DRIVER_STATUSES, getOrderStatusComment, getStatusStageKey, LOGISTICS_STATUSES, PRODUCTION_STATUSES, } from "../constants/deliveryWorkflow"; import { AuditPanel } from "../components/admin/AuditPanel"; import { UserDirectoryPanel } from "../components/admin/UserDirectoryPanel"; import { UserOnboardingPanel } from "../components/admin/UserOnboardingPanel"; import { DeliverySetDetailPanel } from "../components/logistics/DeliverySetDetailPanel"; import { DriverDeliveryDetail } from "../components/driver/DriverDeliveryDetail"; import { DriverDeliveryPlanner } from "../components/driver/DriverDeliveryPlanner"; import { KpiCard } from "../components/dashboard/KpiCard"; import { LogisticsReadinessBoard } from "../components/logistics/LogisticsReadinessBoard"; import { ProductionQueuePanel } from "../components/dashboard/ProductionQueuePanel"; import { RoleWorkspacePanel } from "../components/dashboard/RoleWorkspacePanel"; import { BotControlPanel } from "../components/logistics/BotControlPanel"; import { OrdersCalendarView } from "../components/orders/OrdersCalendarView"; import { OrderDetailPanel } from "../components/orders/OrderDetailPanel"; import { OrderFilters } from "../components/orders/OrderFilters"; import { OrdersKanbanBoard } from "../components/orders/OrdersKanbanBoard"; import { OrdersTable } from "../components/orders/OrdersTable"; import { Badge } from "../components/UI/Badge"; import { Button } from "../components/UI/Button"; import { Modal } from "../components/UI/Modal"; import { Panel } from "../components/UI/Panel"; import { SegmentedTabs } from "../components/UI/SegmentedTabs"; import { Select } from "../components/UI/Select"; import { useAuth } from "../context/AuthContext"; import { useOrders } from "../hooks/useOrders"; import { AppShell } from "../layouts/AppShell"; import { filterDriverDeliveries, getDeliveryDay, } from "../services/driverDeliveries"; import { buildKanbanColumns, filterArchiveOrders, filterKanbanColumnsByStage, filterRegistryOrders, } from "../services/orderViews"; import { getKanbanDropResolution } from "../services/orderService"; import { formatDateTime } from "../utils/formatters"; import { resolveDraggedOrderId } from "../components/orders/ordersKanbanDrag"; export const DashboardPage = () => { const { user, signOut } = useAuth(); const userRole = user?.role; const [activeSection, setActiveSection] = React.useState("overview"); const [overviewTab, setOverviewTab] = React.useState("pulse"); const [ordersViewTab, setOrdersViewTab] = React.useState("registry"); const [isOrderModalOpen, setIsOrderModalOpen] = React.useState(false); const [isOrderWorkspaceExpanded, setIsOrderWorkspaceExpanded] = React.useState(false); const [dragOrderId, setDragOrderId] = React.useState(null); const [dropColumnKey, setDropColumnKey] = React.useState(null); const [kanbanNotice, setKanbanNotice] = React.useState(null); const [kanbanMode, setKanbanMode] = React.useState("by_stage"); const [kanbanDepartmentFilter, setKanbanDepartmentFilter] = React.useState("all"); const [kanbanSort, setKanbanSort] = React.useState("updated_desc"); const [showCompletedInRegistry, setShowCompletedInRegistry] = React.useState(false); const [showCompletedInKanban, setShowCompletedInKanban] = React.useState(false); const [selectedDeliverySet, setSelectedDeliverySet] = React.useState(null); const [driverFilters, setDriverFilters] = React.useState({ dateFrom: "", dateTo: "", city: "all", timeSlot: "all", viewMode: "active", showCompleted: false, }); const { orders, allOrders, selectedOrder, selectedOrderId, setSelectedOrderId, filters, setFilters, notifications, pushNotification, updateStatus, addChatMessage, addInternalMessage, addOrderNote, assignDriver, reassignDelivery, autoAssignLogisticians, saveDriverRouteOrder, metrics, agingAlerts, agingSummary, deliverySetBuckets, users, isSupabaseBacked, isLoading, loadError, } = useOrders(user); const canManageLogistics = userRole === "logistician" || userRole === "admin"; const productionOrders = allOrders.filter((order) => PRODUCTION_STATUSES.includes(order.status)); const logisticsOrders = allOrders.filter((order) => LOGISTICS_STATUSES.includes(order.status)); const driverOrders = userRole === "driver" ? allOrders : allOrders.filter((order) => DRIVER_STATUSES.includes(order.status)); const selectedLogisticsOrder = logisticsOrders.find((order) => order.id === selectedOrderId) || logisticsOrders[0] || null; const registryOrders = React.useMemo( () => filterRegistryOrders(orders, { includeCompleted: showCompletedInRegistry }), [orders, showCompletedInRegistry], ); const archiveOrders = React.useMemo(() => filterArchiveOrders(orders), [orders]); const driverPlannedOrders = React.useMemo( () => filterDriverDeliveries(driverOrders, driverFilters), [driverFilters, driverOrders], ); const todayKey = React.useMemo(() => new Date().toISOString().slice(0, 10), []); const tomorrowKey = React.useMemo(() => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); return tomorrow.toISOString().slice(0, 10); }, []); const driverTodayOrders = React.useMemo( () => driverOrders.filter((order) => getDeliveryDay(order) === todayKey), [driverOrders, todayKey], ); const driverTomorrowOrders = React.useMemo( () => driverOrders.filter((order) => getDeliveryDay(order) === tomorrowKey), [driverOrders, tomorrowKey], ); const driverCompletedOrders = React.useMemo( () => driverOrders.filter((order) => ["Доставлен", "Закрыт"].includes(order.status)), [driverOrders], ); const navItems = React.useMemo(() => { if (userRole === "driver") { return [ { key: "overview", label: "Обзор", description: "Краткая сводка по вашим текущим и ближайшим доставкам.", }, { key: "deliveries", label: "Мои доставки", description: "План маршрута, фильтры по дням и быстрые действия по доставкам.", badge: String(driverOrders.length), }, ]; } const base = [ { key: "overview", label: "Обзор", description: "Сводка по загрузке, событиям и проблемным точкам.", }, { key: "orders", label: "Заказы", description: "Реестр, календарь доставок, канбан, история и архив заказов.", badge: String(allOrders.length), }, ]; if (userRole === "production_lead" || userRole === "admin") { base.push({ key: "production", label: "Производство", description: "Очередь производства, приоритеты и контроль готовности.", badge: String(productionOrders.length), }); } if (userRole === "logistician" || userRole === "admin") { base.push({ key: "logistics", label: "Логистика", description: "Слоты доставки, чатботы и ручная обработка исключений.", badge: String(logisticsOrders.length), }); } if (userRole === "admin") { base.push({ key: "admin", label: "Администрирование", description: "Пользователи, аудит, ошибки интеграций и контроль доступа.", badge: String(notifications.length), }); } base.push({ key: "references", label: "Справочники", description: "Статусы заказа, роли и правила маршрутизации по процессу.", badge: String(ORDER_STATUSES.length), }); return base; }, [ allOrders.length, driverOrders.length, logisticsOrders.length, notifications.length, productionOrders.length, userRole, ]); React.useEffect(() => { if (!navItems.some((item) => item.key === activeSection)) { setActiveSection(navItems[0]?.key || "overview"); } }, [activeSection, navItems]); React.useEffect(() => { setIsOrderWorkspaceExpanded(false); }, [activeSection]); React.useEffect(() => { if (activeSection !== "orders") { setOrdersViewTab("registry"); } }, [activeSection]); const sectionMeta = navItems.find((item) => item.key === activeSection) || navItems[0]; const openOrderModal = (orderId) => { setSelectedOrderId(orderId); setIsOrderModalOpen(true); }; const handleKanbanDrop = (event, column) => { const droppedOrderId = resolveDraggedOrderId(event, dragOrderId); if (!droppedOrderId) { return; } const draggedOrder = allOrders.find((order) => order.id === droppedOrderId); if (!draggedOrder) { setDragOrderId(null); setDropColumnKey(null); return; } const isSameColumn = (kanbanMode === "by_stage" && getStatusStageKey(draggedOrder.status) === column.key) || (kanbanMode === "by_status" && draggedOrder.status === column.key); if (isSameColumn) { setDragOrderId(null); setDropColumnKey(null); return; } const { nextStatus, reason } = getKanbanDropResolution({ order: draggedOrder, column, role: user.role, }); if (!nextStatus || nextStatus === draggedOrder.status) { setKanbanNotice({ tone: "warning", title: "Перенос недоступен", description: `${draggedOrder.orderNumber}: ${reason || `для роли ${ROLE_LABELS[user.role]} этот переход сейчас недоступен`}`, }); pushNotification({ id: `notification-${Date.now()}`, type: "warning", title: "Перенос недоступен", description: `${draggedOrder.orderNumber}: ${reason || `для роли ${ROLE_LABELS[user.role]} этот переход сейчас недоступен`}`, }); setDragOrderId(null); setDropColumnKey(null); return; } setKanbanNotice(null); updateStatus(droppedOrderId, nextStatus, user.name); setDragOrderId(null); setDropColumnKey(null); }; const overviewTabs = [ { key: "pulse", label: "Пульс" }, { key: "events", label: "События" }, { key: "exceptions", label: "Исключения" }, ]; const ordersTabs = [ { key: "registry", label: "Реестр" }, { key: "calendar", label: "Календарь" }, { key: "kanban", label: "Канбан" }, { key: "history", label: "История" }, { key: "archive", label: "Архив" }, ]; const orderHistoryFeed = allOrders .flatMap((order) => order.history.map((entry) => ({ ...entry, orderNumber: order.orderNumber, customerName: order.customer.name, })), ) .sort((left, right) => new Date(right.at) - new Date(left.at)); const sortedKanbanOrders = React.useMemo(() => { const sortableOrders = [...orders]; const sorters = { updated_desc: (left, right) => new Date(right.updatedAt) - new Date(left.updatedAt), created_desc: (left, right) => new Date(right.createdAt) - new Date(left.createdAt), delivery_asc: (left, right) => new Date(left.scheduledDelivery) - new Date(right.scheduledDelivery), client_asc: (left, right) => left.customer.name.localeCompare(right.customer.name), order_asc: (left, right) => left.orderNumber.localeCompare(right.orderNumber), }; return sortableOrders.sort(sorters[kanbanSort] || sorters.updated_desc); }, [kanbanSort, orders]); const kanbanColumns = React.useMemo( () => buildKanbanColumns(sortedKanbanOrders, { includeCompleted: showCompletedInKanban, mode: kanbanMode, }), [kanbanMode, showCompletedInKanban, sortedKanbanOrders], ); const visibleKanbanColumns = React.useMemo( () => filterKanbanColumnsByStage(kanbanColumns, kanbanDepartmentFilter), [kanbanColumns, kanbanDepartmentFilter], ); const eventFeed = React.useMemo(() => { const agingEvents = agingAlerts.slice(0, 4).map((alert) => ({ id: `aging-${alert.order.id}`, title: alert.agingState === "critical" ? `Просрочен ${alert.order.orderNumber}` : `Требует внимания ${alert.order.orderNumber}`, description: `${alert.order.customer.name}: ${alert.order.status} · ${alert.statusAgeLabel}`, })); return [...agingEvents, ...notifications].slice(0, 8); }, [agingAlerts, notifications]); if (!user) { return ; } const renderOrderWorkspace = (order, isExpanded) => { if (!order) { return (

Выберите заказ из таблицы.

); } return (

{order.orderNumber}

{order.customer.name} · {order.customer.address}

{!isExpanded ? ( ) : ( )}
updateStatus(order.id, nextStatus, user.name)} onAssignDriver={(driverId) => assignDriver({ orderId: order.id, driverId, actorName: user.name })} onClientMessage={(text) => addChatMessage(order.id, { sender: "client", channel: order.customer.messenger, text, }) } onInternalMessage={(message) => addInternalMessage(order.id, message)} onOrderNote={(note) => addOrderNote(order.id, note)} users={users} />
); }; const renderActiveTab = () => { if (activeSection === "overview") { if (user.role === "driver") { return (

Как пользоваться

1. Откройте «Мои доставки» и отфильтруйте день, город и половину дня.

2. Перетащите карточки внутри дня, чтобы выстроить удобный порядок точек.

3. Откройте карточку доставки и отметьте: загружен, в пути, доставлен или проблема.

Ближайшие адреса

{driverPlannedOrders.slice(0, 3).map((order) => ( ))}
); } return (
{overviewTab === "pulse" ? (
{agingAlerts.length ? (

Контроль зависших заказов

Смотрите на заказы, которые слишком долго находятся в одном статусе.

{agingSummary.warning ? ( Требуют внимания: {agingSummary.warning} ) : null} {agingSummary.critical ? ( Просрочены: {agingSummary.critical} ) : null}
) : null}

Оперативные действия

Заказов в работе
{metrics.total}
Нужна логистика
{metrics.inLogistics}
{user.role === "admin" || user.role === "logistician" ? (
) : null}
) : null} {overviewTab === "events" ? (

Последние события

{eventFeed.map((notification) => (
{notification.title}
{notification.description}
))}
) : null} {overviewTab === "exceptions" ? (

Проблемные заказы

{allOrders .filter((order) => order.status === "Проблема доставки") .map((order) => ( ))}
) : null}
); } if (activeSection === "orders") { return (
{ordersViewTab === "registry" ? (

Реестр заказов

Основная таблица для ежедневной работы. Данные сюда поступают только из 1С.

Импорт из 1С
{isOrderWorkspaceExpanded ? ( renderOrderWorkspace(selectedOrder, true) ) : ( )}
) : null} {ordersViewTab === "calendar" ? ( ) : null} {ordersViewTab === "kanban" ? (

Канбан показывает только отфильтрованные заказы. Можно переключать вид по этапам и по статусам, а цвет карточки показывает, чья сейчас зона ответственности.

{agingSummary.warning ? ( ) : null} {agingSummary.critical ? ( ) : null}
{ setKanbanNotice(null); setDragOrderId(orderId); }} onDragEnd={() => { setDragOrderId(null); setDropColumnKey(null); }} onDragOverColumn={setDropColumnKey} onDragLeaveColumn={(columnKey) => setDropColumnKey((current) => (current === columnKey ? null : current)) } onDropColumn={handleKanbanDrop} dropColumnKey={dropColumnKey} />
) : null} {ordersViewTab === "history" ? (

История заказов по сотрудникам

Лента показывает, кто и по какому заказу менял статус или выполнял действие.

{orderHistoryFeed.map((entry) => (
{entry.orderNumber} · {entry.customerName}
{entry.userName} · {entry.action}
{formatDateTime(entry.at)}
{entry.oldStatus || "Начало"} → {entry.newStatus}
))}
) : null} {ordersViewTab === "archive" ? (

Архив заказов

Завершённые заказы вынесены отдельно, чтобы не перегружать реестр и канбан.

{archiveOrders.length}
) : null}
); } if (activeSection === "production") { return (
); } if (activeSection === "logistics") { return (
setSelectedDeliverySet(set)} /> {selectedDeliverySet ? ( setSelectedDeliverySet(null)} /> ) : null}
selectedLogisticsOrder && addChatMessage(selectedLogisticsOrder.id, message) } onReschedule={(slot) => selectedLogisticsOrder && reassignDelivery(selectedLogisticsOrder.id, slot, user.name) } />

Логика каналов

Единый интеграционный слой сохраняет входящие и исходящие события в историю заказа, а ответы клиента автоматически меняют статус согласования доставки.

СМС и электронная почта: быстрый старт для первого подтверждения доставки.

ВКонтакте: обратные вызовы и кнопки с привязкой к идентификатору заказа.

Макс: преобразование входящих событий в единую модель чата.

Заказы в логистике

Отдельные заказы из наборов доставки, где нужно согласование, перенос или ручная реакция на исключения.

{logisticsOrders.map((order) => ( ))}
); } if (activeSection === "references") { return (

Статусы заказа

{ORDER_STATUSES.map((status, index) => (
Шаг {index + 1}
{status}
{getOrderStatusComment(status)}
))}

Роли и зоны ответственности

{Object.entries(ROLE_LABELS).map(([roleKey, roleLabel]) => (
{roleLabel}
{ROLE_PERMISSIONS[roleKey].map((permission) => ( {permission} ))}
))}
); } if (activeSection === "deliveries") { return (
updateStatus(orderId, nextStatus, user.name)} onReorder={(orderedIds) => saveDriverRouteOrder({ orderedIds, actorName: user.name, }) } />
); } return (
); }; return ( {isLoading ? ( Загружаем данные из Supabase... ) : null} {isSupabaseBacked ? ( Данные загружены из Supabase, живой контур активен. ) : null} {loadError ? ( {loadError} ) : null} {renderActiveTab()} setIsOrderModalOpen(false)}> {user.role === "driver" ? (

Карточка доставки

Только нужные водителю данные: адрес, клиент, состав заказа и быстрые действия.

selectedOrder && updateStatus(selectedOrder.id, nextStatus, user.name) } users={users} />
) : (

Карточка заказа

Просмотр без потери контекста списка. При необходимости можно раскрыть в рабочую область.

{renderOrderWorkspace(selectedOrder, false)}
)}
); };