462 lines
13 KiB
JavaScript
462 lines
13 KiB
JavaScript
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,
|
||
};
|
||
};
|