supersam/src/services/orderGroupViews.js

468 lines
14 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.

const normalizeDate = (value) => (value ? String(value) : "");
const getDeliveryDate = (group) => normalizeDate(group.deliveryDate || group.customerDate || "");
export const DELIVERY_GROUP_STATUS_LABELS = {
pending_confirmation: "Ожидает согласования",
manual_confirmation_required: "Взят в ручное управление",
agreed: "Согласовано",
driver_assigned: "Назначен водитель",
loaded: "Загружено",
on_route: "В пути",
delivered: "Доставлено",
problem: "Проблема",
paid_storage: "Платное хранение",
cancelled: "Отменено",
};
export const NOTIFICATION_STATUS_LABELS = {
not_started: "",
link_ready: "Ссылка готова",
first_sms_sent: "1-е приглашение отправлено",
second_sms_sent: "2-е приглашение отправлено",
send_failed: "Ошибка отправки",
confirmed: "Согласовано",
manual_required: "Требуется ручное подтверждение",
};
export const DRIVER_VISIBLE_DELIVERY_STATUSES = [
"driver_assigned",
"loaded",
"on_route",
"problem",
"delivered",
];
export const DRIVER_ACTIVE_DELIVERY_STATUSES = ["driver_assigned", "loaded", "on_route", "problem"];
const HALF_DAY_LABELS = {
morning: "Первая половина дня",
afternoon: "Вторая половина дня",
};
const normalizeDeliveryHalfDayLabel = (value) => {
const normalized = normalizeDate(value).trim();
if (!normalized) {
return "";
}
const lower = normalized.toLowerCase();
if (lower.includes("до обеда") || lower.includes("первая половина дня") || lower.includes("утро")) {
return HALF_DAY_LABELS.morning;
}
if (lower.includes("после обеда") || lower.includes("вторая половина дня") || lower.includes("вечер")) {
return HALF_DAY_LABELS.afternoon;
}
return "";
};
const parseJsonIfNeeded = (value) => {
if (typeof value !== "string") {
return value;
}
try {
return JSON.parse(value);
} catch {
return value;
}
};
const findDeliveryHalfDayInValue = (value) => {
const parsedValue = parseJsonIfNeeded(value);
if (Array.isArray(parsedValue)) {
for (const item of parsedValue) {
const match = findDeliveryHalfDayInValue(item);
if (match) {
return match;
}
}
return "";
}
if (parsedValue && typeof parsedValue === "object") {
const candidates = [
parsedValue.deliveryTime,
parsedValue.delivery_time,
parsedValue.time,
parsedValue.deliveryHalfDay,
parsedValue.delivery_half_day,
parsedValue.window,
parsedValue.deliveryWindow,
parsedValue.delivery_window,
parsedValue.slot?.time,
parsedValue.deliverySlot?.time,
];
for (const candidate of candidates) {
const match = normalizeDeliveryHalfDayLabel(candidate);
if (match) {
return match;
}
}
for (const nestedValue of Object.values(parsedValue)) {
const match = findDeliveryHalfDayInValue(nestedValue);
if (match) {
return match;
}
}
}
return normalizeDeliveryHalfDayLabel(parsedValue);
};
export const getOrderGroupDeliveryHalfDay = (group) =>
normalizeDeliveryHalfDayLabel(
group?.deliveryHalfDay ||
group?.deliveryTime ||
group?.deliveryWindow ||
findDeliveryHalfDayInValue(group?.sourceOrders),
);
export const isOrderGroupAgreedForDelivery = (group) => {
if (!group) {
return false;
}
return isOrderGroupVisibleToDriver(group);
};
export const getOrderGroupDeliveryStatusLabel = (status) =>
DELIVERY_GROUP_STATUS_LABELS[status] || status || "Неизвестно";
export const getOrderGroupDisplayStatusLabel = (group) => {
const deliveryStatus = group?.deliveryStatus || group?.delivery_status;
const notificationStatus = group?.notificationStatus || group?.notification_status;
// When auto-SMS failed and logistics hasn't taken action yet → show as a todo item
const isManualRequired = notificationStatus === "manual_required";
const isStillPending = !deliveryStatus || deliveryStatus === "pending_confirmation" || deliveryStatus === "manual_confirmation_required";
if (isManualRequired && isStillPending) {
return "Требуется ручное управление";
}
if (deliveryStatus && deliveryStatus !== "pending_confirmation" && deliveryStatus !== "manual_confirmation_required") {
return getOrderGroupDeliveryStatusLabel(deliveryStatus);
}
const notificationLabel = NOTIFICATION_STATUS_LABELS[notificationStatus];
if (notificationLabel && notificationStatus !== "link_ready" && notificationStatus !== "not_started") {
return notificationLabel;
}
return getOrderGroupStatusLabel(group?.status);
};
export const getOrderGroupDisplayStatusValue = (group) => {
const deliveryStatus = group?.deliveryStatus || group?.delivery_status;
const notificationStatus = group?.notificationStatus || group?.notification_status;
// Unify manual_required into a single bucket regardless of delivery_status detail
const isManualRequired = notificationStatus === "manual_required";
const isStillPending = !deliveryStatus || deliveryStatus === "pending_confirmation" || deliveryStatus === "manual_confirmation_required";
if (isManualRequired && isStillPending) {
return "status:manual_required";
}
if (deliveryStatus && deliveryStatus !== "pending_confirmation" && deliveryStatus !== "manual_confirmation_required") {
return `delivery:${deliveryStatus}`;
}
return `status:${group?.status || "unknown"}`;
};
export const isOrderGroupVisibleToDriver = (group) => {
const deliveryStatus = group?.deliveryStatus || group?.delivery_status || "pending_confirmation";
return DRIVER_VISIBLE_DELIVERY_STATUSES.includes(deliveryStatus);
};
export const parseGroupDate = (value) => {
const normalized = normalizeDate(value);
if (!normalized) {
return null;
}
const isoDateMatch = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoDateMatch) {
return new Date(normalized);
}
const shortDateMatch = normalized.match(/^(\d{2})\.(\d{2})\.(\d{2})$/);
if (shortDateMatch) {
const [, day, month, year] = shortDateMatch;
return new Date(Date.UTC(Number(`20${year}`), Number(month) - 1, Number(day)));
}
const parsed = new Date(normalized);
return Number.isNaN(parsed.getTime()) ? null : parsed;
};
export const filterOrderGroups = (groups, filters = {}) => {
const query = normalizeDate(filters.query).trim().toLowerCase();
const status = filters.status || "all";
const displayStatus = normalizeDate(filters.displayStatus || "all");
const deliveryStatus = normalizeDate(filters.deliveryStatus || "all");
const dateFrom = normalizeDate(filters.dateFrom);
const dateTo = normalizeDate(filters.dateTo);
const deliveryHalfDay = normalizeDate(filters.deliveryHalfDay || filters.timeSlot || "all");
const isWithinDateRange = (group) => {
const deliveryDate = parseGroupDate(getDeliveryDate(group));
if (!deliveryDate) {
return !dateFrom && !dateTo;
}
if (dateFrom) {
const fromDate = parseGroupDate(dateFrom);
if (fromDate && deliveryDate < fromDate) {
return false;
}
}
if (dateTo) {
const toDate = parseGroupDate(dateTo);
if (toDate && deliveryDate > toDate) {
return false;
}
}
return true;
};
const getSearchHaystack = (group) =>
(group.searchText ||
[
group.groupKey,
group.displayTitle,
group.customerName,
group.customerPhone,
group.customerDate,
Array.isArray(group.orderNumbers) ? group.orderNumbers.join(" ") : "",
group.status,
getOrderGroupStatusLabel(group.status),
group.deliveryStatus,
getOrderGroupDeliveryStatusLabel(group.deliveryStatus),
]
.filter(Boolean)
.join(" "))
.toLowerCase();
return (groups || []).filter((group) => {
if (status !== "all" && group.status !== status) {
return false;
}
if (displayStatus !== "all" && getOrderGroupDisplayStatusValue(group) !== displayStatus) {
return false;
}
if (deliveryStatus !== "all") {
const groupDeliveryStatus = group.deliveryStatus || group.delivery_status || "pending_confirmation";
if (groupDeliveryStatus !== deliveryStatus) {
return false;
}
}
if (!isWithinDateRange(group)) {
return false;
}
if (deliveryHalfDay !== "all") {
const groupDeliveryHalfDay = getOrderGroupDeliveryHalfDay(group);
if (deliveryHalfDay === "unknown") {
if (groupDeliveryHalfDay) {
return false;
}
} else if (groupDeliveryHalfDay !== HALF_DAY_LABELS[deliveryHalfDay]) {
return false;
}
}
if (!query) {
return true;
}
return getSearchHaystack(group).includes(query);
});
};
export const ORDER_GROUP_STATUS_LABELS = {
ready_for_notification: "Готово к уведомлению",
sms_sent: "SMS отправлены",
manual_work: "Нужна ручная работа",
ready_to_launch: "Готово к запуску",
};
export const ORDER_GROUP_DISPLAY_STATUS_OPTIONS = [
{ value: "all", label: "Все статусы" },
{ value: "delivery:pending_confirmation", label: DELIVERY_GROUP_STATUS_LABELS.pending_confirmation },
{ value: "delivery:manual_confirmation_required", label: DELIVERY_GROUP_STATUS_LABELS.manual_confirmation_required },
{ value: "delivery:agreed", label: DELIVERY_GROUP_STATUS_LABELS.agreed },
{ value: "delivery:driver_assigned", label: DELIVERY_GROUP_STATUS_LABELS.driver_assigned },
{ value: "delivery:loaded", label: DELIVERY_GROUP_STATUS_LABELS.loaded },
{ value: "delivery:on_route", label: DELIVERY_GROUP_STATUS_LABELS.on_route },
{ value: "delivery:delivered", label: DELIVERY_GROUP_STATUS_LABELS.delivered },
{ value: "delivery:problem", label: DELIVERY_GROUP_STATUS_LABELS.problem },
{ value: "delivery:paid_storage", label: DELIVERY_GROUP_STATUS_LABELS.paid_storage },
{ value: "delivery:cancelled", label: DELIVERY_GROUP_STATUS_LABELS.cancelled },
{ value: "status:manual_required", label: "Требует ручной обработки" },
{ value: "status:second_sms_sent", label: "Повторное SMS" },
{ value: "status:sms_sent", label: "SMS отправлены" },
{ value: "status:ready_for_notification", label: "Готово к уведомлению" },
];
export const getOrderGroupStatusLabel = (status) =>
ORDER_GROUP_STATUS_LABELS[status] || status || "Неизвестно";
export const getOrderGroupDeliveryStatusTone = (status) => {
switch (status) {
case "pending_confirmation":
return "neutral";
case "manual_confirmation_required":
return "warning";
case "agreed":
return "accent";
case "driver_assigned":
return "info";
case "loaded":
return "info";
case "on_route":
return "warning";
case "delivered":
return "accent";
case "paid_storage":
return "warning";
case "problem":
return "danger";
case "cancelled":
return "danger";
default:
return "neutral";
}
};
export const groupOrderGroupsByDate = (groups) => {
const buckets = (groups || []).reduce((accumulator, group) => {
const date = getDeliveryDate(group) || "Без даты";
accumulator[date] = accumulator[date] || [];
accumulator[date].push(group);
return accumulator;
}, {});
return Object.entries(buckets)
.sort(([leftDate], [rightDate]) => {
const leftTime = parseGroupDate(leftDate)?.getTime();
const rightTime = parseGroupDate(rightDate)?.getTime();
if (leftTime != null && rightTime != null && leftTime !== rightTime) {
return leftTime - rightTime;
}
return leftDate.localeCompare(rightDate);
})
.map(([date, items]) => ({
date,
items: [...items].sort((left, right) => {
const leftCount = Number(left.ordersCount || 0);
const rightCount = Number(right.ordersCount || 0);
if (leftCount !== rightCount) {
return rightCount - leftCount;
}
return (right.updatedAt || "").localeCompare(left.updatedAt || "");
}),
}));
};
const getBucketKey = (group) => {
const notificationStatus = group?.notificationStatus || group?.notification_status;
if (notificationStatus === "manual_required") {
return "manual_work";
}
if (group.smsSentAt) {
return "sms_sent";
}
if ((group.readyCount || 0) > 0 && (group.notReadyCount || 0) > 0) {
return "manual_work";
}
if ((group.notReadyCount || 0) > 0) {
return "manual_work";
}
if (group.status === "ready_for_notification" || (group.readyCount || 0) >= (group.ordersCount || 0)) {
return "ready_to_launch";
}
return "manual_work";
};
export const ORDER_GROUP_BUCKET_LABELS = {
ready_to_launch: "Готовы к уведомлению",
sms_sent: "Уведомления отправлены",
manual_work: "Нужна ручная работа",
};
export const ORDER_GROUP_DELIVERY_HALF_DAY_OPTIONS = [
{ value: "all", label: "Все интервалы" },
{ value: "morning", label: HALF_DAY_LABELS.morning },
{ value: "afternoon", label: HALF_DAY_LABELS.afternoon },
{ value: "unknown", label: "Без времени" },
];
export const buildOrderGroupBuckets = (groups) => {
const buckets = {
ready_to_launch: [],
sms_sent: [],
manual_work: [],
};
for (const group of groups || []) {
const bucketKey = getBucketKey(group);
buckets[bucketKey].push(group);
}
return buckets;
};
export const getOrderGroupStatusTone = (group) => {
const deliveryStatus = group?.deliveryStatus || group?.delivery_status;
if (deliveryStatus && deliveryStatus !== "pending_confirmation") {
return getOrderGroupDeliveryStatusTone(deliveryStatus);
}
const notificationStatus = group?.notificationStatus || group?.notification_status;
if (notificationStatus === "send_failed" || notificationStatus === "manual_required") {
return "warning";
}
if (notificationStatus === "first_sms_sent" || notificationStatus === "second_sms_sent") {
return "accent";
}
if (group.smsSentAt) {
return "accent";
}
if ((group.notReadyCount || 0) > 0) {
return "warning";
}
return "neutral";
};