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:
parent
d28e00782b
commit
8d3d8dc0fe
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)]">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)]">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue