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
+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>