From 42d48618ecfd58d7ce019ad587776af35233eb1f Mon Sep 17 00:00:00 2001 From: ochenstarik Date: Fri, 11 Sep 2026 10:05:58 +0700 Subject: [PATCH] feat(mobile): mobile-first responsive architecture, bottom navigation, and tablet overhaul --- e2e/12-mobile-first.spec.ts | 189 +++++++++ playwright.config.ts | 28 +- src/app/layouts/AppShell.tsx | 99 +++-- src/app/shell/BottomNav.tsx | 165 ++++---- src/app/shell/MobileMenuDrawer.tsx | 279 +++++++++++++ src/app/shell/Sidebar.tsx | 14 +- src/app/shell/Topbar.tsx | 393 ++++++++---------- .../configurationAndCrmInteractions.test.tsx | 19 +- src/features/catalog/ModuleInfoModal.tsx | 6 +- src/features/catalog/ModulesCatalog.tsx | 3 +- src/features/metadata/DynamicFormRenderer.tsx | 8 +- src/features/metadata/DynamicTable.tsx | 91 +++- src/features/settings/SettingsModule.tsx | 2 +- .../settings/entities/EntityCatalog.tsx | 56 ++- .../settings/entities/FormBuilder.tsx | 16 +- src/shared/lib/useResponsive.ts | 63 +++ src/shared/ui/Drawer.tsx | 4 +- src/shared/ui/Modal.tsx | 10 +- tsconfig.app.tsbuildinfo | 2 +- 19 files changed, 1044 insertions(+), 403 deletions(-) create mode 100644 e2e/12-mobile-first.spec.ts create mode 100644 src/app/shell/MobileMenuDrawer.tsx create mode 100644 src/shared/lib/useResponsive.ts diff --git a/e2e/12-mobile-first.spec.ts b/e2e/12-mobile-first.spec.ts new file mode 100644 index 0000000..7786719 --- /dev/null +++ b/e2e/12-mobile-first.spec.ts @@ -0,0 +1,189 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Scenario 12: Mobile-First and Tablet Architecture Overhaul', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + }); + + test('1. Mobile viewport (< 768px) automatically displays bottom navigation', async ({ page, viewport }) => { + const isMobileViewport = viewport && viewport.width < 768; + + if (isMobileViewport) { + // Bottom navigation must be visible + const bottomNav = page.locator('[data-testid="bottom-nav"]'); + await expect(bottomNav).toBeVisible({ timeout: 10000 }); + + // Sidebar must NOT be displayed + const sidebar = page.locator('aside'); + await expect(sidebar).not.toBeVisible(); + + // Topbar must have mobile controls (logo + section title + menu button) + await expect(page.locator('[data-testid="mobile-menu-btn"]')).toBeVisible(); + await expect(page.locator('[data-testid="mobile-current-title"]')).toBeVisible(); + } else { + // Desktop / tablet view: sidebar visible, bottom nav not visible + const sidebar = page.locator('[data-testid="sidebar"]'); + await expect(sidebar).toBeVisible({ timeout: 10000 }); + const bottomNav = page.locator('[data-testid="bottom-nav"]'); + await expect(bottomNav).not.toBeVisible(); + } + }); + + test('2. Mobile bottom nav tabs are functional with >= 44x44px touch targets', async ({ page, viewport }) => { + const isMobileViewport = viewport && viewport.width < 768; + if (!isMobileViewport) return; + + // Check touch target heights + const catalogTab = page.locator('[data-testid="bottom-nav-catalog"]'); + await expect(catalogTab).toBeVisible(); + const box = await catalogTab.boundingBox(); + expect(box).not.toBeNull(); + if (box) { + expect(box.height).toBeGreaterThanOrEqual(44); + expect(box.width).toBeGreaterThanOrEqual(44); + } + + // Navigate to Tasks via bottom nav + const tasksTab = page.locator('[data-testid="bottom-nav-tasks"]'); + await tasksTab.click(); + await page.waitForURL('**/tasks'); + await expect(page.locator('h1, h2').first()).toBeVisible(); + + // Navigate to Communications via bottom nav + const commsTab = page.locator('[data-testid="bottom-nav-communications"]'); + await commsTab.click(); + await page.waitForURL('**/communications'); + await expect(page.locator('h1, h2').first()).toBeVisible(); + + // Return to Catalog + await catalogTab.click(); + await page.waitForURL('**/'); + await expect(page.locator('[data-testid="modules-catalog-container"]')).toBeVisible(); + }); + + test('3. Mobile menu drawer opens and allows switching themes and profiles', async ({ page, viewport }) => { + const isMobileViewport = viewport && viewport.width < 768; + if (!isMobileViewport) return; + + // Open via "Ещё" tab in bottom nav + const moreTab = page.locator('[data-testid="bottom-nav-more"]'); + await moreTab.click(); + + // Drawer must be visible + const drawer = page.locator('[data-testid="mobile-menu-drawer"]'); + await expect(drawer).toBeVisible(); + + // Check drawer content: profiles, roles, themes, reset + await expect(drawer).toContainText('Управление платформой'); + await expect(drawer).toContainText('Отрасль и профиль организации'); + await expect(drawer).toContainText('Тема оформления'); + await expect(page.locator('[data-testid="mobile-reset-db-btn"]')).toBeVisible(); + + // Switch theme to dark + const darkThemeBtn = drawer.getByRole('button', { name: /Тёмная/i }); + await darkThemeBtn.click(); + const themeAttr = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); + expect(themeAttr).toBe('dark'); + + // Close drawer + const closeBtn = page.locator('[data-testid="close-mobile-menu"]'); + await closeBtn.click(); + await expect(drawer).not.toBeVisible(); + }); + + test('4. Zero horizontal overflow across all viewports', async ({ page }) => { + // Check main document scroll width vs client width + const isOverflowing = await page.evaluate(() => { + return document.documentElement.scrollWidth > document.documentElement.clientWidth; + }); + expect(isOverflowing).toBe(false); + + // Verify on /crm + await page.goto('/crm'); + await page.waitForLoadState('domcontentloaded'); + const isCrmOverflowing = await page.evaluate(() => { + return document.documentElement.scrollWidth > document.documentElement.clientWidth; + }); + expect(isCrmOverflowing).toBe(false); + + // Verify on /settings/entities + await page.goto('/settings/entities'); + await page.waitForLoadState('domcontentloaded'); + const isSettingsOverflowing = await page.evaluate(() => { + return document.documentElement.scrollWidth > document.documentElement.clientWidth; + }); + expect(isSettingsOverflowing).toBe(false); + }); + + test('5. DynamicTable renders responsive card list on mobile screens', async ({ page, viewport }) => { + await page.goto('/crm'); + await page.waitForLoadState('domcontentloaded'); + + const isMobileViewport = viewport && viewport.width < 768; + if (isMobileViewport) { + // Mobile card list must be visible + const mobileCards = page.locator('[data-testid="mobile-table-cards"]'); + if (await mobileCards.isVisible()) { + await expect(mobileCards).toBeVisible(); + } + } + }); + + test('6. FormBuilder supports touch-friendly up and down field reordering', async ({ page }) => { + await page.goto('/settings/entities/contact/form-builder'); + await page.waitForLoadState('domcontentloaded'); + + // Locate field move buttons + const moveDownBtn = page.locator('[data-testid^="move-field-down-"]').first(); + + if (await moveDownBtn.isVisible()) { + await expect(moveDownBtn).toBeVisible(); + // Verify button has accessible touch target + const box = await moveDownBtn.boundingBox(); + expect(box).not.toBeNull(); + if (box) { + expect(box.height).toBeGreaterThanOrEqual(28); + } + } + }); + + test('7. Full Russian localization check (zero untranslated system labels)', async ({ page }) => { + // Check home page headings and buttons + const bodyText = await page.locator('body').innerText(); + + // Forbidden English words in visible platform headers + expect(bodyText).not.toContain('Business OS'); + expect(bodyText).not.toContain('Role: Owner'); + expect(bodyText).not.toContain('Quick Action'); + }); + + test('8. All 3 brand themes (soft-light, light, dark) apply and persist', async ({ page }) => { + // Set light theme + await page.evaluate(() => { + window.localStorage.setItem('krona_theme_mode', 'light'); + document.documentElement.setAttribute('data-theme', 'light'); + }); + await page.reload(); + let currentTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); + expect(currentTheme).toBe('light'); + + // Set dark theme + await page.evaluate(() => { + window.localStorage.setItem('krona_theme_mode', 'dark'); + document.documentElement.setAttribute('data-theme', 'dark'); + }); + await page.reload(); + currentTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); + expect(currentTheme).toBe('dark'); + + // Restore soft-light + await page.evaluate(() => { + window.localStorage.setItem('krona_theme_mode', 'soft-light'); + document.documentElement.setAttribute('data-theme', 'soft-light'); + }); + await page.reload(); + currentTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); + expect(currentTheme).toBe('soft-light'); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 88c66e9..9b98473 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -16,21 +16,21 @@ export default defineConfig({ }, projects: [ { - name: 'desktop-1440', + name: 'mobile-360', use: { - viewport: { width: 1440, height: 900 }, + viewport: { width: 360, height: 800 }, }, }, { - name: 'desktop-1280', + name: 'mobile-390', use: { - viewport: { width: 1280, height: 800 }, + viewport: { width: 390, height: 844 }, }, }, { - name: 'tablet-1024', + name: 'mobile-430', use: { - viewport: { width: 1024, height: 768 }, + viewport: { width: 430, height: 932 }, }, }, { @@ -40,9 +40,21 @@ export default defineConfig({ }, }, { - name: 'mobile-390', + name: 'tablet-1024', use: { - viewport: { width: 390, height: 844 }, + viewport: { width: 1024, height: 768 }, + }, + }, + { + name: 'desktop-1280', + use: { + viewport: { width: 1280, height: 800 }, + }, + }, + { + name: 'desktop-1440', + use: { + viewport: { width: 1440, height: 900 }, }, }, ], diff --git a/src/app/layouts/AppShell.tsx b/src/app/layouts/AppShell.tsx index 12d5dcf..dd8b121 100644 --- a/src/app/layouts/AppShell.tsx +++ b/src/app/layouts/AppShell.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Outlet } from 'react-router-dom'; import { useDemoStore } from '../../mock/store'; import { Sidebar } from '../shell/Sidebar'; @@ -8,7 +8,8 @@ import { BottomNav } from '../shell/BottomNav'; import { CommandPalette } from '../shell/CommandPalette'; import { QuickActionsModal } from '../shell/QuickActionsModal'; import { NotificationsDrawer } from '../shell/NotificationsDrawer'; -import { Drawer, Button } from '../../shared/ui'; +import { Drawer } from '../../shared/ui'; +import { useResponsive } from '../../shared/lib/useResponsive'; export const AppShell: React.FC = () => { const { @@ -20,61 +21,80 @@ export const AppShell: React.FC = () => { activeLocation, } = useDemoStore(); + const { isMobile, isTablet, isDesktop } = useResponsive(); const profile = getActiveProfile(); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]); - // Viewport mode constraints + // Layout resolution: + // 1. Real screen size is paramount: + // - If actual viewport < 768px -> always Mobile Shell. + // - If actual viewport 768-1199px -> always Tablet Shell. + // 2. Demo emulation frame is only applied when on real desktop (>= 1200px) and user explicitly chose a demo mode. + const isEmulatedOnDesktop = isDesktop && viewportMode !== 'desktop'; + const isEffectiveMobile = isMobile || (isDesktop && viewportMode === 'mobile'); + const isEffectiveTablet = isTablet || (isDesktop && (viewportMode === 'tablet-portrait' || viewportMode === 'tablet-landscape')); + const getViewportContainerStyles = () => { + if (!isEmulatedOnDesktop) { + return 'w-full min-h-screen'; + } switch (viewportMode) { case 'mobile': - return 'max-w-[420px] h-[880px] my-6 border-8 border-[#3A3D35] rounded-[36px] overflow-hidden shadow-2xl relative'; + return 'max-w-[420px] h-[880px] my-6 border-8 border-krona-border rounded-[36px] overflow-hidden shadow-2xl relative'; case 'tablet-portrait': - return 'max-w-[820px] h-[1050px] my-6 border-8 border-[#3A3D35] rounded-[28px] overflow-hidden shadow-2xl relative'; + return 'max-w-[820px] h-[1050px] my-6 border-8 border-krona-border rounded-[28px] overflow-hidden shadow-2xl relative'; case 'tablet-landscape': - return 'max-w-[1180px] h-[800px] my-6 border-8 border-[#3A3D35] rounded-[24px] overflow-hidden shadow-2xl relative'; - case 'desktop': + return 'max-w-[1180px] h-[800px] my-6 border-8 border-krona-border rounded-[24px] overflow-hidden shadow-2xl relative'; default: return 'w-full min-h-screen'; } }; - const isEmulatedDevice = viewportMode !== 'desktop'; - const showMobileNav = viewportMode === 'mobile'; - return ( -
-
- {/* App Main Shell Container */} +
+
+ {/* Main Shell Layout */}
- {/* Left Sidebar (hidden in mobile mode) */} - {!showMobileNav && } + {/* Left Sidebar: hidden on mobile, rendered on tablet & desktop */} + {!isEffectiveMobile && } {/* Center Work Area */}
{/* Main Content Router Outlet */} -
+
- {/* Right Command Panel (Desktop and Tablet-Landscape) */} - {viewportMode === 'desktop' && } + {/* Right Command Panel (Only on full Desktop) */} + {!isEffectiveMobile && !isEffectiveTablet && }
- {/* Mobile Bottom Navigation */} - {showMobileNav && } + {/* Mobile Bottom Navigation (Always present on mobile) */} + {isEffectiveMobile && } {/* Global Modals & Drawers */} - {/* Standalone AI Drawer (triggered via Topbar ✦ button on any screen) */} + {/* Standalone AI Drawer */} setIsAiDrawerOpen(false)} @@ -82,47 +102,34 @@ export const AppShell: React.FC = () => { subtitle={`Контекст: ${profile.name} • ${activeLocation}`} width="md" > -
-
- Krona AI Core v1: Интеллектуальный помощник подключен к локальной модели данных бизнеса, остаткам, расписанию смен и фискальным отчетам. +
+
+ Krona AI: Интеллектуальный помощник подключен к локальной модели данных бизнеса, остаткам, расписанию смен и фискальным отчетам.
Ролевые агенты команды: -
-
+
+
Бизнес-аналитик
Food cost, выручка, чеки
-
+
Складской агент
-
Списания, закупки, пороги
+
Остатки, неснижаемый лимит
-
+
Маркетолог
-
Сегменты, LTV, рассылки
+
Сегменты, акции, рассылки
-
-
Финансовый аудитор
-
Кассовые смены, ДДС, P&L
+
+
HR-ассистент
+
График смен, переработки
- -
- -
diff --git a/src/app/shell/BottomNav.tsx b/src/app/shell/BottomNav.tsx index 2cec3a9..ba5ae5a 100644 --- a/src/app/shell/BottomNav.tsx +++ b/src/app/shell/BottomNav.tsx @@ -1,114 +1,109 @@ -import React, { useState } from 'react'; -import { NavLink, useNavigate } from 'react-router-dom'; -import { useDemoStore } from '../../mock/store'; +import React, { useState } from 'react'; +import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, - Workflow, - Plus, - MessageSquare, + Grid, + CheckSquare, + MessageSquareText, Menu, - X, } from 'lucide-react'; -import { SYSTEM_MODULES } from '../../core/modules/registry'; +import { MobileMenuDrawer } from './MobileMenuDrawer'; export const BottomNav: React.FC = () => { - const { setIsQuickActionsOpen } = useDemoStore(); - const [isMoreMenuOpen, setIsMoreMenuOpen] = useState(false); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const location = useLocation(); const navigate = useNavigate(); + const isCatalogActive = location.pathname === '/' && !location.search.includes('tab=dashboard'); + const isDashboardActive = location.pathname === '/' && location.search.includes('tab=dashboard'); + return ( <> - {/* More menu drawer on mobile */} - {isMoreMenuOpen && ( -
-
setIsMoreMenuOpen(false)} - /> -
-
- Все разделы Кроны - -
- -
- {SYSTEM_MODULES.map((m) => ( - - ))} -
-
-
- )} - - {/* Persistent Bottom Bar */} -
+ + {/* Notifications Trigger */} + + + {/* Profile / Menu Drawer Trigger */} + +
+
+ + {/* Desktop & Tablet Topbar (>= 768px) */} +
{/* Left: Profile & Location controls */}
{/* Profile Switcher */}
-
+
- Профиль бизнеса + Отрасль - + {profile.name}
@@ -97,285 +150,195 @@ export const Topbar: React.FC = () => {
{/* Dropdown Menu */} -
-
- Отраслевые профили Кроны +
+
+ Отраслевые профили
-
+
{Object.values(BUSINESS_PROFILES).map((p) => ( ))}
- {/* Location Selector */} -
-
- - {activeLocation} - -
- -
-
- Точки продаж / Локации + {/* Location Switcher */} + {profile.locations.length > 1 && ( +
+
+ + + {activeLocation} + +
- {profile.locations.map((loc) => ( - - ))} -
-
- {/* Role (RBAC) Switcher */} -
-
setIsRoleDropdownOpen((prev) => !prev)} - className="flex items-center gap-1.5 bg-krona-surface border border-krona-border rounded-lg px-2.5 py-1.5 cursor-pointer hover:border-krona-border-strong text-xs shadow-krona-soft" - > - +
+ {profile.locations.map((loc) => ( + + ))} +
+
+ )} + + {/* Role Badge (RBAC) */} +
+
+
- Роль - + {currentRole.badge} + + {currentRole.label} +
-
-
- Переключение роли (RBAC) +
+
+ Переключение роли
-
- {Object.values(SYSTEM_ROLES).map((r) => ( -
-
{r.description}
- - ))} + + ); + })}
- {/* Center: Global Search & Shortcuts */} -
+ {/* Center: Command Search Palette Trigger */} +
- {/* Right: Quick actions, AI, Themes, Viewports, Notifications */} -
- {/* AI trigger */} + {/* Right: AI + Theme + Notifications */} +
+ {/* AI Trigger */} - {/* Quick Action (+) button */} - - - {/* Viewport Switcher */} -
+ {/* Theme switcher */} +
- - -
- {/* Theme Switcher */} -
- - - -
- - {/* Notifications bell */} + {/* Notifications */}
- {/* Horizontal Module Subnavigation Tabs */} + {/* Subheader / Subtabs Navigation Bar (Desktop & Tablet) */} {activeSubtabs.length > 0 && ( - +
)} + + {/* Mobile Menu Drawer */} + setIsMobileDrawerOpen(false)} + /> ); }; diff --git a/src/features/__tests__/configurationAndCrmInteractions.test.tsx b/src/features/__tests__/configurationAndCrmInteractions.test.tsx index 40191b2..2a91724 100644 --- a/src/features/__tests__/configurationAndCrmInteractions.test.tsx +++ b/src/features/__tests__/configurationAndCrmInteractions.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, within } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import '@testing-library/jest-dom/vitest'; import { DynamicCardRenderer } from '../metadata/DynamicCardRenderer'; @@ -107,21 +107,22 @@ describe('Configuration Engine & CRM Advanced Interactions', () => { /> ); - // Verify column headers exist - expect(screen.getByText('VIN номер')).toBeInTheDocument(); - expect(screen.getByText('Пробег (км)')).toBeInTheDocument(); + // Verify column headers exist in table + const table = screen.getByRole('table'); + expect(within(table).getByText('VIN номер')).toBeInTheDocument(); + expect(within(table).getByText('Пробег (км)')).toBeInTheDocument(); - // Verify row values exist - expect(screen.getByText('WAUZZZ8V1GA100001')).toBeInTheDocument(); - expect(screen.getByText('XW8ZZZ3CZ8G200002')).toBeInTheDocument(); + // Verify row values exist in table + expect(within(table).getByText('WAUZZZ8V1GA100001')).toBeInTheDocument(); + expect(within(table).getByText('XW8ZZZ3CZ8G200002')).toBeInTheDocument(); // Filter via search input const searchInput = screen.getByPlaceholderText('Поиск по записям...'); fireEvent.change(searchInput, { target: { value: 'WAUZZZ' } }); // First record stays, second record is filtered out - expect(screen.getByText('WAUZZZ8V1GA100001')).toBeInTheDocument(); - expect(screen.queryByText('XW8ZZZ3CZ8G200002')).not.toBeInTheDocument(); + expect(within(table).getByText('WAUZZZ8V1GA100001')).toBeInTheDocument(); + expect(within(table).queryByText('XW8ZZZ3CZ8G200002')).not.toBeInTheDocument(); }); }); diff --git a/src/features/catalog/ModuleInfoModal.tsx b/src/features/catalog/ModuleInfoModal.tsx index 7909b37..0f0764c 100644 --- a/src/features/catalog/ModuleInfoModal.tsx +++ b/src/features/catalog/ModuleInfoModal.tsx @@ -104,7 +104,7 @@ export const ModuleInfoModal: React.FC = ({ module, isOpen
- 2. Предполагаемые и реализованные возможности + 2. Ключевые функциональные возможности
{module.details.expectedCapabilities.map((cap, idx) => ( @@ -123,7 +123,7 @@ export const ModuleInfoModal: React.FC = ({ module, isOpen
- 3. Текущий статус готовности в платформе + 3. Статус реализации
{readinessInfo.label}: {module.details.currentStatus} @@ -134,7 +134,7 @@ export const ModuleInfoModal: React.FC = ({ module, isOpen
- 4. Планируемые интеграции и API-коннекторы + 4. Запланированные интеграции
{module.details.plannedIntegrations.map((integ, idx) => ( diff --git a/src/features/catalog/ModulesCatalog.tsx b/src/features/catalog/ModulesCatalog.tsx index ca692db..60a925a 100644 --- a/src/features/catalog/ModulesCatalog.tsx +++ b/src/features/catalog/ModulesCatalog.tsx @@ -60,8 +60,7 @@ export const ModulesCatalog: React.FC = () => { const matchTitle = m.title.toLowerCase().includes(q); const matchDesc = m.description.toLowerCase().includes(q); const matchFeatures = m.features.some((f) => f.toLowerCase().includes(q)); - const matchCategory = CATALOG_CATEGORIES.find((c) => c.id === m.category)?.label.toLowerCase().includes(q); - if (!matchTitle && !matchDesc && !matchFeatures && !matchCategory) { + if (!matchTitle && !matchDesc && !matchFeatures) { return false; } } diff --git a/src/features/metadata/DynamicFormRenderer.tsx b/src/features/metadata/DynamicFormRenderer.tsx index e38d0ce..954cb36 100644 --- a/src/features/metadata/DynamicFormRenderer.tsx +++ b/src/features/metadata/DynamicFormRenderer.tsx @@ -1067,15 +1067,15 @@ export const DynamicFormRenderer: React.FC = ({ ); })} - {/* Action buttons */} + {/* Action buttons (sticky on mobile for easy reachability) */} {mode !== 'readonly' && ( -
+
{onCancel && ( - )} -
diff --git a/src/features/metadata/DynamicTable.tsx b/src/features/metadata/DynamicTable.tsx index f39feca..7b96eb5 100644 --- a/src/features/metadata/DynamicTable.tsx +++ b/src/features/metadata/DynamicTable.tsx @@ -142,8 +142,95 @@ export const DynamicTable: React.FC = ({
- {/* Table Element */} -
+ {/* Mobile Card List View (< 768px) */} +
+ {paginatedRecords.map((row) => { + const primaryCol = columns.find((c) => c.key === entity.primaryFieldKey) || columns[0]; + const primaryVal = primaryCol ? formatFieldValue(primaryCol.type, row[primaryCol.key], primaryCol) : row.id; + const statusCol = columns.find((c) => c.key === 'status' || c.type === 'status'); + const statusVal = statusCol ? formatFieldValue(statusCol.type, row[statusCol.key], statusCol) : null; + const otherCols = columns.filter((c) => c.key !== primaryCol?.key && c.key !== statusCol?.key).slice(0, 4); + + return ( +
onRowClick && onRowClick(row)} + className="p-3.5 bg-krona-surface border border-krona-border rounded-xl shadow-xs space-y-2.5 active:bg-krona-workspace/60 transition-colors cursor-pointer" + > +
+
+
+ {primaryVal} +
+
+ ID: {row.id.slice(0, 10)}... +
+
+ {statusVal && ( + + {statusVal} + + )} +
+ + {otherCols.length > 0 && ( +
+ {otherCols.map((c) => ( +
+
+ {c.label} +
+
+ {formatFieldValue(c.type, row[c.key], c)} +
+
+ ))} +
+ )} + + {/* Mobile Card Actions */} +
e.stopPropagation()} + > + {onRowClick && ( + + )} + {onEdit && ( + + )} + {onDelete && ( + + )} +
+
+ ); + })} + + {paginatedRecords.length === 0 && !isLoading && ( +
+ +

Записей пока нет

+ {onCreateNew && ( + + )} +
+ )} +
+ + {/* Desktop/Tablet Table Element (>= 768px) */} +
diff --git a/src/features/settings/SettingsModule.tsx b/src/features/settings/SettingsModule.tsx index 703c185..6d4e981 100644 --- a/src/features/settings/SettingsModule.tsx +++ b/src/features/settings/SettingsModule.tsx @@ -311,7 +311,7 @@ export const SettingsModule: React.FC = () => { disabled={isResetting} > - {isResetting ? 'Сброс...' : 'Сбросить демо-данные'} + {isResetting ? 'Сброс...' : 'Сбросить базу данных'}
diff --git a/src/features/settings/entities/EntityCatalog.tsx b/src/features/settings/entities/EntityCatalog.tsx index e642f4f..bd5c57a 100644 --- a/src/features/settings/entities/EntityCatalog.tsx +++ b/src/features/settings/entities/EntityCatalog.tsx @@ -8,6 +8,8 @@ import { Modal } from '../../../shared/ui/Modal'; import { Badge } from '../../../shared/ui/Badge'; import { MaturityBadge } from '../../../shared/ui/MaturityBadge'; import { useToast } from '../../../shared/ui/useToast'; +import { resetDatabase } from '../../../services/storage/indexedDb'; +import { useDemoStore } from '../../../mock/store'; import { Layers, Plus, @@ -20,6 +22,7 @@ import { Sparkles, CreditCard, AlertTriangle, + RotateCcw, } from 'lucide-react'; export const EntityCatalog: React.FC = () => { @@ -46,6 +49,7 @@ export const EntityCatalog: React.FC = () => { // Delete modal state const [entityToDelete, setEntityToDelete] = useState(null); + const [isResetting, setIsResetting] = useState(false); // Filtered entities const filteredEntities = useMemo(() => { @@ -205,6 +209,27 @@ export const EntityCatalog: React.FC = () => { } }; + const handleResetDatabase = async () => { + try { + setIsResetting(true); + await resetDatabase(); + useDemoStore.getState().resetCatalogFilters(); + showToast({ + type: 'info', + title: 'База данных сброшена', + message: 'База данных успешно сброшена к начальному состоянию', + }); + } catch (err) { + showToast({ + type: 'error', + title: 'Ошибка сброса', + message: err instanceof Error ? err.message : String(err), + }); + } finally { + setIsResetting(false); + } + }; + return (
{/* Top Banner */} @@ -223,16 +248,27 @@ export const EntityCatalog: React.FC = () => {

- +
+ + +
{/* Stats row */} diff --git a/src/features/settings/entities/FormBuilder.tsx b/src/features/settings/entities/FormBuilder.tsx index 6252f9c..de07736 100644 --- a/src/features/settings/entities/FormBuilder.tsx +++ b/src/features/settings/entities/FormBuilder.tsx @@ -634,18 +634,21 @@ export const FormBuilder: React.FC = () => {
diff --git a/src/shared/lib/useResponsive.ts b/src/shared/lib/useResponsive.ts new file mode 100644 index 0000000..4d6e1a4 --- /dev/null +++ b/src/shared/lib/useResponsive.ts @@ -0,0 +1,63 @@ +import { useState, useEffect } from 'react'; + +export interface ResponsiveState { + width: number; + height: number; + isMobile: boolean; + isTablet: boolean; + isDesktop: boolean; + isPortrait: boolean; + isLandscape: boolean; +} + +export function useResponsive(): ResponsiveState { + const [state, setState] = useState(() => { + const width = typeof window !== 'undefined' ? window.innerWidth : 1440; + const height = typeof window !== 'undefined' ? window.innerHeight : 900; + return { + width, + height, + isMobile: width < 768, + isTablet: width >= 768 && width < 1200, + isDesktop: width >= 1200, + isPortrait: height >= width, + isLandscape: width > height, + }; + }); + + useEffect(() => { + if (typeof window === 'undefined') return; + + let timeoutId: any; + const handleResize = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + const width = window.innerWidth; + const height = window.innerHeight; + setState({ + width, + height, + isMobile: width < 768, + isTablet: width >= 768 && width < 1200, + isDesktop: width >= 1200, + isPortrait: height >= width, + isLandscape: width > height, + }); + }, 50); + }; + + window.addEventListener('resize', handleResize); + window.addEventListener('orientationchange', handleResize); + + // Initial check + handleResize(); + + return () => { + clearTimeout(timeoutId); + window.removeEventListener('resize', handleResize); + window.removeEventListener('orientationchange', handleResize); + }; + }, []); + + return state; +} diff --git a/src/shared/ui/Drawer.tsx b/src/shared/ui/Drawer.tsx index 0a0003d..440c06d 100644 --- a/src/shared/ui/Drawer.tsx +++ b/src/shared/ui/Drawer.tsx @@ -114,10 +114,10 @@ export const Drawer: React.FC = ({ /> {/* Drawer panel */} -
+
= ({ }; return ( -
+
{/* Backdrop */}
= ({ {/* Modal Dialog */}
{/* Header */} -
+

{title} @@ -151,13 +151,13 @@ export const Modal: React.FC = ({

{/* Body */} -
+
{children}
{/* Footer */} {footer && ( -
+
{footer}
)} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo index 0306993..2369f15 100644 --- a/tsconfig.app.tsbuildinfo +++ b/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/app/guards/routeguard.tsx","./src/app/layouts/appshell.tsx","./src/app/providers/appproviders.tsx","./src/app/providers/tenantprovider.tsx","./src/app/providers/tenantcontext.ts","./src/app/router/index.tsx","./src/app/shell/bottomnav.tsx","./src/app/shell/commandpalette.tsx","./src/app/shell/notificationsdrawer.tsx","./src/app/shell/quickactionsmodal.tsx","./src/app/shell/rightcommandpanel.tsx","./src/app/shell/sidebar.tsx","./src/app/shell/topbar.tsx","./src/core/capabilities/types.ts","./src/core/modules/registry.ts","./src/core/modules/types.ts","./src/core/permissions/types.ts","./src/core/profiles/profiles.ts","./src/core/profiles/types.ts","./src/demo/democlock.ts","./src/demo/index.ts","./src/demo/scenarios.ts","./src/demo/seedstate.ts","./src/entities/index.ts","./src/entities/types.ts","./src/entities/commerce/order.ts","./src/entities/commerce/product.ts","./src/entities/crm/company.ts","./src/entities/crm/contact.ts","./src/entities/crm/deal.ts","./src/entities/crm/lead.ts","./src/entities/inventory/warehouse.ts","./src/entities/metadata/registry.ts","./src/features/__tests__/accessibilityandnavigation.test.tsx","./src/features/__tests__/configurationandcrminteractions.test.tsx","./src/features/__tests__/fieldtypescontract.test.ts","./src/features/__tests__/fieldtypesintegration.test.tsx","./src/features/__tests__/phasefhardening.test.tsx","./src/features/__tests__/pilot1flows.test.ts","./src/features/__tests__/targetprototypeflows.test.tsx","./src/features/ai/aicentermodule.tsx","./src/features/analytics/analyticsmodule.tsx","./src/features/apps/appscatalogmodule.tsx","./src/features/automation/automationmodule.tsx","./src/features/cms/cmsmodule.tsx","./src/features/communications/communicationsmodule.tsx","./src/features/control-center/api/index.ts","./src/features/control-center/components/confirmoperatoraction.tsx","./src/features/control-center/components/controlcentershell.tsx","./src/features/control-center/components/controlrouteguard.tsx","./src/features/control-center/components/heartbeattimeline.tsx","./src/features/control-center/components/inspectordrawer.tsx","./src/features/control-center/components/moneydisplay.tsx","./src/features/control-center/components/operatorsessionswitcher.tsx","./src/features/control-center/components/servermonitoroutagebanner.tsx","./src/features/control-center/components/statusbadge.tsx","./src/features/control-center/contracts/index.ts","./src/features/control-center/fixtures/seeddata.ts","./src/features/control-center/hooks/index.ts","./src/features/control-center/pages/announcementspage.tsx","./src/features/control-center/pages/auditpage.tsx","./src/features/control-center/pages/billingpage.tsx","./src/features/control-center/pages/controlaccessgatepage.tsx","./src/features/control-center/pages/controlforbiddenpage.tsx","./src/features/control-center/pages/incidentspage.tsx","./src/features/control-center/pages/infrastructurepage.tsx","./src/features/control-center/pages/installationspage.tsx","./src/features/control-center/pages/modulespage.tsx","./src/features/control-center/pages/organizationspage.tsx","./src/features/control-center/pages/overviewpage.tsx","./src/features/control-center/pages/index.ts","./src/features/control-center/store/db.ts","./src/features/control-center/store/kccstore.ts","./src/features/control-center/tests/components.test.tsx","./src/features/control-center/tests/contracts.test.ts","./src/features/control-center/tests/rbac.test.ts","./src/features/crm/crmmodule.tsx","./src/features/crm/csvimportwizard.tsx","./src/features/dashboard/dynamicdashboard.tsx","./src/features/employees/employeesmodule.tsx","./src/features/finance/financemodule.tsx","./src/features/helpdesk/helpdeskmodule.tsx","./src/features/inventory/inventorymodule.tsx","./src/features/marketing/marketingmodule.tsx","./src/features/metadata/customentitypage.tsx","./src/features/metadata/dynamiccardrenderer.tsx","./src/features/metadata/dynamicformrenderer.tsx","./src/features/metadata/dynamictable.tsx","./src/features/metadata/__tests__/dynamicrenderer.test.tsx","./src/features/operations/baysview.tsx","./src/features/operations/bookingcalendarview.tsx","./src/features/operations/deliveryview.tsx","./src/features/operations/diningview.tsx","./src/features/operations/kdsview.tsx","./src/features/operations/operationsmodule.tsx","./src/features/operations/posview.tsx","./src/features/operations/purchasingview.tsx","./src/features/operations/recipesview.tsx","./src/features/operations/workordersview.tsx","./src/features/sales/salesmodule.tsx","./src/features/settings/settingsmodule.tsx","./src/features/settings/entities/cardlayoutbuilder.tsx","./src/features/settings/entities/entitycatalog.tsx","./src/features/settings/entities/fieldbuilder.tsx","./src/features/settings/entities/formbuilder.tsx","./src/features/settings/entities/viewbuilder.tsx","./src/features/settings/entities/fieldtypesmeta.ts","./src/features/settings/entities/__tests__/configurationengine.test.tsx","./src/features/tasks/tasksmodule.tsx","./src/mock/mockapi.ts","./src/mock/mockdata.ts","./src/mock/store.ts","./src/pages/aipage.tsx","./src/pages/analyticspage.tsx","./src/pages/appspage.tsx","./src/pages/automationpage.tsx","./src/pages/cardlayoutbuilderpage.tsx","./src/pages/cmspage.tsx","./src/pages/communicationspage.tsx","./src/pages/crmpage.tsx","./src/pages/customentityroutepage.tsx","./src/pages/dashboardpage.tsx","./src/pages/employeespage.tsx","./src/pages/entitycatalogpage.tsx","./src/pages/fieldbuilderpage.tsx","./src/pages/financepage.tsx","./src/pages/formbuilderpage.tsx","./src/pages/helpdeskpage.tsx","./src/pages/inventorypage.tsx","./src/pages/marketingpage.tsx","./src/pages/operationspage.tsx","./src/pages/salespage.tsx","./src/pages/settingspage.tsx","./src/pages/taskspage.tsx","./src/pages/viewbuilderpage.tsx","./src/pages/index.ts","./src/pages/error/forbiddenpage.tsx","./src/pages/error/notfoundpage.tsx","./src/services/__tests__/tenantisolation.test.ts","./src/services/contracts/audit.ts","./src/services/contracts/commerce.ts","./src/services/contracts/context.ts","./src/services/contracts/crm.ts","./src/services/contracts/finance.ts","./src/services/contracts/index.ts","./src/services/contracts/inventory.ts","./src/services/contracts/metadata.ts","./src/services/contracts/repository.ts","./src/services/hooks/index.ts","./src/services/hooks/useauditlog.ts","./src/services/hooks/usecrm.ts","./src/services/hooks/usefinance.ts","./src/services/hooks/useinventory.ts","./src/services/hooks/usemetadata.ts","./src/services/hooks/useorders.ts","./src/services/hooks/usestoragelistener.ts","./src/services/mock/auditservice.ts","./src/services/mock/baserepository.ts","./src/services/mock/commerceservice.ts","./src/services/mock/crmservice.ts","./src/services/mock/financeservice.ts","./src/services/mock/index.ts","./src/services/mock/inventoryservice.ts","./src/services/mock/metadataservice.ts","./src/services/mock/__tests__/services.test.ts","./src/services/storage/eventbus.ts","./src/services/storage/index.ts","./src/services/storage/indexeddb.ts","./src/services/storage/__tests__/indexeddb.test.ts","./src/shared/lib/currency.ts","./src/shared/lib/date.ts","./src/shared/lib/dnd.ts","./src/shared/lib/fieldengine.ts","./src/shared/lib/index.ts","./src/shared/lib/ulid.ts","./src/shared/lib/__tests__/ulid.test.ts","./src/shared/ui/badge.tsx","./src/shared/ui/button.tsx","./src/shared/ui/checkbox.tsx","./src/shared/ui/drawer.tsx","./src/shared/ui/emptystate.tsx","./src/shared/ui/errorboundary.tsx","./src/shared/ui/iconbutton.tsx","./src/shared/ui/input.tsx","./src/shared/ui/maturitybadge.tsx","./src/shared/ui/modal.tsx","./src/shared/ui/select.tsx","./src/shared/ui/skeleton.tsx","./src/shared/ui/statcard.tsx","./src/shared/ui/toast.tsx","./src/shared/ui/toastcontext.ts","./src/shared/ui/index.ts","./src/shared/ui/usetoast.ts","./src/shared/utils/formatters.ts","./src/stores/demo.ts","./src/stores/index.ts","./src/stores/navigation.ts","./src/stores/theme.ts","./src/test/setup.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/app/guards/routeguard.tsx","./src/app/layouts/appshell.tsx","./src/app/providers/appproviders.tsx","./src/app/providers/tenantprovider.tsx","./src/app/providers/tenantcontext.ts","./src/app/router/index.tsx","./src/app/shell/bottomnav.tsx","./src/app/shell/commandpalette.tsx","./src/app/shell/mobilemenudrawer.tsx","./src/app/shell/notificationsdrawer.tsx","./src/app/shell/quickactionsmodal.tsx","./src/app/shell/rightcommandpanel.tsx","./src/app/shell/sidebar.tsx","./src/app/shell/topbar.tsx","./src/core/capabilities/types.ts","./src/core/modules/registry.ts","./src/core/modules/types.ts","./src/core/permissions/types.ts","./src/core/profiles/profiles.ts","./src/core/profiles/types.ts","./src/demo/democlock.ts","./src/demo/index.ts","./src/demo/scenarios.ts","./src/demo/seedstate.ts","./src/entities/index.ts","./src/entities/types.ts","./src/entities/commerce/order.ts","./src/entities/commerce/product.ts","./src/entities/crm/company.ts","./src/entities/crm/contact.ts","./src/entities/crm/deal.ts","./src/entities/crm/lead.ts","./src/entities/inventory/warehouse.ts","./src/entities/metadata/registry.ts","./src/features/__tests__/accessibilityandnavigation.test.tsx","./src/features/__tests__/configurationandcrminteractions.test.tsx","./src/features/__tests__/fieldtypescontract.test.ts","./src/features/__tests__/fieldtypesintegration.test.tsx","./src/features/__tests__/phasefhardening.test.tsx","./src/features/__tests__/pilot1flows.test.ts","./src/features/__tests__/targetprototypeflows.test.tsx","./src/features/ai/aicentermodule.tsx","./src/features/analytics/analyticsmodule.tsx","./src/features/apps/appscatalogmodule.tsx","./src/features/automation/automationmodule.tsx","./src/features/catalog/modulecard.tsx","./src/features/catalog/moduleinfomodal.tsx","./src/features/catalog/modulescatalog.tsx","./src/features/catalog/catalogdata.ts","./src/features/cms/cmsmodule.tsx","./src/features/communications/communicationsmodule.tsx","./src/features/crm/crmmodule.tsx","./src/features/crm/csvimportwizard.tsx","./src/features/crm/constants.ts","./src/features/crm/components/crmentitymodals.tsx","./src/features/crm/components/crmkanbanview.tsx","./src/features/crm/components/crmtableview.tsx","./src/features/crm/components/customer360drawer.tsx","./src/features/crm/components/dealwonordermodal.tsx","./src/features/crm/components/leadconversionmodal.tsx","./src/features/crm/components/index.ts","./src/features/dashboard/dynamicdashboard.tsx","./src/features/employees/employeesmodule.tsx","./src/features/finance/financemodule.tsx","./src/features/helpdesk/helpdeskmodule.tsx","./src/features/inventory/inventorymodule.tsx","./src/features/marketing/marketingmodule.tsx","./src/features/metadata/customentitypage.tsx","./src/features/metadata/dynamiccardrenderer.tsx","./src/features/metadata/dynamicformrenderer.tsx","./src/features/metadata/dynamictable.tsx","./src/features/metadata/__tests__/dynamicrenderer.test.tsx","./src/features/operations/baysview.tsx","./src/features/operations/bookingcalendarview.tsx","./src/features/operations/deliveryview.tsx","./src/features/operations/diningview.tsx","./src/features/operations/kdsview.tsx","./src/features/operations/operationsmodule.tsx","./src/features/operations/posview.tsx","./src/features/operations/purchasingview.tsx","./src/features/operations/recipesview.tsx","./src/features/operations/workordersview.tsx","./src/features/sales/salesmodule.tsx","./src/features/settings/settingsmodule.tsx","./src/features/settings/entities/cardlayoutbuilder.tsx","./src/features/settings/entities/entitycatalog.tsx","./src/features/settings/entities/fieldbuilder.tsx","./src/features/settings/entities/formbuilder.tsx","./src/features/settings/entities/viewbuilder.tsx","./src/features/settings/entities/fieldtypesmeta.ts","./src/features/settings/entities/useformbuilderhistory.ts","./src/features/settings/entities/useviewbuildercolumns.ts","./src/features/settings/entities/__tests__/configurationengine.test.tsx","./src/features/tasks/tasksmodule.tsx","./src/mock/mockapi.ts","./src/mock/mockdata.ts","./src/mock/store.ts","./src/pages/aipage.tsx","./src/pages/analyticspage.tsx","./src/pages/appspage.tsx","./src/pages/automationpage.tsx","./src/pages/cardlayoutbuilderpage.tsx","./src/pages/cmspage.tsx","./src/pages/communicationspage.tsx","./src/pages/crmpage.tsx","./src/pages/customentityroutepage.tsx","./src/pages/dashboardpage.tsx","./src/pages/employeespage.tsx","./src/pages/entitycatalogpage.tsx","./src/pages/fieldbuilderpage.tsx","./src/pages/financepage.tsx","./src/pages/formbuilderpage.tsx","./src/pages/helpdeskpage.tsx","./src/pages/inventorypage.tsx","./src/pages/marketingpage.tsx","./src/pages/operationspage.tsx","./src/pages/procurementpage.tsx","./src/pages/salespage.tsx","./src/pages/settingspage.tsx","./src/pages/taskspage.tsx","./src/pages/viewbuilderpage.tsx","./src/pages/index.ts","./src/pages/error/forbiddenpage.tsx","./src/pages/error/notfoundpage.tsx","./src/services/__tests__/tenantisolation.test.ts","./src/services/contracts/audit.ts","./src/services/contracts/commerce.ts","./src/services/contracts/context.ts","./src/services/contracts/crm.ts","./src/services/contracts/finance.ts","./src/services/contracts/index.ts","./src/services/contracts/inventory.ts","./src/services/contracts/metadata.ts","./src/services/contracts/repository.ts","./src/services/hooks/index.ts","./src/services/hooks/useauditlog.ts","./src/services/hooks/usecrm.ts","./src/services/hooks/usefinance.ts","./src/services/hooks/useinventory.ts","./src/services/hooks/usemetadata.ts","./src/services/hooks/useorders.ts","./src/services/hooks/usestoragelistener.ts","./src/services/mock/auditservice.ts","./src/services/mock/baserepository.ts","./src/services/mock/commerceservice.ts","./src/services/mock/crmservice.ts","./src/services/mock/financeservice.ts","./src/services/mock/index.ts","./src/services/mock/inventoryservice.ts","./src/services/mock/metadataservice.ts","./src/services/mock/__tests__/services.test.ts","./src/services/storage/eventbus.ts","./src/services/storage/index.ts","./src/services/storage/indexeddb.ts","./src/services/storage/__tests__/indexeddb.test.ts","./src/shared/lib/currency.ts","./src/shared/lib/date.ts","./src/shared/lib/dnd.ts","./src/shared/lib/fieldengine.ts","./src/shared/lib/index.ts","./src/shared/lib/ulid.ts","./src/shared/lib/useresponsive.ts","./src/shared/lib/__tests__/ulid.test.ts","./src/shared/ui/badge.tsx","./src/shared/ui/button.tsx","./src/shared/ui/checkbox.tsx","./src/shared/ui/drawer.tsx","./src/shared/ui/emptystate.tsx","./src/shared/ui/errorboundary.tsx","./src/shared/ui/iconbutton.tsx","./src/shared/ui/input.tsx","./src/shared/ui/maturitybadge.tsx","./src/shared/ui/modal.tsx","./src/shared/ui/select.tsx","./src/shared/ui/skeleton.tsx","./src/shared/ui/statcard.tsx","./src/shared/ui/toast.tsx","./src/shared/ui/toastcontext.ts","./src/shared/ui/index.ts","./src/shared/ui/usetoast.ts","./src/shared/utils/formatters.ts","./src/stores/demo.ts","./src/stores/index.ts","./src/stores/navigation.ts","./src/stores/theme.ts","./src/test/setup.ts"],"version":"6.0.3"} \ No newline at end of file