feat: action_logs — журнал действий сотрудников с фильтрами
This commit is contained in:
parent
8f50a68687
commit
dac8450586
|
|
@ -0,0 +1,263 @@
|
||||||
|
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 { useAuth } from "../../context/AuthContext";
|
||||||
|
|
||||||
|
const ACTIONS = [
|
||||||
|
"status_change",
|
||||||
|
"driver_assigned",
|
||||||
|
"driver_removed",
|
||||||
|
"date_assigned",
|
||||||
|
"client_confirmed",
|
||||||
|
"client_cancelled",
|
||||||
|
"cancelled",
|
||||||
|
"manual_confirmation",
|
||||||
|
"paid_storage",
|
||||||
|
"sms_sent",
|
||||||
|
"invitation_created",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACTION_TONES = {
|
||||||
|
status_change: "accent",
|
||||||
|
driver_assigned: "info",
|
||||||
|
driver_removed: "warning",
|
||||||
|
date_assigned: "info",
|
||||||
|
client_confirmed: "success",
|
||||||
|
client_cancelled: "danger",
|
||||||
|
cancelled: "danger",
|
||||||
|
manual_confirmation: "success",
|
||||||
|
paid_storage: "warning",
|
||||||
|
sms_sent: "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) => {
|
||||||
|
if (!isoStr) return "—";
|
||||||
|
try {
|
||||||
|
const d = new Date(isoStr);
|
||||||
|
if (isNaN(d.getTime())) return isoStr;
|
||||||
|
// Format in Moscow timezone
|
||||||
|
const msk = new Date(d.getTime() + 3 * 60 * 60 * 1000);
|
||||||
|
const day = String(msk.getUTCDate()).padStart(2, "0");
|
||||||
|
const month = String(msk.getUTCMonth() + 1).padStart(2, "0");
|
||||||
|
const year = msk.getUTCFullYear();
|
||||||
|
const hours = String(msk.getUTCHours()).padStart(2, "0");
|
||||||
|
const mins = String(msk.getUTCMinutes()).padStart(2, "0");
|
||||||
|
return `${day}.${month}.${year} ${hours}:${mins}`;
|
||||||
|
} catch {
|
||||||
|
return isoStr;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_LABELS = {
|
||||||
|
mega_admin: "Мега-админ",
|
||||||
|
admin: "Админ",
|
||||||
|
manager: "Менеджер",
|
||||||
|
logistician: "Логист",
|
||||||
|
driver: "Водитель",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionLogPanel = ({ orderGroupId = null }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [filterAction, setFilterAction] = useState("");
|
||||||
|
const [filterDateFrom, setFilterDateFrom] = useState("");
|
||||||
|
const [filterDateTo, setFilterDateTo] = useState("");
|
||||||
|
const [filterSearch, setFilterSearch] = useState("");
|
||||||
|
const [expandedId, setExpandedId] = useState(null);
|
||||||
|
|
||||||
|
const loadLogs = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await fetchActionLogs({
|
||||||
|
orderGroupId,
|
||||||
|
action: filterAction || null,
|
||||||
|
dateFrom: filterDateFrom || null,
|
||||||
|
dateTo: filterDateTo || null,
|
||||||
|
limit: 500,
|
||||||
|
});
|
||||||
|
if (result.error) {
|
||||||
|
setError(result.error);
|
||||||
|
} else {
|
||||||
|
setLogs(result.data || result || []);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError(e.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [orderGroupId, filterAction, filterDateFrom, filterDateTo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs();
|
||||||
|
}, [loadLogs]);
|
||||||
|
|
||||||
|
const filteredLogs = useMemo(() => {
|
||||||
|
if (!filterSearch) return logs;
|
||||||
|
const q = filterSearch.toLowerCase();
|
||||||
|
return logs.filter((log) =>
|
||||||
|
(log.performer_name || "").toLowerCase().includes(q) ||
|
||||||
|
(log.action || "").toLowerCase().includes(q) ||
|
||||||
|
(getActionLabel(log.action) || "").toLowerCase().includes(q) ||
|
||||||
|
(log.old_value || "").toLowerCase().includes(q) ||
|
||||||
|
(log.new_value || "").toLowerCase().includes(q) ||
|
||||||
|
(log.order_group_id || "").toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}, [logs, filterSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel className="space-y-4 p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">Журнал действий</h3>
|
||||||
|
<p className="text-sm text-[var(--color-text-muted)]">
|
||||||
|
Кто, что и когда делал с доставками
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={loadLogs}
|
||||||
|
disabled={loading}
|
||||||
|
className="rounded-[14px] bg-[var(--color-accent)] px-3 py-1.5 text-sm font-medium text-white hover:opacity-90 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? "Загрузка..." : "Обновить"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Поиск..."
|
||||||
|
value={filterSearch}
|
||||||
|
onChange={(e) => setFilterSearch(e.target.value)}
|
||||||
|
className="rounded-[14px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm outline-none focus:border-[var(--color-accent)] min-w-[160px]"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={filterAction}
|
||||||
|
onChange={(e) => setFilterAction(e.target.value)}
|
||||||
|
className="rounded-[14px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm outline-none"
|
||||||
|
>
|
||||||
|
<option value="">Все действия</option>
|
||||||
|
{ACTIONS.map((a) => (
|
||||||
|
<option key={a} value={a}>{getActionLabel(a)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filterDateFrom}
|
||||||
|
onChange={(e) => setFilterDateFrom(e.target.value)}
|
||||||
|
className="rounded-[14px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm outline-none"
|
||||||
|
title="С"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filterDateTo}
|
||||||
|
onChange={(e) => setFilterDateTo(e.target.value)}
|
||||||
|
className="rounded-[14px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm outline-none"
|
||||||
|
title="По"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-[14px] border border-[var(--color-danger)] bg-[var(--color-surface-strong)] p-3 text-sm text-[var(--color-danger)]">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-[var(--color-border)] text-left text-[var(--color-text-muted)]">
|
||||||
|
<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>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredLogs.length === 0 && !loading && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={orderGroupId ? 5 : 6} className="py-6 text-center text-[var(--color-text-muted)]">
|
||||||
|
Нет записей
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{filteredLogs.map((log) => (
|
||||||
|
<React.Fragment key={log.id}>
|
||||||
|
<tr
|
||||||
|
className="border-b border-[var(--color-border)] cursor-pointer hover:bg-[var(--color-surface-strong)]"
|
||||||
|
onClick={() => setExpandedId(expandedId === log.id ? null : log.id)}
|
||||||
|
>
|
||||||
|
<td className="py-2 pr-3 whitespace-nowrap">{formatMSKCorrect(log.performed_at)}</td>
|
||||||
|
<td className="py-2 pr-3">
|
||||||
|
<span className="font-medium">{log.performer_name || "Система"}</span>
|
||||||
|
{log.performer_role && (
|
||||||
|
<span className="ml-1 text-xs text-[var(--color-text-muted)]">({ROLE_LABELS[log.performer_role] || log.performer_role})</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="py-2 pr-3">
|
||||||
|
<Badge tone={ACTION_TONES[log.action] || "accent"}>
|
||||||
|
{getActionLabel(log.action)}
|
||||||
|
</Badge>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</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">
|
||||||
|
<div className="space-y-1 text-xs">
|
||||||
|
{log.old_value?.length > 40 && (
|
||||||
|
<div><span className="text-[var(--color-text-muted)]">Было:</span> {log.old_value}</div>
|
||||||
|
)}
|
||||||
|
{log.new_value?.length > 40 && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <div className="py-4 text-center text-sm text-[var(--color-text-muted)]">Загрузка...</div>}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { AdminDashboard } from "../components/admin/AdminDashboard";
|
||||||
import UserManagementPanel from "../components/admin/UserManagementPanel";
|
import UserManagementPanel from "../components/admin/UserManagementPanel";
|
||||||
import ErrorLogPanel from "../components/admin/ErrorLogPanel";
|
import ErrorLogPanel from "../components/admin/ErrorLogPanel";
|
||||||
import { StopWordsPanel } from "../components/admin/StopWordsPanel";
|
import { StopWordsPanel } from "../components/admin/StopWordsPanel";
|
||||||
|
import { ActionLogPanel } from "../components/admin/ActionLogPanel";
|
||||||
import { Panel } from "../components/UI/Panel";
|
import { Panel } from "../components/UI/Panel";
|
||||||
import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel";
|
import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
|
@ -22,6 +23,7 @@ const MEGA_ADMIN_NAV = [
|
||||||
{ key: "users", label: "Пользователи", description: "Управление пользователями и ролями.", badge: null },
|
{ key: "users", label: "Пользователи", description: "Управление пользователями и ролями.", badge: null },
|
||||||
{ key: "errors", label: "Ошибки", description: "Журнал ошибок приложения.", badge: null },
|
{ key: "errors", label: "Ошибки", description: "Журнал ошибок приложения.", badge: null },
|
||||||
{ key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из клиентской карточки.", badge: null },
|
{ key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из клиентской карточки.", badge: null },
|
||||||
|
{ key: "action_log", label: "Журнал", description: "Журнал действий сотрудников.", badge: null },
|
||||||
];
|
];
|
||||||
|
|
||||||
const ROLE_SECTION = {
|
const ROLE_SECTION = {
|
||||||
|
|
@ -94,6 +96,7 @@ export const DashboardPage = () => {
|
||||||
{ key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
{ key: "orders", label: "Группы", description: "Реестр групп доставки.", badge: String(allOrderGroups.length || orderGroups.length || 0) },
|
||||||
{ key: "users", label: "Пользователи", description: "Управление пользователями.", badge: null },
|
{ key: "users", label: "Пользователи", description: "Управление пользователями.", badge: null },
|
||||||
{ key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из карточки.", badge: null },
|
{ key: "stop_words", label: "Стоп-слова", description: "Слова, исключаемые из карточки.", badge: null },
|
||||||
|
{ key: "action_log", label: "Журнал", description: "Журнал действий сотрудников.", badge: null },
|
||||||
]
|
]
|
||||||
: userRole === "logistician"
|
: userRole === "logistician"
|
||||||
? [
|
? [
|
||||||
|
|
@ -118,6 +121,7 @@ export const DashboardPage = () => {
|
||||||
if (activeSection === "users") return <div className="space-y-6 xl:space-y-8"><UserManagementPanel /></div>;
|
if (activeSection === "users") return <div className="space-y-6 xl:space-y-8"><UserManagementPanel /></div>;
|
||||||
if (activeSection === "stop_words") return <div className="space-y-6 xl:space-y-8"><StopWordsPanel /></div>;
|
if (activeSection === "stop_words") return <div className="space-y-6 xl:space-y-8"><StopWordsPanel /></div>;
|
||||||
if (activeSection === "errors") return <div className="space-y-6 xl:space-y-8"><ErrorLogPanel /></div>;
|
if (activeSection === "errors") return <div className="space-y-6 xl:space-y-8"><ErrorLogPanel /></div>;
|
||||||
|
if (activeSection === "action_log") return <div className="space-y-6 xl:space-y-8"><ActionLogPanel /></div>;
|
||||||
|
|
||||||
if (userRole === "driver") {
|
if (userRole === "driver") {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { hasSupabaseConfig, supabase } from "../../supabaseClient";
|
||||||
|
import { safeSupabaseCall } from "../safeSupabaseCall";
|
||||||
|
|
||||||
|
const requireSupabase = () => {
|
||||||
|
if (!hasSupabaseConfig || !supabase) {
|
||||||
|
throw new Error("Supabase не сконфигурирован");
|
||||||
|
}
|
||||||
|
return supabase;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACTION_LABELS = {
|
||||||
|
status_change: "Смена статуса",
|
||||||
|
driver_assigned: "Назначение водителя",
|
||||||
|
driver_removed: "Снятие водителя",
|
||||||
|
date_assigned: "Назначение даты доставки",
|
||||||
|
client_confirmed: "Подтверждение клиента",
|
||||||
|
client_cancelled: "Отмена клиентом",
|
||||||
|
cancelled: "Отмена доставки",
|
||||||
|
manual_confirmation: "Ручное подтверждение",
|
||||||
|
paid_storage: "Платное хранение",
|
||||||
|
sms_sent: "Отправка SMS",
|
||||||
|
invitation_created: "Создание приглашения",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActionLabel = (action) => ACTION_LABELS[action] || action;
|
||||||
|
|
||||||
|
export const logAction = async ({ orderGroupId, action, oldValue = null, newValue = null, details = null }) => {
|
||||||
|
const client = requireSupabase();
|
||||||
|
return safeSupabaseCall(async () => {
|
||||||
|
const { data, error } = await client.rpc("log_action", {
|
||||||
|
p_order_group_id: orderGroupId,
|
||||||
|
p_action: action,
|
||||||
|
p_old_value: oldValue,
|
||||||
|
p_new_value: newValue,
|
||||||
|
p_details: details,
|
||||||
|
});
|
||||||
|
if (error) throw error;
|
||||||
|
return data;
|
||||||
|
}, "Ошибка записи в журнал действий");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchActionLogs = async ({
|
||||||
|
orderGroupId = null,
|
||||||
|
action = null,
|
||||||
|
performedBy = null,
|
||||||
|
dateFrom = null,
|
||||||
|
dateTo = null,
|
||||||
|
limit = 200,
|
||||||
|
offset = 0,
|
||||||
|
} = {}) => {
|
||||||
|
const client = requireSupabase();
|
||||||
|
return safeSupabaseCall(async () => {
|
||||||
|
const { data, error } = await client.rpc("get_action_logs", {
|
||||||
|
p_order_group_id: orderGroupId,
|
||||||
|
p_action: action,
|
||||||
|
p_performed_by: performedBy,
|
||||||
|
p_date_from: dateFrom,
|
||||||
|
p_date_to: dateTo,
|
||||||
|
p_limit: limit,
|
||||||
|
p_offset: offset,
|
||||||
|
});
|
||||||
|
if (error) throw error;
|
||||||
|
return data || [];
|
||||||
|
}, "Ошибка загрузки журнала действий");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { safeSupabaseCall } from "../safeSupabaseCall";
|
import { safeSupabaseCall } from "../safeSupabaseCall";
|
||||||
|
import { logAction } from "./actionLogService";
|
||||||
import logger from "../../utils/logger";
|
import logger from "../../utils/logger";
|
||||||
import { hasSupabaseConfig, supabase } from "../../supabaseClient";
|
import { hasSupabaseConfig, supabase } from "../../supabaseClient";
|
||||||
import {
|
import {
|
||||||
|
|
@ -210,6 +211,8 @@ export const updateOrderGroupDeliveryChoice = async ({
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await logAction({ orderGroupId, action: "date_assigned", newValue: "manual: " + deliveryDate + " " + (deliveryTime || ""), details: { delivery_date_source: "manual" } }).catch(() => {});
|
||||||
|
|
||||||
return mapOrderGroupRowToDeliveryGroup(data);
|
return mapOrderGroupRowToDeliveryGroup(data);
|
||||||
}, "Ошибка сохранения согласования доставки");
|
}, "Ошибка сохранения согласования доставки");
|
||||||
};
|
};
|
||||||
|
|
@ -268,6 +271,8 @@ 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(() => {});
|
||||||
|
|
||||||
return mapOrderGroupRowToDeliveryGroup(data);
|
return mapOrderGroupRowToDeliveryGroup(data);
|
||||||
}, "Ошибка назначения водителя");
|
}, "Ошибка назначения водителя");
|
||||||
};
|
};
|
||||||
|
|
@ -333,6 +338,13 @@ export const updateDeliveryStatus = async ({ orderGroupId, status }) => {
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Determine specific action type for better log readability
|
||||||
|
const logActionType = status === "paid_storage" ? "paid_storage"
|
||||||
|
: status === "cancelled" ? "cancelled"
|
||||||
|
: "status_change";
|
||||||
|
const oldValue = current?.delivery_status || null;
|
||||||
|
await logAction({ orderGroupId, action: logActionType, oldValue, newValue: status, details: { source: "admin_panel" } }).catch(() => {});
|
||||||
|
|
||||||
return mapOrderGroupRowToDeliveryGroup(data);
|
return mapOrderGroupRowToDeliveryGroup(data);
|
||||||
}, "Ошибка обновления статуса доставки");
|
}, "Ошибка обновления статуса доставки");
|
||||||
};
|
};
|
||||||
|
|
@ -364,4 +376,4 @@ export const fetchOrderGroups = async () => {
|
||||||
return group;
|
return group;
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}, "Ошибка загрузки групп доставки");
|
}, "Ошибка загрузки групп доставки");
|
||||||
};
|
};
|
||||||
|
|
@ -192,6 +192,19 @@ Deno.serve(async (request) => {
|
||||||
throw groupUpdateError;
|
throw groupUpdateError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log: client confirmed delivery choice
|
||||||
|
await supabase.from("action_logs").insert({
|
||||||
|
order_group_id: invitation.order_group_id,
|
||||||
|
action: "client_confirmed",
|
||||||
|
old_value: currentGroup.delivery_status,
|
||||||
|
new_value: "agreed",
|
||||||
|
details: {
|
||||||
|
delivery_date: requestedSlot.deliveryDate,
|
||||||
|
delivery_time: requestedSlot.deliveryTime,
|
||||||
|
source: "auto",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await insertIntegrationEvent(supabase, {
|
await insertIntegrationEvent(supabase, {
|
||||||
order_id: null,
|
order_id: null,
|
||||||
event_type: "delivery_choice_confirmed",
|
event_type: "delivery_choice_confirmed",
|
||||||
|
|
@ -344,4 +357,4 @@ Deno.serve(async (request) => {
|
||||||
corsHeaders,
|
corsHeaders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Loading…
Reference in New Issue