food-market/src/food-market.web/src/App.tsx
nns 47a019dc6d feat(demands): оптовая отгрузка контрагенту-юрлицу (P1-5)
Domain Demand+DemandLine - зеркалит RetailSale, но всегда с CustomerId
(обязателен, не nullable), способ оплаты DemandPayment с Credit
(постоплата = дебиторка), без RetailPoint/Cashier.

EF + миграция Phase8a_Demands (idempotent CREATE TABLE).
Контроллер api/sales/demands - CRUD + Post/Unpost. Post создаёт
StockMovement тип WholesaleSale с -Quantity; защита от ухода в минус
(409 со списком конфликтов). Unpost возвращает товар.

ApplyLines пишет в DbSet напрямую (не через nav-collection) и Update
использует ExecuteDelete для старых строк - тот же fix-паттерн что в
RetailSalesController (избегает DbUpdateConcurrency на client-side Id).

Permissions переиспользуют DemandsEdit/DemandsPost (уже в RolePermissions).
Метрики observability: food_market_documents_posted_total{type="demand"}
и documents_error_total{type="demand", reason="serialization"}.

Web: /sales/demands (list+edit) с AsyncSelect контрагентов, способом
оплаты включая Credit, PaidAmount-полем для дебиторки. Сайдбар:
"Оптовые отгрузки" в группе Продажи для Admin.

Тесты: 3 интеграционных (post снижает stock + unpost восстанавливает,
over-stock posting -> 409 без побочных эффектов, tenant-изоляция).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 16:18:49 +05:00

