refactor: Phase 2 decomposition — extract CalendarWidget, StatusActionPanel, DriverAssignmentPanel

- CalendarWidget: eliminates duplicated calendar JSX (delivery + pickup variants)
- StatusActionPanel: status action buttons with onConfirmStatus callback
- DriverAssignmentPanel: driver selection with onDriverSelect/onConfirmDriver callbacks
- OrderDetailPanel reduced from 1510 to 1261 lines
- No UI changes — all props passed through, all class names preserved
This commit is contained in:
root 2026-06-12 15:30:30 +00:00
parent 4dde64ff5a
commit 129175fed7
4 changed files with 409 additions and 304 deletions

View File

@ -0,0 +1,185 @@
import React from "react";
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
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 isWeekendDate = (date) => {
const day = date.getDay();
return day === 0 || day === 6;
};
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 CalendarWidget = ({
label,
selectedDate,
onDateChange,
minDateKey,
isCalendarOpen,
setIsCalendarOpen,
currentMonth,
setCurrentMonth,
calendarDays,
monthLabel,
canGoBack,
timeOptions,
selectedTime,
onTimeChange,
layoutClassName,
calendarClassName,
timeClassName,
}) => {
return (
<div className={layoutClassName}>
<div className={calendarClassName}>
<button
type="button"
aria-label={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>{selectedDate ? formatDateForDisplay(selectedDate) : "Выберите дату"}</span>
<span aria-hidden="true" className="text-[var(--color-text-muted)]"></span>
</button>
{isCalendarOpen ? (
<div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]">
<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)]">
{label}
</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) => new Date(month.getFullYear(), month.getMonth() - 1, 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) => new Date(month.getFullYear(), month.getMonth() + 1, 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, minDateKey);
const isSelected = dateKey === selectedDate;
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;
onDateChange(dateKey);
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={timeClassName}>
{timeOptions.map((option) => (
<button
key={option}
type="button"
aria-pressed={selectedTime === option}
className={[
"min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition",
selectedTime === 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={() => onTimeChange(option)}
>
{option}
</button>
))}
</div>
</div>
);
};
export { CalendarWidget, formatDateForDisplay };

View File

@ -0,0 +1,89 @@
import React from "react";
import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Select } from "../UI/Select";
import { Panel } from "../UI/Panel";
const DriverAssignmentPanel = ({
order,
userRole,
canManageDelivery,
isSavingDeliveryChoice,
selectedDriverId,
onDriverSelect,
onConfirmDriver,
driverMessage,
drivers,
}) => {
if (!canManageDelivery || !["manager", "logistician", "admin", "mega_admin"].includes(userRole) || !order) {
return null;
}
const isPickupOrder = order.deliveryType === "pickup" || order.deliveryStatus === "pickup" || order.delivery_status === "pickup";
if (isPickupOrder) return null;
const ds = order.deliveryStatus || order.delivery_status;
const isDriverLocked = ["loaded", "on_route", "delivered"].includes(ds);
return (
<Panel className="space-y-4 p-5">
<div>
<strong>Назначение водителя</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
{(() => {
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}
{!isDriverLocked ? (
<div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
<Select
className="h-[46px] py-0"
value={selectedDriverId}
onChange={(e) => {
onDriverSelect(e.target.value);
}}
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={onConfirmDriver}
disabled={isSavingDeliveryChoice || !selectedDriverId}
>
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
</Button>
</div>
) : null}
{driverMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
) : null}
</Panel>
);
};
export { DriverAssignmentPanel };

View File

@ -42,9 +42,11 @@ 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 { CalendarWidget } from "./CalendarWidget";
import { StatusActionPanel } from "./StatusActionPanel";
import { DriverAssignmentPanel } from "./DriverAssignmentPanel";
import { supabase } from "../../supabaseClient";
import {
getOrderGroupDeliveryStatusLabel,
@ -55,7 +57,6 @@ import {
import { getErrorMessage, normalizeNom } from "../../utils/deliveryUtils";
const DELIVERY_TIME_OPTIONS = ["Первая половина дня", "Вторая половина дня"];
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
const STATUS_LABELS = DELIVERY_GROUP_STATUS_LABELS;
const ConfirmModal = ({ open, title, message, onConfirm, onCancel }) => {
@ -253,11 +254,6 @@ const isFutureDeliveryDate = (value) => {
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 "Выберите дату";
@ -950,193 +946,45 @@ export const OrderDetailPanel = ({
) : null}
</div>
) : deliveryType === "delivery" ? (
<div className="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10">
<div className="relative space-y-3 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-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]">
<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"
<CalendarWidget
label="Календарь доставки"
selectedDate={deliveryDate}
onDateChange={(dateKey) => { setDeliveryDate(dateKey); setFormMessage(""); }}
minDateKey={minSelectableDateKey}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
calendarDays={calendarDays}
monthLabel={monthLabel}
canGoBack={canGoBack}
timeOptions={DELIVERY_TIME_OPTIONS}
selectedTime={deliveryTime}
onTimeChange={(option) => { setDeliveryTime(option); setFormMessage(""); }}
layoutClassName="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10"
calendarClassName="relative space-y-3 md:min-w-0 md:flex-1 md:pr-4"
timeClassName="grid gap-2 sm:grid-cols-2 md:w-[320px] md:flex-none"
/>
) : 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">
<div className="relative">
<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-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]">
<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>
<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>
<CalendarWidget
label="Календарь самовывоза"
selectedDate={pickupDate}
onDateChange={(dateKey) => { setPickupDate(dateKey); setFormMessage(""); }}
minDateKey={minSelectableDateKey}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
calendarDays={calendarDays}
monthLabel={monthLabel}
canGoBack={canGoBack}
timeOptions={DELIVERY_TIME_OPTIONS}
selectedTime={pickupTimeSlot}
onTimeChange={(option) => { setPickupTimeSlot(option); setFormMessage(""); }}
layoutClassName="space-y-3"
calendarClassName="relative"
timeClassName="grid gap-2 sm:grid-cols-2"
/>
)}
<Button
className="w-full md:w-[180px] md:flex-none md:self-start"
@ -1152,120 +1000,35 @@ export const OrderDetailPanel = ({
) : null}
{canManageDelivery && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) && !isPickupOrder ? (
<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={() => setConfirmAction({ type: 'driver' })}
disabled={isSavingDeliveryChoice || !selectedDriverId}
>
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
</Button>
</div>
) : null;
})()}
{driverMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
) : null}
</Panel>
) : null}
<DriverAssignmentPanel
order={order}
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingDeliveryChoice={isSavingDeliveryChoice}
selectedDriverId={selectedDriverId}
onDriverSelect={(id) => { setSelectedDriverId(id); setDriverMessage(""); }}
onConfirmDriver={() => setConfirmAction({ type: 'driver' })}
driverMessage={driverMessage}
drivers={drivers}
/>
{["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;
<StatusActionPanel
order={order}
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingDeliveryChoice={isSavingDeliveryChoice}
onConfirmStatus={(action) => {
if (action.type === "hint") {
setFormMessage(action.hint);
} else if (action.type === "status") {
setConfirmAction({ type: "status", status: action.status });
}
setConfirmAction({ type: 'status', status: statusOption.value });
}}
disabled={isSavingDeliveryChoice}
>
{statusOption.label}
</Button>
</div>
);
})}
</div>
{formMessage ? (
/>
{formMessage && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? (
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p>
) : null}
</Panel>
) : null}
{["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? (

View File

@ -0,0 +1,68 @@
import React from "react";
import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Panel } from "../UI/Panel";
import { DELIVERY_GROUP_STATUS_LABELS } from "../../services/orderGroupViews";
const STATUS_LABELS = DELIVERY_GROUP_STATUS_LABELS;
const StatusActionPanel = ({
order,
userRole,
canManageDelivery,
isSavingDeliveryChoice,
onConfirmStatus,
}) => {
if (!canManageDelivery || !["manager", "logistician", "admin", "mega_admin"].includes(userRole) || !order) {
return null;
}
const currentStatus = order.deliveryStatus || order.delivery_status;
return (
<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 = currentStatus === statusOption.value;
const isClickable = statusOption.manual !== false && !isCurrent;
return (
<div key={statusOption.value} className="relative group">
<Button
variant={isCurrent ? "primary" : "secondary"}
onClick={() => {
if (!isClickable) {
onConfirmStatus?.({ type: "hint", hint: statusOption.hint || "" });
return;
}
onConfirmStatus?.({ type: "status", status: statusOption.value });
}}
disabled={isSavingDeliveryChoice}
>
{statusOption.label}
</Button>
</div>
);
})}
</div>
</Panel>
);
};
export { StatusActionPanel };