import React from "react"; import { formatDateTime } from "../../utils/formatters"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; import { Select } from "../UI/Select"; import { Panel } from "../UI/Panel"; import { DriverShipmentPanel } from "../driver/DriverShipmentPanel"; import { getOrderGroupDeliveryStatusLabel, getOrderGroupDisplayStatusLabel, getOrderGroupStatusTone, } from "../../services/orderGroupViews"; const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"]; const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"]; const DELIVERY_TIME_ALIASES = { "До обеда": "Первая половина дня", "После обеда": "Вторая половина дня", }; const renderList = (values) => { if (!Array.isArray(values) || !values.length) { return

Нет данных

; } return (
{values.map((value, index) => ( {value} ))}
); }; const renderValue = (value) => value || "Нет данных"; const parseOrderList = (order) => { if (!order) return []; // Try orderList first (Supabase JSONB array of positions) if (order.orderList) { let parsed = order.orderList; if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { /* ignore */ } } if (Array.isArray(parsed)) return parsed; } // Fallback: orderListStructured (JSONB with { orders: [...] }) if (order.orderListStructured) { let parsed = order.orderListStructured; if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { /* ignore */ } } if (parsed && Array.isArray(parsed.orders)) return parsed.orders; } // Fallback: sourceOrders (1C exchange data) if (order.sourceOrders) { let parsed = order.sourceOrders; if (typeof parsed === 'string') { try { parsed = JSON.parse(parsed); } catch { /* ignore */ } } if (Array.isArray(parsed) && parsed.length > 0) { if (parsed[0].orderList && Array.isArray(parsed[0].orderList)) { return parsed[0].orderList; } return parsed; } } return []; }; const getErrorMessage = (error, fallbackMessage) => { if (!error) { return fallbackMessage; } if (error instanceof Error) { return error.message || fallbackMessage; } if (typeof error === "string") { return error || fallbackMessage; } return error?.message || fallbackMessage; }; const normalizeDeliveryTimeChoice = (value) => { const normalized = value ? String(value).trim() : ""; const deliveryTime = DELIVERY_TIME_ALIASES[normalized] || normalized; return DELIVERY_TIME_OPTIONS.includes(deliveryTime) ? deliveryTime : DELIVERY_TIME_OPTIONS[0]; }; const toDateKey = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; }; const fromDateKey = (value) => { const normalized = normalizeDateForInput(value); if (!normalized) { return null; } const [year, month, day] = normalized.split("-").map(Number); return new Date(year, month - 1, day); }; const addDays = (date, amount) => { const nextDate = new Date(date); nextDate.setDate(nextDate.getDate() + amount); return nextDate; }; const isWeekendDate = (date) => { const day = date.getDay(); return day === 0 || day === 6; }; export const getNextSelectableDateKey = (referenceDate = new Date()) => { let current = addDays(referenceDate, 1); while (isWeekendDate(current)) { current = addDays(current, 1); } return toDateKey(current); }; const normalizePhoneForTel = (phone) => { const cleaned = String(phone || "").trim(); if (!cleaned) return ""; if (cleaned.startsWith("+7")) return cleaned; if (cleaned.startsWith("8")) return "+7" + cleaned.slice(1); return "+7" + cleaned; }; const isFutureDeliveryDate = (value) => { const parsedDate = fromDateKey(value); if (!parsedDate) { return false; } return !isWeekendDate(parsedDate) && toDateKey(parsedDate) >= getNextSelectableDateKey(); }; const isSelectableCalendarDate = (date, minDateKey) => { const dateKey = toDateKey(date); return dateKey >= minDateKey && !isWeekendDate(date); }; const formatDateForDisplay = (value) => { if (!value) { return "Выберите дату"; } const [year, month, day] = value.split("-").map(Number); if (!year || !month || !day) { return value; } return new Date(year, month - 1, day).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", }); }; const formatDeliveryDateDisplay = (value) => { const normalized = normalizeDateForInput(value); if (!normalized) { return renderValue(value); } return formatDateForDisplay(normalized); }; const startOfMonth = (date) => new Date(date.getFullYear(), date.getMonth(), 1); const addMonths = (date, amount) => new Date(date.getFullYear(), date.getMonth() + amount, 1); const buildCalendarDays = (currentMonth) => { const firstDay = startOfMonth(currentMonth); const lastDay = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0); const firstWeekDay = (firstDay.getDay() + 6) % 7; const totalDays = lastDay.getDate(); const cells = []; for (let index = 0; index < firstWeekDay; index += 1) { cells.push(null); } for (let day = 1; day <= totalDays; day += 1) { cells.push(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)); } while (cells.length % 7 !== 0) { cells.push(null); } return cells; }; const normalizeDateForInput = (value) => { if (!value) { return ""; } const normalized = String(value).trim(); if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) { return normalized; } const shortDateMatch = normalized.match(/^(\d{2})\.(\d{2})\.(\d{2})$/); if (shortDateMatch) { const [, day, month, year] = shortDateMatch; return `20${year}-${month}-${day}`; } return ""; }; const CollapsibleOrderComposition = ({ order }) => { const [isExpanded, setIsExpanded] = React.useState(false); const orders = parseOrderList(order); const totalPositions = orders.reduce((sum, o) => sum + (o.items?.length || 0), 0); return (
{isExpanded && (
{!orders.length ? (

Позиции не указаны

) : ( orders.map((orderItem, idx) => (

{orderItem.nom || orderItem.name || `Заказ ${idx + 1}`}

{orderItem.items && orderItem.items.length > 0 ? (
{orderItem.items.map((item, itemIdx) => (
{item.product_name || item.name || item.title || ''} {item.product_quantity || item.quantity || item.count || item.amount || ""} {item.product_ed || item.unit || ""}
))}
) : (

Позиции не указаны

)}
)) )}
)}
); }; const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoice, setFormMessage }) => { const [showConfirm, setShowConfirm] = React.useState(false); const isPaidStorage = (order.deliveryStatus || order.delivery_status) === "paid_storage"; if (isPaidStorage) { return (
Платное хранение
{order.paidStorageAt && (

Переведено: {formatDateTime(order.paidStorageAt)}

)}
); } return (
Платное хранение

Переведите заказ в статус платного хранения, если клиент не забрал товар в срок.

{showConfirm ? (

Перевести заказ в платное хранение? Клиент получит уведомление.

) : ( )}
); }; export const OrderDetailPanel = ({ order, canManageDelivery = false, onSaveManualDeliveryChoice, isSavingDeliveryChoice = false, drivers = [], onAssignDriver, onChangeDeliveryStatus, userRole, }) => { const [deliveryDate, setDeliveryDate] = React.useState(""); const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]); const [formMessage, setFormMessage] = React.useState(""); const [shipmentState, setShipmentState] = React.useState(null); const [isCalendarOpen, setIsCalendarOpen] = React.useState(false); const [driverMessage, setDriverMessage] = React.useState(""); const [selectedDriverId, setSelectedDriverId] = React.useState(order?.assignedDriverId || ""); const minSelectableDateKey = React.useMemo(() => getNextSelectableDateKey(), []); const [currentMonth, setCurrentMonth] = React.useState(() => { const existingDeliveryDate = fromDateKey(order?.deliveryDate); const fallbackDate = fromDateKey(minSelectableDateKey) || new Date(); const sourceDate = existingDeliveryDate && isFutureDeliveryDate(toDateKey(existingDeliveryDate)) ? existingDeliveryDate : fallbackDate; return startOfMonth(sourceDate); }); const calendarDays = React.useMemo(() => buildCalendarDays(currentMonth), [currentMonth]); const monthLabel = React.useMemo( () => currentMonth.toLocaleDateString("ru-RU", { month: "long", year: "numeric", }), [currentMonth], ); const canGoBack = toDateKey(currentMonth) > toDateKey(startOfMonth(fromDateKey(minSelectableDateKey) || new Date())); React.useEffect(() => { setSelectedDriverId(order?.assignedDriverId || ""); }, [order?.assignedDriverId]); React.useEffect(() => { const normalizedDeliveryDate = normalizeDateForInput(order?.deliveryDate); const nextSelectableDateKey = getNextSelectableDateKey(); const selectedDateKey = isFutureDeliveryDate(normalizedDeliveryDate) ? normalizedDeliveryDate : nextSelectableDateKey; setDeliveryDate(selectedDateKey); const selectedDate = fromDateKey(selectedDateKey) || new Date(); setCurrentMonth(startOfMonth(selectedDate)); setDeliveryTime(normalizeDeliveryTimeChoice(order?.deliveryTime || order?.deliveryHalfDay)); setFormMessage(""); }, [order?.id, order?.deliveryDate, order?.deliveryHalfDay, order?.deliveryTime]); if (!order) { return (

Выберите группу для просмотра деталей.

); } const isDeliveryAgreed = (order.deliveryStatus || order.delivery_status) === "agreed"; const agreedDeliveryLabel = [ formatDeliveryDateDisplay(order.deliveryDate), order.deliveryTime || order.deliveryHalfDay, ].filter((value) => value && value !== "Нет данных").join(" · "); const handleShipmentChange = React.useCallback((state) => { setShipmentState(state); }, []); const handleSaveDeliveryChoice = async () => { if (!deliveryDate || !deliveryTime) { setFormMessage("Укажите дату и половину дня доставки."); return; } if (!isFutureDeliveryDate(deliveryDate)) { setFormMessage("Выберите дату доставки позже сегодняшнего дня."); return; } try { const result = await onSaveManualDeliveryChoice?.({ orderGroupId: order.id, deliveryDate, deliveryTime, }); if (result?.success) { setFormMessage("Доставка согласована вручную."); return; } setFormMessage(getErrorMessage(result?.error, "Не удалось сохранить согласование доставки.")); } catch (error) { setFormMessage(getErrorMessage(error, "Не удалось сохранить согласование доставки.")); } }; const handleAssignDriver = async () => { if (!selectedDriverId) { setDriverMessage("Выберите водителя"); return; } if (!order.deliveryDate) { setDriverMessage("Сначала укажите дату и время доставки."); return; } setDriverMessage(""); const response = await onAssignDriver({ orderGroupId: order.id, driverId: selectedDriverId, }); if (!response.success) { setDriverMessage(response.error || "Не удалось назначить водителя"); } else { setDriverMessage("Водитель назначен"); } }; return (

Карточка группы доставки

{order.displayTitle || order.customerName || order.groupKey}

{order.displaySubtitle || [order.customerPhone, order.customerDate].filter(Boolean).join(" · ") || "Не указано"}

{getOrderGroupDisplayStatusLabel(order)}

Дата доставки

{formatDeliveryDateDisplay(order.deliveryDate)}

Время доставки

{renderValue(order.deliveryTime || order.deliveryHalfDay)}

Водитель

{order.assignedDriverId ? renderValue(order.assignedDriverName) : "Не назначен"}

Телефон

{renderValue(order.customerPhone)}

Адрес доставки

{renderValue(order.deliveryAddress)}

Номер счёта

{renderValue(order.orderNumberSummary)}

Клиент

{renderValue(order.customerName)}

Дата счёта

{renderValue(order.customerDate)}

Всего заказов

{order.ordersCount ?? 0}

Готово

{order.readyCount ?? 0}

Не готово

{order.notReadyCount ?? 0}

Обновлена

{formatDateTime(order.updatedAt)}

Статус доставки

{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}

{canManageDelivery ? (
Ручное согласование доставки

{isDeliveryAgreed ? "Дата и половина дня доставки уже зафиксированы." : "Если клиент согласовал доставку по телефону, сохраните дату и половину дня здесь."}

{isDeliveryAgreed ? (

Доставка согласована

{agreedDeliveryLabel || "Дата и время сохранены"}

Согласовано
) : (
{isCalendarOpen ? (

Календарь доставки

{monthLabel}

{WEEK_DAY_LABELS.map((day) => (
{day}
))}
{calendarDays.map((day, index) => { if (!day) { return
; } const dateKey = toDateKey(day); const isWeekend = isWeekendDate(day); const isSelectable = isSelectableCalendarDate(day, minSelectableDateKey); const isSelected = dateKey === deliveryDate; const isDisabled = !isSelectable; const dayNumber = String(day.getDate()).padStart(2, "0"); return ( ); })}

Выходные отмечены пунктиром и недоступны.

) : null}
{DELIVERY_TIME_OPTIONS.map((option) => ( ))}
)} {formMessage ? (

{formMessage}

) : null} ) : null} {canManageDelivery && ["manager", "logistician", "admin"].includes(userRole) ? (
Назначение водителя

{order.assignedDriverId ? `Назначен водитель: ${order.assignedDriverName || "Неизвестно"}. Вы можете изменить назначение.` : "Выберите водителя для доставки."}

{driverMessage ? (

{driverMessage}

) : null}
) : null} {["manager", "logistician", "admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
Статус доставки

Измените статус, если водитель забыл обновить или нужна корректировка.

{[ { value: "pending_confirmation", label: "Ожидает согласования" }, { value: "agreed", label: "Согласовано" }, { value: "driver_assigned", label: "Назначен водитель" }, { value: "loaded", label: "Загружено" }, { value: "on_route", label: "В пути" }, { value: "delivered", label: "Доставлено" }, { value: "problem", label: "Проблема" }, { value: "cancelled", label: "Отменено" }, ].map((statusOption) => ( ))}
) : null} {["manager", "logistician", "admin"].includes(userRole) && order && onChangeDeliveryStatus ? ( ) : null} {userRole === "driver" && order ? ( ) : null} {userRole === "driver" && order && onChangeDeliveryStatus ? (
Статус доставки

Обновите статус по мере выполнения доставки.

{[ { value: "loaded", label: "Загружено" }, { value: "on_route", label: "В пути" }, { value: "delivered", label: "Доставлено" }, { value: "problem", label: "Проблема" }, ].map((statusOption) => ( ))}
{formMessage ? (

{formMessage}

) : null}
) : null} Номера заказов {renderList(order.orderNumbers)} {userRole !== "driver" ? ( Дополнительные данные
{order.firstSmsSentAt ? (

1-е SMS отправлено

{formatDateTime(order.firstSmsSentAt)}

) : null} {order.secondSmsSentAt ? (

2-е SMS отправлено

{formatDateTime(order.secondSmsSentAt)}

) : null} {!order.firstSmsSentAt && !order.secondSmsSentAt ? (

SMS отправлено

Нет

) : null}

Ручное согласование выполнено

{order.manualConfirmationAt ? formatDateTime(order.manualConfirmationAt) : "Нет"}

Платное хранение

{order.paidStorageAt ? formatDateTime(order.paidStorageAt) : "Нет"}

{order.createdFromExchangeAt ? (

Создано из обмена

{formatDateTime(order.createdFromExchangeAt)}

) : null}
) : null}
); };