From 445e3c83c2433cbc30e965eafa9e9a959bf1d476 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 5 Sep 2026 01:17:10 +0300 Subject: [PATCH] =?UTF-8?q?:sparkles:=20feat(chat)!:=20=D0=B5=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=87=D0=B0=D1=82=20=D0=B8=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D1=87=D0=BA=D0=B0=20=D1=81=D0=BE=D1=82=D1=80?= =?UTF-8?q?=D1=83=D0=B4=D0=BD=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD-=D0=B1=D0=B0=D0=B7=D0=BB=D0=B0?= =?UTF-8?q?=D0=B9=D0=BD=D1=83=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sales- и support-диалоги объединены в один экран «Чат» (маршрут /chat, старые адреса — алиасы). Контекст-панель сама выбирает представление по identity диалога. У сотрудника нет верхней панели и разделов — сайдбар это дерево «Все диалоги · Группы · Агенты», управляющее охватом списка; у менеджера — плоские шесть пунктов макета и группа «Ещё» для служебных разделов. Роуты уведомлений и карточек обзора на бэкенде — chat. Co-Authored-By: Claude Fable 5 --- .../hub_platform/conversations/command.py | 4 +- .../identity/demo_seed/data/operations.json | 4 +- .../hub_platform/notifications/services.py | 4 +- apps/internal-ui/src/App.tsx | 2 +- apps/internal-ui/src/auth/access.test.ts | 5 +- apps/internal-ui/src/auth/access.ts | 5 +- .../src/features/chat/ChatPage.tsx | 107 +++++++++ .../src/features/chat/useChatScope.ts | 30 +++ .../conversations/ConversationWorkspace.tsx | 29 +-- .../src/features/conversations/DialogList.tsx | 7 +- .../src/features/sales/SalesDialogsPage.tsx | 19 -- .../sales/dialogs/SalesContextPanel.tsx | 18 -- .../dialogs/context/SalesContextTabs.tsx | 16 -- .../features/support/SupportDialogsPage.tsx | 20 -- .../support/context/SupportContextPanel.tsx | 35 --- apps/internal-ui/src/layout/Shell.tsx | 31 ++- .../src/layout/ShellRouteContent.tsx | 21 +- apps/internal-ui/src/layout/Sidebar.tsx | 213 ++++++++++++------ apps/internal-ui/src/layout/TopBar.tsx | 8 +- apps/internal-ui/src/layout/styles.css | 100 ++++++++ apps/internal-ui/src/router.test.ts | 18 +- apps/internal-ui/src/router.ts | 10 +- apps/internal-ui/src/routes.ts | 3 +- apps/internal-ui/src/shared/icons.tsx | 3 +- apps/internal-ui/src/types.ts | 2 +- apps/internal-ui/src/useRouteNavigation.ts | 2 +- 26 files changed, 471 insertions(+), 245 deletions(-) create mode 100644 apps/internal-ui/src/features/chat/ChatPage.tsx create mode 100644 apps/internal-ui/src/features/chat/useChatScope.ts delete mode 100644 apps/internal-ui/src/features/sales/SalesDialogsPage.tsx delete mode 100644 apps/internal-ui/src/features/sales/dialogs/SalesContextPanel.tsx delete mode 100644 apps/internal-ui/src/features/sales/dialogs/context/SalesContextTabs.tsx delete mode 100644 apps/internal-ui/src/features/support/SupportDialogsPage.tsx delete mode 100644 apps/internal-ui/src/features/support/context/SupportContextPanel.tsx diff --git a/apps/backend/hub_platform/conversations/command.py b/apps/backend/hub_platform/conversations/command.py index ce47c16..93a05cc 100644 --- a/apps/backend/hub_platform/conversations/command.py +++ b/apps/backend/hub_platform/conversations/command.py @@ -63,7 +63,7 @@ def command_center_overview(context: TenantContext, period: str) -> dict: { "code": str(group.id), "name": group.name, - "route": "salesDialogs", + "route": "chat", "employees": group.member_count, "aiAgents": agents_by_group.get(group.id, 0), "dialogs": _dialog_block(group_open, now), @@ -75,7 +75,7 @@ def command_center_overview(context: TenantContext, period: str) -> dict: { "code": "none", "name": "Без группы", - "route": "salesDialogs", + "route": "chat", "employees": 0, "aiAgents": AIAgent.objects.filter( channel__organization_id=organization_id, diff --git a/apps/backend/hub_platform/identity/demo_seed/data/operations.json b/apps/backend/hub_platform/identity/demo_seed/data/operations.json index 8ec76c6..8d28898 100644 --- a/apps/backend/hub_platform/identity/demo_seed/data/operations.json +++ b/apps/backend/hub_platform/identity/demo_seed/data/operations.json @@ -31,7 +31,7 @@ "level": "WARNING", "title": "Диалог ожидает оператора", "body": "Сергей Орлов ждёт ответа в канале продаж Вектора.", - "targetRoute": "conversations", + "targetRoute": "chat", "targetId": "vektor-queue", "audience": "OPERATORS", "sourceId": "vektor-queue", @@ -42,7 +42,7 @@ "level": "INFO", "title": "Новое сообщение в диалоге", "body": "Наталья Кузнецова написала в продажи Вектора.", - "targetRoute": "conversations", + "targetRoute": "chat", "targetId": "vektor-lead-active", "audience": "OPERATORS", "sourceId": "vektor-lead-active" diff --git a/apps/backend/hub_platform/notifications/services.py b/apps/backend/hub_platform/notifications/services.py index 3f196f7..fa34150 100644 --- a/apps/backend/hub_platform/notifications/services.py +++ b/apps/backend/hub_platform/notifications/services.py @@ -17,8 +17,8 @@ from hub_platform.notifications.selectors import unread_for # Реестр типов: дефолтный уровень и маршрут диплинка. Новый тип события — # одна запись здесь + вызов notify(...) из доменного сервиса. TYPE_META: dict[str, dict] = { - NotificationType.DIALOG_WAITING: {"level": NotificationLevel.WARNING, "route": "salesDialogs"}, - NotificationType.DIALOG_NEW_MESSAGE: {"level": NotificationLevel.INFO, "route": "salesDialogs"}, + NotificationType.DIALOG_WAITING: {"level": NotificationLevel.WARNING, "route": "chat"}, + NotificationType.DIALOG_NEW_MESSAGE: {"level": NotificationLevel.INFO, "route": "chat"}, NotificationType.RELEASE_PUBLISHED: {"level": NotificationLevel.SUCCESS, "route": "agents"}, NotificationType.INTEGRATION_ERROR: {"level": NotificationLevel.CRITICAL, "route": "integrations"}, } diff --git a/apps/internal-ui/src/App.tsx b/apps/internal-ui/src/App.tsx index 635a6f3..0ddb214 100644 --- a/apps/internal-ui/src/App.tsx +++ b/apps/internal-ui/src/App.tsx @@ -163,7 +163,7 @@ export function App() { ) : !canAccess(user, navigation.route) ? ( navigate(defaultRoute(user), null, true)} /> ) : ( - navigate(nextRoute)} selectedEmployeeId={navigation.selectedEmployeeId} selectedProductCode={navigation.selectedProductCode} selectedAgentId={navigation.selectedAgentId} selectedKnowledgeId={navigation.selectedKnowledgeId} selectedConversationId={navigation.selectedConversationId} selectedClientId={navigation.selectedClientId} openClientRoute={(clientId) => navigate("salesClientDetail", clientId)} selectedChannelId={navigation.selectedChannelId} openChannelRoute={(channelId) => navigate("agentDetail", channelId)} selectedSupportPortalId={navigation.selectedSupportPortalId} openSupportPortalRoute={(portalId) => navigate("supportPortalDetail", portalId)} openEmployeeRoute={(employeeId) => navigate("employeeDetail", employeeId)} openAgentCreateRoute={() => navigate("agents")} openAgentRoute={(agentId) => navigate("agentDetail", agentId)} openKnowledgeRoute={(knowledgeId) => navigate("aiKnowledgeDetail", knowledgeId)} openConversationRoute={(conversationId) => navigate("salesDialogs", conversationId)} user={user} data={data} reload={loadData} onUserUpdated={refreshIdentity} onLogout={logout} /> + navigate(nextRoute)} selectedEmployeeId={navigation.selectedEmployeeId} selectedProductCode={navigation.selectedProductCode} selectedAgentId={navigation.selectedAgentId} selectedKnowledgeId={navigation.selectedKnowledgeId} selectedConversationId={navigation.selectedConversationId} selectedClientId={navigation.selectedClientId} openClientRoute={(clientId) => navigate("salesClientDetail", clientId)} selectedChannelId={navigation.selectedChannelId} openChannelRoute={(channelId) => navigate("agentDetail", channelId)} selectedSupportPortalId={navigation.selectedSupportPortalId} openSupportPortalRoute={(portalId) => navigate("supportPortalDetail", portalId)} openEmployeeRoute={(employeeId) => navigate("employeeDetail", employeeId)} openAgentCreateRoute={() => navigate("agents")} openAgentRoute={(agentId) => navigate("agentDetail", agentId)} openKnowledgeRoute={(knowledgeId) => navigate("aiKnowledgeDetail", knowledgeId)} openConversationRoute={(conversationId) => navigate("chat", conversationId)} user={user} data={data} reload={loadData} onUserUpdated={refreshIdentity} onLogout={logout} /> )} ); diff --git a/apps/internal-ui/src/auth/access.test.ts b/apps/internal-ui/src/auth/access.test.ts index 18c0b2c..ab7b45e 100644 --- a/apps/internal-ui/src/auth/access.test.ts +++ b/apps/internal-ui/src/auth/access.test.ts @@ -43,15 +43,14 @@ describe("role-based navigation (SPEC-HUB-0031 §3)", () => { it("limits employee to the chat plus self-service", () => { const user = userWith("EMPLOYEE"); expect(isManager(user)).toBe(false); - expect(canAccess(user, "salesDialogs")).toBe(true); - expect(canAccess(user, "supportDialogs")).toBe(true); + expect(canAccess(user, "chat")).toBe(true); expect(canAccess(user, "profile")).toBe(true); expect(canAccess(user, "settings")).toBe(true); expect(canAccess(user, "employees")).toBe(false); expect(canAccess(user, "agents")).toBe(false); expect(canAccess(user, "aiKnowledge")).toBe(false); expect(canAccess(user, "salesClients")).toBe(false); - expect(defaultRoute(user)).toBe("salesDialogs"); + expect(defaultRoute(user)).toBe("chat"); }); it("reads capabilities straight from the session payload", () => { diff --git a/apps/internal-ui/src/auth/access.ts b/apps/internal-ui/src/auth/access.ts index 1e382db..09d21ea 100644 --- a/apps/internal-ui/src/auth/access.ts +++ b/apps/internal-ui/src/auth/access.ts @@ -5,8 +5,7 @@ import type { RouteKey, SessionUser } from "../types"; // фронтенд лишь скрывает недоступное. const EMPLOYEE_ROUTES: ReadonlySet = new Set([ - "salesDialogs", - "supportDialogs", + "chat", "profile", "settings", ]); @@ -27,5 +26,5 @@ export function canAccess(user: SessionUser, route: RouteKey): boolean { export function defaultRoute(user: SessionUser): RouteKey { if (isManager(user)) return "command"; - return "salesDialogs"; + return "chat"; } diff --git a/apps/internal-ui/src/features/chat/ChatPage.tsx b/apps/internal-ui/src/features/chat/ChatPage.tsx new file mode 100644 index 0000000..731ee0f --- /dev/null +++ b/apps/internal-ui/src/features/chat/ChatPage.tsx @@ -0,0 +1,107 @@ +import { useState, type ReactNode } from "react"; + +import { ConversationWorkspace, type DialogScope } from "../conversations/ConversationWorkspace"; +import { DialogControls } from "../conversations/DialogControls"; +import type { ApiConversation, ConversationCounters } from "../conversations/model"; +import type { ConversationListItem } from "../conversations/types"; +import { ClientContext } from "../sales/dialogs/context/ClientContext"; +import { HistoryContext } from "../sales/dialogs/context/HistoryContext"; +import { OperatorCards } from "../support/context/OperatorCards"; +import { SupportHistory } from "../support/context/SupportHistory"; +import type { EmployeeGroup, SessionUser } from "../../types"; + +// Единый «Чат» (дизайн-базлайн v2 §8.1): один экран для всех диалогов +// организации. Контекст-панель сама выбирает представление по источнику +// identity диалога: sales-контакт или verified support-снапшот. + +type ChatRightTab = "client" | "history"; + +export function ChatPage({ + initialConversationId, + user, + groups = [], + employees = [], + scope, + setScope, + counters, + showScopeSwitcher, +}: { + initialConversationId?: number | null; + user: SessionUser; + groups?: EmployeeGroup[]; + employees?: Array<{ id: number; name: string }>; + scope: DialogScope; + setScope: (scope: DialogScope) => void; + counters: ConversationCounters | null; + showScopeSwitcher: boolean; +}) { + const [rightTab, setRightTab] = useState("client"); + return ( + ( + + )} + /> + ); +} + +function ChatContextPanel({ + rightTab, + setRightTab, + dialog, + detail, + groups, + employees, + applyConversation, +}: { + rightTab: ChatRightTab; + setRightTab: (tab: ChatRightTab) => void; + dialog: ConversationListItem | null; + detail: ApiConversation | null; + groups: EmployeeGroup[]; + employees: Array<{ id: number; name: string }>; + applyConversation: (updated: ApiConversation) => void; +}) { + const isSupport = Boolean(detail?.supportIdentitySnapshot); + return ( +
+
+ setRightTab("client")}>Контакт + setRightTab("history")}>История +
+
+ {rightTab === "client" && ( + isSupport ? ( + <> + {detail && ( + + )} + + + ) : ( + + ) + )} + {rightTab === "history" && (isSupport ? : )} +
+
+ ); +} + +function RightTabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: ReactNode }) { + return ; +} diff --git a/apps/internal-ui/src/features/chat/useChatScope.ts b/apps/internal-ui/src/features/chat/useChatScope.ts new file mode 100644 index 0000000..330dda9 --- /dev/null +++ b/apps/internal-ui/src/features/chat/useChatScope.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; + +import type { DialogScope } from "../conversations/ConversationWorkspace"; +import { fetchConversationCounters, type ConversationCounters } from "../conversations/model"; + +// Охват чата (дерево «Все диалоги · Группы · Агенты») живёт на уровне Shell: +// у сотрудника им управляет сайдбар, у менеджера — поповер в заголовке списка. +export function useChatScope(enabled: boolean) { + const [scope, setScope] = useState({ kind: "all" }); + const [counters, setCounters] = useState(null); + + useEffect(() => { + if (!enabled) return; + let cancelled = false; + const load = () => + fetchConversationCounters() + .then((payload) => { + if (!cancelled) setCounters(payload); + }) + .catch(() => undefined); + void load(); + const timer = setInterval(load, 30000); + return () => { + cancelled = true; + clearInterval(timer); + }; + }, [enabled]); + + return { scope, setScope, counters }; +} diff --git a/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx b/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx index 24863fb..e4d5d15 100644 --- a/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx +++ b/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx @@ -9,7 +9,6 @@ import { closeConversation, controlModeOf, fetchConversation, - fetchConversationCounters, fetchConversations, markConversationAsSpam, releaseConversation, @@ -46,16 +45,20 @@ import { useIncomingMessageSound } from "./useIncomingMessageSound"; // Общий workspace диалогов (SPEC-HUB-0010 §8.2). Видимость inbox решает // backend по группам (ADR-HUB-0043); страница параметризуется заголовком, // placeholder поиска и правой панелью через render-prop. -export function ConversationWorkspace({ isOwner = false, listTitle, searchPlaceholder, renderContextPanel, initialConversationId }: { +export function ConversationWorkspace({ isOwner = false, listTitle, searchPlaceholder, renderContextPanel, initialConversationId, scope, setScope, counters, showScopeSwitcher = true }: { isOwner?: boolean; listTitle?: string; searchPlaceholder?: string; renderContextPanel: (ctx: { dialog: ConversationListItem | null; detail: ApiConversation | null; applyConversation: (updated: ApiConversation) => void }) => ReactNode; initialConversationId?: number | null; + // Охват (дерево фильтров) живёт в Shell: у сотрудника им управляет сайдбар, + // у менеджера — поповер в заголовке списка. + scope: DialogScope; + setScope: (scope: DialogScope) => void; + counters: ConversationCounters | null; + showScopeSwitcher?: boolean; }) { const [listTab, setListTab] = useState("all"); - const [scope, setScope] = useState({ kind: "all" }); - const [counters, setCounters] = useState(null); const [search, setSearch] = useState(""); const [conversations, setConversations] = useState([]); const [listLoaded, setListLoaded] = useState(false); @@ -99,23 +102,6 @@ export function ConversationWorkspace({ isOwner = false, listTitle, searchPlaceh return () => clearInterval(timer); }, [loadList]); - // Счётчики дерева охвата (группы/агенты) обновляются реже списка. - useEffect(() => { - let cancelled = false; - const load = () => - fetchConversationCounters() - .then((payload) => { - if (!cancelled) setCounters(payload); - }) - .catch(() => undefined); - void load(); - const timer = setInterval(load, 30000); - return () => { - cancelled = true; - clearInterval(timer); - }; - }, []); - useEffect(() => { if (initialConversationId != null) setSelectedId(initialConversationId); }, [initialConversationId]); @@ -187,6 +173,7 @@ export function ConversationWorkspace({ isOwner = false, listTitle, searchPlaceh scope={scope} counters={counters} setScope={setScope} + showScopeSwitcher={showScopeSwitcher} title={listTitle} searchPlaceholder={searchPlaceholder} dialogs={dialogs} diff --git a/apps/internal-ui/src/features/conversations/DialogList.tsx b/apps/internal-ui/src/features/conversations/DialogList.tsx index 69f8739..ca3b1a5 100644 --- a/apps/internal-ui/src/features/conversations/DialogList.tsx +++ b/apps/internal-ui/src/features/conversations/DialogList.tsx @@ -10,12 +10,13 @@ import type { ConversationCounters } from "./model"; import type { ConversationListItem, ListTab } from "./types"; import { SearchInput } from "../../shared/ui-controls"; -export function DialogList({ title = "Диалоги", searchPlaceholder = "Поиск по клиенту, продукту…", scope, counters, setScope, dialogs, filtered, listTab, selectedId, search, errorText, setSearch, setListTab, setSelectedId }: { +export function DialogList({ title = "Диалоги", searchPlaceholder = "Поиск по клиенту, продукту…", scope, counters, setScope, showScopeSwitcher = true, dialogs, filtered, listTab, selectedId, search, errorText, setSearch, setListTab, setSelectedId }: { title?: string; searchPlaceholder?: string; scope: DialogScope; counters: ConversationCounters | null; setScope: (scope: DialogScope) => void; + showScopeSwitcher?: boolean; dialogs: ConversationListItem[]; filtered: ConversationListItem[]; listTab: ListTab; @@ -31,7 +32,9 @@ export function DialogList({ title = "Диалоги", searchPlaceholder = "По
- + {showScopeSwitcher + ? + :

{scope.kind === "all" ? title : scopeLabel(scope)}

} {dialogs.length} всего
diff --git a/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx b/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx deleted file mode 100644 index 326279b..0000000 --- a/apps/internal-ui/src/features/sales/SalesDialogsPage.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useState } from "react"; - -import { ConversationWorkspace } from "../../features/conversations/ConversationWorkspace"; -import type { EmployeeGroup, SessionUser } from "../../types"; -import { SalesContextPanel } from "./dialogs/SalesContextPanel"; -import type { SalesRightTab } from "./dialogs/context/SalesContextTabs"; - -export function SalesDialogsPage({ initialConversationId, user, groups = [], employees = [] }: { initialConversationId?: number | null; user: SessionUser; groups?: EmployeeGroup[]; employees?: Array<{ id: number; name: string }> }) { - const [rightTab, setRightTab] = useState("client"); - return ( - ( - - )} - /> - ); -} diff --git a/apps/internal-ui/src/features/sales/dialogs/SalesContextPanel.tsx b/apps/internal-ui/src/features/sales/dialogs/SalesContextPanel.tsx deleted file mode 100644 index 5164ec5..0000000 --- a/apps/internal-ui/src/features/sales/dialogs/SalesContextPanel.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ClientContext } from "./context/ClientContext"; -import { HistoryContext } from "./context/HistoryContext"; -import { SalesContextTabs, type SalesRightTab } from "./context/SalesContextTabs"; -import type { ApiConversation } from "../../conversations/model"; -import type { ConversationListItem } from "../../conversations/types"; -import type { EmployeeGroupRef } from "../../../types"; - -export function SalesContextPanel({ rightTab, setRightTab, dialog, detail, groups = [], employees = [], applyConversation }: { rightTab: SalesRightTab; setRightTab: (tab: SalesRightTab) => void; dialog: ConversationListItem | null; detail: ApiConversation | null; groups?: EmployeeGroupRef[]; employees?: Array<{ id: number; name: string }>; applyConversation?: (updated: ApiConversation) => void }) { - return ( -
- -
- {rightTab === "client" && } - {rightTab === "history" && } -
-
- ); -} diff --git a/apps/internal-ui/src/features/sales/dialogs/context/SalesContextTabs.tsx b/apps/internal-ui/src/features/sales/dialogs/context/SalesContextTabs.tsx deleted file mode 100644 index 2dec446..0000000 --- a/apps/internal-ui/src/features/sales/dialogs/context/SalesContextTabs.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { ReactNode } from "react"; - -export type SalesRightTab = "client" | "history"; - -export function SalesContextTabs({ rightTab, setRightTab }: { rightTab: SalesRightTab; setRightTab: (tab: SalesRightTab) => void }) { - return ( -
- setRightTab("client")}>Контакт - setRightTab("history")}>История -
- ); -} - -function RightTabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: ReactNode }) { - return ; -} diff --git a/apps/internal-ui/src/features/support/SupportDialogsPage.tsx b/apps/internal-ui/src/features/support/SupportDialogsPage.tsx deleted file mode 100644 index 60cc08c..0000000 --- a/apps/internal-ui/src/features/support/SupportDialogsPage.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useState } from "react"; - -import { ConversationWorkspace } from "../conversations/ConversationWorkspace"; -import type { EmployeeGroup, SessionUser } from "../../types"; -import { SupportContextPanel, type SupportRightTab } from "./context/SupportContextPanel"; - -export function SupportDialogsPage({ initialConversationId, user, groups = [], employees = [] }: { initialConversationId?: number | null; user: SessionUser; groups?: EmployeeGroup[]; employees?: Array<{ id: number; name: string }> }) { - const [rightTab, setRightTab] = useState("client"); - return ( - ( - - )} - /> - ); -} diff --git a/apps/internal-ui/src/features/support/context/SupportContextPanel.tsx b/apps/internal-ui/src/features/support/context/SupportContextPanel.tsx deleted file mode 100644 index 696affa..0000000 --- a/apps/internal-ui/src/features/support/context/SupportContextPanel.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import type { ReactNode } from "react"; - -import type { ApiConversation } from "../../conversations/model"; -import { DialogControls } from "../../conversations/DialogControls"; -import { OperatorCards } from "./OperatorCards"; -import { SupportHistory } from "./SupportHistory"; -import type { EmployeeGroupRef } from "../../../types"; - -export type SupportRightTab = "client" | "history"; - -export function SupportContextPanel({ rightTab, setRightTab, detail, groups = [], employees = [], applyConversation }: { rightTab: SupportRightTab; setRightTab: (tab: SupportRightTab) => void; detail: ApiConversation | null; groups?: EmployeeGroupRef[]; employees?: Array<{ id: number; name: string }>; applyConversation?: (updated: ApiConversation) => void }) { - return ( -
-
- setRightTab("client")}>Контакт - setRightTab("history")}>История -
-
- {rightTab === "client" && ( - <> - {detail && applyConversation && ( - - )} - - - )} - {rightTab === "history" && } -
-
- ); -} - -function RightTabButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: ReactNode }) { - return ; -} diff --git a/apps/internal-ui/src/layout/Shell.tsx b/apps/internal-ui/src/layout/Shell.tsx index aa67c81..bed23a0 100644 --- a/apps/internal-ui/src/layout/Shell.tsx +++ b/apps/internal-ui/src/layout/Shell.tsx @@ -5,6 +5,8 @@ import type { AppData, Employee, RouteKey, SessionUser } from "../types"; import { NotificationDrawer } from "../features/notifications/NotificationDrawer"; import { fetchNotifications, markAllRead, markRead, type AppNotification } from "../features/notifications/model"; import { fetchWaitingCount } from "../features/conversations/model"; +import { useChatScope } from "../features/chat/useChatScope"; +import { isManager } from "../auth/access"; import { Sidebar } from "./Sidebar"; import { ShellRouteContent } from "./ShellRouteContent"; import { TopBar } from "./TopBar"; @@ -17,6 +19,10 @@ export function Shell({ route, setRoute, selectedEmployeeId, selectedProductCode const [notifOpen, setNotifOpen] = useState(false); const [waitingCount, setWaitingCount] = useState(0); const prevUnread = useRef(null); + const manager = isManager(user); + // Охват чата живёт здесь: сотрудницкий сайдбар и страница чата делят одно + // состояние (дизайн-базлайн v2 §4.1). + const chatScope = useChatScope(true); const loadWaitingCount = useCallback(async () => { try { @@ -58,10 +64,14 @@ export function Shell({ route, setRoute, selectedEmployeeId, selectedProductCode await markRead([notification.id]).catch(() => undefined); void loadNotifications(); } - if (notification.targetRoute === "salesDialogs" && notification.targetId) { + // «salesDialogs»/«conversations» — легаси-маршруты старых уведомлений в БД. + const targetRoute = notification.targetRoute === "salesDialogs" || notification.targetRoute === "conversations" + ? "chat" + : notification.targetRoute; + if (targetRoute === "chat" && notification.targetId) { openConversationRoute(Number(notification.targetId)); - } else if (notification.targetRoute) { - setRoute(notification.targetRoute as RouteKey); + } else if (targetRoute) { + setRoute(targetRoute as RouteKey); } } @@ -76,11 +86,9 @@ export function Shell({ route, setRoute, selectedEmployeeId, selectedProductCode function openEmployee(employee: Employee) { openEmployeeRoute(employee.id); } - const isSalesWorkspace = route === "salesClientDetail" || route === "salesClients" || route === "salesDialogs"; - const isSupportWorkspace = route === "supportOverview" || route === "supportDialogs" || route === "supportPortals" || route === "supportPortalDetail"; - const isSalesDialogs = route === "salesDialogs"; - const isSupportDialogs = route === "supportDialogs"; - const isDialogsWorkspace = isSalesDialogs || isSupportDialogs; + const isSalesWorkspace = route === "salesClientDetail" || route === "salesClients" || route === "chat"; + const isSupportWorkspace = route === "supportOverview" || route === "supportPortals" || route === "supportPortalDetail"; + const isDialogsWorkspace = route === "chat"; const isSalesClients = route === "salesClients"; const isSalesClientDetail = route === "salesClientDetail"; const isAiFullWidth = false; @@ -88,12 +96,13 @@ export function Shell({ route, setRoute, selectedEmployeeId, selectedProductCode const isKnowledgeEditor = route === "aiKnowledgeCreate" || route === "aiKnowledgeDetail"; return (
- +
- { setNotifOpen(true); void loadNotifications(); }} /> + {/* У сотрудника верхней панели нет (дизайн-базлайн v2 §4.1). */} + {manager && { setNotifOpen(true); void loadNotifications(); }} />}
- +
diff --git a/apps/internal-ui/src/layout/ShellRouteContent.tsx b/apps/internal-ui/src/layout/ShellRouteContent.tsx index 9b435b5..be362f6 100644 --- a/apps/internal-ui/src/layout/ShellRouteContent.tsx +++ b/apps/internal-ui/src/layout/ShellRouteContent.tsx @@ -11,10 +11,11 @@ import { EmployeeDetailPage, EmployeesPage } from "../features/employees/Employe import { ProfilePage } from "../features/profile/ProfilePage"; import { SettingsPage } from "../features/settings/SettingsPage"; import { AdministrationPage } from "../features/administration/AdministrationPage"; +import { ChatPage } from "../features/chat/ChatPage"; +import type { DialogScope } from "../features/conversations/ConversationWorkspace"; +import type { ConversationCounters } from "../features/conversations/model"; import { SalesClientDetailPage } from "../features/sales/client-detail/SalesClientDetailPage"; import { SalesClientsPage } from "../features/sales/SalesClientsPage"; -import { SalesDialogsPage } from "../features/sales/SalesDialogsPage"; -import { SupportDialogsPage } from "../features/support/SupportDialogsPage"; import { SupportOverviewPage } from "../features/support/SupportOverviewPage"; import { LoadingState } from "../shared/ui"; import type { AppData, Employee, RouteKey, SessionUser } from "../types"; @@ -27,7 +28,7 @@ const SupportPortalDetailPage = lazy(() => import("../features/support-portals/S (module) => ({ default: module.SupportPortalDetailPage }), )); -export function ShellRouteContent({ route, data, currentEmployee, selectedProductCode, selectedAgentId, selectedKnowledgeId, selectedConversationId, selectedClientId, openClient, selectedChannelId, openChannel, selectedSupportPortalId, openSupportPortal, openConversation, openEmployee, openAgentCreate, openAgent, openKnowledge, onAgentLoaded, onChannelLoaded, reload, setRoute, user, onUserUpdated, onLogout }: { route: RouteKey; data: AppData; currentEmployee: Employee | null; selectedProductCode: string | null; selectedAgentId: number | null; selectedKnowledgeId: number | null; selectedConversationId: number | null; selectedClientId: number | null; openClient: (clientId: number) => void; selectedChannelId: number | null; openChannel: (channelId: number) => void; selectedSupportPortalId: number | null; openSupportPortal: (portalId: number) => void; openConversation: (conversationId: number) => void; openEmployee: (employee: Employee) => void; openAgentCreate: (productCode: string | null) => void; openAgent: (agentId: number) => void; openKnowledge: (knowledgeId: number) => void; onAgentLoaded: (name: string | null) => void; onChannelLoaded: (name: string | null) => void; reload: () => void; setRoute: (route: RouteKey) => void; user: SessionUser; onUserUpdated: (user: SessionUser) => void; onLogout: () => void }) { +export function ShellRouteContent({ chatScope, setChatScope, chatCounters, chatScopeSwitcher, route, data, currentEmployee, selectedProductCode, selectedAgentId, selectedKnowledgeId, selectedConversationId, selectedClientId, openClient, selectedChannelId, openChannel, selectedSupportPortalId, openSupportPortal, openConversation, openEmployee, openAgentCreate, openAgent, openKnowledge, onAgentLoaded, onChannelLoaded, reload, setRoute, user, onUserUpdated, onLogout }: { chatScope: DialogScope; setChatScope: (scope: DialogScope) => void; chatCounters: ConversationCounters | null; chatScopeSwitcher: boolean; route: RouteKey; data: AppData; currentEmployee: Employee | null; selectedProductCode: string | null; selectedAgentId: number | null; selectedKnowledgeId: number | null; selectedConversationId: number | null; selectedClientId: number | null; openClient: (clientId: number) => void; selectedChannelId: number | null; openChannel: (channelId: number) => void; selectedSupportPortalId: number | null; openSupportPortal: (portalId: number) => void; openConversation: (conversationId: number) => void; openEmployee: (employee: Employee) => void; openAgentCreate: (productCode: string | null) => void; openAgent: (agentId: number) => void; openKnowledge: (knowledgeId: number) => void; onAgentLoaded: (name: string | null) => void; onChannelLoaded: (name: string | null) => void; reload: () => void; setRoute: (route: RouteKey) => void; user: SessionUser; onUserUpdated: (user: SessionUser) => void; onLogout: () => void }) { return ( <> {route === "command" && } @@ -47,8 +48,18 @@ export function ShellRouteContent({ route, data, currentEmployee, selectedProduc {route === "salesClients" && } {route === "salesClientDetail" && } {route === "supportOverview" && } - {route === "supportDialogs" && } - {route === "salesDialogs" && } + {route === "chat" && ( + + )} {route === "agents" && } {route === "agentDetail" && ( [0]["name"]; label: string; route: RouteKey; - routeKey?: RouteKey; + routeKey: RouteKey; setRoute: (route: RouteKey) => void; }; -function SidebarLink({ activeRoutes, disabled, icon, label, route, routeKey, setRoute }: SidebarLinkProps) { +function SidebarLink({ activeRoutes, badge, icon, label, route, routeKey, setRoute }: SidebarLinkProps) { const active = activeRoutes?.includes(route) ?? routeKey === route; return ( ); } -const SALES_ITEMS: SidebarNavSectionItem[] = [ - { activeRoutes: ["salesDialogs"], key: "salesDialogs", label: "Диалоги" }, - { activeRoutes: ["salesClients", "salesClientDetail"], key: "salesClients", label: "Контакты" }, -]; - -const SUPPORT_ITEMS: SidebarNavSectionItem[] = [ - { activeRoutes: ["supportOverview"], key: "supportOverview", label: "Обзор" }, - { activeRoutes: ["supportDialogs"], key: "supportDialogs", label: "Диалоги" }, - { activeRoutes: ["supportPortals", "supportPortalDetail"], key: "supportPortals", label: "Порталы" }, -]; - -const AI_ITEMS: SidebarNavSectionItem[] = [ +const SERVICE_ITEMS: SidebarNavSectionItem[] = [ + { activeRoutes: ["command"], key: "command", label: "Обзор" }, + { activeRoutes: ["supportOverview"], key: "supportOverview", label: "Обзор поддержки" }, { activeRoutes: ["aiKnowledge", "aiKnowledgeCreate", "aiKnowledgeDetail"], key: "aiKnowledge", label: "Знания" }, - { activeRoutes: ["aiUsage"], disabled: true, key: "aiUsage", label: "Использование AI" }, + { activeRoutes: ["aiUsage"], key: "aiUsage", label: "Использование AI" }, + { activeRoutes: ["integrations"], key: "integrations", label: "Интеграции" }, + { activeRoutes: ["administrationOrganization"], key: "administrationOrganization", label: "Организация" }, + { activeRoutes: ["administrationAudit"], key: "administrationAudit", label: "Аудит" }, ]; -const ADMINISTRATION_ITEMS: SidebarNavSectionItem[] = [ - { - activeRoutes: ["administrationOrganization"], - key: "administrationOrganization", - label: "Организация", - }, - { - activeRoutes: ["administrationAudit"], - key: "administrationAudit", - label: "Аудит", - }, -]; - -function visibleItems(user: SessionUser, items: SidebarNavSectionItem[]) { - return items.filter((item) => canAccess(user, item.key)); -} - -export function Sidebar({ route, user, setRoute, onLogout, waitingCount = 0 }: { route: RouteKey; user: SessionUser; setRoute: (route: RouteKey) => void; onLogout: () => void; waitingCount?: number }) { +export function Sidebar({ + route, + user, + setRoute, + onLogout, + waitingCount = 0, + chatScope, + setChatScope, + chatCounters, +}: { + route: RouteKey; + user: SessionUser; + setRoute: (route: RouteKey) => void; + onLogout: () => void; + waitingCount?: number; + chatScope: DialogScope; + setChatScope: (scope: DialogScope) => void; + chatCounters: ConversationCounters | null; +}) { + const manager = isManager(user); const sectionStorageKey = (section: string) => ( `chatbolls.sidebar.${user.organizationPublicId}.${section}.expanded` ); - const salesItems = visibleItems(user, SALES_ITEMS).map((item) => ( - item.key === "salesDialogs" && waitingCount > 0 - ? { ...item, badge: String(waitingCount) } - : item - )); - const supportItems = visibleItems(user, SUPPORT_ITEMS); - const aiItems = visibleItems(user, AI_ITEMS); - const administrationItems = visibleItems(user, ADMINISTRATION_ITEMS); - const isManagerUser = isManager(user); return (
-
ChatbollsУправление компанией
+
{user.organizationName || "Chatbolls"}Chatbolls
- + ) : ( + { + setChatScope(scope); + setRoute("chat"); + }} /> - - {isManagerUser && } + )} + {manager && } ); } + +// Дерево «Диалоги» сотрудника (кадры A-H): пункты — это охват списка чата. +function ChatScopeTree({ + route, + scope, + counters, + setScope, +}: { + route: RouteKey; + scope: DialogScope; + counters: ConversationCounters | null; + setScope: (scope: DialogScope) => void; +}) { + const inChat = route === "chat"; + const isActive = (candidate: DialogScope) => + inChat + && scope.kind === candidate.kind + && (scope.kind !== "group" || candidate.kind !== "group" || scope.id === candidate.id) + && (scope.kind !== "agent" || candidate.kind !== "agent" || scope.id === candidate.id); + + return ( + + ); +} diff --git a/apps/internal-ui/src/layout/TopBar.tsx b/apps/internal-ui/src/layout/TopBar.tsx index 4126189..4abd65b 100644 --- a/apps/internal-ui/src/layout/TopBar.tsx +++ b/apps/internal-ui/src/layout/TopBar.tsx @@ -6,8 +6,7 @@ export function TopBar({ route, user, currentEmployee, currentAgentName, current const isCommand = route === "command"; const isAiDetail = route === "aiKnowledgeCreate" || route === "aiKnowledgeDetail"; const isAi = route.startsWith("ai") && !isAiDetail; - const isSalesWorkspace = route === "salesClientDetail" || route === "salesClients" || route === "salesDialogs"; - const isSupportWorkspace = route === "supportOverview" || route === "supportDialogs" || route === "supportPortals" || route === "supportPortalDetail"; + const isSupportWorkspace = route === "supportOverview" || route === "supportPortals" || route === "supportPortalDetail"; return (
@@ -15,14 +14,13 @@ export function TopBar({ route, user, currentEmployee, currentAgentName, current / {route === "employeeDetail" && <>/{currentEmployee?.fullName || currentEmployee?.email || routes[route]}} {route === "agentDetail" && <>/{currentAgentName || routes[route]}} - {isSalesWorkspace && route !== "salesClientDetail" && <>/{routes[route]}} - {route === "salesClientDetail" && <>//{routes[route]}} + {route === "salesClientDetail" && <>/{routes[route]}} {isSupportWorkspace && route !== "supportPortalDetail" && <>/{routes[route]}} {route === "supportPortalDetail" && <>//{routes[route]}} {isAi && <>/{routes[route]}} {route === "aiKnowledgeDetail" && <>//{currentAgentName || routes[route]}} {route === "aiKnowledgeCreate" && <>//{routes[route]}} - {route !== "employeeDetail" && route !== "agentDetail" && route !== "supportPortalDetail" && !isSalesWorkspace && !isSupportWorkspace && !isAi && !isAiDetail && {routes[route]}} + {route !== "employeeDetail" && route !== "agentDetail" && route !== "supportPortalDetail" && route !== "salesClientDetail" && !isSupportWorkspace && !isAi && !isAiDetail && {routes[route]}}
diff --git a/apps/internal-ui/src/layout/styles.css b/apps/internal-ui/src/layout/styles.css index 6c3a0e5..1f0e15b 100644 --- a/apps/internal-ui/src/layout/styles.css +++ b/apps/internal-ui/src/layout/styles.css @@ -203,6 +203,106 @@ background: var(--n-9); } +/* Дерево «Диалоги» сотрудника (дизайн-базлайн v2 §3): сайдбар = фильтр чата. */ +.chat-scope-head { + display: flex; + align-items: center; + gap: 9px; + height: 34px; + padding: 0 8px; + color: var(--n-1); + font-size: 13.5px; + font-weight: 700; +} + +.chat-scope-section { + display: flex; + align-items: center; + gap: 8px; + height: 30px; + margin: 6px 0 0 16px; + padding: 0 10px 0 12px; + color: var(--n-4); + font-size: 12.5px; + font-weight: 600; +} + +.chat-scope-item { + display: flex; + align-items: center; + gap: 9px; + height: 32px; + padding: 0 10px 0 12px; + border: 0; + border-radius: 8px; + color: var(--n-2); + background: transparent; + font-size: 13px; + font-weight: 500; + text-align: left; +} + +.chat-scope-item.is-top { + height: 34px; + margin-left: 16px; +} + +.chat-scope-item.is-nested { + margin-left: 34px; +} + +.chat-scope-item:hover { + background: var(--n-9); +} + +.chat-scope-item.is-active { + color: var(--primary-text); + background: var(--primary-bg); + font-weight: 600; +} + +.chat-scope-item > span:not(.chat-scope-agent) { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.chat-scope-item small { + color: var(--n-5); + font-size: 11.5px; + font-weight: 600; +} + +.chat-scope-item.is-active small { + color: var(--primary-text); +} + +.chat-scope-dot { + flex: none; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--primary); +} + +.chat-scope-dot.is-muted { + background: var(--n-5); +} + +.chat-scope-agent { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 5px; + color: var(--ai); + background: color-mix(in srgb, var(--ai) 14%, var(--surface-card)); +} + .profile-link { margin: 0; padding: 8px 10px; diff --git a/apps/internal-ui/src/router.test.ts b/apps/internal-ui/src/router.test.ts index 833b09e..55a1cf4 100644 --- a/apps/internal-ui/src/router.test.ts +++ b/apps/internal-ui/src/router.test.ts @@ -54,14 +54,24 @@ describe("ai knowledge routes", () => { }); describe("support routes", () => { - it("parses support overview and dialogs URLs", () => { + it("parses the support overview URL", () => { expect(routeFromPath("/departments/support")).toEqual({ route: "supportOverview", ...empty }); - expect(routeFromPath("/departments/support/dialogs")).toEqual({ route: "supportDialogs", ...empty }); }); - it("creates support overview and dialogs URLs", () => { + it("creates the support overview URL", () => { expect(pathFromRoute("supportOverview")).toBe("/departments/support"); - expect(pathFromRoute("supportDialogs")).toBe("/departments/support/dialogs"); + }); +}); + +describe("chat route", () => { + it("parses /chat and the legacy dialog URLs", () => { + expect(routeFromPath("/chat")).toEqual({ route: "chat", ...empty }); + expect(routeFromPath("/departments/sales/dialogs")).toEqual({ route: "chat", ...empty }); + expect(routeFromPath("/departments/support/dialogs")).toEqual({ route: "chat", ...empty }); + }); + + it("creates the chat URL", () => { + expect(pathFromRoute("chat")).toBe("/chat"); }); }); diff --git a/apps/internal-ui/src/router.ts b/apps/internal-ui/src/router.ts index eaed42b..097992f 100644 --- a/apps/internal-ui/src/router.ts +++ b/apps/internal-ui/src/router.ts @@ -20,15 +20,16 @@ export function routeFromPath(pathname: string, search = ""): RouteState { const base = { employeeId: null, productCode: null, agentId: null, knowledgeId: null, clientId: null, channelId: null, supportPortalId: null }; const state = { organizationPublicId, ...base }; if (path === "/" || path === "/command") return { route: "command", ...state }; - if (path === "/departments/sales") return { route: "salesDialogs", ...state }; + // Устаревшие адреса разделённых чатов ведут в единый «Чат». + if (path === "/chat" || path === "/departments/sales" || path === "/departments/sales/dialogs" || path === "/departments/support/dialogs") { + return { route: "chat", ...state }; + } if (path === "/departments/support") return { route: "supportOverview", ...state }; - if (path === "/departments/support/dialogs") return { route: "supportDialogs", ...state }; if (path === "/departments/sales/clients") return { route: "salesClients", ...state }; if (path.startsWith("/departments/sales/clients/")) { const id = Number(path.split("/")[4]); return Number.isInteger(id) && id > 0 ? { ...state, route: "salesClientDetail", clientId: id } : { route: "salesClients", ...state }; } - if (path === "/departments/sales/dialogs") return { route: "salesDialogs", ...state }; if (path === "/employees") return { route: "employees", ...state }; if (path.startsWith("/employees/")) { const id = Number(path.split("/")[2]); @@ -76,10 +77,9 @@ export function pathFromRoute(route: RouteKey, entityId: number | null = null, p const prefix = organizationPublicId ? `/organizations/${organizationPublicId}` : ""; if (route === "command") return `${prefix}/`; if (route === "supportOverview") return `${prefix}/departments/support`; - if (route === "supportDialogs") return `${prefix}/departments/support/dialogs`; if (route === "salesClients") return `${prefix}/departments/sales/clients`; if (route === "salesClientDetail") return entityId ? `${prefix}/departments/sales/clients/${entityId}` : `${prefix}/departments/sales/clients`; - if (route === "salesDialogs") return `${prefix}/departments/sales/dialogs`; + if (route === "chat") return `${prefix}/chat`; if (route === "employees") return `${prefix}/employees`; if (route === "employeeDetail") return entityId ? `${prefix}/employees/${entityId}` : `${prefix}/employees`; if (route === "supportPortals") return `${prefix}/departments/support/portals`; diff --git a/apps/internal-ui/src/routes.ts b/apps/internal-ui/src/routes.ts index a245b07..3e98578 100644 --- a/apps/internal-ui/src/routes.ts +++ b/apps/internal-ui/src/routes.ts @@ -10,9 +10,8 @@ export const routes: Record = { settings: "Настройки", salesClientDetail: "Контакт", salesClients: "Контакты", - salesDialogs: "Диалоги", + chat: "Чат", supportOverview: "Обзор", - supportDialogs: "Диалоги", supportPortals: "Порталы", supportPortalDetail: "Портал поддержки", agents: "Агенты", diff --git a/apps/internal-ui/src/shared/icons.tsx b/apps/internal-ui/src/shared/icons.tsx index 18acb8d..3276c9c 100644 --- a/apps/internal-ui/src/shared/icons.tsx +++ b/apps/internal-ui/src/shared/icons.tsx @@ -35,12 +35,13 @@ export function MaxLogo({ size = 24 }: { size?: number }) { ); } -export function Icon({ name, size = 17 }: { name: "grid" | "building" | "team" | "box" | "robot" | "plug" | "settings" | "bell" | "chevron" | "plus" | "arrow" | "paperclip" | "send" | "phone" | "video" | "lock" | "mail" | "eye" | "eyeOff" | "logout" | "user" | "refresh" | "transfer" | "warning" | "bolt" | "shop" | "search" | "more" | "external" | "key" | "pause" | "play" | "percent" | "save" | "check" | "copy" | "clock" | "message" | "cart" | "columns" | "download" | "list" | "split" | "gitBranch" | "route" | "expand" | "edit" | "trash" | "wrench" | "xCircle" | "folder" | "grip"; size?: number }) { +export function Icon({ name, size = 17 }: { name: "grid" | "building" | "team" | "box" | "robot" | "plug" | "settings" | "bell" | "chevron" | "plus" | "arrow" | "paperclip" | "send" | "phone" | "video" | "lock" | "mail" | "eye" | "eyeOff" | "logout" | "user" | "refresh" | "transfer" | "warning" | "bolt" | "shop" | "search" | "more" | "external" | "key" | "pause" | "play" | "percent" | "save" | "check" | "copy" | "clock" | "message" | "inbox" | "cart" | "columns" | "download" | "list" | "split" | "gitBranch" | "route" | "expand" | "edit" | "trash" | "wrench" | "xCircle" | "folder" | "grip"; size?: number }) { const common = { width: size, height: size, fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round" } as const; const paths: Record = { grid: <>, building: <>, team: <>, + inbox: <>, box: <>, // Tabler ti-robot. Глаза — точки: рисуются нулевыми сегментами v.01 // и держатся на strokeLinecap="round" из common. diff --git a/apps/internal-ui/src/types.ts b/apps/internal-ui/src/types.ts index 82466d7..3f84134 100644 --- a/apps/internal-ui/src/types.ts +++ b/apps/internal-ui/src/types.ts @@ -115,7 +115,7 @@ export type Product = { updatedAt: string; }; -export type RouteKey = "administrationOrganization" | "administrationAudit" | "command" | "employeeDetail" | "employees" | "profile" | "settings" | "salesClientDetail" | "salesClients" | "salesDialogs" | "supportOverview" | "supportDialogs" | "supportPortals" | "supportPortalDetail" | "agents" | "agentDetail" | "aiKnowledge" | "aiKnowledgeCreate" | "aiKnowledgeDetail" | "aiUsage" | "integrations"; +export type RouteKey = "administrationOrganization" | "administrationAudit" | "command" | "employeeDetail" | "employees" | "profile" | "settings" | "salesClientDetail" | "salesClients" | "chat" | "supportOverview" | "supportPortals" | "supportPortalDetail" | "agents" | "agentDetail" | "aiKnowledge" | "aiKnowledgeCreate" | "aiKnowledgeDetail" | "aiUsage" | "integrations"; export type AppData = { employees: Employee[]; diff --git a/apps/internal-ui/src/useRouteNavigation.ts b/apps/internal-ui/src/useRouteNavigation.ts index 8f90844..e5b0a01 100644 --- a/apps/internal-ui/src/useRouteNavigation.ts +++ b/apps/internal-ui/src/useRouteNavigation.ts @@ -49,7 +49,7 @@ export function useRouteNavigation( }; applyRouteState(nextState); setSelectedConversationId( - nextRoute === "salesDialogs" || nextRoute === "supportDialogs" ? entityId : null, + nextRoute === "chat" ? entityId : null, ); const nextPath = pathFromRoute( nextRoute,