fix: 8 bugfixes — current undefined, auth email, sequential statuses, problem dialog, action log details, KPI layout, date sort, driver lock

This commit is contained in:
root 2026-05-28 10:03:19 +00:00
parent 2ee437e83e
commit 805ceca152
9 changed files with 294 additions and 103 deletions

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Panel } from "../UI/Panel"; import { Panel } from "../UI/Panel";
import { Badge } from "../UI/Badge"; import { Badge } from "../UI/Badge";
import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService"; import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
const ACTIONS = [ const ACTIONS = [
@ -78,6 +79,7 @@ const ROLE_LABELS = {
export const ActionLogPanel = ({ orderGroupId = null }) => { export const ActionLogPanel = ({ orderGroupId = null }) => {
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate();
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@ -194,15 +196,16 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
<th className="pb-2 pr-3 font-medium">Дата/Время</th> <th className="pb-2 pr-3 font-medium">Дата/Время</th>
<th className="pb-2 pr-3 font-medium">Сотрудник</th> <th className="pb-2 pr-3 font-medium">Сотрудник</th>
<th className="pb-2 pr-3 font-medium">Действие</th> <th className="pb-2 pr-3 font-medium">Действие</th>
<th className="pb-2 pr-3 font-medium">Действие</th>
<th className="pb-2 pr-3 font-medium">Было</th> <th className="pb-2 pr-3 font-medium">Было</th>
<th className="pb-2 pr-3 font-medium">Стало</th> <th className="pb-2 pr-3 font-medium">Стало</th>
{!orderGroupId && <th className="pb-2 pr-3 font-medium">Группа</th>} {!orderGroupId && <th className="pb-2 pr-3 font-medium">Группа доставки</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredLogs.length === 0 && !loading && ( {filteredLogs.length === 0 && !loading && (
<tr> <tr>
<td colSpan={orderGroupId ? 5 : 6} className="py-6 text-center text-[var(--color-text-muted)]"> <td colSpan={orderGroupId ? 6 : 7} className="py-6 text-center text-[var(--color-text-muted)]">
Нет записей Нет записей
</td> </td>
</tr> </tr>
@ -224,27 +227,57 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
<Badge tone={ACTION_TONES[log.action] || "accent"}> <Badge tone={ACTION_TONES[log.action] || "accent"}>
{getActionLabel(log.action)} {getActionLabel(log.action)}
</Badge> </Badge>
<div className="mt-0.5 text-xs text-[var(--color-text-muted)]">
{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 || "")}
</div>
</td> </td>
<td className="py-2 pr-3 max-w-[150px] truncate">{log.old_value || "—"}</td> <td className="py-2 pr-3 max-w-[150px] truncate">{log.old_value || "—"}</td>
<td className="py-2 pr-3 max-w-[150px] truncate">{log.new_value || "—"}</td> <td className="py-2 pr-3 max-w-[150px] truncate">{log.new_value || "—"}</td>
{!orderGroupId && ( {!orderGroupId && (
<td className="py-2 pr-3 text-xs font-mono text-[var(--color-text-muted)]"> <td className="py-2 pr-3 text-sm">
{log.order_group_id?.slice(0, 8)}... {log.order_group_id ? (
<a
href={`/dashboard/group/${log.order_group_id}`}
className="text-[var(--color-accent)] hover:underline font-medium"
onClick={(e) => { e.preventDefault(); navigate(`/dashboard/group/${log.order_group_id}`); }}
>
Группа
</a>
) : "—"}
</td> </td>
)} )}
</tr> </tr>
{expandedId === log.id && (log.details || log.old_value?.length > 40 || log.new_value?.length > 40) && ( {expandedId === log.id && (log.details || log.old_value?.length > 40 || log.new_value?.length > 40) && (
<tr className="bg-[var(--color-surface-strong)]"> <tr className="bg-[var(--color-surface-strong)]">
<td colSpan={orderGroupId ? 5 : 6} className="py-2 px-3"> <td colSpan={orderGroupId ? 6 : 7} className="py-2 px-3">
<div className="space-y-1 text-xs"> <div className="space-y-1 text-xs">
{log.old_value?.length > 40 && ( {log.old_value && (
<div><span className="text-[var(--color-text-muted)]">Было:</span> {log.old_value}</div> <div><span className="text-[var(--color-text-muted)]">Было:</span> {log.old_value}</div>
)} )}
{log.new_value?.length > 40 && ( {log.new_value && (
<div><span className="text-[var(--color-text-muted)]">Стало:</span> {log.new_value}</div> <div><span className="text-[var(--color-text-muted)]">Стало:</span> {log.new_value}</div>
)} )}
{log.details && ( {log.details && typeof log.details === "object" && (
<div><span className="text-[var(--color-text-muted)]">Детали:</span> {JSON.stringify(log.details)}</div> <div className="space-y-0.5">
{Object.entries(log.details).map(([k, v]) => (
<div key={k}><span className="text-[var(--color-text-muted)]">{k}:</span> {String(v)}</div>
))}
</div>
)}
{log.details && typeof log.details === "string" && (
<div><span className="text-[var(--color-text-muted)]">Детали:</span> {log.details}</div>
)}
{log.order_group_id && (
<div><span className="text-[var(--color-text-muted)]">Группа:</span>{" "}
<a href={`/dashboard/group/${log.order_group_id}`}
className="text-[var(--color-accent)] hover:underline"
onClick={(e) => { e.preventDefault(); navigate(`/dashboard/group/${log.order_group_id}`); }}
>Перейти к группе</a>
</div>
)} )}
</div> </div>
</td> </td>

View File

@ -141,7 +141,7 @@ export const AdminDashboard = () => {
</div> </div>
{/* KPI — centered on mobile */} {/* KPI — centered on mobile */}
<div style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr 1fr' : `repeat(auto-fit, minmax(${kpiMin}, 1fr))`, gap: '0.4rem' }}> <div style={{ display: 'grid', gridTemplateColumns: mobile ? '1fr 1fr' : `repeat(auto-fit, minmax(${kpiMin}, 160px))`, gap: '0.4rem' }}>
{[ {[
{ label: 'Всего', val: totalGroups }, { label: 'Всего', val: totalGroups },
{ label: 'Ожидает', val: sv.pending }, { label: 'Ожидает', val: sv.pending },
@ -152,7 +152,7 @@ export const AdminDashboard = () => {
].map((kpi, i) => ( ].map((kpi, i) => (
<Panel key={i} style={{ padding: mobile ? '0.4rem 0.6rem' : '0.5rem 0.75rem', textAlign: 'center' }}> <Panel key={i} style={{ padding: mobile ? '0.4rem 0.6rem' : '0.5rem 0.75rem', textAlign: 'center' }}>
<div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)', marginBottom: '0.05rem' }}>{kpi.label}</div> <div style={{ fontSize: fontSize.xs, color: 'var(--color-text-muted)', marginBottom: '0.05rem' }}>{kpi.label}</div>
<div style={{ fontSize: mobile ? '1rem' : '1.2rem', fontWeight: 700, color: 'var(--color-text)' }}>{kpi.val ?? '—'}</div> <div style={{ fontSize: mobile ? '1.1rem' : '1.3rem', fontWeight: 700, color: 'var(--color-text)', textAlign: 'center' }}>{kpi.val ?? '—'}</div>
</Panel> </Panel>
))} ))}
</div> </div>

View File

@ -5,9 +5,9 @@ export const KpiCard = ({ label, value, hint }) => {
return ( return (
<Panel className="p-5"> <Panel className="p-5">
<p className="text-sm text-[var(--color-text-muted)]">{label}</p> <p className="text-sm text-[var(--color-text-muted)]">{label}</p>
<div className="mt-4 flex items-end justify-between gap-4"> <div className="mt-3">
<span className="text-3xl font-semibold">{value}</span> <span className="text-3xl font-semibold">{value}</span>
<span className="text-xs text-[var(--color-text-muted)]">{hint}</span> {hint && <p className="mt-1 text-xs text-[var(--color-text-muted)]">{hint}</p>}
</div> </div>
</Panel> </Panel>
); );

View File

@ -1,10 +1,42 @@
import React from "react"; import React, { useState } from "react";
import { getAvailableTransitionsByRole, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow"; import { getAvailableTransitionsByRole, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow";
import { getDeliveryCity, getDeliveryDay, getDeliveryHalfDay } from "../../services/driverDeliveries"; import { getDeliveryCity, getDeliveryDay, getDeliveryHalfDay } from "../../services/driverDeliveries";
import { Badge } from "../UI/Badge"; import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button"; import { Button } from "../UI/Button";
import { Panel } from "../UI/Panel"; 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 }) => (
<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>
);
const splitItem = (item) => { const splitItem = (item) => {
if (!item) { if (!item) {
return { name: "Позиция", quantity: "" }; return { name: "Позиция", quantity: "" };
@ -29,6 +61,8 @@ const splitItem = (item) => {
}; };
export const DriverDeliveryDetail = ({ order, onStatusChange }) => { export const DriverDeliveryDetail = ({ order, onStatusChange }) => {
const [showProblemModal, setShowProblemModal] = useState(false);
if (!order) { if (!order) {
return null; return null;
} }
@ -39,8 +73,42 @@ export const DriverDeliveryDetail = ({ order, onStatusChange }) => {
}); });
const orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : []; 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 ( return (
<div className="space-y-4"> <div className="space-y-4">
{showProblemModal && (
<ProblemReasonModal
onSelect={(reasonValue, reasonLabel) => {
setShowProblemModal(false);
onStatusChange?.("Проблема доставки", { reason: reasonValue, reasonLabel });
}}
onCancel={() => setShowProblemModal(false)}
/>
)}
<Panel className="space-y-5 p-6"> <Panel className="space-y-5 p-6">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-start justify-between gap-4">
<div> <div>
@ -110,22 +178,28 @@ export const DriverDeliveryDetail = ({ order, onStatusChange }) => {
</div> </div>
</Panel> </Panel>
{availableTransitions.length ? ( {actionButtons.length > 0 && (
<Panel className="space-y-4 p-6"> <Panel className="space-y-4 p-6">
<h3 className="text-lg font-semibold">Быстрые действия</h3> <h3 className="text-lg font-semibold">Быстрые действия</h3>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{availableTransitions.map((status) => ( {actionButtons.map((btn) => (
<Button <Button
key={status} key={btn.value}
variant={status === "Проблема доставки" ? "ghost" : "secondary"} variant={btn.value === "Проблема доставки" ? "ghost" : "secondary"}
onClick={() => onStatusChange?.(status)} onClick={() => {
if (btn.value === "Проблема доставки") {
setShowProblemModal(true);
return;
}
onStatusChange?.(btn.value);
}}
> >
{status} {btn.label}
</Button> </Button>
))} ))}
</div> </div>
</Panel> </Panel>
) : null} )}
</div> </div>
); );
}; };

View File

@ -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 }) => (
<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 = ({ export const OrderDetailPanel = ({
order, order,
canManageDelivery = false, canManageDelivery = false,
@ -408,6 +441,7 @@ export const OrderDetailPanel = ({
onChangeDeliveryStatus, onChangeDeliveryStatus,
userRole, userRole,
}) => { }) => {
const [problemReason, setProblemReason] = React.useState(null);
const [deliveryDate, setDeliveryDate] = React.useState(""); const [deliveryDate, setDeliveryDate] = React.useState("");
const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]); const [deliveryTime, setDeliveryTime] = React.useState(DELIVERY_TIME_OPTIONS[0]);
const [formMessage, setFormMessage] = React.useState(""); const [formMessage, setFormMessage] = React.useState("");
@ -804,9 +838,15 @@ export const OrderDetailPanel = ({
<div> <div>
<strong>Назначение водителя</strong> <strong>Назначение водителя</strong>
<p className="mt-1 text-sm text-[var(--color-text-muted)]"> <p className="mt-1 text-sm text-[var(--color-text-muted)]">
{order.assignedDriverId {(() => {
? `Назначен водитель: ${order.assignedDriverName || "Неизвестно"}. Вы можете изменить назначение.` const ds = order.deliveryStatus || order.delivery_status;
: "Выберите водителя для доставки."} if (["loaded", "on_route", "delivered"].includes(ds)) {
return "Доставка в процессе — сменить водителя нельзя.";
}
return order.assignedDriverId
? "Назначен водитель. Вы можете изменить назначение."
: "Выберите водителя для доставки.";
})()}
</p> </p>
</div> </div>
{order.assignedDriverId ? ( {order.assignedDriverId ? (
@ -824,6 +864,10 @@ export const OrderDetailPanel = ({
</div> </div>
</div> </div>
) : null} ) : 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]"> <div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
<Select <Select
className="h-[46px] py-0" className="h-[46px] py-0"
@ -847,6 +891,8 @@ export const OrderDetailPanel = ({
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"} {isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
</Button> </Button>
</div> </div>
) : null;
})()}
{driverMessage ? ( {driverMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p> <p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
) : null} ) : null}
@ -932,20 +978,59 @@ export const OrderDetailPanel = ({
Обновите статус по мере выполнения доставки. Обновите статус по мере выполнения доставки.
</p> </p>
</div> </div>
{problemReason !== null ? (
<ProblemReasonModal
onSelect={(reasonValue, reasonLabel) => {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "problem",
details: { reason: reasonValue, reasonLabel },
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось обновить статус");
} else {
setFormMessage("Статус обновлён: проблема — " + reasonLabel);
}
setProblemReason(null);
});
}}
onCancel={() => setProblemReason(null)}
/>
) : null}
<div className="flex flex-wrap gap-2"> <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 availableButtons = [];
if (currentStatus === "driver_assigned") {
availableButtons = [
{ value: "loaded", label: "Загружено" }, { value: "loaded", label: "Загружено" },
{ value: "on_route", label: "В пути" }, { value: "problem", label: "Проблема" },
];
} else if (isOnRoute) {
availableButtons = [
{ value: "delivered", label: "Доставлено" }, { value: "delivered", label: "Доставлено" },
{ value: "problem", label: "Проблема" }, { value: "problem", label: "Проблема" },
].map((statusOption) => ( ];
} else if (currentStatus === "delivered" || currentStatus === "problem" || currentStatus === "cancelled" || currentStatus === "paid_storage") {
availableButtons = [];
} else {
availableButtons = [
{ value: "loaded", label: "Загружено" },
{ value: "delivered", label: "Доставлено" },
{ value: "problem", label: "Проблема" },
];
}
return availableButtons.map((statusOption) => (
<Button <Button
key={statusOption.value} key={statusOption.value}
variant={ variant={currentStatus === statusOption.value ? "primary" : "secondary"}
(order.deliveryStatus || order.delivery_status) === statusOption.value ? "primary" : "secondary"}
onClick={() => { onClick={() => {
if (statusOption.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) { if (statusOption.value === "problem") {
setFormMessage("Укажите причину для каждой неотгруженной позиции перед завершением доставки."); setProblemReason("selecting");
return; return;
} }
onChangeDeliveryStatus({ onChangeDeliveryStatus({
@ -963,7 +1048,8 @@ export const OrderDetailPanel = ({
> >
{statusOption.label} {statusOption.label}
</Button> </Button>
))} ));
})()}
</div> </div>
{formMessage ? ( {formMessage ? (
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p> <p className="text-sm text-[var(--color-warning)]">{formMessage}</p>

View File

@ -223,7 +223,7 @@ export const ORDER_STATUS_TRANSITIONS = {
"Доставка согласована": ["Назначен водитель", "Ожидает согласования доставки", "Проблема доставки"], "Доставка согласована": ["Назначен водитель", "Ожидает согласования доставки", "Проблема доставки"],
"Передан логисту": ["Доставка согласована", "Платное хранение", "Проблема доставки", "Отменён"], "Передан логисту": ["Доставка согласована", "Платное хранение", "Проблема доставки", "Отменён"],
"Назначен водитель": ["Загружен", "Проблема доставки"], "Назначен водитель": ["Загружен", "Проблема доставки"],
Загружен: ["В пути", "Проблема доставки"], Загружен: ["Доставлен", "Проблема доставки"],
"В пути": ["Доставлен", "Проблема доставки"], "В пути": ["Доставлен", "Проблема доставки"],
Доставлен: ["Закрыт"], Доставлен: ["Закрыт"],
"Проблема доставки": ["Ожидает согласования доставки", "Назначен водитель", "Отменён", "Закрыт"], "Проблема доставки": ["Ожидает согласования доставки", "Назначен водитель", "Отменён", "Закрыт"],
@ -248,7 +248,7 @@ export const ROLE_TRANSITION_TARGETS = {
"Закрыт", "Закрыт",
"Отменён", "Отменён",
], ],
driver: ["Загружен", "В пути", "Доставлен", "Проблема доставки"], driver: ["Загружен", "Доставлен", "Проблема доставки"],
admin: ORDER_STATUSES, 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 || "Комментарий не задан."; export const getOrderStatusComment = (status) => ORDER_STATUS_META[status]?.comment || "Комментарий не задан.";

View File

@ -176,10 +176,10 @@ export const useOrderGroups = () => {
} }
}, []); }, []);
const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status }) => { const changeDeliveryStatus = React.useCallback(async ({ orderGroupId, status, details }) => {
setIsSavingDeliveryChoice(true); setIsSavingDeliveryChoice(true);
try { try {
const result = await updateDeliveryStatus({ orderGroupId, status }); const result = await updateDeliveryStatus({ orderGroupId, status, details });
if (result.error) { if (result.error) {
return { return {
success: false, success: false,

View File

@ -366,7 +366,7 @@ export const groupOrderGroupsByDate = (groups) => {
const rightTime = parseGroupDate(rightDate)?.getTime(); const rightTime = parseGroupDate(rightDate)?.getTime();
if (leftTime != null && rightTime != null && leftTime !== rightTime) { if (leftTime != null && rightTime != null && leftTime !== rightTime) {
return rightTime - leftTime; return leftTime - rightTime;
} }
return leftDate.localeCompare(rightDate); return leftDate.localeCompare(rightDate);

View File

@ -277,10 +277,19 @@ export const assignDriverToOrderGroup = async ({
}, "Ошибка назначения водителя"); }, "Ошибка назначения водителя");
}; };
export const updateDeliveryStatus = async ({ orderGroupId, status }) => { export const updateDeliveryStatus = async ({ orderGroupId, status, details } = {}) => {
return safeSupabaseCall(async () => { return safeSupabaseCall(async () => {
const client = requireSupabase(); 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 // Bypass stale RPC for paid_storage transitions
// Server-side RPC still enforces driver-assignment checks that block // Server-side RPC still enforces driver-assignment checks that block
// manager/logistician from moving groups into/out of paid_storage. // manager/logistician from moving groups into/out of paid_storage.
@ -297,17 +306,7 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => {
.eq("id", orderGroupId); .eq("id", orderGroupId);
if (updateError) throw updateError; if (updateError) throw updateError;
} else { } else if (current.delivery_status === "paid_storage" && status === "pending_confirmation") {
// 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") {
const { error: updateError } = await client const { error: updateError } = await client
.from("order_groups") .from("order_groups")
.update({ .update({
@ -327,7 +326,6 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => {
if (rpcError) throw rpcError; if (rpcError) throw rpcError;
} }
}
// Fetch updated group // Fetch updated group
const { data, error } = await client const { data, error } = await client
@ -343,7 +341,7 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => {
: status === "cancelled" ? "cancelled" : status === "cancelled" ? "cancelled"
: "status_change"; : "status_change";
const oldValue = current?.delivery_status || null; 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); return mapOrderGroupRowToDeliveryGroup(data);
}, "Ошибка обновления статуса доставки"); }, "Ошибка обновления статуса доставки");