From 129175fed7012d6d14042d0799785f9a971508a3 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 12 Jun 2026 15:30:30 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=202=20decomposition=20?= =?UTF-8?q?=E2=80=94=20extract=20CalendarWidget,=20StatusActionPanel,=20Dr?= =?UTF-8?q?iverAssignmentPanel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/orders/CalendarWidget.jsx | 185 +++++++++ .../orders/DriverAssignmentPanel.jsx | 89 +++++ src/components/orders/OrderDetailPanel.jsx | 371 ++++-------------- src/components/orders/StatusActionPanel.jsx | 68 ++++ 4 files changed, 409 insertions(+), 304 deletions(-) create mode 100644 src/components/orders/CalendarWidget.jsx create mode 100644 src/components/orders/DriverAssignmentPanel.jsx create mode 100644 src/components/orders/StatusActionPanel.jsx diff --git a/src/components/orders/CalendarWidget.jsx b/src/components/orders/CalendarWidget.jsx new file mode 100644 index 0000000..2290a4a --- /dev/null +++ b/src/components/orders/CalendarWidget.jsx @@ -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 ( +
+
+ + {isCalendarOpen ? ( +
+
+
+

+ {label} +

+

+ {monthLabel} +

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

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

+
+ ) : null} +
+
+ {timeOptions.map((option) => ( + + ))} +
+
+ ); +}; + +export { CalendarWidget, formatDateForDisplay }; \ No newline at end of file diff --git a/src/components/orders/DriverAssignmentPanel.jsx b/src/components/orders/DriverAssignmentPanel.jsx new file mode 100644 index 0000000..af511cb --- /dev/null +++ b/src/components/orders/DriverAssignmentPanel.jsx @@ -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 ( + +
+ Назначение водителя +

+ {(() => { + if (["loaded", "on_route", "delivered"].includes(ds)) { + return "Доставка в процессе — сменить водителя нельзя."; + } + return order.assignedDriverId + ? "Назначен водитель. Вы можете изменить назначение." + : "Выберите водителя для доставки."; + })()} +

+
+ {order.assignedDriverId ? ( +
+
+
+

+ Водитель назначен +

+

+ {order.assignedDriverName || "Неизвестно"} +

+
+ Назначен +
+
+ ) : null} + {!isDriverLocked ? ( +
+ + +
+ ) : null} + {driverMessage ? ( +

{driverMessage}

+ ) : null} +
+ ); +}; + +export { DriverAssignmentPanel }; \ No newline at end of file diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx index 62b56a8..ebc0580 100644 --- a/src/components/orders/OrderDetailPanel.jsx +++ b/src/components/orders/OrderDetailPanel.jsx @@ -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}
) : deliveryType === "delivery" ? ( -
-
- - {isCalendarOpen ? ( -
-
-
-

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

-

- {monthLabel} -

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

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

-
- ) : null} -
-
- {DELIVERY_TIME_OPTIONS.map((option) => ( - - ))} -
-
+ { 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" + /> ) : ( -
-
- - {isCalendarOpen ? ( -
-
-
-

Календарь самовывоза

-

{monthLabel}

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

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

-
- ) : null} -
-
- {DELIVERY_TIME_OPTIONS.map((option) => ( - - ))} -
-
+ { 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" + /> )} -
- ) : null; - })()} - {driverMessage ? ( -

{driverMessage}

- ) : null} - - ) : null} + { setSelectedDriverId(id); setDriverMessage(""); }} + onConfirmDriver={() => setConfirmAction({ type: 'driver' })} + driverMessage={driverMessage} + drivers={drivers} + /> - {["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: "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 ( -
- -
- ); - })} -
- {formMessage ? ( -

{formMessage}

- ) : null} -
+ { + if (action.type === "hint") { + setFormMessage(action.hint); + } else if (action.type === "status") { + setConfirmAction({ type: "status", status: action.status }); + } + }} + /> + {formMessage && ["manager", "logistician", "admin", "mega_admin"].includes(userRole) && order && onChangeDeliveryStatus ? ( +

{formMessage}

) : null} diff --git a/src/components/orders/StatusActionPanel.jsx b/src/components/orders/StatusActionPanel.jsx new file mode 100644 index 0000000..b173912 --- /dev/null +++ b/src/components/orders/StatusActionPanel.jsx @@ -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 ( + +
+ Статус доставки +

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

+
+
+ {[ + { 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 ( +
+ +
+ ); + })} +
+
+ ); +}; + +export { StatusActionPanel }; \ No newline at end of file