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>
156 lines
9.9 KiB
TypeScript
156 lines
9.9 KiB
TypeScript
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>
|
||
)
|
||
}
|