diff --git a/src/food-market.web/src/App.tsx b/src/food-market.web/src/App.tsx index 40a3d4a..a42ed0d 100644 --- a/src/food-market.web/src/App.tsx +++ b/src/food-market.web/src/App.tsx @@ -28,6 +28,8 @@ import { SupplyEditPage } from '@/pages/SupplyEditPage' 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' const queryClient = new QueryClient({ @@ -46,7 +48,20 @@ export default function App() { } /> }> - }> + {/* SuperAdmin консоль — отдельный layout c индиго-сайдбаром, + * системными разделами и быстрым «Открыть организацию» в topbar. + * Setup wizard вне layout'а — full-screen onboarding. */} + } /> + }> + } /> + } /> + } /> + } /> + + + {/* Tenant-роуты — обычный AppLayout, но с TenantRouteGuard: + * SuperAdmin без активного override → редирект на /super-admin/organizations. */} + }> } /> } /> } /> @@ -71,11 +86,6 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> - } /> - } /> } /> diff --git a/src/food-market.web/src/components/AppLayout.tsx b/src/food-market.web/src/components/AppLayout.tsx index 533d67c..376ca30 100644 --- a/src/food-market.web/src/components/AppLayout.tsx +++ b/src/food-market.web/src/components/AppLayout.tsx @@ -6,7 +6,7 @@ import { logout } from '@/lib/auth' import { cn } from '@/lib/utils' import { LayoutDashboard, Package, FolderTree, Ruler, Tag, - Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download, UserCog, Shield, ShieldCheck, Building, FileClock, + Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download, UserCog, Shield, ShieldCheck, Boxes, History, TruckIcon, ShoppingCart, Settings, Menu, X, } from 'lucide-react' import { Logo } from './Logo' @@ -63,11 +63,11 @@ function buildNav(isSuperAdmin: boolean): NavSection[] { { to: '/settings/employee-roles', icon: Shield, label: 'Роли' }, ]}, ...(isSuperAdmin ? [{ + // Только одна точка возврата — система. Системный sidebar отдельно + // в SuperAdminLayout. В tenant-меню здесь — только переход в консоль. group: 'Супер-админ', items: [ - { to: '/super-admin', icon: ShieldCheck, label: 'Консоль', end: true }, - { to: '/super-admin/organizations', icon: Building, label: 'Организации' }, - { to: '/super-admin/audit-log', icon: FileClock, label: 'Журнал' }, + { to: '/super-admin', icon: ShieldCheck, label: 'Системная консоль', end: true }, ], }] : []), ] @@ -82,6 +82,15 @@ export function AppLayout() { const isSuperAdmin = !!me?.roles?.includes('SuperAdmin') const nav = buildNav(isSuperAdmin) + // При активном «открыто как…» — слабая жёлтая тонировка фона tenant-области + // даёт периферийный сигнал «я не в своей админке». + const inOverride = typeof window !== 'undefined' && !!localStorage.getItem('superAdminAsOrg') + // Toast на guard-message (TenantRouteGuard кладёт сообщение в sessionStorage + // перед редиректом — показываем разово). + useEffect(() => { + const msg = sessionStorage.getItem('tenant-guard-msg') + if (msg) { sessionStorage.removeItem('tenant-guard-msg'); alert(msg) } + }, []) // Если SuperAdmin зашёл, а в системе ноль организаций — выкидываем // на setup wizard (нельзя пропустить, пока не создадут первую орг). const location2 = useLocation() @@ -153,7 +162,10 @@ export function AppLayout() { ) return ( -
+
{/* Mobile header with hamburger — только на узких экранах. */}
+
+ + ) + + return ( +
+
+ +
+ + Super +
+
+ + + + {drawerOpen && ( +
+
setDrawerOpen(false)} /> + +
+ )} + +
+ {/* Topbar системной консоли с быстрым переключателем «Открыть как…» */} +
+
+ Системная консоль + · + Super Admin +
+
+ + {orgPickerOpen && ( +
+ {orgs.isLoading &&
Загрузка…
} + {orgs.data?.length === 0 &&
Нет организаций
} + {orgs.data?.map((o) => ( + + ))} +
+ )} +
+
+ +
+
+ ) +} diff --git a/src/food-market.web/src/components/TenantRouteGuard.tsx b/src/food-market.web/src/components/TenantRouteGuard.tsx new file mode 100644 index 0000000..bd66079 --- /dev/null +++ b/src/food-market.web/src/components/TenantRouteGuard.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useMe } from '@/lib/useMe' +import { getOrgOverride } from '@/lib/api' + +/** Не пускает SuperAdmin'а в tenant-роуты без активного override — + * SuperAdmin сам по себе не сотрудник конкретной орги, любая tenant-страница + * для него бессмысленна. Если override не активен — редирект на + * /super-admin/organizations с alert'ом «Откройте организацию через "Открыть как…"». */ +export function TenantRouteGuard({ children }: { children: React.ReactNode }) { + const me = useMe() + const navigate = useNavigate() + useEffect(() => { + if (!me.data) return + const isSuper = me.data.roles?.includes('SuperAdmin') + if (isSuper && !getOrgOverride()) { + // Маленькое уведомление перед редиректом — alert удобнее тоста для разовой ситуации. + try { sessionStorage.setItem('tenant-guard-msg', 'Откройте конкретную организацию через "Открыть как…"') } catch { /* ignore */ } + navigate('/super-admin/organizations', { replace: true }) + } + }, [me.data, navigate]) + return <>{children} +} diff --git a/src/food-market.web/src/pages/LoginPage.tsx b/src/food-market.web/src/pages/LoginPage.tsx index 0ed2c99..842c0e9 100644 --- a/src/food-market.web/src/pages/LoginPage.tsx +++ b/src/food-market.web/src/pages/LoginPage.tsx @@ -1,8 +1,11 @@ import { useState, type FormEvent } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { login } from '@/lib/auth' +import { api } from '@/lib/api' import { Logo } from '@/components/Logo' +interface MeResp { roles: string[] } + export function LoginPage() { const navigate = useNavigate() const location = useLocation() @@ -19,7 +22,15 @@ export function LoginPage() { setError(null) try { await login(email, password) - navigate(from, { replace: true }) + // SuperAdmin → системная консоль; обычные tenant-юзеры → tenant landing. + let target = from + try { + const me = (await api.get('/api/me')).data + if (me.roles?.includes('SuperAdmin') && (from === '/' || from === '/dashboard')) { + target = '/super-admin' + } + } catch { /* ignore — fallback на from */ } + navigate(target, { replace: true }) } catch (err) { setError(err instanceof Error ? err.message : 'Ошибка входа') } finally { diff --git a/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx b/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx index c71ee6a..26e4ee1 100644 --- a/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx +++ b/src/food-market.web/src/pages/SuperAdminDashboardPage.tsx @@ -10,6 +10,11 @@ interface DashboardStats { totalProducts: number; totalSuppliesThisMonth: number } +interface AuditRow { + id: string; createdAt: string; actionType: string + organizationName: string | null; description: string | null +} + const fmt = new Intl.NumberFormat('ru') function Kpi({ icon: Icon, label, value, hint }: { @@ -34,6 +39,10 @@ export function SuperAdminDashboardPage() { queryKey: ['/api/super-admin/dashboard'], queryFn: async () => (await api.get('/api/super-admin/dashboard')).data, }) + const audit = useQuery({ + queryKey: ['/api/super-admin/audit-log', 'recent'], + queryFn: async () => (await api.get<{ items: AuditRow[] }>('/api/super-admin/audit-log?pageSize=10')).data.items, + }) return (
@@ -69,11 +78,34 @@ export function SuperAdminDashboardPage() {
-
+
Создать организацию
+ +
+
+

Последние события

+ Весь журнал → +
+ {audit.data?.length === 0 ? ( +
Пока нет записей.
+ ) : ( +
    + {audit.data?.map((r) => ( +
  • + {new Date(r.createdAt).toLocaleString('ru')} + {r.actionType} + + {r.organizationName && «{r.organizationName}»} + {r.description} + +
  • + ))} +
+ )} +
)