fix: action log — resolve UUID to names, show old→new, add oldValue for driver reassignment
This commit is contained in:
parent
581a275bc0
commit
1e0344ee34
|
|
@ -4,6 +4,8 @@ import { Badge } from "../UI/Badge";
|
|||
import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { safeSupabaseCall } from "../../services/safeSupabaseCall";
|
||||
import { hasSupabaseConfig, supabase } from "../../supabaseClient";
|
||||
|
||||
const ACTIONS = [
|
||||
"status_change",
|
||||
|
|
@ -33,6 +35,16 @@ const ACTION_TONES = {
|
|||
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) => {
|
||||
if (!isoStr) return "—";
|
||||
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 }) => {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
|
@ -99,6 +73,38 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
|
|||
const [filterDateTo, setFilterDateTo] = useState("");
|
||||
const [filterSearch, setFilterSearch] = useState("");
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
|
|
@ -142,6 +148,45 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
|
|||
);
|
||||
}, [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 (
|
||||
<Panel className="space-y-4 p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -240,7 +285,7 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
|
|||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 pr-3">
|
||||
<span className="text-sm">{getActionDescription(log)}</span>
|
||||
<span className="text-sm">{getActionDesc(log)}</span>
|
||||
</td>
|
||||
{!orderGroupId && (
|
||||
<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">
|
||||
<div className="space-y-1 text-xs">
|
||||
{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 && (
|
||||
<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" && (
|
||||
<div className="space-y-0.5">
|
||||
|
|
@ -273,8 +318,8 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
|
|||
.map(([k, v]) => (
|
||||
<div key={k}>
|
||||
<span className="text-[var(--color-text-muted)]">
|
||||
{{driver_name: "Водитель", driver_id: "ID водителя", problem_type: "Тип проблемы"}[k] || k}:
|
||||
</span> {String(v)}
|
||||
{{driver_name: "Водитель", driver_id: "ID", problem_type: "Тип проблемы", delivery_date_source: "Источник даты"}[k] || k}:
|
||||
</span> {UUID_RE.test(String(v)) ? resolveName(String(v)) : String(v)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export const assignDriverToOrderGroup = async ({
|
|||
const driverName = data?.assigned_driver?.name || driverId || "—";
|
||||
const oldDriverName = currentGroup?.assigned_driver?.name || currentGroup?.assigned_driver_id || "";
|
||||
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 } };
|
||||
await logAction(logPayload).catch(() => {});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue