supersam/src/services/orderService.js

462 lines
13 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.

import { demoOrders } from "../data/mockAppData";
import {
getAvailableTransitionsByRole,
getStatusOwnerRole,
getStatusStageKey,
LOGISTICS_STATUSES,
PRODUCTION_STATUSES,
} from "../constants/deliveryWorkflow";
import { getOrderAgingState } from "./orderViews";
const DELIVERY_HANDOFF_STATUS = "Назначен водитель";
const DELIVERY_HANDOFF_REQUIRES_DRIVER_MESSAGE =
"Сначала назначьте водителя, потом заказ можно передать в доставку.";
const DELIVERY_HANDOFF_ROLE_MESSAGE =
"Логист может передать заказ в доставку только через статус «Назначен водитель».";
export const cloneOrders = (orders = demoOrders) =>
orders.map((order) => ({
...order,
customer: { ...order.customer },
history: [...order.history],
chatMessages: [...order.chatMessages],
internalMessages: [...(order.internalMessages || [])],
orderNotes: [...(order.orderNotes || [])],
deliverySlots: [...order.deliverySlots],
logisticianIds: [...order.logisticianIds],
assignedDriverId: order.assignedDriverId || null,
driverRouteOrder: order.driverRouteOrder ?? null,
deliveryAgreementStatus: order.deliveryAgreementStatus || "Не начато",
comments: [...order.comments],
items: [...(order.items || [])],
tags: [...order.tags],
}));
export const buildSearchBlob = (order) => {
return [
order.orderNumber,
order.customer.name,
order.customer.phone,
order.status,
order.deliveryAgreementStatus,
order.exception || "",
order.customer.messenger,
...(order.items || []),
...order.comments,
...order.tags,
]
.join(" ")
.toLowerCase();
};
export const filterOrdersByView = ({ orders, currentUser, filters, now }) => {
const normalizedFilters = {
query: "",
status: "all",
stage: "all",
ownerRole: "all",
agingState: "all",
managerId: "all",
logisticianId: "all",
messenger: "all",
...filters,
};
const visibleOrders = orders.filter((order) => {
if (!currentUser) {
return false;
}
if (currentUser.role === "manager" || currentUser.role === "admin" || currentUser.role === "mega_admin") {
return true;
}
return getStatusOwnerRole(order.status) === currentUser.role;
});
const normalizedQuery = normalizedFilters.query.trim().toLowerCase();
const filteredOrders = visibleOrders.filter((order) => {
const stageKey = getStatusStageKey(order.status);
const ownerRole = getStatusOwnerRole(order.status);
const { agingState } = getOrderAgingState(order, { now });
if (normalizedFilters.status !== "all" && order.status !== normalizedFilters.status) {
return false;
}
if (normalizedFilters.stage !== "all" && stageKey !== normalizedFilters.stage) {
return false;
}
if (normalizedFilters.ownerRole !== "all" && ownerRole !== normalizedFilters.ownerRole) {
return false;
}
if (normalizedFilters.agingState !== "all" && agingState !== normalizedFilters.agingState) {
return false;
}
if (normalizedFilters.managerId !== "all" && order.managerId !== normalizedFilters.managerId) {
return false;
}
if (
normalizedFilters.logisticianId !== "all" &&
!order.logisticianIds.includes(normalizedFilters.logisticianId)
) {
return false;
}
if (
normalizedFilters.messenger !== "all" &&
order.customer.messenger !== normalizedFilters.messenger
) {
return false;
}
if (normalizedQuery && !buildSearchBlob(order).includes(normalizedQuery)) {
return false;
}
return true;
});
return { visibleOrders, filteredOrders };
};
export const appendHistoryEntry = (order, entry) => ({
...order,
history: [
{
id: `history-${Date.now()}`,
at: new Date().toISOString(),
...entry,
},
...order.history,
],
});
export const getAvailableTransitions = ({ status, role }) =>
getAvailableTransitionsByRole({ status, role });
export const getAvailableTransitionsForOrder = ({ order, role }) =>
getAvailableTransitionsByRole({ status: order.status, role }).filter((nextStatus) => {
if (nextStatus === DELIVERY_HANDOFF_STATUS && !order.assignedDriverId) {
return false;
}
return true;
});
export const getTransitionBlockedReason = ({ order, nextStatus, role }) => {
if (nextStatus === DELIVERY_HANDOFF_STATUS && !order.assignedDriverId) {
return DELIVERY_HANDOFF_REQUIRES_DRIVER_MESSAGE;
}
const roleTransitions = getAvailableTransitionsByRole({ status: order.status, role });
if (!roleTransitions.includes(nextStatus) && role === "logistician" && ["Загружен", "В пути", "Доставлен"].includes(nextStatus)) {
return DELIVERY_HANDOFF_ROLE_MESSAGE;
}
return "Этот переход сейчас недоступен для вашей роли.";
};
export const getKanbanDropResolution = ({ order, column, role }) => {
const allowedStatuses = getAvailableTransitionsForOrder({ order, role });
const nextStatus = column.statuses.find((status) => allowedStatuses.includes(status));
if (nextStatus) {
return {
nextStatus,
reason: null,
};
}
if (column.statuses.includes(DELIVERY_HANDOFF_STATUS)) {
return {
nextStatus: null,
reason: getTransitionBlockedReason({
order,
nextStatus: DELIVERY_HANDOFF_STATUS,
role,
}),
};
}
return {
nextStatus: null,
reason: getTransitionBlockedReason({
order,
nextStatus: column.statuses[0],
role,
}),
};
};
const deriveAgreementStatus = (currentOrder, nextStatus) => {
if (nextStatus === "Ожидает ответа клиента") {
return currentOrder.deliveryAgreementStatus === "Подтверждено клиентом"
? "Отправлено клиенту"
: "Ожидание ответа";
}
if (nextStatus === "Ожидает согласования доставки") {
return currentOrder.deliveryAgreementStatus === "Подтверждено клиентом"
? "Отправлено клиенту"
: "Ожидание ответа";
}
if (nextStatus === "Доставка согласована") {
return "Подтверждено клиентом";
}
if (nextStatus === "Передан логисту") {
return currentOrder.deliveryAgreementStatus === "Подтверждено клиентом"
? "Перенос запрошен"
: "Нет ответа";
}
if (nextStatus === "Платное хранение") {
return "Нет ответа";
}
if (nextStatus === "Проблема доставки") {
return currentOrder.deliveryAgreementStatus === "Не начато"
? "Ошибка отправки"
: currentOrder.deliveryAgreementStatus;
}
return currentOrder.deliveryAgreementStatus || "Не начато";
};
export const applyStatusUpdate = (order, nextStatus, actorName) => {
return appendHistoryEntry(
{
...order,
status: nextStatus,
deliveryAgreementStatus: deriveAgreementStatus(order, nextStatus),
updatedAt: new Date().toISOString(),
},
{
action: "Изменение статуса",
oldStatus: order.status,
newStatus: nextStatus,
userName: actorName,
},
);
};
export const assignDriverToOrder = (order, driverId, actorName) => {
const normalizedDriverId = driverId || null;
return appendHistoryEntry(
{
...order,
assignedDriverId: normalizedDriverId,
driverRouteOrder: normalizedDriverId ? order.driverRouteOrder : null,
updatedAt: new Date().toISOString(),
},
{
action: "Назначение водителя",
oldStatus: order.status,
newStatus: order.status,
userName: actorName,
},
);
};
export const appendChatMessageToOrder = (order, message) => ({
...order,
updatedAt: new Date().toISOString(),
chatMessages: [
{
id: `chat-${Date.now()}`,
sentAt: new Date().toISOString(),
...message,
},
...order.chatMessages,
],
});
export const appendInternalMessageToOrder = (order, message) => ({
...order,
updatedAt: new Date().toISOString(),
internalMessages: [
{
id: `internal-${Date.now()}`,
sentAt: new Date().toISOString(),
...message,
},
...(order.internalMessages || []),
],
});
export const appendOrderNote = (order, note) => ({
...order,
updatedAt: new Date().toISOString(),
orderNotes: [
{
id: `note-${Date.now()}`,
createdAt: new Date().toISOString(),
...note,
},
...(order.orderNotes || []),
],
});
export const applyDeliveryReschedule = (order, deliverySlot, actorName) => {
const normalizedTime =
deliverySlot.time === "Вторая половина дня" ? "13:00:00Z" : "09:00:00Z";
return appendHistoryEntry(
{
...order,
status: "Ожидает согласования доставки",
deliveryAgreementStatus: "Перенос запрошен",
updatedAt: new Date().toISOString(),
scheduledDelivery: `${deliverySlot.date}T${normalizedTime}`,
deliverySlots: [
{
id: `slot-${Date.now()}`,
...deliverySlot,
status: "Ожидает подтверждения",
},
...order.deliverySlots,
],
},
{
action: "Перенос доставки",
oldStatus: order.status,
newStatus: "Ожидает согласования доставки",
userName: actorName,
},
);
};
export const updateOrderDetails = (order, payload, actorName) => {
return appendHistoryEntry(
{
...order,
customer: {
...order.customer,
name: payload.customerName,
phone: payload.customerPhone,
address: payload.customerAddress,
messenger: payload.messenger,
},
orderNumber: payload.orderNumber,
managerId: payload.managerId,
items: payload.items
.split("\n")
.map((item) => item.trim())
.filter(Boolean),
comments: payload.comments
.split(",")
.map((item) => item.trim())
.filter(Boolean),
tags: payload.tags
.split(",")
.map((item) => item.trim())
.filter(Boolean),
updatedAt: new Date().toISOString(),
},
{
action: "Редактирование заказа",
oldStatus: order.status,
newStatus: order.status,
userName: actorName,
},
);
};
export const createOrderRecord = ({ payload, actorName, availableLogisticians = [] }) => {
const assignedLogistician = availableLogisticians[0]?.id || null;
return {
id: `order-${Date.now()}`,
orderNumber: payload.orderNumber,
customer: {
name: payload.customerName,
phone: payload.customerPhone,
address: payload.customerAddress,
messenger: payload.messenger,
},
status: "Новый",
managerId: payload.managerId,
logisticianIds: assignedLogistician ? [assignedLogistician] : [],
assignedDriverId: null,
driverRouteOrder: null,
deliveryAgreementStatus: "Не начато",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
scheduledDelivery: payload.deliveryDate
? `${payload.deliveryDate}T10:00:00Z`
: new Date().toISOString(),
tags: payload.tags
.split(",")
.map((item) => item.trim())
.filter(Boolean),
items: payload.items
.split("\n")
.map((item) => item.trim())
.filter(Boolean),
comments: payload.comments
.split(",")
.map((item) => item.trim())
.filter(Boolean),
history: [
{
id: `history-${Date.now()}`,
action: "Создание заказа",
oldStatus: null,
newStatus: "Новый",
userName: actorName,
at: new Date().toISOString(),
},
],
chatMessages: [],
internalMessages: [],
orderNotes: [],
deliverySlots: payload.deliveryDate
? [
{
id: `slot-${Date.now()}`,
date: payload.deliveryDate,
time: "Первая половина дня",
logisticianId: assignedLogistician,
status: "Черновик",
},
]
: [],
exception: null,
};
};
export const autoAssignOrders = (orders, logisticians) => {
if (!logisticians.length) {
return orders;
}
return orders.map((order, index) =>
appendHistoryEntry(
{
...order,
logisticianIds: [logisticians[index % logisticians.length].id],
updatedAt: new Date().toISOString(),
},
{
action: "Автораспределение логиста",
oldStatus: order.status,
newStatus: order.status,
userName: "Система",
},
),
);
};
export const buildMetrics = (orders) => {
const byStatus = orders.reduce((accumulator, order) => {
accumulator[order.status] = (accumulator[order.status] || 0) + 1;
return accumulator;
}, {});
return {
total: orders.length,
readyToShip: byStatus["Готов к отгрузке"] || 0,
awaitingDeliveryCoordination: byStatus["Ожидает согласования доставки"] || 0,
inProduction: PRODUCTION_STATUSES.reduce((sum, status) => sum + (byStatus[status] || 0), 0),
inLogistics: LOGISTICS_STATUSES.reduce((sum, status) => sum + (byStatus[status] || 0), 0),
exceptions: byStatus["Проблема доставки"] || 0,
};
};