feat(orders): tabbed navigation for logistics blocks (Все + per-block tabs)

This commit is contained in:
root 2026-08-06 08:03:11 +00:00
parent 811e2a1254
commit 1f8a8ea2c4
1 changed files with 426 additions and 405 deletions

View File

@ -691,6 +691,16 @@ export const OrderDetailPanel = ({
} catch {}
return DEFAULT_BLOCK_ORDER;
});
const [activeTab, setActiveTab] = React.useState(() => {
try {
return localStorage.getItem("supersam-active-tab") || "all";
} catch {}
return "all";
});
const handleTabChange = (tab) => {
setActiveTab(tab);
try { localStorage.setItem("supersam-active-tab", tab); } catch {}
};
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
);
@ -886,6 +896,353 @@ export const OrderDetailPanel = ({
}
};
// Render the inner content of a block by blockKey (used in both DnD "all" mode and single-tab mode)
const renderBlockContent = (blockKey) => {
if (blockKey === "manual_confirmation" && canManageDelivery) {
return (
<>
<p className="text-sm text-[var(--color-text-muted)]">
{isDeliveryAgreed
? "Дата и время уже зафиксированы."
: "Если клиент согласовал доставку или самовывоз по телефону, сохраните дату и время здесь."}
</p>
{/* Delivery type tabs */}
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<button
type="button"
className={["flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition",
deliveryType === "delivery"
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
: "text-[var(--color-text-muted)] hover:bg-[var(--color-accent-soft)]"
].join(" ")}
onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }}
>
🚚 Доставка
</button>
<button
type="button"
className={["flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition",
deliveryType === "pickup"
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
: "text-[var(--color-text-muted)] hover:bg-[var(--color-accent-soft)]"
].join(" ")}
onClick={() => { setDeliveryType("pickup"); setFormMessage(""); }}
>
🏪 Самовывоз
</button>
</div>
{deliveryType === "pickup" && (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-sm text-[var(--color-text-muted)]">
<p className="font-semibold text-[var(--color-text)]"> Условия хранения</p>
<p className="mt-1">Бесплатное хранение <strong>2 рабочих дня</strong> с даты готовности.</p>
<p>Начиная с 3-го рабочего дня <strong>300 /день</strong> платного хранения.</p>
</div>
)}
{deliveryType === "delivery" && (
<div className="space-y-2">
<label className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Адрес доставки
</label>
<input
type="text"
value={deliveryAddress}
onChange={(e) => setDeliveryAddress(e.target.value)}
placeholder="Введите адрес доставки"
className="w-full rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3 text-sm !text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none"
/>
</div>
)}
{agreedTypeMatchesTab ? (
<div className="space-y-3">
<div className="rounded-[24px] border border-[rgba(18,128,92,0.35)] bg-[var(--color-accent-soft)] p-4 !text-[var(--color-text)]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-accent)]">
{deliveryType === "pickup" ? "Самовывоз согласован" : "Доставка согласована"}
</p>
<p className="mt-1 text-lg font-semibold">
{agreedDeliveryLabel || "Дата не указана — нажмите «Изменить дату»"}
</p>
</div>
<Badge tone="accent">Согласовано</Badge>
</div>
</div>
{canEditDelivery ? (
<Button
variant="secondary"
onClick={() => { setIsEditingDate(true); setFormMessage(""); }}
disabled={isSavingDeliveryChoice}
className="text-sm"
>
Изменить дату {deliveryType === "pickup" ? "самовывоза" : "доставки"}
</Button>
) : null}
</div>
) : deliveryType === "delivery" ? (
<CalendarWidget
label="Календарь доставки"
selectedDate={deliveryDate}
onDateChange={(dateKey) => { setDeliveryDate(dateKey); setFormMessage(""); }}
minDateKey={minSelectableDateKey}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
calendarDays={calendarDays}
monthLabel={monthLabel}
canGoBack={canGoBack}
timeOptions={DELIVERY_TIME_OPTIONS}
selectedTime={deliveryTime}
onTimeChange={(option) => { setDeliveryTime(option); setFormMessage(""); }}
layoutClassName="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10"
calendarClassName="relative space-y-3 md:min-w-0 md:flex-1 md:pr-4"
timeClassName="grid gap-2 sm:grid-cols-2 md:w-[320px] md:flex-none"
/>
) : (
<CalendarWidget
label="Календарь самовывоза"
selectedDate={pickupDate}
onDateChange={(dateKey) => { setPickupDate(dateKey); setFormMessage(""); }}
minDateKey={minSelectableDateKey}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
calendarDays={calendarDays}
monthLabel={monthLabel}
canGoBack={canGoBack}
timeOptions={DELIVERY_TIME_OPTIONS}
selectedTime={pickupTimeSlot}
onTimeChange={(option) => { setPickupTimeSlot(option); setFormMessage(""); }}
layoutClassName="space-y-3"
calendarClassName="relative"
timeClassName="grid gap-2 sm:grid-cols-2"
/>
)}
<Button
className="w-full md:w-[180px] md:flex-none md:self-start"
onClick={() => setConfirmAction({ type: 'delivery' })}
disabled={isSavingDeliveryChoice}
>
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
</Button>
{formMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{formMessage}</p>
) : null}
</>
);
}
if (blockKey === "driver_assignment" && deliveryType === "delivery") {
return (
<DriverAssignmentPanel
order={order}
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingDriverAssignment={isSavingDriverAssignment}
selectedDriverId={selectedDriverId}
onDriverSelect={(id) => { setSelectedDriverId(id); setDriverMessage(""); }}
onConfirmDriver={() => setConfirmAction({ type: 'driver' })}
driverMessage={driverMessage}
drivers={drivers}
/>
);
}
if (blockKey === "shipment") {
return (
<DriverShipmentPanel
order={order}
onShipmentChange={handleShipmentChange}
onSaveShipment={handleSaveShipment}
isSavingShipment={isSavingShipment}
onResetStatus={() => {
if (onChangeDeliveryStatus) {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "driver_assigned",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось сбросить статус");
} else {
setFormMessage("Статус сброшен, отгрузка очищена");
}
});
}
}}
isSavingStatusChange={isSavingStatusChange}
groupByOrder={true}
/>
);
}
if (blockKey === "sms_status") {
return (
<SmsStatusCard order={order} userRole={userRole} />
);
}
if (blockKey === "status_actions") {
return (
<>
<StatusActionPanel
order={order}
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingStatusChange={isSavingStatusChange}
onRefreshOrder={() => {}}
onConfirmStatus={(action) => {
if (action.type === "hint") {
setFormMessage(action.hint);
} else if (action.type === "status") {
setConfirmAction({
type: "status",
status: action.status,
label: action.label,
mismatch: action.mismatch,
deliveryType: action.deliveryType,
});
}
}}
/>
{formMessage && onChangeDeliveryStatus ? (
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p>
) : null}
</>
);
}
if (blockKey === "paid_storage" && onChangeDeliveryStatus) {
return (
<PaidStoragePanel
order={order}
onChangeDeliveryStatus={onChangeDeliveryStatus}
isSavingStatusChange={isSavingStatusChange}
setFormMessage={setFormMessage}
/>
);
}
if (blockKey === "delivery_link" && order?.deliveryLink) {
return (
<>
<p className="text-sm text-[var(--color-text-muted)]">
Отправьте эту ссылку клиенту, чтобы он мог согласовать доставку или самовывоз самостоятельно.
</p>
<div className="flex flex-wrap items-center gap-2">
<a
href={order.deliveryLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-2xl bg-[var(--color-accent)] px-4 py-2.5 text-sm font-semibold text-white transition hover:opacity-90"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
Открыть страницу согласования
</a>
<Button
variant="secondary"
onClick={() => {
navigator.clipboard?.writeText(order.deliveryLink).then(() => {
setFormMessage("Ссылка скопирована в буфер обмена");
setTimeout(() => setFormMessage(""), 3000);
}).catch(() => {
setFormMessage("Не удалось скопировать ссылку");
});
}}
>
<span className="inline-flex items-center gap-1.5">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
Скопировать ссылку
</span>
</Button>
</div>
{order.invitationAccessCount > 0 ? (
<p className="text-sm text-[var(--color-text-muted)]">
👁 Клиент открывал страницу {order.invitationAccessCount} раз{(order.invitationLastAccessedAt || order.invitationOpenedAt) ? `, последний раз ${new Date(order.invitationLastAccessedAt || order.invitationOpenedAt).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "2-digit" })}` : ""}.
</p>
) : (
<p className="text-sm text-[var(--color-text-muted)]">
Клиент ещё не открывал страницу согласования.
</p>
)}
</>
);
}
if (blockKey === "order_history") {
return (
<OrderHistoryTimeline order={order} userRole={userRole} />
);
}
if (blockKey === "extra_data") {
return (
<div className="grid gap-4 md:grid-cols-2">
{order.managerName ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Менеджер</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.managerName}</p>
{order.managerTel ? (
<a href={`tel:${order.managerTel}`} className="text-sm text-[var(--color-accent)] hover:underline">{order.managerTel}</a>
) : null}
</div>
) : null}
<div>
<p className="text-xs text-[var(--color-text-muted)]">Оплата доставки</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">
{order.isPayedShip ? (
<span className="inline-flex items-center gap-1.5">
<span className="rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-semibold text-[var(--color-accent)]"> Оплачено</span>
{order.payedShip ? <span className="text-sm">{Number(order.payedShip).toLocaleString("ru-RU")} </span> : null}
</span>
) : (
<span className="text-sm text-[var(--color-text-muted)]">Не оплачено</span>
)}
</p>
</div>
{order.firstSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">1-е SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.firstSmsSentAt)}</p>
</div>
) : null}
{order.secondSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">2-е SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.secondSmsSentAt)}</p>
</div>
) : null}
{!order.firstSmsSentAt && !order.secondSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">Нет</p>
</div>
) : null}
<div>
<p className="text-xs text-[var(--color-text-muted)]">Ручное согласование выполнено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.manualConfirmationAt ? formatDateTime(order.manualConfirmationAt) : "Нет"}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Платное хранение</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.paidStorageAt ? formatDateTime(order.paidStorageAt) : "Нет"}</p>
</div>
{order.createdFromExchangeAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Создано из обмена</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.createdFromExchangeAt)}</p>
</div>
) : null}
</div>
);
}
return null;
};
return (
<div className="space-y-5">
<Panel className="space-y-5 p-6">
@ -1049,414 +1406,78 @@ export const OrderDetailPanel = ({
</div>
</Panel>
{/* ===== Collapsible + sortable blocks (logistics/admin/manager only) ===== */}
{/* ===== Tabbed blocks (logistics/admin/manager only) ===== */}
{isLogisticsRole && order ? (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={blockOrder} strategy={verticalListSortingStrategy}>
<>
{/* Tab bar */}
<div className="flex gap-2 overflow-x-auto pb-2">
<button
type="button"
onClick={() => handleTabChange("all")}
className={["rounded-xl px-3 py-2 text-sm font-medium whitespace-nowrap transition",
activeTab === "all"
? "bg-[var(--color-accent)] text-white"
: "bg-[var(--color-surface)] text-[var(--color-text-muted)]"
].join(" ")}
>
Все
</button>
{blockOrder.map(blockKey => {
/* manual_confirmation */
if (blockKey === "manual_confirmation" && canManageDelivery) {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<p className="text-sm text-[var(--color-text-muted)]">
{isDeliveryAgreed
? "Дата и время уже зафиксированы."
: "Если клиент согласовал доставку или самовывоз по телефону, сохраните дату и время здесь."}
</p>
{/* Delivery type tabs */}
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<button
type="button"
className={["flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition",
deliveryType === "delivery"
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
: "text-[var(--color-text-muted)] hover:bg-[var(--color-accent-soft)]"
].join(" ")}
onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }}
>
🚚 Доставка
</button>
<button
type="button"
className={["flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition",
deliveryType === "pickup"
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
: "text-[var(--color-text-muted)] hover:bg-[var(--color-accent-soft)]"
].join(" ")}
onClick={() => { setDeliveryType("pickup"); setFormMessage(""); }}
>
🏪 Самовывоз
</button>
</div>
{deliveryType === "pickup" && (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-sm text-[var(--color-text-muted)]">
<p className="font-semibold text-[var(--color-text)]"> Условия хранения</p>
<p className="mt-1">Бесплатное хранение <strong>2 рабочих дня</strong> с даты готовности.</p>
<p>Начиная с 3-го рабочего дня <strong>300 /день</strong> платного хранения.</p>
</div>
)}
{deliveryType === "delivery" && (
<div className="space-y-2">
<label className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-text-muted)]">
Адрес доставки
</label>
<input
type="text"
value={deliveryAddress}
onChange={(e) => setDeliveryAddress(e.target.value)}
placeholder="Введите адрес доставки"
className="w-full rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-3 text-sm !text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none"
/>
</div>
)}
{agreedTypeMatchesTab ? (
<div className="space-y-3">
<div className="rounded-[24px] border border-[rgba(18,128,92,0.35)] bg-[var(--color-accent-soft)] p-4 !text-[var(--color-text)]">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-[var(--color-accent)]">
{deliveryType === "pickup" ? "Самовывоз согласован" : "Доставка согласована"}
</p>
<p className="mt-1 text-lg font-semibold">
{agreedDeliveryLabel || "Дата не указана — нажмите «Изменить дату»"}
</p>
</div>
<Badge tone="accent">Согласовано</Badge>
</div>
</div>
{canEditDelivery ? (
<Button
variant="secondary"
onClick={() => { setIsEditingDate(true); setFormMessage(""); }}
disabled={isSavingDeliveryChoice}
className="text-sm"
>
Изменить дату {deliveryType === "pickup" ? "самовывоза" : "доставки"}
</Button>
) : null}
</div>
) : deliveryType === "delivery" ? (
<CalendarWidget
label="Календарь доставки"
selectedDate={deliveryDate}
onDateChange={(dateKey) => { setDeliveryDate(dateKey); setFormMessage(""); }}
minDateKey={minSelectableDateKey}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
calendarDays={calendarDays}
monthLabel={monthLabel}
canGoBack={canGoBack}
timeOptions={DELIVERY_TIME_OPTIONS}
selectedTime={deliveryTime}
onTimeChange={(option) => { setDeliveryTime(option); setFormMessage(""); }}
layoutClassName="flex flex-col gap-3 md:flex-row md:items-start md:relative md:z-10"
calendarClassName="relative space-y-3 md:min-w-0 md:flex-1 md:pr-4"
timeClassName="grid gap-2 sm:grid-cols-2 md:w-[320px] md:flex-none"
/>
) : (
<CalendarWidget
label="Календарь самовывоза"
selectedDate={pickupDate}
onDateChange={(dateKey) => { setPickupDate(dateKey); setFormMessage(""); }}
minDateKey={minSelectableDateKey}
isCalendarOpen={isCalendarOpen}
setIsCalendarOpen={setIsCalendarOpen}
currentMonth={currentMonth}
setCurrentMonth={setCurrentMonth}
calendarDays={calendarDays}
monthLabel={monthLabel}
canGoBack={canGoBack}
timeOptions={DELIVERY_TIME_OPTIONS}
selectedTime={pickupTimeSlot}
onTimeChange={(option) => { setPickupTimeSlot(option); setFormMessage(""); }}
layoutClassName="space-y-3"
calendarClassName="relative"
timeClassName="grid gap-2 sm:grid-cols-2"
/>
)}
<Button
className="w-full md:w-[180px] md:flex-none md:self-start"
onClick={() => setConfirmAction({ type: 'delivery' })}
disabled={isSavingDeliveryChoice}
>
{isSavingDeliveryChoice ? "Сохраняем..." : "Согласовать"}
</Button>
{formMessage ? (
<p className="text-sm text-[var(--color-text-muted)]">{formMessage}</p>
) : null}
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* driver_assignment */
if (blockKey === "driver_assignment" && deliveryType === "delivery") {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<DriverAssignmentPanel
order={order}
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingDriverAssignment={isSavingDriverAssignment}
selectedDriverId={selectedDriverId}
onDriverSelect={(id) => { setSelectedDriverId(id); setDriverMessage(""); }}
onConfirmDriver={() => setConfirmAction({ type: 'driver' })}
driverMessage={driverMessage}
drivers={drivers}
/>
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* shipment — DriverShipmentPanel */
if (blockKey === "shipment") {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<DriverShipmentPanel
order={order}
onShipmentChange={handleShipmentChange}
onSaveShipment={handleSaveShipment}
isSavingShipment={isSavingShipment}
onResetStatus={() => {
if (onChangeDeliveryStatus) {
onChangeDeliveryStatus({
orderGroupId: order.id,
status: "driver_assigned",
}).then((response) => {
if (!response.success) {
setFormMessage(response.error || "Не удалось сбросить статус");
} else {
setFormMessage("Статус сброшен, отгрузка очищена");
}
});
}
}}
isSavingStatusChange={isSavingStatusChange}
groupByOrder={true}
/>
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* sms_status */
if (blockKey === "sms_status") {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<SmsStatusCard order={order} userRole={userRole} />
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* status_actions */
if (blockKey === "status_actions") {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<StatusActionPanel
order={order}
userRole={userRole}
canManageDelivery={canManageDelivery}
isSavingStatusChange={isSavingStatusChange}
onRefreshOrder={() => {}}
onConfirmStatus={(action) => {
if (action.type === "hint") {
setFormMessage(action.hint);
} else if (action.type === "status") {
setConfirmAction({
type: "status",
status: action.status,
label: action.label,
mismatch: action.mismatch,
deliveryType: action.deliveryType,
});
}
}}
/>
{formMessage && onChangeDeliveryStatus ? (
<p className="text-sm text-[var(--color-warning)]">{formMessage}</p>
) : null}
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* paid_storage */
if (blockKey === "paid_storage" && onChangeDeliveryStatus) {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<PaidStoragePanel
order={order}
onChangeDeliveryStatus={onChangeDeliveryStatus}
isSavingStatusChange={isSavingStatusChange}
setFormMessage={setFormMessage}
/>
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* delivery_link */
if (blockKey === "delivery_link" && order?.deliveryLink) {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<p className="text-sm text-[var(--color-text-muted)]">
Отправьте эту ссылку клиенту, чтобы он мог согласовать доставку или самовывоз самостоятельно.
</p>
<div className="flex flex-wrap items-center gap-2">
<a
href={order.deliveryLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-2xl bg-[var(--color-accent)] px-4 py-2.5 text-sm font-semibold text-white transition hover:opacity-90"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
Открыть страницу согласования
</a>
<Button
variant="secondary"
onClick={() => {
navigator.clipboard?.writeText(order.deliveryLink).then(() => {
setFormMessage("Ссылка скопирована в буфер обмена");
setTimeout(() => setFormMessage(""), 3000);
}).catch(() => {
setFormMessage("Не удалось скопировать ссылку");
});
}}
>
<span className="inline-flex items-center gap-1.5">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
Скопировать ссылку
</span>
</Button>
</div>
{order.invitationAccessCount > 0 ? (
<p className="text-sm text-[var(--color-text-muted)]">
👁 Клиент открывал страницу {order.invitationAccessCount} раз{(order.invitationLastAccessedAt || order.invitationOpenedAt) ? `, последний раз ${new Date(order.invitationLastAccessedAt || order.invitationOpenedAt).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "2-digit" })}` : ""}.
</p>
) : (
<p className="text-sm text-[var(--color-text-muted)]">
Клиент ещё не открывал страницу согласования.
</p>
)}
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* order_history */
if (blockKey === "order_history") {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
<OrderHistoryTimeline order={order} userRole={userRole} />
</CollapsibleBlock>
)}
</SortableBlock>
);
}
/* extra_data */
if (blockKey === "extra_data") {
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners} defaultCollapsed={true}>
<div className="grid gap-4 md:grid-cols-2">
{order.managerName ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Менеджер</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.managerName}</p>
{order.managerTel ? (
<a href={`tel:${order.managerTel}`} className="text-sm text-[var(--color-accent)] hover:underline">{order.managerTel}</a>
) : null}
</div>
) : null}
<div>
<p className="text-xs text-[var(--color-text-muted)]">Оплата доставки</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">
{order.isPayedShip ? (
<span className="inline-flex items-center gap-1.5">
<span className="rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-semibold text-[var(--color-accent)]"> Оплачено</span>
{order.payedShip ? <span className="text-sm">{Number(order.payedShip).toLocaleString("ru-RU")} </span> : null}
</span>
) : (
<span className="text-sm text-[var(--color-text-muted)]">Не оплачено</span>
)}
</p>
</div>
{order.firstSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">1-е SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.firstSmsSentAt)}</p>
</div>
) : null}
{order.secondSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">2-е SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.secondSmsSentAt)}</p>
</div>
) : null}
{!order.firstSmsSentAt && !order.secondSmsSentAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">SMS отправлено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">Нет</p>
</div>
) : null}
<div>
<p className="text-xs text-[var(--color-text-muted)]">Ручное согласование выполнено</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.manualConfirmationAt ? formatDateTime(order.manualConfirmationAt) : "Нет"}</p>
</div>
<div>
<p className="text-xs text-[var(--color-text-muted)]">Платное хранение</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{order.paidStorageAt ? formatDateTime(order.paidStorageAt) : "Нет"}</p>
</div>
{order.createdFromExchangeAt ? (
<div>
<p className="text-xs text-[var(--color-text-muted)]">Создано из обмена</p>
<p className="mt-1 font-medium !text-[var(--color-text)]">{formatDateTime(order.createdFromExchangeAt)}</p>
</div>
) : null}
</div>
</CollapsibleBlock>
)}
</SortableBlock>
);
}
return null;
// Only show tab if the block would be visible (match gating conditions)
if (blockKey === "manual_confirmation" && !canManageDelivery) return null;
if (blockKey === "driver_assignment" && deliveryType !== "delivery") return null;
if (blockKey === "paid_storage" && !onChangeDeliveryStatus) return null;
if (blockKey === "delivery_link" && !order?.deliveryLink) return null;
return (
<button
key={blockKey}
type="button"
onClick={() => handleTabChange(blockKey)}
className={["rounded-xl px-3 py-2 text-sm font-medium whitespace-nowrap transition",
activeTab === blockKey
? "bg-[var(--color-accent)] text-white"
: "bg-[var(--color-surface)] text-[var(--color-text-muted)]"
].join(" ")}
>
{BLOCK_TITLES[blockKey]}
</button>
);
})}
</SortableContext>
</DndContext>
</div>
{/* Block content — "all" mode = DnD + collapsible, single-tab = just the block content */}
{activeTab === "all" ? (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={blockOrder} strategy={verticalListSortingStrategy}>
{blockOrder.map(blockKey => {
const renderWith = (extraProps = {}) => {
if (blockKey === "extra_data") return { ...extraProps, defaultCollapsed: true };
return extraProps;
};
// Check visibility gates
if (blockKey === "manual_confirmation" && !canManageDelivery) return null;
if (blockKey === "driver_assignment" && deliveryType !== "delivery") return null;
if (blockKey === "paid_storage" && !onChangeDeliveryStatus) return null;
if (blockKey === "delivery_link" && !order?.deliveryLink) return null;
return (
<SortableBlock key={blockKey} id={blockKey}>
{({ dragAttributes, dragListeners }) => (
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners} {...renderWith()}>
{renderBlockContent(blockKey)}
</CollapsibleBlock>
)}
</SortableBlock>
);
})}
</SortableContext>
</DndContext>
) : (
<Panel className="space-y-4 p-4">
<div className="font-bold border-b pb-2 mb-2">{BLOCK_TITLES[activeTab]}</div>
{renderBlockContent(activeTab)}
</Panel>
)}
</>
) : null}
{/* ===== Driver-only blocks (not sortable/collapsible) ===== */}