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"; };