refactor: unify all buttons to <Button> component across app

- Replace 82 inline <button> elements with <Button variant/size> in 19 files
- UI primitives (SegmentedTabs, ThemeToggle, DatePicker) and ErrorBoundary kept as-is
- Consistent styling: primary (accent), secondary (outline), ghost (transparent)
This commit is contained in:
root 2026-06-16 20:11:57 +00:00
parent d28e00782b
commit 8d3d8dc0fe
19 changed files with 204 additions and 191 deletions

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useMemo, useCallback } from "react"; import React, { useState, useEffect, useMemo, useCallback } from "react";
import { Panel } from "../UI/Panel"; import { Panel } from "../UI/Panel";
import { Badge } from "../UI/Badge"; import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Skeleton } from "../UI/Loading"; import { Skeleton } from "../UI/Loading";
import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService"; import { fetchActionLogs, getActionLabel } from "../../services/supabase/actionLogService";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -197,13 +198,13 @@ export const ActionLogPanel = ({ orderGroupId = null }) => {
Кто, что и когда делал с доставками Кто, что и когда делал с доставками
</p> </p>
</div> </div>
<button <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

@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Panel } from '../UI/Panel'; import { Panel } from '../UI/Panel';
import { Badge } from '../UI/Badge'; import { Badge } from '../UI/Badge';
import { Button } from '../UI/Button';
import { Select } from '../UI/Select'; import { Select } from '../UI/Select';
import { Skeleton } from '../UI/Loading'; import { Skeleton } from '../UI/Loading';
import { supabase } from '../../supabaseClient'; import { supabase } from '../../supabaseClient';
@ -235,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 onClick={fetchErrors} style={btnBase} title="Обновить"></Button>
<button onClick={handleCopyAll} disabled={errors.length === 0} style={{ ...btnAccent, opacity: errors.length === 0 ? 0.4 : 1 }}> <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 onClick={handleCopySelected} style={btnAccent}>
📋 Копировать выбр. 📋 Копировать выбр.
</button> </Button>
)} )}
<button onClick={downloadJSON} disabled={errors.length === 0} style={{ ...btnBase, opacity: errors.length === 0 ? 0.4 : 1 }}> <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 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 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 onClick={handleDeleteAll} disabled={deleting || errors.length === 0} style={{ ...btnDanger, opacity: deleting || errors.length === 0 ? 0.4 : 1 }}>
🗑 Удалить все 🗑 Удалить все
</button> </Button>
</div> </div>
</div> </div>
@ -382,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 onClick={() => handleDeleteOne(err.id)} disabled={deleting} style={{ ...btnDanger, fontSize: '0.8rem' }}>
Удалить запись Удалить запись
</button> </Button>
</div> </div>
</div> </div>
)} )}

View File

