feat(catalog): compact mobile modules catalog, filter drawer, view switcher, and quick access
This commit is contained in:
parent
6681e0ef56
commit
3dfeac7734
5 changed files with 383 additions and 23 deletions
|
|
@ -158,4 +158,33 @@ test.describe('Scenario 11: Universal Modules Catalog & Profiles', () => {
|
||||||
await catalogTab.click();
|
await catalogTab.click();
|
||||||
await expect(page.locator('[data-testid="modules-catalog-container"]')).toBeVisible();
|
await expect(page.locator('[data-testid="modules-catalog-container"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('8. Mobile filter drawer, density switcher, and quick access block function smoothly', async ({ page }) => {
|
||||||
|
// Quick access block
|
||||||
|
const quickAccess = page.locator('[data-testid="quick-access-block"]');
|
||||||
|
await expect(quickAccess).toBeVisible();
|
||||||
|
await expect(page.locator('[data-testid="quick-access-crm"]')).toBeVisible();
|
||||||
|
|
||||||
|
// Density toggle
|
||||||
|
const compactToggle = page.locator('[data-testid="density-toggle-compact"]');
|
||||||
|
const detailedToggle = page.locator('[data-testid="density-toggle-detailed"]');
|
||||||
|
await expect(compactToggle).toBeVisible();
|
||||||
|
await expect(detailedToggle).toBeVisible();
|
||||||
|
|
||||||
|
await compactToggle.click();
|
||||||
|
await expect(page.locator('text=Ключевые возможности:').first()).not.toBeVisible();
|
||||||
|
|
||||||
|
await detailedToggle.click();
|
||||||
|
await expect(page.locator('text=Ключевые возможности:').first()).toBeVisible();
|
||||||
|
|
||||||
|
// Filter drawer
|
||||||
|
const filterDrawerBtn = page.locator('[data-testid="mobile-filter-drawer-btn"]');
|
||||||
|
await expect(filterDrawerBtn).toBeVisible();
|
||||||
|
await filterDrawerBtn.click();
|
||||||
|
|
||||||
|
const applyDrawerBtn = page.locator('[data-testid="apply-filters-drawer-btn"]');
|
||||||
|
await expect(applyDrawerBtn).toBeVisible();
|
||||||
|
await applyDrawerBtn.click();
|
||||||
|
await expect(applyDrawerBtn).not.toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
184
src/features/catalog/CatalogFilterDrawer.tsx
Normal file
184
src/features/catalog/CatalogFilterDrawer.tsx
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Drawer } from '../../shared/ui/Drawer';
|
||||||
|
import { Button } from '../../shared/ui/Button';
|
||||||
|
import { BUSINESS_PROFILES } from '../../core/profiles/profiles';
|
||||||
|
import { CATALOG_CATEGORIES, READINESS_CONFIG, ReadinessStatus } from './catalogData';
|
||||||
|
import { Building, Layers, ShieldCheck, RotateCcw, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface CatalogFilterDrawerProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
profileFilter: string;
|
||||||
|
onSelectProfileFilter: (profileId: string) => void;
|
||||||
|
categoryFilter: string;
|
||||||
|
onSelectCategoryFilter: (categoryId: string) => void;
|
||||||
|
statusFilter: string;
|
||||||
|
onSelectStatusFilter: (status: string) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
resultsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CatalogFilterDrawer: React.FC<CatalogFilterDrawerProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
profileFilter,
|
||||||
|
onSelectProfileFilter,
|
||||||
|
categoryFilter,
|
||||||
|
onSelectCategoryFilter,
|
||||||
|
statusFilter,
|
||||||
|
onSelectStatusFilter,
|
||||||
|
onReset,
|
||||||
|
resultsCount,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Фильтры каталога"
|
||||||
|
subtitle={`Найдено модулей: ${resultsCount}`}
|
||||||
|
width="md"
|
||||||
|
footer={
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="md"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => {
|
||||||
|
onReset();
|
||||||
|
}}
|
||||||
|
data-testid="reset-filters-drawer-btn"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-1.5" />
|
||||||
|
<span>Сбросить</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
className="flex-1"
|
||||||
|
onClick={onClose}
|
||||||
|
data-testid="apply-filters-drawer-btn"
|
||||||
|
>
|
||||||
|
<span>Показать ({resultsCount})</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-6 text-xs text-krona-primary py-1">
|
||||||
|
{/* Section 1: Industry / Profile */}
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-krona-secondary">
|
||||||
|
<Building className="w-3.5 h-3.5 text-krona-gold" />
|
||||||
|
<span>Отраслевой фокус</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectProfileFilter('all')}
|
||||||
|
className={`p-3 rounded-xl border text-left flex items-center justify-between transition-colors ${
|
||||||
|
profileFilter === 'all'
|
||||||
|
? 'bg-krona-gold/15 border-krona-gold font-bold text-krona-primary'
|
||||||
|
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>Все модули (без отраслевого фокуса)</span>
|
||||||
|
{profileFilter === 'all' && <Check className="w-4 h-4 text-krona-gold" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectProfileFilter('recommended')}
|
||||||
|
className={`p-3 rounded-xl border text-left flex items-center justify-between transition-colors ${
|
||||||
|
profileFilter === 'recommended'
|
||||||
|
? 'bg-emerald-500/15 border-emerald-500 font-bold text-krona-primary'
|
||||||
|
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>Рекомендовано для активного профиля</span>
|
||||||
|
{profileFilter === 'recommended' && <Check className="w-4 h-4 text-emerald-500" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{Object.values(BUSINESS_PROFILES).map((p) => (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
onClick={() => onSelectProfileFilter(p.id)}
|
||||||
|
className={`p-3 rounded-xl border text-left flex items-center justify-between transition-colors ${
|
||||||
|
profileFilter === p.id
|
||||||
|
? 'bg-krona-gold/15 border-krona-gold font-bold text-krona-primary'
|
||||||
|
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{p.name}</span>
|
||||||
|
{profileFilter === p.id && <Check className="w-4 h-4 text-krona-gold" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 2: Categories */}
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-krona-secondary">
|
||||||
|
<Layers className="w-3.5 h-3.5 text-sky-500" />
|
||||||
|
<span>Категория</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
|
{CATALOG_CATEGORIES.map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => onSelectCategoryFilter(cat.id)}
|
||||||
|
className={`p-2.5 rounded-xl border text-left flex items-center justify-between text-xs transition-colors ${
|
||||||
|
categoryFilter === cat.id
|
||||||
|
? 'bg-sky-500/15 border-sky-500 font-bold text-krona-primary'
|
||||||
|
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{cat.label}</span>
|
||||||
|
{categoryFilter === cat.id && <Check className="w-3.5 h-3.5 text-sky-500 shrink-0 ml-1" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 3: Status / Readiness */}
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-wider text-krona-secondary">
|
||||||
|
<ShieldCheck className="w-3.5 h-3.5 text-emerald-500" />
|
||||||
|
<span>Статус реализации</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-1.5">
|
||||||
|
<button
|
||||||
|
onClick={() => onSelectStatusFilter('all')}
|
||||||
|
className={`p-2.5 rounded-xl border text-left flex items-center justify-between text-xs transition-colors ${
|
||||||
|
statusFilter === 'all'
|
||||||
|
? 'bg-emerald-500/15 border-emerald-500 font-bold text-krona-primary'
|
||||||
|
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>Все статусы реализации</span>
|
||||||
|
{statusFilter === 'all' && <Check className="w-4 h-4 text-emerald-500" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{(Object.keys(READINESS_CONFIG) as ReadinessStatus[]).map((st) => {
|
||||||
|
const info = READINESS_CONFIG[st];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={st}
|
||||||
|
onClick={() => onSelectStatusFilter(st)}
|
||||||
|
className={`p-2.5 rounded-xl border text-left flex items-center justify-between text-xs transition-colors ${
|
||||||
|
statusFilter === st
|
||||||
|
? 'bg-emerald-500/15 border-emerald-500 font-bold text-krona-primary'
|
||||||
|
: 'bg-krona-workspace border-krona-border text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{info.label}</span>
|
||||||
|
{statusFilter === st && <Check className="w-4 h-4 text-emerald-500" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -51,12 +51,14 @@ export interface ModuleCardProps {
|
||||||
module: CatalogModuleItem;
|
module: CatalogModuleItem;
|
||||||
onOpenDetails: (module: CatalogModuleItem) => void;
|
onOpenDetails: (module: CatalogModuleItem) => void;
|
||||||
isRecommended?: boolean;
|
isRecommended?: boolean;
|
||||||
|
isCompact?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModuleCard: React.FC<ModuleCardProps> = ({
|
export const ModuleCard: React.FC<ModuleCardProps> = ({
|
||||||
module,
|
module,
|
||||||
onOpenDetails,
|
onOpenDetails,
|
||||||
isRecommended = false,
|
isRecommended = false,
|
||||||
|
isCompact = false,
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const readiness = READINESS_CONFIG[module.readiness];
|
const readiness = READINESS_CONFIG[module.readiness];
|
||||||
|
|
@ -69,17 +71,19 @@ export const ModuleCard: React.FC<ModuleCardProps> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={`module-card-${module.id}`}
|
data-testid={`module-card-${module.id}`}
|
||||||
className={`bg-krona-surface rounded-2xl border transition-all duration-200 flex flex-col justify-between hover:shadow-krona-card hover:border-krona-gold/60 p-5 ${
|
className={`bg-krona-surface rounded-2xl border transition-all duration-200 flex flex-col justify-between hover:shadow-krona-card hover:border-krona-gold/60 ${
|
||||||
|
isCompact ? 'p-3.5 space-y-3' : 'p-5 space-y-4'
|
||||||
|
} ${
|
||||||
isRecommended
|
isRecommended
|
||||||
? 'border-krona-gold/50 bg-gradient-to-b from-krona-gold/5 to-transparent'
|
? 'border-krona-gold/50 bg-gradient-to-b from-krona-gold/5 to-transparent'
|
||||||
: 'border-krona-border'
|
: 'border-krona-border'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="space-y-3.5">
|
<div className={isCompact ? 'space-y-2' : 'space-y-3.5'}>
|
||||||
{/* Top meta bar: category + badges */}
|
{/* Top meta bar: category + badges */}
|
||||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="p-2 rounded-xl bg-krona-workspace border border-krona-border/60 shadow-xs">
|
<div className={`${isCompact ? 'p-1.5' : 'p-2'} rounded-xl bg-krona-workspace border border-krona-border/60 shadow-xs`}>
|
||||||
{ICON_COMPONENTS[module.iconName] || <Grid className="w-5 h-5 text-krona-secondary" />}
|
{ICON_COMPONENTS[module.iconName] || <Grid className="w-5 h-5 text-krona-secondary" />}
|
||||||
</div>
|
</div>
|
||||||
{isRecommended && (
|
{isRecommended && (
|
||||||
|
|
@ -91,23 +95,26 @@ export const ModuleCard: React.FC<ModuleCardProps> = ({
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 flex-wrap">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
<MaturityBadge level={readiness.maturity} size="sm" />
|
<MaturityBadge level={readiness.maturity} size="sm" />
|
||||||
|
{!isCompact && (
|
||||||
<Badge variant={availability.variant} size="sm">
|
<Badge variant={availability.variant} size="sm">
|
||||||
{availability.label}
|
{availability.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title and description */}
|
{/* Title and description */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-base font-bold text-krona-primary leading-tight">
|
<h3 className={`${isCompact ? 'text-sm' : 'text-base'} font-bold text-krona-primary leading-tight`}>
|
||||||
{module.title}
|
{module.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-krona-secondary mt-1.5 line-clamp-2 leading-relaxed">
|
<p className="text-xs text-krona-secondary mt-1 line-clamp-2 leading-relaxed">
|
||||||
{module.description}
|
{module.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features list */}
|
{/* Features list (Only in detailed mode) */}
|
||||||
|
{!isCompact && (
|
||||||
<div className="space-y-1.5 pt-1 border-t border-krona-border/50">
|
<div className="space-y-1.5 pt-1 border-t border-krona-border/50">
|
||||||
<span className="text-[10px] uppercase font-semibold tracking-wider text-krona-secondary block mb-1">
|
<span className="text-[10px] uppercase font-semibold tracking-wider text-krona-secondary block mb-1">
|
||||||
Ключевые возможности:
|
Ключевые возможности:
|
||||||
|
|
@ -119,10 +126,11 @@ export const ModuleCard: React.FC<ModuleCardProps> = ({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex items-center gap-2 pt-4 mt-4 border-t border-krona-border/60">
|
<div className={`flex items-center gap-2 ${isCompact ? 'pt-2.5 mt-2' : 'pt-4 mt-4'} border-t border-krona-border/60`}>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDemoStore } from '../../mock/store';
|
import { useDemoStore } from '../../mock/store';
|
||||||
import { BUSINESS_PROFILES } from '../../core/profiles/profiles';
|
import { BUSINESS_PROFILES } from '../../core/profiles/profiles';
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,6 +9,7 @@ import {
|
||||||
} from './catalogData';
|
} from './catalogData';
|
||||||
import { ModuleCard } from './ModuleCard';
|
import { ModuleCard } from './ModuleCard';
|
||||||
import { ModuleInfoModal } from './ModuleInfoModal';
|
import { ModuleInfoModal } from './ModuleInfoModal';
|
||||||
|
import { CatalogFilterDrawer } from './CatalogFilterDrawer';
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
|
|
@ -18,11 +20,22 @@ import {
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Grid,
|
Grid,
|
||||||
|
Zap,
|
||||||
|
LayoutGrid,
|
||||||
|
List,
|
||||||
|
SlidersHorizontal,
|
||||||
|
Users,
|
||||||
|
ShoppingBag,
|
||||||
|
Store,
|
||||||
|
Package,
|
||||||
|
CreditCard,
|
||||||
|
CheckSquare,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '../../shared/ui/Button';
|
import { Button } from '../../shared/ui/Button';
|
||||||
import { DynamicDashboard } from '../dashboard/DynamicDashboard';
|
import { DynamicDashboard } from '../dashboard/DynamicDashboard';
|
||||||
|
|
||||||
export const ModulesCatalog: React.FC = () => {
|
export const ModulesCatalog: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
currentProfileId,
|
currentProfileId,
|
||||||
catalogProfileFilter,
|
catalogProfileFilter,
|
||||||
|
|
@ -38,6 +51,19 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
|
|
||||||
const [activeViewMode, setActiveViewMode] = useState<'catalog' | 'dashboard'>('catalog');
|
const [activeViewMode, setActiveViewMode] = useState<'catalog' | 'dashboard'>('catalog');
|
||||||
const [selectedModuleForModal, setSelectedModuleForModal] = useState<CatalogModuleItem | null>(null);
|
const [selectedModuleForModal, setSelectedModuleForModal] = useState<CatalogModuleItem | null>(null);
|
||||||
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
|
const [displayDensity, setDisplayDensity] = useState<'compact' | 'detailed'>(() => {
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
return 'compact';
|
||||||
|
}
|
||||||
|
return 'detailed';
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeFiltersCount =
|
||||||
|
(catalogProfileFilter !== 'all' ? 1 : 0) +
|
||||||
|
(catalogCategoryFilter !== 'all' ? 1 : 0) +
|
||||||
|
(catalogStatusFilter !== 'all' ? 1 : 0) +
|
||||||
|
(catalogSearchQuery.trim() ? 1 : 0);
|
||||||
|
|
||||||
const activeProfile = BUSINESS_PROFILES[currentProfileId] || BUSINESS_PROFILES.restaurant_hall_delivery;
|
const activeProfile = BUSINESS_PROFILES[currentProfileId] || BUSINESS_PROFILES.restaurant_hall_delivery;
|
||||||
|
|
||||||
|
|
@ -165,9 +191,12 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-krona-primary">
|
<h1 className="text-2xl md:text-3xl font-bold text-krona-primary">
|
||||||
Единый каталог модулей Krona
|
Единый каталог модулей Krona
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs md:text-sm text-krona-secondary leading-relaxed">
|
<p className="text-xs md:text-sm text-krona-secondary leading-relaxed hidden md:block">
|
||||||
Платформа открыта для изучения целиком без предварительного выбора отрасли. Исследуйте Pilot 1 транзакции, интерактивные прототипы и перспективные блоки экосистемы.
|
Платформа открыта для изучения целиком без предварительного выбора отрасли. Исследуйте Pilot 1 транзакции, интерактивные прототипы и перспективные блоки экосистемы.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-xs text-krona-secondary leading-relaxed md:hidden">
|
||||||
|
Все 18 модулей единой бизнес-платформы
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats badges */}
|
{/* Stats badges */}
|
||||||
|
|
@ -214,6 +243,97 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Controls Bar: Mobile Filter Button + Density Toggle */}
|
||||||
|
<div className="flex items-center justify-between gap-3 bg-krona-surface p-3 rounded-2xl border border-krona-border shadow-krona-soft">
|
||||||
|
{/* Filter Drawer Trigger Button */}
|
||||||
|
<Button
|
||||||
|
variant={activeFiltersCount > 0 ? 'primary' : 'secondary'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setIsFilterDrawerOpen(true)}
|
||||||
|
data-testid="mobile-filter-drawer-btn"
|
||||||
|
className="text-xs flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="w-3.5 h-3.5" />
|
||||||
|
<span>Фильтры</span>
|
||||||
|
{activeFiltersCount > 0 && (
|
||||||
|
<span className="ml-1 px-1.5 py-0.2 rounded-full text-[10px] bg-white/20 text-white font-bold">
|
||||||
|
{activeFiltersCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Density Toggle */}
|
||||||
|
<div className="flex items-center bg-krona-workspace p-1 rounded-xl border border-krona-border text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDisplayDensity('compact')}
|
||||||
|
data-testid="density-toggle-compact"
|
||||||
|
className={`px-2.5 py-1 rounded-lg font-medium transition-colors flex items-center gap-1 ${
|
||||||
|
displayDensity === 'compact'
|
||||||
|
? 'bg-krona-surface text-krona-primary font-bold shadow-xs border border-krona-border'
|
||||||
|
: 'text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
title="Компактный вид карточек"
|
||||||
|
>
|
||||||
|
<LayoutGrid className="w-3.5 h-3.5" />
|
||||||
|
<span>Компактно</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDisplayDensity('detailed')}
|
||||||
|
data-testid="density-toggle-detailed"
|
||||||
|
className={`px-2.5 py-1 rounded-lg font-medium transition-colors flex items-center gap-1 ${
|
||||||
|
displayDensity === 'detailed'
|
||||||
|
? 'bg-krona-surface text-krona-primary font-bold shadow-xs border border-krona-border'
|
||||||
|
: 'text-krona-secondary hover:text-krona-primary'
|
||||||
|
}`}
|
||||||
|
title="Подробный вид карточек"
|
||||||
|
>
|
||||||
|
<List className="w-3.5 h-3.5" />
|
||||||
|
<span>Подробно</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Access Block */}
|
||||||
|
<div className="p-3.5 sm:p-4 rounded-2xl bg-krona-surface border border-krona-border shadow-krona-soft space-y-2.5" data-testid="quick-access-block">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-bold text-krona-primary uppercase tracking-wider">
|
||||||
|
<Zap className="w-3.5 h-3.5 text-krona-gold" />
|
||||||
|
<span>Быстрый доступ</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-krona-secondary">Ключевые модули платформы</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
|
||||||
|
{[
|
||||||
|
{ id: 'crm', title: 'CRM', path: '/crm', icon: Users, color: 'text-sky-500' },
|
||||||
|
{ id: 'pos', title: 'Продажи', path: '/sales', icon: ShoppingBag, color: 'text-emerald-500' },
|
||||||
|
{ id: 'operations', title: 'Касса и операции', path: '/operations', icon: Store, color: 'text-amber-500' },
|
||||||
|
{ id: 'inventory', title: 'Склад', path: '/inventory', icon: Package, color: 'text-blue-500' },
|
||||||
|
{ id: 'finance', title: 'Финансы', path: '/finance', icon: CreditCard, color: 'text-purple-500' },
|
||||||
|
{ id: 'tasks', title: 'Задачи', path: '/tasks', icon: CheckSquare, color: 'text-rose-500' },
|
||||||
|
].map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => navigate(item.path)}
|
||||||
|
data-testid={`quick-access-${item.id}`}
|
||||||
|
className="flex items-center gap-2 p-2 rounded-xl bg-krona-workspace hover:bg-krona-hover border border-krona-border transition-colors text-left group"
|
||||||
|
>
|
||||||
|
<div className="p-1.5 rounded-lg bg-krona-surface border border-krona-border/60 group-hover:border-krona-gold/40 shrink-0">
|
||||||
|
<Icon className={`w-3.5 h-3.5 ${item.color}`} />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-xs font-semibold text-krona-primary truncate">{item.title}</div>
|
||||||
|
<div className="text-[10px] text-krona-secondary truncate">Перейти</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Industry Profiles Filter Bar */}
|
{/* Industry Profiles Filter Bar */}
|
||||||
<div className="p-4 rounded-2xl bg-krona-surface border border-krona-border shadow-krona-soft space-y-3">
|
<div className="p-4 rounded-2xl bg-krona-surface border border-krona-border shadow-krona-soft space-y-3">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-krona-border/60 pb-2.5">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-krona-border/60 pb-2.5">
|
||||||
|
|
@ -393,6 +513,7 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
key={mod.id}
|
key={mod.id}
|
||||||
module={mod}
|
module={mod}
|
||||||
isRecommended={true}
|
isRecommended={true}
|
||||||
|
isCompact={displayDensity === 'compact'}
|
||||||
onOpenDetails={setSelectedModuleForModal}
|
onOpenDetails={setSelectedModuleForModal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -425,6 +546,7 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
key={mod.id}
|
key={mod.id}
|
||||||
module={mod}
|
module={mod}
|
||||||
isRecommended={false}
|
isRecommended={false}
|
||||||
|
isCompact={displayDensity === 'compact'}
|
||||||
onOpenDetails={setSelectedModuleForModal}
|
onOpenDetails={setSelectedModuleForModal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -457,6 +579,7 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
<ModuleCard
|
<ModuleCard
|
||||||
key={mod.id}
|
key={mod.id}
|
||||||
module={mod}
|
module={mod}
|
||||||
|
isCompact={displayDensity === 'compact'}
|
||||||
onOpenDetails={setSelectedModuleForModal}
|
onOpenDetails={setSelectedModuleForModal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
@ -474,6 +597,22 @@ export const ModulesCatalog: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Mobile Bottom-Sheet Filter Drawer */}
|
||||||
|
<CatalogFilterDrawer
|
||||||
|
isOpen={isFilterDrawerOpen}
|
||||||
|
onClose={() => setIsFilterDrawerOpen(false)}
|
||||||
|
profileFilter={catalogProfileFilter}
|
||||||
|
onSelectProfileFilter={(id) => setCatalogProfileFilter(id)}
|
||||||
|
categoryFilter={catalogCategoryFilter}
|
||||||
|
onSelectCategoryFilter={(id) => setCatalogCategoryFilter(id as any)}
|
||||||
|
statusFilter={catalogStatusFilter}
|
||||||
|
onSelectStatusFilter={(id) => setCatalogStatusFilter(id)}
|
||||||
|
onReset={() => {
|
||||||
|
resetCatalogFilters();
|
||||||
|
}}
|
||||||
|
resultsCount={isProfileFilterActive ? recommendedList.length + otherList.length : filteredModules.length}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Module Info Modal */}
|
{/* Module Info Modal */}
|
||||||
<ModuleInfoModal
|
<ModuleInfoModal
|
||||||
module={selectedModuleForModal}
|
module={selectedModuleForModal}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue