diff --git a/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx b/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx index 6ad356f..2a82ad8 100644 --- a/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx +++ b/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx @@ -1,26 +1,95 @@ -import { useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { SalesComposer } from "./dialogs/SalesComposer"; import { SalesContextPanel } from "./dialogs/SalesContextPanel"; import { SalesConversation } from "./dialogs/SalesConversation"; import { SalesDialogList } from "./dialogs/SalesDialogList"; -import { dialogs } from "./dialogs/data"; -import type { ControlMode, ListTab, RightTab } from "./dialogs/types"; +import { + claimConversation, + controlModeOf, + fetchConversation, + fetchConversations, + releaseConversation, + toDialog, + type ApiConversation, +} from "./dialogs/model"; +import type { ListTab, RightTab } from "./dialogs/types"; export function SalesDialogsPage() { const [listTab, setListTab] = useState("all"); const [rightTab, setRightTab] = useState("client"); - const [selectedId, setSelectedId] = useState(1); - const [controlMode, setControlMode] = useState("waiting"); + const [conversations, setConversations] = useState([]); + const [selectedId, setSelectedId] = useState(null); + const [detail, setDetail] = useState(null); - const selected = dialogs.find((dialog) => dialog.id === selectedId) ?? dialogs[0]; - const filtered = useMemo(() => dialogs.filter((dialog) => { - if (listTab === "wait") return dialog.mode === "wait"; - if (listTab === "ai") return dialog.mode === "ai"; - if (listTab === "operator") return dialog.mode === "operator"; - if (listTab === "unread") return dialog.unread > 0; - return true; - }), [listTab]); + const loadList = useCallback(async () => { + try { + const items = await fetchConversations(); + setConversations(items); + setSelectedId((current) => current ?? items[0]?.id ?? null); + } catch { + /* keep previous list on transient errors */ + } + }, []); + + const loadDetail = useCallback(async (id: number) => { + try { + setDetail(await fetchConversation(id)); + } catch { + /* ignore */ + } + }, []); + + useEffect(() => { + void loadList(); + const timer = setInterval(loadList, 4000); + return () => clearInterval(timer); + }, [loadList]); + + useEffect(() => { + if (selectedId == null) return; + void loadDetail(selectedId); + const timer = setInterval(() => loadDetail(selectedId), 3000); + return () => clearInterval(timer); + }, [selectedId, loadDetail]); + + const dialogs = useMemo(() => conversations.map(toDialog), [conversations]); + const filtered = useMemo( + () => + dialogs.filter((dialog) => { + if (listTab === "wait") return dialog.mode === "wait"; + if (listTab === "ai") return dialog.mode === "ai"; + if (listTab === "operator") return dialog.mode === "operator"; + if (listTab === "unread") return dialog.unread > 0; + return true; + }), + [dialogs, listTab], + ); + + const selectedDialog = dialogs.find((dialog) => dialog.id === selectedId) ?? null; + const controlMode = detail ? controlModeOf(detail) : "ai"; + + function applyUpdated(updated: ApiConversation) { + setDetail(updated); + void loadList(); + } + + const onClaim = async () => { + if (selectedId == null) return; + try { + applyUpdated(await claimConversation(selectedId)); + } catch { + /* ignore */ + } + }; + const onRelease = async () => { + if (selectedId == null) return; + try { + applyUpdated(await releaseConversation(selectedId)); + } catch { + /* ignore */ + } + }; return (
@@ -28,13 +97,19 @@ export function SalesDialogsPage() { dialogs={dialogs} filtered={filtered} listTab={listTab} - selectedId={selectedId} + selectedId={selectedId ?? -1} setListTab={setListTab} setSelectedId={setSelectedId} />
- - + + selectedId != null && loadDetail(selectedId)} + />
diff --git a/apps/internal-ui/src/features/sales/dialogs/SalesComposer.tsx b/apps/internal-ui/src/features/sales/dialogs/SalesComposer.tsx index 7461161..feee5a7 100644 --- a/apps/internal-ui/src/features/sales/dialogs/SalesComposer.tsx +++ b/apps/internal-ui/src/features/sales/dialogs/SalesComposer.tsx @@ -1,14 +1,19 @@ +import { useState } from "react"; + import { Icon } from "../../../shared/icons"; +import { sendOperatorMessage } from "./model"; import type { ControlMode } from "./types"; -export function SalesComposer({ mode, setMode }: { mode: ControlMode; setMode: (mode: ControlMode) => void }) { +export function SalesComposer({ mode, conversationId, onClaim, onRelease, onSent }: { mode: ControlMode; conversationId: number | null; onClaim: () => void; onRelease: () => void; onSent: () => void }) { + const [text, setText] = useState(""); + const [sending, setSending] = useState(false); + if (mode === "waiting") { return (
Диалог ждёт оператора

Возьмите диалог, чтобы ответить клиенту. AI поставлен на паузу.

- - +
); } @@ -18,24 +23,40 @@ export function SalesComposer({ mode, setMode }: { mode: ControlMode; setMode: (
AI ведёт диалог

Перехватите, чтобы ответить вручную. Контроль перейдёт к вам.

- +
); } + async function send() { + const value = text.trim(); + if (!value || conversationId == null || sending) return; + setSending(true); + try { + await sendOperatorMessage(conversationId, value); + setText(""); + onSent(); + } finally { + setSending(false); + } + } + return (
- - - - +
-