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:
parent
17fe2125a5
commit
cdb2ec7098
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)]">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue