411 lines
17 KiB
TypeScript
411 lines
17 KiB
TypeScript
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>
|
||
);
|
||
};
|