Desktop POS — Phase 2

Что сделано

Domain layer

  • packages/domain/src/types/staff.tsPosEmployee (id, first_name, last_name, role_names[]) + хелпер fullName(e)
  • packages/domain/src/types/order.ts — добавлены Order.table_id?: string | null, AddItemsRequest, CloseWithPaymentRequest
  • packages/domain/src/types/index.tsexport * from "./staff.ts"

API client

  • packages/api-client/src/endpoints/staff.ts (new) — listPosEmployees()
  • packages/api-client/src/endpoints/orders.ts (extended):
    • findOpenByTable(tableId) — возвращает Order | null (404 трактуется как “нет открытого заказа”)
    • addItems(orderId, items) — POST в BFF /add-items с уже распакованными items
    • closeWithPayment(orderId, req) — POST в BFF /close-with-payment (мапит payment в плоский shape)

Stores

  • stores/cartStore.tsmode: CartMode = { type: 'new' } | { type: 'append', orderId, orderNumber, tableId, tableNumber }. setMode(append) автоматически выставляет orderType=dine_in + tableId/tableNumber. clear() сбрасывает в { type: 'new' }.
  • stores/staffStore.ts (new) — items, loaded, byId; load() ленивый, reload() форсированный.

Components

  • components/WaiterPickerModal.tsx (new) — поиск по имени/роли, кнопка «Снять назначение» если есть currentWaiterId, выбор → onSelect(waiter_id | null). Подгружает staffStore.load() при открытии.

Screens

  • screens/TableOrderScreen.tsx (new, /tables/:tableId):

    • Загружает tablesStore.load() + staffStore.load() + orders.findOpenByTable(tableId)
    • Header: «Стол №X — Label» + StatusBadge заказа
    • Метаданные: вместимость, текущий официант (с кнопкой «Назначить»/«Сменить»)
    • Если активный заказ есть — позиции с timestamps (created_at заказа), Итого
    • Действия: «Добавить позиции» (→ MainScreen append-mode), «Закрыть наличными/картой» (close-with-payment напрямую), «Оплата через PayKeeper» (→ /orders/:id для PK polling-flow)
    • Если заказа нет — плейсхолдер + «Освободить стол» если стол занят (рассинхрон)
  • screens/MainScreen.tsx (extended):

    • В append-mode: header «Дозаказ к №X» + кнопка «Отменить дозаказ» (clearCart)
    • Красный bar «Стол №X · добавляем позиции в открытый заказ»
    • Скрыт OrderTypeSwitcher и индикатор стола (всё уже зафиксировано append)
    • Единственная кнопка «Добавить в заказ №N» (вместо Оплатить + На кухню)
    • handleAppendItems()addItems(mode.orderId, items)clearCart()navigate('/tables/${tableId}')
  • screens/TablesScreen.tsx (extended):

    • handleViewOrder для occupied → navigate('/tables/:id') (раньше → /orders/:id)
    • handleOpenOrder чистит корзину + сбрасывает mode перед навигацией
    • В action sheet добавлен onAssignWaiter callback → WaiterPickerModal открывается прямо отсюда (для free и occupied)
  • screens/OrderDetailScreen.tsx (extended):

    • Подгружает tablesStore + staffStore при mount
    • Для order_type === 'dine_in' показывает Meta «Стол №X (label)» + Meta «Официант» (имя из staffStore через current_waiter_id стола)
  • components/TableActionSheet.tsx — добавлен prop onAssignWaiter; кнопка «Сменить/Назначить официанта» показывается в free и occupied

Wiring

  • api/client.tsgetStaffEndpoints() factory (mock или real)
  • api/mockEndpoints.ts:
    • addItems(orderId, items) — append + пересчёт total
    • findOpenByTable(tableId) — поиск order по table_id где status not in (closed/cancelled)
    • closeWithPayment(orderId, req) — выставляет paid_at + status=closed + автоматически освобождает стол
    • createMockStaffEndpoints + 4 employees (Иван Иванов / Анна Петрова / Дмитрий Сидоров / Елена Козлова)
    • seedMockData: demo1 теперь order_type=dine_in на mock-table-2 (для теста append-flow)
  • App.tsx — добавлен <Route path="/tables/:tableId"> под Protected+RequireShift+AppShell

Файлы

Created (5):

  • packages/domain/src/types/staff.ts
  • packages/api-client/src/endpoints/staff.ts
  • apps/desktop/src/stores/staffStore.ts
  • apps/desktop/src/components/WaiterPickerModal.tsx
  • apps/desktop/src/screens/TableOrderScreen.tsx

Modified (12):

  • packages/domain/src/types/index.ts
  • packages/domain/src/types/order.ts
  • packages/api-client/src/index.ts
  • packages/api-client/src/endpoints/orders.ts
  • apps/desktop/src/api/client.ts
  • apps/desktop/src/api/mockEndpoints.ts
  • apps/desktop/src/stores/cartStore.ts
  • apps/desktop/src/components/TableActionSheet.tsx
  • apps/desktop/src/screens/MainScreen.tsx
  • apps/desktop/src/screens/OrderDetailScreen.tsx
  • apps/desktop/src/screens/TablesScreen.tsx
  • apps/desktop/src/App.tsx

Тесты

  • pnpm typecheck — zero errors во всём monorepo
  • pnpm exec vite build — passing (188 modules, +5 от Phase 1)
  • E2E на VPS:
    • findOpenByTable(17d5214f...) → возвращает order 007 со 1 позицией
    • addItems(50c45f4f..., [{Тестовая пицца ×2}]) → total 2.00 → 201.00 (199 = 2×99.50), order_number 007 НЕ изменился, новые items в той же order_id
    • pos-employees для реальной ТТ → Мария Петрова + Дмитрий Сидоров