BUG-027: Декомпозиция

Минимальный фикс на фронте erp-admin/web. Бэк не трогаем — он работает по спеке.

Код

Admin Franchise (erp-admin/web)

  • web/src/pages/payroll/PayrollPage.tsx:208-224 — переписать handleExportCsv:
    • items.length === 0showToast("Нет ведомостей для экспорта", "error"); return;
    • items.length > 1showToast("В выборке N ведомостей. Сузьте фильтр до одной ТТ", "error"); return;
    • items.length === 1exportPayrollCsv(items[0].id) → download blob → payroll-${period}.csv
  • web/src/pages/payroll/PayrollPage.tsx:348-352 — на кнопке «Экспорт CSV» добавить disabled={items.length === 0 || actionLoading === "export"}
  • pnpm --filter @erp/admin-web exec tsc --noEmit — пройти typecheck

Backend / BFF / shared

Не требуется. Спека GET /payroll/{id}/export корректна, реализация работает.

Спеки

Не требуются. Поведение покрыто существующим контрактом.

Тест

  • Положительный: 1 ведомость в выборке → клик → скачивается CSV с UTF-8 BOM и русскими заголовками
  • Отрицательный 1: пустая выборка → кнопка disabled, клика нет
  • Отрицательный 2: >1 ведомость → toast, файл не скачивается
  • Regression: остальные кнопки PayrollPage (calculate / confirm / mark-paid) — не задеваем

Деплой

erp-admin (admin-bff + admin-frontend контейнеры в erp-front namespace). Один сервис.