Add COD-in-transit summary and received tab to the dashboard

- GET /orders/summary: count and sum of cash-on-delivery for parcels not
  yet picked up (excludes received, refused, deleted/unknown TTNs, paid COD);
  shown as a card above the tabs.
- New «Полученные» tab: orders with NP received codes (9, 10, 11, 106) move
  there automatically, regardless of receipt; 106 is now a final status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 01:23:39 +03:00
co-authored by Claude Opus 5.5
parent 528a869657
commit a6072800b2
11 changed files with 202 additions and 19 deletions
+5 -1
View File
@@ -1,10 +1,14 @@
import { apiFetch } from '@/api/client'
import type { Order, OrderTab, OrderUpdate } from '@/features/orders/types'
import type { Order, OrdersSummary, OrderTab, OrderUpdate } from '@/features/orders/types'
export function getOrders(tab: OrderTab): Promise<Order[]> {
return apiFetch<Order[]>(`/orders?tab=${tab}`)
}
export function getOrdersSummary(): Promise<OrdersSummary> {
return apiFetch<OrdersSummary>('/orders/summary')
}
export function deleteOrder(orderId: string): Promise<void> {
return apiFetch<void>(`/orders/${orderId}`, { method: 'DELETE' })
}
+7 -1
View File
@@ -6,7 +6,7 @@
import type { ReceiptStatus } from '@/features/receipts/types'
/** Вкладка дашборда (`OrderTab` в backend/app/services/orders.py). */
export type OrderTab = 'no_receipt' | 'has_receipt' | 'refused'
export type OrderTab = 'no_receipt' | 'has_receipt' | 'received' | 'refused'
export interface OrderGood {
id: string
@@ -62,3 +62,9 @@ export interface OrderUpdate {
total_amount: string
goods: OrderGoodUpdate[]
}
/** Ответ `GET /orders/summary` (`OrdersSummaryOut`). */
export interface OrdersSummary {
cod_in_transit_count: number
cod_in_transit_amount: string
}
+12 -1
View File
@@ -1,11 +1,13 @@
import { useQuery } from '@tanstack/react-query'
import { getOrders } from '@/api/orders'
import { getOrders, getOrdersSummary } from '@/api/orders'
import type { OrderTab } from '@/features/orders/types'
/** Пока чек отправляется в Checkbox — опрашиваем часто, иначе статусы чеков обновляет worker раз в минуту. */
const PENDING_POLL_MS = 3_000
const RECEIPTS_POLL_MS = 30_000
// Статусы NP обновляет worker раз в минуту — чаще сводку опрашивать незачем.
const SUMMARY_POLL_MS = 60_000
export function useOrders(tab: OrderTab) {
return useQuery({
@@ -17,3 +19,12 @@ export function useOrders(tab: OrderTab) {
},
})
}
export function useOrdersSummary() {
return useQuery({
// Под ключом ['orders', ...] — чтобы invalidateQueries(['orders']) обновлял и сводку.
queryKey: ['orders', 'summary'],
queryFn: getOrdersSummary,
refetchInterval: SUMMARY_POLL_MS,
})
}
+33
View File
@@ -65,6 +65,39 @@
justify-content: space-between;
}
.orders-summary {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.orders-summary-card {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 220px;
padding: 14px 18px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 10px;
}
.orders-summary-label {
font-size: 13px;
color: var(--color-text-muted);
}
.orders-summary-value {
font-size: 22px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.orders-summary-hint {
font-size: 12px;
color: var(--color-text-muted);
}
.orders-create-btn {
border: none;
background: var(--color-primary);
+28 -3
View File
@@ -8,7 +8,7 @@ import '@/pages/DashboardPage.css'
import { useAuth } from '@/features/auth/useAuth'
import { OrderDetailModal } from '@/features/orders/OrderDetailModal'
import type { Order, OrderTab } from '@/features/orders/types'
import { useOrders } from '@/features/orders/useOrders'
import { useOrders, useOrdersSummary } from '@/features/orders/useOrders'
import { defaultPrepayment, prepaymentMatches, toKopecks } from '@/features/receipts/money'
import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types'
import type { ReceiptRequestItem } from '@/features/receipts/types'
@@ -22,6 +22,7 @@ const ROLE_LABEL: Record<string, string> = {
const TABS: { key: OrderTab; label: string }[] = [
{ key: 'no_receipt', label: 'Без чека' },
{ key: 'has_receipt', label: 'Выписаны чеки' },
{ key: 'received', label: 'Полученные' },
{ key: 'refused', label: 'Отказы' },
]
@@ -44,10 +45,17 @@ function paymentBadge(order: Order): { label: string; tone: 'delivered' | 'proce
return null
}
const MONEY_FORMAT = new Intl.NumberFormat('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
function formatMoney(amount: string): string {
return MONEY_FORMAT.format(Number(amount))
}
export function DashboardPage() {
const { user, logout } = useAuth()
const [tab, setTab] = useState<OrderTab>('no_receipt')
const { data: orders, isLoading, isError } = useOrders(tab)
const { data: summary } = useOrdersSummary()
const queryClient = useQueryClient()
const [selected, setSelected] = useState<Set<string>>(new Set())
const [search, setSearch] = useState('')
@@ -102,7 +110,12 @@ export function DashboardPage() {
async function handleCancel(order: Order) {
if (!order.receipt_id) return
const outcome = tab === 'refused' ? 'Заказ останется в отказах.' : 'Заказ вернётся в очередь.'
const outcome = {
no_receipt: 'Заказ вернётся в очередь.',
has_receipt: 'Заказ вернётся в очередь.',
received: 'Заказ останется в полученных.',
refused: 'Заказ останется в отказах.',
}[tab]
if (!window.confirm(`Отменить ЕТТН-чек по заказу ${order.id}? ${outcome}`)) return
setCancellingId(order.id)
try {
@@ -193,6 +206,18 @@ export function DashboardPage() {
</div>
</div>
<div className="orders-summary">
<div className="orders-summary-card" title="Посылки с наложкой, которые ещё не забрали и по которым нет отказа">
<span className="orders-summary-label">Наложка в пути</span>
<span className="orders-summary-value">
{summary ? `${formatMoney(summary.cod_in_transit_amount)} ₴` : '—'}
</span>
<span className="orders-summary-hint">
{summary ? `${summary.cod_in_transit_count} посылок` : ' '}
</span>
</div>
</div>
{notice && (
<div className={`orders-notice orders-notice--${notice.tone}`}>
<ul>
@@ -354,7 +379,7 @@ export function DashboardPage() {
Отменить
</button>
)}
{canDelete && (tab === 'no_receipt' || (tab === 'refused' && !order.has_receipt)) && (
{canDelete && (tab === 'no_receipt' || ((tab === 'refused' || tab === 'received') && !order.has_receipt)) && (
<button
type="button"
className="orders-delete-btn"