fix: action log — resolve UUID to names, show old→new, add oldValue for driver reassignment

This commit is contained in:
root 2026-05-28 10:17:29 +00:00
parent 581a275bc0
commit 1e0344ee34
2 changed files with 89 additions and 44 deletions

View File

@ -4,6 +4,8 @@ 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 { useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
import { safeSupabaseCall } from "../../services/safeSupabaseCall";
import { hasSupabaseConfig, supabase } from "../../supabaseClient";
const ACTIONS = [ const ACTIONS = [
"status_change", "status_change",
@ -33,6 +35,16 @@ const ACTION_TONES = {
invitation_created: "accent", invitation_created: "accent",
}; };
const ROLE_LABELS = {
mega_admin: "Мега-админ",
admin: "Админ",
manager: "Менеджер",
logistician: "Логист",
driver: "Водитель",
};
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const formatMSKCorrect = (isoStr) => { const formatMSKCorrect = (isoStr) => {
if (!isoStr) return "—"; if (!isoStr) return "—";
try { try {
@ -50,44 +62,6 @@ const formatMSKCorrect = (isoStr) => {
} }
}; };
const ROLE_LABELS = {
mega_admin: "Мега-админ",
admin: "Админ",
manager: "Менеджер",
logistician: "Логист",
driver: "Водитель",
};
/** Human-readable description of what happened */
const getActionDescription = (log) => {
switch (log.action) {
case "status_change":
return `${log.old_value || "—"}${log.new_value || "—"}`;
case "driver_assigned":
return `Назначен: ${log.details?.driver_name || log.new_value || "водитель"}`;
case "driver_removed":
return `Снят: ${log.details?.driver_name || log.old_value || "водитель"}`;
case "date_assigned":
return `Дата: ${log.new_value || "—"}`;
case "client_confirmed":
return `Клиент подтвердил`;
case "client_cancelled":
return `Клиент отменил`;
case "cancelled":
return `Отменено`;
case "manual_confirmation":
return `Ручное подтверждение`;
case "paid_storage":
return `Платное хранение`;
case "sms_sent":
return `SMS отправлено`;
case "invitation_created":
return `Приглашение создано`;
default:
return log.new_value || getActionLabel(log.action);
}
};
export const ActionLogPanel = ({ orderGroupId = null }) => { export const ActionLogPanel = ({ orderGroupId = null }) => {
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
@ -99,6 +73,38 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
const [filterDateTo, setFilterDateTo] = useState(""); const [filterDateTo, setFilterDateTo] = useState("");
const [filterSearch, setFilterSearch] = useState(""); const [filterSearch, setFilterSearch] = useState("");
const [expandedId, setExpandedId] = useState(null); const [expandedId, setExpandedId] = useState(null);
const [userNames, setUserNames] = useState({});
// Fetch user name map for resolving UUIDs
useEffect(() => {
const fetchNames = async () => {
if (!hasSupabaseConfig || !supabase) return;
const result = await safeSupabaseCall(
async () => {
const { data, error } = await supabase.from("users").select("id, name");
if (error) throw error;
return data;
},
"Ошибка загрузки пользователей"
);
if (result && !result.error) {
const map = {};
(Array.isArray(result) ? result : result.data || []).forEach((u) => {
map[u.id] = u.name;
});
setUserNames(map);
}
};
fetchNames();
}, []);
const resolveName = useCallback(
(uuid) => {
if (!uuid || !UUID_RE.test(uuid)) return uuid;
return userNames[uuid] || uuid;
},
[userNames]
);
const loadLogs = useCallback(async () => { const loadLogs = useCallback(async () => {
setLoading(true); setLoading(true);
@ -142,6 +148,45 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
); );
}, [logs, filterSearch]); }, [logs, filterSearch]);
/** Human-readable description */
const getActionDescription = (log) => {
switch (log.action) {
case "status_change": {
const oldVal = resolveName(log.old_value) || "—";
const newVal = resolveName(log.new_value) || "—";
return `${oldVal}${newVal}`;
}
case "driver_assigned": {
const name = log.details?.driver_name || resolveName(log.new_value) || "водитель";
return `Назначен: ${name}`;
}
case "driver_removed": {
const name = log.details?.driver_name || resolveName(log.old_value) || "водитель";
return `Снят: ${name}`;
}
case "date_assigned":
return `Дата: ${log.new_value || "—"}`;
case "client_confirmed":
return "Клиент подтвердил";
case "client_cancelled":
return "Клиент отменил";
case "cancelled":
return "Отменено";
case "manual_confirmation":
return "Ручное подтверждение";
case "paid_storage":
return "Платное хранение";
case "sms_sent":
return "SMS отправлено";
case "invitation_created":
return "Приглашение создано";
default:
return log.new_value || getActionLabel(log.action);
}
};
const getActionDesc = getActionDescription;
return ( return (
<Panel className="space-y-4 p-5"> <Panel className="space-y-4 p-5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -240,7 +285,7 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
</Badge> </Badge>
</td> </td>
<td className="py-2 pr-3"> <td className="py-2 pr-3">
<span className="text-sm">{getActionDescription(log)}</span> <span className="text-sm">{getActionDesc(log)}</span>
</td> </td>
{!orderGroupId && ( {!orderGroupId && (
<td className="py-2 pr-3 text-sm"> <td className="py-2 pr-3 text-sm">
@ -261,10 +306,10 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
<td colSpan={orderGroupId ? 4 : 5} className="py-2 px-3"> <td colSpan={orderGroupId ? 4 : 5} className="py-2 px-3">
<div className="space-y-1 text-xs"> <div className="space-y-1 text-xs">
{log.old_value && ( {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> {resolveName(log.old_value)}</div>
)} )}
{log.new_value && ( {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> {resolveName(log.new_value)}</div>
)} )}
{log.details && typeof log.details === "object" && ( {log.details && typeof log.details === "object" && (
<div className="space-y-0.5"> <div className="space-y-0.5">
@ -273,8 +318,8 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
.map(([k, v]) => ( .map(([k, v]) => (
<div key={k}> <div key={k}>
<span className="text-[var(--color-text-muted)]"> <span className="text-[var(--color-text-muted)]">
{{driver_name: "Водитель", driver_id: "ID водителя", problem_type: "Тип проблемы"}[k] || k}: {{driver_name: "Водитель", driver_id: "ID", problem_type: "Тип проблемы", delivery_date_source: "Источник даты"}[k] || k}:
</span> {String(v)} </span> {UUID_RE.test(String(v)) ? resolveName(String(v)) : String(v)}
</div> </div>
))} ))}
</div> </div>

View File

@ -274,7 +274,7 @@ export const assignDriverToOrderGroup = async ({
const driverName = data?.assigned_driver?.name || driverId || "—"; const driverName = data?.assigned_driver?.name || driverId || "—";
const oldDriverName = currentGroup?.assigned_driver?.name || currentGroup?.assigned_driver_id || ""; const oldDriverName = currentGroup?.assigned_driver?.name || currentGroup?.assigned_driver_id || "";
const logPayload = driverId const logPayload = driverId
? { orderGroupId, action: "driver_assigned", newValue: driverName, details: { driver_id: driverId, driver_name: driverName } } { orderGroupId, action: "driver_assigned", oldValue: oldDriverName || undefined, newValue: driverName, details: { driver_id: driverId, driver_name: driverName } }
: { orderGroupId, action: "driver_removed", oldValue: oldDriverName, newValue: "Снят", details: { driver_name: oldDriverName } }; : { orderGroupId, action: "driver_removed", oldValue: oldDriverName, newValue: "Снят", details: { driver_name: oldDriverName } };
await logAction(logPayload).catch(() => {}); await logAction(logPayload).catch(() => {});