367 lines
16 KiB
TypeScript
367 lines
16 KiB
TypeScript
import React from 'react';
|
||
import { useLocation, useNavigate } from 'react-router-dom';
|
||
import { useDemoStore } from '../../mock/store';
|
||
import { BUSINESS_PROFILES } from '../../core/profiles/profiles';
|
||
import { SYSTEM_ROLES } from '../../core/permissions/types';
|
||
import { SYSTEM_MODULES } from '../../core/modules/registry';
|
||
import { Button } from '../../shared/components/Button';
|
||
import {
|
||
Search,
|
||
Plus,
|
||
Bell,
|
||
Sun,
|
||
Moon,
|
||
Sparkles,
|
||
Monitor,
|
||
Tablet,
|
||
Smartphone,
|
||
ChevronDown,
|
||
MapPin,
|
||
Shield,
|
||
Layers,
|
||
} from 'lucide-react';
|
||
|
||
export const Topbar: React.FC = () => {
|
||
const {
|
||
theme,
|
||
setTheme,
|
||
viewportMode,
|
||
setViewportMode,
|
||
currentProfileId,
|
||
setProfileId,
|
||
currentRoleId,
|
||
setRoleId,
|
||
activeLocation,
|
||
setActiveLocation,
|
||
setIsCommandPaletteOpen,
|
||
setIsQuickActionsOpen,
|
||
setIsNotificationsOpen,
|
||
setIsAiDrawerOpen,
|
||
notifications,
|
||
getActiveProfile,
|
||
getActiveCapabilities,
|
||
} = useDemoStore();
|
||
|
||
const location = useLocation();
|
||
const navigate = useNavigate();
|
||
|
||
const profile = getActiveProfile();
|
||
const capabilities = getActiveCapabilities();
|
||
const currentRole = SYSTEM_ROLES[currentRoleId];
|
||
|
||
const unreadNotificationsCount = notifications.filter((n) => !n.read).length;
|
||
|
||
// Determine current active module from route
|
||
const currentPath = location.pathname;
|
||
const currentModule = SYSTEM_MODULES.find((m) => {
|
||
if (m.route === '/') return currentPath === '/';
|
||
return currentPath.startsWith(m.route);
|
||
}) || SYSTEM_MODULES[0];
|
||
|
||
// For operations, tabs are dynamic based on active profile & capabilities!
|
||
const getSubtabs = () => {
|
||
if (currentModule.id === 'operations') {
|
||
return profile.operationsTabs.filter((tab) => {
|
||
if (!tab.requiredCapability) return true;
|
||
return capabilities[tab.requiredCapability];
|
||
});
|
||
}
|
||
return currentModule.sections.filter((sec) => {
|
||
if (!sec.requiredCapability) return true;
|
||
return capabilities[sec.requiredCapability];
|
||
});
|
||
};
|
||
|
||
const activeSubtabs = getSubtabs();
|
||
|
||
return (
|
||
<header className="sticky top-0 z-30 bg-krona-workspace/95 backdrop-blur-sm border-b border-krona-border">
|
||
{/* Upper Control Bar */}
|
||
<div className="h-14 px-4 flex items-center justify-between gap-3">
|
||
{/* Left: Profile & Location controls */}
|
||
<div className="flex items-center gap-2.5">
|
||
{/* Profile Switcher */}
|
||
<div className="relative group">
|
||
<div className="flex items-center gap-2 bg-krona-surface border border-krona-border rounded-lg px-2.5 py-1.5 cursor-pointer hover:border-krona-gold/60 transition-colors shadow-krona-soft">
|
||
<span className="w-2 h-2 rounded-full bg-krona-gold"></span>
|
||
<div className="flex flex-col text-left">
|
||
<span className="text-[10px] uppercase font-semibold tracking-wider text-krona-secondary">
|
||
Профиль бизнеса
|
||
</span>
|
||
<span className="text-xs font-semibold text-krona-primary max-w-[170px] truncate">
|
||
{profile.name}
|
||
</span>
|
||
</div>
|
||
<ChevronDown className="w-3.5 h-3.5 text-krona-secondary" />
|
||
</div>
|
||
|
||
{/* Dropdown Menu */}
|
||
<div className="absolute left-0 top-full mt-1 w-72 bg-krona-raised border border-krona-border rounded-xl shadow-krona-modal p-1.5 hidden group-hover:block z-50 animate-fade-in">
|
||
<div className="px-2.5 py-1.5 text-[11px] font-semibold text-krona-secondary uppercase tracking-wider border-b border-krona-border/60">
|
||
Отраслевые профили Кроны
|
||
</div>
|
||
<div className="max-h-80 overflow-y-auto py-1 space-y-0.5">
|
||
{Object.values(BUSINESS_PROFILES).map((p) => (
|
||
<button
|
||
key={p.id}
|
||
onClick={() => {
|
||
setProfileId(p.id);
|
||
navigate('/operations');
|
||
}}
|
||
className={`w-full text-left px-2.5 py-2 rounded-lg text-xs flex items-center justify-between transition-colors ${
|
||
p.id === currentProfileId
|
||
? 'bg-krona-gold/15 text-krona-gold-text font-medium'
|
||
: 'hover:bg-krona-workspace text-krona-primary'
|
||
}`}
|
||
>
|
||
<div>
|
||
<div className="font-medium truncate">{p.name}</div>
|
||
<div className="text-[10px] text-krona-secondary truncate">{p.companyName}</div>
|
||
</div>
|
||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-krona-muted text-krona-secondary shrink-0">
|
||
{p.category}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Location Selector */}
|
||
<div className="relative group">
|
||
<div className="flex items-center gap-1.5 bg-krona-surface border border-krona-border rounded-lg px-2.5 py-1.5 cursor-pointer hover:border-krona-border-strong text-xs text-krona-primary shadow-krona-soft">
|
||
<MapPin className="w-3.5 h-3.5 text-krona-secondary" />
|
||
<span className="max-w-[130px] truncate font-medium">{activeLocation}</span>
|
||
<ChevronDown className="w-3 h-3 text-krona-secondary" />
|
||
</div>
|
||
|
||
<div className="absolute left-0 top-full mt-1 w-56 bg-krona-raised border border-krona-border rounded-lg shadow-krona-modal p-1 hidden group-hover:block z-50">
|
||
<div className="px-2 py-1 text-[10px] font-semibold text-krona-secondary uppercase">
|
||
Точки продаж / Локации
|
||
</div>
|
||
{profile.locations.map((loc) => (
|
||
<button
|
||
key={loc}
|
||
onClick={() => setActiveLocation(loc)}
|
||
className={`w-full text-left px-2.5 py-1.5 rounded text-xs transition-colors ${
|
||
loc === activeLocation ? 'bg-krona-green/10 text-krona-green font-medium' : 'hover:bg-krona-workspace text-krona-primary'
|
||
}`}
|
||
>
|
||
{loc}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Role (RBAC) Switcher */}
|
||
<div className="relative group">
|
||
<div className="flex items-center gap-1.5 bg-krona-surface border border-krona-border rounded-lg px-2.5 py-1.5 cursor-pointer hover:border-krona-border-strong text-xs shadow-krona-soft">
|
||
<Shield className="w-3.5 h-3.5 text-krona-green" />
|
||
<div className="flex flex-col text-left">
|
||
<span className="text-[9px] uppercase font-semibold text-krona-secondary">Роль</span>
|
||
<span className="font-semibold text-krona-primary truncate max-w-[90px]">
|
||
{currentRole.badge}
|
||
</span>
|
||
</div>
|
||
<ChevronDown className="w-3 h-3 text-krona-secondary" />
|
||
</div>
|
||
|
||
<div className="absolute left-0 top-full mt-1 w-64 bg-krona-raised border border-krona-border rounded-xl shadow-krona-modal p-1.5 hidden group-hover:block z-50">
|
||
<div className="px-2 py-1 text-[10px] font-semibold text-krona-secondary uppercase border-b border-krona-border/60">
|
||
Переключение роли (RBAC)
|
||
</div>
|
||
<div className="max-h-72 overflow-y-auto py-1 space-y-0.5">
|
||
{Object.values(SYSTEM_ROLES).map((r) => (
|
||
<button
|
||
key={r.id}
|
||
onClick={() => {
|
||
setRoleId(r.id);
|
||
navigate(r.defaultRoute);
|
||
}}
|
||
className={`w-full text-left px-2.5 py-2 rounded-lg text-xs transition-colors ${
|
||
r.id === currentRoleId
|
||
? 'bg-krona-green/15 text-krona-green-text font-medium'
|
||
: 'hover:bg-krona-workspace text-krona-primary'
|
||
}`}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<span className="font-medium">{r.label}</span>
|
||
<span className="text-[10px] px-1 py-0.5 rounded bg-krona-muted text-krona-secondary">
|
||
{r.badge}
|
||
</span>
|
||
</div>
|
||
<div className="text-[10px] text-krona-secondary truncate mt-0.5">{r.description}</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Center: Global Search & Shortcuts */}
|
||
<div className="flex-1 max-w-md hidden md:block">
|
||
<button
|
||
onClick={() => setIsCommandPaletteOpen(true)}
|
||
className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg bg-krona-surface border border-krona-border text-xs text-krona-secondary hover:border-krona-border-strong shadow-krona-soft transition-colors"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<Search className="w-3.5 h-3.5" />
|
||
<span>Поиск по Кроне, клиентам, чекам, KDS...</span>
|
||
</div>
|
||
<kbd className="px-1.5 py-0.5 text-[10px] bg-krona-workspace border border-krona-border rounded text-krona-secondary font-mono">
|
||
Ctrl+K
|
||
</kbd>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Right: Quick actions, AI, Themes, Viewports, Notifications */}
|
||
<div className="flex items-center gap-1.5">
|
||
{/* AI trigger */}
|
||
<Button
|
||
variant="gold"
|
||
size="sm"
|
||
onClick={() => setIsAiDrawerOpen(true)}
|
||
className="hidden sm:inline-flex text-xs font-medium gap-1.5"
|
||
>
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
<span>✦ Спросить Крону</span>
|
||
</Button>
|
||
|
||
{/* Quick Action (+) button */}
|
||
<Button
|
||
variant="primary"
|
||
size="sm"
|
||
onClick={() => setIsQuickActionsOpen(true)}
|
||
className="px-2.5"
|
||
title="Быстрое создание"
|
||
>
|
||
<Plus className="w-4 h-4" />
|
||
</Button>
|
||
|
||
{/* Viewport Switcher */}
|
||
<div className="flex items-center bg-krona-surface border border-krona-border rounded-lg p-0.5 shadow-krona-soft">
|
||
<button
|
||
onClick={() => setViewportMode('desktop')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewportMode === 'desktop' ? 'bg-krona-workspace text-krona-green' : 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
title="Desktop View"
|
||
>
|
||
<Monitor className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => setViewportMode('tablet-landscape')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewportMode === 'tablet-landscape' ? 'bg-krona-workspace text-krona-green' : 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
title="Tablet Landscape"
|
||
>
|
||
<Tablet className="w-3.5 h-3.5 rotate-90" />
|
||
</button>
|
||
<button
|
||
onClick={() => setViewportMode('tablet-portrait')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewportMode === 'tablet-portrait' ? 'bg-krona-workspace text-krona-green' : 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
title="Tablet Portrait"
|
||
>
|
||
<Tablet className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => setViewportMode('mobile')}
|
||
className={`p-1.5 rounded transition-colors ${
|
||
viewportMode === 'mobile' ? 'bg-krona-workspace text-krona-green' : 'text-krona-secondary hover:text-krona-primary'
|
||
}`}
|
||
title="Mobile View"
|
||
>
|
||
<Smartphone className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Theme Switcher */}
|
||
<div className="flex items-center bg-krona-surface border border-krona-border rounded-lg p-0.5 shadow-krona-soft">
|
||
<button
|
||
onClick={() => setTheme('soft-light')}
|
||
className={`px-2 py-1 text-[11px] font-medium rounded transition-colors ${
|
||
theme === 'soft-light' ? 'bg-krona-workspace text-krona-gold-text' : 'text-krona-secondary'
|
||
}`}
|
||
title="Soft Light (Основная)"
|
||
>
|
||
Soft
|
||
</button>
|
||
<button
|
||
onClick={() => setTheme('light')}
|
||
className={`p-1 rounded transition-colors ${
|
||
theme === 'light' ? 'bg-krona-workspace text-krona-gold' : 'text-krona-secondary'
|
||
}`}
|
||
title="Light"
|
||
>
|
||
<Sun className="w-3.5 h-3.5" />
|
||
</button>
|
||
<button
|
||
onClick={() => setTheme('dark')}
|
||
className={`p-1 rounded transition-colors ${
|
||
theme === 'dark' ? 'bg-krona-workspace text-krona-gold' : 'text-krona-secondary'
|
||
}`}
|
||
title="Dark"
|
||
>
|
||
<Moon className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Notifications bell */}
|
||
<button
|
||
onClick={() => setIsNotificationsOpen(true)}
|
||
className="relative p-2 rounded-lg bg-krona-surface border border-krona-border text-krona-secondary hover:text-krona-primary shadow-krona-soft transition-colors"
|
||
title="Уведомления"
|
||
>
|
||
<Bell className="w-4 h-4" />
|
||
{unreadNotificationsCount > 0 && (
|
||
<span className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-krona-danger text-white text-[10px] font-bold flex items-center justify-center">
|
||
{unreadNotificationsCount}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Horizontal Module Subnavigation Tabs */}
|
||
{activeSubtabs.length > 0 && (
|
||
<nav className="px-4 flex items-center gap-1 overflow-x-auto h-10 border-t border-krona-border/50 bg-krona-workspace/70 text-xs">
|
||
<span className="text-[11px] font-bold uppercase tracking-wider text-krona-secondary mr-2 flex items-center gap-1">
|
||
<Layers className="w-3 h-3 text-krona-secondary" />
|
||
{currentModule.label}:
|
||
</span>
|
||
{activeSubtabs.map((tab) => {
|
||
const isActive =
|
||
currentPath === tab.route ||
|
||
(tab.route !== '/operations' && tab.route !== '/' && currentPath.startsWith(tab.route));
|
||
|
||
return (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => navigate(tab.route)}
|
||
className={`px-3 py-1.5 rounded-md font-medium whitespace-nowrap transition-all flex items-center gap-1.5 ${
|
||
isActive
|
||
? 'bg-[#2F684C] text-[#F8F6F0] shadow-sm'
|
||
: 'text-krona-secondary hover:text-krona-primary hover:bg-krona-surface/60'
|
||
}`}
|
||
>
|
||
<span>{tab.label}</span>
|
||
{tab.badge && (
|
||
<span
|
||
className={`text-[10px] px-1.5 py-0.2 rounded-full font-bold ${
|
||
isActive ? 'bg-white/20 text-white' : 'bg-krona-muted text-krona-secondary'
|
||
}`}
|
||
>
|
||
{tab.badge}
|
||
</span>
|
||
)}
|
||
</button>
|
||
);
|
||
})}
|
||
</nav>
|
||
)}
|
||
</header>
|
||
);
|
||
};
|