From 8fffdc3ce617df85c64361d813e3be8eb06ccf6a Mon Sep 17 00:00:00 2001 From: Liutenko Oleksandr Date: Fri, 25 Sep 2026 17:11:57 +0300 Subject: [PATCH] Add main menu page with role-based modules (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `/` is now the main menu; orders/receipts page moved to `/receipts` - Module registry (features/menu/modules.ts) drives both the menu tiles and route guards (ModuleRoute); admin sees every module - Menu: module search with Ctrl+K, profile with logout, responsive grid, palette matching the login page - «← Головне меню» link on receipts and cash registers pages; «Каси» moved from the receipts toolbar into the menu Co-Authored-By: Claude Opus 5.5 --- frontend/index.html | 6 + frontend/src/app/routes.tsx | 11 +- frontend/src/features/auth/roles.ts | 7 + frontend/src/features/menu/ModuleRoute.tsx | 11 + frontend/src/features/menu/modules.ts | 46 +++ frontend/src/index.css | 13 + frontend/src/pages/CashRegistersPage.tsx | 14 +- frontend/src/pages/DashboardPage.css | 6 + frontend/src/pages/DashboardPage.tsx | 21 +- frontend/src/pages/MainMenuPage.css | 458 +++++++++++++++++++++ frontend/src/pages/MainMenuPage.tsx | 157 +++++++ 11 files changed, 728 insertions(+), 22 deletions(-) create mode 100644 frontend/src/features/auth/roles.ts create mode 100644 frontend/src/features/menu/ModuleRoute.tsx create mode 100644 frontend/src/features/menu/modules.ts create mode 100644 frontend/src/pages/MainMenuPage.css create mode 100644 frontend/src/pages/MainMenuPage.tsx diff --git a/frontend/index.html b/frontend/index.html index cd9a1f5..fb32bc3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,12 @@ + + + Assistant System diff --git a/frontend/src/app/routes.tsx b/frontend/src/app/routes.tsx index 5358585..b434ca7 100644 --- a/frontend/src/app/routes.tsx +++ b/frontend/src/app/routes.tsx @@ -2,8 +2,10 @@ import { Navigate, Route, Routes } from 'react-router-dom' import { LoginPage } from '@/features/auth/LoginPage' import { ProtectedRoute } from '@/features/auth/ProtectedRoute' +import { ModuleRoute } from '@/features/menu/ModuleRoute' import { CashRegistersPage } from '@/pages/CashRegistersPage' import { DashboardPage } from '@/pages/DashboardPage' +import { MainMenuPage } from '@/pages/MainMenuPage' export function AppRoutes() { return ( @@ -11,8 +13,13 @@ export function AppRoutes() { } /> }> - } /> - } /> + } /> + }> + } /> + + }> + } /> + } /> diff --git a/frontend/src/features/auth/roles.ts b/frontend/src/features/auth/roles.ts new file mode 100644 index 0000000..14ddcae --- /dev/null +++ b/frontend/src/features/auth/roles.ts @@ -0,0 +1,7 @@ +import type { UserRole } from '@/api/types' + +export const ROLE_LABEL: Record = { + admin: 'Адміністратор', + cashier: 'Касир', + viewer: 'Спостерігач', +} diff --git a/frontend/src/features/menu/ModuleRoute.tsx b/frontend/src/features/menu/ModuleRoute.tsx new file mode 100644 index 0000000..5600597 --- /dev/null +++ b/frontend/src/features/menu/ModuleRoute.tsx @@ -0,0 +1,11 @@ +import { Navigate, Outlet } from 'react-router-dom' + +import { useAuth } from '@/features/auth/useAuth' +import { canAccess, getModule } from '@/features/menu/modules' + +/** Пускает в модуль только роли из реестра; остальных возвращает в главное меню. */ +export function ModuleRoute({ moduleId }: { moduleId: string }) { + const { user } = useAuth() + if (!canAccess(user, getModule(moduleId).roles)) return + return +} diff --git a/frontend/src/features/menu/modules.ts b/frontend/src/features/menu/modules.ts new file mode 100644 index 0000000..3c5ea1c --- /dev/null +++ b/frontend/src/features/menu/modules.ts @@ -0,0 +1,46 @@ +import type { User, UserRole } from '@/api/types' + +/** + * Реестр модулей главного меню. Единственное место, где модуль связывается + * с путём и ролями: по нему рисуется меню и защищается роут модуля. + */ +export interface AppModule { + id: string + title: string + description: string + path: string + /** Роли, которым модуль доступен; администратор видит всё и в списке не нужен. */ + roles: readonly UserRole[] + /** `d` для stroke-иконки 24×24. */ + icon: string +} + +export const MODULES: readonly AppModule[] = [ + { + id: 'receipts', + title: 'Чеки', + description: 'Замовлення з CRM, ЕТТН-чеки Checkbox і статуси Нової Пошти', + path: '/receipts', + roles: ['cashier', 'viewer'], + icon: 'M6 3h12v18l-3-2-3 2-3-2-3 2z M9 8h6 M9 12h6 M9 16h3', + }, + { + id: 'cash-registers', + title: 'Каси', + description: 'Каси Checkbox, ліцензійні ключі та ключі Нової Пошти', + path: '/cash-registers', + roles: [], + icon: 'M4 10h16v10H4z M7 10V4h10v6 M8 14h2 M14 14h2 M8 17h2 M14 17h2', + }, +] + +export function canAccess(user: User | null | undefined, roles: readonly UserRole[]): boolean { + if (!user) return false + return user.role === 'admin' || roles.includes(user.role) +} + +export function getModule(id: string): AppModule { + const module = MODULES.find((m) => m.id === id) + if (!module) throw new Error(`Невідомий модуль: ${id}`) + return module +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 5a65ec6..5863908 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -74,6 +74,19 @@ input { /* --- Общие утилиты, используются в нескольких экранах --- */ +/* Скрыт визуально, но читается скринридером (подписи к полям без видимого label). */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + .page-center { min-height: 100svh; display: flex; diff --git a/frontend/src/pages/CashRegistersPage.tsx b/frontend/src/pages/CashRegistersPage.tsx index 7ce2c7a..25de462 100644 --- a/frontend/src/pages/CashRegistersPage.tsx +++ b/frontend/src/pages/CashRegistersPage.tsx @@ -1,7 +1,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query' import { useState } from 'react' import type { FormEvent } from 'react' -import { Link, Navigate } from 'react-router-dom' +import { Link } from 'react-router-dom' import { checkCashRegister, @@ -55,8 +55,6 @@ export function CashRegistersPage() { const [busy, setBusy] = useState(false) const [message, setMessage] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null) - if (user && user.role !== 'admin') return - async function run(action: () => Promise, okText: string) { setBusy(true) try { @@ -123,10 +121,12 @@ export function CashRegistersPage() { return (
- Assistant System - - ← До замовлень - +
+ + ← Головне меню + + Assistant System +
diff --git a/frontend/src/pages/DashboardPage.css b/frontend/src/pages/DashboardPage.css index a19ec83..971ea9c 100644 --- a/frontend/src/pages/DashboardPage.css +++ b/frontend/src/pages/DashboardPage.css @@ -13,6 +13,12 @@ border-bottom: 1px solid var(--color-border); } +.dashboard-topbar-start { + display: flex; + align-items: center; + gap: 16px; +} + .dashboard-brand { font-size: 16px; font-weight: 700; diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index cf93d92..f3bf6d1 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -5,6 +5,7 @@ import { Link } from 'react-router-dom' import { deleteOrder } from '@/api/orders' import { cancelReceipt, createReceipts } from '@/api/receipts' import '@/pages/DashboardPage.css' +import { ROLE_LABEL } from '@/features/auth/roles' import { useAuth } from '@/features/auth/useAuth' import { OrderDetailModal } from '@/features/orders/OrderDetailModal' import type { Order, OrderTab } from '@/features/orders/types' @@ -13,12 +14,6 @@ import { defaultPrepayment, prepaymentMatches, toKopecks } from '@/features/rece import { CANCELLABLE, RECEIPT_STATUS } from '@/features/receipts/types' import type { ReceiptRequestItem } from '@/features/receipts/types' -const ROLE_LABEL: Record = { - admin: 'Адміністратор', - cashier: 'Касир', - viewer: 'Спостерігач', -} - const TABS: { key: OrderTab; label: string }[] = [ { key: 'no_receipt', label: 'Без чека' }, { key: 'has_receipt', label: 'Виписані чеки' }, @@ -174,10 +169,15 @@ export function DashboardPage() { return (
- Assistant System +
+ + ← Головне меню + + Assistant System +
{user?.full_name} - {user ? (ROLE_LABEL[user.role] ?? user.role) : ''} + {user ? ROLE_LABEL[user.role] : ''} @@ -188,11 +188,6 @@ export function DashboardPage() {

Замовлення

- {user?.role === 'admin' && ( - - Каси - - )} {tab === 'no_receipt' && canFiscalize && ( +
+ )} +
+
+ +
+
+
+ Головне меню +

З чого почнемо сьогодні?

+
+

Оберіть модуль, щоб перейти до роботи.

+
+ + +
+
+ ) +}