156 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { LoginPage } from '@/pages/LoginPage'
import { AuthBridgePage } from '@/pages/AuthBridgePage'
import { DashboardPage } from '@/pages/DashboardPage'
import { OnboardingPage } from '@/pages/OnboardingPage'
import { SuperAdminDashboardPage } from '@/pages/SuperAdminDashboardPage'
import { SuperAdminOrganizationsPage } from '@/pages/SuperAdminOrganizationsPage'
import { SuperAdminOrgCreatePage } from '@/pages/SuperAdminOrgCreatePage'
import { SuperAdminAuditLogPage } from '@/pages/SuperAdminAuditLogPage'
import { SuperAdminSetupPage } from '@/pages/SuperAdminSetupPage'
import { SuperAdminSettingsPage } from '@/pages/SuperAdminSettingsPage'
import { CountriesPage } from '@/pages/CountriesPage'
import { UnitsOfMeasurePage } from '@/pages/UnitsOfMeasurePage'
import { SuperAdminUnitsOfMeasurePage } from '@/pages/SuperAdminUnitsOfMeasurePage'
import { PriceTypesPage } from '@/pages/PriceTypesPage'
import { StoresPage } from '@/pages/StoresPage'
import { RetailPointsPage } from '@/pages/RetailPointsPage'
import { ProductGroupsPage } from '@/pages/ProductGroupsPage'
import { CounterpartiesPage } from '@/pages/CounterpartiesPage'
import { ProductsPage } from '@/pages/ProductsPage'
import { ProductEditPage } from '@/pages/ProductEditPage'
import { MoySkladImportPage } from '@/pages/MoySkladImportPage'
import { OrganizationSettingsPage } from '@/pages/OrganizationSettingsPage'
import { EmployeesPage } from '@/pages/EmployeesPage'
import { EmployeeRolesPage } from '@/pages/EmployeeRolesPage'
import { StockPage } from '@/pages/StockPage'
import { StockMovementsPage } from '@/pages/StockMovementsPage'
import { SuppliesPage } from '@/pages/SuppliesPage'
import { SupplyEditPage } from '@/pages/SupplyEditPage'
import { EntersPage } from '@/pages/EntersPage'
import { EnterEditPage } from '@/pages/EnterEditPage'
import { LossesPage } from '@/pages/LossesPage'
import { LossEditPage } from '@/pages/LossEditPage'
import { TransfersPage } from '@/pages/TransfersPage'
import { TransferEditPage } from '@/pages/TransferEditPage'
import { InventoriesPage } from '@/pages/InventoriesPage'
import { InventoryEditPage } from '@/pages/InventoryEditPage'
import { SupplierReturnsPage } from '@/pages/SupplierReturnsPage'
import { SupplierReturnEditPage } from '@/pages/SupplierReturnEditPage'
import { DemandsPage } from '@/pages/DemandsPage'
import { DemandEditPage } from '@/pages/DemandEditPage'
import { SalesReportPage } from '@/pages/SalesReportPage'
import { StockReportPage } from '@/pages/StockReportPage'
import { ProfitReportPage } from '@/pages/ProfitReportPage'
import { AbcReportPage } from '@/pages/AbcReportPage'
import { RetailSalesPage } from '@/pages/RetailSalesPage'
import { RetailSaleEditPage } from '@/pages/RetailSaleEditPage'
import { AppLayout } from '@/components/AppLayout'
import { SuperAdminLayout } from '@/components/SuperAdminLayout'
import { TenantRouteGuard } from '@/components/TenantRouteGuard'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { NoOrganizationPage } from '@/pages/NoOrganizationPage'
import { SuperAdminOrgEmployeesPage } from '@/pages/SuperAdminOrgEmployeesPage'
import { SuperAdminPlatformSettingsPage } from '@/pages/SuperAdminPlatformSettingsPage'
import { ForgotPasswordPage } from '@/pages/ForgotPasswordPage'
import { ResetPasswordPage } from '@/pages/ResetPasswordPage'
import { RoleGuard } from '@/components/RoleGuard'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
},
})
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
<Route path="/reset-password" element={<ResetPasswordPage />} />
<Route path="/auth-bridge" element={<AuthBridgePage />} />
<Route element={<ProtectedRoute />}>
{/* Fallback для orphan AppUser без активной org / Employee.
* Без layout'а — full-screen, оттуда CTA на /signup или mailto. */}
<Route path="/no-organization" element={<NoOrganizationPage />} />
{/* SuperAdmin консоль — отдельный layout c индиго-сайдбаром,
* системными разделами и быстрым «Открыть организацию» в topbar.
* Setup wizard вне layout'а — full-screen onboarding. */}
<Route path="/super-admin/setup" element={<SuperAdminSetupPage />} />
<Route path="/super-admin" element={<SuperAdminLayout />}>
<Route index element={<SuperAdminDashboardPage />} />
<Route path="organizations" element={<SuperAdminOrganizationsPage />} />
<Route path="organizations/new" element={<SuperAdminOrgCreatePage />} />
<Route path="organizations/:id/employees" element={<SuperAdminOrgEmployeesPage />} />
<Route path="audit-log" element={<SuperAdminAuditLogPage />} />
<Route path="countries" element={<CountriesPage />} />
<Route path="groups" element={<ProductGroupsPage />} />
<Route path="units" element={<SuperAdminUnitsOfMeasurePage />} />
<Route path="settings" element={<SuperAdminSettingsPage />} />
<Route path="platform-settings" element={<SuperAdminPlatformSettingsPage />} />
</Route>
{/* Tenant-роуты — обычный AppLayout, но с TenantRouteGuard:
* SuperAdmin без активного override → редирект на /super-admin/organizations. */}
<Route element={<TenantRouteGuard><AppLayout /></TenantRouteGuard>}>
<Route path="/" element={<OnboardingPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/catalog/products" element={<ProductsPage />} />
<Route path="/catalog/products/new" element={<ProductEditPage />} />
<Route path="/catalog/products/:id" element={<ProductEditPage />} />
<Route path="/catalog/product-groups" element={<ProductGroupsPage />} />
<Route path="/catalog/units" element={<UnitsOfMeasurePage />} />
<Route path="/catalog/price-types" element={<PriceTypesPage />} />
<Route path="/catalog/counterparties" element={<RoleGuard roles={['Admin']}><CounterpartiesPage /></RoleGuard>} />
<Route path="/catalog/stores" element={<RoleGuard roles={['Admin']}><StoresPage /></RoleGuard>} />
<Route path="/catalog/retail-points" element={<RoleGuard roles={['Admin']}><RetailPointsPage /></RoleGuard>} />
<Route path="/inventory/stock" element={<StockPage />} />
<Route path="/inventory/movements" element={<StockMovementsPage />} />
<Route path="/purchases/supplies" element={<SuppliesPage />} />
<Route path="/purchases/supplies/new" element={<SupplyEditPage />} />
<Route path="/purchases/supplies/:id" element={<SupplyEditPage />} />
<Route path="/inventory/enters" element={<EntersPage />} />
<Route path="/inventory/enters/new" element={<EnterEditPage />} />
<Route path="/inventory/enters/:id" element={<EnterEditPage />} />
<Route path="/inventory/losses" element={<LossesPage />} />
<Route path="/inventory/losses/new" element={<LossEditPage />} />
<Route path="/inventory/losses/:id" element={<LossEditPage />} />
<Route path="/inventory/transfers" element={<TransfersPage />} />
<Route path="/inventory/transfers/new" element={<TransferEditPage />} />
<Route path="/inventory/transfers/:id" element={<TransferEditPage />} />
<Route path="/inventory/inventories" element={<InventoriesPage />} />
<Route path="/inventory/inventories/new" element={<InventoryEditPage />} />
<Route path="/inventory/inventories/:id" element={<InventoryEditPage />} />
<Route path="/purchases/supplier-returns" element={<SupplierReturnsPage />} />
<Route path="/purchases/supplier-returns/new" element={<SupplierReturnEditPage />} />
<Route path="/purchases/supplier-returns/:id" element={<SupplierReturnEditPage />} />
<Route path="/reports/sales" element={<SalesReportPage />} />
<Route path="/reports/stock" element={<StockReportPage />} />
<Route path="/reports/profit" element={<ProfitReportPage />} />
<Route path="/reports/abc" element={<AbcReportPage />} />
<Route path="/sales/retail" element={<RetailSalesPage />} />
<Route path="/sales/retail/new" element={<RetailSaleEditPage />} />
<Route path="/sales/retail/:id" element={<RetailSaleEditPage />} />
<Route path="/sales/demands" element={<DemandsPage />} />
<Route path="/sales/demands/new" element={<DemandEditPage />} />
<Route path="/sales/demands/:id" element={<DemandEditPage />} />
<Route path="/admin/import/moysklad" element={<RoleGuard roles={['Admin']}><MoySkladImportPage /></RoleGuard>} />
<Route path="/settings/organization" element={<RoleGuard roles={['Admin']}><OrganizationSettingsPage /></RoleGuard>} />
<Route path="/settings/employees" element={<RoleGuard roles={['Admin']}><EmployeesPage /></RoleGuard>} />
<Route path="/settings/employee-roles" element={<RoleGuard roles={['Admin']}><EmployeeRolesPage /></RoleGuard>} />
</Route>
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
)
}