624 lines
29 KiB
TypeScript
624 lines
29 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useDemoStore } from '../../mock/store';
|
||
import { BUSINESS_PROFILES } from '../../core/profiles/profiles';
|
||
import {
|
||
CATALOG_MODULES,
|
||
CATALOG_CATEGORIES,
|
||
CatalogModuleItem,
|
||
} from './catalogData';
|
||
import { ModuleCard } from './ModuleCard';
|
||
import { ModuleInfoModal } from './ModuleInfoModal';
|
||
import { CatalogFilterDrawer } from './CatalogFilterDrawer';
|
||
import {
|
||
Search,
|
||
X,
|
||
Sparkles,
|
||
Layers,
|
||
Filter,
|
||
Building,
|
||
RotateCcw,
|
||
BarChart3,
|
||
Grid,
|
||
Zap,
|
||
LayoutGrid,
|
||
List,
|
||
SlidersHorizontal,
|
||
Users,
|
||
ShoppingBag,
|
||
Store,
|
||
Package,
|
||
CreditCard,
|
||
CheckSquare,
|
||
} from 'lucide-react';
|
||
import { Button } from '../../shared/ui/Button';
|
||
import { DynamicDashboard } from '../dashboard/DynamicDashboard';
|
||
|
||
export const ModulesCatalog: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const {
|
||
currentProfileId,
|
||
catalogProfileFilter,
|
||
setCatalogProfileFilter,
|
||
catalogCategoryFilter,
|
||
setCatalogCategoryFilter,
|
||
catalogStatusFilter,
|
||
setCatalogStatusFilter,
|
||
catalogSearchQuery,
|
||
setCatalogSearchQuery,
|
||
resetCatalogFilters,
|
||
} = useDemoStore();
|
||
|
||
const [activeViewMode, setActiveViewMode] = useState<'catalog' | 'dashboard'>('catalog');
|
||
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;
|
||
|
||
// Search and Category filtering
|
||
const filteredModules = useMemo(() => {
|
||
return CATALOG_MODULES.filter((m) => {
|
||
// Category filter
|
||
if (catalogCategoryFilter !== 'all' && m.category !== catalogCategoryFilter) {
|
||
return false;
|
||
}
|
||
|
||
// Status filter
|
||
if (catalogStatusFilter !== 'all' && m.readiness !== catalogStatusFilter) {
|
||
return false;
|
||
}
|
||
|
||
// Search filter
|
||
if (catalogSearchQuery.trim()) {
|
||
const q = catalogSearchQuery.toLowerCase();
|
||
const matchTitle = m.title.toLowerCase().includes(q);
|
||
const matchDesc = m.description.toLowerCase().includes(q);
|
||
const matchFeatures = m.features.some((f) => f.toLowerCase().includes(q));
|
||
if (!matchTitle && !matchDesc && !matchFeatures) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
});
|
||
}, [catalogCategoryFilter, catalogStatusFilter, catalogSearchQuery]);
|
||
|
||
// Split into recommended vs other when a specific profile or "recommended" is active
|
||
const isProfileFilterActive = catalogProfileFilter !== 'all';
|
||
const effectiveProfileId =
|
||
catalogProfileFilter === 'recommended' ? currentProfileId : catalogProfileFilter;
|
||
|
||
const { recommendedList, otherList } = useMemo(() => {
|
||
if (!isProfileFilterActive) {
|
||
return { recommendedList: [], otherList: filteredModules };
|
||
}
|
||
|
||
const rec: CatalogModuleItem[] = [];
|
||
const oth: CatalogModuleItem[] = [];
|
||
|
||
for (const m of filteredModules) {
|
||
if (m.recommendedProfiles.includes(effectiveProfileId)) {
|
||
rec.push(m);
|
||
} else {
|
||
oth.push(m);
|
||
}
|
||
}
|
||
|
||
return { recommendedList: rec, otherList: oth };
|
||
}, [isProfileFilterActive, effectiveProfileId, filteredModules]);
|
||
|
||
const stats = useMemo(() => {
|
||
const ready = CATALOG_MODULES.filter((m) => m.readiness === 'ready').length;
|
||
const target = CATALOG_MODULES.filter((m) => m.readiness === 'ui_prepared').length;
|
||
const planned = CATALOG_MODULES.filter((m) => m.readiness === 'planned').length;
|
||
const service = CATALOG_MODULES.filter((m) => m.readiness === 'service_required').length;
|
||
return { total: CATALOG_MODULES.length, ready, target, planned, service };
|
||
}, []);
|
||
|
||
return (
|
||
<div className="space-y-6 max-w-7xl mx-auto animate-fade-in pb-12" data-testid="modules-catalog-container">
|
||
{/* View Mode Switcher Header: Catalog vs KPI Dashboard */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 bg-krona-surface p-3.5 rounded-2xl border border-krona-border shadow-krona-soft">
|
||
<div className="flex items-center gap-2">
|
||
<div className="p-2 rounded-xl bg-krona-gold/15 text-krona-gold border border-krona-gold/30">
|
||
<Grid className="w-5 h-5" />
|
||
</div>
|
||
<div>
|
||
<span className="text-[10px] uppercase font-bold tracking-wider text-krona-secondary">
|
||
Обзор платформы Krona Business OS
|
||
</span>
|
||
<div className="text-xs text-krona-primary font-semibold">
|
||
Единый каталог всех 18 модулей и функциональных блоков
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tab switch between Catalog and Dashboard */}
|
||
<div className="flex items-center bg-krona-workspace p-1 rounded-xl border border-krona-border text-xs">
|
||
<button
|
||
onClick={() => setActiveViewMode('catalog')}
|
||
data-testid="view-tab-catalog"
|
||
className={`px-3 py-1.5 rounded-lg font-medium transition-colors flex items-center gap-1.5 ${
|
||
activeViewMode === 'catalog'
|
||
? 'bg-krona-surface text-krona-primary font-bold shadow-xs border border-krona-border'
|
||
: 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
>
|
||
<Grid className="w-3.5 h-3.5" />
|
||
<span>Каталог модулей ({stats.total})</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveViewMode('dashboard')}
|
||
data-testid="view-tab-dashboard"
|
||
className={`px-3 py-1.5 rounded-lg font-medium transition-colors flex items-center gap-1.5 ${
|
||
activeViewMode === 'dashboard'
|
||
? 'bg-krona-surface text-krona-primary font-bold shadow-xs border border-krona-border'
|
||
: 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
>
|
||
<BarChart3 className="w-3.5 h-3.5" />
|
||
<span>Операционная сводка / KPI</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* If Dashboard mode is active, render DynamicDashboard */}
|
||
{activeViewMode === 'dashboard' ? (
|
||
<DynamicDashboard />
|
||
) : (
|
||
/* Catalog Mode */
|
||
<div className="space-y-6">
|
||
{/* Main Hero Header */}
|
||
<div className="p-6 md:p-8 rounded-3xl bg-gradient-to-br from-[#252722] via-krona-surface to-[#1F211C] border border-krona-border shadow-krona-card space-y-4">
|
||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||
<div className="space-y-2 max-w-2xl">
|
||
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-500 text-xs font-bold uppercase tracking-wider">
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
<span>Все возможности бизнес-платформы</span>
|
||
</div>
|
||
<h1 className="text-2xl md:text-3xl font-bold text-krona-primary">
|
||
Единый каталог модулей Krona
|
||
</h1>
|
||
<p className="text-xs md:text-sm text-krona-secondary leading-relaxed hidden md:block">
|
||
Платформа открыта для изучения целиком без предварительного выбора отрасли. Исследуйте Pilot 1 транзакции, интерактивные прототипы и перспективные блоки экосистемы.
|
||
</p>
|
||
<p className="text-xs text-krona-secondary leading-relaxed md:hidden">
|
||
Все 18 модулей единой бизнес-платформы
|
||
</p>
|
||
</div>
|
||
|
||
{/* Stats badges */}
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-center text-xs">
|
||
<div className="p-3 rounded-xl bg-krona-workspace/80 border border-krona-border">
|
||
<div className="text-lg font-bold text-emerald-500">{stats.ready}</div>
|
||
<div className="text-[10px] text-krona-secondary">Работает в демо</div>
|
||
</div>
|
||
<div className="p-3 rounded-xl bg-krona-workspace/80 border border-krona-border">
|
||
<div className="text-lg font-bold text-sky-500">{stats.target}</div>
|
||
<div className="text-[10px] text-krona-secondary">Интерфейс готов</div>
|
||
</div>
|
||
<div className="p-3 rounded-xl bg-krona-workspace/80 border border-krona-border">
|
||
<div className="text-lg font-bold text-amber-500">{stats.planned}</div>
|
||
<div className="text-[10px] text-krona-secondary">Запланирован</div>
|
||
</div>
|
||
<div className="p-3 rounded-xl bg-krona-workspace/80 border border-krona-border">
|
||
<div className="text-lg font-bold text-purple-500">{stats.service}</div>
|
||
<div className="text-[10px] text-krona-secondary">Сервисные</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Live Search Input */}
|
||
<div className="relative pt-2">
|
||
<Search className="w-4 h-4 absolute left-3.5 top-5 text-krona-secondary" />
|
||
<input
|
||
type="text"
|
||
value={catalogSearchQuery}
|
||
onChange={(e) => setCatalogSearchQuery(e.target.value)}
|
||
placeholder="Быстрый поиск по названию, возможностям или описанию модуля..."
|
||
data-testid="catalog-search-input"
|
||
className="w-full pl-10 pr-10 py-2.5 text-xs bg-krona-workspace border border-krona-border rounded-xl text-krona-primary placeholder:text-krona-secondary focus:outline-none focus:border-krona-gold shadow-inner"
|
||
/>
|
||
{catalogSearchQuery && (
|
||
<button
|
||
onClick={() => setCatalogSearchQuery('')}
|
||
className="absolute right-3.5 top-5 text-krona-secondary hover:text-krona-primary p-0.5"
|
||
title="Очистить поиск"
|
||
>
|
||
<X className="w-3.5 h-3.5" />
|
||
</button>
|
||
)}
|
||
</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 */}
|
||
<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 items-center gap-2">
|
||
<Building className="w-4 h-4 text-krona-gold" />
|
||
<span className="text-xs font-bold text-krona-primary uppercase tracking-wider">
|
||
Отраслевой фокус (не скрывает остальные модули):
|
||
</span>
|
||
</div>
|
||
{isProfileFilterActive && (
|
||
<button
|
||
onClick={() => setCatalogProfileFilter('all')}
|
||
className="text-[11px] text-krona-gold hover:underline flex items-center gap-1"
|
||
data-testid="reset-profile-filter-btn"
|
||
>
|
||
<RotateCcw className="w-3 h-3" />
|
||
<span>Сбросить в «Все модули»</span>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 text-xs">
|
||
{/* Все модули (По умолчанию) */}
|
||
<button
|
||
data-testid="profile-filter-all"
|
||
onClick={() => setCatalogProfileFilter('all')}
|
||
className={`px-3 py-1.5 rounded-lg font-medium whitespace-nowrap transition-colors ${
|
||
catalogProfileFilter === 'all'
|
||
? 'bg-krona-gold text-white font-semibold shadow-xs'
|
||
: 'bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border'
|
||
}`}
|
||
>
|
||
Все модули ({stats.total})
|
||
</button>
|
||
|
||
{/* Рекомендовано для текущей отрасли */}
|
||
<button
|
||
data-testid="profile-filter-recommended"
|
||
onClick={() => setCatalogProfileFilter('recommended')}
|
||
className={`px-3 py-1.5 rounded-lg font-medium whitespace-nowrap transition-colors flex items-center gap-1 ${
|
||
catalogProfileFilter === 'recommended'
|
||
? 'bg-[#2F684C] text-white font-semibold shadow-xs'
|
||
: 'bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border'
|
||
}`}
|
||
>
|
||
<Sparkles className="w-3 h-3 text-krona-gold" />
|
||
<span>Рекомендовано для моей отрасли ({activeProfile.name})</span>
|
||
</button>
|
||
|
||
{/* Specific profiles */}
|
||
{Object.values(BUSINESS_PROFILES).map((p) => (
|
||
<button
|
||
key={p.id}
|
||
data-testid={`profile-filter-${p.id}`}
|
||
onClick={() => setCatalogProfileFilter(p.id)}
|
||
className={`px-3 py-1.5 rounded-lg font-medium whitespace-nowrap transition-colors ${
|
||
catalogProfileFilter === p.id
|
||
? 'bg-[#2F684C] text-white font-semibold shadow-xs'
|
||
: 'bg-krona-workspace text-krona-secondary hover:text-krona-primary border border-krona-border'
|
||
}`}
|
||
>
|
||
{p.name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Categories and Status Filters Bar */}
|
||
<div className="space-y-3">
|
||
{/* Categories horizontal selector */}
|
||
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 text-xs">
|
||
{CATALOG_CATEGORIES.map((cat) => {
|
||
const isSelected = catalogCategoryFilter === cat.id;
|
||
const count =
|
||
cat.id === 'all'
|
||
? CATALOG_MODULES.length
|
||
: CATALOG_MODULES.filter((m) => m.category === cat.id).length;
|
||
|
||
return (
|
||
<button
|
||
key={cat.id}
|
||
data-testid={`category-filter-${cat.id}`}
|
||
onClick={() => setCatalogCategoryFilter(cat.id)}
|
||
className={`px-3 py-1.5 rounded-xl font-medium whitespace-nowrap transition-all flex items-center gap-1.5 ${
|
||
isSelected
|
||
? 'bg-krona-surface text-krona-primary font-bold border-2 border-krona-green shadow-xs'
|
||
: 'bg-krona-surface/60 text-krona-secondary hover:text-krona-primary border border-krona-border'
|
||
}`}
|
||
>
|
||
<span>{cat.label}</span>
|
||
<span className={`text-[10px] px-1.5 py-0.2 rounded-full ${
|
||
isSelected ? 'bg-krona-green/20 text-krona-green' : 'bg-krona-workspace text-krona-secondary'
|
||
}`}>
|
||
{count}
|
||
</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Status & Reset filter row */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<div className="flex items-center gap-1 text-krona-secondary">
|
||
<Filter className="w-3.5 h-3.5" />
|
||
<span>Статус готовности:</span>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1 flex-wrap">
|
||
{[
|
||
{ id: 'all', label: 'Все статусы' },
|
||
{ id: 'ready', label: 'Работает в демо' },
|
||
{ id: 'ui_prepared', label: 'Интерфейс подготовлен' },
|
||
{ id: 'planned', label: 'Запланирован' },
|
||
{ id: 'service_required', label: 'Требует подключения' },
|
||
].map((st) => (
|
||
<button
|
||
key={st.id}
|
||
data-testid={`readiness-filter-${st.id}`}
|
||
onClick={() => setCatalogStatusFilter(st.id)}
|
||
className={`px-2.5 py-1 rounded-lg transition-colors ${
|
||
catalogStatusFilter === st.id
|
||
? 'bg-krona-surface text-krona-primary font-bold border border-krona-border shadow-xs'
|
||
: 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
>
|
||
{st.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{(catalogCategoryFilter !== 'all' ||
|
||
catalogStatusFilter !== 'all' ||
|
||
catalogSearchQuery ||
|
||
catalogProfileFilter !== 'all') && (
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
onClick={resetCatalogFilters}
|
||
className="text-xs"
|
||
data-testid="reset-all-filters-btn"
|
||
>
|
||
<RotateCcw className="w-3 h-3 mr-1" />
|
||
<span>Сбросить все фильтры</span>
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Module Cards Sections */}
|
||
{isProfileFilterActive ? (
|
||
/* When an industry profile is selected: 2 sections */
|
||
<div className="space-y-8">
|
||
{/* Section 1: Recommended */}
|
||
<div className="space-y-4" data-testid="recommended-section">
|
||
<div className="flex items-center justify-between border-b border-krona-border pb-2">
|
||
<div className="flex items-center gap-2">
|
||
<Sparkles className="w-4 h-4 text-krona-gold" />
|
||
<h2 className="text-base font-bold text-krona-primary">
|
||
Рекомендовано для вашей отрасли ({
|
||
catalogProfileFilter === 'recommended'
|
||
? activeProfile.name
|
||
: BUSINESS_PROFILES[catalogProfileFilter]?.name || 'Выбранная отрасль'
|
||
})
|
||
</h2>
|
||
<span className="text-xs px-2 py-0.5 rounded-full bg-krona-gold/15 text-krona-gold-text font-bold">
|
||
{recommendedList.length}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{recommendedList.length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{recommendedList.map((mod) => (
|
||
<ModuleCard
|
||
key={mod.id}
|
||
module={mod}
|
||
isRecommended={true}
|
||
isCompact={displayDensity === 'compact'}
|
||
onOpenDetails={setSelectedModuleForModal}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="p-6 text-center text-xs text-krona-secondary italic bg-krona-surface/40 rounded-2xl border border-krona-border">
|
||
По текущим фильтрам нет рекомендованных модулей
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Section 2: Other platform capabilities */}
|
||
<div className="space-y-4" data-testid="other-modules-section">
|
||
<div className="flex items-center justify-between border-b border-krona-border pb-2">
|
||
<div className="flex items-center gap-2">
|
||
<Layers className="w-4 h-4 text-krona-secondary" />
|
||
<h2 className="text-base font-bold text-krona-primary">
|
||
Все остальные возможности платформы (доступны для подключения)
|
||
</h2>
|
||
<span className="text-xs px-2 py-0.5 rounded-full bg-krona-workspace text-krona-secondary font-bold">
|
||
{otherList.length}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{otherList.length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{otherList.map((mod) => (
|
||
<ModuleCard
|
||
key={mod.id}
|
||
module={mod}
|
||
isRecommended={false}
|
||
isCompact={displayDensity === 'compact'}
|
||
onOpenDetails={setSelectedModuleForModal}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="p-6 text-center text-xs text-krona-secondary italic bg-krona-surface/40 rounded-2xl border border-krona-border">
|
||
По текущим фильтрам нет дополнительных модулей
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* Unified Grid of All Modules */
|
||
<div className="space-y-4" data-testid="all-modules-grid">
|
||
<div className="flex items-center justify-between border-b border-krona-border pb-2">
|
||
<div className="flex items-center gap-2">
|
||
<Grid className="w-4 h-4 text-krona-gold" />
|
||
<h2 className="text-base font-bold text-krona-primary">
|
||
Все модули платформы Krona
|
||
</h2>
|
||
<span className="text-xs px-2 py-0.5 rounded-full bg-krona-gold/15 text-krona-gold-text font-bold">
|
||
Найдено: {filteredModules.length}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{filteredModules.length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{filteredModules.map((mod) => (
|
||
<ModuleCard
|
||
key={mod.id}
|
||
module={mod}
|
||
isCompact={displayDensity === 'compact'}
|
||
onOpenDetails={setSelectedModuleForModal}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="p-12 text-center text-xs text-krona-secondary bg-krona-surface/40 rounded-2xl border border-krona-border space-y-3">
|
||
<p>По запросу «{catalogSearchQuery}» ничего не найдено.</p>
|
||
<Button variant="secondary" size="sm" onClick={resetCatalogFilters}>
|
||
Сбросить фильтры поиска
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</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 */}
|
||
<ModuleInfoModal
|
||
module={selectedModuleForModal}
|
||
isOpen={Boolean(selectedModuleForModal)}
|
||
onClose={() => setSelectedModuleForModal(null)}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|