feat(orders): tabbed navigation for logistics blocks (Все + per-block tabs)
This commit is contained in:
parent
811e2a1254
commit
1f8a8ea2c4
|
|
@ -691,6 +691,16 @@ export const OrderDetailPanel = ({
|
||||||
} catch {}
|
} catch {}
|
||||||
return DEFAULT_BLOCK_ORDER;
|
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(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
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 (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<Panel className="space-y-5 p-6">
|
<Panel className="space-y-5 p-6">
|
||||||
|
|
@ -1049,414 +1406,78 @@ export const OrderDetailPanel = ({
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
{/* ===== Collapsible + sortable blocks (logistics/admin/manager only) ===== */}
|
{/* ===== Tabbed blocks (logistics/admin/manager only) ===== */}
|
||||||
{isLogisticsRole && order ? (
|
{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 => {
|
{blockOrder.map(blockKey => {
|
||||||
/* manual_confirmation */
|
// Only show tab if the block would be visible (match gating conditions)
|
||||||
if (blockKey === "manual_confirmation" && canManageDelivery) {
|
if (blockKey === "manual_confirmation" && !canManageDelivery) return null;
|
||||||
return (
|
if (blockKey === "driver_assignment" && deliveryType !== "delivery") return null;
|
||||||
<SortableBlock key={blockKey} id={blockKey}>
|
if (blockKey === "paid_storage" && !onChangeDeliveryStatus) return null;
|
||||||
{({ dragAttributes, dragListeners }) => (
|
if (blockKey === "delivery_link" && !order?.deliveryLink) return null;
|
||||||
<CollapsibleBlock blockKey={blockKey} title={BLOCK_TITLES[blockKey]} dragAttributes={dragAttributes} dragListeners={dragListeners}>
|
return (
|
||||||
<p className="text-sm text-[var(--color-text-muted)]">
|
<button
|
||||||
{isDeliveryAgreed
|
key={blockKey}
|
||||||
? "Дата и время уже зафиксированы."
|
type="button"
|
||||||
: "Если клиент согласовал доставку или самовывоз по телефону, сохраните дату и время здесь."}
|
onClick={() => handleTabChange(blockKey)}
|
||||||
</p>
|
className={["rounded-xl px-3 py-2 text-sm font-medium whitespace-nowrap transition",
|
||||||
{/* Delivery type tabs */}
|
activeTab === blockKey
|
||||||
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
|
? "bg-[var(--color-accent)] text-white"
|
||||||
<button
|
: "bg-[var(--color-surface)] text-[var(--color-text-muted)]"
|
||||||
type="button"
|
].join(" ")}
|
||||||
className={["flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition",
|
>
|
||||||
deliveryType === "delivery"
|
{BLOCK_TITLES[blockKey]}
|
||||||
? "bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
|
</button>
|
||||||
: "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;
|
|
||||||
})}
|
})}
|
||||||
</SortableContext>
|
</div>
|
||||||
</DndContext>
|
|
||||||
|
{/* 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}
|
) : null}
|
||||||
|
|
||||||
{/* ===== Driver-only blocks (not sortable/collapsible) ===== */}
|
{/* ===== Driver-only blocks (not sortable/collapsible) ===== */}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue