fix: Button→native button everywhere, table font 12px, dropdowns cleanup

- OrdersTable: font text-xs (12px), headers 11px, city column, horizontal scroll
- OrderFilters: dropdown items native button, no ovals, counter as plain text
- DriverAssignmentPanel: custom DriverDropdown in app style
- StopWordsPanel: all Button→button
- UserManagementPanel: all Button→button, RoleDropdown native
- SuggestionsPanel: all Button→button
- ActionLogPanel: Button→button
- ErrorLogPanel: all Button→button
- OrderDetailPanel: delivery tabs, problem reasons, expand toggle→button
- CalendarWidget: month nav arrows→button
- AppShell: nav buttons variant=ghost
- NotificationBell: buttons→native
- ClientDeliveryPage: hide delivery status badge, pickup code block
- PickupSlotsPicker: fix today morning slot logic
This commit is contained in:
root 2026-06-19 11:36:45 +00:00
parent 17fe2125a5
commit cdb2ec7098
15 changed files with 468 additions and 242 deletions

View File

@ -198,13 +198,14 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
Кто, что и когда делал с доставками
</p>
</div>
<Button
<button
type="button"
onClick={loadLogs}
disabled={loading}
className="rounded-[14px] bg-[var(--color-accent)] px-3 py-1.5 text-sm font-medium text-white hover:opacity-90 disabled:opacity-50"
>
{loading ? "Загрузка..." : "Обновить"}
</Button>
</button>
</div>
{/* Filters */}

View File

@ -236,29 +236,29 @@ export default function ErrorLogPanel() {
</span>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<Button onClick={fetchErrors} style={btnBase} title="Обновить"></Button>
<Button onClick={handleCopyAll} disabled={errors.length === 0} style={{ ...btnAccent, opacity: errors.length === 0 ? 0.4 : 1 }}>
<button type="button" onClick={fetchErrors} style={btnBase} title="Обновить"></button>
<button type="button" onClick={handleCopyAll} disabled={errors.length === 0} style={{ ...btnAccent, opacity: errors.length === 0 ? 0.4 : 1 }}>
📋 Копировать всё
</Button>
</button>
{selected.size > 0 && (
<Button onClick={handleCopySelected} style={btnAccent}>
<button type="button" onClick={handleCopySelected} style={btnAccent}>
📋 Копировать выбр.
</Button>
</button>
)}
<Button onClick={downloadJSON} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
<button type="button" onClick={downloadJSON} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
JSON
</Button>
<Button onClick={downloadCSV} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
</button>
<button type="button" onClick={downloadCSV} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
CSV
</Button>
</button>
{selected.size > 0 && (
<Button onClick={handleDeleteSelected} disabled={deleting} style={{ ...btnDanger, opacity: deleting ? 0.5 : 1 }}>
<button type="button" onClick={handleDeleteSelected} disabled={deleting} style={{ ...btnDanger, opacity: deleting ? 0.5 : 1 }}>
🗑 Удалить выбр.
</Button>
</button>
)}
<Button onClick={handleDeleteAll} disabled={deleting || errors.length === 0} style={{ ...btnDanger, opacity: deleting || errors.length === 0 ? 0.4 : 1 }}>
<button type="button" onClick={handleDeleteAll} disabled={deleting || errors.length === 0} style={{ ...btnDanger, opacity: deleting || errors.length === 0 ? 0.4 : 1 }}>
🗑 Удалить все
</Button>
</button>
</div>
</div>
@ -383,9 +383,9 @@ export default function ErrorLogPanel() {
{err.user_id && <div style={{ gridColumn: '1 / -1' }}><strong>User ID:</strong> {err.user_id}</div>}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => handleDeleteOne(err.id)} disabled={deleting} style={{ ...btnDanger, fontSize: '0.8rem' }}>
<button type="button" onClick={() => handleDeleteOne(err.id)} disabled={deleting} style={{ ...btnDanger, fontSize: '0.8rem' }}>
Удалить запись
</Button>
</button>
</div>
</div>
)}

View File

