import { safeSupabaseCall } from "../safeSupabaseCall"; import { CRIMEAN_CITIES } from "../../constants/cities.js"; import { logAction } from "./actionLogService"; import logger from "../../utils/logger"; import { hasSupabaseConfig, supabase } from "../../supabaseClient"; import { getOrderGroupDeliveryHalfDay, getOrderGroupDeliveryStatusLabel, getOrderGroupStatusLabel, } from "../orderGroupViews"; import { normalizeNom } from "../../utils/deliveryUtils"; const requireSupabase = () => { if (!hasSupabaseConfig || !supabase) { throw new Error("Supabase не сконфигурирован"); } return supabase; }; const normalizeText = (value) => (value == null ? "" : String(value)).trim(); const ALLOWED_DELIVERY_TIMES = new Set(["Первая половина дня", "Вторая половина дня"]); const normalizePhone = (value) => normalizeText(value).replace(/[\s\-()]/g, ""); const toNumber = (value, fallback = 0) => { const nextValue = Number(value); return Number.isFinite(nextValue) ? nextValue : fallback; }; const toStringArray = (value) => { if (Array.isArray(value)) { return value.filter((item) => item != null && String(item).trim() !== "").map(String); } if (value == null || value === "") { return []; } return [String(value)]; }; const parseGroupKey = (groupKey) => { if (!groupKey || typeof groupKey !== "string") { return { phone: "", date: "" }; } const [phone = "", date = ""] = groupKey.split("|"); return { phone: normalizePhone(phone), date: normalizeText(date), }; }; export const mapOrderGroupRowToDeliveryGroup = (row) => { if (!row) { return null; } const parsedKey = parseGroupKey(row.group_key); const customerName = normalizeText(row.customer_name || row.legacy_customer_name); const customerPhone = normalizeText(row.customer_phone || row.legacy_customer_phone || parsedKey.phone); const customerDate = normalizeText(row.customer_date || row.legacy_customer_date || parsedKey.date); const orderNumbers = toStringArray(row.order_numbers); // Extract ALL bill numbers from source_orders (1C sends full orderList in every source_order) const allBillNumbers = (() => { const srcOrders = row.source_orders; if (!Array.isArray(srcOrders) || !srcOrders.length) return orderNumbers; const seen = new Set(); const result = []; for (const src of srcOrders) { if (src && Array.isArray(src.orderList)) { for (const ol of src.orderList) { if (ol && ol.nom) { const n = normalizeNom(ol.nom); if (n && !seen.has(n)) { seen.add(n); result.push(n); } } } } } return result.length > 0 ? result : orderNumbers; })(); const inferredOrderCount = orderNumbers.length; const ordersCount = toNumber(row.orders_count ?? row.orders_total ?? row.legacy_orders_total, inferredOrderCount); const readyCount = toNumber( row.ready_count ?? row.orders_ready ?? row.legacy_orders_ready, row.status === "ready_for_notification" ? ordersCount : 0, ); const notReadyCount = toNumber( row.not_ready_count ?? row.orders_not_ready ?? row.legacy_orders_not_ready, Math.max(ordersCount - readyCount, 0), ); const deliveryStatus = normalizeText(row.delivery_status) || "pending_confirmation"; const deliveryDate = normalizeText(row.delivery_date); const rawDeliveryTime = normalizeText(row.delivery_time); const rawDeliveryHalfDay = normalizeText(row.delivery_half_day); const deliveryTime = ALLOWED_DELIVERY_TIMES.has(rawDeliveryTime) ? rawDeliveryTime : ALLOWED_DELIVERY_TIMES.has(rawDeliveryHalfDay) ? rawDeliveryHalfDay : ""; const extractAddressFromSourceOrders = (sourceOrders) => { if (!Array.isArray(sourceOrders) || !sourceOrders.length) { return ""; } const first = sourceOrders[0]; return normalizeText(first.adress || first.address || ""); }; const deliveryAddress = normalizeText(row.delivery_address) || extractAddressFromSourceOrders(row.source_orders); // Detect pickup from source_orders ship field (1C sends "САМОВЫВОЗ") const isPickupFromSource = Array.isArray(row.source_orders) && row.source_orders.length > 0 && normalizeText(row.source_orders[0].ship || "").toUpperCase() === "САМОВЫВОЗ"; // Also treat address equal to "САМОВЫВОЗ" as pickup indicator const isPickupAddress = deliveryAddress.toUpperCase() === "САМОВЫВОЗ"; // Resolve effective delivery type: // - If DB explicitly says "pickup" → pickup // - If status is "pickup" → pickup // - If DB explicitly says "delivery" (manually confirmed by logistician) → honor it, don't override // - Otherwise fall back to source/address detection const effectiveDeliveryType = (row.delivery_type === "pickup" || deliveryStatus === "pickup") ? "pickup" : row.delivery_type === "delivery" ? "delivery" : (isPickupFromSource || isPickupAddress) ? "pickup" : (row.delivery_type || "delivery"); // Preserve original address for pre-filling delivery form (don't clear for pickup) const originalDeliveryAddress = deliveryAddress; // For display: show nothing for pickup placeholder addresses const resolvedDeliveryAddress = (effectiveDeliveryType === "pickup" && (deliveryAddress.toUpperCase() === "САМОВЫВОЗ" || !deliveryAddress)) ? "" : deliveryAddress; const customerAddress = normalizeText(row.customer_address) || ""; const extractCity = (addr) => { if (!addr) return ""; // 1) explicit marker: г. Ялта, пгт. Куйбышево, etc. // Word markers (город/село) require space after to avoid matching "Стройгородок" // Dot markers (г./гор./etc) allow zero space (г.Ялта) const m = addr.match(/(?:г\.\s*|гор\.\s*|пос\.\s*|с\.\s*|дер\.\s*|пгт\.\s*|город\s+|село\s+|г\s+)\s*([А-ЯЁа-яёA-Za-z\-\s]+?)(?:[,\s]|$)/i); if (m) { const candidate = m[1].trim(); for (const city of CRIMEAN_CITIES) { if (city.toLowerCase() === candidate.toLowerCase()) return city; } // Not a known city — continue to step 2 } // 2) known city name via custom word boundary (JS \b doesn't work with Cyrillic) // City must be preceded by start/comma/space/dot and followed by comma/space/end const lower = addr.toLowerCase(); for (const city of CRIMEAN_CITIES) { const esc = city.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const re = new RegExp("(^|[,.\\s])" + esc + "(?=[,\\s]|$)", "i"); if (re.test(lower)) return city; } // 3) Бахчисарайский р-н → Бахчисарай const district = addr.match(/([А-ЯЁа-яё]+)ский\s*(?:р-н|район)/i); if (district) { const base = district[1]; for (const city of CRIMEAN_CITIES) { if (city.toLowerCase().startsWith(base.toLowerCase())) return city; } } // 4) no match → empty (caller falls back to Севастополь) return ""; }; const city = extractCity(customerAddress) || extractCity(deliveryAddress) || "Севастополь"; return { id: row.id, groupKey: row.group_key, customer: { name: customerName, phone: customerPhone, phoneNormalized: parsedKey.phone || normalizePhone(customerPhone), date: customerDate, ordersCount, readyCount, notReadyCount, }, customerName, customerPhone, customerPhoneNormalized: parsedKey.phone || normalizePhone(customerPhone), customerDate, town: row.town || null, deliveryAddress: resolvedDeliveryAddress, originalDeliveryAddress, customerAddress, city, assignedDriverId: row.assigned_driver_id || null, assignedDriverName: row.assigned_driver?.name || "", ordersCount, readyCount, notReadyCount, orderNumbers, allBillNumbers, status: row.status || "draft", smsSentAt: row.sms_sent_at || null, firstSmsSentAt: row.first_sms_sent_at || null, secondSmsSentAt: row.second_sms_sent_at || null, manualConfirmationAt: row.manual_confirmation_at || null, paidStorageAt: row.paid_storage_at || null, notificationStatus: normalizeText(row.notification_status), lastSmsError: row.last_sms_error || null, smsAttempts: row.sms_attempts || 0, createdFromExchangeAt: row.created_from_exchange_at || null, sourceKey: row.source_key || null, legacyCustomerName: row.legacy_customer_name || null, legacyCustomerPhone: row.legacy_customer_phone || null, legacyCustomerPhoneNormalized: row.legacy_customer_phone_normalized || null, legacyCustomerDate: row.legacy_customer_date || null, legacyOrdersTotal: row.legacy_orders_total ?? null, legacyOrdersReady: row.legacy_orders_ready ?? null, legacyOrdersNotReady: row.legacy_orders_not_ready ?? null, sourceOrders: row.source_orders || null, orderList: row.order_list || null, orderListStructured: row.order_list_structured || null, createdAt: row.created_at, updatedAt: row.updated_at, deliveryStatus, delivery_status: deliveryStatus, displayTitle: customerName || `Группа ${row.group_key || row.id}`, displaySubtitle: [customerPhone, customerDate].filter(Boolean).join(" · ") || row.group_key || row.id, deliveryDate, deliveryTime, deliveryDateSource: row.delivery_date_source || null, autocallOptOut: !!row.autocall_opt_out, autocall_opt_out: !!row.autocall_opt_out, autocallStatus: row.autocall_status || null, autocall_status: row.autocall_status || null, autocallExclusionReason: row.autocall_exclusion_reason || null, autocall_exclusion_reason: row.autocall_exclusion_reason || null, deliveryType: effectiveDeliveryType, pickupDate: row.pickup_date || null, pickupTimeSlot: row.pickup_time_slot || null, driverShipmentData: row.driver_shipment_data || null, hasDeliveryProblem: row.has_delivery_problem || false, deliveryProblemNote: row.delivery_problem_note || null, pickupCode: row.pickup_code || null, syncedTo1cAt: row.synced_to_1c_at || null, deliveryLink: row.delivery_link || null, managerName: normalizeText(row.manager_name) || null, managerEmail: normalizeText(row.manager_email) || null, managerTel: normalizeText(row.manager_tel) || null, payedShip: row.payed_ship || null, isPayedShip: row.is_payed_ship || false, called: !!row.called, calledAt: row.called_at || null, called_at: row.called_at || null, calledComment: row.called_comment || null, called_comment: row.called_comment || null, invitationConfirmedAt: row.delivery_invitation?.confirmed_at || null, invitationOpenedAt: row.delivery_invitation?.opened_at || null, invitationAccessCount: row.delivery_invitation?.access_count || 0, invitationLastAccessedAt: row.delivery_invitation?.last_accessed_at || null, deliveryHalfDay: getOrderGroupDeliveryHalfDay({ deliveryHalfDay: rawDeliveryHalfDay, deliveryTime: rawDeliveryTime, deliveryWindow: row.delivery_window, sourceOrders: row.source_orders, }), orderNumberSummary: allBillNumbers.length ? allBillNumbers.join(", ") : "Номера не указаны", searchText: [ row.group_key, customerName, customerPhone, customerDate, resolvedDeliveryAddress, customerAddress, city, rawDeliveryHalfDay, rawDeliveryTime, row.delivery_window, deliveryStatus, getOrderGroupDeliveryStatusLabel(deliveryStatus), orderNumbers.join(" "), allBillNumbers.join(" "), row.status, getOrderGroupStatusLabel(row.status), getOrderGroupDeliveryHalfDay({ deliveryHalfDay: rawDeliveryHalfDay, deliveryTime: rawDeliveryTime, deliveryWindow: row.delivery_window, sourceOrders: row.source_orders, }), getOrderGroupDeliveryStatusLabel(deliveryStatus), row.notification_status, extractAddressFromSourceOrders(row.source_orders), ] .filter(Boolean) .join(" ") .toLowerCase(), }; }; 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, autocall_opt_out, 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(confirmed_at, 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)`; 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, autocall_opt_out, 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, autocall_exclusion_reason, autocall_status, delivery_invitation:delivery_invitations!order_groups_delivery_invitation_id_fkey(confirmed_at)`; // Delivery statuses treated as "finished" — hidden by default in the board. const COMPLETED_DELIVERY_STATUSES = ["delivered", "picked_up", "cancelled"]; export const updateOrderGroupDeliveryChoice = async ({ orderGroupId, deliveryDate, deliveryTime, deliveryType, deliveryAddress, pickupDate, pickupTimeSlot, }) => { orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); const effectiveDeliveryStatus = deliveryType === "pickup" ? "pickup" : "agreed"; const updatePayload = { delivery_status: effectiveDeliveryStatus, delivery_date: deliveryDate, delivery_time: deliveryTime, delivery_type: deliveryType || "delivery", delivery_date_source: "manual", notification_status: "confirmed", updated_at: new Date().toISOString(), }; if (deliveryType === "pickup") { updatePayload.pickup_date = pickupDate || null; updatePayload.pickup_time_slot = pickupTimeSlot || null; // Pickup orders don't need a driver — clear assignment updatePayload.assigned_driver_id = null; } else { updatePayload.pickup_date = null; updatePayload.pickup_time_slot = null; if (deliveryAddress !== undefined) { updatePayload.delivery_address = deliveryAddress; } } const updateResult = await client .from("order_groups") .update(updatePayload) .eq("id", orderGroupId); if (updateResult.error) { throw updateResult.error; } const { data, error } = await client .from("order_groups") .select(ORDER_GROUP_SELECT_FIELDS) .eq("id", orderGroupId) .single(); if (error) { throw error; } await logAction({ orderGroupId, action: "date_assigned", newValue: (deliveryType === "pickup" ? "pickup: " : "manual: ") + deliveryDate + " " + (deliveryTime || ""), details: { delivery_date_source: "manual", delivery_type: deliveryType, pickup_date: pickupDate, pickup_time_slot: pickupTimeSlot } }).catch(() => {}); return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка сохранения согласования доставки"); }; export const assignDriverToOrderGroup = async ({ orderGroupId, driverId, }) => { orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); logger.debug("[assignDriver] orderGroupId:", orderGroupId, "driverId:", driverId); // Direct UPDATE — RLS allows manager/logistician/admin const { data: currentGroup, error: fetchCurrentError } = await client .from("order_groups") .select("delivery_status, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .eq("id", orderGroupId) .single(); if (fetchCurrentError) { throw fetchCurrentError; } // When assigning a driver, advance status to driver_assigned if currently agreed. // For loaded/on_route/delivered, keep existing status (driver reassigned mid-delivery). // For pending/manual statuses, also set driver_assigned (driver chosen before formal agreement). const currentStatus = currentGroup.delivery_status; const ADVANCED_STATUSES = ["loaded", "on_route", "delivered", "picked_up"]; const newStatus = driverId ? (ADVANCED_STATUSES.includes(currentStatus) ? undefined : "driver_assigned") : "agreed"; // removing driver → revert to agreed (NOT null — would violate NOT NULL constraint) const updates = { assigned_driver_id: driverId || null, ...(newStatus !== undefined && { delivery_status: newStatus }), updated_at: new Date().toISOString(), }; const { error: updateError } = await client .from("order_groups") .update(updates) .eq("id", orderGroupId); if (updateError) { throw updateError; } // Fetch with driver join for the mapper const { data, error } = await client .from("order_groups") .select("*, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .eq("id", orderGroupId) .single(); if (error) { throw error; } const driverName = data?.assigned_driver?.name || driverId || "—"; const oldDriverName = currentGroup?.assigned_driver?.name || currentGroup?.assigned_driver_id || ""; const logPayload = driverId ? { orderGroupId, action: "driver_assigned", oldValue: oldDriverName || undefined, newValue: driverName, details: { driver_name: driverName } } : { orderGroupId, action: "driver_removed", oldValue: oldDriverName, newValue: "Снят", details: { driver_name: oldDriverName } }; await logAction(logPayload).catch(() => {}); return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка назначения водителя"); }; export const saveShipmentData = async ({ orderGroupId, shipmentData }) => { orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); const hasAnyShipped = shipmentData.some((i) => i.shipped); const hasProblem = shipmentData.some((i) => !i.shipped); const problemNote = hasProblem ? shipmentData.filter((i) => !i.shipped).map((i) => `${i.name}${i.quantity ? ` (${i.quantity}${i.unit ? ` ${i.unit}` : ""})` : ""}${i.comment ? ` — ${i.comment}` : ""}`).join("; ") : null; // If all shipped → delivered. If partial (some shipped, some not) → problem. // If nothing shipped → keep current status (driver just clearing checkboxes). let newDeliveryStatus; if (hasAnyShipped && !hasProblem) { newDeliveryStatus = "delivered"; } else if (hasAnyShipped && hasProblem) { newDeliveryStatus = "problem"; } else { newDeliveryStatus = undefined; } const updatePayload = { driver_shipment_data: shipmentData, has_delivery_problem: hasProblem, delivery_problem_note: problemNote, updated_at: new Date().toISOString(), }; if (newDeliveryStatus) { updatePayload.delivery_status = newDeliveryStatus; } const { error: updateError } = await client .from("order_groups") .update(updatePayload) .eq("id", orderGroupId); if (updateError) throw updateError; const { data, error } = await client .from("order_groups") .select(ORDER_GROUP_SELECT_FIELDS) .eq("id", orderGroupId) .single(); if (error) throw error; await logAction({ orderGroupId, action: "shipment_data_saved", newValue: "shipment_data_updated", details: { itemCount: shipmentData?.length || 0 }, }).catch(() => {}); return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка сохранения данных отгрузки"); }; export const updateDeliveryStatus = async ({ orderGroupId, status, details, shipmentData } = {}) => { orderGroupsCache.clear(); return safeSupabaseCall(async () => { const client = requireSupabase(); // Fetch current status before any update (needed for audit log + status sync) const { data: current, error: fetchCurrentError } = await client .from("order_groups") .select("delivery_status, delivery_type") .eq("id", orderGroupId) .single(); if (fetchCurrentError) throw fetchCurrentError; // Compute status column: pickup+picked_up → picked_up, delivery+picked_up → delivered const deliveryType = current.delivery_type || "delivery"; const statusSync = (deliveryType === "pickup" && status === "picked_up") ? "picked_up" : (deliveryType === "delivery" && status === "picked_up") ? "delivered" : (status === "delivered") ? "delivered" : status; // Bypass stale RPC for paid_storage transitions // Server-side RPC still enforces driver-assignment checks that block // manager/logistician from moving groups into/out of paid_storage. // RLS policy "order groups update coordination roles" allows // manager/logistician/admin to update order_groups directly. if (status === "paid_storage") { const { error: updateError } = await client .from("order_groups") .update({ delivery_status: status, status: statusSync, paid_storage_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .eq("id", orderGroupId); if (updateError) throw updateError; } else if (current.delivery_status === "paid_storage" && status === "pending_confirmation") { const { error: updateError } = await client .from("order_groups") .update({ delivery_status: status, status: statusSync, paid_storage_at: null, updated_at: new Date().toISOString(), }) .eq("id", orderGroupId); if (updateError) throw updateError; } else { // All other statuses use the RPC (driver workflows, etc.) const { error: rpcError } = await client.rpc("update_delivery_status", { p_order_group_id: orderGroupId, p_status: status, }); if (rpcError) throw rpcError; } // Save shipment data if provided (e.g. partial delivery info) if (shipmentData) { const { error: shipmentUpdateError } = await client .from("order_groups") .update({ driver_shipment_data: shipmentData, updated_at: new Date().toISOString(), }) .eq("id", orderGroupId); if (shipmentUpdateError) { // Log but don't fail the status update console.error("[updateDeliveryStatus] Failed to save shipment data:", shipmentUpdateError); } } // Fetch updated group const { data, error } = await client .from("order_groups") .select("*, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .eq("id", orderGroupId) .single(); 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", ...details } }).catch(() => {}); return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка обновления статуса доставки"); }; // 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 { data: cached, error: null }; } return safeSupabaseCall(async () => { const client = requireSupabase(); let query = client .from("order_groups") .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; } // Load driver names to patch groups where assigned_driver join is missing const { data: drivers, error: driversError } = await client.rpc("get_drivers"); const driverMap = new Map(); if (!driversError && drivers) { drivers.forEach((d) => driverMap.set(d.id, d.name || d.email)); } 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; }, "Ошибка загрузки групп доставки"); }; export const fetchOrderGroupById = async (orderGroupId) => { return safeSupabaseCall(async () => { const client = requireSupabase(); const { data, error } = await client .from("order_groups") .select(ORDER_GROUP_SELECT_FIELDS) .eq("id", orderGroupId) .single(); if (error) { throw error; } const group = mapOrderGroupRowToDeliveryGroup(data); if (group && group.assignedDriverId && !group.assignedDriverName) { const { data: drivers, error: driversError } = await client.rpc("get_drivers"); if (!driversError && drivers) { const driver = drivers.find((d) => d.id === group.assignedDriverId); if (driver) { group.assignedDriverName = driver.name || driver.email; } } } return group; }, "Ошибка загрузки группы доставки"); };