From 805ceca152b75e233597d0d11865ae0fd3ab3eb9 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 28 May 2026 10:03:19 +0000 Subject: [PATCH] =?UTF-8?q?fix:=208=20bugfixes=20=E2=80=94=20current=20und?= =?UTF-8?q?efined,=20auth=20email,=20sequential=20statuses,=20problem=20di?= =?UTF-8?q?alog,=20action=20log=20details,=20KPI=20layout,=20date=20sort,?= =?UTF-8?q?=20driver=20lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/admin/ActionLogPanel.jsx | 51 ++++- src/components/admin/AdminDashboard.jsx | 4 +- src/components/dashboard/KpiCard.jsx | 4 +- .../driver/DriverDeliveryDetail.jsx | 92 +++++++- src/components/orders/OrderDetailPanel.jsx | 200 +++++++++++++----- src/constants/deliveryWorkflow.js | 6 +- src/hooks/useOrderGroups.js | 4 +- src/services/orderGroupViews.js | 2 +- src/services/supabase/orderGroupRepository.js | 34 ++- 9 files changed, 294 insertions(+), 103 deletions(-) diff --git a/src/components/admin/ActionLogPanel.jsx b/src/components/admin/ActionLogPanel.jsx index 0cc00a6..31c05cd 100644 --- a/src/components/admin/ActionLogPanel.jsx +++ b/src/components/admin/ActionLogPanel.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Panel } from "../UI/Panel"; import { Badge } from "../UI/Badge"; import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService"; +import { useNavigate } from "react-router-dom"; import { useAuth } from "../../context/AuthContext"; const ACTIONS = [ @@ -78,6 +79,7 @@ const ROLE_LABELS = { export const ActionLogPanel = ({ orderGroupId = null }) => { const { user } = useAuth(); + const navigate = useNavigate(); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -194,15 +196,16 @@ export const ActionLogPanel = ({ orderGroupId = null }) => { Дата/Время Сотрудник Действие + Действие Было Стало - {!orderGroupId && Группа} + {!orderGroupId && Группа доставки} {filteredLogs.length === 0 && !loading && ( - + Нет записей @@ -224,27 +227,57 @@ export const ActionLogPanel = ({ orderGroupId = null }) => { {getActionLabel(log.action)} +
+ {log.action === "status_change" && (log.new_value || "")} + {log.action === "driver_assigned" && "→ " + (log.new_value || "водитель")} + {log.action === "driver_removed" && "← " + (log.old_value || "водитель")} + {log.action === "date_assigned" && (log.new_value || "")} + {log.action === "paid_storage" && (log.new_value || "")} +
{log.old_value || "—"} {log.new_value || "—"} {!orderGroupId && ( - - {log.order_group_id?.slice(0, 8)}... + + {log.order_group_id ? ( + { e.preventDefault(); navigate(`/dashboard/group/${log.order_group_id}`); }} + > + Группа + + ) : "—"} )} {expandedId === log.id && (log.details || log.old_value?.length > 40 || log.new_value?.length > 40) && ( - +
- {log.old_value?.length > 40 && ( + {log.old_value && (
Было: {log.old_value}
)} - {log.new_value?.length > 40 && ( + {log.new_value && (
Стало: {log.new_value}
)} - {log.details && ( -
Детали: {JSON.stringify(log.details)}
+ {log.details && typeof log.details === "object" && ( +
+ {Object.entries(log.details).map(([k, v]) => ( +
{k}: {String(v)}
+ ))} +
+ )} + {log.details && typeof log.details === "string" && ( +
Детали: {log.details}
+ )} + {log.order_group_id && ( +
Группа:{" "} + { e.preventDefault(); navigate(`/dashboard/group/${log.order_group_id}`); }} + >Перейти к группе +
)}
diff --git a/src/components/admin/AdminDashboard.jsx b/src/components/admin/AdminDashboard.jsx index 6688054..bb703c2 100644 --- a/src/components/admin/AdminDashboard.jsx +++ b/src/components/admin/AdminDashboard.jsx @@ -141,7 +141,7 @@ export const AdminDashboard = () => { {/* KPI — centered on mobile */} -
+
{[ { label: 'Всего', val: totalGroups }, { label: 'Ожидает', val: sv.pending }, @@ -152,7 +152,7 @@ export const AdminDashboard = () => { ].map((kpi, i) => (
{kpi.label}
-
{kpi.val ?? '—'}
+
{kpi.val ?? '—'}
))}
diff --git a/src/components/dashboard/KpiCard.jsx b/src/components/dashboard/KpiCard.jsx index 1e2a6db..c782372 100644 --- a/src/components/dashboard/KpiCard.jsx +++ b/src/components/dashboard/KpiCard.jsx @@ -5,9 +5,9 @@ export const KpiCard = ({ label, value, hint }) => { return (

{label}

-
+
{value} - {hint} + {hint &&

{hint}

}
); diff --git a/src/components/driver/DriverDeliveryDetail.jsx b/src/components/driver/DriverDeliveryDetail.jsx index cfd5907..4ac9276 100644 --- a/src/components/driver/DriverDeliveryDetail.jsx +++ b/src/components/driver/DriverDeliveryDetail.jsx @@ -1,10 +1,42 @@ -import React from "react"; +import React, { useState } from "react"; import { getAvailableTransitionsByRole, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow"; import { getDeliveryCity, getDeliveryDay, getDeliveryHalfDay } from "../../services/driverDeliveries"; import { Badge } from "../UI/Badge"; import { Button } from "../UI/Button"; import { Panel } from "../UI/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 }) => ( +
+ e.stopPropagation()}> +

Причина проблемы

+

Укажите причину возникшей проблемы с доставкой.

+
+ {PROBLEM_REASONS.map((reason) => ( + + ))} +
+
+ +
+
+
+); + const splitItem = (item) => { if (!item) { return { name: "Позиция", quantity: "" }; @@ -29,6 +61,8 @@ const splitItem = (item) => { }; export const DriverDeliveryDetail = ({ order, onStatusChange }) => { + const [showProblemModal, setShowProblemModal] = useState(false); + if (!order) { return null; } @@ -39,8 +73,42 @@ export const DriverDeliveryDetail = ({ order, onStatusChange }) => { }); const orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : []; + const currentStatus = order.status; + const IN_TRANSIT_STATUSES = ["Загружен", "В пути"]; + const isOnRoute = IN_TRANSIT_STATUSES.includes(currentStatus); + + let actionButtons = []; + if (currentStatus === "Назначен водитель") { + actionButtons = [ + { value: "Загружен", label: "Загружено" }, + { value: "Проблема доставки", label: "Проблема" }, + ]; + } else if (isOnRoute) { + actionButtons = [ + { value: "Доставлен", label: "Доставлено" }, + { value: "Проблема доставки", label: "Проблема" }, + ]; + } else if (currentStatus === "Доставлен" || currentStatus === "Проблема доставки" || currentStatus === "Закрыт" || currentStatus === "Отменён") { + actionButtons = []; + } else { + actionButtons = availableTransitions.map((status) => ({ + value: status, + label: status === "Проблема доставки" ? "Проблема" : status, + })); + } + return (
+ {showProblemModal && ( + { + setShowProblemModal(false); + onStatusChange?.("Проблема доставки", { reason: reasonValue, reasonLabel }); + }} + onCancel={() => setShowProblemModal(false)} + /> + )} +
@@ -110,22 +178,28 @@ export const DriverDeliveryDetail = ({ order, onStatusChange }) => {
- {availableTransitions.length ? ( + {actionButtons.length > 0 && (

Быстрые действия

- {availableTransitions.map((status) => ( + {actionButtons.map((btn) => ( ))}
- ) : null} + )}
); -}; +}; \ No newline at end of file diff --git a/src/components/orders/OrderDetailPanel.jsx b/src/components/orders/OrderDetailPanel.jsx index d4d7602..e1d3e9c 100644 --- a/src/components/orders/OrderDetailPanel.jsx +++ b/src/components/orders/OrderDetailPanel.jsx @@ -398,6 +398,39 @@ const PaidStoragePanel = ({ order, onChangeDeliveryStatus, isSavingDeliveryChoic ); }; + +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) => ( + + ))} +
+
+ +
+
+
+); + export const OrderDetailPanel = ({ order, canManageDelivery = false, @@ -408,6 +441,7 @@ export const OrderDetailPanel = ({ onChangeDeliveryStatus, userRole, }) => { + const [problemReason, setProblemReason] = React.useState(null); const [deliveryDate, setDeliveryDate] = React.useState(""); const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]); const [formMessage, setFormMessage] = React.useState(""); @@ -804,9 +838,15 @@ export const OrderDetailPanel = ({
Назначение водителя

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

{order.assignedDriverId ? ( @@ -824,29 +864,35 @@ export const OrderDetailPanel = ({
) : null} -
- - -
+ {(() => { + const ds = order.deliveryStatus || order.delivery_status; + const isDriverLocked = ["loaded", "on_route", "delivered"].includes(ds); + return !isDriverLocked ? ( +
+ + +
+ ) : null; + })()} {driverMessage ? (

{driverMessage}

) : null} @@ -924,7 +970,7 @@ export const OrderDetailPanel = ({ ) : null} - {userRole === "driver" && order && onChangeDeliveryStatus ? ( +{userRole === "driver" && order && onChangeDeliveryStatus ? (
Статус доставки @@ -932,38 +978,78 @@ export const OrderDetailPanel = ({ Обновите статус по мере выполнения доставки.

-
- {[ - { value: "loaded", label: "Загружено" }, - { value: "on_route", label: "В пути" }, - { value: "delivered", label: "Доставлено" }, - { value: "problem", label: "Проблема" }, - ].map((statusOption) => ( - - ))} + onChangeDeliveryStatus({ + orderGroupId: order.id, + status: statusOption.value, + }).then((response) => { + if (!response.success) { + setFormMessage(response.error || "Не удалось обновить статус"); + } else { + setFormMessage(""); + } + }); + }} + disabled={isSavingDeliveryChoice} + > + {statusOption.label} + + )); + })()}
{formMessage ? (

{formMessage}

diff --git a/src/constants/deliveryWorkflow.js b/src/constants/deliveryWorkflow.js index ffcdd00..1ad1ad7 100644 --- a/src/constants/deliveryWorkflow.js +++ b/src/constants/deliveryWorkflow.js @@ -223,7 +223,7 @@ export const ORDER_STATUS_TRANSITIONS = { "Доставка согласована": ["Назначен водитель", "Ожидает согласования доставки", "Проблема доставки"], "Передан логисту": ["Доставка согласована", "Платное хранение", "Проблема доставки", "Отменён"], "Назначен водитель": ["Загружен", "Проблема доставки"], - Загружен: ["В пути", "Проблема доставки"], + Загружен: ["Доставлен", "Проблема доставки"], "В пути": ["Доставлен", "Проблема доставки"], Доставлен: ["Закрыт"], "Проблема доставки": ["Ожидает согласования доставки", "Назначен водитель", "Отменён", "Закрыт"], @@ -248,7 +248,7 @@ export const ROLE_TRANSITION_TARGETS = { "Закрыт", "Отменён", ], - driver: ["Загружен", "В пути", "Доставлен", "Проблема доставки"], + driver: ["Загружен", "Доставлен", "Проблема доставки"], admin: ORDER_STATUSES, }; @@ -267,7 +267,7 @@ export const LOGISTICS_STATUSES = [ "Проблема доставки", ]; -export const DRIVER_STATUSES = ["Назначен водитель", "Загружен", "В пути", "Доставлен"]; +export const DRIVER_STATUSES = ["Назначен водитель", "Загружен", "Доставлен"]; export const getOrderStatusComment = (status) => ORDER_STATUS_META[status]?.comment || "Комментарий не задан."; diff --git a/src/hooks/useOrderGroups.js b/src/hooks/useOrderGroups.js index 1c65850..6b5c922 100644 --- a/src/hooks/useOrderGroups.js +++ b/src/hooks/useOrderGroups.js @@ -176,10 +176,10 @@ export const useOrderGroups = () => { } }, []); - const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status }) => { + const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status, details }) => { setIsSavingDeliveryChoice(true); try { - const result = await updateDeliveryStatus({ orderGroupId, status }); + const result = await updateDeliveryStatus({ orderGroupId, status, details }); if (result.error) { return { success: false, diff --git a/src/services/orderGroupViews.js b/src/services/orderGroupViews.js index 1e5b56e..fd28939 100644 --- a/src/services/orderGroupViews.js +++ b/src/services/orderGroupViews.js @@ -366,7 +366,7 @@ export const groupOrderGroupsByDate = (groups) => { const rightTime = parseGroupDate(rightDate)?.getTime(); if (leftTime != null && rightTime != null && leftTime !== rightTime) { - return rightTime - leftTime; + return leftTime - rightTime; } return leftDate.localeCompare(rightDate); diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js index 3a1b472..6b342ac 100644 --- a/src/services/supabase/orderGroupRepository.js +++ b/src/services/supabase/orderGroupRepository.js @@ -277,10 +277,19 @@ export const assignDriverToOrderGroup = async ({ }, "Ошибка назначения водителя"); }; -export const updateDeliveryStatus = async ({ orderGroupId, status }) => { +export const updateDeliveryStatus = async ({ orderGroupId, status, details } = {}) => { return safeSupabaseCall(async () => { const client = requireSupabase(); + // Fetch current status before any update (needed for audit log) + const { data: current, error: fetchCurrentError } = await client + .from("order_groups") + .select("delivery_status") + .eq("id", orderGroupId) + .single(); + + if (fetchCurrentError) throw fetchCurrentError; + // Bypass stale RPC for paid_storage transitions // Server-side RPC still enforces driver-assignment checks that block // manager/logistician from moving groups into/out of paid_storage. @@ -297,17 +306,7 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => { .eq("id", orderGroupId); if (updateError) throw updateError; - } else { - // For cancelling paid_storage: check current status first - const { data: current, error: fetchError } = await client - .from("order_groups") - .select("delivery_status") - .eq("id", orderGroupId) - .single(); - - if (fetchError) throw fetchError; - - if (current.delivery_status === "paid_storage" && status === "pending_confirmation") { + } else if (current.delivery_status === "paid_storage" && status === "pending_confirmation") { const { error: updateError } = await client .from("order_groups") .update({ @@ -318,15 +317,14 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => { .eq("id", orderGroupId); if (updateError) throw updateError; - } else { - // All other statuses use the RPC (driver workflows, etc.) - const { error: rpcError } = await client.rpc("update_delivery_status", { + } else { + // All other statuses use the RPC (driver workflows, etc.) + const { error: rpcError } = await client.rpc("update_delivery_status", { p_order_group_id: orderGroupId, p_status: status, }); - if (rpcError) throw rpcError; - } + if (rpcError) throw rpcError; } // Fetch updated group @@ -343,7 +341,7 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => { : status === "cancelled" ? "cancelled" : "status_change"; const oldValue = current?.delivery_status || null; - await logAction({ orderGroupId, action: logActionType, oldValue, newValue: status, details: { source: "admin_panel" } }).catch(() => {}); + await logAction({ orderGroupId, action: logActionType, oldValue, newValue: status, details: { source: "admin_panel", ...details } }).catch(() => {}); return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка обновления статуса доставки");