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