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> </p>
</div> </div>
<Button <button
type="button"
onClick={loadLogs} onClick={loadLogs}
disabled={loading} 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" 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 ? "Загрузка..." : "Обновить"} {loading ? "Загрузка..." : "Обновить"}
</Button> </button>
</div> </div>
{/* Filters */} {/* Filters */}

View File

@ -236,29 +236,29 @@ export default function ErrorLogPanel() {
</span> </span>
</div> </div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<Button onClick={fetchErrors} style={btnBase} title="Обновить"></Button> <button type="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={handleCopyAll} disabled={errors.length === 0} style={{ ...btnAccent, opacity: errors.length === 0 ? 0.4 : 1 }}>
📋 Копировать всё 📋 Копировать всё
</Button> </button>
{selected.size > 0 && ( {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 JSON
</Button> </button>
<Button onClick={downloadCSV} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}> <button type="button" onClick={downloadCSV} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}>
CSV CSV
</Button> </button>
{selected.size > 0 && ( {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>
</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>} {err.user_id && <div style={{ gridColumn: '1 / -1' }}><strong>User ID:</strong> {err.user_id}</div>}
</div> </div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}> <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>
</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"> <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> <p className="text-sm font-medium text-[var(--color-text)]">Где применять стоп-слова:</p>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <button
type="button" type="button"
disabled={isSavingScope} disabled={isSavingScope}
onClick={() => handleScopeChange("everywhere")} onClick={() => handleScopeChange("everywhere")}
className={[ 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" scope === "everywhere"
? "border-[var(--color-accent)] bg-[var(--color-accent)] text-[var(--color-accent-contrast)]" ? "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)]", : "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(" ")} ].join(" ")}
> >
Везде Везде
</Button> </button>
<Button <button
type="button" type="button"
disabled={isSavingScope} disabled={isSavingScope}
onClick={() => handleScopeChange("client_only")} onClick={() => handleScopeChange("client_only")}
className={[ 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" scope === "client_only"
? "border-[var(--color-accent)] bg-[var(--color-accent)] text-[var(--color-accent-contrast)]" ? "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)]", : "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(" ")} ].join(" ")}
> >
Только карточка клиента Только карточка клиента
</Button> </button>
</div> </div>
<p className="text-xs text-[var(--color-text-muted)]"> <p className="text-xs text-[var(--color-text-muted)]">
{scope === "everywhere" {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)]" 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} 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> </div>
{error && ( {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)]" 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} {w.word}
<Button <button
type="button" type="button"
disabled={savingId === w.id} disabled={savingId === w.id}
onClick={() => handleDelete(w.id)} onClick={() => handleDelete(w.id)}
@ -186,7 +189,7 @@ export const StopWordsPanel = () => {
aria-label={`Удалить ${w.word}`} aria-label={`Удалить ${w.word}`}
> >
</Button> </button>
</span> </span>
))} ))}
</div> </div>

View File

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

View File

@ -69,12 +69,12 @@ function RoleDropdown({ value, onChange }) {
return ( return (
<div ref={ref} className="relative w-full"> <div ref={ref} className="relative w-full">
<Button <button
type="button" type="button"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className={[ 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)]', '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(' ')} ].join(' ')}
> >
{value ? ( {value ? (
@ -83,24 +83,24 @@ function RoleDropdown({ value, onChange }) {
<span className="text-[var(--color-text-muted)]">Выберите роль</span> <span className="text-[var(--color-text-muted)]">Выберите роль</span>
)} )}
<span className="text-[var(--color-text-muted)] text-xs"></span> <span className="text-[var(--color-text-muted)] text-xs"></span>
</Button> </button>
{open && ( {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"> <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) => { {ROLES.map((r) => {
const sel = r === value; const sel = r === value;
return ( return (
<Button <button
key={r} key={r}
type="button" type="button"
onClick={() => pick(r)} onClick={() => pick(r)}
className={[ 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)]', sel ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent)]' : 'text-[var(--color-text)] hover:bg-[var(--color-surface)]',
].join(' ')} ].join(' ')}
> >
<span>{ROLE_LABELS[r]}</span> <span>{ROLE_LABELS[r]}</span>
{sel && <span className="text-[var(--color-accent)]"></span>} {sel && <span className="text-[var(--color-accent)]"></span>}
</Button> </button>
); );
})} })}
</div> </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="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"> <div className="flex gap-3 pt-2">
<Button <button
type="button" type="button"
onClick={onClose} 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" 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" type="submit"
disabled={submitting} 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" 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 ? 'Добавление…' : 'Добавить'} {submitting ? 'Добавление…' : 'Добавить'}
</Button> </button>
</div> </div>
</form> </form>
</div> </div>
@ -295,12 +295,13 @@ export default function UserManagementPanel() {
{/* Toolbar */} {/* Toolbar */}
<div className="flex items-center justify-between gap-2 flex-wrap mb-4"> <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> <span className="text-sm text-[var(--color-text-muted)]">{users.length} пользователей</span>
<Button <button
type="button"
onClick={() => { setShowAddForm(true); setAddError(null); }} 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" className="rounded-full bg-[var(--color-accent)] px-4 py-2 text-sm font-semibold text-white hover:opacity-90 transition"
> >
+ Добавить + Добавить
</Button> </button>
</div> </div>
{/* Add user modal */} {/* Add user modal */}
@ -355,15 +356,17 @@ export default function UserManagementPanel() {
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <button
type="button"
onClick={cancelEdit} 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" 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} onClick={saveEdit}
disabled={saving} 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" 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>
</div> </div>
); );
@ -380,20 +383,22 @@ export default function UserManagementPanel() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge tone={ROLE_TONES[rn] || 'neutral'}>{ROLE_LABELS[rn] || rn}</Badge> <Badge tone={ROLE_TONES[rn] || 'neutral'}>{ROLE_LABELS[rn] || rn}</Badge>
<Button <button
type="button"
onClick={() => startEdit(user)} 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" 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 ? ( {deleteConfirmId === user.id ? (
<div className="flex gap-1"> <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 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 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={() => 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> </div>
) : ( ) : (
<Button <button
type="button"
onClick={() => setDeleteConfirmId(user.id)} 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" 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>
</div> </div>

View File

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

View File

@ -57,11 +57,20 @@ const getPickupSlots = (referenceDate = new Date()) => {
const slots = []; const slots = [];
if (isTodayWorkday && hour < 12) { if (isTodayWorkday) {
if (hour < 12) {
slots.push({
id: `pickup-${todayKey}-first`,
date: todayKey,
time: "Первая половина дня",
label: "Сегодня",
pickupType: "today",
});
}
slots.push({ slots.push({
id: `pickup-${todayKey}-first`, id: `pickup-${todayKey}-second`,
date: todayKey, date: todayKey,
time: "Первая половина дня", time: "Вторая половина дня",
label: "Сегодня", label: "Сегодня",
pickupType: "today", pickupType: "today",
}); });

View File

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

View File

@ -84,23 +84,23 @@ const CalendarWidget = ({
</h4> </h4>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <button
variant="ghost" type="button"
disabled={!canGoBack} disabled={!canGoBack}
aria-label="Предыдущий месяц" 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))} onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() - 1, 1))}
> >
</Button> </button>
<Button <button
variant="ghost" type="button"
aria-label="Следующий месяц" 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))} onClick={() => setCurrentMonth((month) => new Date(month.getFullYear(), month.getMonth() + 1, 1))}
> >
</Button> </button>
</div> </div>
</div> </div>
<div className="mt-4 grid grid-cols-7 gap-1 text-center text-[10px] font-semibold uppercase text-[var(--color-text-muted)]"> <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 React from "react";
import { Badge } from "../UI/Badge"; import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button"; import { Button } from "../UI/Button";
import { Select } from "../UI/Select";
import { Panel } from "../UI/Panel"; 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 = ({ const DriverAssignmentPanel = ({
order, order,
userRole, userRole,
@ -57,19 +131,13 @@ const DriverAssignmentPanel = ({
) : null} ) : null}
{!isDriverLocked ? ( {!isDriverLocked ? (
<div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]"> <div className="grid gap-3 md:grid-cols-[minmax(16rem,24rem)_auto]">
<Select <DriverDropdown
className="h-[46px] py-0"
value={selectedDriverId} value={selectedDriverId}
onChange={(e) => { drivers={drivers}
onDriverSelect(e.target.value); onChange={onDriverSelect}
}}
disabled={isSavingDriverAssignment} disabled={isSavingDriverAssignment}
> placeholder={order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}
<option value="">{order.assignedDriverId ? "Сменить водителя..." : "Выберите водителя..."}</option> />
{drivers.map((driver) => (
<option key={driver.id} value={driver.id}>{driver.name || driver.email}</option>
))}
</Select>
<Button <Button
className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start" className="md:px-4 md:py-2 md:whitespace-nowrap md:self-start"
onClick={onConfirmDriver} onClick={onConfirmDriver}

View File

@ -84,14 +84,13 @@ const CollapsibleChips = ({ label, items }) => {
if (!Array.isArray(items) || items.length === 0) return null; if (!Array.isArray(items) || items.length === 0) return null;
return ( return (
<span className="inline"> <span className="inline">
<Button <button
variant="ghost" type="button"
size="sm"
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)]" 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)} onClick={() => setOpen(!open)}
> >
{label} {open ? "▲" : "▼"} {label} {open ? "▲" : "▼"}
</Button> </button>
{open && ( {open && (
<span className="ml-1 inline-flex flex-wrap gap-1"> <span className="ml-1 inline-flex flex-wrap gap-1">
{items.map((item, idx) => ( {items.map((item, idx) => (
@ -517,15 +516,15 @@ const ProblemReasonModal = ({ onSelect, onCancel }) => (
<p className="text-sm text-[var(--color-text-muted)]">Укажите причину возникшей проблемы с доставкой.</p> <p className="text-sm text-[var(--color-text-muted)]">Укажите причину возникшей проблемы с доставкой.</p>
<div className="space-y-2"> <div className="space-y-2">
{PROBLEM_REASONS.map((reason) => ( {PROBLEM_REASONS.map((reason) => (
<Button <button
key={reason.value} key={reason.value}
variant="secondary" type="button"
className="w-full rounded-[16px] p-3 text-left" 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)} onClick={() => onSelect(reason.value, reason.label)}
> >
<span className="font-medium">{reason.label}</span> <span className="font-medium">{reason.label}</span>
<p className="mt-0.5 text-xs text-[var(--color-text-muted)]">{reason.description}</p> <p className="mt-0.5 text-xs text-[var(--color-text-muted)]">{reason.description}</p>
</Button> </button>
))} ))}
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
@ -885,20 +884,28 @@ export const OrderDetailPanel = ({
</div> </div>
{/* Delivery type tabs */} {/* Delivery type tabs */}
<div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1"> <div className="flex gap-2 rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<Button <button
variant={deliveryType === "delivery" ? "primary" : "ghost"} type="button"
className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold" 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(""); }} onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }}
> >
🚚 Доставка 🚚 Доставка
</Button> </button>
<Button <button
variant={deliveryType === "pickup" ? "primary" : "ghost"} type="button"
className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold" 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(""); }} onClick={() => { setDeliveryType("pickup"); setFormMessage(""); }}
> >
🏪 Самовывоз 🏪 Самовывоз
</Button> </button>
</div> </div>
{deliveryType === "pickup" && ( {deliveryType === "pickup" && (
<div className="rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-sm text-[var(--color-text-muted)]"> <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 { Panel } from "../UI/Panel";
import { DatePicker } from "../UI/DatePicker"; 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 = [] }) => { export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities = [] }) => {
const statusValue = filters.displayStatus || filters.status || "all"; const statusValue = filters.displayStatus || filters.status || "all";
const selectedStatusLabel = statusOptions.find((option) => option.value === statusValue)?.label || statusValue; 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: "" })); 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 ( return (
<Panel className="p-4"> <Panel className="p-4">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{/* Row 1: Status + City + Search */} {/* Row 1: Status + City + DeliveryType + Search */}
<div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,0.5fr)_minmax(0,1.6fr)] md:items-end"> <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"> <div ref={statusMenuRef} className="relative flex min-w-0 flex-col gap-2">
<Button <button
variant="ghost" type="button"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={isStatusOpen} aria-expanded={isStatusOpen}
className={["flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition", 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 className="min-w-0 flex-1 truncate">{selectedStatusLabel}</span>
<span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]"></span> <span aria-hidden="true" className="ml-3 text-[var(--color-text-muted)]"></span>
</Button> </button>
{isStatusOpen ? ( {isStatusOpen ? (
<div <div
@ -65,12 +149,12 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
{statusOptions.map((option) => { {statusOptions.map((option) => {
const isSelected = option.value === statusValue; const isSelected = option.value === statusValue;
return ( return (
<Button <button
key={option.value} key={option.value}
variant="ghost" type="button"
role="option" role="option"
aria-selected={isSelected} 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 isSelected
? "bg-[var(--color-accent-soft)] text-[var(--color-text)]" ? "bg-[var(--color-accent-soft)] text-[var(--color-text)]"
: "text-[var(--color-text)] hover:bg-[var(--color-surface-strong)]", : "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="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)]"> <span className="ml-2 text-xs text-[var(--color-text-muted)]">{option.count || 0}</span>
{option.count || 0}
</span>
{isSelected ? <span className="ml-3 text-[var(--color-accent)]"></span> : null} {isSelected ? <span className="ml-3 text-[var(--color-accent)]"></span> : null}
</Button> </button>
); );
})} })}
</div> </div>
@ -93,27 +175,20 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
</div> </div>
{cities.length > 0 && ( {cities.length > 0 && (
<select <DropdownSelect
value={filters.city || ""} value={filters.city || ""}
onChange={(e) => updateFilter("city", e.target.value)} options={cityOptions}
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)]" onChange={(v) => updateFilter("city", v)}
> placeholder="Все города"
<option value="">Все города</option> />
{cities.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
)} )}
<select <DropdownSelect
value={filters.deliveryType || ""} value={filters.deliveryType || ""}
onChange={(e) => updateFilter("deliveryType", e.target.value)} options={deliveryTypeOptions}
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)]" onChange={(v) => updateFilter("deliveryType", v)}
> placeholder="Тип получения"
<option value="">Тип получения</option> />
<option value="delivery">Доставка</option>
<option value="pickup">Самовывоз</option>
</select>
<Input <Input
className="h-[46px] py-0" 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]" className="min-w-[140px] flex-1 md:max-w-[200px]"
/> />
{hasDateFilter && ( {hasDateFilter && (
<Button <button
variant="ghost" type="button"
onClick={clearDateFilter} 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)]" 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>
</div> </div>

View File

@ -1,6 +1,6 @@
import { formatDateTime } from "../../utils/formatters"; import { formatDateTime } from "../../utils/formatters";
import { Badge } from "../UI/Badge"; import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Panel } from "../UI/Panel"; import { Panel } from "../UI/Panel";
import { SkeletonTable } from "../UI/Loading"; import { SkeletonTable } from "../UI/Loading";
import { OrderFilters } from "./OrderFilters"; import { OrderFilters } from "./OrderFilters";
@ -126,109 +126,130 @@ export const OrdersTable = ({
? "border-[var(--color-danger)] bg-[rgba(201,61,61,0.1)]" ? "border-[var(--color-danger)] bg-[rgba(201,61,61,0.1)]"
: "border-[var(--color-border)] bg-[var(--color-surface-strong)]"; : "border-[var(--color-border)] bg-[var(--color-surface-strong)]";
return ( const allNumbers = group.allBillNumbers || group.orderNumbers || [];
<button const primaryBill = allNumbers[0] || "—";
key={group.id} const totalCount = allNumbers.length;
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">
{group.displayTitle || group.customerName || group.groupKey}
</div>
<div className="flex flex-wrap items-center justify-end 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>
<Badge tone={getOrderGroupStatusTone(group)}>
{getOrderGroupDisplayStatusLabel(group)}
</Badge>
</div>
</div>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-[var(--color-text-muted)]"> return (
{renderMobileOrderNumbers(group)} <div key={group.id}>
{(group.ordersTotal || 0) > 1 && ( <div className="mb-1 flex items-center gap-2 px-1 text-xs">
<span className="rounded-full bg-[var(--color-surface)] px-2 py-0.5 text-xs"> <span className="font-medium text-[var(--color-text)]"> {primaryBill}</span>
{group.ordersTotal} сч. {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> </span>
)} )}
</div> </div>
{group.hasDeliveryProblem && ( <button
<div className="mt-2 rounded-lg border border-[var(--color-warning)] bg-[var(--color-warning-soft)] px-2 py-1 text-xs"> type="button"
<span className="font-medium text-[var(--color-warning)]"> Проблема с доставкой</span> onClick={() => onOpenOrder(group.id)}
{group.deliveryProblemNote && ( className={[baseClass, selectedClass, "flex flex-col items-start justify-start p-4"].join(" ")}
<div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div> >
)} <div className="min-w-0 w-full font-medium leading-snug break-words">
{group.displayTitle || group.customerName || group.groupKey}
</div> </div>
)} <div className="mt-1.5 flex flex-wrap items-center gap-1.5">
</button> <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>
<Badge tone={getOrderGroupStatusTone(group)}>
{getOrderGroupDisplayStatusLabel(group)}
</Badge>
</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>
{group.deliveryProblemNote && (
<div className="mt-0.5 text-[var(--color-text-muted)]">{group.deliveryProblemNote}</div>
)}
</div>
)}
</button>
</div>
); );
})} })}
</div> </div>
<div className="hidden overflow-x-auto md:block"> <div className="hidden md:block">
{!orderGroups.length ? ( {!orderGroups.length ? (
<div className="px-5 py-6 text-sm text-[var(--color-text-muted)]"> <div className="px-5 py-6 text-sm text-[var(--color-text-muted)]">
Группы не найдены. Попробуйте изменить поиск или статус. Группы не найдены. Попробуйте изменить поиск или статус.
</div> </div>
) : ( ) : (
<div> <div className="overflow-x-auto">
<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="min-w-[920px]">
<div className="px-5 py-4 font-medium">Группа / Клиент</div> <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-5 py-4 font-medium">Счёта</div> <div className="px-3 py-2 font-medium">Группа / Клиент</div>
<div className="px-5 py-4 font-medium">Статус</div> <div className="px-3 py-2 font-medium">Счета</div>
<div className="px-5 py-4 font-medium">Водитель</div> <div className="px-3 py-2 font-medium">Город</div>
<div className="px-5 py-4 font-medium">Дата доставки</div> <div className="px-3 py-2 font-medium">Статус</div>
<div className="px-5 py-4 font-medium">Тип</div> <div className="px-3 py-2 font-medium">Дата доставки</div>
</div> <div className="px-3 py-2 font-medium">Тип</div>
{orderGroups.map((group) => { <div className="px-3 py-2 font-medium">Водитель</div>
const hasProblem = group.hasDeliveryProblem; </div>
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 ${ {orderGroups.map((group) => {
hasProblem const hasProblem = group.hasDeliveryProblem;
? "bg-[rgba(201,61,61,0.1)] hover:bg-[rgba(201,61,61,0.15)]" 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 ${
: "hover:bg-[var(--color-accent-soft)]" hasProblem
} ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`; ? "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)]" : ""}`;
return ( const billNumbers = group.allBillNumbers || group.orderNumbers || [];
<Button const primaryBill = billNumbers[0] || "—";
key={group.id} const totalBills = billNumbers.length;
variant="ghost"
className={rowClassName} return (
onClick={() => onOpenOrder(group.id)} <button
> key={group.id}
<div className="px-5 py-4"> type="button"
<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> className={rowClassName}
<div className="mt-1 text-sm text-[var(--color-text-muted)]"> onClick={() => onOpenOrder(group.id)}
{group.customerPhone || ""} >
</div> <div className="min-w-0 px-3 py-2">
</div> <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="max-w-[260px] px-5 py-4 text-sm text-[var(--color-text-muted)]"> <div className="mt-0.5 text-[11px] text-[var(--color-text-muted)]">
{renderOrderNumbers(group)} {group.customerPhone || ""}
</div> </div>
<div className="px-5 py-4"> </div>
<Badge tone={getOrderGroupStatusTone(group)}>{getOrderGroupDisplayStatusLabel(group)}</Badge> <div className="px-3 py-2">
</div> <div className="text-xs text-[var(--color-text)]">{primaryBill}</div>
<div className="px-5 py-4 text-sm"> {totalBills > 1 && (
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]"></span>} <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)]">
</div> {totalBills} сч.
<div className="px-5 py-4 text-sm"> </span>
{group.deliveryDate ? ( )}
<span>{fmtDate(group.deliveryDate)}{group.deliveryTime ? <span className="text-[var(--color-text-muted)]"> · {group.deliveryTime}</span> : ""}</span> </div>
) : ( <div className="px-3 py-2 text-xs text-[var(--color-text-muted)]">
<span className="text-[var(--color-text-muted)]"></span> {group.city || "—"}
)} </div>
</div> <div className="px-3 py-2">
<div className="px-5 py-4 text-sm"> <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)"}}>
{getDeliveryTypeLabel(group.deliveryType)} {getOrderGroupDisplayStatusLabel(group)}
</div> </span>
</Button> </div>
); <div className="px-3 py-2 text-xs">
})} {group.deliveryDate ? (
</div> <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-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>
<div className="px-3 py-2 text-xs">
{group.assignedDriverName || <span className="text-[var(--color-text-muted)]"></span>}
</div>
</button>
);
})}
</div>
</div>
)} )}
</div> </div>
</Panel> </Panel>

View File

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

View File

@ -7,7 +7,7 @@ import { OrderCompositionPanel } from "../components/client/OrderCompositionPane
import { getInvitationReferenceLabel } from "../components/client/invitationReference"; import { getInvitationReferenceLabel } from "../components/client/invitationReference";
import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice"; import { DeliveryStateNotice } from "../components/client/DeliveryStateNotice";
import { Panel } from "../components/UI/Panel"; import { Panel } from "../components/UI/Panel";
import { Button } from "../components/UI/Button";
import { Skeleton } from "../components/UI/Loading"; import { Skeleton } from "../components/UI/Loading";
import { formatDeliveryDate } from "../components/client/deliveryDateFormatting"; import { formatDeliveryDate } from "../components/client/deliveryDateFormatting";
import { import {
@ -65,7 +65,16 @@ export const groupSlotsFromInvitation = (invitation, referenceDate = new Date())
const deliveryTime = invitation.deliveryTime; const deliveryTime = invitation.deliveryTime;
if (!rawSlots.length && !deliveryDate) { 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) { if (!rawSlots.length && deliveryDate) {
@ -393,7 +402,7 @@ export const ClientDeliveryPage = () => {
<> <>
{/* Tab switcher */} {/* Tab switcher */}
<div className="flex gap-2 rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] p-1"> <div className="flex gap-2 rounded-[28px] border border-[var(--color-border)] bg-[var(--color-surface)] p-1">
<Button <button
type="button" type="button"
className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${ className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${
activeTab === TAB_DELIVERY activeTab === TAB_DELIVERY
@ -408,8 +417,8 @@ export const ClientDeliveryPage = () => {
}} }}
> >
🚚 Доставка 🚚 Доставка
</Button> </button>
<Button <button
type="button" type="button"
className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${ className={`flex-1 rounded-[24px] px-4 py-2.5 text-sm font-semibold transition ${
activeTab === TAB_PICKUP activeTab === TAB_PICKUP
@ -424,7 +433,7 @@ export const ClientDeliveryPage = () => {
}} }}
> >
🏪 Самовывоз 🏪 Самовывоз
</Button> </button>
</div> </div>
{activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && ( {activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && (
@ -448,6 +457,22 @@ export const ClientDeliveryPage = () => {
</Panel> </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 ? ( {activeTab === TAB_DELIVERY && slots.length ? (
<DeliverySlotsPicker <DeliverySlotsPicker
slots={slots} slots={slots}
@ -464,6 +489,14 @@ export const ClientDeliveryPage = () => {
/> />
) : null} ) : 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 ? ( {activeTab === TAB_DELIVERY && !slots.length ? (
<Panel className="p-5 sm:p-6"> <Panel className="p-5 sm:p-6">
<p className="text-sm text-[var(--color-text-muted)]">Нет доступных слотов для выбора доставки.</p> <p className="text-sm text-[var(--color-text-muted)]">Нет доступных слотов для выбора доставки.</p>