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 { 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 }) => {
|
|||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredLogs.length === 0 && !loading && (
|
||||
<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>
|
||||
</tr>
|
||||
|
|
@ -224,27 +227,57 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
|
|||
<Badge tone={ACTION_TONES[log.action] || "accent"}>
|
||||
{getActionLabel(log.action)}
|
||||
</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 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>
|
||||
{!orderGroupId && (
|
||||
<td className="py-2 pr-3 text-xs font-mono text-[var(--color-text-muted)]">
|
||||
{log.order_group_id?.slice(0, 8)}...
|
||||
<td className="py-2 pr-3 text-sm">
|
||||
{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>
|
||||
)}
|
||||
</tr>
|
||||
{expandedId === log.id && (log.details || log.old_value?.length > 40 || log.new_value?.length > 40) && (
|
||||
<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">
|
||||
{log.old_value?.length > 40 && (
|
||||
{log.old_value && (
|
||||
<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>
|
||||
)}
|
||||
{log.details && (
|
||||
<div><span className="text-[var(--color-text-muted)]">Детали:</span> {JSON.stringify(log.details)}</div>
|
||||
{log.details && typeof log.details === "object" && (
|
||||
<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>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export const AdminDashboard = () => {
|
|||
</div>
|
||||
|
||||
{/* 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: sv.pending },
|
||||
|
|
@ -152,7 +152,7 @@ export const AdminDashboard = () => {
|
|||
].map((kpi, i) => (
|
||||
<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: 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>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ export const KpiCard = ({ label, value, hint }) => {
|
|||
return (
|
||||
<Panel className="p-5">
|
||||
<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-xs text-[var(--color-text-muted)]">{hint}</span>
|
||||
{hint && <p className="mt-1 text-xs text-[var(--color-text-muted)]">{hint}</p>}
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<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) => {
|
||||
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 (
|
||||
<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">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
|
|
@ -110,22 +178,28 @@ export const DriverDeliveryDetail = ({ order, onStatusChange }) => {
|
|||
</div>
|
||||
</Panel>
|
||||
|
||||
{availableTransitions.length ? (
|
||||
{actionButtons.length > 0 && (
|
||||
<Panel className="space-y-4 p-6">
|
||||
<h3 className="text-lg font-semibold">Быстрые действия</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableTransitions.map((status) => (
|
||||
{actionButtons.map((btn) => (
|
||||
<Button
|
||||
key={status}
|
||||
variant={status === "Проблема доставки" ? "ghost" : "secondary"}
|
||||
onClick={() => onStatusChange?.(status)}
|
||||
key={btn.value}
|
||||
variant={btn.value === "Проблема доставки" ? "ghost" : "secondary"}
|
||||
onClick={() => {
|
||||
if (btn.value === "Проблема доставки") {
|
||||
setShowProblemModal(true);
|
||||
return;
|
||||
}
|
||||
onStatusChange?.(btn.value);
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
{btn.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Panel>
|
||||
) : null}
|
||||
)}
|
||||
</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 = ({
|
||||
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 = ({
|
|||
<div>
|
||||
<strong>Назначение водителя</strong>
|
||||
<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>
|
||||
</div>
|
||||
{order.assignedDriverId ? (
|
||||
|
|
@ -824,29 +864,35 @@ export const OrderDetailPanel = ({
|
|||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<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={handleAssignDriver}
|
||||
disabled={isSavingDeliveryChoice || !selectedDriverId}
|
||||
>
|
||||
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
|
||||
</Button>
|
||||
</div>
|
||||
{(() => {
|
||||
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={handleAssignDriver}
|
||||
disabled={isSavingDeliveryChoice || !selectedDriverId}
|
||||
>
|
||||
{isSavingDeliveryChoice ? "Назначаем..." : "Назначить"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
{driverMessage ? (
|
||||
<p className="text-sm text-[var(--color-text-muted)]">{driverMessage}</p>
|
||||
) : null}
|
||||
|
|
@ -924,7 +970,7 @@ export const OrderDetailPanel = ({
|
|||
<DriverShipmentPanel order={order} onShipmentChange={handleShipmentChange} />
|
||||
) : null}
|
||||
|
||||
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
||||
{userRole === "driver" && order && onChangeDeliveryStatus ? (
|
||||
<Panel className="space-y-4 p-5">
|
||||
<div>
|
||||
<strong>Статус доставки</strong>
|
||||
|
|
@ -932,38 +978,78 @@ export const OrderDetailPanel = ({
|
|||
Обновите статус по мере выполнения доставки.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
{ value: "loaded", label: "Загружено" },
|
||||
{ value: "on_route", label: "В пути" },
|
||||
{ value: "delivered", label: "Доставлено" },
|
||||
{ value: "problem", label: "Проблема" },
|
||||
].map((statusOption) => (
|
||||
<Button
|
||||
key={statusOption.value}
|
||||
variant={
|
||||
(order.deliveryStatus || order.delivery_status) === statusOption.value ? "primary" : "secondary"}
|
||||
onClick={() => {
|
||||
if (statusOption.value === "delivered" && shipmentState && !shipmentState.canMarkDelivered) {
|
||||
setFormMessage("Укажите причину для каждой неотгруженной позиции перед завершением доставки.");
|
||||
return;
|
||||
{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);
|
||||
}
|
||||
onChangeDeliveryStatus({
|
||||
orderGroupId: order.id,
|
||||
status: statusOption.value,
|
||||
}).then((response) => {
|
||||
if (!response.success) {
|
||||
setFormMessage(response.error || "Не удалось обновить статус");
|
||||
} else {
|
||||
setFormMessage("");
|
||||
setProblemReason(null);
|
||||
});
|
||||
}}
|
||||
onCancel={() => setProblemReason(null)}
|
||||
/>
|
||||
) : null}
|
||||
<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: "problem", label: "Проблема" },
|
||||
];
|
||||
} else if (isOnRoute) {
|
||||
availableButtons = [
|
||||
{ value: "delivered", label: "Доставлено" },
|
||||
{ value: "problem", label: "Проблема" },
|
||||
];
|
||||
} 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
|
||||
key={statusOption.value}
|
||||
variant={currentStatus === statusOption.value ? "primary" : "secondary"}
|
||||
onClick={() => {
|
||||
if (statusOption.value === "problem") {
|
||||
setProblemReason("selecting");
|
||||
return;
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={isSavingDeliveryChoice}
|
||||
>
|
||||
{statusOption.label}
|
||||
</Button>
|
||||
))}
|
||||
onChangeDeliveryStatus({
|
||||
orderGroupId: order.id,
|
||||
status: statusOption.value,
|
||||
}).then((response) => {
|
||||
if (!response.success) {
|
||||
setFormMessage(response.error || "Не удалось обновить статус");
|
||||
} else {
|
||||
setFormMessage("");
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={isSavingDeliveryChoice}
|
||||
>
|
||||
{statusOption.label}
|
||||
</Button>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
{formMessage ? (
|
||||
<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,
|
||||
};
|
||||
|
||||
|
|
@ -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 || "Комментарий не задан.";
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}, "Ошибка обновления статуса доставки");
|
||||
|
|
|
|||
Loading…
Reference in New Issue