From a3cd0838f0a2586bc03aad1849e702f0921c40a5 Mon Sep 17 00:00:00 2001 From: Hermes Date: Wed, 19 Aug 2026 11:58:20 +0200 Subject: [PATCH] perf: hide-completed server filter + in-memory list cache for fast return-to-list --- .../logistics/LogisticsReadinessBoard.jsx | 24 ++++++- src/hooks/useOrderGroups.js | 36 ++++++++--- src/pages/DashboardPage.jsx | 13 +++- src/services/supabase/orderGroupRepository.js | 62 +++++++++++++++++-- 4 files changed, 117 insertions(+), 18 deletions(-) diff --git a/src/components/logistics/LogisticsReadinessBoard.jsx b/src/components/logistics/LogisticsReadinessBoard.jsx index d996ad8..9395486 100644 --- a/src/components/logistics/LogisticsReadinessBoard.jsx +++ b/src/components/logistics/LogisticsReadinessBoard.jsx @@ -302,7 +302,7 @@ const isFilterActive = (filters) => { return false; }; -export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS, isLoading = false }) => { +export const LogisticsReadinessBoard = ({ orderGroups = [], totalGroups: totalGroupsProp, hideCompleted = true, onToggleHideCompleted, onSelectSet, statusOptions = ORDER_GROUP_DISPLAY_STATUS_OPTIONS, isLoading = false }) => { const FILTERS_KEY = 'logistics-board-filters'; const [filters, setFilters] = React.useState(() => { try { @@ -487,7 +487,27 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO Перетаскивайте секции за ручку слева, чтобы изменить порядок отображения.

- {totalGroups} групп +
+ {totalGroupsProp ?? totalGroups} групп + {typeof onToggleHideCompleted === "function" && ( + + )} +
{ - const [orderGroups, setOrderGroups] = React.useState(() => []); +export const useOrderGroups = ({ + enabled = true, + details = false, + groupId = null, + hideCompleted = true, +} = {}) => { + const [orderGroups, setOrderGroups] = React.useState(() => { + // Instant paint from cache on return-to-list, then background refresh. + const cached = readOrderGroupsCache(details, hideCompleted); + return cached?.data || []; + }); + const [totalGroups, setTotalGroups] = React.useState(() => { + const cached = readOrderGroupsCache(details, hideCompleted); + return cached?.total || 0; + }); const FILTERS_STORAGE_KEY = "supersam_order_filters"; const [filters, setFilters] = React.useState(() => { try { @@ -33,7 +46,7 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null const [isSavingDriverAssignment, setIsSavingDriverAssignment] = React.useState(false); const [isSavingStatusChange, setIsSavingStatusChange] = React.useState(false); - const loadLiveData = React.useCallback(async (showLoading = true) => { + const loadLiveData = React.useCallback(async (showLoading = true, opts = {}) => { if (showLoading) { setIsLoading(true); } @@ -59,7 +72,7 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null return; } - const groupsResult = await fetchOrderGroups({ details }); + const groupsResult = await fetchOrderGroups({ details, hideCompleted, forceRefresh: opts?.forceRefresh }); if (!groupsResult) { return; @@ -72,16 +85,20 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null return; } - setOrderGroups(groupsResult.data || []); + const payload = groupsResult.data || {}; + setOrderGroups(payload.data || []); + setTotalGroups(payload.total ?? (payload.data?.length || 0)); setIsLoading(false); - }, [enabled, details, groupId]); + }, [enabled, details, groupId, hideCompleted]); // Initial load React.useEffect(() => { let cancelled = false; const doLoad = async () => { - setIsLoading(true); - await loadLiveData(true); + const hasCache = !!readOrderGroupsCache(details, hideCompleted); + // If we seeded from cache, don't flash the spinner — refresh silently in bg. + setIsLoading(!hasCache); + await loadLiveData(!hasCache, { forceRefresh: true }); if (cancelled) return; }; if (enabled) doLoad(); else { setOrderGroups([]); setIsLoading(false); } @@ -262,6 +279,7 @@ export const useOrderGroups = ({ enabled = true, details = false, groupId = null return { orderGroups, allOrderGroups: orderGroups, + totalGroups, filteredOrderGroups, visibleOrderGroups, selectedOrderGroup, diff --git a/src/pages/DashboardPage.jsx b/src/pages/DashboardPage.jsx index cc667a4..bd653d0 100644 --- a/src/pages/DashboardPage.jsx +++ b/src/pages/DashboardPage.jsx @@ -91,6 +91,14 @@ export const DashboardPage = () => { const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus(); // ── Order Groups ───────────────────────────────────────────────────────── + const HIDE_COMPLETED_KEY = "supersam-hide-completed"; + const [hideCompleted, setHideCompleted] = React.useState(() => { + try { return localStorage.getItem(HIDE_COMPLETED_KEY) !== "false"; } catch { return true; } + }); + React.useEffect(() => { + try { localStorage.setItem(HIDE_COMPLETED_KEY, hideCompleted ? "true" : "false"); } catch {} + }, [hideCompleted]); + const { orderGroups, allOrderGroups, @@ -102,7 +110,8 @@ export const DashboardPage = () => { statusOptions, isLoading, loadError, - } = useOrderGroups({ enabled: ["orders", "logistics", "deliveries"].includes(activeSection), details: false }); + totalGroups, + } = useOrderGroups({ enabled: ["orders", "logistics", "deliveries"].includes(activeSection), details: false, hideCompleted }); // ── Derived City List ───────────────────────────────────────────────────── const cities = React.useMemo(() => { @@ -200,7 +209,7 @@ const ALLOWED_DASHBOARD_ROLES = ["admin", "mega_admin", "manager", "logistician" // All roles (logistician, admin, mega_admin, manager) — same grouped board return (
- +
); }; diff --git a/src/services/supabase/orderGroupRepository.js b/src/services/supabase/orderGroupRepository.js index 24d3d0f..a72bb3a 100644 --- a/src/services/supabase/orderGroupRepository.js +++ b/src/services/supabase/orderGroupRepository.js @@ -289,7 +289,10 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => { }; const ORDER_GROUP_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, source_orders, order_list, order_list_structured, delivery_invitation_id, delivery_link, delivery_link_code, notification_status, sms_attempts, first_sms_sent_at, second_sms_sent_at, last_sms_error, next_notification_check_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, delivery_invitation:delivery_invitations!order_groups_delivery_invitation_id_fkey(opened_at, access_count, last_accessed_at), called, called_at, called_comment, order_history(id, action, old_status, new_status, user_id, metadata, created_at)`; -const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment`; +export const ORDER_GROUP_LIST_SELECT_FIELDS = `id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, town, customer_name, customer_phone, customer_phone_normalized, customer_date, orders_total, orders_ready, orders_not_ready, notification_status, sms_attempts, last_sms_error, first_sms_sent_at, second_sms_sent_at, delivery_date, delivery_time, delivery_address, customer_address, delivery_date_source, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name), driver_shipment_data, delivery_type, pickup_date, pickup_time_slot, has_delivery_problem, delivery_problem_note, pickup_code, synced_to_1c_at, manager_name, manager_email, manager_tel, payed_ship, is_payed_ship, called, called_at, called_comment`; + +// Delivery statuses treated as "finished" — hidden by default in the board. +const COMPLETED_DELIVERY_STATUSES = ["delivered", "picked_up", "cancelled"]; export const updateOrderGroupDeliveryChoice = async ({ orderGroupId, @@ -300,6 +303,7 @@ export const updateOrderGroupDeliveryChoice = async ({ pickupDate, pickupTimeSlot, }) => { + orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); const effectiveDeliveryStatus = deliveryType === "pickup" ? "pickup" : "agreed"; @@ -354,6 +358,7 @@ export const assignDriverToOrderGroup = async ({ orderGroupId, driverId, }) => { + orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); @@ -416,6 +421,7 @@ export const assignDriverToOrderGroup = async ({ }; export const saveShipmentData = async ({ orderGroupId, shipmentData }) => { + orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); const hasAnyShipped = shipmentData.some((i) => i.shipped); @@ -472,6 +478,7 @@ export const saveShipmentData = async ({ orderGroupId, shipmentData }) => { }; export const updateDeliveryStatus = async ({ orderGroupId, status, details, shipmentData } = {}) => { + orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); @@ -566,14 +573,55 @@ export const updateDeliveryStatus = async ({ orderGroupId, status, details, ship }, "Ошибка обновления статуса доставки"); }; -export const fetchOrderGroups = async ({ details = true } = {}) => { +// Module-level cache so returning to the list from a card doesn't refetch the +// whole board (the previous "return to list is slow" annoyance). Keyed by details +// + hideCompleted. The hook refetches silently in the background to stay fresh. +const orderGroupsCache = new Map(); + +const orderGroupsCacheKey = (details, hideCompleted) => + `v1:${details ? "detail" : "list"}:${hideCompleted ? "hide" : "all"}`; + +/** Synchronous read of the cached board list ({ data, total } | null). */ +export const readOrderGroupsCache = (details = false, hideCompleted = true) => + orderGroupsCache.get(orderGroupsCacheKey(details, hideCompleted)) || null; + +/** + * Fetch order groups. + * + * Returns { data, total } where total is the full count matching the filter + * (requires `count` support from PostgREST — available on self-hosted). + * + * @param {object} options + * @param {boolean} [options.details=false] Load full detail fields (for detail pages) + * @param {boolean} [options.hideCompleted=true] Exclude delivered/picked_up/cancelled groups + * @param {boolean} [options.forceRefresh=false] Bypass the in-memory cache and refetch + */ +export const fetchOrderGroups = async ({ + details = false, + hideCompleted = true, + forceRefresh = false, +} = {}) => { + const cacheKey = orderGroupsCacheKey(details, hideCompleted); + + if (!forceRefresh) { + const cached = orderGroupsCache.get(cacheKey); + if (cached) return cached; + } + return safeSupabaseCall(async () => { const client = requireSupabase(); - const { data, error } = await client + + let query = client .from("order_groups") - .select(details ? ORDER_GROUP_SELECT_FIELDS : ORDER_GROUP_LIST_SELECT_FIELDS) + .select(details ? ORDER_GROUP_SELECT_FIELDS : ORDER_GROUP_LIST_SELECT_FIELDS, { count: "exact" }) .order("updated_at", { ascending: false }); + if (hideCompleted) { + query = query.not("delivery_status", "in", `(${COMPLETED_DELIVERY_STATUSES.join(",")})`); + } + + const { data, error, count } = await query; + if (error) { throw error; } @@ -585,13 +633,17 @@ export const fetchOrderGroups = async ({ details = true } = {}) => { drivers.forEach((d) => driverMap.set(d.id, d.name || d.email)); } - return (data || []).map((row) => { + const groups = (data || []).map((row) => { const group = mapOrderGroupRowToDeliveryGroup(row); if (group && group.assignedDriverId && !group.assignedDriverName) { group.assignedDriverName = driverMap.get(group.assignedDriverId) || ""; } return group; }).filter(Boolean); + + const result = { data: groups, total: count ?? groups.length }; + orderGroupsCache.set(cacheKey, result); + return result; }, "Ошибка загрузки групп доставки"); };