krona-front/src/app/shell/Topbar.tsx

367 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};