240 lines
9.8 KiB
JavaScript
240 lines
9.8 KiB
JavaScript
import React from "react";
|
||
import { getAvailableTransitionsByRole, getDeliveryAgreementComment, getOrderStatusComment, getStatusTone } from "../../constants/deliveryWorkflow";
|
||
import { demoUsers } from "../../data/mockAppData";
|
||
import { formatDateTime } from "../../utils/formatters";
|
||
import { Badge } from "../UI/Badge";
|
||
import { Button } from "../UI/Button";
|
||
import { Panel } from "../UI/Panel";
|
||
|
||
const getUsers = (users) => (Array.isArray(users) && users.length ? users : demoUsers);
|
||
const resolveUserName = (users, userId) => getUsers(users).find((user) => user.id === userId)?.name || "Не назначен";
|
||
|
||
const splitItem = (item) => {
|
||
if (!item) {
|
||
return { name: "Позиция", quantity: "" };
|
||
}
|
||
|
||
if (typeof item === "string") {
|
||
const [name, quantity] = item.split("|").map((part) => part.trim());
|
||
return {
|
||
name: name || item,
|
||
quantity: quantity || "",
|
||
};
|
||
}
|
||
|
||
if (typeof item === "object") {
|
||
return {
|
||
name: item.name || item.label || "Позиция",
|
||
quantity: typeof item.quantity === "number" ? String(item.quantity) : item.quantity || "",
|
||
};
|
||
}
|
||
|
||
return { name: "Позиция", quantity: "" };
|
||
};
|
||
|
||
export const OrderDetailPanel = ({ order, users, currentUser, onStatusChange, onAssignDriver }) => {
|
||
if (!order) {
|
||
return (
|
||
<Panel className="flex min-h-[460px] items-center justify-center">
|
||
<p className="text-sm text-[var(--color-text-muted)]">Выберите заказ для просмотра деталей.</p>
|
||
</Panel>
|
||
);
|
||
}
|
||
|
||
const orderItems = Array.isArray(order.items) ? order.items.map(splitItem) : [];
|
||
const orderHistory = Array.isArray(order.history) ? order.history : [];
|
||
const role = currentUser?.role;
|
||
const availableTransitions = role ? getAvailableTransitionsByRole({ status: order.status, role }) : [];
|
||
const drivers = (Array.isArray(users) && users.length ? users : demoUsers).filter((u) => u.role === "driver");
|
||
const canAssignDriver = role === "logistician" || role === "admin";
|
||
|
||
return (
|
||
<div className="space-y-5">
|
||
<Panel className="space-y-5 p-6">
|
||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<p className="text-sm uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Карточка заказа</p>
|
||
<h2 className="mt-2 text-2xl font-semibold">{order.orderNumber}</h2>
|
||
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
|
||
{order.customer.name} · {order.customer.address}
|
||
</p>
|
||
</div>
|
||
<Badge tone={getStatusTone(order.status)}>{order.status}</Badge>
|
||
</div>
|
||
|
||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||
{getOrderStatusComment(order.status)}
|
||
</p>
|
||
|
||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Менеджер</p>
|
||
<p className="mt-1 font-medium">{resolveUserName(users, order.managerId)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Логист</p>
|
||
<p className="mt-1 font-medium">{resolveUserName(users, order.logisticianIds?.[0])}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Водитель</p>
|
||
<p className="mt-1 font-medium">{resolveUserName(users, order.assignedDriverId)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Дата создания</p>
|
||
<p className="mt-1 font-medium">{formatDateTime(order.createdAt)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">План доставки</p>
|
||
<p className="mt-1 font-medium">{formatDateTime(order.scheduledDelivery)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Канал связи</p>
|
||
<p className="mt-1 font-medium">{order.customer.messenger}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Согласование доставки</p>
|
||
<p className="mt-1 font-medium">{order.deliveryAgreementStatus}</p>
|
||
</div>
|
||
</div>
|
||
</Panel>
|
||
|
||
<Panel className="space-y-4 p-5">
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<strong>Данные клиента</strong>
|
||
<Badge tone={getStatusTone(order.status)}>{order.status}</Badge>
|
||
</div>
|
||
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
|
||
{getDeliveryAgreementComment(order.deliveryAgreementStatus)}
|
||
</p>
|
||
<div className="grid gap-4 md:grid-cols-2">
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Клиент</p>
|
||
<p className="mt-1 font-medium">{order.customer.name}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Телефон</p>
|
||
<p className="mt-1 font-medium">{order.customer.phone}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Адрес</p>
|
||
<p className="mt-1 font-medium">{order.customer.address}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-[var(--color-text-muted)]">Дата доставки</p>
|
||
<p className="mt-1 font-medium">{formatDateTime(order.scheduledDelivery)}</p>
|
||
</div>
|
||
</div>
|
||
</Panel>
|
||
|
||
<Panel className="space-y-4 p-5">
|
||
<strong>Состав заказа</strong>
|
||
<div className="space-y-3">
|
||
{orderItems.length ? (
|
||
orderItems.map((item) => (
|
||
<div
|
||
key={`${item.name}-${item.quantity || "item"}`}
|
||
className="flex items-center justify-between gap-3 rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3 text-sm"
|
||
>
|
||
<span>{item.name}</span>
|
||
{item.quantity ? <Badge tone="neutral">{item.quantity}</Badge> : null}
|
||
</div>
|
||
))
|
||
) : (
|
||
<p className="text-sm text-[var(--color-text-muted)]">Состав заказа не указан.</p>
|
||
)}
|
||
</div>
|
||
</Panel>
|
||
|
||
{order.orderNotes?.length ? (
|
||
<Panel className="space-y-3 p-5">
|
||
<strong>Комментарии</strong>
|
||
<div className="space-y-2">
|
||
{order.orderNotes.map((note) => (
|
||
<div
|
||
key={note.id}
|
||
className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-sm leading-6"
|
||
>
|
||
{note.text}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Panel>
|
||
) : null}
|
||
|
||
{order.comments?.length ? (
|
||
<Panel className="space-y-3 p-5">
|
||
<strong>Дополнительные комментарии</strong>
|
||
<div className="space-y-2 text-sm leading-6 text-[var(--color-text-muted)]">
|
||
{order.comments.map((comment, index) => (
|
||
<div key={`${comment}-${index}`} className="rounded-[20px] bg-[var(--color-surface)] p-4">
|
||
{comment}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Panel>
|
||
) : null}
|
||
|
||
{availableTransitions.length ? (
|
||
<Panel className="space-y-4 p-5">
|
||
<strong>Действия</strong>
|
||
<div className="flex flex-wrap gap-2">
|
||
{availableTransitions.map((status) => (
|
||
<Button
|
||
key={status}
|
||
variant={status === "Проблема доставки" || status === "Платное хранение" || status === "Отменён" ? "ghost" : "secondary"}
|
||
onClick={() => onStatusChange?.(status)}
|
||
>
|
||
{status}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</Panel>
|
||
) : null}
|
||
|
||
{canAssignDriver ? (
|
||
<Panel className="space-y-4 p-5">
|
||
<strong>Назначить водителя</strong>
|
||
<div className="flex flex-wrap gap-2">
|
||
{drivers.map((driver) => (
|
||
<Button
|
||
key={driver.id}
|
||
variant={order.assignedDriverId === driver.id ? "primary" : "secondary"}
|
||
onClick={() => onAssignDriver?.({ orderId: order.id, driverId: driver.id, actorName: currentUser.name })}
|
||
>
|
||
{driver.name}
|
||
</Button>
|
||
))}
|
||
{order.assignedDriverId ? (
|
||
<Button variant="ghost" onClick={() => onAssignDriver?.({ orderId: order.id, driverId: null, actorName: currentUser.name })}>
|
||
Снять водителя
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</Panel>
|
||
) : null}
|
||
|
||
{orderHistory.length ? (
|
||
<Panel className="space-y-3 p-5">
|
||
<strong>История</strong>
|
||
<div className="space-y-2">
|
||
{orderHistory.map((entry) => (
|
||
<div
|
||
key={entry.id}
|
||
className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-sm leading-6"
|
||
>
|
||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||
<span className="font-medium">{entry.action}</span>
|
||
<span className="text-[var(--color-text-muted)]">{formatDateTime(entry.at)}</span>
|
||
</div>
|
||
<div className="mt-2 text-[var(--color-text-muted)]">
|
||
{entry.oldStatus || "Начало"} → {entry.newStatus}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Panel>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|