Desktop POS — Phase 4

Что сделано

Domain layer

  • packages/domain/src/types/aggregator.ts (new):
    • AggregatorProvider = "yandex_eats" | "market_delivery" | "delivery_club" | string
    • AggregatorOrderListItem — соответствует Java OrderListItem с external_provider
    • AGGREGATOR_PROVIDER_LABEL map → русские названия
    • aggregatorProviderLabel(p) helper

API client

  • packages/api-client/src/endpoints/aggregatorOrders.ts (new):
    • list()AggregatorOrderListItem[]
    • getById(orderId)Order
    • accept / ready / handOver / reject(orderId, reason?) → POST в BFF

Stores

  • stores/aggregatorStore.ts (new):
    • items (полный список), loading/error/loaded
    • knownIds: Set<string> — все заказы которые мы уже видели
    • newSinceLastReload: string[] — diff между предыдущим и текущим reload (только status=new)
    • load() / reload() / accept/ready/handOver/reject (с auto-reload)
    • newCount() / acceptedCount() — для badge / column counters
    • clearNew() — после того как звук проиграли

Lib

  • apps/desktop/src/lib/notificationSound.ts (new):
    • playNewOrderSound() через Web Audio API
    • Двойной beep: 880 Гц → 1320 Гц, 0.16с each
    • Ленивый AudioContext, переиспользуется

Screens

  • screens/AggregatorOrdersScreen.tsx (new, /aggregator):
    • 3 columns layout (Новые / Готовятся / Готовы — статусы distributed)
    • Cards: order_number + StatusBadge + provider label + external_order_id + elapsed time + total
    • Actions per status: new → Принять/Отказать; accepted → Готов; ready → Передан курьеру
    • reject — prompt() для reason
    • usePolling(reload, 15s)
    • useEffect слушает newSinceLastReload → играет звук + toast → clearNew()

Components / Wiring

  • components/AppShell.tsx (extended):
    • NavItem «Агрегаторы» с badge={aggNewCount} — pulsing red badge
    • Новый NavItem принимает badge?: number — рендерит отдельный pill справа от текста
    • Фоновый useEffect: poll aggReload() каждые 30 сек (всегда, не только на /aggregator)
    • Fallback useEffect: если NEW появились на не-/aggregator экране — играем звук и push toast прямо отсюда (пользователь не пропустит)
    • <style> с @keyframes pulse для badge animation
  • App.tsx/aggregator route под Protected+RequireShift+AppShell
  • api/client.tsgetAggregatorOrdersEndpoints() factory
  • api/mockEndpoints.ts:
    • 3 seed заказа: Y-1024 new (Я.Еда), M-2057 accepted (Маркет), Y-1019 ready (Я.Еда)
    • createMockAggregatorOrdersEndpoints — accept меняет status, hand-over/reject убирает из массива

Файлы

Created (5):

  • packages/domain/src/types/aggregator.ts
  • packages/api-client/src/endpoints/aggregatorOrders.ts
  • apps/desktop/src/stores/aggregatorStore.ts
  • apps/desktop/src/lib/notificationSound.ts
  • apps/desktop/src/screens/AggregatorOrdersScreen.tsx

Modified (5):

  • packages/domain/src/types/index.ts
  • packages/api-client/src/index.ts
  • apps/desktop/src/App.tsx
  • apps/desktop/src/components/AppShell.tsx
  • apps/desktop/src/api/client.ts
  • apps/desktop/src/api/mockEndpoints.ts

Тесты

  • pnpm typecheck — zero errors
  • pnpm exec vite build — passing (201 modules, +5)
  • Mock-mode E2E:
    • Открыть /aggregator → видим 3 карточки
    • Принять Y-1024 → перешла из «Новые» в «Готовятся»
    • В фоне (через 30 сек) — symptom: badge число обновляется
    • Открытие /main с предыдущим mock seed: badge показывает «1» (Y-1024 в new)

Ссылки