Add refusals tab to the order dashboard
Orders whose Nova Poshta status is a refusal (102/103/105/108) now show only under «Отказы», whether or not they have a receipt. GET /orders takes tab=no_receipt|has_receipt|refused instead of has_receipt. NP statuses are also polled for orders that already have a receipt, until NP reports a final status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { apiFetch } from '@/api/client'
|
||||
import type { Order, OrderUpdate } from '@/features/orders/types'
|
||||
import type { Order, OrderTab, OrderUpdate } from '@/features/orders/types'
|
||||
|
||||
export function getOrders(hasReceipt: boolean): Promise<Order[]> {
|
||||
return apiFetch<Order[]>(`/orders?has_receipt=${hasReceipt}`)
|
||||
export function getOrders(tab: OrderTab): Promise<Order[]> {
|
||||
return apiFetch<Order[]>(`/orders?tab=${tab}`)
|
||||
}
|
||||
|
||||
export function deleteOrder(orderId: string): Promise<void> {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
import type { ReceiptStatus } from '@/features/receipts/types'
|
||||
|
||||
/** Вкладка дашборда (`OrderTab` в backend/app/services/orders.py). */
|
||||
export type OrderTab = 'no_receipt' | 'has_receipt' | 'refused'
|
||||
|
||||
export interface OrderGood {
|
||||
id: string
|
||||
sku: string
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { getOrders } from '@/api/orders'
|
||||
import type { OrderTab } from '@/features/orders/types'
|
||||
|
||||
/** Пока чек отправляется в Checkbox — опрашиваем часто, иначе статусы чеков обновляет worker раз в минуту. */
|
||||
const PENDING_POLL_MS = 3_000
|
||||
const RECEIPTS_POLL_MS = 30_000
|
||||
|
||||
export function useOrders(hasReceipt: boolean) {
|
||||
export function useOrders(tab: OrderTab) {
|
||||
return useQuery({
|
||||
queryKey: ['orders', hasReceipt],
|
||||
queryFn: () => getOrders(hasReceipt),
|
||||
queryKey: ['orders', tab],
|
||||
queryFn: () => getOrders(tab),
|
||||
refetchInterval: (query) => {
|
||||
if (query.state.data?.some((order) => order.receipt_status === 'pending')) return PENDING_POLL_MS
|
||||
return hasReceipt ? RECEIPTS_POLL_MS : false
|
||||
return tab === 'has_receipt' ? RECEIPTS_POLL_MS : false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cancelReceipt, createReceipts } from '@/api/receipts'
|
||||
import '@/pages/DashboardPage.css'
|
||||
import { useAuth } from '@/features/auth/useAuth'
|
||||
import { OrderDetailModal } from '@/features/orders/OrderDetailModal'
|
||||
import type { Order } from '@/features/orders/types'
|
||||
import type { Order, OrderTab } from '@/features/orders/types'
|
||||
import { useOrders } from '@/features/orders/useOrders'
|
||||
import { defaultPrepayment, prepaymentMatches, toKopecks } from '@/features/receipts/money'
|
||||
import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types'
|
||||
@@ -19,11 +19,10 @@ const ROLE_LABEL: Record<string, string> = {
|
||||
viewer: 'Наблюдатель',
|
||||
}
|
||||
|
||||
type Tab = 'no_receipt' | 'has_receipt'
|
||||
|
||||
const TABS: { key: Tab; label: string }[] = [
|
||||
const TABS: { key: OrderTab; label: string }[] = [
|
||||
{ key: 'no_receipt', label: 'Без чека' },
|
||||
{ key: 'has_receipt', label: 'Выписаны чеки' },
|
||||
{ key: 'refused', label: 'Отказы' },
|
||||
]
|
||||
|
||||
function npStatusTone(order: Order): 'delivered' | 'processing' | 'danger' | 'new' {
|
||||
@@ -47,8 +46,8 @@ function paymentBadge(order: Order): { label: string; tone: 'delivered' | 'proce
|
||||
|
||||
export function DashboardPage() {
|
||||
const { user, logout } = useAuth()
|
||||
const [tab, setTab] = useState<Tab>('no_receipt')
|
||||
const { data: orders, isLoading, isError } = useOrders(tab === 'has_receipt')
|
||||
const [tab, setTab] = useState<OrderTab>('no_receipt')
|
||||
const { data: orders, isLoading, isError } = useOrders(tab)
|
||||
const queryClient = useQueryClient()
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -103,7 +102,8 @@ export function DashboardPage() {
|
||||
|
||||
async function handleCancel(order: Order) {
|
||||
if (!order.receipt_id) return
|
||||
if (!window.confirm(`Отменить ЕТТН-чек по заказу ${order.id}? Заказ вернётся в очередь.`)) return
|
||||
const outcome = tab === 'refused' ? 'Заказ останется в отказах.' : 'Заказ вернётся в очередь.'
|
||||
if (!window.confirm(`Отменить ЕТТН-чек по заказу ${order.id}? ${outcome}`)) return
|
||||
setCancellingId(order.id)
|
||||
try {
|
||||
await cancelReceipt(order.receipt_id)
|
||||
@@ -336,7 +336,7 @@ export function DashboardPage() {
|
||||
Чек
|
||||
</button>
|
||||
)}
|
||||
{order.receipt_status && (tab === 'has_receipt' || !CANCELLABLE.has(order.receipt_status)) && (
|
||||
{order.receipt_status && (tab !== 'no_receipt' || !CANCELLABLE.has(order.receipt_status)) && (
|
||||
<span
|
||||
className={`orders-status orders-status--${RECEIPT_STATUS[order.receipt_status].tone}`}
|
||||
title={order.receipt_error ?? undefined}
|
||||
@@ -344,7 +344,7 @@ export function DashboardPage() {
|
||||
{RECEIPT_STATUS[order.receipt_status].label}
|
||||
</span>
|
||||
)}
|
||||
{tab === 'has_receipt' && canFiscalize && order.receipt_status && CANCELLABLE.has(order.receipt_status) && (
|
||||
{tab !== 'no_receipt' && canFiscalize && order.receipt_status && CANCELLABLE.has(order.receipt_status) && (
|
||||
<button
|
||||
type="button"
|
||||
className="orders-delete-btn"
|
||||
@@ -354,7 +354,7 @@ export function DashboardPage() {
|
||||
Отменить
|
||||
</button>
|
||||
)}
|
||||
{canDelete && tab === 'no_receipt' && (
|
||||
{canDelete && (tab === 'no_receipt' || (tab === 'refused' && !order.has_receipt)) && (
|
||||
<button
|
||||
type="button"
|
||||
className="orders-delete-btn"
|
||||
|
||||
Reference in New Issue
Block a user