supersam/src/components/orders/OrderDetailPanel.jsx

1419 lines
66 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const DriverShipmentReport = ({ shipmentData }) => {
if (!Array.isArray(shipmentData) || shipmentData.length === 0) return null;
return (
<Panel className="space-y-4 p-5 border-[var(--color-warning)]">
<div className="flex items-center gap-2">
<svg className="h-5 w-5 text-[var(--color-warning)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<strong className="text-[var(--color-warning)]">Проблемы с доставкой позиций</strong>
</div>
<p className="text-sm text-[var(--color-text-muted)]">
Не доставлено {shipmentData.length} {shipmentData.length === 1 ? "позиция" : shipmentData.length < 5 ? "позиции" : "позиций"}. Остальное доставлено.
</p>
<div className="space-y-2">
{shipmentData.map((item) => (
<div
key={item.id || item.name}
className="rounded-[18px] border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-4 py-3 text-sm"
>
<div className="flex items-center justify-between gap-2">
<span className="text-[var(--color-text)]">{item.name}</span>
{item.quantity || item.unit ? (
<Badge tone="neutral">{[item.quantity, item.unit].filter(Boolean).join(" ")}</Badge>
) : null}
</div>
{item.comment ? (
<p className="mt-1 text-xs text-[var(--color-text-muted)]">Причина: {item.comment}</p>
) : (
<p className="mt-1 text-xs text-[var(--color-text-muted)] italic">Причина не указана</p>
)}
</div>
))}
</div>
</Panel>
);
};
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 <p className="text-sm text-[var(--color-text-muted)]">Нет данных</p>;
}
return (
<div className="flex flex-wrap gap-2">
{values.map((value, index) => (
<span
key={`${value}-${index}`}
className="rounded-full bg-[var(--color-surface)] px-3 py-1 text-xs text-[var(--color-text-muted)]"
>
{value}
</span>
))}
</div>
);
};
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 (
<div className="space-y-3">
<button
type="button"
className="flex w-full items-center justify-between text-left"
onClick={() => setIsExpanded(!isExpanded)}
>
<span className="font-semibold">Состав заказа</span>
<span className="flex items-center gap-2 text-sm text-[var(--color-text-muted)]">
{active && filteredPositions < allPositions
? `${filteredPositions} поз. из ${allPositions}`
: filteredPositions > 0
? `${filteredPositions} поз.`
: ''}
<svg
className="h-4 w-4 transition-transform"
style={{ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)' }}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</span>
</button>
{isExpanded && (
<div className="space-y-3">
{!orders.length ? (
<p className="text-sm text-[var(--color-text-muted)]">Позиции не указаны</p>
) : (
orders.map((orderItem, idx) => (
<div key={idx} className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4">
<div className="mb-3 pb-2 border-b border-[var(--color-border)]">
<p className="font-bold text-[var(--color-text)] text-sm">{orderItem.nom || orderItem.name || `Заказ ${idx + 1}`}</p>
</div>
{(() => {
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 <p className="text-sm text-[var(--color-text-muted)] italic">Только услуги скрыты стоп-словами</p>;
}
if (filtered.length === 0) {
return <p className="text-sm text-[var(--color-text-muted)]">Позиции не указаны</p>;
}
return (
<div className="space-y-2">
{filtered.map((item, itemIdx) => (
<div key={itemIdx} className="grid grid-cols-[1fr_auto] gap-x-4 gap-y-1 text-sm">
<span className="text-[var(--color-text)] min-w-0">{item.product_name || item.name || item.title || ''}</span>
<span className="text-[var(--color-text-muted)] whitespace-nowrap text-right">
{item.product_quantity || item.quantity || item.count || item.amount || ""} {item.product_ed || item.unit || ""}
</span>
</div>
))}
</div>
);
})()}
</div>
))
)}
</div>
)}
</div>
);
};
const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoice, setFormMessage }) => {
const [showConfirm, setShowConfirm] = React.useState(false);
const isPaidStorage = (order.deliveryStatus || order.delivery_status) === "paid_storage";
if (isPaidStorage) {
return (
<Panel className="space-y-4 p-5">
<div className="flex items-center gap-2">
<span className="inline-flex h-2 w-2 rounded-full bg-[var(--color-warning)]"></span>
<strong>Платное хранение</strong>
</div>
{order.paidStorageAt && (
<p className="text-sm text-[var(--color-text-muted)]">
Переведено: {formatDateTime(order.paidStorageAt)}
</p>
)}
<Button
variant="secondary"
onClick={() => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "pending_confirmation",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось отменить платное хранение");
} else {
setFormMessage("Платное хранение отменено");
}
});
}}
disabled={isSavingDeliveryChoice}
>
Отменить платное хранение
</Button>
</Panel>
);
}
return (
<Panel className="space-y-4 p-5">
<div>
<strong>Платное хранение</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Переведите заказ в статус платного хранения, если клиент не забрал товар в срок.
</p>
</div>
{showConfirm ? (
<div className="space-y-3 rounded-2xl border border-[var(--color-warning)] bg-[var(--color-warning-soft)] p-4">
<p className="text-sm font-medium">Перевести заказ в платное хранение? Клиент получит уведомление.</p>
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "paid_storage",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось обновить статус");
} else {
setFormMessage("Заказ переведён в платное хранение");
setShowConfirm(false);
}
});
}}
disabled={isSavingDeliveryChoice}
>
Да, перевести
</Button>
<Button
variant="secondary"
onClick={() => setShowConfirm(false)}
disabled={isSavingDeliveryChoice}
>
Отмена
</Button>
</div>
</div>
) : (
<Button
variant="secondary"
onClick={() => setShowConfirm(true)}
disabled={isSavingDeliveryChoice}
>
Перевести в платное хранение
</Button>
)}
</Panel>
);
};
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 }) => (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={onCancel}>
<Panel className="mx-4 w-full max-w-md space-y-4 p-6" onClick={(e) => e.stopPropagation()}>
<h3 className="text-lg font-semibold">Причина проблемы</h3>
<p className="text-sm text-[var(--color-text-muted)]">Укажите причину возникшей проблемы с доставкой.</p>
<div className="space-y-2">
{PROBLEM_REASONS.map((reason) => (
<button
key={reason.value}
type="button"
className="w-full rounded-[16px] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-accent-soft)]"
onClick={() => onSelect(reason.value, reason.label)}
>
<span className="font-medium">{reason.label}</span>
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">{reason.description}</p>
</button>
))}
</div>
<div className="flex justify-end">
<Button variant="ghost" onClick={onCancel}>Отмена</Button>
</div>
</Panel>
</div>
);
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 [deliveryAddress, setDeliveryAddress] = React.useState(order?.deliveryAddress || order?.customerAddress || "");
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 (
<Panel className="flex min-h-[460px] items-center justify-center">
<p className="text-sm text-[var(--color-text-muted)]">Выберите группу для просмотра деталей.</p>
</Panel>
);
}
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 } : {}),
...(deliveryType === "delivery" && deliveryAddress.trim() ? { deliveryAddress: deliveryAddress.trim() } : {}),
});
if (result?.success) {
setFormMessage(deliveryType === "pickup" ? "Самовывоз согласован вручную." : "Доставка согласована вручную.");
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 (
<div className="space-y-5">
<Panel className="space-y-5 p-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.2em] text-[var(--color-text-muted)]">
{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Карточка группы самовывоза" : "Карточка группы доставки"}
</p>
<h2 className="mt-2 text-2xl font-semibold">
{order.displayTitle || order.customerName || order.groupKey}
</h2>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
{(() => {
const parts = [];
if (order.orderNumbers && order.orderNumbers.length > 0) parts.push(order.orderNumbers.join(", "));
const sub = order.displaySubtitle || [order.customerPhone, order.customerDate].filter(Boolean).join(" · ");
if (sub) parts.push(sub);
return parts.join(" · ") || "Не указано";
})()}
</p>
</div>
<Badge tone={getOrderGroupStatusTone(order)}>{getOrderGroupDisplayStatusLabel(order)}</Badge>
</div>
{(() => {
const isPickup = order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup";
const deliveryTypeLabel = isPickup
? "Самовывоз"
: (order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address")
? "Доставка (требуется адрес)"
: "Доставка";
const dateLabel = isPickup ? "Дата самовывоза" : "Дата доставки";
const timeLabel = isPickup ? "Время самовывоза" : "Время доставки";
const addressLabel = isPickup ? "Адрес самовывоза" : "Адрес доставки";
// For pickup orders, hide the delivery address if it's just a placeholder like "Самовывоз"
const effectiveAddress = isPickup
? (order.deliveryAddress && order.deliveryAddress !== "Самовывоз" && order.deliveryAddress !== "самовывоз" ? order.deliveryAddress : "")
: order.deliveryAddress;
return (
<div className="grid gap-3 rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 md:grid-cols-4">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
{dateLabel}
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{formatDeliveryDateDisplay(order.deliveryDate)}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
{timeLabel}
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{renderValue(order.deliveryTime || order.deliveryHalfDay)}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Тип
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{deliveryTypeLabel}</p>
{(order.deliveryStatus === "requires_address" || order.delivery_status === "requires_address") && (
<div className="mt-2 flex items-start gap-2 rounded-xl border border-[rgba(239,68,68,0.3)] bg-[rgba(239,68,68,0.08)] p-3">
<span className="text-lg">📍</span>
<div>
<p className="text-sm font-semibold text-[var(--color-text)]">Адрес доставки не указан</p>
<p className="text-xs text-[var(--color-text-muted)]">Клиент выбрал доставку, но адрес отсутствует. Уточните адрес у клиента и заполните поле ниже.</p>
</div>
</div>
)}
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Водитель
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{order.assignedDriverId ? renderValue(order.assignedDriverName) : (isPickup ? "Не нужен" : "Не назначен")}</p>
</div>
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Телефон
</p>
<a
href={`tel:${normalizePhoneForTel(order.customerPhone)}`}
className="mt-1 block text-base font-medium !text-[var(--color-accent)] hover:underline"
>
{renderValue(order.customerPhone)}
</a>
</div>
{!isPickup || effectiveAddress ? (
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
{addressLabel}
</p>
<p className="mt-1 text-base font-medium !text-[var(--color-text)]">{renderValue(effectiveAddress)}</p>
</div>
) : null}
</div>
);
})()}
<div className="grid gap-x-4 gap-y-2 grid-cols-2 md:grid-cols-4">
<div>
<p className="text-xs text-[var(--color-text-muted)]">Заказ</p>
<p className="font-medium !text-[var(--color-text)]">{(() => {
const mainNumbers = order.orderNumbers || [];
const allNumbers = order.allBillNumbers || [];
const mainSet = new Set(mainNumbers.map(String));
const extraNumbers = allNumbers.filter((n) => !mainSet.has(String(n)));
if (mainNumbers.length > 0) {
return (
<span>
<span className="font-bold">{mainNumbers.join(", ")}</span>
{extraNumbers.length > 0 && (
<span className="text-[var(--color-text-muted)]"> +{extraNumbers.length} сч.</span>
)}
</span>
);
}
return renderValue(order.orderNumberSummary);
})()}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Клиент</p>
<p className="font-medium !text-[var(--color-text)]">{renderValue(order.customerName)}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Дата счёта</p>
<p className="font-medium !text-[var(--color-text)]">{renderValue(order.customerDate)}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Всего заказов</p>
<p className="font-medium !text-[var(--color-text)]">{order.ordersCount ?? 0}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Готово</p>
<p className="font-medium !text-[var(--color-text)]">{order.readyCount ?? 0}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Не готово</p>
<p className="font-medium !text-[var(--color-text)]">{order.notReadyCount ?? 0}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Обновлена</p>
<p className="font-medium !text-[var(--color-text)]">{formatDateTime(order.updatedAt)}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Статус самовывоза" : "Статус доставки"}</p>
<p className="font-medium !text-[var(--color-text)]">{getOrderGroupDeliveryStatusLabel(order.deliveryStatus || order.delivery_status)}</p>
</div>
</div>
</Panel>
{canManageDelivery ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Ручное согласование</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
{isDeliveryAgreed
? "Дата и время уже зафиксированы."
: "Если клиент согласовал доставку или самовывоз по телефону, сохраните дату и время здесь."}
</p>
</div>
{/* Delivery type tabs */}
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<button
type="button"
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition ${
deliveryType === "delivery"
? "bg-[var(--color-accent)] text-white"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
}`}
onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }}
>
🚚 Доставка
</button>
<button
type="button"
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition ${
deliveryType === "pickup"
? "bg-[var(--color-accent)] text-white"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
}`}
onClick={() => { setDeliveryType("pickup"); setFormMessage(""); }}
>
🏪 Самовывоз
</button>
</div>
{deliveryType === "pickup" && (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-sm text-[var(--color-text-muted)]">
<p className="font-semibold text-[var(--color-text)]"> Условия хранения</p>
<p className="mt-1">Бесплатное хранение <strong>2 рабочих дня</strong> с даты готовности.</p>
<p>Начиная с 3-го рабочего дня <strong>300 /день</strong> платного хранения.</p>
</div>
)}
{isDeliveryAgreed && !isEditingDate ? (
<div className="space-y-3">
<div className="rounded-[24px] border border-[rgba(18,128,92,0.35)] bg-[var(--color-accent-soft)] p-4 !text-[var(--color-text)]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-accent)]">
{(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "Самовывоз согласован" : "Доставка согласована"}
</p>
<p className="mt-1 text-lg font-semibold">
{agreedDeliveryLabel || "Дата и время сохранены"}
</p>
</div>
<Badge tone="accent">Согласовано</Badge>
</div>
</div>
{canEditDelivery ? (
<Button
variant="secondary"
onClick={() => { setIsEditingDate(true); setFormMessage(""); }}
disabled={isSavingDeliveryChoice}
className="text-sm"
>
Изменить {(order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup") ? "дату самовывоза" : "дату доставки"}
</Button>
) : null}
</div>
) : deliveryType === "delivery" ? (
<div className="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10">
<div className="space-y-3 md:relative md:z-30 md:min-w-0 md:flex-1 md:pr-4">
<button
type="button"
aria-label="Дата доставки"
aria-expanded={isCalendarOpen}
className="flex min-h-[54px] w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-left text-sm font-medium !text-[var(--color-text)] transition hover:border-[var(--color-accent)] focus:border-[var(--color-accent)] focus:outline-none"
onClick={() => setIsCalendarOpen((current) => !current)}
>
<span>{formatDateForDisplay(deliveryDate)}</span>
<span aria-hidden="true" className="text-[var(--color-text-muted)]"></span>
</button>
{isCalendarOpen ? (
<div className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 shadow-soft md:absolute md:left-0 md:top-full md:z-50 md:mt-3 md:w-[min(460px,calc(100vw-3rem))]">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Календарь доставки
</p>
<h4
className="mt-1 text-base font-semibold capitalize"
style={{ color: "var(--color-text)" }}
>
{monthLabel}
</h4>
</div>
<div className="flex items-center gap-2">
<button
type="button"
disabled={!canGoBack}
aria-label="Предыдущий месяц"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-40"
onClick={() => setCurrentMonth((month) => addMonths(month, -1))}
>
</button>
<button
type="button"
aria-label="Следующий месяц"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]"
onClick={() => setCurrentMonth((month) => addMonths(month, 1))}
>
</button>
</div>
</div>
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-[10px] font-semibold uppercase text-[var(--color-text-muted)]">
{WEEK_DAY_LABELS.map((day) => (
<div key={day} className="px-1 py-1">
{day}
</div>
))}
</div>
<div className="mt-1 grid grid-cols-7 gap-1">
{calendarDays.map((day, index) => {
if (!day) {
return <div key={`empty-${index}`} className="aspect-square" />;
}
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 (
<button
key={dateKey}
type="button"
disabled={isDisabled}
title={isWeekend ? "Выходной, доставки нет" : isSelectable ? "Можно выбрать" : "Недоступно"}
className={[
"relative flex aspect-square items-center justify-center rounded-xl border text-sm font-semibold transition",
isSelected
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]"
: isWeekend
? "border-dashed border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]",
isDisabled ? "cursor-not-allowed opacity-45" : "",
].join(" ")}
onClick={() => {
if (isDisabled) {
return;
}
setDeliveryDate(dateKey);
setFormMessage("");
setIsCalendarOpen(false);
}}
>
<span>{dayNumber}</span>
{isWeekend ? (
<span
aria-hidden="true"
className="absolute inset-x-2 top-1/2 h-px -rotate-12 bg-[var(--color-text-muted)] opacity-70"
/>
) : null}
</button>
);
})}
</div>
<p className="mt-2 text-xs text-[var(--color-text-muted)]">
Выходные отмечены пунктиром и недоступны.
</p>
</div>
) : null}
</div>
<div className="grid gap-2 sm:grid-cols-2 md:w-[320px] md:flex-none">
{DELIVERY_TIME_OPTIONS.map((option) => (
<button
key={option}
type="button"
aria-pressed={deliveryTime === option}
className={[
"min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition",
deliveryTime === option
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]",
].join(" ")}
onClick={() => {
setDeliveryTime(option);
setFormMessage("");
}}
>
{option}
</button>
))}
</div>
</div>
) : (
<div className="space-y-3">
<button
type="button"
aria-label="Дата самовывоза"
aria-expanded={isCalendarOpen}
className="flex min-h-[54px] w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-left text-sm font-medium !text-[var(--color-text)] transition hover:border-[var(--color-accent)] focus:border-[var(--color-accent)] focus:outline-none"
onClick={() => setIsCalendarOpen((current) => !current)}
>
<span>{pickupDate ? formatDateForDisplay(pickupDate) : "Выберите дату"}</span>
<span aria-hidden="true" className="text-[var(--color-text-muted)]"></span>
</button>
{isCalendarOpen ? (
<div className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 shadow-soft md:relative md:z-50">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">Календарь самовывоза</p>
<h4 className="mt-1 text-base font-semibold capitalize" style={{ color: "var(--color-text)" }}>{monthLabel}</h4>
</div>
<div className="flex items-center gap-2">
<button type="button" disabled={!canGoBack} aria-label="Предыдущий месяц" className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-40" onClick={() => setCurrentMonth((month) => addMonths(month, -1))}></button>
<button type="button" aria-label="Следующий месяц" className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]" onClick={() => setCurrentMonth((month) => addMonths(month, 1))}></button>
</div>
</div>
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-[10px] font-semibold uppercase text-[var(--color-text-muted)]">
{WEEK_DAY_LABELS.map((day) => (<div key={day} className="px-1 py-1">{day}</div>))}
</div>
<div className="mt-1 grid grid-cols-7 gap-1">
{calendarDays.map((day, index) => {
if (!day) return <div key={`empty-${index}`} className="aspect-square" />;
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 (
<button key={dateKey} type="button" disabled={isDisabled} title={isWeekend ? "Выходной" : isSelectable ? "Можно выбрать" : "Недоступно"} className={["relative flex aspect-square items-center justify-center rounded-xl border text-sm font-semibold transition", isSelected ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]" : isWeekend ? "border-dashed border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)]" : "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]", isDisabled ? "cursor-not-allowed opacity-45" : ""].join(" ")} onClick={() => { if (!isDisabled) { setPickupDate(dateKey); setFormMessage(""); setIsCalendarOpen(false); } }}>
<span>{dayNumber}</span>
{isWeekend ? (<span aria-hidden="true" className="absolute inset-x-2 top-1/2 h-px -rotate-12 bg-[var(--color-text-muted)] opacity-70" />) : null}
</button>
);
})}
</div>
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Выходные отмечены пунктиром и недоступны.</p>
</div>
) : null}
<div className="grid gap-2 sm:grid-cols-2">
{DELIVERY_TIME_OPTIONS.map((option) => (
<button key={option} type="button" aria-pressed={pickupTimeSlot === option} className={["min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition", pickupTimeSlot === option ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]" : "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]"].join(" ")} onClick={() => { setPickupTimeSlot(option); setFormMessage(""); }}>
{option}
</button>
))}
</div>
</div>
)}
<Button
className="w-full md:w-[180px] md:flex-none md:self-start"
onClick={handleSaveDeliveryChoice}
disabled={isSavingDeliveryChoice}
>
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
</Button>
{formMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{formMessage}</p>
) : null}
</Panel>
) : null}
{canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Назначение водителя</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
{(() => {
const ds = order.deliveryStatus || order.delivery_status;
if (["loaded", "on_route", "delivered"].includes(ds)) {
return "Доставка в процессе — сменить водителя нельзя.";
}
return order.assignedDriverId
? "Назначен водитель. Вы можете изменить назначение."
: "Выберите водителя для доставки.";
})()}
</p>
</div>
{order.assignedDriverId ? (
<div className="rounded-[24px] border border-[rgba(59,130,246,0.35)] bg-[var(--color-accent-soft)] p-4 !text-[var(--color-text)]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-accent)]">
Водитель назначен
</p>
<p className="mt-1 text-lg font-semibold">
{order.assignedDriverName || "Неизвестно"}
</p>
</div>
<Badge tone="accent">Назначен</Badge>
</div>
</div>
) : null}
{(() => {
const ds = order.deliveryStatus || order.delivery_status;
const isDriverLocked = ["loaded", "on_route", "delivered"].includes(ds);
return !isDriverLocked ? (
<div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
<Select
className="h-[46px] py-0"
value={selectedDriverId}
onChange={(e) => {
setSelectedDriverId(e.target.value);
setDriverMessage("");
}}
disabled={isSavingDeliveryChoice}
>
<option value="">{order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>{driver.name || driver.email}</option>
))}
</Select>
<Button
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
onClick={handleAssignDriver}
disabled={isSavingDeliveryChoice || !selectedDriverId}
>
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
</Button>
</div>
) : null;
})()}
{driverMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
) : null}
</Panel>
) : null}
{["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Статус доставки</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Измените статус, если водитель забыл обновить или нужна корректировка.
</p>
</div>
<div className="flex flex-wrap gap-2">
{[
{ 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: "requires_address", 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 (
<div key={statusOption.value} className="relative group">
<Button
variant={isCurrent ? "primary" : "secondary"}
onClick={() => {
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}
</Button>
</div>
);
})}
</div>
{formMessage ? (
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p>
) : null}
</Panel>
) : null}
{["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
<PaidStoragePanel
order={order}
onChangeDeliveryStatus={onChangeDeliveryStatus}
isSavingDeliveryChoice={isSavingDeliveryChoice}
setFormMessage={setFormMessage}
/>
) : null}
{userRole === "driver" && order ? (
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} />
) : null}
{userRole === "driver" && order && onChangeDeliveryStatus ? (
<Panel className="space-y-4 p-5">
<div>
<strong>Статус доставки</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Выберите статус и нажмите «Сохранить».
</p>
</div>
{problemReason !== null ? (
<ProblemReasonModal
onSelect={(reasonValue, reasonLabel) => {
setPendingStatus({ value: "problem", reason: reasonValue, reasonLabel });
setProblemReason(null);
}}
onCancel={() => setProblemReason(null)}
/>
) : null}
<div className="flex flex-wrap gap-2">
{(() => {
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 (
<Button
key={statusOption.value}
variant={isSelected ? "primary" : "secondary"}
disabled={deliveryBlocked}
title={deliveryBlocked ? "Сначала отметьте все позиции как отгруженные" : undefined}
onClick={() => {
if (statusOption.value === "problem") {
setProblemReason("selecting");
return;
}
setPendingStatus({ value: statusOption.value });
}}
>
{statusOption.label}
{isDeliveredBtn && shipmentState && !shipmentState.canMarkDelivered ? (
<span className="ml-1.5 text-xs text-[var(--color-text-muted)]">
({shipmentState.shipped}/{shipmentState.total})
</span>
) : null}
</Button>
);
});
})()}
</div>
{pendingStatus ? (
<div className="flex items-center gap-3 mt-2">
<Button
variant="primary"
disabled={isSavingDeliveryChoice}
onClick={() => {
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);
}
});
}}
>
Сохранить
</Button>
<Button variant="ghost" onClick={() => setPendingStatus(null)}>
Отмена
</Button>
</div>
) : null}
{formMessage ? (
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p>
) : null}
</Panel>
) : null}
<Panel className="space-y-4 p-5">
<strong>Счета</strong>
{(() => {
const mainNumbers = order.orderNumbers || [];
const allNumbers = getAllBillNumbers(order);
const mainSet = new Set(mainNumbers.map(String));
const extraNumbers = allNumbers.filter((n) => !mainSet.has(String(n)));
return (
<div className="space-y-2">
{mainNumbers.length > 0 && (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Основной счёт</p>
<div className="flex flex-wrap gap-2">
{mainNumbers.map((num, idx) => (
<span key={idx} className="rounded-full bg-[var(--color-accent-soft)] px-3 py-1 text-sm font-semibold text-[var(--color-accent)]">{num}</span>
))}
</div>
</div>
)}
{extraNumbers.length > 0 && (
<div>
<p className="text-xs text-[var(--color-text-muted)]">{mainNumbers.length > 0 ? "Составные заказы" : "Все счета"}</p>
<div className="flex flex-wrap gap-2">
{extraNumbers.map((num, idx) => (
<span key={idx} className="rounded-full bg-[var(--color-surface)] px-3 py-1 text-xs text-[var(--color-text-muted)]">{num}</span>
))}
</div>
</div>
)}
{mainNumbers.length === 0 && extraNumbers.length === 0 && (
<p className="text-sm text-[var(--color-text-muted)]">Нет данных</p>
)}
</div>
);
})()}
</Panel>
<Panel className="space-y-4 p-5">
<CollapsibleOrderComposition order={order} />
</Panel>
{userRole !== "driver" && (order?.driver_shipment_data || order?.driverShipmentData) ? (
<DriverShipmentReport shipmentData={order.driver_shipment_data || order.driverShipmentData} />
) : null}
{userRole !== "driver" ? (
<Panel className="space-y-4 p-5">
<strong>Дополнительные данные</strong>
<div className="grid gap-4 md:grid-cols-2">
{order.firstSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">1-е SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.firstSmsSentAt)}</p>
</div>
) : null}
{order.secondSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">2-е SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.secondSmsSentAt)}</p>
</div>
) : null}
{!order.firstSmsSentAt && !order.secondSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">Нет</p>
</div>
) : null}
<div>
<p className="text-xs text-[var(--color-text-muted)]">Ручное согласование выполнено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.manualConfirmationAt ? formatDateTime(order.manualConfirmationAt) : "Нет"}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Платное хранение</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.paidStorageAt ? formatDateTime(order.paidStorageAt) : "Нет"}</p>
</div>
{order.createdFromExchangeAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Создано из обмена</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.createdFromExchangeAt)}</p>
</div>
) : null}
</div>
</Panel>
) : null}
</div>
);
};