feat(catalog): universal first-screen modules catalog, procurement demo page and e2e scenario 11

This commit is contained in:
ochenstarik 2026-09-11 09:36:45 +07:00
parent 59d767e549
commit a0d995c1f1
16 changed files with 2046 additions and 25 deletions

View file

@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test';
test.describe('Scenario 10: IndexedDB Snapshots (Export, Reset, Import)', () => { test.describe('Scenario 10: IndexedDB Snapshots (Export, Reset, Import)', () => {
test('exports database snapshot, resets demo data, imports snapshot, and verifies restored state', async ({ page }) => { test('exports database snapshot, resets demo data, imports snapshot, and verifies restored state', async ({ page }) => {
await page.goto('/settings'); test.setTimeout(60000);
await page.waitForLoadState('domcontentloaded'); await page.goto('/settings', { waitUntil: 'domcontentloaded' });
// 1. Export database snapshot // 1. Export database snapshot
const exportBtn = page.locator('[data-testid="export-snapshot-btn"]'); const exportBtn = page.locator('[data-testid="export-snapshot-btn"]');

View file

@ -0,0 +1,162 @@
import { test, expect } from '@playwright/test';
test.describe('Scenario 11: Universal Modules Catalog & Profiles', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test('1. First open immediately displays all 18 modules with "Все модули" active by default', async ({ page }) => {
const container = page.locator('[data-testid="modules-catalog-container"]');
await expect(container).toBeVisible({ timeout: 15000 });
await expect(page.locator('h1')).toContainText('Единый каталог модулей Krona');
const allProfileBtn = page.locator('[data-testid="profile-filter-all"]');
await expect(allProfileBtn).toBeVisible();
await expect(allProfileBtn).toContainText('Все модули (18)');
const cards = page.locator('[data-testid^="module-card-"]');
await expect(cards).toHaveCount(18);
await expect(page.locator('[data-testid="module-card-crm"]')).toBeVisible();
await expect(page.locator('[data-testid="module-card-procurement"]')).toBeVisible();
await expect(page.locator('[data-testid="module-card-operations"]')).toBeVisible();
await expect(page.locator('[data-testid="module-card-finance"]')).toBeVisible();
});
test('2. Industry profile highlights recommended modules without hiding the rest', async ({ page }) => {
const restaurantBtn = page.locator('[data-testid="profile-filter-restaurant_hall_delivery"]');
await expect(restaurantBtn).toBeVisible();
await restaurantBtn.click();
const recommendedSection = page.locator('[data-testid="recommended-section"]');
const otherSection = page.locator('[data-testid="other-modules-section"]');
await expect(recommendedSection).toBeVisible();
await expect(otherSection).toBeVisible();
// Recommended section must contain restaurant modules (e.g. operations)
await expect(recommendedSection.locator('[data-testid="module-card-operations"]')).toBeVisible();
// Other section must contain non-restaurant recommended modules (e.g. helpdesk, not hidden!)
await expect(otherSection.locator('[data-testid="module-card-helpdesk"]')).toBeVisible();
// Total count of modules across both sections must still be 18
const totalCards = page.locator('[data-testid^="module-card-"]');
await expect(totalCards).toHaveCount(18);
// Reset button returns to "Все модули"
const resetBtn = page.locator('[data-testid="reset-profile-filter-btn"]');
await expect(resetBtn).toBeVisible();
await resetBtn.click();
await expect(page.locator('[data-testid="all-modules-grid"]')).toBeVisible();
});
test('3. Search and category filters work properly', async ({ page }) => {
const goodsCategoryBtn = page.locator('[data-testid="category-filter-inventory"]');
await expect(goodsCategoryBtn).toBeVisible();
await goodsCategoryBtn.click();
const filteredCards = page.locator('[data-testid^="module-card-"]');
await expect(filteredCards).toHaveCount(2);
await expect(page.locator('[data-testid="module-card-inventory"]')).toBeVisible();
await expect(page.locator('[data-testid="module-card-procurement"]')).toBeVisible();
const searchInput = page.locator('[data-testid="catalog-search-input"]');
await searchInput.fill('закупки');
await expect(filteredCards).toHaveCount(1);
await expect(page.locator('[data-testid="module-card-procurement"]')).toBeVisible();
const resetAllBtn = page.locator('[data-testid="reset-all-filters-btn"]');
await expect(resetAllBtn).toBeVisible();
await resetAllBtn.click();
await expect(page.locator('[data-testid^="module-card-"]')).toHaveCount(18);
});
test('4. Module cards navigate directly to module route', async ({ page }) => {
const crmOpenBtn = page.locator('[data-testid="module-open-btn-crm"]');
await expect(crmOpenBtn).toBeVisible();
await crmOpenBtn.click();
await page.waitForURL('**/crm');
await expect(page.locator('h1, h2').first()).toBeVisible();
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const procOpenBtn = page.locator('[data-testid="module-open-btn-procurement"]');
await expect(procOpenBtn).toBeVisible();
await procOpenBtn.click();
await page.waitForURL('**/procurement');
await expect(page.locator('h1')).toContainText('Закупки и управление поставщиками');
await expect(page.getByTestId('maturity-badge-planned')).toBeVisible();
});
test('5. "О модуле" opens informative details modal', async ({ page }) => {
const detailsBtn = page.locator('[data-testid="module-details-btn-procurement"]');
await expect(detailsBtn).toBeVisible();
await detailsBtn.click();
const modal = page.locator('[data-testid="module-info-modal"]');
await expect(modal).toBeVisible();
await expect(modal).toContainText('Закупки и поставщики');
await expect(modal).toContainText('Назначение модуля');
await expect(modal).toContainText('Ключевые функциональные возможности');
await expect(modal).toContainText('Статус реализации');
await expect(modal).toContainText('Запланированные интеграции');
const closeBtn = page.locator('[data-testid="close-module-info-modal"]');
await closeBtn.click();
await expect(modal).not.toBeVisible();
});
test('6. Database reset in Settings restores "Все модули" filter mode', async ({ page }) => {
const retailBtn = page.locator('[data-testid="profile-filter-retail"]');
await expect(retailBtn).toBeVisible();
await retailBtn.click();
await expect(page.locator('[data-testid="recommended-section"]')).toBeVisible();
await page.goto('/settings/entities');
await page.waitForLoadState('domcontentloaded');
const resetBtn = page.getByRole('button', { name: /Сбросить базу данных/i });
await expect(resetBtn).toBeVisible({ timeout: 10000 });
await resetBtn.click();
const confirmBtn = page.getByRole('button', { name: /Да, сбросить/i });
if (await confirmBtn.isVisible()) {
await confirmBtn.click();
}
await page.waitForTimeout(1000);
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
const allProfileBtn = page.locator('[data-testid="profile-filter-all"]');
await expect(allProfileBtn).toBeVisible();
await expect(page.locator('[data-testid="all-modules-grid"]')).toBeVisible();
await expect(page.locator('[data-testid^="module-card-"]')).toHaveCount(18);
});
test('7. Catalog renders with zero horizontal overflow across viewport', async ({ page }) => {
const isOverflowing = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(isOverflowing).toBe(false);
const kpiTab = page.locator('[data-testid="view-tab-dashboard"]');
await expect(kpiTab).toBeVisible();
await kpiTab.click();
await expect(page.locator('text=Выручка').first()).toBeVisible({ timeout: 10000 });
const catalogTab = page.locator('[data-testid="view-tab-catalog"]');
await catalogTab.click();
await expect(page.locator('[data-testid="modules-catalog-container"]')).toBeVisible();
});
});

View file

@ -2,7 +2,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({ export default defineConfig({
testDir: './e2e', testDir: './e2e',
testMatch: ['**/0[1-9]-*.spec.ts', '**/10-*.spec.ts'], testMatch: ['**/0[1-9]-*.spec.ts', '**/1[0-9]-*.spec.ts'],
testIgnore: ['**/*control-center*'], testIgnore: ['**/*control-center*'],
timeout: 30000, timeout: 30000,
fullyParallel: false, fullyParallel: false,
@ -11,33 +11,45 @@ export default defineConfig({
workers: 1, workers: 1,
reporter: 'list', reporter: 'list',
use: { use: {
baseURL: 'http://localhost:5173', baseURL: 'http://127.0.0.1:5173',
trace: 'on-first-retry', trace: 'on-first-retry',
}, },
projects: [ projects: [
{ {
name: 'desktop', name: 'desktop-1440',
use: { use: {
viewport: { width: 1440, height: 900 }, viewport: { width: 1440, height: 900 },
}, },
}, },
{ {
name: 'tablet', name: 'desktop-1280',
use: {
viewport: { width: 1280, height: 800 },
},
},
{
name: 'tablet-1024',
use: {
viewport: { width: 1024, height: 768 },
},
},
{
name: 'tablet-768',
use: { use: {
viewport: { width: 768, height: 1024 }, viewport: { width: 768, height: 1024 },
}, },
}, },
{ {
name: 'mobile', name: 'mobile-390',
use: { use: {
viewport: { width: 390, height: 844 }, viewport: { width: 390, height: 844 },
}, },
}, },
], ],
webServer: { webServer: {
command: 'npm run dev', command: 'npx vite preview --port 5173 --host 127.0.0.1',
url: 'http://localhost:5173', url: 'http://127.0.0.1:5173',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
timeout: 120000, timeout: 30000,
}, },
}); });

View file

@ -25,6 +25,7 @@ import {
CardLayoutBuilderPage, CardLayoutBuilderPage,
ViewBuilderPage, ViewBuilderPage,
CustomEntityRoutePage, CustomEntityRoutePage,
ProcurementPage,
ForbiddenPage, ForbiddenPage,
NotFoundPage, NotFoundPage,
} from '../../pages'; } from '../../pages';
@ -47,6 +48,7 @@ export const router = createBrowserRouter([
{ path: 'crm/*', element: <CrmPage /> }, { path: 'crm/*', element: <CrmPage /> },
{ path: 'sales/*', element: <SalesPage /> }, { path: 'sales/*', element: <SalesPage /> },
{ path: 'inventory/*', element: <InventoryPage /> }, { path: 'inventory/*', element: <InventoryPage /> },
{ path: 'procurement/*', element: <ProcurementPage /> },
{ path: 'finance/*', element: <FinancePage /> }, { path: 'finance/*', element: <FinancePage /> },
{ path: 'employees/*', element: <EmployeesPage /> }, { path: 'employees/*', element: <EmployeesPage /> },
{ path: 'communications/*', element: <CommunicationsPage /> }, { path: 'communications/*', element: <CommunicationsPage /> },

View file

@ -22,6 +22,7 @@ import {
Sparkles, Sparkles,
Grid, Grid,
Settings, Settings,
Truck,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
} from 'lucide-react'; } from 'lucide-react';
@ -32,6 +33,7 @@ const ICON_MAP: Record<string, React.ReactNode> = {
Users: <Users className="w-4 h-4 shrink-0" />, Users: <Users className="w-4 h-4 shrink-0" />,
ShoppingBag: <ShoppingBag className="w-4 h-4 shrink-0" />, ShoppingBag: <ShoppingBag className="w-4 h-4 shrink-0" />,
Boxes: <Boxes className="w-4 h-4 shrink-0" />, Boxes: <Boxes className="w-4 h-4 shrink-0" />,
Truck: <Truck className="w-4 h-4 shrink-0" />,
BadgeDollarSign: <BadgeDollarSign className="w-4 h-4 shrink-0" />, BadgeDollarSign: <BadgeDollarSign className="w-4 h-4 shrink-0" />,
UserCheck: <UserCheck className="w-4 h-4 shrink-0" />, UserCheck: <UserCheck className="w-4 h-4 shrink-0" />,
MessageSquareText: <MessageSquareText className="w-4 h-4 shrink-0" />, MessageSquareText: <MessageSquareText className="w-4 h-4 shrink-0" />,
@ -48,22 +50,17 @@ const ICON_MAP: Record<string, React.ReactNode> = {
export const Sidebar: React.FC = () => { export const Sidebar: React.FC = () => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const { currentRoleId, getActiveCapabilities } = useDemoStore(); const { currentRoleId } = useDemoStore();
const { entities } = useMetadata(); const { entities } = useMetadata();
const customEntities = entities.filter((e) => !e.isSystem); const customEntities = entities.filter((e) => !e.isSystem);
const capabilities = getActiveCapabilities();
const role = SYSTEM_ROLES[currentRoleId]; const role = SYSTEM_ROLES[currentRoleId];
// Filter modules by role permissions and active capabilities // Filter modules by role permissions (industry profile does not hide modules)
const visibleModules = SYSTEM_MODULES.filter((m) => { const visibleModules = SYSTEM_MODULES.filter((m) => {
// Role check // Role check
if (!role.allowedModules.includes('*') && !role.allowedModules.includes(m.id)) { if (!role.allowedModules.includes('*') && !role.allowedModules.includes(m.id)) {
return false; return false;
} }
// Capability check
if (m.requiredCapability && !capabilities[m.requiredCapability]) {
return false;
}
return true; return true;
}); });

View file

@ -66,6 +66,18 @@ export const SYSTEM_MODULES: ModuleDefinition[] = [
{ id: 'audit', label: 'Инвентаризация', route: '/inventory/audit' }, { id: 'audit', label: 'Инвентаризация', route: '/inventory/audit' },
], ],
}, },
{
id: 'procurement',
label: 'Закупки',
iconName: 'Truck',
route: '/procurement',
sections: [
{ id: 'orders', label: 'Заказы поставщикам', route: '/procurement' },
{ id: 'vendors', label: 'База поставщиков', route: '/procurement?tab=vendors' },
{ id: 'contracts', label: 'Договоры & Условия', route: '/procurement?tab=contracts' },
{ id: 'invoices', label: 'Входящие накладные', route: '/procurement?tab=invoices' },
],
},
{ {
id: 'finance', id: 'finance',
label: 'Финансы', label: 'Финансы',

View file

@ -0,0 +1,151 @@
import React from 'react';
import { CatalogModuleItem, READINESS_CONFIG, AVAILABILITY_CONFIG } from './catalogData';
import { MaturityBadge } from '../../shared/ui/MaturityBadge';
import { Badge } from '../../shared/ui/Badge';
import { Button } from '../../shared/ui/Button';
import {
Users,
ShoppingBag,
Boxes,
Truck,
Workflow,
UserCheck,
MessageSquareText,
Headset,
CheckSquare,
BadgeDollarSign,
Megaphone,
BarChart3,
Cpu,
Sparkles,
Globe,
Settings,
Grid,
ArrowRight,
Info,
Check,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
const ICON_COMPONENTS: Record<string, React.ReactNode> = {
Users: <Users className="w-5 h-5 text-emerald-500" />,
ShoppingBag: <ShoppingBag className="w-5 h-5 text-sky-500" />,
Boxes: <Boxes className="w-5 h-5 text-amber-500" />,
Truck: <Truck className="w-5 h-5 text-orange-500" />,
Workflow: <Workflow className="w-5 h-5 text-krona-gold" />,
UserCheck: <UserCheck className="w-5 h-5 text-indigo-500" />,
MessageSquareText: <MessageSquareText className="w-5 h-5 text-teal-500" />,
Headset: <Headset className="w-5 h-5 text-rose-500" />,
CheckSquare: <CheckSquare className="w-5 h-5 text-blue-500" />,
BadgeDollarSign: <BadgeDollarSign className="w-5 h-5 text-emerald-600" />,
Megaphone: <Megaphone className="w-5 h-5 text-pink-500" />,
BarChart3: <BarChart3 className="w-5 h-5 text-violet-500" />,
Cpu: <Cpu className="w-5 h-5 text-cyan-500" />,
Sparkles: <Sparkles className="w-5 h-5 text-krona-gold" />,
Globe: <Globe className="w-5 h-5 text-emerald-400" />,
Settings: <Settings className="w-5 h-5 text-slate-400" />,
Grid: <Grid className="w-5 h-5 text-amber-400" />,
};
export interface ModuleCardProps {
module: CatalogModuleItem;
onOpenDetails: (module: CatalogModuleItem) => void;
isRecommended?: boolean;
}
export const ModuleCard: React.FC<ModuleCardProps> = ({
module,
onOpenDetails,
isRecommended = false,
}) => {
const navigate = useNavigate();
const readiness = READINESS_CONFIG[module.readiness];
const availability = AVAILABILITY_CONFIG[module.availability];
const handleOpen = () => {
navigate(module.route);
};
return (
<div
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 ${
isRecommended
? 'border-krona-gold/50 bg-gradient-to-b from-krona-gold/5 to-transparent'
: 'border-krona-border'
}`}
>
<div className="space-y-3.5">
{/* Top meta bar: category + badges */}
<div className="flex items-center justify-between gap-2 flex-wrap">
<div className="flex items-center gap-2">
<div className="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" />}
</div>
{isRecommended && (
<span className="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full bg-krona-gold/20 text-krona-gold-text border border-krona-gold/40">
★ Рекомендовано
</span>
)}
</div>
<div className="flex items-center gap-1.5 flex-wrap">
<MaturityBadge level={readiness.maturity} size="sm" />
<Badge variant={availability.variant} size="sm">
{availability.label}
</Badge>
</div>
</div>
{/* Title and description */}
<div>
<h3 className="text-base font-bold text-krona-primary leading-tight">
{module.title}
</h3>
<p className="text-xs text-krona-secondary mt-1.5 line-clamp-2 leading-relaxed">
{module.description}
</p>
</div>
{/* Features list */}
<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>
{module.features.slice(0, 3).map((f, i) => (
<div key={i} className="flex items-start gap-1.5 text-xs text-krona-primary">
<Check className="w-3.5 h-3.5 text-krona-green shrink-0 mt-0.5" />
<span className="line-clamp-1">{f}</span>
</div>
))}
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-2 pt-4 mt-4 border-t border-krona-border/60">
<Button
variant="secondary"
size="sm"
onClick={() => onOpenDetails(module)}
className="text-xs px-2.5"
title="Подробная информация о модуле"
data-testid={`module-details-btn-${module.id}`}
>
<Info className="w-3.5 h-3.5 mr-1" />
<span>О модуле</span>
</Button>
<Button
variant={isRecommended ? 'primary' : 'secondary'}
size="sm"
onClick={handleOpen}
className="flex-1 text-xs justify-center"
data-testid={`module-open-btn-${module.id}`}
>
<span>Открыть</span>
<ArrowRight className="w-3.5 h-3.5 ml-1" />
</Button>
</div>
</div>
);
};

View file

@ -0,0 +1,175 @@
import React, { useEffect } from 'react';
import { CatalogModuleItem, READINESS_CONFIG, AVAILABILITY_CONFIG } from './catalogData';
import { Button } from '../../shared/ui/Button';
import { Badge } from '../../shared/ui/Badge';
import { MaturityBadge } from '../../shared/ui/MaturityBadge';
import {
X,
ArrowRight,
CheckCircle2,
Layers,
Link2,
ShieldCheck,
Target,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
export interface ModuleInfoModalProps {
module: CatalogModuleItem | null;
isOpen: boolean;
onClose: () => void;
}
export const ModuleInfoModal: React.FC<ModuleInfoModalProps> = ({ module, isOpen, onClose }) => {
const navigate = useNavigate();
// Escape key handler
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
if (!isOpen || !module) return null;
const readinessInfo = READINESS_CONFIG[module.readiness];
const availabilityInfo = AVAILABILITY_CONFIG[module.availability];
const handleOpenModule = () => {
onClose();
navigate(module.route);
};
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4"
role="dialog"
aria-modal="true"
aria-labelledby="module-info-title"
data-testid="module-info-modal"
>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/60 backdrop-blur-xs animate-fade-in"
onClick={onClose}
/>
{/* Modal Card */}
<div className="relative bg-krona-surface border border-krona-border rounded-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-krona-modal z-10 animate-scale-in p-6 space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4 border-b border-krona-border/70 pb-4">
<div className="space-y-1.5">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs uppercase font-bold tracking-wider text-krona-gold">
Информация о модуле
</span>
<MaturityBadge level={readinessInfo.maturity} size="sm" />
<Badge variant={availabilityInfo.variant} size="sm">
{availabilityInfo.label}
</Badge>
</div>
<h2 id="module-info-title" className="text-xl font-bold text-krona-primary">
{module.title}
</h2>
<p className="text-xs text-krona-secondary">
{module.description}
</p>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg text-krona-secondary hover:text-krona-primary hover:bg-krona-workspace transition-colors"
title="Закрыть (Esc)"
data-testid="close-module-info-modal"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Section 1: Purpose */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<Target className="w-4 h-4 text-krona-green" />
<span>1. Назначение модуля</span>
</div>
<div className="p-3 bg-krona-workspace rounded-xl border border-krona-border text-xs text-krona-primary leading-relaxed">
{module.details.purpose}
</div>
</div>
{/* Section 2: Expected Capabilities */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<Layers className="w-4 h-4 text-krona-gold" />
<span>2. Предполагаемые и реализованные возможности</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{module.details.expectedCapabilities.map((cap, idx) => (
<div
key={idx}
className="p-2.5 rounded-xl border border-krona-border bg-krona-surface text-xs text-krona-primary flex items-start gap-2"
>
<CheckCircle2 className="w-4 h-4 text-krona-green shrink-0 mt-0.5" />
<span>{cap}</span>
</div>
))}
</div>
</div>
{/* Section 3: Current Status */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<ShieldCheck className="w-4 h-4 text-sky-500" />
<span>3. Текущий статус готовности в платформе</span>
</div>
<div className="p-3 rounded-xl border border-sky-500/30 bg-sky-500/10 text-xs text-sky-900 dark:text-sky-200 leading-relaxed">
<strong>{readinessInfo.label}:</strong> {module.details.currentStatus}
</div>
</div>
{/* Section 4: Planned Integrations */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<Link2 className="w-4 h-4 text-purple-500" />
<span>4. Планируемые интеграции и API-коннекторы</span>
</div>
<div className="flex flex-wrap gap-2">
{module.details.plannedIntegrations.map((integ, idx) => (
<span
key={idx}
className="px-2.5 py-1 text-xs rounded-lg border border-krona-border bg-krona-workspace text-krona-primary font-medium"
>
{integ}
</span>
))}
</div>
</div>
{/* Footer Actions */}
<div className="flex items-center justify-between pt-4 border-t border-krona-border/70">
<span className="text-[11px] text-krona-secondary">
Маршрут в системе: <code className="font-mono text-krona-primary">{module.route}</code>
</span>
<div className="flex items-center gap-2">
<Button variant="secondary" size="sm" onClick={onClose}>
Закрыть
</Button>
<Button
variant="primary"
size="sm"
onClick={handleOpenModule}
data-testid={`modal-open-module-${module.id}`}
>
<span>Перейти в модуль</span>
<ArrowRight className="w-3.5 h-3.5 ml-1" />
</Button>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,486 @@
import React, { useState, useMemo } from 'react';
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 {
Search,
X,
Sparkles,
Layers,
Filter,
Building,
RotateCcw,
BarChart3,
Grid,
} from 'lucide-react';
import { Button } from '../../shared/ui/Button';
import { DynamicDashboard } from '../dashboard/DynamicDashboard';
export const ModulesCatalog: React.FC = () => {
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 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));
const matchCategory = CATALOG_CATEGORIES.find((c) => c.id === m.category)?.label.toLowerCase().includes(q);
if (!matchTitle && !matchDesc && !matchFeatures && !matchCategory) {
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">
Платформа открыта для изучения целиком без предварительного выбора отрасли. Исследуйте Pilot 1 транзакции, интерактивные прототипы и перспективные блоки экосистемы.
</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>
{/* 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}
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}
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}
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>
)}
{/* Module Info Modal */}
<ModuleInfoModal
module={selectedModuleForModal}
isOpen={Boolean(selectedModuleForModal)}
onClose={() => setSelectedModuleForModal(null)}
/>
</div>
);
};

View file

@ -0,0 +1,747 @@
export type ModuleCategory =
| 'sales' // Клиенты и продажи
| 'operations' // Операционная работа
| 'inventory' // Товары и закупки
| 'employees' // Сотрудники
| 'communications' // Коммуникации
| 'finance' // Финансы
| 'marketing' // Продвижение
| 'analytics' // Управление и аналитика
| 'settings'; // Настройки платформы
export type ReadinessStatus = 'ready' | 'ui_prepared' | 'planned' | 'service_required';
export type AvailabilityStatus = 'connected' | 'demo' | 'in_development' | 'requires_setup';
export interface ModuleInfoDetails {
purpose: string;
expectedCapabilities: string[];
currentStatus: string;
plannedIntegrations: string[];
}
export interface CatalogModuleItem {
id: string;
title: string;
category: ModuleCategory;
description: string;
features: string[];
readiness: ReadinessStatus;
availability: AvailabilityStatus;
route: string;
iconName: string;
recommendedProfiles: string[];
details: ModuleInfoDetails;
}
export const CATALOG_CATEGORIES: { id: ModuleCategory | 'all'; label: string }[] = [
{ id: 'all', label: 'Все категории' },
{ id: 'sales', label: 'Клиенты и продажи' },
{ id: 'operations', label: 'Операционная работа' },
{ id: 'inventory', label: 'Товары и закупки' },
{ id: 'employees', label: 'Сотрудники' },
{ id: 'communications', label: 'Коммуникации' },
{ id: 'finance', label: 'Финансы' },
{ id: 'marketing', label: 'Продвижение' },
{ id: 'analytics', label: 'Управление и аналитика' },
{ id: 'settings', label: 'Настройки платформы' },
];
export const READINESS_CONFIG: Record<ReadinessStatus, {
label: string;
maturity: 'pilot1' | 'target' | 'demo' | 'planned';
color: string;
description: string;
}> = {
ready: {
label: 'Работает в демо',
maturity: 'pilot1',
color: 'emerald',
description: 'Полный интерактивный сценарий с хранением в IndexedDB и транзакционной логикой',
},
ui_prepared: {
label: 'Интерфейс подготовлен',
maturity: 'target',
color: 'sky',
description: 'Интерактивный целевой интерфейс с бизнес-логикой и демонстрационными данными',
},
planned: {
label: 'Запланирован',
maturity: 'planned',
color: 'amber',
description: 'Архитектура спроектирована, разработка в дорожной карте (Q4)',
},
service_required: {
label: 'Требует подключения',
maturity: 'demo',
color: 'purple',
description: 'Работает в режиме эмуляции, требует подключения внешнего провайдера/LLM',
},
};
export const AVAILABILITY_CONFIG: Record<AvailabilityStatus, {
label: string;
variant: 'green' | 'blue' | 'warning' | 'gray';
}> = {
connected: { label: 'Подключён', variant: 'green' },
demo: { label: 'Демо-режим', variant: 'blue' },
in_development: { label: 'В разработке', variant: 'warning' },
requires_setup: { label: 'Требует настройки', variant: 'gray' },
};
export const CATALOG_MODULES: CatalogModuleItem[] = [
// 1. CRM
{
id: 'crm',
title: 'CRM и клиенты',
category: 'sales',
description: 'Управление базой контактов, компаний, лидов и сквозная воронка продаж в Kanban-виде.',
features: [
'Воронка сделок с этапами и вероятностью (Kanban Drag & Drop)',
'Карточка «Клиент 360°» с таймлайном, сделками и заказами',
'Конвертация лидов в контакты и связанные сделки в один клик',
'Мастер импорта клиентских баз из CSV с маппингом полей',
],
readiness: 'ready',
availability: 'connected',
route: '/crm',
iconName: 'Users',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Централизованное управление отношениями с контрагентами, ведение лидов до победной сделки и полная история взаимодействия.',
expectedCapabilities: [
'Омниканальный сбор входящих лидов из сайтов и мессенджеров',
'Интерактивная канбан-доска с расчетом взвешенной суммы этапов',
'Сквозное создание заказа в Коммерции сразу при переводе в Won',
'Фильтрация, кастомные сегменты и экспорт в Excel/CSV',
],
currentStatus: 'Pilot 1: полностью реализован, хранение в IndexedDB, сквозной транзакционный контур.',
plannedIntegrations: ['Телефония (Asterisk/Mango)', 'WhatsApp Business API', 'Telegram Bot API', 'Сервисы обогащения по ИНН (DaData)'],
},
},
// 2. Sales
{
id: 'sales',
title: 'Продажи и заказы',
category: 'sales',
description: 'Управление коммерческими заказами, счетами, оплатами и статусами отгрузки со склада.',
features: [
'Жизненный цикл заказа: Подтверждение → Оплата → Отгрузка',
'Автоматическое резервирование товара на складе при подтверждении',
'Фиксация оплат и автоформирование входящего денежного потока (ДДС)',
'Обработка возвратов с восстановлением остатка и корректировкой кассы',
],
readiness: 'ready',
availability: 'connected',
route: '/sales',
iconName: 'ShoppingBag',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Обеспечение безошибочного исполнения сделок, взаиморасчетов с покупателями и контроля складских резервов.',
expectedCapabilities: [
'Создание и редактирование многопозиционных заказов с НДС и скидками',
'Генерация фискальных чеков и печатных форм счетов (УПД/ТОРГ-12)',
'Синхронизация складских резервов в реальном времени',
'Контроль дебиторской задолженности и отсрочек платежей',
],
currentStatus: 'Pilot 1: полностью реализован, связан со складом и финансами.',
plannedIntegrations: ['Банковский эквайринг (Сбер, Тинькофф)', 'Онлайн-кассы (54-ФЗ, Атол, Эвотор)', 'Службы доставки (СДЭК, Яндекс Доставка)'],
},
},
// 3. Inventory
{
id: 'inventory',
title: 'Товары и склад',
category: 'inventory',
description: 'Многоскладской партионный учет, адресное хранение, приемка, перемещения и инвентаризация.',
features: [
'Раздельный учет: физический остаток, резервы и доступно к продаже',
'Оформление доприемки товаров с пересчетом себестоимости',
'Журнал складских перемещений между торговыми точками',
'Акты инвентаризации и списаний с фиксацией в аудит-логе',
],
readiness: 'ready',
availability: 'connected',
route: '/inventory',
iconName: 'Boxes',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Предотвращение пересортицы и кассовых разрывов за счет прозрачного учета складских запасов в реальном времени.',
expectedCapabilities: [
'Адресное хранение по ячейкам, зонам и стеллажам',
'Партионный учет по срокам годности (FIFO) и серийным номерам',
'Мобильный сканер штрихкодов для быстрой инвентаризации',
'Автоматический расчет средней себестоимости списаний',
],
currentStatus: 'Pilot 1: полностью реализован, хранение в IndexedDB, реактивная синхронизация.',
plannedIntegrations: ['ТСД (Терминалы сбора данных)', 'Национальная система маркировки Честный ЗНАК', 'Весовое оборудование'],
},
},
// 4. Procurement
{
id: 'procurement',
title: 'Закупки и поставщики',
category: 'inventory',
description: 'Управление базой поставщиков, договорами, графиками поставок и автозаказом по неснижаемым остаткам.',
features: [
'Реестр поставщиков с реквизитами, прайс-листами и договорами',
'Заявки на закупку (PO) с контролем согласования бюджетов',
'Автоматическое формирование заказов по точкам неснижаемого остатка',
'Входящие накладные и сверка расхождений цен и объемов',
],
readiness: 'planned',
availability: 'in_development',
route: '/procurement',
iconName: 'Truck',
recommendedProfiles: [
'restaurant_hall_delivery',
'retail_store',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Бесперебойное снабжение бизнеса сырьем и товарами по лучшим ценам с исключением ошибок ручного заказа.',
expectedCapabilities: [
'Автозаказ по алгоритму прогнозирования темпов расхода',
'Контроль задолженности перед поставщиками и графиков оплат',
'Автоматический прием электронных накладных через ЭДО',
'Оценка надежности и рейтинга поставщиков по срокам поставок',
],
currentStatus: 'Запланирован: архитектурная спецификация утверждена, интерфейс прототипируется, релиз Q4.',
plannedIntegrations: ['Диадок / СБИС (ЭДО)', '1С:Предприятие 8.3', 'Банк-Клиент (DirectBank)', 'ГИС Меркурий (ВетИС)'],
},
},
// 5. Operations
{
id: 'operations',
title: 'Операционная работа',
category: 'operations',
description: 'Рабочие места кассиров (POS), экраны кухни (KDS), бронирование столов и заказ-наряды автосервиса.',
features: [
'Сенсорный кассовый терминал с быстрой модификацией позиций и чеками',
'Кухонный монитор (KDS) со станциями готовки и таймерами ожидания',
'Интерактивная карта залов, бронирование столов и предчеки',
'Управление постами, заказ-нарядами и мастерами автосервиса',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/operations',
iconName: 'Workflow',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'beauty_salon',
],
details: {
purpose: 'Автоматизация фронт-офиса и операционного персонала на передовой линии обслуживания клиентов.',
expectedCapabilities: [
'Работа в офлайн-режиме с последующей синхронизацией чеков',
'Разделение чеков по гостям и смешанная оплата (нал + карта)',
'Звуковые оповещения KDS о поступлении новых блюд в цех',
'Учет выработки мастеров и нормо-часов в заказ-нарядах',
],
currentStatus: 'Target prototype: интерактивные POS, KDS и карта столов с живой бизнес-логикой.',
plannedIntegrations: ['Фискальные регистраторы (Штрих-М, Атол)', 'Банковские пин-пады', 'Кухонные принтеры чеков (ESC/POS)'],
},
},
// 6. Employees
{
id: 'employees',
title: 'Сотрудники и смены',
category: 'employees',
description: 'Управление персоналом, интерактивный график смен, контроль переработок и учет рабочего времени.',
features: [
'Планировщик смен с удобной недельной и месячной сеткой',
'Автоматическое обнаружение конфликтов графика (>12 ч подряд)',
'Учет ролей, квалификаций, часовых ставок и выходов',
'Табель учета фактически отработанного времени (Т-13)',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/employees',
iconName: 'UserCheck',
recommendedProfiles: [
'restaurant_hall_delivery',
'auto_service',
'retail_store',
'beauty_salon',
'production_workshop',
],
details: {
purpose: 'Оптимизация фонда оплаты труда, справедливое планирование смен и соблюдение норм трудового распорядка.',
expectedCapabilities: [
'Самозапись сотрудников на открытые смены через чат-бот',
'Фиксация прихода/ухода по пин-коду или биометрии планшета',
'Автоматический расчет авансов и премий от личной выручки',
'Контроль медицинских книжек и допусков к работе',
],
currentStatus: 'Target prototype: интерактивный планировщик смен с защитой от переработок.',
plannedIntegrations: ['1С:ЗУП (Зарплата и управление персоналом)', 'СКУД (Турникеты)', 'Telegram-бот для персонала'],
},
},
// 7. Communications
{
id: 'communications',
title: 'Коммуникации и Inbox',
category: 'communications',
description: 'Единый омниканальный центр диалогов: Telegram, WhatsApp, онлайн-чат сайта и внутренние чаты команды.',
features: [
'Единая лента входящих сообщений (Unified Inbox)',
'Интерактивная симуляция ответа клиенту с моментальной доставкой',
'Фильтрация диалогов по каналам, статусу и ответственным',
'Передача переписки между операторами без потери контекста',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/communications',
iconName: 'MessageSquareText',
recommendedProfiles: [
'restaurant_hall_delivery',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
],
details: {
purpose: 'Ни одно обращение клиента не теряется: объединение всех цифровых каналов в одном удобном окне оператора.',
expectedCapabilities: [
'Быстрые шаблоны ответов с автоподстановкой имени и заказа',
'Автоматическое создание лида в CRM из нового диалога',
'Поддержка медиафайлов: фото чеков, голосовые, накладные',
'Маршрутизация чатов по квалификации дежурных менеджеров',
],
currentStatus: 'Target prototype: омниканальный inbox с тестовой симуляцией отправки сообщений.',
plannedIntegrations: ['Telegram Bot API', 'WhatsApp Cloud API / WABA', 'ВКонтакте Сообщения', 'Web-виджет чата'],
},
},
// 8. HelpDesk
{
id: 'helpdesk',
title: 'HelpDesk и поддержка',
category: 'communications',
description: 'Система тикетов поддержки клиентов, контроль соблюдения SLA, база знаний и внутренние заметки.',
features: [
'Очередь тикетов с таймерами дедлайнов и контролем SLA',
'Разделение публичной переписки с клиентом и приватных заметок',
'Категоризация инцидентов по приоритету (Critical, High, Medium)',
'Аналитика скорости первого ответа и решения проблем',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/helpdesk',
iconName: 'Headset',
recommendedProfiles: [
'wholesale_trade',
'retail_store',
'auto_service',
'production_workshop',
],
details: {
purpose: 'Стандартизация сервисного обслуживания, устранение жалоб и повышение индекса удовлетворенности клиентов (CSAT).',
expectedCapabilities: [
'Автоэскалация просроченных тикетов старшему администратору',
'База знаний готовых регламентов и инструкций для операторов',
'Оценка качества обслуживания клиентом после закрытия заявки',
'Привязка тикета к конкретному заказу, сделке или отгрузке',
],
currentStatus: 'Target prototype: рабочая очередь обращений с расчетом SLA и заметками.',
plannedIntegrations: ['E-mail (IMAP/SMTP)', 'Клиентский портал самообслуживания', 'Telegram HelpDesk бот'],
},
},
// 9. Tasks
{
id: 'tasks',
title: 'Задачи и проекты',
category: 'analytics',
description: 'Командный таск-трекер с канбан-доской, списками подзадач, дедлайнами и связкой с клиентами.',
features: [
'Канбан-доска задач по стадиям (К выполнению, В работе, Готово)',
'Интерактивные чек-листы подзадач внутри каждой карточки',
'Контроль дедлайнов с подсветкой просроченных поручений',
'Привязка задач к клиентам CRM и коммерческим сделкам',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/tasks',
iconName: 'CheckSquare',
recommendedProfiles: [
'restaurant_hall_delivery',
'auto_service',
'wholesale_trade',
'production_workshop',
'beauty_salon',
],
details: {
purpose: 'Прозрачное распределение обязанностей в команде и строгий контроль выполнения регулярных регламентов.',
expectedCapabilities: [
'Шаблоны регулярных повторяющихся задач (чек-листы открытия смены)',
'Диаграмма Ганта для крупных проектов запуска новых точек',
'Учет трудозатрат (тайм-трекер) по каждой задаче',
'Push-уведомления исполнителям о назначении и приближении дедлайна',
],
currentStatus: 'Target prototype: функциональная канбан-доска задач с чек-листами подзадач.',
plannedIntegrations: ['Календари (Google, Yandex, Apple iCal)', 'Telegram-уведомления', 'Krona Automation'],
},
},
// 10. Finance
{
id: 'finance',
title: 'Финансы и ДДС',
category: 'finance',
description: 'Управленческий учет движения денег (ДДС), расчет прибылей и убытков (P&L) и контроль кассовых смен.',
features: [
'Отчет о ДДС (Cash Flow) в реальном времени по счетам и статьям',
'Расчет операционной прибыли (P&L) с маржинальностью бизнеса',
'Автоматическое зачисление платежей из оплаченных заказов',
'Контроль кассовых смен, инкассаций и остатков в сейфах',
],
readiness: 'ready',
availability: 'connected',
route: '/finance',
iconName: 'BadgeDollarSign',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Защита от кассовых разрывов, понимание чистой прибыли и абсолютный финансовый контроль каждого рубля бизнеса.',
expectedCapabilities: [
'Многоуровневый справочник статей доходов и расходов (DDS)',
'Платежный календарь для согласования плановых расходов',
'Учет нескольких юрлиц, расчетных счетов и касс',
'Автоматический импорт банковских выписок (1С/Банк-Клиент)',
],
currentStatus: 'Pilot 1: полностью реализован, связан с заказами продаж и возвратами.',
plannedIntegrations: ['Прямой шлюз Банк-Клиент (DirectBank)', 'Онлайн-бухгалтерия (Контур.Эльба, Моё Дело)', 'Эквайринг'],
},
},
// 11. Marketing
{
id: 'marketing',
title: 'Маркетинг и промо',
category: 'marketing',
description: 'Сегментация клиентов, акции, скидки, промокоды и симуляция запуска промо-кампаний.',
features: [
'Динамические когорты клиентов (Новые, Постоянные, В зоне риска)',
'Конструктор промо-акций: проценты, фиксированные суммы, подарки',
'Симулятор отправки рассылок с метриками доставки и конверсии',
'Аналитика влияния акций на средний чек и повторные визиты',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/marketing',
iconName: 'Megaphone',
recommendedProfiles: [
'restaurant_hall_delivery',
'retail_store',
'beauty_salon',
'auto_service',
],
details: {
purpose: 'Привлечение новых клиентов и удержание существующей базы с помощью персонализированных предложений.',
expectedCapabilities: [
'RFM-анализ базы клиентов с автоматическим присвоением статусов',
'Триггерные механики: поздравление с днем рождения, «мы скучаем»',
'Интеграция с бонусной программой лояльности (кешбэк баллами)',
'Оценка ROI каждого маркетингового канала и затрат на привлечение',
],
currentStatus: 'Target prototype: сегменты, промокоды и симулятор промо-кампаний.',
plannedIntegrations: ['SMS-шлюзы', 'Telegram / WhatsApp рассылки', 'Программы лояльности', 'Яндекс Метрика'],
},
},
// 12. Analytics
{
id: 'analytics',
title: 'Аналитика и BI',
category: 'analytics',
description: 'Бизнес-аналитика, наглядные дашборды выручки, динамика чеков и ABC/XYZ матрица номенклатуры.',
features: [
'Интерактивные графики выручки и среднего чека по периодам',
'Матрица ABC-анализа: выявление ключевых драйверов прибыли',
'Когортный анализ возвращаемости клиентов (Retention)',
'Сравнение показателей по торговым точкам и сменам',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/analytics',
iconName: 'BarChart3',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Принятие управленческих решений на основе точных цифр, графиков и предиктивных трендов.',
expectedCapabilities: [
'Настраиваемый конструктор виджетов и ключевых показателей (KPI)',
'Выявление скрытых потерь, списаний и убыточных позиций меню',
'План-фактный анализ выполнения целей продаж',
'Экспорт управленческих отчетов в PDF и Excel',
],
currentStatus: 'Target prototype: графики Recharts, фильтр периодов и расчет ABC-матрицы.',
plannedIntegrations: ['Внешние BI-системы (PowerBI, Apache Superset)', 'Data Warehouse (ClickHouse, BigQuery)'],
},
},
// 13. Automation
{
id: 'automation',
title: 'Автоматизация Workflows',
category: 'analytics',
description: 'No-Code конструктор сценариев: автоматические цепочки действий при событиях в платформе.',
features: [
'Визуальный конструктор сценариев (Триггер → Условие → Действие)',
'Готовые отраслевые шаблоны (уведомления об остатках, автозадачи)',
'Интерактивный запуск тестового прогона с логом выполнения',
'История срабатываний и аудит автоматических изменений',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/automation',
iconName: 'Cpu',
recommendedProfiles: [
'restaurant_hall_delivery',
'auto_service',
'retail_store',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Освобождение сотрудников от рутины за счет автоматизации стандартных бизнес-процессов.',
expectedCapabilities: [
'Триггеры по изменению статусов заказов, времени, порогам остатков',
'Действия: отправка SMS/Telegram, смена статуса сделки, создание задачи',
'Ветвление логики IF/ELSE с проверкой любых полей сущностей',
'Webhook-триггеры для интеграции с внешними микросервисами',
],
currentStatus: 'Target prototype: визуальный конструктор цепочек с интерактивным логом шагов.',
plannedIntegrations: ['Webhooks API', 'Zapier / Make совместимость', 'Krona EventBus'],
},
},
// 14. AI Center
{
id: 'ai',
title: 'Krona AI — ИИ-центр',
category: 'analytics',
description: 'Интеллектуальный бизнес-оркестратор и специализированные агенты: Аналитик, Склад, Маркетолог.',
features: [
'Контекстный чат с ИИ, знающим актуальные остатки и выручку',
'Команда агентов: Бизнес-аналитик, Складской агент, Маркетолог',
'Прозрачный лог вызова инструментов (Tool Calls) и расчетов',
'Генерация артефактов, отчетов и рекомендаций в модальном окне',
],
readiness: 'service_required',
availability: 'demo',
route: '/ai',
iconName: 'Sparkles',
recommendedProfiles: [
'restaurant_hall_delivery',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Второй пилот для руководителя: мгновенные инсайты по операционным данным без ручного копания в таблицах.',
expectedCapabilities: [
'Автономный мониторинг критических отклонений выручки и запасов',
'Генерация привлекательных текстов промо-рассылок и постов',
'Голосовой ввод поручений и создание задач на естественном языке',
'Строгие границы безопасности без утечки конфиденциальных данных',
],
currentStatus: 'Демо-режим: интерактивная симуляция работы ИИ-агентов и вызова инструментов.',
plannedIntegrations: ['OpenAI / Anthropic API', 'Google Gemini API', 'Локальные модели vLLM / Ollama'],
},
},
// 15. CMS
{
id: 'cms',
title: 'CMS и Конструктор сайтов',
category: 'marketing',
description: 'Визуальный конструктор посадочных страниц, интернет-витрин и лид-форм с адаптивным превью.',
features: [
'6 готовых адаптивных блоков: Hero, Каталог/Меню, Преимущества, Отзывы, Форма, Контакты',
'Переключение экранов предпросмотра (Desktop 1440, Tablet 768, Mobile 390)',
'Интерактивное редактирование текста, заголовков и цен',
'Сохранение структуры страницы в локальное состояние',
],
readiness: 'ui_prepared',
availability: 'demo',
route: '/cms',
iconName: 'Globe',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'retail_store',
'beauty_salon',
'auto_service',
],
details: {
purpose: 'Быстрый запуск продающих лендингов и меню доставки без привлечения веб-разработчиков.',
expectedCapabilities: [
'Прямая интеграция каталога товаров и цен из складского модуля',
'Автоматический прием заказов с сайта напрямую в кассу (POS)',
'Привязка собственного доменного имени и бесплатный SSL',
'SEO-оптимизация мета-тегов и OpenGraph для соцсетей',
],
currentStatus: 'Target prototype: 6 типов блоков, переключение адаптивности и экспорт структуры.',
plannedIntegrations: ['DNS-провайдеры', 'CDN для быстрой загрузки медиа', 'Яндекс Метрика'],
},
},
// 16. Entity Builder
{
id: 'entity_builder',
title: 'Конструкторы сущностей и полей',
category: 'settings',
description: 'No-Code студия проектирования данных: создание справочников, 39 типов полей, форм и карточек.',
features: [
'Создание кастомных справочников на лету без изменения программного кода',
'Поддержка 39 специализированных типов полей (включая AST-формулы без eval)',
'Drag & Drop Form Builder с адаптивным превью и 50 шагами Undo/Redo',
'Конструктор карточек и табличных представлений с фильтрами',
],
readiness: 'ready',
availability: 'connected',
route: '/settings/entities',
iconName: 'Settings',
recommendedProfiles: [
'restaurant_hall_delivery',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Полная адаптация платформы под специфику любого бизнеса без ожидания доработок от программистов.',
expectedCapabilities: [
'Мгновенная регистрация новых сущностей в навигации и роутинге',
'Защита системных инвариантов ядра (нельзя сломать базовые поля)',
'Условная видимость и доступность полей по ролям сотрудников',
'Экспорт и импорт метамоделей в JSON-формате',
],
currentStatus: 'Pilot 1: полностью реализован, динамические рендереры форм, карточек и таблиц.',
plannedIntegrations: ['Krona Metadata Registry', 'IndexedDB schema engine', 'REST API Adapter'],
},
},
// 17. Settings & Profiles
{
id: 'settings',
title: 'Настройки и профили бизнеса',
category: 'settings',
description: 'Параметры компании, отраслевые профили (Ресторан, Авто, Розница), роли (RBAC) и бэкапы.',
features: [
'Переключение отраслевых конфигураций с автонастройкой терминологии',
'Ролевая модель (RBAC): Владелец, Управляющий, Кассир, Кладовщик',
'Резервное копирование: экспорт, сброс и импорт снимков IndexedDB',
'Управление торговыми точками, локациями и темами оформления',
],
readiness: 'ready',
availability: 'connected',
route: '/settings',
iconName: 'Settings',
recommendedProfiles: [
'restaurant_hall_delivery',
'restaurant_delivery_only',
'auto_service',
'retail_store',
'beauty_salon',
'wholesale_trade',
'production_workshop',
],
details: {
purpose: 'Центральный пульт администрирования компании, управление доступом персонала и безопасность данных.',
expectedCapabilities: [
'Настройка прав доступа вплоть до отдельных кнопок и полей',
'Аудит действий пользователей с фиксацией IP и времени',
'Персонализация интерфейса под корпоративный брендбук',
'Экспорт полных резервных копий базы в защищенный архив',
],
currentStatus: 'Pilot 1: полностью реализован, ролевой контроль доступа (403), снэпшоты IndexedDB.',
plannedIntegrations: ['SSO (Single Sign-On / SAML / OIDC)', 'Active Directory / LDAP', 'Cloud Backup'],
},
},
// 18. Apps
{
id: 'apps',
title: 'Каталог приложений и API',
category: 'settings',
description: 'Каталог расширений, готовые коннекторы к кассам, платежным шлюзам, доставке и внешним сервисам.',
features: [
'Каталог готовых отраслевых расширений и интеграций',
'Управление статусом установленных плагинов организации',
'Настройка API-ключей, токенов доступа и вебхуков',
'Модульная архитектура расширения функционала платформы',
],
readiness: 'service_required',
availability: 'demo',
route: '/apps',
iconName: 'Grid',
recommendedProfiles: [
'restaurant_hall_delivery',
'retail_store',
'auto_service',
'wholesale_trade',
],
details: {
purpose: 'Открытая экосистема: легкое подключение внешних IT-сервисов и сервисов партнеров к бизнес-платформе.',
expectedCapabilities: [
'Маркетплейс проверенных плагинов от сторонних разработчиков',
'Песочница для безопасного тестирования API-запросов',
'Журнал доставки событий Webhook с авто-повтором при сбоях',
'Контроль разрешений каждого установленного приложения',
],
currentStatus: 'Демо-каталог: витрина расширений с симуляцией подключения сервисов.',
plannedIntegrations: ['Krona App Store SDK', 'OpenAPI 3.1 Gateway', 'OAuth 2.0 Authorization Server'],
},
},
];

View file

@ -73,6 +73,7 @@ export const SettingsModule: React.FC = () => {
try { try {
setIsResetting(true); setIsResetting(true);
await resetDatabase(); await resetDatabase();
useDemoStore.getState().resetCatalogFilters();
setSnapshotStats('База данных сброшена к начальному состоянию'); setSnapshotStats('База данных сброшена к начальному состоянию');
showToast('База данных успешно сброшена к начальному состоянию!', 'info'); showToast('База данных успешно сброшена к начальному состоянию!', 'info');
} catch (err) { } catch (err) {

View file

@ -155,6 +155,17 @@ interface DemoStore {
dismissNotification: (id: string) => void; dismissNotification: (id: string) => void;
markAllNotificationsRead: () => void; markAllNotificationsRead: () => void;
// Catalog filters (Default: 'all' - Все модули)
catalogProfileFilter: string;
setCatalogProfileFilter: (filter: string) => void;
catalogCategoryFilter: string;
setCatalogCategoryFilter: (category: string) => void;
catalogStatusFilter: string;
setCatalogStatusFilter: (status: string) => void;
catalogSearchQuery: string;
setCatalogSearchQuery: (query: string) => void;
resetCatalogFilters: () => void;
// Helper getters // Helper getters
getActiveProfile: () => BusinessProfile; getActiveProfile: () => BusinessProfile;
getActiveCapabilities: () => Record<CapabilityId, boolean>; getActiveCapabilities: () => Record<CapabilityId, boolean>;
@ -544,6 +555,23 @@ export const useDemoStore = create<DemoStore>((set, get) => ({
notifications: state.notifications.map((n) => ({ ...n, read: true })), notifications: state.notifications.map((n) => ({ ...n, read: true })),
})), })),
// Catalog filters (Default: 'all' - Все модули)
catalogProfileFilter: 'all',
setCatalogProfileFilter: (catalogProfileFilter) => set({ catalogProfileFilter }),
catalogCategoryFilter: 'all',
setCatalogCategoryFilter: (catalogCategoryFilter) => set({ catalogCategoryFilter }),
catalogStatusFilter: 'all',
setCatalogStatusFilter: (catalogStatusFilter) => set({ catalogStatusFilter }),
catalogSearchQuery: '',
setCatalogSearchQuery: (catalogSearchQuery) => set({ catalogSearchQuery }),
resetCatalogFilters: () =>
set({
catalogProfileFilter: 'all',
catalogCategoryFilter: 'all',
catalogStatusFilter: 'all',
catalogSearchQuery: '',
}),
// Getters // Getters
getActiveProfile: () => { getActiveProfile: () => {
const s = get(); const s = get();

View file

@ -3,16 +3,16 @@ import { PageSkeleton } from '../shared/ui/Skeleton';
import { ErrorBoundary } from '../shared/ui/ErrorBoundary'; import { ErrorBoundary } from '../shared/ui/ErrorBoundary';
import { RouteGuard } from '../app/guards/RouteGuard'; import { RouteGuard } from '../app/guards/RouteGuard';
const DynamicDashboard = lazy(() => const ModulesCatalog = lazy(() =>
import('../features/dashboard/DynamicDashboard').then((m) => ({ default: m.DynamicDashboard })) import('../features/catalog/ModulesCatalog').then((m) => ({ default: m.ModulesCatalog }))
); );
export const DashboardPage: React.FC = () => { export const DashboardPage: React.FC = () => {
return ( return (
<RouteGuard module="dashboard" moduleName="Дашборд"> <RouteGuard module="dashboard" moduleName="Каталог модулей">
<ErrorBoundary scope="Модуль Дашборд"> <ErrorBoundary scope="Каталог модулей">
<Suspense fallback={<PageSkeleton title="Дашборд" />}> <Suspense fallback={<PageSkeleton title="Каталог модулей" />}>
<DynamicDashboard /> <ModulesCatalog />
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
</RouteGuard> </RouteGuard>

View file

@ -0,0 +1,240 @@
import React, { useState } from 'react';
import { RouteGuard } from '../app/guards/RouteGuard';
import { ErrorBoundary } from '../shared/ui/ErrorBoundary';
import { MaturityBadge } from '../shared/ui/MaturityBadge';
import { Badge } from '../shared/ui/Badge';
import { Button } from '../shared/ui/Button';
import { useToast } from '../shared/ui/useToast';
import {
Truck,
ArrowLeft,
CheckCircle2,
FileText,
Building2,
Clock,
Link2,
Calculator,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
export const ProcurementPage: React.FC = () => {
const navigate = useNavigate();
const { showToast } = useToast();
const [requestedEmail, setRequestedEmail] = useState('');
const [monthlySpend, setMonthlySpend] = useState<number>(350000);
const estimatedSavings = Math.round(monthlySpend * 0.085); // ~8.5% procurement optimization
const handleRequestEarlyAccess = (e: React.FormEvent) => {
e.preventDefault();
if (!requestedEmail.trim()) return;
showToast(`Заявка на ранний доступ для ${requestedEmail} успешно зарегистрирована!`, 'success');
setRequestedEmail('');
};
return (
<RouteGuard module="procurement" moduleName="Закупки и поставщики">
<ErrorBoundary scope="Модуль Закупки">
<div className="space-y-6 max-w-6xl mx-auto animate-fade-in pb-12" data-testid="procurement-demo-page">
{/* Top navigation back to catalog */}
<div className="flex items-center justify-between">
<button
onClick={() => navigate('/')}
className="inline-flex items-center text-xs font-medium text-krona-secondary hover:text-krona-primary transition-colors gap-1.5"
>
<ArrowLeft className="w-4 h-4" />
<span>Вернуться в общий каталог модулей</span>
</button>
<div className="flex items-center gap-2">
<MaturityBadge level="planned" size="md" />
<Badge variant="warning" size="md">В разработке (Q4)</Badge>
</div>
</div>
{/* Hero Banner */}
<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="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-amber-500/10 border border-amber-500/30 text-amber-500 text-xs font-bold uppercase tracking-wider">
<Clock className="w-3.5 h-3.5" />
<span>Дорожная карта платформы • Релиз Q4</span>
</div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-2 max-w-2xl">
<h1 className="text-2xl md:text-3xl font-bold text-krona-primary flex items-center gap-3">
<div className="p-3 rounded-2xl bg-amber-500/15 border border-amber-500/30 text-amber-500">
<Truck className="w-7 h-7" />
</div>
<span>Закупки и управление поставщиками</span>
</h1>
<p className="text-xs md:text-sm text-krona-secondary leading-relaxed">
Централизованная система снабжения для ресторанов, ритейла, автосервисов и производств. Автозаказ по точкам неснижаемого остатка, сверка электронных накладных через ЭДО и прозрачный контроль цен контрагентов.
</p>
</div>
<div className="p-4 rounded-2xl bg-krona-workspace/80 border border-krona-border text-center space-y-1 shrink-0">
<div className="text-[10px] uppercase font-bold text-krona-secondary">Плановый срок запуска</div>
<div className="text-xl font-bold text-krona-gold">IV квартал 2026</div>
<div className="text-[11px] text-krona-green">Интегрирован со «Складом»</div>
</div>
</div>
</div>
{/* 4 Architectural Columns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* 1. Назначение */}
<div className="p-5 rounded-2xl bg-krona-surface border border-krona-border space-y-3">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<Building2 className="w-4 h-4 text-krona-green" />
<span>1. Бизнес-назначение модуля</span>
</div>
<p className="text-xs text-krona-secondary leading-relaxed">
Исключение перебоев в поставках ключевого сырья, устранение человеческого фактора при расчете объема закупок и защита бюджета компании от несанкционированного завышения цен поставщиками.
</p>
<ul className="text-xs text-krona-primary space-y-1.5 pt-1">
<li className="flex items-center gap-2">
<CheckCircle2 className="w-3.5 h-3.5 text-krona-green" />
<span>Единый реестр контрагентов, договоров и спецификаций</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle2 className="w-3.5 h-3.5 text-krona-green" />
<span>Контроль минимальных квантов отгрузки и графиков доставки</span>
</li>
<li className="flex items-center gap-2">
<CheckCircle2 className="w-3.5 h-3.5 text-krona-green" />
<span>Рейтинг поставщиков по показателю точности сроков (OTIF)</span>
</li>
</ul>
</div>
{/* 2. Возможности */}
<div className="p-5 rounded-2xl bg-krona-surface border border-krona-border space-y-3">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<FileText className="w-4 h-4 text-krona-gold" />
<span>2. Запланированные возможности</span>
</div>
<ul className="text-xs text-krona-primary space-y-1.5">
<li className="flex items-start gap-2">
<CheckCircle2 className="w-3.5 h-3.5 text-krona-gold mt-0.5" />
<span><strong>Автозаказ (MRP lite):</strong> автоматическое формирование заявок поставщикам при падении складского остатка ниже страхового минимума.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-3.5 h-3.5 text-krona-gold mt-0.5" />
<span><strong>Согласование бюджетов:</strong> многоуровневый маршрут утверждения заявок руководителями до отправки вендору.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 className="w-3.5 h-3.5 text-krona-gold mt-0.5" />
<span><strong>Сверка накладных:</strong> сравнение цен в заказе с ценами во входящем УПД с фиксацией расхождений.</span>
</li>
</ul>
</div>
{/* 3. Статус */}
<div className="p-5 rounded-2xl bg-krona-surface border border-krona-border space-y-3">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<Clock className="w-4 h-4 text-sky-500" />
<span>3. Текущий статус разработки</span>
</div>
<div className="p-3 bg-sky-500/10 border border-sky-500/30 rounded-xl text-xs text-sky-900 dark:text-sky-200 leading-relaxed">
Спецификация API взаимодействия с ядром «Склад» и «Финансы» успешно утверждена. Подготовлены макеты экранных форм и схемы базы данных для партионного сопоставления.
</div>
<p className="text-[11px] text-krona-secondary">
В демо-версии платформы данные закупок эмулируются через приходные накладные в модуле «Склад» и расходные транзакции в «Финансах».
</p>
</div>
{/* 4. Интеграции */}
<div className="p-5 rounded-2xl bg-krona-surface border border-krona-border space-y-3">
<div className="flex items-center gap-2 text-xs font-bold text-krona-primary uppercase tracking-wider">
<Link2 className="w-4 h-4 text-purple-500" />
<span>4. Планируемые внешние интеграции</span>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="p-2 rounded-xl bg-krona-workspace border border-krona-border">
<div className="font-semibold text-krona-primary">ЭДО (Диадок / СБИС)</div>
<div className="text-[10px] text-krona-secondary">Прием входящих УПД</div>
</div>
<div className="p-2 rounded-xl bg-krona-workspace border border-krona-border">
<div className="font-semibold text-krona-primary">1С:Предприятие 8.3</div>
<div className="text-[10px] text-krona-secondary">Синхронизация счетов</div>
</div>
<div className="p-2 rounded-xl bg-krona-workspace border border-krona-border">
<div className="font-semibold text-krona-primary">Честный ЗНАК</div>
<div className="text-[10px] text-krona-secondary">Приемка кодов DataMatrix</div>
</div>
<div className="p-2 rounded-xl bg-krona-workspace border border-krona-border">
<div className="font-semibold text-krona-primary">ГИС Меркурий (ВетИС)</div>
<div className="text-[10px] text-krona-secondary">Гашение ВСД общепита</div>
</div>
</div>
</div>
</div>
{/* Interactive ROI Calculator & Early Access Box */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6 rounded-3xl bg-krona-surface border border-krona-border shadow-krona-card">
{/* ROI Calculator */}
<div className="md:col-span-2 space-y-3">
<div className="flex items-center gap-2 text-sm font-bold text-krona-primary">
<Calculator className="w-4 h-4 text-krona-green" />
<span>Интерактивный калькулятор экономии снабжения</span>
</div>
<p className="text-xs text-krona-secondary">
Оцените потенциальную экономию за счет устранения завышения цен поставщиков и оптимизации страховых запасов:
</p>
<div className="space-y-2 pt-2">
<div className="flex items-center justify-between text-xs">
<span className="text-krona-secondary">Среднемесячный объем закупок:</span>
<span className="font-bold text-krona-primary font-mono">{monthlySpend.toLocaleString('ru-RU')} ₽ / мес</span>
</div>
<input
type="range"
min="50000"
max="3000000"
step="50000"
value={monthlySpend}
onChange={(e) => setMonthlySpend(Number(e.target.value))}
className="w-full accent-krona-green"
/>
</div>
<div className="p-3 bg-krona-green/10 border border-krona-green/30 rounded-xl flex items-center justify-between">
<div className="text-xs text-krona-green">
<strong>Прогнозируемая экономия в год:</strong> (~8.5% оптимизации)
</div>
<div className="text-base font-bold text-krona-green font-mono">
+{(estimatedSavings * 12).toLocaleString('ru-RU')} ₽ / год
</div>
</div>
</div>
{/* Early Access Form */}
<div className="p-4 rounded-2xl bg-krona-workspace border border-krona-border space-y-3 flex flex-col justify-between">
<div>
<div className="text-xs font-bold text-krona-primary">Заявка на ранний доступ</div>
<p className="text-[11px] text-krona-secondary mt-1">
Оставьте контакт для участия в закрытом бета-тестировании модуля Закупок:
</p>
</div>
<form onSubmit={handleRequestEarlyAccess} className="space-y-2">
<input
type="email"
placeholder="name@company.ru"
value={requestedEmail}
onChange={(e) => setRequestedEmail(e.target.value)}
className="w-full px-3 py-1.5 text-xs bg-krona-surface border border-krona-border rounded-lg text-krona-primary focus:outline-none focus:border-krona-green"
/>
<Button type="submit" variant="primary" size="sm" className="w-full text-xs">
Запросить ранний доступ
</Button>
</form>
</div>
</div>
</div>
</ErrorBoundary>
</RouteGuard>
);
};
export default ProcurementPage;

View file

@ -21,5 +21,6 @@ export * from './FormBuilderPage';
export * from './CardLayoutBuilderPage'; export * from './CardLayoutBuilderPage';
export * from './ViewBuilderPage'; export * from './ViewBuilderPage';
export * from './CustomEntityRoutePage'; export * from './CustomEntityRoutePage';
export * from './ProcurementPage';
export * from './error/ForbiddenPage'; export * from './error/ForbiddenPage';
export * from './error/NotFoundPage'; export * from './error/NotFoundPage';

View file

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { CheckCircle2, Layers, FlaskConical } from 'lucide-react'; import { CheckCircle2, Layers, FlaskConical, Clock } from 'lucide-react';
export type MaturityLevel = 'pilot1' | 'target' | 'demo'; export type MaturityLevel = 'pilot1' | 'target' | 'demo' | 'planned';
export interface MaturityBadgeProps extends React.HTMLAttributes<HTMLSpanElement> { export interface MaturityBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
level: MaturityLevel; level: MaturityLevel;
@ -40,6 +40,13 @@ export const MaturityBadge: React.FC<MaturityBadgeProps> = ({
icon: FlaskConical, icon: FlaskConical,
iconClass: 'text-purple-600 dark:text-purple-400', iconClass: 'text-purple-600 dark:text-purple-400',
}, },
planned: {
label: 'Запланирован',
description: 'В разработке (дорожная карта Q4)',
badgeClass: 'bg-amber-50 text-amber-800 border-amber-300 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-700',
icon: Clock,
iconClass: 'text-amber-600 dark:text-amber-400',
},
}[level]; }[level];
const Icon = config.icon; const Icon = config.icon;