diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx
index 543e76e..bbb3cd0 100644
--- a/src/components/orders/OrderDetailPanel.jsx
+++ b/src/components/orders/OrderDetailPanel.jsx
@@ -840,8 +840,7 @@ export const OrderDetailPanel = ({
) : null}
- ) : (
-{deliveryType === "delivery" ? (
+ ) : deliveryType === "delivery" ? (
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
- )}
{formMessage ? (
{formMessage}
) : null}
diff --git a/src/components/orders/OrderDetailPanel.jsx.bak b/src/components/orders/OrderDetailPanel.jsx.bak
new file mode 100644
index 0000000..6f8bd5d
--- /dev/null
+++ b/src/components/orders/OrderDetailPanel.jsx.bak
@@ -0,0 +1,1333 @@
+
+const DriverShipmentReport = ({ shipmentData }) => {
+ if (!Array.isArray(shipmentData) || shipmentData.length === 0) return null;
+
+ return (
+
+
+
+
+
+
Проблемы с доставкой позиций
+
+
+ Не доставлено {shipmentData.length} {shipmentData.length === 1 ? "позиция" : shipmentData.length < 5 ? "позиции" : "позиций"}. Остальное — доставлено.
+
+
+ {shipmentData.map((item) => (
+
+
+ {item.name}
+ {item.quantity || item.unit ? (
+ {[item.quantity, item.unit].filter(Boolean).join(" ")}
+ ) : null}
+
+ {item.comment ? (
+
Причина: {item.comment}
+ ) : (
+
Причина не указана
+ )}
+
+ ))}
+
+
+
+ );
+};
+
+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 { supabase } from "../../supabaseClient";
+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 normalizeNom = (nom) => {
+ if (!nom) return '';
+ // 1C escapes backslashes: "СФ Т\\ЕА-33584" → normalize for comparison
+ return String(nom).replace(/\\\\/g, '\\').trim();
+};
+
+const getAllBillNumbers = (order) => {
+ const orders = parseOrderList(order);
+ if (!orders.length) return order.orderNumbers || [];
+ return orders.map((o) => o.nom || o.name || '').filter(Boolean);
+};
+
+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)
+ // 1C sends the FULL order composition (main + associated bills) in EVERY source order's orderList.
+ // We must deduplicate by nom to avoid showing the same items multiple times.
+ 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) {
+ const seen = new Set();
+ const allItems = [];
+ for (const src of parsed) {
+ if (src && Array.isArray(src.orderList)) {
+ for (const ol of src.orderList) {
+ if (ol && (ol.items || ol.nom || ol.name)) {
+ const normalizedNom = normalizeNom(ol.nom || ol.name || '');
+ // Deduplicate by nom — 1C repeats same orderList in every source order
+ if (seen.has(normalizedNom)) continue;
+ seen.add(normalizedNom);
+ allItems.push(ol);
+ }
+ }
+ }
+ }
+ if (allItems.length > 0) return allItems;
+ // Legacy: return whole array if no orderList structure
+ 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 matchesStopWord = (name, stopWords) => {
+ if (!stopWords || !stopWords.length) return false;
+ const lower = name.toLowerCase();
+ return stopWords.some((sw) => lower.includes(sw.toLowerCase()));
+};
+
+const useStopWords = () => {
+ const [stopWords, setStopWords] = React.useState([]);
+ const [active, setActive] = React.useState(true);
+ React.useEffect(() => {
+ if (!supabase) return;
+ Promise.all([
+ supabase.from("stop_words").select("word").then(r => r.data || []),
+ supabase.from("stop_words_scope").select("scope").eq("id", 1).single().then(r => r.data),
+ ]).then(([words, scopeRow]) => {
+ setStopWords(words.map((d) => d.word));
+ setActive(scopeRow?.scope !== "client_only");
+ });
+ }, []);
+ return { stopWords, active };
+};
+
+const CollapsibleOrderComposition = ({ order }) => {
+ const [isExpanded, setIsExpanded] = React.useState(false);
+ const { stopWords, active } = useStopWords();
+
+ const orders = parseOrderList(order);
+ const allPositions = orders.reduce((sum, o) => sum + (o.items?.length || 0), 0);
+ const filteredPositions = active ? orders.reduce((sum, o) => {
+ if (!o.items) return sum;
+ return sum + o.items.filter((item) => {
+ const name = String(item.product_name || item.name || item.title || "");
+ return !matchesStopWord(name, stopWords);
+ }).length;
+ }, 0) : allPositions;
+
+ return (
+
+
setIsExpanded(!isExpanded)}
+ >
+ Состав заказа
+
+{active && filteredPositions < allPositions
+ ? `${filteredPositions} поз. из ${allPositions}`
+ : filteredPositions > 0
+ ? `${filteredPositions} поз.`
+ : ''}
+
+
+
+
+
+ {isExpanded && (
+
+ {!orders.length ? (
+
Позиции не указаны
+ ) : (
+ orders.map((orderItem, idx) => (
+
+
+
{orderItem.nom || orderItem.name || `Заказ ${idx + 1}`}
+
+ {(() => {
+ const filtered = (orderItem.items || []).filter((item) => {
+ const name = String(item.product_name || item.name || item.title || "");
+ return active ? !matchesStopWord(name, stopWords) : true;
+ });
+ if (filtered.length === 0 && active && (orderItem.items || []).length > 0) {
+ return
Только услуги — скрыты стоп-словами
;
+ }
+ if (filtered.length === 0) {
+ return
Позиции не указаны
;
+ }
+ return (
+
+ {filtered.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)}
+
+ )}
+ {
+ onChangeDeliveryStatus({
+ orderGroupId: order.id,
+ status: "pending_confirmation",
+ }).then((response) => {
+ if (!response.success) {
+ setFormMessage(response.error || "Не удалось отменить платное хранение");
+ } else {
+ setFormMessage("Платное хранение отменено");
+ }
+ });
+ }}
+ disabled={isSavingDeliveryChoice}
+ >
+ Отменить платное хранение
+
+
+ );
+ }
+
+ return (
+
+
+
Платное хранение
+
+ Переведите заказ в статус платного хранения, если клиент не забрал товар в срок.
+
+
+
+ {showConfirm ? (
+
+
Перевести заказ в платное хранение? Клиент получит уведомление.
+
+ {
+ onChangeDeliveryStatus({
+ orderGroupId: order.id,
+ status: "paid_storage",
+ }).then((response) => {
+ if (!response.success) {
+ setFormMessage(response.error || "Не удалось обновить статус");
+ } else {
+ setFormMessage("Заказ переведён в платное хранение");
+ setShowConfirm(false);
+ }
+ });
+ }}
+ disabled={isSavingDeliveryChoice}
+ >
+ Да, перевести
+
+ setShowConfirm(false)}
+ disabled={isSavingDeliveryChoice}
+ >
+ Отмена
+
+
+
+ ) : (
+ setShowConfirm(true)}
+ disabled={isSavingDeliveryChoice}
+ >
+ Перевести в платное хранение
+
+ )}
+
+ );
+};
+
+
+const PROBLEM_REASONS = [
+ { value: "client_absent", label: "Клиент не принял", description: "Клиент отказался или не вышел на связь" },
+ { value: "damage", label: "Повреждение заказа", description: "Товар повреждён при транспортировке" },
+ { value: "wrong_address", label: "Неверный адрес", description: "Адрес доставки указан неверно" },
+ { value: "other", label: "Другое", description: "Иная причина проблемы доставки" },
+];
+
+const ProblemReasonModal = ({ onSelect, onCancel }) => (
+
+
e.stopPropagation()}>
+ Причина проблемы
+ Укажите причину возникшей проблемы с доставкой.
+
+ {PROBLEM_REASONS.map((reason) => (
+
onSelect(reason.value, reason.label)}
+ >
+ {reason.label}
+ {reason.description}
+
+ ))}
+
+
+ Отмена
+
+
+
+);
+
+export const OrderDetailPanel = ({
+ order,
+ canManageDelivery = false,
+ onSaveManualDeliveryChoice,
+ isSavingDeliveryChoice = false,
+ drivers = [],
+ onAssignDriver,
+ onChangeDeliveryStatus,
+ userRole,
+}) => {
+ const [problemReason, setProblemReason] = React.useState(null);
+ const [pendingStatus, setPendingStatus] = React.useState(null);
+ 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 [deliveryType, setDeliveryType] = React.useState(order?.deliveryType || "delivery");
+ const [pickupDate, setPickupDate] = React.useState(order?.pickupDate || "");
+ const [pickupTimeSlot, setPickupTimeSlot] = React.useState(DELIVERY_TIME_OPTIONS[0]);
+ 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 || "");
+ setIsEditingDate(false);
+ }, [order?.id, 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));
+ setDeliveryType(order?.deliveryType || "delivery");
+ setPickupDate(order?.pickupDate || "");
+ setPickupTimeSlot(normalizeDeliveryTimeChoice(order?.pickupTimeSlot || order?.deliveryTime || order?.deliveryHalfDay));
+ setFormMessage("");
+ }, [order?.id, order?.deliveryDate, order?.deliveryHalfDay, order?.deliveryTime, order?.deliveryType, order?.pickupDate, order?.pickupTimeSlot]);
+
+ if (!order) {
+ return (
+
+ Выберите группу для просмотра деталей.
+
+ );
+ }
+
+ const isDeliveryAgreed = ["agreed", "driver_assigned", "loaded", "on_route", "delivered"].includes(order.deliveryStatus || order.delivery_status);
+ const canEditDelivery = canManageDelivery && ["admin", "mega_admin", "logistician"].includes(userRole);
+ const [isEditingDate, setIsEditingDate] = React.useState(false);
+ const agreedDeliveryLabel = [
+ formatDeliveryDateDisplay(order.deliveryDate),
+ order.deliveryTime || order.deliveryHalfDay,
+ ].filter((value) => value && value !== "Нет данных").join(" · ");
+
+ const handleShipmentChange = React.useCallback((state) => {
+ setShipmentState(state);
+ }, []);
+
+ const handleSaveDeliveryChoice = async () => {
+ const effectiveDate = deliveryType === "pickup" ? pickupDate : deliveryDate;
+ const effectiveTime = deliveryType === "pickup" ? pickupTimeSlot : deliveryTime;
+ if (!effectiveDate || !effectiveTime) {
+ setFormMessage(deliveryType === "pickup" ? "Укажите дату и время самовывоза." : "Укажите дату и половину дня доставки.");
+ return;
+ }
+
+ if (!isFutureDeliveryDate(effectiveDate)) {
+ setFormMessage(deliveryType === "pickup" ? "Выберите дату самовывоза позже сегодняшнего дня." : "Выберите дату доставки позже сегодняшнего дня.");
+ return;
+ }
+
+ try {
+ const result = await onSaveManualDeliveryChoice?.({
+ orderGroupId: order.id,
+ deliveryDate: deliveryType === "pickup" ? pickupDate : deliveryDate,
+ deliveryTime: deliveryType === "pickup" ? pickupTimeSlot : deliveryTime,
+ deliveryType,
+ ...(deliveryType === "pickup" ? { pickupDate, pickupTimeSlot } : {}),
+ });
+
+ 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.deliveryType === "pickup" ? "Самовывоз" : "Доставка"}
+
+
+
+ Водитель
+
+
{order.assignedDriverId ? renderValue(order.assignedDriverName) : "Не назначен"}
+
+
+
+
+ Адрес доставки
+
+
{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
+ ? "Дата и половина дня доставки уже зафиксированы."
+ : "Если клиент согласовал доставку по телефону, сохраните дату и половину дня здесь."}
+
+
+ {/* Delivery type tabs */}
+
+ { setDeliveryType("delivery"); setFormMessage(""); }}
+ >
+ 🚚 Доставка
+
+ { setDeliveryType("pickup"); setFormMessage(""); }}
+ >
+ 🏪 Самовывоз
+
+
+ {deliveryType === "pickup" && (
+
+
ℹ️ Условия хранения
+
Бесплатное хранение — 2 рабочих дня с даты готовности.
+
Начиная с 3-го рабочего дня — 300 ₽/день платного хранения.
+
+ )}
+ {isDeliveryAgreed && !isEditingDate ? (
+
+
+
+
+
+ Доставка согласована
+
+
+ {agreedDeliveryLabel || "Дата и время сохранены"}
+
+
+
Согласовано
+
+
+ {canEditDelivery ? (
+
{ setIsEditingDate(true); setFormMessage(""); }}
+ disabled={isSavingDeliveryChoice}
+ className="text-sm"
+ >
+ Изменить дату доставки
+
+ ) : null}
+
+ ) : deliveryType === "delivery" ? (
+
+
+
setIsCalendarOpen((current) => !current)}
+ >
+ {formatDateForDisplay(deliveryDate)}
+ ▾
+
+ {isCalendarOpen ? (
+
+
+
+
+ Календарь доставки
+
+
+ {monthLabel}
+
+
+
+ setCurrentMonth((month) => addMonths(month, -1))}
+ >
+ ‹
+
+ setCurrentMonth((month) => addMonths(month, 1))}
+ >
+ ›
+
+
+
+
+ {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 (
+
{
+ if (isDisabled) {
+ return;
+ }
+
+ setDeliveryDate(dateKey);
+ setFormMessage("");
+ setIsCalendarOpen(false);
+ }}
+ >
+ {dayNumber}
+ {isWeekend ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+ Выходные отмечены пунктиром и недоступны.
+
+
+ ) : null}
+
+
+ {DELIVERY_TIME_OPTIONS.map((option) => (
+ {
+ setDeliveryTime(option);
+ setFormMessage("");
+ }}
+ >
+ {option}
+
+ ))}
+
+ ) : (
+
+
setIsCalendarOpen((current) => !current)}
+ >
+ {pickupDate ? formatDateForDisplay(pickupDate) : "Выберите дату"}
+ ▾
+
+ {isCalendarOpen ? (
+
+
+
+
Календарь самовывоза
+
{monthLabel}
+
+
+ setCurrentMonth((month) => addMonths(month, -1))}>‹
+ setCurrentMonth((month) => addMonths(month, 1))}>›
+
+
+
+ {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 === pickupDate;
+ const isDisabled = !isSelectable;
+ const dayNumber = String(day.getDate()).padStart(2, "0");
+ return (
+
{ if (!isDisabled) { setPickupDate(dateKey); setFormMessage(""); setIsCalendarOpen(false); } }}>
+ {dayNumber}
+ {isWeekend ? ( ) : null}
+
+ );
+ })}
+
+
Выходные отмечены пунктиром и недоступны.
+
+ ) : null}
+
+ {DELIVERY_TIME_OPTIONS.map((option) => (
+ { setPickupTimeSlot(option); setFormMessage(""); }}>
+ {option}
+
+ ))}
+
+
+ )}
+
+ {isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
+
+ )}
+ {formMessage ? (
+
{formMessage}
+ ) : null}
+
+ ) : null}
+
+
+ {canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) ? (
+
+
+
Назначение водителя
+
+ {(() => {
+ const ds = order.deliveryStatus || order.delivery_status;
+ if (["loaded", "on_route", "delivered"].includes(ds)) {
+ return "Доставка в процессе — сменить водителя нельзя.";
+ }
+ return order.assignedDriverId
+ ? "Назначен водитель. Вы можете изменить назначение."
+ : "Выберите водителя для доставки.";
+ })()}
+
+
+ {order.assignedDriverId ? (
+
+
+
+
+ Водитель назначен
+
+
+ {order.assignedDriverName || "Неизвестно"}
+
+
+
Назначен
+
+
+ ) : null}
+ {(() => {
+ const ds = order.deliveryStatus || order.delivery_status;
+ const isDriverLocked = ["loaded", "on_route", "delivered"].includes(ds);
+ return !isDriverLocked ? (
+
+ {
+ setSelectedDriverId(e.target.value);
+ setDriverMessage("");
+ }}
+ disabled={isSavingDeliveryChoice}
+ >
+ {order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}
+ {drivers.map((driver) => (
+ {driver.name || driver.email}
+ ))}
+
+
+ {isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
+
+
+ ) : null;
+ })()}
+ {driverMessage ? (
+ {driverMessage}
+ ) : null}
+
+ ) : null}
+
+
+ {["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
+
+
+
Статус доставки
+
+ Измените статус, если водитель забыл обновить или нужна корректировка.
+
+
+
+ {[
+ { value: "pending_confirmation", label: "Ожидает согласования", manual: true },
+ { value: "agreed", label: "Согласовано", manual: false, hint: "Согласуйте дату доставки выше" },
+ { value: "driver_assigned", label: "Назначен водитель", manual: false, hint: "Назначьте водителя из списка" },
+ { value: "loaded", label: "Загружено", manual: true },
+ { value: "on_route", label: "В пути", manual: true },
+ { value: "delivered", label: "Доставлено", manual: true },
+ { value: "pickup", label: "Самовывоз", manual: true },
+ { value: "problem", label: "Проблема", manual: true },
+ { value: "cancelled", label: "Отменено", manual: true },
+ ].map((statusOption) => {
+ const isCurrent = (order.deliveryStatus || order.delivery_status) === statusOption.value;
+ const isClickable = statusOption.manual !== false && !isCurrent;
+ return (
+
+ {
+ if (!isClickable) {
+ setFormMessage(statusOption.hint || "");
+ return;
+ }
+ setFormMessage("");
+ onChangeDeliveryStatus({
+ orderGroupId: order.id,
+ status: statusOption.value,
+ }).then((response) => {
+ if (!response.success) {
+ setFormMessage(response.error || "Не удалось обновить статус");
+ } else {
+ setFormMessage("");
+ }
+ });
+ }}
+ disabled={isSavingDeliveryChoice}
+ >
+ {statusOption.label}
+
+
+ );
+ })}
+
+ {formMessage ? (
+ {formMessage}
+ ) : null}
+
+ ) : null}
+
+
+ {["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
+
+ ) : null}
+
+ {userRole === "driver" && order ? (
+
+ ) : null}
+
+{userRole === "driver" && order && onChangeDeliveryStatus ? (
+
+
+
Статус доставки
+
+ Выберите статус и нажмите «Сохранить».
+
+
+ {problemReason !== null ? (
+ {
+ setPendingStatus({ value: "problem", reason: reasonValue, reasonLabel });
+ setProblemReason(null);
+ }}
+ onCancel={() => setProblemReason(null)}
+ />
+ ) : null}
+
+ {(() => {
+ const currentStatus = order.deliveryStatus || order.delivery_status;
+ const IN_TRANSIT_STATUSES = ["loaded", "on_route"];
+ const isOnRoute = IN_TRANSIT_STATUSES.includes(currentStatus);
+
+ let statusOptions = [];
+ if (currentStatus === "delivered" || currentStatus === "problem" || currentStatus === "cancelled" || currentStatus === "paid_storage") {
+ statusOptions = [];
+ } else {
+ statusOptions = [
+ { value: "delivered", label: "Доставлено" },
+ { value: "problem", label: "Проблема" },
+ ];
+ }
+
+ if (statusOptions.length === 0) return null;
+
+ return statusOptions.map((statusOption) => {
+ const isSelected = pendingStatus?.value === statusOption.value;
+ const isDeliveredBtn = statusOption.value === "delivered";
+ const deliveryBlocked = isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered;
+ return (
+ {
+ if (statusOption.value === "problem") {
+ setProblemReason("selecting");
+ return;
+ }
+ setPendingStatus({ value: statusOption.value });
+ }}
+ >
+ {statusOption.label}
+ {isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered ? (
+
+ ({shipmentState.shipped}/{shipmentState.total})
+
+ ) : null}
+
+ );
+ });
+ })()}
+
+ {pendingStatus ? (
+
+ {
+ if (pendingStatus.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) return;
+ onChangeDeliveryStatus({
+ orderGroupId: order.id,
+ status: pendingStatus.value,
+ details: pendingStatus.reason ? { reason: pendingStatus.reason, reasonLabel: pendingStatus.reasonLabel } : undefined,
+ shipmentData: pendingStatus.value === "delivered" && shipmentState ? shipmentState.shipmentData.filter((i) => !i.shipped) : undefined,
+ }).then((response) => {
+ if (!response.success) {
+ setFormMessage(response.error || "Не удалось обновить статус");
+ } else {
+ setFormMessage("");
+ setPendingStatus(null);
+ }
+ });
+ }}
+ >
+ Сохранить
+
+ setPendingStatus(null)}>
+ Отмена
+
+
+ ) : null}
+ {formMessage ? (
+ {formMessage}
+ ) : null}
+
+ ) : null}
+
+
+ Счета
+ {renderList(getAllBillNumbers(order))}
+
+
+
+
+
+ {userRole !== "driver" && (order?.driver_shipment_data || order?.driverShipmentData) ? (
+
+ ) : null}
+ {userRole !== "driver" ? (
+
+ Дополнительные данные
+
+ {order.firstSmsSentAt ? (
+
+
1-е SMS отправлено
+
{formatDateTime(order.firstSmsSentAt)}
+
+ ) : null}
+ {order.secondSmsSentAt ? (
+
+
2-е SMS отправлено
+
{formatDateTime(order.secondSmsSentAt)}
+
+ ) : null}
+ {!order.firstSmsSentAt && !order.secondSmsSentAt ? (
+
+ ) : null}
+
+
Ручное согласование выполнено
+
{order.manualConfirmationAt ? formatDateTime(order.manualConfirmationAt) : "Нет"}
+
+
+
Платное хранение
+
{order.paidStorageAt ? formatDateTime(order.paidStorageAt) : "Нет"}
+
+ {order.createdFromExchangeAt ? (
+
+
Создано из обмена
+
{formatDateTime(order.createdFromExchangeAt)}
+
+ ) : null}
+
+
+ ) : null}
+
+ );
+};
diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js
index c699cb2..a4eb953 100644
--- a/src/services/supabase/orderGroupRepository.js
+++ b/src/services/supabase/orderGroupRepository.js
@@ -281,18 +281,6 @@ export const updateOrderGroupDeliveryChoice = async ({
}, "Ошибка сохранения согласования доставки");
};
- .eq("id", orderGroupId)
- .single();
-
- if (error) {
- throw error;
- }
-
- await logAction({ orderGroupId, action: "date_assigned", newValue: "manual: " + deliveryDate + " " + (deliveryTime || ""), details: { delivery_date_source: "manual" } }).catch(() => {});
-
- return mapOrderGroupRowToDeliveryGroup(data);
- }, "Ошибка сохранения согласования доставки");
-};
export const assignDriverToOrderGroup = async ({
orderGroupId,
@@ -456,3 +444,4 @@ export const fetchOrderGroups = async () => {
return group;
}).filter(Boolean);
}, "Ошибка загрузки групп доставки");
+};