// Dialogs screen — 3-column layout
const { useState: useStateD, useEffect: useEffectD, useRef: useRefD, useMemo: useMemoD } = React;
function ConvCard({ conv, active, onClick }) {
const statusDot = {
new: "bg-[#4F8EF7]",
in_progress: "bg-[#eab308]",
closed: "bg-zinc-500",
}[conv.status];
return (
);
}
function MessageBubble({ msg, onImageClick }) {
if (msg.kind === "system") {
return (
);
}
if (msg.kind === "user") {
return (
{msg.fileType === "photo" || msg.kindOfContent === "image" ? (
) : (
{msg.text}
)}
{msg.time}
);
}
if (msg.kind === "ai") {
return (
);
}
if (msg.kind === "operator") {
return (
{msg.text}
{msg.operator} · {msg.time}
);
}
return null;
}
function DialogsScreen({
conversations, setConversations,
activeId, setActiveId,
showToast,
onReply, onToggleAI, onClose, onHandoff, onBillingAction,
servers,
}) {
const [searchQ, setSearchQ] = useStateD("");
const [filter, setFilter] = useStateD("all");
const [draft, setDraft] = useStateD("");
const [aiEnabled, setAiEnabled] = useStateD(true);
const [lightboxOpen, setLightboxOpen] = useStateD(false);
const [confirmClose, setConfirmClose] = useStateD(false);
const scrollRef = useRefD(null);
const active = conversations.find((c) => c.id === activeId) || conversations[0];
// Sync AI toggle state when active dialog changes
useEffectD(() => {
if (active) setAiEnabled(active.aiEnabled ?? true);
}, [active?.id, active?.aiEnabled]);
useEffectD(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [active?.id, active?.messages?.length]);
const filtered = useMemoD(() => {
let list = conversations;
if (filter === "open") list = list.filter((c) => c.status === "new");
if (filter === "wip") list = list.filter((c) => c.status === "in_progress");
if (filter === "closed") list = list.filter((c) => c.status === "closed");
if (searchQ.trim()) {
const q = searchQ.toLowerCase();
list = list.filter(
(c) => c.name.toLowerCase().includes(q) || c.username.toLowerCase().includes(q) || c.preview.toLowerCase().includes(q)
);
}
return list;
}, [conversations, filter, searchQ]);
const counts = useMemoD(() => ({
all: conversations.length,
open: conversations.filter((c) => c.status === "new").length,
wip: conversations.filter((c) => c.status === "in_progress").length,
closed: conversations.filter((c) => c.status === "closed").length,
}), [conversations]);
function sendMessage() {
const text = draft.trim();
if (!text || !active) return;
const time = new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
const newMsg = { id: Date.now(), kind: "operator", text, time, operator: "Мария" };
// Optimistic update
setConversations((convs) =>
convs.map((c) =>
c.id === active.id
? { ...c, messages: [...(c.messages || []), newMsg], preview: text, time, status: c.status === "new" ? "in_progress" : c.status, unread: 0 }
: c
)
);
setDraft("");
// API call
if (onReply) onReply(active.id, text, "Мария");
}
function handoffToOperator() {
if (!active) return;
const time = new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
setConversations((convs) =>
convs.map((c) =>
c.id === active.id
? { ...c, messages: [...(c.messages || []), { id: Date.now(), kind: "system", text: "Диалог передан оператору Мария", time }], status: "in_progress", operatorCalled: true }
: c
)
);
showToast("Диалог взят в работу");
if (onHandoff) onHandoff(active.id, "Мария");
}
function closeDialog() {
if (!active) return;
const time = new Date().toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
setConversations((convs) =>
convs.map((c) =>
c.id === active.id
? { ...c, messages: [...(c.messages || []), { id: Date.now(), kind: "system", text: "Диалог закрыт оператором", time }], status: "closed", operatorCalled: false }
: c
)
);
setConfirmClose(false);
showToast("Диалог закрыт");
if (onClose) onClose(active.id);
}
async function toggleAI() {
if (!active) return;
if (onToggleAI) {
const result = await onToggleAI(active.id);
if (result && result.ai_enabled !== undefined) {
setAiEnabled(result.ai_enabled);
}
} else {
setAiEnabled((v) => !v);
}
}
const filterTabs = [
{ id: "all", label: "Все", count: counts.all },
{ id: "open", label: "Открытые", count: counts.open },
{ id: "wip", label: "В работе", count: counts.wip },
{ id: "closed", label: "Закрытые", count: counts.closed },
];
return (
<>
{/* Left: conversation list */}
{/* Center: chat */}
{active && (
<>
{/* Top bar */}
{active.username} · ID {active.tgId}
{/* Messages */}
{(active.messages || []).length === 0 && (
Загрузка сообщений...
)}
{(active.messages || []).map((m) => (
setLightboxOpen(true)} />
))}
{/* Composer */}
Cmd/Ctrl + Enter для отправки
>
)}
{/* Right: user info */}
{/* Lightbox */}
{lightboxOpen && (
setLightboxOpen(false)}
className="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-8"
>
screenshot.png
1280 × 720 · 284 KB
)}
{/* Close confirm */}
{confirmClose && (
setConfirmClose(false)}>
e.stopPropagation()}>
Закрыть диалог?
Пользователь сможет открыть новый, написав в чат.
)}
>
);
}
function UserInfoPanel({ conv, showToast, servers, onBillingAction }) {
const [historyOpen, setHistoryOpen] = useStateD(true);
const isPro = conv.plan === "Pro";
const trafficPct = Math.min(100, (conv.traffic.used / conv.traffic.total) * 100);
const trafficColor = trafficPct > 85 ? "#ef4444" : trafficPct > 60 ? "#eab308" : "#22c55e";
function billingAction(action, label) {
if (onBillingAction) {
onBillingAction(conv.id, action);
} else {
showToast(label);
}
}
return (
{/* User section */}
Пользователь
{conv.name}
{conv.username}
ID {conv.tgId}
Подписка
След. платёж
{conv.nextPayment}
Трафик
{conv.traffic.used} / {conv.traffic.total} GB
{/* Billing — Pro only */}
Биллинг
{isPro && PRO}
{isPro ? (
Последний платёж
{conv.lastPayment.amount} · {conv.lastPayment.date}
) : (
Доступно для тарифа Pro
)}
{/* Servers — Pro only */}
Серверы VPN
{isPro && PRO}
{isPro ? (
{servers.map((s) => {
const dot = { ok: "bg-[#22c55e]", down: "bg-[#ef4444]", high: "bg-[#eab308]" }[s.status];
const label = { ok: "OK", down: "Недоступен", high: "Высокая нагрузка" }[s.status];
const labelColor = { ok: "text-[#22c55e]", down: "text-[#ef4444]", high: "text-[#eab308]" }[s.status];
return (
{s.name}
{s.load}
{label}
);
})}
) : (
Доступно для тарифа Pro
)}
{/* History */}
{historyOpen && (
{(conv.tickets || []).length === 0 && (
Нет закрытых обращений
)}
{(conv.tickets || []).map((t) => (
{t.id}
Решён
{t.title}
{t.date}
))}
)}
);
}
Object.assign(window, { DialogsScreen });