Add orders table mockup to post-login dashboard

Static layout per design mock: order queue table with selection,
search, prepayment inputs, status badges, and bulk-receipt button.
Uses mock data only — no API wiring, per project rule to not build
order features against a guessed CRM shape ahead of stage 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 19:54:55 +03:00
co-authored by Claude Sonnet 5
parent 2664cb8213
commit ef55689295
2 changed files with 316 additions and 17 deletions
+179 -8
View File
@@ -51,21 +51,192 @@
color: var(--color-primary);
}
.dashboard-body {
.orders-body {
flex: 1;
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.orders-toolbar {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
justify-content: space-between;
}
.dashboard-placeholder {
text-align: center;
max-width: 420px;
.orders-create-btn {
border: none;
background: var(--color-primary);
color: #fff;
border-radius: 8px;
padding: 10px 16px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.orders-create-btn:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.orders-create-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.orders-filterbar {
display: flex;
align-items: center;
gap: 12px;
}
.orders-selected-count {
font-size: 13px;
color: var(--color-text-muted);
white-space: nowrap;
}
.dashboard-placeholder h2 {
.orders-search {
flex: 1;
max-width: 320px;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
margin-bottom: 8px;
font-size: 14px;
}
.orders-filter-btn {
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
cursor: pointer;
}
.orders-filter-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.orders-table-wrap {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
box-shadow: var(--shadow-card);
overflow-x: auto;
}
.orders-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.orders-table th,
.orders-table td {
padding: 12px 14px;
text-align: left;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.orders-table th {
color: var(--color-text-muted);
font-weight: 600;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.orders-table tbody tr:last-child td {
border-bottom: none;
}
.orders-table tbody tr:hover {
background: var(--color-bg);
}
.orders-checkbox-col {
width: 36px;
}
.orders-prepayment-input {
width: 64px;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
font-size: 13px;
}
.orders-status {
display: inline-block;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
}
.orders-status--processing {
background: #fef3c7;
color: #92400e;
}
.orders-status--new {
background: #dbeafe;
color: #1e40af;
}
.orders-status--delivered {
background: #dcfce7;
color: #166534;
}
@media (prefers-color-scheme: dark) {
.orders-status--processing {
background: rgba(217, 119, 6, 0.18);
color: #fbbf24;
}
.orders-status--new {
background: rgba(59, 130, 246, 0.18);
color: #93c5fd;
}
.orders-status--delivered {
background: rgba(22, 163, 74, 0.18);
color: #86efac;
}
}
.orders-receipt-btn {
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text);
border-radius: 6px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
}
.orders-receipt-btn:hover:not(:disabled) {
border-color: var(--color-primary);
color: var(--color-primary);
}
.orders-receipt-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.orders-empty {
text-align: center;
color: var(--color-text-muted);
padding: 32px 0;
}
+137 -9
View File
@@ -1,3 +1,5 @@
import { useMemo, useState } from 'react'
import '@/pages/DashboardPage.css'
import { useAuth } from '@/features/auth/useAuth'
@@ -7,12 +9,68 @@ const ROLE_LABEL: Record<string, string> = {
viewer: 'Наблюдатель',
}
type OrderStatus = 'processing' | 'new' | 'delivered'
const STATUS_LABEL: Record<OrderStatus, string> = {
processing: 'Обработка',
new: 'Новый',
delivered: 'Доставлен',
}
interface OrderRow {
id: string
ttn: string
client: string
totalLabel: string
prepayment: string
status: OrderStatus
fiscalized: boolean
}
/**
* Временная заглушка. Очередь «получено, чек не пробит» — этап 6 плана,
* пока здесь только подтверждение, что вход и защищённый роут работают.
* Вёрстка макета из ТЗ — статичные данные, реальная очередь «доставлено, чек
* не пробит» приедет на этапе 3+ вместе с CrmClient/StubCrmClient и заказами
* из БД. Пока не подключаем это к API, чтобы не гадать форму данных CRM.
*/
const MOCK_ORDERS: OrderRow[] = [
{ id: '#ORD-24810', ttn: '20400123456', client: 'Иван Смирнов', totalLabel: '45 990 ₴', prepayment: '', status: 'processing', fiscalized: false },
{ id: '#ORD-24809', ttn: '20400123457', client: 'Ольга Ткаченко', totalLabel: '12 450 ₴', prepayment: '0%', status: 'new', fiscalized: false },
{ id: '#ORD-24808', ttn: '20400123458', client: 'Иван Смирнов', totalLabel: '45 990 ₴', prepayment: '15%', status: 'new', fiscalized: false },
{ id: '#ORD-24807', ttn: '20400123459', client: 'Марія Бondar', totalLabel: '8 200 ₴', prepayment: '20%', status: 'delivered', fiscalized: false },
{ id: '#ORD-24806', ttn: '20400123460', client: 'Петро Коваль', totalLabel: '31 000 ₴', prepayment: '0%', status: 'delivered', fiscalized: false },
{ id: '#ORD-24805', ttn: '20400123461', client: 'Іван Смирнов', totalLabel: '45 990 ₴', prepayment: '0%', status: 'delivered', fiscalized: false },
]
export function DashboardPage() {
const { user, logout } = useAuth()
const [selected, setSelected] = useState<Set<string>>(new Set())
const [search, setSearch] = useState('')
const filtered = useMemo(() => {
const query = search.trim().toLowerCase()
if (!query) return MOCK_ORDERS
return MOCK_ORDERS.filter(
(order) =>
order.id.toLowerCase().includes(query) ||
order.ttn.includes(query) ||
order.client.toLowerCase().includes(query),
)
}, [search])
const allSelected = filtered.length > 0 && filtered.every((order) => selected.has(order.id))
function toggleAll() {
setSelected(allSelected ? new Set() : new Set(filtered.map((order) => order.id)))
}
function toggleOne(id: string) {
setSelected((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
return (
<div className="dashboard-shell">
@@ -27,13 +85,83 @@ export function DashboardPage() {
</div>
</header>
<main className="dashboard-body">
<div className="dashboard-placeholder">
<h2>Очередь заказов появится здесь</h2>
<p>
Вход выполнен успешно. Экран «получено, чек не пробит» будет добавлен на следующих
этапах — после интеграций с Новой Поштой и Checkbox.
</p>
<main className="orders-body">
<div className="orders-toolbar">
<h2>Заказы</h2>
<button type="button" className="orders-create-btn" disabled={selected.size === 0}>
Создать чеки по выбранным ({selected.size})
</button>
</div>
<div className="orders-filterbar">
<span className="orders-selected-count">Выбрано: {selected.size} заказов</span>
<input
type="search"
className="orders-search"
placeholder="Поиск по ID, ТТН, клиенту"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button type="button" className="orders-filter-btn">
Фильтр
</button>
</div>
<div className="orders-table-wrap">
<table className="orders-table">
<thead>
<tr>
<th className="orders-checkbox-col">
<input type="checkbox" checked={allSelected} onChange={toggleAll} aria-label="Выбрать всё" />
</th>
<th>ID заказа</th>
<th>Номер ТТН</th>
<th>Клиент</th>
<th>Сумма</th>
<th>Предоплата</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{filtered.map((order) => (
<tr key={order.id}>
<td>
<input
type="checkbox"
checked={selected.has(order.id)}
onChange={() => toggleOne(order.id)}
aria-label={`Выбрать заказ ${order.id}`}
/>
</td>
<td>{order.id}</td>
<td>{order.ttn}</td>
<td>{order.client}</td>
<td>{order.totalLabel}</td>
<td>
<input type="text" className="orders-prepayment-input" defaultValue={order.prepayment} placeholder="0%" />
</td>
<td>
<span className={`orders-status orders-status--${order.status}`}>
{STATUS_LABEL[order.status]}
</span>
</td>
<td>
<button type="button" className="orders-receipt-btn" disabled={order.status !== 'delivered'}>
Чек
</button>
</td>
</tr>
))}
{filtered.length === 0 && (
<tr>
<td colSpan={8} className="orders-empty">
Ничего не найдено
</td>
</tr>
)}
</tbody>
</table>
</div>
</main>
</div>