supersam/src/services/supabase/orderGroupRepository.js

689 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}, "Ошибка загрузки группы доставки");
};