fix: action log — readable driver names, no duplicate columns, meaningful descriptions

This commit is contained in:
root 2026-05-28 10:11:57 +00:00
parent 805ceca152
commit 581a275bc0
2 changed files with 59 additions and 46 deletions

View File

@ -33,30 +33,11 @@ const ACTION_TONES = {
invitation_created: "accent", invitation_created: "accent",
}; };
const formatMSK = (isoStr) => {
if (!isoStr) return "—";
try {
const d = new Date(isoStr);
if (isNaN(d.getTime())) return isoStr;
const day = String(d.getDate()).padStart(2, "0");
const month = String(d.getMonth() + 1).padStart(2, "0");
const year = d.getFullYear();
const hours = String(d.getUTCHours() + 3).padStart(2, "0"); // UTC+3
const mins = String(d.getUTCMinutes()).padStart(2, "0");
const h = parseInt(hours, 10);
const adjustedHours = h >= 24 ? String(h - 24).padStart(2, "0") : hours;
return `${day}.${month}.${year} ${adjustedHours}:${mins}`;
} catch {
return isoStr;
}
};
const formatMSKCorrect = (isoStr) => { const formatMSKCorrect = (isoStr) => {
if (!isoStr) return "—"; if (!isoStr) return "—";
try { try {
const d = new Date(isoStr); const d = new Date(isoStr);
if (isNaN(d.getTime())) return isoStr; if (isNaN(d.getTime())) return isoStr;
// Format in Moscow timezone
const msk = new Date(d.getTime() + 3 * 60 * 60 * 1000); const msk = new Date(d.getTime() + 3 * 60 * 60 * 1000);
const day = String(msk.getUTCDate()).padStart(2, "0"); const day = String(msk.getUTCDate()).padStart(2, "0");
const month = String(msk.getUTCMonth() + 1).padStart(2, "0"); const month = String(msk.getUTCMonth() + 1).padStart(2, "0");
@ -77,6 +58,36 @@ const ROLE_LABELS = {
driver: "Водитель", 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();
@ -125,7 +136,9 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
(getActionLabel(log.action) || "").toLowerCase().includes(q) || (getActionLabel(log.action) || "").toLowerCase().includes(q) ||
(log.old_value || "").toLowerCase().includes(q) || (log.old_value || "").toLowerCase().includes(q) ||
(log.new_value || "").toLowerCase().includes(q) || (log.new_value || "").toLowerCase().includes(q) ||
(log.order_group_id || "").toLowerCase().includes(q) (log.order_group_id || "").toLowerCase().includes(q) ||
(getActionDescription(log) || "").toLowerCase().includes(q) ||
(log.details?.driver_name || "").toLowerCase().includes(q)
); );
}, [logs, filterSearch]); }, [logs, filterSearch]);
@ -196,16 +209,14 @@ 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> {!orderGroupId && <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>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredLogs.length === 0 && !loading && ( {filteredLogs.length === 0 && !loading && (
<tr> <tr>
<td colSpan={orderGroupId ? 6 : 7} className="py-6 text-center text-[var(--color-text-muted)]"> <td colSpan={orderGroupId ? 4 : 5} className="py-6 text-center text-[var(--color-text-muted)]">
Нет записей Нет записей
</td> </td>
</tr> </tr>
@ -227,16 +238,10 @@ 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">
<td className="py-2 pr-3 max-w-[150px] truncate">{log.new_value || "—"}</td> <span className="text-sm">{getActionDescription(log)}</span>
</td>
{!orderGroupId && ( {!orderGroupId && (
<td className="py-2 pr-3 text-sm"> <td className="py-2 pr-3 text-sm">
{log.order_group_id ? ( {log.order_group_id ? (
@ -251,9 +256,9 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
</td> </td>
)} )}
</tr> </tr>
{expandedId === log.id && (log.details || log.old_value?.length > 40 || log.new_value?.length > 40) && ( {expandedId === log.id && (
<tr className="bg-[var(--color-surface-strong)]"> <tr className="bg-[var(--color-surface-strong)]">
<td colSpan={orderGroupId ? 6 : 7} 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> {log.old_value}</div>
@ -263,16 +268,20 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
)} )}
{log.details && typeof log.details === "object" && ( {log.details && typeof log.details === "object" && (
<div className="space-y-0.5"> <div className="space-y-0.5">
{Object.entries(log.details).map(([k, v]) => ( {Object.entries(log.details)
<div key={k}><span className="text-[var(--color-text-muted)]">{k}:</span> {String(v)}</div> .filter(([k]) => k !== "source")
.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)}
</div>
))} ))}
</div> </div>
)} )}
{log.details && typeof log.details === "string" && (
<div><span className="text-[var(--color-text-muted)]">Детали:</span> {log.details}</div>
)}
{log.order_group_id && ( {log.order_group_id && (
<div><span className="text-[var(--color-text-muted)]">Группа:</span>{" "} <div>
<span className="text-[var(--color-text-muted)]">Группа:</span>{" "}
<a href={`/dashboard/group/${log.order_group_id}`} <a href={`/dashboard/group/${log.order_group_id}`}
className="text-[var(--color-accent)] hover:underline" className="text-[var(--color-accent)] hover:underline"
onClick={(e) => { e.preventDefault(); navigate(`/dashboard/group/${log.order_group_id}`); }} onClick={(e) => { e.preventDefault(); navigate(`/dashboard/group/${log.order_group_id}`); }}
@ -292,5 +301,4 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
{loading && <div className="py-4 text-center text-sm text-[var(--color-text-muted)]">Загрузка...</div>} {loading && <div className="py-4 text-center text-sm text-[var(--color-text-muted)]">Загрузка...</div>}
</Panel> </Panel>
); );
}; };

View File

@ -229,7 +229,7 @@ export const assignDriverToOrderGroup = async ({
// Direct UPDATE — RLS allows manager/logistician/admin // Direct UPDATE — RLS allows manager/logistician/admin
const { data: currentGroup, error: fetchCurrentError } = await client const { data: currentGroup, error: fetchCurrentError } = await client
.from("order_groups") .from("order_groups")
.select("delivery_status") .select("delivery_status, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)")
.eq("id", orderGroupId) .eq("id", orderGroupId)
.single(); .single();
@ -271,7 +271,12 @@ export const assignDriverToOrderGroup = async ({
throw error; throw error;
} }
await logAction({ orderGroupId, action: driverId ? "driver_assigned" : "driver_removed", newValue: driverId || "removed", details: { driver_id: driverId } }).catch(() => {}); 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_removed", oldValue: oldDriverName, newValue: "Снят", details: { driver_name: oldDriverName } };
await logAction(logPayload).catch(() => {});
return mapOrderGroupRowToDeliveryGroup(data); return mapOrderGroupRowToDeliveryGroup(data);
}, "Ошибка назначения водителя"); }, "Ошибка назначения водителя");