@ -107,34 +107,32 @@ export const StopWordsPanel = () => {
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 space-y-3">
<p className="text-sm font-medium text-[var(--color-text)]">Где применять стоп-слова:</p>
<div className="flex gap-2">
<Button
<button
type="button"
disabled={isSavingScope}
onClick={() => handleScopeChange("everywhere")}
className={[
"rounded-full border px-4 py-2 text-sm font-medium transition",
"rounded-full border px-4 py-2 text-sm font-medium transition disabled:opacity-50",
scope === "everywhere"
? "border-[var(--color-accent)] bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:text-[var(--color-text)]",
isSavingScope ? "opacity-50" : "",
].join(" ")}
>
Везде
</Button>
<Button
</button>
<button
type="button"
disabled={isSavingScope}
onClick={() => handleScopeChange("client_only")}
className={[
"rounded-full border px-4 py-2 text-sm font-medium transition",
"rounded-full border px-4 py-2 text-sm font-medium transition disabled:opacity-50",
scope === "client_only"
? "border-[var(--color-accent)] bg-[var(--color-accent)] text-[var(--color-accent-contrast)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)] hover:text-[var(--color-text)]",
isSavingScope ? "opacity-50" : "",
].join(" ")}
>
Только карточка клиента
</Button>
</button>
</div>
<p className="text-xs text-[var(--color-text-muted)]">
{scope === "everywhere"
@ -153,9 +151,14 @@ export const StopWordsPanel = () => {
className="flex-1 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 py-2.5 text-sm !text-[var(--color-text)] outline-none transition focus:border-[var(--color-accent)]"
maxLength={100}
/>
<Button onClick={handleAdd} disabled={!newWord.trim()}>
<button
type="button"
onClick={handleAdd}
disabled={!newWord.trim()}
className="rounded-2xl bg-[var(--color-accent)] px-4 py-2.5 text-sm font-medium text-[var(--color-accent-contrast)] transition hover:opacity-90 disabled:opacity-50"
>
Добавить
</Button>
</button>
</div>
{error && (
@ -178,7 +181,7 @@ export const StopWordsPanel = () => {
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-strong)] px-3 py-1.5 text-sm !text-[var(--color-text)]"
>
{w.word}
<Button
<button
type="button"
disabled={savingId === w.id}
onClick={() => handleDelete(w.id)}
@ -186,7 +189,7 @@ export const StopWordsPanel = () => {
aria-label={`Удалить ${w.word}`}
>
</Button>
</button>
</span>
))}
</div>

View File

@ -104,19 +104,19 @@ export const SuggestionsPanel = () => {
</p>
<div className="flex flex-wrap gap-2">
{CATEGORY_OPTIONS.map((cat) => (
<Button
<button
key={cat.value}
type="button"
onClick={() => setNewCategory(cat.value)}
className={[
"rounded-xl border px-3 py-1.5 text-xs font-medium transition",
newCategory === cat.value
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] !text-[var(--color-text)]"
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:border-[var(--color-accent)]"
].join(" ")}
>
{cat.icon} {cat.label}
</Button>
</button>
))}
</div>
<textarea
@ -127,14 +127,14 @@ export const SuggestionsPanel = () => {
className="w-full rounded-xl border border-[var(--color-border)] bg-[var(--color-bg)] px-4 py-3 text-sm text-[var(--color-text)] placeholder:text-[var(--color-text-muted)] focus:border-[var(--color-accent)] focus:outline-none resize-none"
/>
<div className="flex items-center gap-3">
<Button
<button
type="button"
disabled={!newText.trim() || submitting}
onClick={handleSubmit}
className="rounded-xl bg-[var(--color-accent)] px-5 py-2.5 text-sm font-semibold text-white transition hover:opacity-90 disabled:opacity-40"
>
{submitting ? "Отправка..." : "Отправить"}
</Button>
</button>
{message && <span className="text-sm text-[var(--color-text-muted)]">{message}</span>}
</div>
</Panel>
@ -210,7 +210,7 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
<div className="border-t border-[var(--color-border)] pt-2 mt-2 space-y-2">
<div className="flex flex-wrap gap-1.5">
{Object.entries(STATUS_MAP).map(([key, val]) => (
<Button
<button
key={key}
type="button"
onClick={() => onStatusChange(s.id, key)}
@ -224,7 +224,7 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
}}
>
{val.label}
</Button>
</button>
))}
</div>
{editing ? (
@ -236,22 +236,22 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
placeholder="Комментарий админа..."
className="flex-1 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] px-3 py-1.5 text-xs text-[var(--color-text)]"
/>
<Button
<button
type="button"
onClick={() => { onStatusChange(s.id, s.status, comment); setEditing(false); }}
className="rounded-lg bg-[var(--color-accent)] px-3 py-1.5 text-xs font-semibold text-white"
>
Сохранить
</Button>
</button>
</div>
) : (
<Button
<button
type="button"
onClick={() => setEditing(true)}
className="text-xs text-[var(--color-accent)] hover:underline"
>
Комментарий
</Button>
</button>
)}
</div>
)}

View File