@ -107,7 +107,7 @@ 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")}
@ -120,8 +120,8 @@ export const StopWordsPanel = () => {
].join(" ")} ].join(" ")}
> >
Везде Везде
</button> </Button>
<button <Button
type="button" type="button"
disabled={isSavingScope} disabled={isSavingScope}
onClick={() => handleScopeChange("client_only")} onClick={() => handleScopeChange("client_only")}
@ -134,7 +134,7 @@ export const StopWordsPanel = () => {
].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"
@ -178,7 +178,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 +186,7 @@ export const StopWordsPanel = () => {
aria-label={`Удалить ${w.word}`} aria-label={`Удалить ${w.word}`}
> >
</button> </Button>
</span> </span>
))} ))}
</div> </div>

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Panel } from "../UI/Panel"; import { Panel } from "../UI/Panel";
import { Button } from "../UI/Button";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
const CATEGORY_OPTIONS = [ const CATEGORY_OPTIONS = [
@ -103,7 +104,7 @@ 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)}
@ -115,7 +116,7 @@ export const SuggestionsPanel = () => {
].join(" ")} ].join(" ")}
> >
{cat.icon} {cat.label} {cat.icon} {cat.label}
</button> </Button>
))} ))}
</div> </div>
<textarea <textarea
@ -126,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>
@ -209,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)}
@ -223,7 +224,7 @@ const SuggestionCard = ({ suggestion: s, category, status, isAdmin, onStatusChan
}} }}
> >
{val.label} {val.label}
</button> </Button>
))} ))}
</div> </div>
{editing ? ( {editing ? (
@ -235,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

@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Panel } from '../UI/Panel'; import { Panel } from '../UI/Panel';
import { Badge } from '../UI/Badge'; import { Badge } from '../UI/Badge';
import { Button } from '../UI/Button';
import { Input } from '../UI/Input'; import { Input } from '../UI/Input';
import { Skeleton } from '../UI/Loading'; import { Skeleton } from '../UI/Loading';
@ -68,7 +69,7 @@ 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={[
@ -82,13 +83,13 @@ 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)}
@ -99,7 +100,7 @@ function RoleDropdown({ value, onChange }) {
> >
<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>
@ -161,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>
@ -294,12 +295,12 @@ 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
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 */}
@ -354,15 +355,15 @@ export default function UserManagementPanel() {
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <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
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>
); );
@ -379,20 +380,20 @@ 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
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 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 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
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

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
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 { getInvitationReferenceLabel } from "./invitationReference"; import { getInvitationReferenceLabel } from "./invitationReference";
import { supabase } from "../../supabaseClient"; import { supabase } from "../../supabaseClient";
@ -100,8 +101,8 @@ export const OrderCompositionPanel = ({ invitation = {} }) => {
return ( return (
<Panel className="space-y-3 border shadow-soft p-5 sm:p-6"> <Panel className="space-y-3 border shadow-soft p-5 sm:p-6">
<button <Button
type="button" variant="ghost"
className="flex w-full items-center justify-between text-left" className="flex w-full items-center justify-between text-left"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
> >
@ -121,7 +122,7 @@ export const OrderCompositionPanel = ({ invitation = {} }) => {
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg> </svg>
</span> </span>
</button> </Button>
{isExpanded && ( {isExpanded && (
<div className="space-y-2"> <div className="space-y-2">
{products.map((product, idx) => ( {products.map((product, idx) => (

View File

@ -19,15 +19,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}
type="button" variant="secondary"
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)]" className="w-full rounded-[16px] p-3 text-left"
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">

View File

@ -10,6 +10,7 @@ import {
parseGroupDate, parseGroupDate,
} from "../../services/orderGroupViews"; } from "../../services/orderGroupViews";
import { Badge } from "../UI/Badge"; import { Badge } from "../UI/Badge";
import { Button } from "../UI/Button";
import { Input } from "../UI/Input"; import { Input } from "../UI/Input";
import { Select } from "../UI/Select"; import { Select } from "../UI/Select";
import { Panel } from "../UI/Panel"; import { Panel } from "../UI/Panel";
@ -247,31 +248,35 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
{/* Date pills */} {/* Date pills */}
{sortedDeliveryDates.length > 0 && ( {sortedDeliveryDates.length > 0 && (
<div className="flex flex-wrap gap-2 pt-2"> <div className="flex flex-wrap gap-2 pt-2">
<button <Button
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setFilters((current) => ({ ...current, selectedDate: "" }))} onClick={() => setFilters((current) => ({ ...current, selectedDate: "" }))}
className={[ className={[
"rounded-full border px-3 py-1.5 text-xs font-medium transition", "rounded-full px-3 py-1.5 text-xs font-medium",
!filters.selectedDate !filters.selectedDate
? "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)]", : "text-[var(--color-text-muted)]",
].join(" ")} ].join(" ")}
> >
Все даты Все даты
</button> </Button>
{sortedDeliveryDates.map((date) => { {sortedDeliveryDates.map((date) => {
const count = dateDeliveryMap.get(date) || 0; const count = dateDeliveryMap.get(date) || 0;
const selected = isDateSelected(date); const selected = isDateSelected(date);
return ( return (
<button <Button
key={date} key={date}
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setFilters((current) => ({ ...current, selectedDate: date }))} onClick={() => setFilters((current) => ({ ...current, selectedDate: date }))}
className={[ className={[
"flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition", "flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium",
selected selected
? "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)]", : "text-[var(--color-text-muted)]",
].join(" ")} ].join(" ")}
> >
<span>{parseGroupDate(date)?.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }) || "—"}</span> <span>{parseGroupDate(date)?.toLocaleDateString("ru-RU", { day: "numeric", month: "short" }) || "—"}</span>
@ -280,7 +285,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
{count} {count}
</span> </span>
)} )}
</button> </Button>
); );
})} })}
</div> </div>
@ -289,31 +294,35 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
{/* City pills */} {/* City pills */}
{sortedCities.length > 1 && ( {sortedCities.length > 1 && (
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button <Button
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setFilters((current) => ({ ...current, selectedCity: "" }))} onClick={() => setFilters((current) => ({ ...current, selectedCity: "" }))}
className={[ className={[
"rounded-full border px-3 py-1.5 text-xs font-medium transition", "rounded-full px-3 py-1.5 text-xs font-medium",
!filters.selectedCity !filters.selectedCity
? "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)]", : "text-[var(--color-text-muted)]",
].join(" ")} ].join(" ")}
> >
Все города Все города
</button> </Button>
{sortedCities.map((city) => { {sortedCities.map((city) => {
const count = cityDeliveryMap.get(city) || 0; const count = cityDeliveryMap.get(city) || 0;
const selected = filters.selectedCity === city; const selected = filters.selectedCity === city;
return ( return (
<button <Button
key={city} key={city}
type="button" type="button"
variant="ghost"
size="sm"
onClick={() => setFilters((current) => ({ ...current, selectedCity: city }))} onClick={() => setFilters((current) => ({ ...current, selectedCity: city }))}
className={[ className={[
"flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition", "flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs font-medium",
selected selected
? "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)]", : "text-[var(--color-text-muted)]",
].join(" ")} ].join(" ")}
> >
<span>{city}</span> <span>{city}</span>
@ -322,7 +331,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
{count} {count}
</span> </span>
)} )}
</button> </Button>
); );
})} })}
</div> </div>
@ -359,8 +368,9 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
return ( return (
<Panel key={group.date} className="space-y-4 p-5"> <Panel key={group.date} className="space-y-4 p-5">
<button <Button
type="button" type="button"
variant="ghost"
className="flex w-full items-center gap-3 text-left" className="flex w-full items-center gap-3 text-left"
onClick={() => toggleDate(group.date)} onClick={() => toggleDate(group.date)}
> >
@ -381,7 +391,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
<Badge key={status} tone={tone}>{count} {label}</Badge> <Badge key={status} tone={tone}>{count} {label}</Badge>
))} ))}
</div> </div>
</button> </Button>
{!isCollapsed && ( {!isCollapsed && (
<div className="space-y-4 pt-1"> <div className="space-y-4 pt-1">
@ -393,10 +403,10 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
</div> </div>
<div className="grid gap-3"> <div className="grid gap-3">
{items.map((item) => ( {items.map((item) => (
<button <Button
key={item.id} key={item.id}
type="button" variant="secondary"
className="rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-left transition hover:bg-[var(--color-accent-soft)]" className="rounded-[24px] p-4 text-left"
onClick={() => onOpenOrder?.(item.id)} onClick={() => onOpenOrder?.(item.id)}
> >
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
@ -414,7 +424,7 @@ export const DriverDeliveryPlanner = ({ orderGroups = [], onOpenOrder, currentUs
<div className="mt-3 text-sm text-[var(--color-text-muted)]"> <div className="mt-3 text-sm text-[var(--color-text-muted)]">
{item.deliveryAddress || item.delivery_address || "Адрес не указан"} {item.deliveryAddress || item.delivery_address || "Адрес не указан"}
</div> </div>
</button> </Button>
))} ))}
</div> </div>
</div> </div>

View File

@ -376,13 +376,15 @@ export const DriverShipmentPanel = ({ order, onShipmentChange, onSaveShipment, i
{!justSaved && savedShipment.length > 0 && ( {!justSaved && savedShipment.length > 0 && (
<div className="rounded-xl border border-[var(--color-accent)] bg-[var(--color-accent-soft)] px-3 py-2 text-xs text-[var(--color-accent)]"> <div className="rounded-xl border border-[var(--color-accent)] bg-[var(--color-accent-soft)] px-3 py-2 text-xs text-[var(--color-accent)]">
Отгрузка ранее сохранена: {savedShipment.filter((i) => i.shipped).length}/{savedShipment.length} позиций Отгрузка ранее сохранена: {savedShipment.filter((i) => i.shipped).length}/{savedShipment.length} позиций
<button <Button
type="button" type="button"
className="ml-2 underline text-[var(--color-text-muted)]" variant="ghost"
size="sm"
className="underline text-[var(--color-text-muted)]"
onClick={() => setJustSaved(true)} onClick={() => setJustSaved(true)}
> >
Показать Показать
</button> </Button>
</div> </div>
)} )}
</Panel> </Panel>

View File

@ -7,6 +7,7 @@ import {
ORDER_GROUP_DISPLAY_STATUS_OPTIONS, ORDER_GROUP_DISPLAY_STATUS_OPTIONS,
} from "../../services/orderGroupViews"; } from "../../services/orderGroupViews";
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 { SkeletonPage } from "../UI/Loading"; import { SkeletonPage } from "../UI/Loading";
import { OrderFilters } from "../orders/OrderFilters"; import { OrderFilters } from "../orders/OrderFilters";
@ -113,9 +114,9 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
return ( return (
<Panel key={statusValue} className="overflow-hidden p-0"> <Panel key={statusValue} className="overflow-hidden p-0">
<button <Button
type="button" variant="ghost"
className="flex w-full items-center justify-between px-5 py-3 text-left transition hover:bg-[var(--color-accent-soft)]" className="flex w-full items-center justify-between px-5 py-3 text-left"
onClick={() => { onClick={() => {
setCollapsedSections((prev) => { setCollapsedSections((prev) => {
const next = new Set(prev); const next = new Set(prev);
@ -142,16 +143,16 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
> >
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg> </svg>
</button> </Button>
{!isCollapsed && ( {!isCollapsed && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<TableHeader /> <TableHeader />
{groups.map((group) => ( {groups.map((group) => (
<button <Button
key={group.id} key={group.id}
type="button" variant="ghost"
className={`grid ${COLS} gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)]`} className={`grid ${COLS} gap-0 w-full border-t border-[var(--color-border)] text-left`}
onClick={() => { if (onSelectSet) onSelectSet(group.id); }} onClick={() => { if (onSelectSet) onSelectSet(group.id); }}
> >
<div className="px-4 py-2.5"> <div className="px-4 py-2.5">
@ -176,7 +177,7 @@ export const LogisticsReadinessBoard = ({ orderGroups = [], onSelectSet, statusO
<div className="px-4 py-2.5 text-sm text-[var(--color-text-muted)]"> <div className="px-4 py-2.5 text-sm text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)} {formatDateTime(group.updatedAt)}
</div> </div>
</button> </Button>
))} ))}
</div> </div>
)} )}

View File

@ -47,7 +47,7 @@ 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)]" className="relative flex h-9 w-9 items-center justify-center rounded-full transition hover:bg-[var(--color-surface-strong)]"
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,29 @@ 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)]" className="rounded 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)]" className="rounded 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)]" className="rounded 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

@ -2,6 +2,7 @@ import React from "react";
import { useNotificationPreferences } from "../../hooks/useNotifications"; import { useNotificationPreferences } from "../../hooks/useNotifications";
import { usePushNotifications } from "../../hooks/usePushNotifications"; import { usePushNotifications } from "../../hooks/usePushNotifications";
import { Panel } from "../UI/Panel"; import { Panel } from "../UI/Panel";
import { Button } from "../UI/Button";
import { Bell, Settings } from "../UI/Icons"; import { Bell, Settings } from "../UI/Icons";
import { Skeleton } from "../UI/Loading"; import { Skeleton } from "../UI/Loading";
@ -27,12 +28,12 @@ export function NotificationSettings({ userId, userRole, onBack }) {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{onBack && ( {onBack && (
<button <Button
className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]" className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
onClick={onBack} onClick={onBack}
> >
Назад Назад
</button> </Button>
)} )}
<h2 className="text-lg font-semibold">Настройки уведомлений</h2> <h2 className="text-lg font-semibold">Настройки уведомлений</h2>
</div> </div>
@ -67,12 +68,12 @@ export function NotificationSettings({ userId, userRole, onBack }) {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{onBack && ( {onBack && (
<button <Button
className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]" className="rounded p-1 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
onClick={onBack} onClick={onBack}
> >
Назад Назад
</button> </Button>
)} )}
<h2 className="text-lg font-semibold">Настройки уведомлений</h2> <h2 className="text-lg font-semibold">Настройки уведомлений</h2>
</div> </div>
@ -94,7 +95,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
</div> </div>
</div> </div>
{isSupported && ( {isSupported && (
<button <Button
className={`relative h-6 w-11 rounded-full transition-colors ${ className={`relative h-6 w-11 rounded-full transition-colors ${
isSubscribed ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]" isSubscribed ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]"
}`} }`}
@ -106,7 +107,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
isSubscribed ? "translate-x-5" : "translate-x-0" isSubscribed ? "translate-x-5" : "translate-x-0"
}`} }`}
/> />
</button> </Button>
)} )}
</div> </div>
</Panel> </Panel>
@ -124,7 +125,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
<p className="text-sm font-medium">{label}</p> <p className="text-sm font-medium">{label}</p>
<p className="text-xs text-[var(--color-text-muted)]">{description}</p> <p className="text-xs text-[var(--color-text-muted)]">{description}</p>
</div> </div>
<button <Button
className={`relative mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors ${ className={`relative mt-0.5 h-6 w-11 shrink-0 rounded-full transition-colors ${
prefs[key] ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]" prefs[key] ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]"
}`} }`}
@ -136,7 +137,7 @@ export function NotificationSettings({ userId, userRole, onBack }) {
prefs[key] ? "translate-x-5" : "translate-x-0" prefs[key] ? "translate-x-5" : "translate-x-0"
}`} }`}
/> />
</button> </Button>
</label> </label>
))} ))}
</div> </div>

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { Button } from "../UI/Button";
const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"]; const WEEK_DAY_LABELS = ["ПН", "ВТ", "СР", "ЧТ", "ПТ", "СБ", "ВС"];
@ -58,8 +59,8 @@ const CalendarWidget = ({
return ( return (
<div className={layoutClassName}> <div className={layoutClassName}>
<div className={calendarClassName}> <div className={calendarClassName}>
<button <Button
type="button" variant="ghost"
aria-label={label} aria-label={label}
aria-expanded={isCalendarOpen} aria-expanded={isCalendarOpen}
className="flex min-h-[54px] w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-left text-sm font-medium !text-[var(--color-text)] transition hover:border-[var(--color-accent)] focus:border-[var(--color-accent)] focus:outline-none" className="flex min-h-[54px] w-full items-center justify-between rounded-2xl border border-[var(--color-border)] bg-[var(--color-surface)] px-4 text-left text-sm font-medium !text-[var(--color-text)] transition hover:border-[var(--color-accent)] focus:border-[var(--color-accent)] focus:outline-none"
@ -67,7 +68,7 @@ const CalendarWidget = ({
> >
<span>{selectedDate ? formatDateForDisplay(selectedDate) : "Выберите дату"}</span> <span>{selectedDate ? formatDateForDisplay(selectedDate) : "Выберите дату"}</span>
<span aria-hidden="true" className="text-[var(--color-text-muted)]"></span> <span aria-hidden="true" className="text-[var(--color-text-muted)]"></span>
</button> </Button>
{isCalendarOpen ? ( {isCalendarOpen ? (
<div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]"> <div className="rounded-[20px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-3 shadow-lg absolute left-0 top-full z-50 mt-2 w-[300px]">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
@ -83,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
type="button" variant="ghost"
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
type="button" variant="ghost"
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)]">
@ -123,9 +124,9 @@ const CalendarWidget = ({
const dayNumber = String(day.getDate()).padStart(2, "0"); const dayNumber = String(day.getDate()).padStart(2, "0");
return ( return (
<button <Button
key={dateKey} key={dateKey}
type="button" variant="ghost"
disabled={isDisabled} disabled={isDisabled}
title={isWeekend ? "Выходной, доставки нет" : isSelectable ? "Можно выбрать" : "Недоступно"} title={isWeekend ? "Выходной, доставки нет" : isSelectable ? "Можно выбрать" : "Недоступно"}
className={[ className={[
@ -150,7 +151,7 @@ const CalendarWidget = ({
className="absolute inset-x-2 top-1/2 h-px -rotate-12 bg-[var(--color-text-muted)] opacity-70" className="absolute inset-x-2 top-1/2 h-px -rotate-12 bg-[var(--color-text-muted)] opacity-70"
/> />
) : null} ) : null}
</button> </Button>
); );
})} })}
</div> </div>
@ -162,9 +163,9 @@ const CalendarWidget = ({
</div> </div>
<div className={timeClassName}> <div className={timeClassName}>
{timeOptions.map((option) => ( {timeOptions.map((option) => (
<button <Button
key={option} key={option}
type="button" variant="ghost"
aria-pressed={selectedTime === option} aria-pressed={selectedTime === option}
className={[ className={[
"min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition", "min-h-[54px] rounded-2xl border px-4 text-left text-sm font-medium transition",
@ -175,7 +176,7 @@ const CalendarWidget = ({
onClick={() => onTimeChange(option)} onClick={() => onTimeChange(option)}
> >
{option} {option}
</button> </Button>
))} ))}
</div> </div>
</div> </div>

View File

@ -67,8 +67,8 @@ const ConfirmModal = ({ open, title, message, onConfirm, onCancel }) => {
{title && <h3 className="text-lg font-semibold">{title}</h3>} {title && <h3 className="text-lg font-semibold">{title}</h3>}
{message && <p className="mt-2 text-sm text-[var(--color-text-muted)]">{message}</p>} {message && <p className="mt-2 text-sm text-[var(--color-text-muted)]">{message}</p>}
<div className="mt-5 flex justify-end gap-3"> <div className="mt-5 flex justify-end gap-3">
<button type="button" className="rounded-xl border border-[var(--color-border)] px-4 py-2 text-sm font-medium text-[var(--color-text-muted)] hover:bg-[var(--color-surface)]" onClick={onCancel}>Отмена</button> <Button variant="secondary" onClick={onCancel}>Отмена</Button>
<button type="button" className="rounded-xl bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-white hover:opacity-90" onClick={onConfirm}>Подтвердить</button> <Button variant="primary" onClick={onConfirm}>Подтвердить</Button>
</div> </div>
</div> </div>
</div> </div>
@ -84,13 +84,14 @@ 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
type="button" variant="ghost"
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) => (
@ -342,8 +343,8 @@ const CollapsibleOrderComposition = ({ order }) => {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<button <Button
type="button" variant="ghost"
className="flex w-full items-center justify-between text-left" className="flex w-full items-center justify-between text-left"
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
> >
@ -365,7 +366,7 @@ const CollapsibleOrderComposition = ({ order }) => {
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg> </svg>
</span> </span>
</button> </Button>
{isExpanded && ( {isExpanded && (
<div className="space-y-3"> <div className="space-y-3">
{!orders.length ? ( {!orders.length ? (
@ -516,15 +517,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}
type="button" variant="secondary"
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)]" className="w-full rounded-[16px] p-3 text-left"
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">
@ -884,28 +885,20 @@ 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
type="button" variant={deliveryType === "delivery" ? "primary" : "ghost"}
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition ${ className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold"
deliveryType === "delivery"
? "bg-[var(--color-accent)] text-white"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
}`}
onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }} onClick={() => { setDeliveryType("delivery"); setFormMessage(""); }}
> >
🚚 Доставка 🚚 Доставка
</button> </Button>
<button <Button
type="button" variant={deliveryType === "pickup" ? "primary" : "ghost"}
className={`flex-1 rounded-xl px-3 py-2 text-sm font-semibold transition ${ className="flex-1 rounded-xl px-3 py-2 text-sm font-semibold"
deliveryType === "pickup"
? "bg-[var(--color-accent)] text-white"
: "text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
}`}
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

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { Button } from "../UI/Button";
import { Input } from "../UI/Input"; 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";
@ -41,12 +42,11 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
{/* Row 1: Status + City + Search */} {/* 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"> <div className="grid gap-3 md:grid-cols-[minmax(12rem,0.7fr)_minmax(0,0.5fr)_minmax(0,1.6fr)] 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
type="button" variant="ghost"
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={isStatusOpen} aria-expanded={isStatusOpen}
className={[ className={["flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
"flex h-[46px] w-full items-center justify-between rounded-2xl border px-4 text-left text-sm transition",
isStatusOpen isStatusOpen
? "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)] hover:border-[var(--color-accent)]", : "border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text)] hover:border-[var(--color-accent)]",
@ -55,7 +55,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,13 +65,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}
type="button" variant="ghost"
role="option" role="option"
aria-selected={isSelected} aria-selected={isSelected}
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-3 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)]",
@ -86,7 +85,7 @@ export const OrderFilters = ({ filters, setFilters, statusOptions = [], cities =
{option.count || 0} {option.count || 0}
</span> </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>
@ -131,13 +130,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
type="button" variant="ghost"
onClick={clearDateFilter} onClick={clearDateFilter}
className="mb-0.5 flex h-[46px] items-center 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)] transition" 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

@ -107,15 +107,15 @@ export const OrdersCalendarView = ({ orders, onOpenOrder }) => {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{dayOrders.map((order) => ( {dayOrders.map((order) => (
<button <Button
key={order.id} key={order.id}
type="button" variant="ghost"
className="w-full rounded-[16px] px-3 py-3 text-left text-sm"
onClick={() => onOpenOrder(order.id)} onClick={() => onOpenOrder(order.id)}
className="w-full rounded-[16px] bg-[var(--color-surface)] px-3 py-3 text-left text-sm hover:bg-[var(--color-accent-soft)]"
> >
<div className="font-medium">{order.orderNumber}</div> <div className="font-medium">{order.orderNumber}</div>
<div className="mt-1 text-xs text-[var(--color-text-muted)]">{order.customer.name}</div> <div className="mt-1 text-xs text-[var(--color-text-muted)]">{order.customer.name}</div>
</button> </Button>
))} ))}
</div> </div>
</div> </div>
@ -157,17 +157,17 @@ export const OrdersCalendarView = ({ orders, onOpenOrder }) => {
<div className="space-y-2"> <div className="space-y-2">
{dayOrders.slice(0, 2).map((order) => ( {dayOrders.slice(0, 2).map((order) => (
<button <Button
key={order.id} key={order.id}
type="button" variant="ghost"
className="w-full rounded-[16px] px-3 py-2 text-left text-sm"
onClick={() => onOpenOrder(order.id)} onClick={() => onOpenOrder(order.id)}
className="w-full rounded-[16px] bg-[var(--color-surface)] px-3 py-2 text-left text-sm hover:bg-[var(--color-accent-soft)]"
> >
<div className="font-medium">{order.orderNumber}</div> <div className="font-medium">{order.orderNumber}</div>
<div className="mt-1 text-xs text-[var(--color-text-muted)]"> <div className="mt-1 text-xs text-[var(--color-text-muted)]">
{order.customer.name} {order.customer.name}
</div> </div>
</button> </Button>
))} ))}
{dayOrders.length > 2 ? ( {dayOrders.length > 2 ? (
<div className="px-1 text-xs text-[var(--color-text-muted)]"> <div className="px-1 text-xs text-[var(--color-text-muted)]">

View File

@ -1,5 +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";
@ -111,13 +112,12 @@ export const OrdersTable = ({
</div> </div>
) : null} ) : null}
{orderGroups.map((group) => ( {orderGroups.map((group) => (
<button <Button
key={group.id} key={group.id}
type="button" variant="ghost"
onClick={() => onOpenOrder(group.id)} onClick={() => onOpenOrder(group.id)}
className={[ className={["w-full rounded-[22px] border text-left transition",
"w-full rounded-[22px] border border-[var(--color-border)] bg-[var(--color-surface-strong)] p-4 text-left transition", selectedOrderGroupId === group.id ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]" : "border-[var(--color-border)] bg-[var(--color-surface-strong)]",
selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : "",
].join(" ")} ].join(" ")}
> >
<div className="space-y-1"> <div className="space-y-1">
@ -150,7 +150,7 @@ export const OrdersTable = ({
<div className="mt-3 text-xs text-[var(--color-text-muted)]"> <div className="mt-3 text-xs text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)} {formatDateTime(group.updatedAt)}
</div> </div>
</button> </Button>
))} ))}
</div> </div>
@ -171,9 +171,9 @@ export const OrdersTable = ({
<div className="px-5 py-4 font-medium">Обновлён</div> <div className="px-5 py-4 font-medium">Обновлён</div>
</div> </div>
{orderGroups.map((group) => ( {orderGroups.map((group) => (
<button <Button
key={group.id} key={group.id}
type="button" variant="ghost"
className={`grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`} className={`grid grid-cols-[minmax(180px,2.5fr)_minmax(100px,1.2fr)_minmax(100px,1fr)_minmax(80px,1fr)_minmax(120px,1.2fr)_minmax(80px,0.8fr)_minmax(130px,1.2fr)] gap-0 w-full border-t border-[var(--color-border)] text-left transition hover:bg-[var(--color-accent-soft)] ${selectedOrderGroupId === group.id ? "bg-[var(--color-accent-soft)]" : ""}`}
onClick={() => onOpenOrder(group.id)} onClick={() => onOpenOrder(group.id)}
> >
@ -219,7 +219,7 @@ export const OrdersTable = ({
<div className="px-5 py-4 text-sm text-[var(--color-text-muted)]"> <div className="px-5 py-4 text-sm text-[var(--color-text-muted)]">
{formatDateTime(group.updatedAt)} {formatDateTime(group.updatedAt)}
</div> </div>
</button> </Button>
))} ))}
</div> </div>
)} )}

View File

@ -57,7 +57,7 @@ export const AppShell = ({
<div className="space-y-1"> <div className="space-y-1">
{navItems.map((item) => ( {navItems.map((item) => (
<button <Button
key={item.key} key={item.key}
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",
@ -70,7 +70,7 @@ export const AppShell = ({
> >
<span className="font-medium">{item.label}</span> <span className="font-medium">{item.label}</span>
{item.badge ? <Badge tone="accent">{item.badge}</Badge> : null} {item.badge ? <Badge tone="accent">{item.badge}</Badge> : null}
</button> </Button>
))} ))}
</div> </div>
@ -129,7 +129,7 @@ export const AppShell = ({
<div className="sticky inset-x-0 top-0 z-40 -mx-3 -mt-4 border-b border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 backdrop-blur xl:hidden sm:-mx-4 md:-mx-6"> <div className="sticky inset-x-0 top-0 z-40 -mx-3 -mt-4 border-b border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 backdrop-blur xl:hidden sm:-mx-4 md:-mx-6">
<div className="flex gap-1 overflow-x-auto" style={{ WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}> <div className="flex gap-1 overflow-x-auto" style={{ WebkitOverflowScrolling: 'touch', scrollbarWidth: 'none' }}>
{navItems.map((item) => ( {navItems.map((item) => (
<button <Button
key={item.key} key={item.key}
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",
@ -144,7 +144,7 @@ export const AppShell = ({
{item.badge ? ( {item.badge ? (
<Badge tone={activeSection === item.key ? "neutral" : "accent"}>{item.badge}</Badge> <Badge tone={activeSection === item.key ? "neutral" : "accent"}>{item.badge}</Badge>
) : null} ) : null}
</button> </Button>
))} ))}
</div> </div>
</div> </div>

View File

@ -7,6 +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 {
@ -392,7 +393,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
@ -407,8 +408,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
@ -423,7 +424,7 @@ export const ClientDeliveryPage = () => {
}} }}
> >
🏪 Самовывоз 🏪 Самовывоз
</button> </Button>
</div> </div>
{activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && ( {activeTab === TAB_DELIVERY && !invitation?.deliveryAddress && !invitation?.customerAddress && (