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:
@@ -118,7 +118,7 @@ Not yet enforced by types anywhere in the current code, but is a hard project co
|
|||||||
|
|
||||||
## Project status (see the plan for the full roadmap)
|
## Project status (see the plan for the full roadmap)
|
||||||
|
|
||||||
Stages 1–4 (scaffolding, auth/audit, CRM order queue, Nova Poshta tracking) are done. Checkbox ETTN receipts are implemented per `.plans/checkbox-ettn-receipts.md` but not yet verified on a real cash register.
|
Stages 1–4 (scaffolding, auth/audit, CRM order queue, Nova Poshta tracking) are done. The dashboard has three tabs (`GET /orders?tab=no_receipt|has_receipt|refused`, `services/orders.OrderTab`); an order whose NP status code is a refusal (`NP_REFUSAL_STATUS_CODES`) shows only under «Отказы», whether or not it has a receipt. Checkbox ETTN receipts are implemented per `.plans/checkbox-ettn-receipts.md` but not yet verified on a real cash register.
|
||||||
|
|
||||||
### Checkbox ETTN receipts
|
### Checkbox ETTN receipts
|
||||||
|
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ from app.schemas.orders import OrderRowOut, OrderUpdateIn
|
|||||||
from app.services import audit
|
from app.services import audit
|
||||||
from app.services import orders as orders_service
|
from app.services import orders as orders_service
|
||||||
from app.services import receipts as receipts_service
|
from app.services import receipts as receipts_service
|
||||||
|
from app.services.orders import OrderTab
|
||||||
|
|
||||||
router = APIRouter(prefix="/orders", tags=["orders"], dependencies=[Depends(require_any)])
|
router = APIRouter(prefix="/orders", tags=["orders"], dependencies=[Depends(require_any)])
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[OrderRowOut], response_model_by_alias=False)
|
@router.get("", response_model=list[OrderRowOut], response_model_by_alias=False)
|
||||||
async def list_orders(
|
async def list_orders(
|
||||||
session: SessionDep, crm: CrmClientDep, has_receipt: bool = False
|
session: SessionDep, crm: CrmClientDep, tab: OrderTab = OrderTab.NO_RECEIPT
|
||||||
) -> list[OrderRowOut]:
|
) -> list[OrderRowOut]:
|
||||||
# by_alias=False: без этого FastAPI сериализует вложенные OrderGoodOut через
|
# by_alias=False: без этого FastAPI сериализует вложенные OrderGoodOut через
|
||||||
# CRM-алиасы (SKU/Name/...) вместо snake_case полей, которые ждёт фронтенд.
|
# CRM-алиасы (SKU/Name/...) вместо snake_case полей, которые ждёт фронтенд.
|
||||||
await orders_service.sync_orders_from_crm(session, crm)
|
await orders_service.sync_orders_from_crm(session, crm)
|
||||||
orders = await orders_service.list_orders(session, has_receipt=has_receipt)
|
orders = await orders_service.list_orders(session, tab=tab)
|
||||||
receipts = await receipts_service.latest_receipts_by_order(
|
receipts = await receipts_service.latest_receipts_by_order(
|
||||||
session, [order.id for order in orders]
|
session, [order.id for order in orders]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from decimal import ROUND_HALF_UP, Decimal
|
from decimal import ROUND_HALF_UP, Decimal
|
||||||
@@ -20,6 +21,14 @@ _CRM_STATUS = "APPROVED"
|
|||||||
# NP отклоняет запросы с более чем 100 накладными за раз (см. np_client.py).
|
# NP отклоняет запросы с более чем 100 накладными за раз (см. np_client.py).
|
||||||
_NP_BATCH_SIZE = 100
|
_NP_BATCH_SIZE = 100
|
||||||
|
|
||||||
|
# Коды NP "відмова одержувача" / "припинено зберігання" — клиент не забрал
|
||||||
|
# посылку, заказ уходит во вкладку «Отказы».
|
||||||
|
NP_REFUSAL_STATUS_CODES = ("102", "103", "105", "108")
|
||||||
|
|
||||||
|
# Коды NP "відправлення отримано" и отказы — после них статус ТТН больше не
|
||||||
|
# опрашивается.
|
||||||
|
_NP_FINAL_STATUS_CODES = ("9", "10", "11", *NP_REFUSAL_STATUS_CODES)
|
||||||
|
|
||||||
# Поля, которые кассир может править в карточке заказа (кроме goods/total).
|
# Поля, которые кассир может править в карточке заказа (кроме goods/total).
|
||||||
_EDITABLE_FIELDS = (
|
_EDITABLE_FIELDS = (
|
||||||
"recipient_name",
|
"recipient_name",
|
||||||
@@ -30,6 +39,12 @@ _EDITABLE_FIELDS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTab(enum.StrEnum):
|
||||||
|
NO_RECEIPT = "no_receipt"
|
||||||
|
HAS_RECEIPT = "has_receipt"
|
||||||
|
REFUSED = "refused"
|
||||||
|
|
||||||
|
|
||||||
class OrderEditError(Exception):
|
class OrderEditError(Exception):
|
||||||
"""Заказ нельзя сохранить — сообщение показывается кассиру."""
|
"""Заказ нельзя сохранить — сообщение показывается кассиру."""
|
||||||
|
|
||||||
@@ -80,21 +95,31 @@ async def sync_orders_from_crm(session: AsyncSession, crm: CrmClient) -> None:
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
async def list_orders(session: AsyncSession, *, has_receipt: bool) -> list[Order]:
|
async def list_orders(session: AsyncSession, *, tab: OrderTab) -> list[Order]:
|
||||||
receipt_filter = (
|
"""Заказы вкладки. Отказ клиента важнее наличия чека: такой заказ показывается
|
||||||
Order.receipt_created_at.is_not(None) if has_receipt else Order.receipt_created_at.is_(None)
|
только в «Отказах», даже если по нему уже создан ЕТТН-чек."""
|
||||||
)
|
refused = Order.np_status_code.in_(NP_REFUSAL_STATUS_CODES)
|
||||||
|
not_refused = Order.np_status_code.is_(None) | ~refused
|
||||||
|
tab_filter = {
|
||||||
|
OrderTab.REFUSED: refused,
|
||||||
|
OrderTab.NO_RECEIPT: Order.receipt_created_at.is_(None) & not_refused,
|
||||||
|
OrderTab.HAS_RECEIPT: Order.receipt_created_at.is_not(None) & not_refused,
|
||||||
|
}[tab]
|
||||||
result = await session.scalars(
|
result = await session.scalars(
|
||||||
select(Order)
|
select(Order)
|
||||||
.where(Order.is_deleted.is_(False))
|
.where(Order.is_deleted.is_(False))
|
||||||
.where(receipt_filter)
|
.where(tab_filter)
|
||||||
.order_by(Order.create_date_time.desc())
|
.order_by(Order.create_date_time.desc())
|
||||||
)
|
)
|
||||||
return list(result)
|
return list(result)
|
||||||
|
|
||||||
|
|
||||||
async def sync_np_statuses(session: AsyncSession, np: NovaPoshtaClient) -> None:
|
async def sync_np_statuses(session: AsyncSession, np: NovaPoshtaClient) -> None:
|
||||||
"""Обновляет статус ТТН и сумму наложенного платежа для заказов без чека.
|
"""Обновляет статус ТТН, сумму и статус оплаты наложки по заказам в пути.
|
||||||
|
|
||||||
|
Опрашиваются и заказы с уже созданным ЕТТН-чеком: именно после создания
|
||||||
|
чека посылка доезжает и оплачивается, и этот переход кассир должен видеть.
|
||||||
|
Опрос прекращается, когда NP вернул финальный статус "отримано" или отказ.
|
||||||
|
|
||||||
Вызывается ARQ worker'ом раз в минуту (см. `app/worker.py`), а не из
|
Вызывается ARQ worker'ом раз в минуту (см. `app/worker.py`), а не из
|
||||||
HTTP-запроса: опрос статусов не должен зависеть от того, открыт ли сейчас
|
HTTP-запроса: опрос статусов не должен зависеть от того, открыт ли сейчас
|
||||||
@@ -103,8 +128,11 @@ async def sync_np_statuses(session: AsyncSession, np: NovaPoshtaClient) -> None:
|
|||||||
orders = await session.scalars(
|
orders = await session.scalars(
|
||||||
select(Order)
|
select(Order)
|
||||||
.where(Order.is_deleted.is_(False))
|
.where(Order.is_deleted.is_(False))
|
||||||
.where(Order.receipt_created_at.is_(None))
|
|
||||||
.where(Order.waybill_number.is_not(None))
|
.where(Order.waybill_number.is_not(None))
|
||||||
|
.where(
|
||||||
|
Order.np_status_code.is_(None)
|
||||||
|
| Order.np_status_code.not_in(_NP_FINAL_STATUS_CODES)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
orders_by_waybill: dict[str, Order] = {order.waybill_number: order for order in orders}
|
orders_by_waybill: dict[str, Order] = {order.waybill_number: order for order in orders}
|
||||||
if not orders_by_waybill:
|
if not orders_by_waybill:
|
||||||
|
|||||||
@@ -69,8 +69,8 @@ def _patch_orders_service(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||||||
async def fake_sync(session: object, crm: object) -> None:
|
async def fake_sync(session: object, crm: object) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def fake_list(session: object, *, has_receipt: bool) -> list[Order]:
|
async def fake_list(session: object, *, tab: str) -> list[Order]:
|
||||||
return [] if has_receipt else [_order()]
|
return [_order()] if tab == "no_receipt" else []
|
||||||
|
|
||||||
async def fake_delete(session: object, order_id: str) -> Order | None:
|
async def fake_delete(session: object, order_id: str) -> Order | None:
|
||||||
return _order(order_id) if order_id == "1" else None
|
return _order(order_id) if order_id == "1" else None
|
||||||
@@ -132,7 +132,7 @@ class TestListOrders:
|
|||||||
) -> None:
|
) -> None:
|
||||||
app.dependency_overrides[get_current_user] = lambda: _user(role)
|
app.dependency_overrides[get_current_user] = lambda: _user(role)
|
||||||
|
|
||||||
response = client.get("/api/v1/orders", params={"has_receipt": "false"})
|
response = client.get("/api/v1/orders", params={"tab": "no_receipt"})
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
body = response.json()
|
body = response.json()
|
||||||
@@ -142,14 +142,22 @@ class TestListOrders:
|
|||||||
assert body[0]["has_receipt"] is False
|
assert body[0]["has_receipt"] is False
|
||||||
assert body[0]["goods"][0]["sku"] == "SKU-1"
|
assert body[0]["goods"][0]["sku"] == "SKU-1"
|
||||||
|
|
||||||
def test_has_receipt_tab_is_empty_for_now(self, client: TestClient) -> None:
|
@pytest.mark.parametrize("tab", ["has_receipt", "refused"])
|
||||||
|
def test_other_tabs_are_passed_to_service(self, client: TestClient, tab: str) -> None:
|
||||||
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
|
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
|
||||||
|
|
||||||
response = client.get("/api/v1/orders", params={"has_receipt": "true"})
|
response = client.get("/api/v1/orders", params={"tab": tab})
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == []
|
assert response.json() == []
|
||||||
|
|
||||||
|
def test_unknown_tab_is_rejected(self, client: TestClient) -> None:
|
||||||
|
app.dependency_overrides[get_current_user] = lambda: _user(UserRole.CASHIER)
|
||||||
|
|
||||||
|
response = client.get("/api/v1/orders", params={"tab": "bogus"})
|
||||||
|
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
def test_requires_authentication(self, client: TestClient) -> None:
|
def test_requires_authentication(self, client: TestClient) -> None:
|
||||||
response = client.get("/api/v1/orders")
|
response = client.get("/api/v1/orders")
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|||||||
@@ -7,10 +7,18 @@ from datetime import UTC, datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
from app.db.models.order import Order
|
from app.db.models.order import Order
|
||||||
from app.schemas.orders import OrderUpdateIn
|
from app.schemas.orders import OrderUpdateIn
|
||||||
from app.services.orders import OrderEditError, _parse_crm_datetime, _to_kopecks, update_order
|
from app.services.orders import (
|
||||||
|
OrderEditError,
|
||||||
|
OrderTab,
|
||||||
|
_parse_crm_datetime,
|
||||||
|
_to_kopecks,
|
||||||
|
list_orders,
|
||||||
|
update_order,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestToKopecks:
|
class TestToKopecks:
|
||||||
@@ -148,3 +156,37 @@ class TestUpdateOrder:
|
|||||||
assert asyncio.run(update_order(_FakeSession(None), "1", payload)) is None # type: ignore[arg-type]
|
assert asyncio.run(update_order(_FakeSession(None), "1", payload)) is None # type: ignore[arg-type]
|
||||||
deleted = _FakeSession(_order(is_deleted=True))
|
deleted = _FakeSession(_order(is_deleted=True))
|
||||||
assert asyncio.run(update_order(deleted, "1", payload)) is None # type: ignore[arg-type]
|
assert asyncio.run(update_order(deleted, "1", payload)) is None # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
class _CapturingSession:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.statement: Any = None
|
||||||
|
|
||||||
|
async def scalars(self, statement: Any) -> list[Order]:
|
||||||
|
self.statement = statement
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _list_where(tab: OrderTab) -> str:
|
||||||
|
"""WHERE-часть запроса вкладки (SELECT перечисляет все колонки, в т.ч. receipt_created_at)."""
|
||||||
|
session = _CapturingSession()
|
||||||
|
asyncio.run(list_orders(session, tab=tab)) # type: ignore[arg-type]
|
||||||
|
sql = str(
|
||||||
|
session.statement.compile(
|
||||||
|
dialect=postgresql.dialect(), compile_kwargs={"literal_binds": True}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sql.split("WHERE", 1)[1]
|
||||||
|
|
||||||
|
|
||||||
|
class TestListOrders:
|
||||||
|
def test_refused_tab_filters_by_refusal_codes_only(self) -> None:
|
||||||
|
sql = _list_where(OrderTab.REFUSED)
|
||||||
|
assert "orders.np_status_code IN ('102', '103', '105', '108')" in sql
|
||||||
|
assert "receipt_created_at" not in sql
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("tab", [OrderTab.NO_RECEIPT, OrderTab.HAS_RECEIPT])
|
||||||
|
def test_other_tabs_exclude_refusals(self, tab: OrderTab) -> None:
|
||||||
|
sql = _list_where(tab)
|
||||||
|
assert "NOT IN ('102', '103', '105', '108')" in sql
|
||||||
|
assert "receipt_created_at" in sql
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { apiFetch } from '@/api/client'
|
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[]> {
|
export function getOrders(tab: OrderTab): Promise<Order[]> {
|
||||||
return apiFetch<Order[]>(`/orders?has_receipt=${hasReceipt}`)
|
return apiFetch<Order[]>(`/orders?tab=${tab}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteOrder(orderId: string): Promise<void> {
|
export function deleteOrder(orderId: string): Promise<void> {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
import type { ReceiptStatus } from '@/features/receipts/types'
|
import type { ReceiptStatus } from '@/features/receipts/types'
|
||||||
|
|
||||||
|
/** Вкладка дашборда (`OrderTab` в backend/app/services/orders.py). */
|
||||||
|
export type OrderTab = 'no_receipt' | 'has_receipt' | 'refused'
|
||||||
|
|
||||||
export interface OrderGood {
|
export interface OrderGood {
|
||||||
id: string
|
id: string
|
||||||
sku: string
|
sku: string
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
|
||||||
import { getOrders } from '@/api/orders'
|
import { getOrders } from '@/api/orders'
|
||||||
|
import type { OrderTab } from '@/features/orders/types'
|
||||||
|
|
||||||
/** Пока чек отправляется в Checkbox — опрашиваем часто, иначе статусы чеков обновляет worker раз в минуту. */
|
/** Пока чек отправляется в Checkbox — опрашиваем часто, иначе статусы чеков обновляет worker раз в минуту. */
|
||||||
const PENDING_POLL_MS = 3_000
|
const PENDING_POLL_MS = 3_000
|
||||||
const RECEIPTS_POLL_MS = 30_000
|
const RECEIPTS_POLL_MS = 30_000
|
||||||
|
|
||||||
export function useOrders(hasReceipt: boolean) {
|
export function useOrders(tab: OrderTab) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['orders', hasReceipt],
|
queryKey: ['orders', tab],
|
||||||
queryFn: () => getOrders(hasReceipt),
|
queryFn: () => getOrders(tab),
|
||||||
refetchInterval: (query) => {
|
refetchInterval: (query) => {
|
||||||
if (query.state.data?.some((order) => order.receipt_status === 'pending')) return PENDING_POLL_MS
|
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 '@/pages/DashboardPage.css'
|
||||||
import { useAuth } from '@/features/auth/useAuth'
|
import { useAuth } from '@/features/auth/useAuth'
|
||||||
import { OrderDetailModal } from '@/features/orders/OrderDetailModal'
|
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 { useOrders } from '@/features/orders/useOrders'
|
||||||
import { defaultPrepayment, prepaymentMatches, toKopecks } from '@/features/receipts/money'
|
import { defaultPrepayment, prepaymentMatches, toKopecks } from '@/features/receipts/money'
|
||||||
import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types'
|
import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types'
|
||||||
@@ -19,11 +19,10 @@ const ROLE_LABEL: Record<string, string> = {
|
|||||||
viewer: 'Наблюдатель',
|
viewer: 'Наблюдатель',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = 'no_receipt' | 'has_receipt'
|
const TABS: { key: OrderTab; label: string }[] = [
|
||||||
|
|
||||||
const TABS: { key: Tab; label: string }[] = [
|
|
||||||
{ key: 'no_receipt', label: 'Без чека' },
|
{ key: 'no_receipt', label: 'Без чека' },
|
||||||
{ key: 'has_receipt', label: 'Выписаны чеки' },
|
{ key: 'has_receipt', label: 'Выписаны чеки' },
|
||||||
|
{ key: 'refused', label: 'Отказы' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function npStatusTone(order: Order): 'delivered' | 'processing' | 'danger' | 'new' {
|
function npStatusTone(order: Order): 'delivered' | 'processing' | 'danger' | 'new' {
|
||||||
@@ -47,8 +46,8 @@ function paymentBadge(order: Order): { label: string; tone: 'delivered' | 'proce
|
|||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const [tab, setTab] = useState<Tab>('no_receipt')
|
const [tab, setTab] = useState<OrderTab>('no_receipt')
|
||||||
const { data: orders, isLoading, isError } = useOrders(tab === 'has_receipt')
|
const { data: orders, isLoading, isError } = useOrders(tab)
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
@@ -103,7 +102,8 @@ export function DashboardPage() {
|
|||||||
|
|
||||||
async function handleCancel(order: Order) {
|
async function handleCancel(order: Order) {
|
||||||
if (!order.receipt_id) return
|
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)
|
setCancellingId(order.id)
|
||||||
try {
|
try {
|
||||||
await cancelReceipt(order.receipt_id)
|
await cancelReceipt(order.receipt_id)
|
||||||
@@ -336,7 +336,7 @@ export function DashboardPage() {
|
|||||||
Чек
|
Чек
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{order.receipt_status && (tab === 'has_receipt' || !CANCELLABLE.has(order.receipt_status)) && (
|
{order.receipt_status && (tab !== 'no_receipt' || !CANCELLABLE.has(order.receipt_status)) && (
|
||||||
<span
|
<span
|
||||||
className={`orders-status orders-status--${RECEIPT_STATUS[order.receipt_status].tone}`}
|
className={`orders-status orders-status--${RECEIPT_STATUS[order.receipt_status].tone}`}
|
||||||
title={order.receipt_error ?? undefined}
|
title={order.receipt_error ?? undefined}
|
||||||
@@ -344,7 +344,7 @@ export function DashboardPage() {
|
|||||||
{RECEIPT_STATUS[order.receipt_status].label}
|
{RECEIPT_STATUS[order.receipt_status].label}
|
||||||
</span>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="orders-delete-btn"
|
className="orders-delete-btn"
|
||||||
@@ -354,7 +354,7 @@ export function DashboardPage() {
|
|||||||
Отменить
|
Отменить
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{canDelete && tab === 'no_receipt' && (
|
{canDelete && (tab === 'no_receipt' || (tab === 'refused' && !order.has_receipt)) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="orders-delete-btn"
|
className="orders-delete-btn"
|
||||||
|
|||||||
Reference in New Issue
Block a user