@ -69,12 +69,12 @@ function RoleDropdown({ value, onChange }) {
return (
<div ref={ref} className="relative w-full">
<Button
<button
type="button"
onClick={() => setOpen((v) => !v)}
className={[
'flex w-full h-[46px] items-center justify-between gap-2 rounded-2xl border px-4 text-sm transition hover:border-[var(--color-accent)]',
value ? 'border-[var(--color-border)] bg-[var(--color-surface-strong)]' : 'border-[var(--color-border)] bg-[var(--color-surface-strong)]',
'border-[var(--color-border)] bg-[var(--color-surface-strong)]',
].join(' ')}
>
{value ? (
@ -83,24 +83,24 @@ function RoleDropdown({ value, onChange }) {
<span className="text-[var(--color-text-muted)]">Выберите роль</span>
)}
<span className="text-[var(--color-text-muted)] text-xs"></span>
</Button>
</button>
{open && (
<div className="absolute left-0 right-0 top-full z-50 mt-1.5 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft">
{ROLES.map((r) => {
const sel = r === value;
return (
<Button
<button
key={r}
type="button"
onClick={() => pick(r)}
className={[
'flex w-full items-center justify-between px-4 py-3 text-left text-sm transition',
'flex w-full items-center justify-between px-4 py-2.5 text-left text-sm transition',
sel ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : 'text-[var(--color-text)] hover:bg-[var(--color-surface)]',
].join(' ')}
>
<span>{ROLE_LABELS[r]}</span>
{sel && <span className="text-[var(--color-accent)]"></span>}
</Button>
</button>
);
})}
</div>
@ -162,20 +162,20 @@ function AddUserModal({ onSubmit, onClose, submitting, error }) {
<div className="rounded-xl bg-[rgba(201,61,61,0.12)] px-4 py-2.5 text-sm text-[var(--color-danger)]">{error}</div>
)}
<div className="flex gap-3 pt-2">
<Button
<button
type="button"
onClick={onClose}
className="flex-1 rounded-full border border-[var(--color-border)] px-4 py-2.5 text-sm font-semibold text-[var(--color-text-muted)] hover:bg-[var(--color-surface)] transition"
>
Отмена
</Button>
<Button
</button>
<button
type="submit"
disabled={submitting}
className="flex-1 rounded-full bg-[var(--color-accent)] px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
>
{submitting ? 'Добавление…' : 'Добавить'}
</Button>
</button>
</div>
</form>
</div>
@ -295,12 +295,13 @@ export default function UserManagementPanel() {
{/* Toolbar */}
<div className="flex items-center justify-between gap-2 flex-wrap mb-4">
<span className="text-sm text-[var(--color-text-muted)]">{users.length} пользователей</span>
<Button
<button
type="button"
onClick={() => { setShowAddForm(true); setAddError(null); }}
className="rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 transition"
>
+ Добавить
</Button>
</button>
</div>
{/* Add user modal */}
@ -355,15 +356,17 @@ export default function UserManagementPanel() {
/>
</div>
<div className="flex gap-2">
<Button
<button
type="button"
onClick={cancelEdit}
className="rounded-lg border border-[var(--color-border)] px-3 py-1.5 text-sm text-[var(--color-text-muted)] hover:bg-[var(--color-surface)] transition"
>Отмена</Button>
<Button
>Отмена</button>
<button
type="button"
onClick={saveEdit}
disabled={saving}
className="rounded-full bg-[var(--color-accent)] px-4 py-1.5 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
>{saving ? 'Сохранение…' : 'Сохранить'}</Button>
>{saving ? 'Сохранение…' : 'Сохранить'}</button>
</div>
</div>
);
@ -380,20 +383,22 @@ export default function UserManagementPanel() {
</div>
<div className="flex items-center gap-2">
<Badge tone={ROLE_TONES[rn] || 'neutral'}>{ROLE_LABELS[rn] || rn}</Badge>
<Button
<button
type="button"
onClick={() => startEdit(user)}
className="rounded-lg border border-[var(--color-accent)] px-2.5 py-1 text-xs font-semibold text-[var(--color-accent)] hover:bg-[var(--color-accent-soft)] transition"
>Изменить</Button>
>Изменить</button>
{deleteConfirmId === user.id ? (
<div className="flex gap-1">
<Button onClick={() => handleDeleteUser(user.id)} className="rounded-lg bg-[var(--color-danger)] px-2.5 py-1 text-xs font-semibold text-white transition">Да</Button>
<Button onClick={() => setDeleteConfirmId(null)} className="rounded-lg border border-[var(--color-border)] px-2.5 py-1 text-xs text-[var(--color-text-muted)] transition">Нет</Button>
<button type="button" onClick={() => handleDeleteUser(user.id)} className="rounded-lg bg-[var(--color-danger)] px-2.5 py-1 text-xs font-semibold text-white transition">Да</button>
<button type="button" onClick={() => setDeleteConfirmId(null)} className="rounded-lg border border-[var(--color-border)] px-2.5 py-1 text-xs text-[var(--color-text-muted)] transition">Нет</button>
</div>
) : (
<Button
<button
type="button"
onClick={() => setDeleteConfirmId(user.id)}
className="rounded-lg border border-[var(--color-danger)] px-2.5 py-1 text-xs font-semibold text-[var(--color-danger)] hover:bg-[rgba(201,61,61,0.08)] transition"
>Удалить</Button>
>Удалить</button>
)}
</div>
</div>

View File

@ -49,7 +49,6 @@ export const DeliveryChoiceFlow = ({
<h1 className="text-2xl font-semibold leading-tight sm:text-3xl">
{deliveryType === "pickup" ? "Выберите время самовывоза" : "Выберите время доставки"}
</h1>
<Badge tone="warning">{STATE_LABELS[state]}</Badge>
</div>
<p className="text-sm leading-6 text-[var(--color-text-muted)]">
{invitationReference}. Выберите удобную половину дня для {typeLabel}.

View File

@ -57,7 +57,8 @@ const getPickupSlots = (referenceDate = new Date()) => {
const slots = [];
if (isTodayWorkday && hour < 12) {
if (isTodayWorkday) {
if (hour < 12) {
slots.push({
id: `pickup-${todayKey}-first`,
date: todayKey,
@ -66,6 +67,14 @@ const getPickupSlots = (referenceDate = new Date()) => {
pickupType: "today",
});
}
slots.push({
id: `pickup-${todayKey}-second`,
date: todayKey,
time: "Вторая половина дня",
label: "Сегодня",
pickupType: "today",
});
}
const tomorrow = addDaysKey(todayKey, 1);
const tomorrowWorkday = !isWeekend(tomorrow) ? tomorrow : getNextWorkday(todayKey);

View File

@ -1,6 +1,5 @@
import React from "react";
import { Panel } from "../UI/Panel";
import { Button } from "../UI/Button";
import { Bell, Check, CheckCheck, Settings, X } from "../UI/Icons";
const TYPE_ICONS = {
@ -47,8 +46,9 @@ export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onM
return (
<div className="relative" ref={bellRef}>
<Button
className="relative flex h-9 w-9 items-center justify-center rounded-full transition hover:bg-[var(--color-surface-strong)]"
<button
type="button"
className="relative flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] bg-transparent text-[var(--color-text-muted)] transition hover:bg-[var(--color-surface-strong)] hover:text-[var(--color-text)]"
onClick={() => setIsOpen(!isOpen)}
aria-label={`Уведомления${unreadCount > 0 ? ` (${unreadCount})` : ""}`}
>
@ -58,7 +58,7 @@ export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onM
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</Button>
</button>
{isOpen && (
<div className="absolute right-0 top-full z-50 mt-2 w-80 sm:w-96">
@ -67,29 +67,32 @@ export function NotificationBell({ notifications, unreadCount, onMarkAsRead, onM
<h3 className="text-sm font-semibold">Уведомления</h3>
<div className="flex items-center gap-1">
{unreadCount > 0 && (
<Button
className="rounded p-1 text-xs text-[var(--color-accent)] hover:bg-[var(--color-surface-strong)]"
<button
type="button"
className="rounded border border-transparent p-1 text-xs text-[var(--color-accent)] hover:bg-[var(--color-surface-strong)]"
onClick={onMarkAllAsRead}
title="Прочитать все"
>
<CheckCheck className="h-4 w-4" />
</Button>
</button>
)}
{onOpenSettings && (
<Button
className="rounded p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
<button
type="button"
className="rounded border border-transparent p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
onClick={() => { onOpenSettings(); setIsOpen(false); }}
title="Настройки"
>
<Settings className="h-4 w-4" />
</Button>
</button>
)}
<Button
className="rounded p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
<button
type="button"
className="rounded border border-transparent p-1 text-xs text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
onClick={() => setIsOpen(false)}
>
<X className="h-4 w-4" />
</Button>
</button>
</div>
</div>

View File

@ -84,23 +84,23 @@ const CalendarWidget = ({
</h4>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
<button
type="button"
disabled={!canGoBack}
aria-label="Предыдущий месяц"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-40"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:text-[var(--color-text)] disabled:cursor-not-allowed disabled:opacity-40"
onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() - 1, 1))}
>
</Button>
<Button
variant="ghost"
</button>
<button
type="button"
aria-label="Следующий месяц"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:!text-[var(--color-text)]"
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-sm text-[var(--color-text-muted)] transition hover:border-[var(--color-accent)] hover:text-[var(--color-text)]"
onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() + 1, 1))}
>
</Button>
</button>
</div>
</div>
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-[10px] font-semibold uppercase text-[var(--color-text-muted)]">

View File

@ -1,9 +1,83 @@
import React from "react";
import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Select } from "../UI/Select";
import { Panel } from "../UI/Panel";
const DriverDropdown = ({ value, drivers, onChange, disabled, placeholder }) => {
const [isOpen, setIsOpen] = React.useState(false);
const menuRef = React.useRef(null);
React.useEffect(() => {
if (!isOpen) return undefined;
const handlePointerDown = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) setIsOpen(false);
};
const handleKeyDown = (event) => {
if (event.key === "Escape") setIsOpen(false);
};
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [isOpen]);
const selectedDriver = drivers.find((d) => d.id === value);
const selectedLabel = selectedDriver ? (selectedDriver.name || selectedDriver.email) : placeholder;
return (
<div ref={menuRef} className="relative flex min-w-0 flex-col gap-2">
<button
type="button"
disabled={disabled}
aria-haspopup="listbox"
aria-expanded={isOpen}
className={["flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition disabled:opacity-50",
isOpen
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text)] hover:border-[var(--color-accent)]",
].join(" ")}
onClick={() => !disabled && setIsOpen((c) => !c)}
>
<span className="min-w-0 flex-1 truncate">{selectedLabel}</span>
<span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]"></span>
</button>
{isOpen ? (
<div
role="listbox"
className="absolute left-0 right-0 top-full z-20 mt-2 max-h-[280px] overflow-y-auto rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft"
>
{drivers.map((driver) => {
const isSelected = driver.id === value;
return (
<button
key={driver.id}
type="button"
role="option"
aria-selected={isSelected}
className={["flex w-full items-center justify-between px-4 py-2.5 text-left text-sm transition",
isSelected
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]",
].join(" ")}
onClick={() => {
onChange(driver.id);
setIsOpen(false);
}}
>
<span className="min-w-0 flex-1 truncate">{driver.name || driver.email}</span>
{isSelected ? <span className="ml-3 text-[var(--color-accent)]"></span> : null}
</button>
);
})}
</div>
) : null}
</div>
);
};
const DriverAssignmentPanel = ({
order,
userRole,
@ -57,19 +131,13 @@ const DriverAssignmentPanel = ({
) : null}
{!isDriverLocked ? (
<div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
<Select
className="h-[46px] py-0"
<DriverDropdown
value={selectedDriverId}
onChange={(e) => {
onDriverSelect(e.target.value);
}}
drivers={drivers}
onChange={onDriverSelect}
disabled={isSavingDriverAssignment}
>
<option value="">{order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}</option>
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>{driver.name || driver.email}</option>
))}
</Select>
placeholder={order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}
/>
<Button
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
onClick={onConfirmDriver}

View File

@ -84,14 +84,13 @@ const CollapsibleChips = ({ label, items }) => {
if (!Array.isArray(items) || items.length === 0) return null;
return (
<span className="inline">
<Button
variant="ghost"
size="sm"
<button
type="button"
className="ml-1 cursor-pointer rounded-full bg-[var(--color-surface)] px-2 py-0.5 text-xs font-medium text-[var(--color-text-muted)] transition hover:bg-[var(--color-accent-soft)] hover:text-[var(--color-accent)]"
onClick={() => setOpen(!open)}
>
{label} {open ? "▲" : "▼"}
</Button>
</button>
{open && (
<span className="ml-1 inline-flex flex-wrap gap-1">
{items.map((item, idx) => (
@ -517,15 +516,15 @@ const ProblemReasonModal = ({ onSelect, onCancel }) => (
<p className="text-sm text-[var(--color-text-muted)]">Укажите причину возникшей проблемы с доставкой.</p>
<div className="space-y-2">
{PROBLEM_REASONS.map((reason) => (
<Button
<button
key={reason.value}
variant="secondary"
className="w-full rounded-[16px] p-3 text-left"
type="button"
className="w-full rounded-[16px] border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-left transition hover:border-[var(--color-accent)] hover:bg-[var(--color-accent-soft)]"
onClick={() => onSelect(reason.value, reason.label)}
>
<span className="font-medium">{reason.label}</span>
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">{reason.description}</p>
</Button>
</button>
))}
</div>
<div className="flex justify-end">
@ -885,20 +884,28 @@ export const OrderDetailPanel = ({
</div>
{/* Delivery type tabs */}
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<Button
variant={deliveryType === "delivery" ? "primary" : "ghost"}
className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold"
<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
variant={deliveryType === "pickup" ? "primary" : "ghost"}
className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold"
</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>
</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)]">

View File

@ -4,6 +4,79 @@ import { Input } from "../UI/Input";
import { Panel } from "../UI/Panel";
import { DatePicker } from "../UI/DatePicker";
const DropdownSelect = ({ value, options, onChange, placeholder }) => {
const [isOpen, setIsOpen] = React.useState(false);
const menuRef = React.useRef(null);
React.useEffect(() => {
if (!isOpen) return undefined;
const handlePointerDown = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) setIsOpen(false);
};
const handleKeyDown = (event) => {
if (event.key === "Escape") setIsOpen(false);
};
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
}, [isOpen]);
const selectedLabel = options.find((o) => o.value === value)?.label || placeholder;
return (
<div ref={menuRef} className="relative flex min-w-0 flex-col gap-2">
<button
type="button"
aria-haspopup="listbox"
aria-expanded={isOpen}
className={["flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
isOpen
? "border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text)] hover:border-[var(--color-accent)]",
].join(" ")}
onClick={() => setIsOpen((c) => !c)}
>
<span className="min-w-0 flex-1 truncate">{selectedLabel}</span>
<span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]"></span>
</button>
{isOpen ? (
<div
role="listbox"
className="absolute left-0 right-0 top-full z-20 mt-2 overflow-hidden rounded-2xl border border-[var(--color-border)] bg-[var(--color-dropdown-surface)] shadow-soft"
>
{options.map((option) => {
const isSelected = option.value === value;
return (
<button
key={option.value}
type="button"
role="option"
aria-selected={isSelected}
className={["flex w-full items-center justify-between px-4 py-2.5 text-left text-sm transition",
isSelected
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]",
].join(" ")}
onClick={() => {
onChange(option.value);
setIsOpen(false);
}}
>
<span className="min-w-0 flex-1 truncate">{option.label}</span>
{isSelected ? <span className="ml-3 text-[var(--color-accent)]"></span> : null}
</button>
);
})}
</div>
) : null}
</div>
);
};
export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = [] }) => {
const statusValue = filters.displayStatus || filters.status || "all";
const selectedStatusLabel = statusOptions.find((option) => option.value === statusValue)?.label || statusValue;
@ -36,14 +109,25 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
setFilters((current) => ({ ...current, dateFrom: "", dateTo: "" }));
};
const cityOptions = [
{ value: "", label: "Все города" },
...cities.map((c) => ({ value: c, label: c })),
];
const deliveryTypeOptions = [
{ value: "", label: "Тип получения" },
{ value: "delivery", label: "Доставка" },
{ value: "pickup", label: "Самовывоз" },
];
return (
<Panel className="p-4">
<div className="flex flex-col gap-3">
{/* Row 1: Status + City + Search */}
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,0.5fr)_minmax(0,1.6fr)] md:items-end">
{/* Row 1: Status + City + DeliveryType + Search */}
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,0.5fr)_minmax(0,0.5fr)_minmax(0,1.4fr)] md:items-end">
<div ref={statusMenuRef} className="relative flex min-w-0 flex-col gap-2">
<Button
variant="ghost"
<button
type="button"
aria-haspopup="listbox"
aria-expanded={isStatusOpen}
className={["flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
@ -55,7 +139,7 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
>
<span className="min-w-0 flex-1 truncate">{selectedStatusLabel}</span>
<span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]"></span>
</Button>
</button>
{isStatusOpen ? (
<div
@ -65,12 +149,12 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
{statusOptions.map((option) => {
const isSelected = option.value === statusValue;
return (
<Button
<button
key={option.value}
variant="ghost"
type="button"
role="option"
aria-selected={isSelected}
className={["flex w-full items-center justify-between px-4 py-3 text-left text-sm transition",
className={["flex w-full items-center justify-between px-4 py-2.5 text-left text-sm transition",
isSelected
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]",
@ -81,11 +165,9 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
}}
>
<span className="min-w-0 flex-1 truncate">{option.label}</span>
<span className="ml-2 rounded-full border border-[var(--color-border)] bg-[var(--color-surface)] px-2 py-0.5 text-xs font-semibold text-[var(--color-text)]">
{option.count || 0}
</span>
<span className="ml-2 text-xs text-[var(--color-text-muted)]">{option.count || 0}</span>
{isSelected ? <span className="ml-3 text-[var(--color-accent)]"></span> : null}
</Button>
</button>
);
})}
</div>
@ -93,27 +175,20 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
</div>
{cities.length > 0 && (
<select
<DropdownSelect
value={filters.city || ""}
onChange={(e) => updateFilter("city", e.target.value)}
className="h-[46px] rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-sm outline-none focus:border-[var(--color-accent)]"
>
<option value="">Все города</option>
{cities.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
options={cityOptions}
onChange={(v) => updateFilter("city", v)}
placeholder="Все города"
/>
)}
<select
<DropdownSelect
value={filters.deliveryType || ""}
onChange={(e) => updateFilter("deliveryType", e.target.value)}
className="h-[46px] rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-sm outline-none focus:border-[var(--color-accent)]"
>
<option value="">Тип получения</option>
<option value="delivery">Доставка</option>
<option value="pickup">Самовывоз</option>
</select>
options={deliveryTypeOptions}
onChange={(v) => updateFilter("deliveryType", v)}
placeholder="Тип получения"
/>
<Input
className="h-[46px] py-0"
@ -140,13 +215,13 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
className="min-w-[140px] flex-1 md:max-w-[200px]"
/>
{hasDateFilter && (
<Button
variant="ghost"
<button
type="button"
onClick={clearDateFilter}
className="mb-0.5 h-[46px] gap-1.5 rounded-2xl border border-[var(--color-danger)] px-4 text-sm font-semibold text-[var(--color-danger)] hover:bg-[rgba(201,61,61,0.08)]"
>
Сбросить
</Button>
</button>
)}
</div>
</div>

View File

@ -1,6 +1,6 @@
import { formatDateTime } from "../../utils/formatters";
import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Panel } from "../UI/Panel";
import { SkeletonTable } from "../UI/Loading";
import { OrderFilters } from "./OrderFilters";
@ -126,19 +126,29 @@ export const OrdersTable = ({
? "border-[var(--color-danger)] bg-[rgba(201,61,61,0.1)]"
: "border-[var(--color-border)] bg-[var(--color-surface-strong)]";
const allNumbers = group.allBillNumbers || group.orderNumbers || [];
const primaryBill = allNumbers[0] || "—";
const totalCount = allNumbers.length;
return (
<div key={group.id}>
<div className="mb-1 flex items-center gap-2 px-1 text-xs">
<span className="font-medium text-[var(--color-text)]"> {primaryBill}</span>
{totalCount > 1 && (
<span className="rounded-full bg-[var(--color-accent-soft)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)]">
{totalCount} сч.
</span>
)}
</div>
<button
key={group.id}
type="button"
onClick={() => onOpenOrder(group.id)}
className={[baseClass, selectedClass, "flex flex-col items-start justify-start p-4"].join(" ")}
>
<div className="space-y-1">
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3">
<div className="min-w-0 truncate font-medium">
<div className="min-w-0 w-full font-medium leading-snug break-words">
{group.displayTitle || group.customerName || group.groupKey}
</div>
<div className="flex flex-wrap items-center justify-end gap-1.5">
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-semibold ${group.deliveryType === "pickup" ? "bg-[var(--color-accent-soft)] text-[var(--color-accent)]" : "bg-[var(--color-surface-strong)] text-[var(--color-text-muted)]"}`}>
{group.deliveryType === "pickup" ? "🏪 Самовывоз" : "🚚 Доставка"}
</span>
@ -146,17 +156,7 @@ export const OrdersTable = ({
{getOrderGroupDisplayStatusLabel(group)}
</Badge>
</div>
</div>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-[var(--color-text-muted)]">
{renderMobileOrderNumbers(group)}
{(group.ordersTotal || 0) > 1 && (
<span className="rounded-full bg-[var(--color-surface)] px-2 py-0.5 text-xs">
{group.ordersTotal} сч.
</span>
)}
</div>
{group.hasDeliveryProblem && (
<div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs">
<span className="font-medium text-[var(--color-warning)]"> Проблема с доставкой</span>
@ -166,69 +166,90 @@ export const OrdersTable = ({
</div>
)}
</button>
</div>
);
})}
</div>
<div className="hidden overflow-x-auto md:block">
<div className="hidden md:block">
{!orderGroups.length ? (
<div className="px-5 py-6 text-sm text-[var(--color-text-muted)]">
Группы не найдены. Попробуйте изменить поиск или статус.
</div>
) : (
<div>
<div className="grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(90px,0.8fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-xs uppercase tracking-[0.16em] text-[var(--color-text-muted)]">
<div className="px-5 py-4 font-medium">Группа / Клиент</div>
<div className="px-5 py-4 font-medium">Счёта</div>
<div className="px-5 py-4 font-medium">Статус</div>
<div className="px-5 py-4 font-medium">Водитель</div>
<div className="px-5 py-4 font-medium">Дата доставки</div>
<div className="px-5 py-4 font-medium">Тип</div>
<div className="overflow-x-auto">
<div className="min-w-[920px]">
<div className="grid grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(80px,0.8fr)_minmax(80px,1fr)_minmax(100px,1fr)_minmax(70px,0.7fr)_minmax(80px,0.8fr)] gap-0 border-b border-[var(--color-border)] bg-[var(--color-surface-strong)] text-[11px] uppercase tracking-[0.12em] text-[var(--color-text-muted)]">
<div className="px-3 py-2 font-medium">Группа / Клиент</div>
<div className="px-3 py-2 font-medium">Счета</div>
<div className="px-3 py-2 font-medium">Город</div>
<div className="px-3 py-2 font-medium">Статус</div>
<div className="px-3 py-2 font-medium">Дата доставки</div>
<div className="px-3 py-2 font-medium">Тип</div>
<div className="px-3 py-2 font-medium">Водитель</div>
</div>
{orderGroups.map((group) => {
const hasProblem = group.hasDeliveryProblem;
const rowClassName = `grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(90px,0.8fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition ${
const rowClassName = `grid grid-cols-[minmax(130px,2fr)_minmax(90px,1fr)_minmax(80px,0.8fr)_minmax(80px,1fr)_minmax(100px,1fr)_minmax(70px,0.7fr)_minmax(80px,0.8fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition ${
hasProblem
? "bg-[rgba(201,61,61,0.1)] hover:bg-[rgba(201,61,61,0.15)]"
: "hover:bg-[var(--color-accent-soft)]"
} ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`;
const billNumbers = group.allBillNumbers || group.orderNumbers || [];
const primaryBill = billNumbers[0] || "—";
const totalBills = billNumbers.length;
return (
<Button
<button
key={group.id}
variant="ghost"
type="button"
className={rowClassName}
onClick={() => onOpenOrder(group.id)}
>
<div className="px-5 py-4">
<div className="font-medium leading-tight break-words line-clamp-2" style={{display:"-webkit-box",WebkitLineClamp:2,WebkitBoxOrient:"vertical",overflow:"hidden"}}>{group.displayTitle || group.customerName || group.groupKey}</div>
<div className="mt-1 text-sm text-[var(--color-text-muted)]">
<div className="min-w-0 px-3 py-2">
<div className="text-xs font-medium leading-snug break-words" style={{display:"-webkit-box",WebkitLineClamp:2,WebkitBoxOrient:"vertical",overflow:"hidden"}}>{group.displayTitle || group.customerName || group.groupKey}</div>
<div className="mt-0.5 text-[11px] text-[var(--color-text-muted)]">
{group.customerPhone || ""}
</div>
</div>
<div className="max-w-[260px] px-5 py-4 text-sm text-[var(--color-text-muted)]">
{renderOrderNumbers(group)}
<div className="px-3 py-2">
<div className="text-xs text-[var(--color-text)]">{primaryBill}</div>
{totalBills > 1 && (
<span className="inline-block mt-0.5 rounded-full bg-[var(--color-accent-soft)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--color-accent)]">
{totalBills} сч.
</span>
)}
</div>
<div className="px-5 py-4">
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge>
<div className="px-3 py-2 text-xs text-[var(--color-text-muted)]">
{group.city || "—"}
</div>
<div className="px-5 py-4 text-sm">
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]"></span>}
<div className="px-3 py-2">
<span className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium leading-tight" style={{borderColor: "var(--color-border)", background: "var(--color-surface)", color: "var(--color-text)"}}>
{getOrderGroupDisplayStatusLabel(group)}
</span>
</div>
<div className="px-5 py-4 text-sm">
<div className="px-3 py-2 text-xs">
{group.deliveryDate ? (
<span>{fmtDate(group.deliveryDate)}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span>
) : (
<span className="text-[var(--color-text-muted)]"></span>
)}
</div>
<div className="px-5 py-4 text-sm">
{getDeliveryTypeLabel(group.deliveryType)}
<div className="px-3 py-2 text-xs">
<span className="inline-flex items-center gap-1">
{group.deliveryType === "pickup" ? "🏪" : "🚚"}
<span className="text-[var(--color-text-muted)]">{group.deliveryType === "pickup" ? "Самовывоз" : "Доставка"}</span>
</span>
</div>
</Button>
<div className="px-3 py-2 text-xs">
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]"></span>}
</div>
</button>
);
})}
</div>
</div>
)}
</div>
</Panel>

View File

@ -59,6 +59,7 @@ export const AppShell = ({
{navItems.map((item) => (
<Button
key={item.key}
variant="ghost"
className={[
"flex w-full items-center justify-between rounded-[18px] px-3 py-3 text-left text-sm transition",
activeSection === item.key
@ -131,6 +132,7 @@ export const AppShell = ({
{navItems.map((item) => (
<Button
key={item.key}
variant="ghost"
className={[
"flex flex-shrink-0 items-center gap-1.5 rounded-[14px] px-3 py-2 text-sm transition",
activeSection === item.key

View File

@ -7,7 +7,7 @@ import { OrderCompositionPanel } from "../components/client/OrderCompositionPane
import { getInvitationReferenceLabel } from "../components/client/invitationReference";
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
import { Panel } from "../components/UI/Panel";
import { Button } from "../components/UI/Button";
import { Skeleton } from "../components/UI/Loading";
import { formatDeliveryDate } from "../components/client/deliveryDateFormatting";
import {
@ -65,7 +65,16 @@ export const groupSlotsFromInvitation = (invitation, referenceDate = new Date())
const deliveryTime = invitation.deliveryTime;
if (!rawSlots.length && !deliveryDate) {
return [];
// Fallback: generate default delivery slots (tomorrow + dayAfter, both halves)
const todayKey = getBusinessTodayKey(referenceDate);
const tomorrow = addDaysToDateKey(todayKey, 1);
const dayAfter = addDaysToDateKey(todayKey, 2);
return [
{ id: `slot-${tomorrow}-first`, date: tomorrow, time: "Первая половина дня" },
{ id: `slot-${tomorrow}-second`, date: tomorrow, time: "Вторая половина дня" },
{ id: `slot-${dayAfter}-first`, date: dayAfter, time: "Первая половина дня" },
{ id: `slot-${dayAfter}-second`, date: dayAfter, time: "Вторая половина дня" },
].filter((s) => s.date);
}
if (!rawSlots.length && deliveryDate) {
@ -393,7 +402,7 @@ export const ClientDeliveryPage = () => {
<>
{/* Tab switcher */}
<div className="flex gap-2 rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<Button
<button
type="button"
className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${
activeTab === TAB_DELIVERY
@ -408,8 +417,8 @@ export const ClientDeliveryPage = () => {
}}
>
🚚 Доставка
</Button>
<Button
</button>
<button
type="button"
className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${
activeTab === TAB_PICKUP
@ -424,7 +433,7 @@ export const ClientDeliveryPage = () => {
}}
>
🏪 Самовывоз
</Button>
</button>
</div>
{activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && (
@ -448,6 +457,22 @@ export const ClientDeliveryPage = () => {
</Panel>
)}
{activeTab === TAB_DELIVERY && !invitation?.isPayedShip ? (
<div className="rounded-2xl border border-[var(--color-warning)] bg-[var(--color-warning-soft)] p-4">
<div className="flex items-start gap-3">
<span className="text-lg">💰</span>
<div>
<p className="text-sm font-semibold text-[var(--color-warning)]">
Стоимость доставки не оплачена
</p>
<p className="mt-1 text-sm text-[var(--color-text-muted)]">
Стоимость доставки будет рассчитана отдельно и согласована с логистом после выбора даты и времени.
</p>
</div>
</div>
</div>
) : null}
{activeTab === TAB_DELIVERY && slots.length ? (
<DeliverySlotsPicker
slots={slots}
@ -464,6 +489,14 @@ export const ClientDeliveryPage = () => {
/>
) : null}
{activeTab === TAB_PICKUP && (invitation?.pickupCode || invitation?.pickup_code) ? (
<div className="rounded-2xl border-2 border-[var(--color-accent)] bg-[var(--color-accent-soft)] p-4 text-center">
<p className="text-xs uppercase tracking-[0.2em] text-[var(--color-text-muted)]">Код выдачи</p>
<p className="mt-1 text-3xl font-bold tracking-[0.4em] text-[var(--color-accent)]">{invitation.pickupCode || invitation.pickup_code}</p>
<p className="mt-2 text-xs text-[var(--color-text-muted)]">Покажите этот код при получении заказа</p>
</div>
) : null}
{activeTab === TAB_DELIVERY && !slots.length ? (
<Panel className="p-5 sm:p-6">
<p className="text-sm text-[var(--color-text-muted)]">Нет доступных слотов для выбора доставки.</p>