import { safeSupabaseCall } from "../safeSupabaseCall"; import logger from "../../utils/logger"; import { hasSupabaseConfig, supabase } from "../../supabaseClient"; import { getOrderGroupDeliveryHalfDay, getOrderGroupDeliveryStatusLabel, getOrderGroupStatusLabel, } from "../orderGroupViews"; 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); 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); 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, deliveryAddress, assignedDriverId: row.assigned_driver_id || null, assignedDriverName: row.assigned_driver?.name || "", ordersCount, readyCount, notReadyCount, orderNumbers, 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), 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, deliveryHalfDay: getOrderGroupDeliveryHalfDay({ deliveryHalfDay: rawDeliveryHalfDay, deliveryTime: rawDeliveryTime, deliveryWindow: row.delivery_window, sourceOrders: row.source_orders, }), orderNumberSummary: orderNumbers.length ? orderNumbers.join(", ") : "Номера не указаны", searchText: [ row.group_key, customerName, customerPhone, customerDate, deliveryAddress, rawDeliveryHalfDay, rawDeliveryTime, row.delivery_window, deliveryStatus, getOrderGroupDeliveryStatusLabel(deliveryStatus), orderNumbers.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(), }; }; export const updateOrderGroupDeliveryChoice = async ({ orderGroupId, deliveryDate, deliveryTime, }) => { return safeSupabaseCall(async () => { const client = requireSupabase(); const updateResult = await client .from("order_groups") .update({ delivery_status: "agreed", delivery_date: deliveryDate, delivery_time: deliveryTime, notification_status: "confirmed", updated_at: new Date().toISOString(), }) .eq("id", orderGroupId); if (updateResult.error) { throw updateResult.error; } const { data, error } = await client .from("order_groups") .select("id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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, 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, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .eq("id", orderGroupId) .single(); if (error) { throw error; } return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка сохранения согласования доставки"); }; export const assignDriverToOrderGroup = async ({ orderGroupId, driverId, }) => { return safeSupabaseCall(async () => { const client = requireSupabase(); logger.debug("[assignDriver] orderGroupId:", orderGroupId, "driverId:", driverId); // Direct UPDATE — RLS allows manager/logistician/admin const { error: updateError } = await client .from("order_groups") .update({ assigned_driver_id: driverId || null, delivery_status: driverId ? "driver_assigned" : undefined, updated_at: new Date().toISOString(), }) .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; } return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка назначения водителя"); }; export const updateDeliveryStatus = async ({ orderGroupId, status }) => { return safeSupabaseCall(async () => { const client = requireSupabase(); // 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, paid_storage_at: new Date().toISOString(), updated_at: new Date().toISOString(), }) .eq("id", orderGroupId); if (updateError) throw updateError; } else { // For cancelling paid_storage: check current status first const { data: current, error: fetchError } = await client .from("order_groups") .select("delivery_status") .eq("id", orderGroupId) .single(); if (fetchError) throw fetchError; if (current.delivery_status === "paid_storage" && status === "pending_confirmation") { const { error: updateError } = await client .from("order_groups") .update({ delivery_status: status, 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; } } // 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; return mapOrderGroupRowToDeliveryGroup(data); }, "Ошибка обновления статуса доставки"); }; export const fetchOrderGroups = async () => { return safeSupabaseCall(async () => { const client = requireSupabase(); const { data, error } = await client .from("order_groups") .select("id, group_key, order_numbers, status, delivery_status, sms_sent_at, created_at, updated_at, created_from_exchange_at, source_key, 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, 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, manual_confirmation_at, paid_storage_at, assigned_driver_id, assigned_driver:users!order_groups_assigned_driver_id_fkey(id, name)") .order("updated_at", { ascending: false }); 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)); } return (data || []).map((row) => { const group = mapOrderGroupRowToDeliveryGroup(row); if (group && group.assignedDriverId && !group.assignedDriverName) { group.assignedDriverName = driverMap.get(group.assignedDriverId) || ""; } return group; }).filter(Boolean); }, "Ошибка загрузки групп доставки"); };