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:
parent
2ee437e83e
commit
805ceca152
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -924,7 +970,7 @@ export const OrderDetailPanel = ({
|
||||||
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} />
|
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
||||||
<Panel className="space-y-4 p-5">
|
<Panel className="space-y-4 p-5">
|
||||||
<div>
|
<div>
|
||||||
<strong>Статус доставки</strong>
|
<strong>Статус доставки</strong>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 || "Комментарий не задан.";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}, "Ошибка обновления статуса доставки");
|
}, "Ошибка обновления статуса доставки");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue