supersam/src/services/supabase/orderGroupRepository.js

379 lines
14 KiB
JavaScript

import { safeSupabaseCall } from "../safeSupabaseCall";
import { logAction } from "./actionLogService";
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,
deliveryDateSource: row.delivery_date_source || null,
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,
delivery_date_source: "manual",
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, delivery_date_source, 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;
}
await logAction({ orderGroupId, action: "date_assigned", newValue: "manual: " + deliveryDate + " " + (deliveryTime || ""), details: { delivery_date_source: "manual" } }).catch(() => {});
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 { data: currentGroup, error: fetchCurrentError } = await client
.from("order_groups")
.select("delivery_status")
.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"];
const newStatus = driverId
? (ADVANCED_STATUSES.includes(currentStatus) ? undefined : "driver_assigned")
: null; // removing driver → reset to pending
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;
}
await logAction({ orderGroupId, action: driverId ? "driver_assigned" : "driver_removed", newValue: driverId || "removed", details: { driver_id: driverId } }).catch(() => {});
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;
// 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);
}, "Ошибка обновления статуса доставки");
};
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, delivery_date_source, 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);
}, "Ошибка загрузки групп доставки");
};