feat(orders): progress stepper + fix field_change label

This commit is contained in:
root 2026-08-06 07:14:46 +00:00
parent 15b69102b0
commit 464cc57eb9
1 changed files with 134 additions and 2 deletions

View File

@ -60,6 +60,7 @@ const ACTION_LABELS = {
delivery_status_change: "Изменение статуса доставки",
notification_status_change: "Изменение статуса уведомления",
status_change: "Изменение статуса",
field_change: "Изменение данных заказа",
};
const SMS_CAMPAIGN_LABELS = {
@ -113,6 +114,134 @@ const formatChanges = (changes) => {
return lines;
};
// Progress Stepper
// Visual horizontal stepper showing where the client is in the journey.
// Stages: Добавлен SMS Согласовано Водитель (delivery only) Доставлен
const STAGES_DELIVERY = [
{ key: "created", icon: "📥", label: "Добавлен" },
{ key: "sms", icon: "📨", label: "SMS" },
{ key: "agreed", icon: "✅", label: "Согласовано" },
{ key: "driver", icon: "🚚", label: "Водитель" },
{ key: "delivered", icon: "📦", label: "Доставлено" },
];
const STAGES_PICKUP = [
{ key: "created", icon: "📥", label: "Добавлен" },
{ key: "sms", icon: "📨", label: "SMS" },
{ key: "agreed", icon: "✅", label: "Согласовано" },
{ key: "picked_up", icon: "📦", label: "Вывезено" },
];
const getActiveStageIndex = (order) => {
const deliveryStatus = order?.deliveryStatus || order?.delivery_status || "";
const notifStatus = order?.notificationStatus || order?.notification_status || "";
const firstSms = order?.firstSmsSentAt || order?.first_sms_sent_at;
const isPickup = (order?.deliveryType || order?.delivery_type) === "pickup";
const stages = isPickup ? STAGES_PICKUP : STAGES_DELIVERY;
// Terminal states
if (deliveryStatus === "cancelled") return -1; // special: show all grey
if (deliveryStatus === "problem") return -2; // special: show problem indicator
if (deliveryStatus === "paid_storage") return -3;
// Final delivery stage
if (isPickup) {
if (deliveryStatus === "picked_up") return stages.length - 1;
} else {
if (deliveryStatus === "delivered") return stages.length - 1;
}
// Driver assigned (delivery only)
if (!isPickup && ["driver_assigned", "loaded", "on_route"].includes(deliveryStatus)) {
return 3; // driver stage
}
// Agreed
if (deliveryStatus === "agreed" || notifStatus === "confirmed") {
return 2;
}
// SMS sent
if (firstSms || ["first_sms_sent", "second_sms_sent", "confirmed"].includes(notifStatus)) {
return 1;
}
// Just created
return 0;
};
const ProgressStepper = ({ order }) => {
const isPickup = (order?.deliveryType || order?.delivery_type) === "pickup";
const stages = isPickup ? STAGES_PICKUP : STAGES_DELIVERY;
const activeIdx = getActiveStageIndex(order);
// Special states
if (activeIdx === -1) {
return (
<div className="flex items-center gap-2 rounded-xl bg-[rgba(239,68,68,0.08)] px-3 py-2">
<span className="text-sm"></span>
<span className="text-sm font-semibold text-[var(--color-danger)]">Заказ отменён</span>
</div>
);
}
if (activeIdx === -2) {
return (
<div className="flex items-center gap-2 rounded-xl bg-[rgba(239,68,68,0.08)] px-3 py-2">
<span className="text-sm"></span>
<span className="text-sm font-semibold text-[var(--color-danger)]">Проблема с заказом</span>
</div>
);
}
if (activeIdx === -3) {
return (
<div className="flex items-center gap-2 rounded-xl bg-[rgba(245,158,11,0.08)] px-3 py-2">
<span className="text-sm">📦</span>
<span className="text-sm font-semibold text-[var(--color-warning)]">Платное хранение</span>
</div>
);
}
return (
<div className="flex items-center gap-1">
{stages.map((stage, idx) => {
const isDone = idx < activeIdx;
const isActive = idx === activeIdx;
const isFuture = idx > activeIdx;
return (
<React.Fragment key={stage.key}>
{idx > 0 && (
<div className={`h-0.5 flex-1 min-w-[12px] rounded-full transition-colors ${
isDone ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]"
}`} />
)}
<div className="flex flex-col items-center gap-1 flex-shrink-0">
<div className={`flex h-7 w-7 items-center justify-center rounded-full text-xs transition-all ${
isActive
? "bg-[var(--color-accent)] text-white ring-2 ring-[var(--color-accent)] ring-offset-2 ring-offset-[var(--color-surface)]"
: isDone
? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]"
: "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]"
}`}>
{isDone ? "✓" : stage.icon}
</div>
<span className={`text-[10px] whitespace-nowrap ${
isActive
? "font-bold text-[var(--color-text)]"
: isDone
? "font-medium text-[var(--color-accent)]"
: "text-[var(--color-text-muted)]"
}`}>
{stage.label}
</span>
</div>
</React.Fragment>
);
})}
</div>
);
};
export const OrderHistoryTimeline = ({ order, userRole }) => {
const [history, setHistory] = useState(null);
const [smsLogs, setSmsLogs] = useState([]);
@ -211,6 +340,8 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
if (allEvents.length === 0) return null;
return (
<div className="space-y-4">
<ProgressStepper order={order} />
<ol className="relative space-y-3 pl-4">
{allEvents.map((ev, idx) => (
<li key={ev.id ?? idx} className="relative border-l border-[var(--color-border)] pl-4">
@ -254,6 +385,7 @@ export const OrderHistoryTimeline = ({ order, userRole }) => {
</li>
))}
</ol>
</div>
);
};