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

411 lines
17 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, { useState } from 'react';
import { useDemoStore } from '../../mock/store';
import { Button } from '../../shared/components/Button';
import { Input } from '../../shared/components/Input';
import { Badge } from '../../shared/components/Badge';
import {
Sparkles,
Send,
MessageSquare,
Zap,
ChevronRight,
ChevronLeft,
ShoppingBag,
Calendar,
CheckSquare,
UserPlus,
Car,
Receipt,
RotateCcw,
Bot,
User,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
export const RightCommandPanel: React.FC = () => {
const {
isRightPanelCollapsed,
setIsRightPanelCollapsed,
getActiveProfile,
setIsQuickActionsOpen,
activeLocation,
} = useDemoStore();
const profile = getActiveProfile();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<'ai' | 'chat' | 'actions'>('ai');
// AI Mock Chat state
const [messages, setMessages] = useState<{ id: string; sender: 'user' | 'ai'; text: string; time: string }[]>([
{
id: '1',
sender: 'ai',
text: `Приветствую! Я Krona AI. Анализирую точку «${activeLocation}» (${profile.name}). Выручка в норме, но обратите внимание на запас охлажденного лосося — осталось 2.4 кг. Чем помочь?`,
time: '12:30',
},
]);
const [prompt, setPrompt] = useState('');
const [isAiTyping, setIsAiTyping] = useState(false);
// Communications mini state
const [chatCategory, setChatCategory] = useState<'all' | 'clients' | 'team'>('all');
const QUICK_QUESTIONS = [
'Что требует внимания сегодня?',
'Какие позиции скоро закончатся?',
'Самые прибыльные позиции?',
'Каких клиентов стоит вернуть?',
];
const handleSendPrompt = (textToSend?: string) => {
const q = textToSend || prompt;
if (!q.trim()) return;
const userMsg = {
id: `usr-${Date.now()}`,
sender: 'user' as const,
text: q,
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
};
setMessages((prev) => [...prev, userMsg]);
setPrompt('');
setIsAiTyping(true);
setTimeout(() => {
let aiReply = 'Анализирую операционные данные Кроны...';
if (q.includes('внимания')) {
aiReply = '⚠️ 3 критических алерта: 1) Запас лосося ниже минимума (2.4 кг из 6 кг); 2) Заказ #104 готовится уже 18 минут; 3) Курьер Алексей завершил смену с 24 500 ₽ наличных к инкассации.';
} else if (q.includes('закончатся') || q.includes('склад')) {
aiReply = '📦 Скоро закончатся: Лосось атлантический (2.4 кг), Угорь копченый (0.0 кг — в стоп-листе!), Тигровые креветки (4.8 кг). Рекомендую сформировать авто-заявку в СибРыбТорг.';
} else if (q.includes('прибыльные') || q.includes('блюда')) {
aiReply = '💰 Топ по маржинальности: 1) Гёдза свинина (маржа 76.1%, food cost 23.9%); 2) Рамен Тонкоцу (маржа 72.1%); 3) Филадельфия Премиум (выручка 58 650 ₽/день).';
} else if (q.includes('клиент')) {
aiReply = '👥 В зоне риска 14 постоянных клиентов (не было визитов более 30 дней). Например: Дмитрий Мельников (LTV 18 500 ₽). Предлагаю запустить триггерную рассылку со скидкой 15% в Telegram-боте.';
} else {
aiReply = `Ответ AI по профилю «${profile.name}»: Задача зафиксирована в оркестраторе Кроны. Контекст передан специализированным агентам.`;
}
setMessages((prev) => [
...prev,
{
id: `ai-${Date.now()}`,
sender: 'ai',
text: aiReply,
time: new Date().toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }),
},
]);
setIsAiTyping(false);
}, 650);
};
if (isRightPanelCollapsed) {
return (
<button
onClick={() => setIsRightPanelCollapsed(false)}
className="fixed right-0 top-20 bg-krona-surface border-y border-l border-krona-border rounded-l-lg p-2 text-krona-secondary hover:text-krona-primary shadow-krona-soft z-30 flex flex-col items-center gap-2 transition-all"
title="Развернуть панель управления"
>
<ChevronLeft className="w-4 h-4 text-krona-gold" />
<span className="text-[10px] font-semibold uppercase [writing-mode:vertical-rl] tracking-wider text-krona-secondary">
Command Panel
</span>
</button>
);
}
return (
<aside className="w-80 h-[calc(100vh-3.5rem)] bg-krona-workspace/95 border-l border-krona-border flex flex-col shrink-0 z-20">
{/* Top Header & Tab switcher */}
<div className="p-3 border-b border-krona-border flex items-center justify-between bg-krona-surface/50">
<div className="flex items-center gap-1 bg-krona-workspace p-0.5 rounded-lg border border-krona-border text-xs">
<button
onClick={() => setActiveTab('ai')}
className={`px-2.5 py-1 rounded-md font-medium flex items-center gap-1 transition-colors ${
activeTab === 'ai'
? 'bg-krona-gold text-white font-semibold shadow-sm'
: 'text-krona-secondary hover:text-krona-primary'
}`}
>
<Sparkles className="w-3 h-3" />
<span>Krona AI</span>
</button>
<button
onClick={() => setActiveTab('actions')}
className={`px-2.5 py-1 rounded-md font-medium flex items-center gap-1 transition-colors ${
activeTab === 'actions'
? 'bg-[#2F684C] text-white font-semibold shadow-sm'
: 'text-krona-secondary hover:text-krona-primary'
}`}
>
<Zap className="w-3 h-3" />
<span>Действия</span>
</button>
<button
onClick={() => setActiveTab('chat')}
className={`px-2.5 py-1 rounded-md font-medium flex items-center gap-1 transition-colors ${
activeTab === 'chat'
? 'bg-krona-surface text-krona-primary shadow-sm'
: 'text-krona-secondary hover:text-krona-primary'
}`}
>
<MessageSquare className="w-3 h-3" />
<span>Чаты</span>
</button>
</div>
<button
onClick={() => setIsRightPanelCollapsed(true)}
className="p-1 rounded text-krona-secondary hover:text-krona-primary"
title="Свернуть панель"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
{/* Content based on Active Tab */}
{activeTab === 'ai' && (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Quick prompts chips */}
<div className="p-2.5 border-b border-krona-border/60 bg-krona-surface/40 flex items-center gap-1.5 overflow-x-auto text-[11px]">
{QUICK_QUESTIONS.map((q, idx) => (
<button
key={idx}
onClick={() => handleSendPrompt(q)}
className="whitespace-nowrap px-2 py-1 rounded-full bg-krona-gold/10 hover:bg-krona-gold/20 text-krona-gold-text border border-krona-gold/30 transition-colors"
>
{q}
</button>
))}
</div>
{/* Conversation stream */}
<div className="flex-1 overflow-y-auto p-3 space-y-3 text-xs">
{messages.map((m) => (
<div
key={m.id}
className={`flex gap-2 ${m.sender === 'user' ? 'justify-end' : 'justify-start'}`}
>
{m.sender === 'ai' && (
<div className="w-6 h-6 rounded-full bg-krona-gold/20 border border-krona-gold/40 flex items-center justify-center text-krona-gold shrink-0">
<Sparkles className="w-3.5 h-3.5" />
</div>
)}
<div
className={`p-2.5 rounded-lg max-w-[85%] leading-relaxed ${
m.sender === 'user'
? 'bg-[#2F684C] text-[#F8F6F0] rounded-br-none'
: 'bg-krona-surface border border-krona-border text-krona-primary rounded-bl-none shadow-krona-soft'
}`}
>
<p>{m.text}</p>
<span
className={`block text-[9px] mt-1 text-right ${
m.sender === 'user' ? 'text-white/70' : 'text-krona-secondary'
}`}
>
{m.time}
</span>
</div>
</div>
))}
{isAiTyping && (
<div className="flex items-center gap-1 text-krona-gold text-xs italic">
<Sparkles className="w-3.5 h-3.5 animate-spin" />
<span>Krona AI думает...</span>
</div>
)}
</div>
{/* Prompt input */}
<div className="p-2.5 border-t border-krona-border bg-krona-surface/60">
<form
onSubmit={(e) => {
e.preventDefault();
handleSendPrompt();
}}
className="flex items-center gap-1.5"
>
<input
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Спросите AI об остатках, выручке..."
className="flex-1 text-xs bg-krona-surface border border-krona-border rounded-md px-3 py-2 text-krona-primary placeholder:text-krona-secondary focus:outline-none focus:border-krona-gold"
/>
<Button type="submit" variant="gold" size="sm" className="px-2.5">
<Send className="w-3.5 h-3.5" />
</Button>
</form>
</div>
</div>
)}
{activeTab === 'actions' && (
<div className="flex-1 overflow-y-auto p-3 space-y-2 text-xs">
<div className="text-[11px] font-semibold text-krona-secondary uppercase tracking-wider mb-2">
Быстрые операции: {profile.name}
</div>
<Button
variant="outline"
className="w-full justify-start text-xs"
onClick={() => setIsQuickActionsOpen(true)}
>
<ShoppingBag className="w-4 h-4 mr-2 text-krona-green" />
<span>Новый чек / заказ</span>
</Button>
{profile.capabilities.booking && (
<Button
variant="outline"
className="w-full justify-start text-xs"
onClick={() => setIsQuickActionsOpen(true)}
>
<Calendar className="w-4 h-4 mr-2 text-krona-gold" />
<span>Забронировать {profile.terminology.bookingResourceLabel.toLowerCase()}</span>
</Button>
)}
{profile.capabilities.work_orders && (
<Button
variant="outline"
className="w-full justify-start text-xs"
onClick={() => setIsQuickActionsOpen(true)}
>
<Car className="w-4 h-4 mr-2 text-krona-info" />
<span>Открыть заказ-наряд на пост</span>
</Button>
)}
<Button
variant="outline"
className="w-full justify-start text-xs"
onClick={() => navigate('/operations/pos')}
>
<Receipt className="w-4 h-4 mr-2 text-krona-green" />
<span>Открыть кассовый терминал (POS)</span>
</Button>
<Button
variant="outline"
className="w-full justify-start text-xs"
onClick={() => navigate('/tasks')}
>
<CheckSquare className="w-4 h-4 mr-2 text-krona-secondary" />
<span>Поставить задачу сотруднику</span>
</Button>
<Button
variant="outline"
className="w-full justify-start text-xs"
onClick={() => navigate('/inventory')}
>
<RotateCcw className="w-4 h-4 mr-2 text-krona-warning" />
<span>Провести списание / инвентаризацию</span>
</Button>
</div>
)}
{activeTab === 'chat' && (
<div className="flex-1 flex flex-col overflow-hidden">
<div className="p-2 border-b border-krona-border flex gap-1 text-[11px]">
<button
onClick={() => setChatCategory('all')}
className={`px-2 py-0.5 rounded ${
chatCategory === 'all' ? 'bg-krona-surface font-semibold text-krona-primary' : 'text-krona-secondary'
}`}
>
Все (3)
</button>
<button
onClick={() => setChatCategory('clients')}
className={`px-2 py-0.5 rounded ${
chatCategory === 'clients' ? 'bg-krona-surface font-semibold text-krona-primary' : 'text-krona-secondary'
}`}
>
Клиенты (2)
</button>
<button
onClick={() => setChatCategory('team')}
className={`px-2 py-0.5 rounded ${
chatCategory === 'team' ? 'bg-krona-surface font-semibold text-krona-primary' : 'text-krona-secondary'
}`}
>
Команда (1)
</button>
</div>
<div className="flex-1 overflow-y-auto divide-y divide-krona-border/50 text-xs">
<div
onClick={() => navigate('/communications')}
className="p-3 hover:bg-krona-surface cursor-pointer transition-colors"
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5 font-semibold text-krona-primary">
<span className="w-2 h-2 rounded-full bg-blue-500"></span>
<span>Александр Сорокин</span>
</div>
<span className="text-[10px] text-krona-secondary">13:42</span>
</div>
<p className="text-xs text-krona-secondary truncate">
Добрый день! Подскажите, стол 4 у окна сегодня свободен?
</p>
<div className="mt-1 flex items-center gap-1.5">
<Badge variant="gold" size="sm">Telegram</Badge>
<Badge variant="green" size="sm">Новый</Badge>
</div>
</div>
<div
onClick={() => navigate('/communications')}
className="p-3 hover:bg-krona-surface cursor-pointer transition-colors"
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5 font-semibold text-krona-primary">
<span className="w-2 h-2 rounded-full bg-green-500"></span>
<span>Екатерина Морозова</span>
</div>
<span className="text-[10px] text-krona-secondary">12:15</span>
</div>
<p className="text-xs text-krona-secondary truncate">
Спасибо за быструю доставку! Курьер был очень вежливым.
</p>
<Badge variant="info" size="sm">WhatsApp</Badge>
</div>
<div
onClick={() => navigate('/communications')}
className="p-3 hover:bg-krona-surface cursor-pointer transition-colors"
>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5 font-semibold text-krona-primary">
<span className="w-2 h-2 rounded-full bg-krona-gold"></span>
<span>Кухня & Шеф (Внутренний)</span>
</div>
<span className="text-[10px] text-krona-secondary">11:10</span>
</div>
<p className="text-xs text-krona-secondary truncate">
Артур: Заканчивается лосось, переключите позиции на контроль.
</p>
<Badge variant="warning" size="sm">Командный чат</Badge>
</div>
</div>
<div className="p-2 border-t border-krona-border bg-krona-surface/50 text-center">
<button
onClick={() => navigate('/communications')}
className="text-xs text-krona-green font-medium hover:underline"
>
Открыть единый Inbox →
</button>
</div>
</div>
)}
</aside>
);
};