446 lines
13 KiB
JavaScript
446 lines
13 KiB
JavaScript
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: "Проблема",
|
||
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 = [
|
||
"agreed",
|
||
"driver_assigned",
|
||
"loaded",
|
||
"on_route",
|
||
"problem",
|
||
"delivered",
|
||
];
|
||
|
||
export const DRIVER_ACTIVE_DELIVERY_STATUSES = ["agreed", "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;
|
||
|
||
if (deliveryStatus && deliveryStatus !== "pending_confirmation") {
|
||
return getOrderGroupDeliveryStatusLabel(deliveryStatus);
|
||
}
|
||
|
||
const notificationStatus = group?.notificationStatus || group?.notification_status;
|
||
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;
|
||
|
||
if (deliveryStatus) {
|
||
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);
|
||
};
|
||
|
||
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:cancelled", label: DELIVERY_GROUP_STATUS_LABELS.cancelled },
|
||
];
|
||
|
||
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 "accent";
|
||
case "delivered":
|
||
return "accent";
|
||
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";
|
||
};
|