✨ feat(webchat): перенести настройки WEB-подключения на страницу

This commit is contained in:
Andrey committed 2026-09-30 09:33:34 +03:00
1 parent 36c95199a0
commit dc6e38b689
26 files changed
+438 -43

No files matched your search

+1 -1
View File
@@ -250,7 +250,7 @@ export function App() {
// Ключ организации — на провайдере: при переключении сокет обязан
// переоткрыться на адрес новой, а не остаться на прежней.
<RealtimeProvider key={user.organizationPublicId}>
<Shell key={user.organizationPublicId} route={navigation.route} setRoute={(nextRoute) => navigate(nextRoute)} selectedEmployeeId={navigation.selectedEmployeeId} 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)} portalSettingsSection={navigation.selectedPortalSection} openPortalSettingsRoute={(portalId, section) => navigate("supportPortalSettings", `${portalId}/${section ?? ""}`)} settingsSection={navigation.selectedSettingsSection} openSettingsRoute={(section) => navigate("settings", section)} openEmployeeRoute={(employeeId) => navigate("employeeDetail", employeeId)} openAgentRoute={(agentId) => navigate("agentDetail", agentId)} openKnowledgeRoute={(knowledgeId) => navigate("knowledgeDetail", knowledgeId)} openKnowledgeEditorRoute={(knowledgeId) => (knowledgeId === null ? navigate("knowledgeCreate") : navigate("knowledgeEdit", knowledgeId))} openConversationRoute={(conversationId) => navigate("chat", conversationId)} user={user} data={data} reload={loadData} onUserUpdated={refreshIdentity} onLogout={logout} onSwitchOrganization={switchOrganization} onOrganizationCreated={finishOrganizationCreate} />
<Shell key={user.organizationPublicId} route={navigation.route} setRoute={(nextRoute) => navigate(nextRoute)} selectedEmployeeId={navigation.selectedEmployeeId} 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)} selectedIntegrationId={navigation.selectedIntegrationId} openWebIntegrationRoute={(id) => navigate("webIntegrationSettings", id)} selectedSupportPortalId={navigation.selectedSupportPortalId} openSupportPortalRoute={(portalId) => navigate("supportPortalDetail", portalId)} portalSettingsSection={navigation.selectedPortalSection} openPortalSettingsRoute={(portalId, section) => navigate("supportPortalSettings", `${portalId}/${section ?? ""}`)} settingsSection={navigation.selectedSettingsSection} openSettingsRoute={(section) => navigate("settings", section)} openEmployeeRoute={(employeeId) => navigate("employeeDetail", employeeId)} openAgentRoute={(agentId) => navigate("agentDetail", agentId)} openKnowledgeRoute={(knowledgeId) => navigate("knowledgeDetail", knowledgeId)} openKnowledgeEditorRoute={(knowledgeId) => (knowledgeId === null ? navigate("knowledgeCreate") : navigate("knowledgeEdit", knowledgeId))} openConversationRoute={(conversationId) => navigate("chat", conversationId)} user={user} data={data} reload={loadData} onUserUpdated={refreshIdentity} onLogout={logout} onSwitchOrganization={switchOrganization} onOrganizationCreated={finishOrganizationCreate} />
</RealtimeProvider>
)}
</ConfigProvider>
@@ -42,7 +42,9 @@ export function ConnectionsTable({ items, testingId, onTest, onEdit, onToggleAct
<td>
<div className="product-cell">
<ConnectionIcon provider={item.provider} />
<span><strong>{item.name}</strong><small>{subtitle(item)}</small></span>
<span>{item.provider === "WEB"
? <button className="link is-strong" type="button" onClick={() => onEdit(item)}>{item.name}</button>
: <strong>{item.name}</strong>}<small>{subtitle(item)}</small></span>
</div>
</td>
<td>
@@ -0,0 +1,42 @@
import { Modal } from "antd";
import { useState } from "react";
import { api } from "../../api/client";
import { t } from "../../i18n";
import { Button } from "../../shared/ui-controls";
import type { Integration } from "./model";
export function DeleteIntegrationDialog({ integration, onClose, onDeleted }: {
integration: Integration;
onClose: () => void;
onDeleted: () => void;
}) {
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
async function confirmDelete() {
setBusy(true);
setError(null);
try {
await api(`/api/v1/integrations/${integration.id}/`, { method: "DELETE" });
onDeleted();
} catch (caught) {
setError(caught instanceof Error ? caught.message : t("settings.could_not_delete"));
} finally {
setBusy(false);
}
}
return (
<Modal open title={t("settings.delete_integration")} onCancel={onClose} footer={null} destroyOnClose>
<div className="integration-form">
<p>{t("settings.will_be_deleted_irreversible", { name: integration.name })}</p>
{error && <div className="integration-form-error">{error}</div>}
<div className="integration-form-actions">
<Button variant="secondary" disabled={busy} onClick={onClose}>{t("common.cancel")}</Button>
<Button variant="danger-outline" disabled={busy} onClick={() => void confirmDelete()}>{t("common.delete")}</Button>
</div>
</div>
</Modal>
);
}
@@ -1,9 +1,8 @@
import { Modal } from "antd";
import { useState } from "react";
import { api } from "../../api/client";
import { DeleteIntegrationDialog } from "./DeleteIntegrationDialog";
import { EmptyState } from "../../shared/ui";
import { Button } from "../../shared/ui-controls";
import { ConnectionsTable } from "./ConnectionsTable";
import type { Integration, IntegrationKind } from "./model";
import { ProvidersTable } from "./ProvidersTable";
@@ -22,7 +21,6 @@ export function IntegrationsSection({ kind, items, reload, onEdit }: {
}) {
const [testingId, setTestingId] = useState<number | null>(null);
const [deleting, setDeleting] = useState<Integration | null>(null);
const [deletingError, setDeletingError] = useState<string | null>(null);
async function test(integration: Integration) {
setTestingId(integration.id);
@@ -47,24 +45,13 @@ export function IntegrationsSection({ kind, items, reload, onEdit }: {
}
}
async function confirmDelete() {
if (!deleting) return;
try {
await api(`/api/v1/integrations/${deleting.id}/`, { method: "DELETE" });
setDeleting(null);
reload();
} catch (caught) {
setDeletingError(caught instanceof Error ? caught.message : t("settings.could_not_delete"));
}
}
const isConnections = kind === "MESSENGER";
const rowHandlers = {
testingId,
onTest: test,
onEdit,
onToggleActive: toggleActive,
onDelete: (item: Integration) => { setDeletingError(null); setDeleting(item); },
onDelete: (item: Integration) => setDeleting(item),
};
return (
@@ -76,21 +63,7 @@ export function IntegrationsSection({ kind, items, reload, onEdit }: {
) : (
<ProvidersTable items={items} {...rowHandlers} />
)}
{isConnections && items.length > 0 && (
<p className="settings-section-note">{t("settings.web_widget_embed_snippet_copied")}</p>
)}
{deleting && (
<Modal open title={t("settings.delete_integration")} onCancel={() => setDeleting(null)} footer={null} destroyOnClose>
<div className="integration-form">
<p>{t("settings.will_be_deleted_irreversible", { name: deleting.name })}</p>
{deletingError && <div className="integration-form-error">{deletingError}</div>}
<div className="integration-form-actions">
<Button variant="secondary" onClick={() => setDeleting(null)}>{t("common.cancel")}</Button>
<Button variant="danger-outline" onClick={() => void confirmDelete()}>{t("common.delete")}</Button>
</div>
</div>
</Modal>
)}
{deleting && <DeleteIntegrationDialog integration={deleting} onClose={() => setDeleting(null)} onDeleted={() => { setDeleting(null); reload(); }} />}
</div>
);
}
@@ -0,0 +1,67 @@
import { useState } from "react";
import { api } from "../../api/client";
import { t } from "../../i18n";
import { FormField } from "../../shared/form-controls";
import { Button, CopyButton } from "../../shared/ui-controls";
import {
formatAllowedOrigins, invalidAllowedOrigin, parseAllowedOrigins,
webWidgetSnippet, type Integration,
} from "./model";
export function WebIntegrationBasics({ integration, onSaved }: {
integration: Integration;
onSaved: (updated: Integration) => void;
}) {
const [name, setName] = useState(integration.name);
const [allowedOrigins, setAllowedOrigins] = useState(formatAllowedOrigins(integration.config.allowedOrigins));
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const origins = parseAllowedOrigins(allowedOrigins);
const badOrigin = invalidAllowedOrigin(origins);
const ready = Boolean(name.trim() && origins.length && !badOrigin);
const snippet = integration.webChatWidget ? webWidgetSnippet(integration.webChatWidget.publicKey) : "";
async function save() {
if (!ready) return;
setBusy(true);
setError(null);
try {
const response = await api<{ integration: Integration }>(`/api/v1/integrations/${integration.id}/`, {
method: "PATCH",
body: JSON.stringify({ name: name.trim(), config: { ...integration.config, allowedOrigins: origins } }),
});
onSaved(response.integration);
} catch (caught) {
setError(caught instanceof Error ? caught.message : t("common.could_not_save"));
} finally {
setBusy(false);
}
}
return (
<div className="portal-settings-card web-integration-basics">
<FormField label={t("common.title")} value={name} onChange={setName} />
<div>
<FormField
label={t("settings.allowed_domains")}
value={allowedOrigins}
onChange={setAllowedOrigins}
error={badOrigin && t("settings.unclear_domain", { domain: badOrigin })}
placeholder={t("settings.example_com_example_com_comma")}
/>
<div className="integration-form-hint">{t("settings.sites_where_widget_may_open")}</div>
</div>
{snippet && <div className="web-integration-snippet">
<FormField label={t("settings.embed_snippet_site")} mono value={snippet} />
<CopyButton value={snippet} label={t("common.copy")} className="secondary-button" />
</div>}
{error && <div className="integration-form-error">{error}</div>}
<div className="portal-settings-actions">
<Button variant="primary" disabled={!ready || busy} onClick={() => void save()}>
{busy ? t("ai.saving") : t("common.save")}
</Button>
</div>
</div>
);
}
@@ -0,0 +1,41 @@
import { t } from "../../i18n";
import { Icon } from "../../shared/icons";
import { CopyButton } from "../../shared/ui-controls";
import { webWidgetSnippet, type Integration } from "./model";
export function WebIntegrationHeader({ integration, onOpenSettings, onBack }: {
integration: Integration;
onOpenSettings: () => void;
onBack: () => void;
}) {
const snippet = integration.webChatWidget ? webWidgetSnippet(integration.webChatWidget.publicKey) : "";
const working = integration.isActive && integration.status === "OK";
return (
<header className="web-integration-head">
<nav className="portal-breadcrumbs">
<button className="link is-strong" type="button" onClick={onOpenSettings}>{t("common.settings")}</button>
<span>/</span>
<button className="link is-muted" type="button" onClick={onBack}>{t("common.integrations")}</button>
<span>/</span><b>{integration.name}</b>
</nav>
<div className="web-integration-title">
<span className="web-integration-icon"><Icon name="message" size={17} strokeWidth={1.9} /></span>
<h2>{integration.name}</h2>
{working && <span className="web-integration-status">{t("portals.working")}</span>}
<span className="web-integration-agent">
{integration.channel
? t("settings.web_integration_agent", { name: integration.channel.name })
: t("common.web_widget")}
</span>
</div>
{snippet && <div className="web-integration-embed">
<span className="web-integration-embed-code">
<code>{snippet}</code>
<CopyButton value={snippet} className="web-integration-copy" />
</span>
<span>{t("settings.embed_before_body_end")}</span>
</div>}
</header>
);
}
@@ -0,0 +1,57 @@
import { useState } from "react";
import "../support-portals/styles-detail.css";
import "../support-portals/styles-settings.css";
import { t } from "../../i18n";
import { SectionMenu, type SectionMenuItem } from "../../shared/SectionMenu";
import { EmptyState, LoadingState } from "../../shared/ui";
import { DeleteIntegrationDialog } from "./DeleteIntegrationDialog";
import { WebIntegrationBasics } from "./WebIntegrationBasics";
import { WebIntegrationHeader } from "./WebIntegrationHeader";
import { useWebIntegration } from "./useWebIntegration";
type Section = "basics" | "fields" | "form" | "look" | "danger";
const sections: SectionMenuItem<Section>[] = [
{ key: "basics", label: t("portals.basics"), icon: "settings" },
{ key: "fields", label: t("settings.site_data"), icon: "code", disabled: true },
{ key: "form", label: t("settings.pre_chat_form"), icon: "doc", disabled: true },
{ key: "look", label: t("common.appearance"), icon: "paint", disabled: true, divider: true },
{ key: "danger", label: t("settings.delete_connection"), icon: "trash", danger: true },
];
export function WebIntegrationPage({ integrationId, onOpenSettings, onBack }: {
integrationId: number;
onOpenSettings: () => void;
onBack: () => void;
}) {
const { integration, loading, failed, setIntegration } = useWebIntegration(integrationId);
const [confirmingDelete, setConfirmingDelete] = useState(false);
if (loading) return <LoadingState />;
if (failed) return <EmptyState title={t("settings.could_not_load_integrations")} />;
if (!integration) return <EmptyState title={t("settings.web_integration_not_found")} />;
return (
<section className="web-integration-page">
<WebIntegrationHeader integration={integration} onOpenSettings={onOpenSettings} onBack={onBack} />
<div className="portal-settings-layout">
<SectionMenu
items={sections}
activeKey="basics"
note={t("settings.web_changes_apply_after_save")}
onSelect={(section) => { if (section === "danger") setConfirmingDelete(true); }}
/>
<div className="portal-settings-content">
<div className="portal-settings-inner">
<div className="portal-settings-heading">
<h3>{t("portals.basics")}</h3>
</div>
<WebIntegrationBasics key={integration.id} integration={integration} onSaved={setIntegration} />
</div>
</div>
</div>
{confirmingDelete && <DeleteIntegrationDialog integration={integration} onClose={() => setConfirmingDelete(false)} onDeleted={onBack} />}
</section>
);
}
@@ -0,0 +1,22 @@
import { useCallback, useEffect, useState } from "react";
import { api } from "../../api/client";
import type { Integration } from "./model";
export function useWebIntegration(id: number) {
const [integration, setIntegration] = useState<Integration | null>(null);
const [loading, setLoading] = useState(true);
const [failed, setFailed] = useState(false);
const reload = useCallback(() => {
setLoading(true);
setFailed(false);
api<{ items: Integration[] }>("/api/v1/integrations/")
.then(({ items }) => setIntegration(items.find((item) => item.id === id && item.provider === "WEB") ?? null))
.catch(() => setFailed(true))
.finally(() => setLoading(false));
}, [id]);
useEffect(() => { reload(); }, [reload]);
return { integration, loading, failed, reload, setIntegration };
}
@@ -0,0 +1,98 @@
.web-integration-page {
display: flex;
height: 100%;
min-height: 0;
flex-direction: column;
background: var(--surface-feed);
}
.web-integration-head {
flex: none;
padding: 18px 28px 16px;
border-bottom: 1px solid var(--n-8);
background: var(--surface-card);
}
.web-integration-title, .web-integration-embed {
display: flex;
align-items: center;
gap: 11px;
}
.web-integration-title { margin-bottom: 10px; }
.web-integration-title h2 {
margin: 0;
color: var(--n-1);
font-size: 21px;
font-weight: 700;
letter-spacing: -0.015em;
}
.web-integration-icon {
display: grid;
width: 32px;
height: 32px;
flex: none;
place-items: center;
border-radius: 8px;
background: color-mix(in srgb, #0f9b8e 14%, var(--surface-card));
color: #0f9b8e;
}
.web-integration-status {
padding: 4px 9px;
border-radius: 6px;
background: var(--success-bg);
color: var(--success-text);
font-size: 11.5px;
font-weight: 600;
}
.web-integration-agent { color: var(--n-4); font-size: 12.5px; }
.web-integration-embed { gap: 8px; color: var(--n-4); font-size: 12px; }
.web-integration-embed-code {
display: flex;
min-width: 0;
max-width: 650px;
height: 34px;
align-items: center;
gap: 9px;
padding: 0 5px 0 11px;
border: 1px solid var(--n-7);
border-radius: 9px;
background: var(--surface-sidebar);
}
.web-integration-embed-code code {
overflow: hidden;
color: var(--n-2);
font-family: var(--font-mono);
font-size: 12.5px;
text-overflow: ellipsis;
white-space: nowrap;
}
.web-integration-copy {
display: grid;
width: 26px;
height: 26px;
flex: none;
place-items: center;
padding: 0;
border: 0;
border-radius: 7px;
background: transparent;
color: var(--n-4);
cursor: pointer;
}
.web-integration-copy:hover { background: var(--n-9); color: var(--n-2); }
.web-integration-basics .integration-form-hint { margin-top: 4px; }
.web-integration-snippet { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; }
.web-integration-snippet .readonly-field { width: 100%; }
@media (max-width: 900px) {
.web-integration-head { padding: 16px; }
.web-integration-title, .web-integration-embed { flex-wrap: wrap; }
.web-integration-page .portal-settings-layout { flex-direction: column; }
.web-integration-page .section-menu { width: 100%; border-right: 0; border-bottom: 1px solid var(--n-8); }
.web-integration-page .section-menu-gap, .web-integration-page .section-menu p { display: none; }
.web-integration-page .portal-settings-inner { padding: 20px 16px; }
}
@@ -42,13 +42,14 @@ const SECTION_TOUR_TARGET: Partial<Record<SettingsSectionKey, TourTarget>> = {
// «Платформа» кадра в базлайне не имеет — раздел собран по стандарту N6
// («Хранилище файлов»): заголовок карточки, поля, ряд действий.
export function SettingsPage({ user, onUserUpdated, reload, groups = [], section, openSection, setRoute }: {
export function SettingsPage({ user, onUserUpdated, reload, groups = [], section, openSection, openWebIntegration, setRoute }: {
user: SessionUser;
onUserUpdated: (user: SessionUser) => void;
reload: () => void;
groups?: EmployeeGroup[];
section: SettingsSectionKey | null;
openSection: (section: SettingsSectionKey | null) => void;
openWebIntegration: (id: number) => void;
setRoute: (route: RouteKey) => void;
}) {
const manager = isManager(user);
@@ -159,7 +160,7 @@ export function SettingsPage({ user, onUserUpdated, reload, groups = [], section
integrations={integrations}
providers={providers}
connections={connections}
onEditIntegration={(item) => setForm({ kind: item.kind, initial: item })}
onEditIntegration={(item) => item.provider === "WEB" ? openWebIntegration(item.id) : setForm({ kind: item.kind, initial: item })}
templates={templates}
onEditTemplate={(item) => setTemplateForm({ initial: item })}
/>
+7
View File
@@ -1527,6 +1527,7 @@ export const en: Record<MessageKey, Message> = {
"settings.default_model": "Default model",
"settings.delete_group": "Delete the group?",
"settings.delete_integration": "Delete the integration?",
"settings.delete_connection": "Delete connection",
"settings.demo_contents": "Operators and groups, agents with connections, knowledge, conversations in every state, voice messages, reply templates, a support portal, calls and notifications. Removed as a whole — your own data is untouched.",
"settings.demo_data": "Demo data",
"settings.demo_data_being_installed_sections": "The demo data is being installed — the sections fill up in a few seconds.",
@@ -1542,6 +1543,7 @@ export const en: Record<MessageKey, Message> = {
"settings.email_not_configured_nothing_sent": "Email is not configured: nothing is sent, and inviting an operator will not work.",
"settings.email_sent_to": "An email was sent to {email}. If it does not arrive, check the spam folder.",
"settings.embed_snippet_site": "Embed snippet for the site",
"settings.embed_before_body_end": "insert before </body>",
"settings.endpoint_non_aws_providers": "Endpoint (for non-AWS providers)",
"settings.entry_point": "Entry point",
"settings.entry_point_off": " · switched off",
@@ -1565,6 +1567,7 @@ export const en: Record<MessageKey, Message> = {
"settings.installing": "Installing",
"settings.installing_2": "Installing…",
"settings.integration_actions": "Integration actions",
"settings.pre_chat_form": "Pre-chat form",
"settings.knowledge_attachments_voice_messages_photos": "Knowledge attachments, voice messages, photos and logos",
"settings.knowledge_attachments_voice_messages_photos_2": "Knowledge attachments, voice messages, photos and logos will be written to the bucket under per-organization prefixes. Files already uploaded stay available from disk until they are migrated.",
"settings.language": "Interface language",
@@ -1628,6 +1631,7 @@ export const en: Record<MessageKey, Message> = {
"settings.settings_available_owner_administrators": "Settings are available to the owner and administrators",
"settings.sign_as_demo_operator_private": "Sign in as a demo operator in a private window to see the system through their eyes:",
"settings.sites_where_widget_may_open": "Sites where the widget may open: a domain, subdomains via “*.” or a full origin with a port. Everywhere else the chat answers “Chat is temporarily unavailable”",
"settings.site_data": "Site data",
"settings.smtp_server": "SMTP server",
"settings.storage_reachable_test_object_was": "The storage is reachable: a test object was written and deleted.",
"settings.switched_off": "· switched off",
@@ -1653,6 +1657,9 @@ export const en: Record<MessageKey, Message> = {
"settings.voice_transcription_model": "Voice transcription model",
"settings.was_already_created_by_installation": "was already created by the installation — there is nothing to enter.",
"settings.web_widget_embed_snippet_copied": "The web widget embed snippet is copied from its agent's card.",
"settings.web_changes_apply_after_save": "Changes appear on the site as soon as you save — there is no need to replace the embed snippet.",
"settings.web_integration_agent": "Web widget · agent “{name}”",
"settings.web_integration_not_found": "Web connection not found",
"settings.where_customer_operator_can_record": "Where a customer and an operator can record voice messages and start audio and video calls.",
"settings.where_keep_files": "Where to keep files",
"settings.will_be_deleted_irreversible": "“{name}” will be deleted. This cannot be undone.",
+7
View File
@@ -1528,6 +1528,7 @@ export const ru = {
"settings.default_model": "Модель по умолчанию",
"settings.delete_group": "Удалить группу?",
"settings.delete_integration": "Удалить интеграцию?",
"settings.delete_connection": "Удалить подключение",
"settings.demo_contents": "Сотрудники и группы, агенты с подключениями, знания, диалоги во всех состояниях, голосовые, шаблоны ответов, портал поддержки, звонки и уведомления. Удаляются целиком — ваши данные не затрагиваются.",
"settings.demo_data": "Демо-данные",
"settings.demo_data_being_installed_sections": "Демо-данные устанавливаются — разделы наполнятся через несколько секунд.",
@@ -1543,6 +1544,7 @@ export const ru = {
"settings.email_not_configured_nothing_sent": "Почта не настроена: письма никуда не уходят, пригласить сотрудника не получится.",
"settings.email_sent_to": "Письмо отправлено на {email}. Не пришло — проверьте папку «Спам».",
"settings.embed_snippet_site": "Код вставки на сайт",
"settings.embed_before_body_end": "вставьте перед </body>",
"settings.endpoint_non_aws_providers": "Endpoint (для не-AWS провайдеров)",
"settings.entry_point": "Точка входа",
"settings.entry_point_off": " · выключена",
@@ -1566,6 +1568,7 @@ export const ru = {
"settings.installing": "Устанавливаются",
"settings.installing_2": "Устанавливаются…",
"settings.integration_actions": "Действия интеграции",
"settings.pre_chat_form": "Форма перед чатом",
"settings.knowledge_attachments_voice_messages_photos": "Вложения знаний, голосовые, фото и логотипы",
"settings.knowledge_attachments_voice_messages_photos_2": "Вложения знаний, голосовые, фото и логотипы будут записываться в бакет под префиксами организаций. Уже загруженные файлы остаются доступны с диска, пока не перенесены.",
"settings.language": "Язык интерфейса",
@@ -1629,6 +1632,7 @@ export const ru = {
"settings.settings_available_owner_administrators": "Настройки доступны владельцу и администратору",
"settings.sign_as_demo_operator_private": "Войдите под демо-сотрудником в приватном окне, чтобы увидеть систему его глазами:",
"settings.sites_where_widget_may_open": "Сайты, на которых виджету разрешено открываться: домен, поддомены через «*.» или полный origin с портом. На остальных чат ответит «Чат временно недоступен»",
"settings.site_data": "Данные с сайта",
"settings.smtp_server": "SMTP-сервер",
"settings.storage_reachable_test_object_was": "Хранилище доступно: пробный объект записан и удалён.",
"settings.switched_off": "· выключена",
@@ -1654,6 +1658,9 @@ export const ru = {
"settings.voice_transcription_model": "Модель расшифровки голосовых",
"settings.was_already_created_by_installation": "уже создан установкой — вводить его не нужно.",
"settings.web_widget_embed_snippet_copied": "Код вставки Web-виджета копируется на карточке его агента.",
"settings.web_changes_apply_after_save": "Изменения применяются к виджету на сайте сразу после сохранения — перевставлять код не нужно.",
"settings.web_integration_agent": "Веб-виджет · агент «{name}»",
"settings.web_integration_not_found": "Веб-подключение не найдено",
"settings.where_customer_operator_can_record": "Где клиент и сотрудник могут записывать голосовые и начинать аудио- и видеозвонки.",
"settings.where_keep_files": "Где хранить файлы",
"settings.will_be_deleted_irreversible": "«{name}» будет удалена. Действие необратимо.",
+3 -3
View File
@@ -19,7 +19,7 @@ import { OnboardingProvider, useOnboardingState } from "../features/onboarding/u
import { UpdateBanner } from "../features/updates/UpdateBanner";
import { ShellRouteContent } from "./ShellRouteContent";
export function Shell({ route, setRoute, settingsSection, openSettingsRoute, selectedEmployeeId, selectedAgentId, selectedKnowledgeId, selectedConversationId, selectedClientId, selectedChannelId, selectedSupportPortalId, portalSettingsSection, openChannelRoute, openSupportPortalRoute, openPortalSettingsRoute, openEmployeeRoute, openAgentRoute, openKnowledgeRoute, openKnowledgeEditorRoute, openConversationRoute, openClientRoute, user, data, reload, onUserUpdated, onLogout, onSwitchOrganization, onOrganizationCreated }: { route: RouteKey; setRoute: (route: RouteKey) => void; settingsSection: SettingsSectionKey | null; openSettingsRoute: (section: SettingsSectionKey | null) => void; selectedEmployeeId: number | null; selectedAgentId: number | null; selectedKnowledgeId: number | null; selectedConversationId: number | null; selectedClientId: number | null; selectedChannelId: number | null; selectedSupportPortalId: number | null; portalSettingsSection: PortalSettingsSectionKey | null; openEmployeeRoute: (employeeId: number) => void; openAgentRoute: (agentId: number) => void; openKnowledgeRoute: (knowledgeId: number) => void;
export function Shell({ route, setRoute, settingsSection, openSettingsRoute, selectedEmployeeId, selectedAgentId, selectedKnowledgeId, selectedConversationId, selectedClientId, selectedChannelId, selectedIntegrationId, openWebIntegrationRoute, selectedSupportPortalId, portalSettingsSection, openChannelRoute, openSupportPortalRoute, openPortalSettingsRoute, openEmployeeRoute, openAgentRoute, openKnowledgeRoute, openKnowledgeEditorRoute, openConversationRoute, openClientRoute, user, data, reload, onUserUpdated, onLogout, onSwitchOrganization, onOrganizationCreated }: { route: RouteKey; setRoute: (route: RouteKey) => void; settingsSection: SettingsSectionKey | null; openSettingsRoute: (section: SettingsSectionKey | null) => void; selectedEmployeeId: number | null; selectedAgentId: number | null; selectedKnowledgeId: number | null; selectedConversationId: number | null; selectedClientId: number | null; selectedChannelId: number | null; selectedIntegrationId: number | null; openWebIntegrationRoute: (id: number) => void; selectedSupportPortalId: number | null; portalSettingsSection: PortalSettingsSectionKey | null; openEmployeeRoute: (employeeId: number) => void; openAgentRoute: (agentId: number) => void; openKnowledgeRoute: (knowledgeId: number) => void;
openKnowledgeEditorRoute: (knowledgeId: number | null) => void; openConversationRoute: (conversationId: number) => void; openClientRoute: (clientId: number) => void; openChannelRoute: (channelId: number) => void; openSupportPortalRoute: (portalId: number) => void; openPortalSettingsRoute: (portalId: number, section?: PortalSettingsSectionKey) => void; user: SessionUser; data: AppData; reload: () => void; onUserUpdated: (user: SessionUser) => void; onLogout: () => void; onSwitchOrganization: (organizationPublicId: string) => void; onOrganizationCreated: (identity: AuthenticatedUser, organizationPublicId: string) => void }) {
const [notifications, setNotifications] = useState<AppNotification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
@@ -118,7 +118,7 @@ export function Shell({ route, setRoute, settingsSection, openSettingsRoute, sel
|| route === "knowledgeCategories" || route === "knowledgeImport";
const isKnowledgeEditor = route === "knowledgeCreate" || route === "knowledgeEdit";
// «Настройки» занимают всю область как чат: своё субменю 240px и своя лента.
const isSettings = route === "settings";
const isSettings = route === "settings" || route === "webIntegrationSettings";
// «Профиль» — своя лента на --surface-feed и мобильные подэкраны (кадр M).
const isProfile = route === "profile";
// «Контакты» — своя лента на --surface-feed (кадры K1–K5).
@@ -143,7 +143,7 @@ export function Shell({ route, setRoute, settingsSection, openSettingsRoute, sel
«назад» живут в самой странице, уведомления — в меню профиля сайдбара. */}
<main className={`hub-scroll ${isDialogsWorkspace ? "sales-dialogs-scroll" : ""} ${isAiFullWidth ? "ai-fullwidth-scroll" : ""} ${isSettings ? "settings-scroll" : ""} ${isProfile ? "profile-scroll" : ""} ${isContacts ? "contacts-scroll" : ""} ${isAgents ? "agents-scroll" : ""} ${isEmployees ? "employees-scroll" : ""} ${isAudit ? "audit-scroll" : ""} ${isPortals ? "portals-scroll" : ""} ${isKnowledge || isKnowledgeEditor ? "knowledge-scroll" : ""}`}>
<div key={route} className={`hub-page enter-surface ${isSalesWorkspace || isSupportWorkspace ? "sales-workspace-page" : ""} ${isDialogsWorkspace ? "sales-dialogs-page" : ""} ${isAiFullWidth ? "ai-fullwidth-page" : ""} ${isSettings ? "settings-page" : ""} ${isProfile ? "profile-page-shell" : ""} ${isContacts ? "contacts-page-shell" : ""} ${isAgents ? "agents-page-shell" : ""} ${isEmployees ? "employees-page-shell" : ""} ${isAudit ? "audit-page-shell" : ""} ${isKnowledge ? "knowledge-page-shell" : ""} ${isKnowledgeEditor ? "knowledge-editor-shell" : ""} ${isPortals ? "portals-page-shell" : ""}`}>
<ShellRouteContent settingsSection={settingsSection} openSettings={openSettingsRoute} chatScope={chatScope.scope} setChatScope={chatScope.setScope} chatCounters={chatScope.counters} chatScopeSwitcher={manager} route={route} data={data} selectedEmployeeId={selectedEmployeeId} selectedAgentId={selectedAgentId} selectedKnowledgeId={selectedKnowledgeId} selectedConversationId={selectedConversationId} selectedClientId={selectedClientId} openClient={openClientRoute} selectedChannelId={selectedChannelId} openChannel={openChannelRoute} selectedSupportPortalId={selectedSupportPortalId} portalSettingsSection={portalSettingsSection} openSupportPortal={openSupportPortalRoute} openPortalSettings={openPortalSettingsRoute} openConversation={openConversationRoute} openEmployee={openEmployee} openAgent={openAgentRoute} openKnowledge={openKnowledgeRoute} openKnowledgeEditor={openKnowledgeEditorRoute} onAgentLoaded={() => undefined} onChannelLoaded={() => undefined} reload={reload} setRoute={setRoute} user={user} onUserUpdated={onUserUpdated} onLogout={onLogout} onOpenSidebar={() => setSidebarExpanded(true)} onOrganizationCreated={onOrganizationCreated} />
<ShellRouteContent settingsSection={settingsSection} openSettings={openSettingsRoute} chatScope={chatScope.scope} setChatScope={chatScope.setScope} chatCounters={chatScope.counters} chatScopeSwitcher={manager} route={route} data={data} selectedEmployeeId={selectedEmployeeId} selectedAgentId={selectedAgentId} selectedKnowledgeId={selectedKnowledgeId} selectedConversationId={selectedConversationId} selectedClientId={selectedClientId} openClient={openClientRoute} selectedChannelId={selectedChannelId} openChannel={openChannelRoute} selectedIntegrationId={selectedIntegrationId} openWebIntegration={openWebIntegrationRoute} selectedSupportPortalId={selectedSupportPortalId} portalSettingsSection={portalSettingsSection} openSupportPortal={openSupportPortalRoute} openPortalSettings={openPortalSettingsRoute} openConversation={openConversationRoute} openEmployee={openEmployee} openAgent={openAgentRoute} openKnowledge={openKnowledgeRoute} openKnowledgeEditor={openKnowledgeEditorRoute} onAgentLoaded={() => undefined} onChannelLoaded={() => undefined} reload={reload} setRoute={setRoute} user={user} onUserUpdated={onUserUpdated} onLogout={onLogout} onOpenSidebar={() => setSidebarExpanded(true)} onOrganizationCreated={onOrganizationCreated} />
</div>
</main>
</div>
@@ -5,6 +5,7 @@ import { AgentDetailPage } from "../features/agents/AgentDetailPage";
import { EmployeeDetailPage, EmployeesPage } from "../features/employees/EmployeesPage";
import { ProfilePage } from "../features/profile/ProfilePage";
import { SettingsPage } from "../features/settings/SettingsPage";
import { WebIntegrationPage } from "../features/integrations/WebIntegrationPage";
import { AuditPage } from "../features/administration/AuditPage";
import { OrganizationCreatePage } from "../features/organizations/OrganizationCreatePage";
import { ChatPage } from "../features/chat/ChatPage";
@@ -43,14 +44,15 @@ const SupportPortalDetailPage = lazy(() => import("../features/support-portals/S
(module) => ({ default: module.SupportPortalDetailPage }),
));
export function ShellRouteContent({ settingsSection, openSettings, chatScope, setChatScope, chatCounters, chatScopeSwitcher, route, data, selectedEmployeeId, selectedAgentId, selectedKnowledgeId, selectedConversationId, selectedClientId, openClient, selectedChannelId, openChannel, selectedSupportPortalId, portalSettingsSection, openSupportPortal, openPortalSettings, openConversation, openEmployee, openAgent, openKnowledge, openKnowledgeEditor, onAgentLoaded, onChannelLoaded, reload, setRoute, user, onUserUpdated, onLogout, onOpenSidebar, onOrganizationCreated }: { settingsSection: SettingsSectionKey | null; openSettings: (section: SettingsSectionKey | null) => void; chatScope: DialogScope; setChatScope: (scope: DialogScope) => void; chatCounters: ConversationCounters | null; chatScopeSwitcher: boolean; route: RouteKey; data: AppData; selectedEmployeeId: number | 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; portalSettingsSection: PortalSettingsSectionKey | null; openSupportPortal: (portalId: number) => void; openPortalSettings: (portalId: number, section?: PortalSettingsSectionKey) => void; openConversation: (conversationId: number) => void; openEmployee: (employee: Employee) => void; openAgent: (agentId: number) => void; openKnowledge: (knowledgeId: number) => void; openKnowledgeEditor: (knowledgeId: number | null) => 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; onOpenSidebar: () => void; onOrganizationCreated: (identity: AuthenticatedUser, organizationPublicId: string) => void }) {
export function ShellRouteContent({ settingsSection, openSettings, chatScope, setChatScope, chatCounters, chatScopeSwitcher, route, data, selectedEmployeeId, selectedAgentId, selectedKnowledgeId, selectedConversationId, selectedClientId, openClient, selectedChannelId, openChannel, selectedIntegrationId, openWebIntegration, selectedSupportPortalId, portalSettingsSection, openSupportPortal, openPortalSettings, openConversation, openEmployee, openAgent, openKnowledge, openKnowledgeEditor, onAgentLoaded, onChannelLoaded, reload, setRoute, user, onUserUpdated, onLogout, onOpenSidebar, onOrganizationCreated }: { settingsSection: SettingsSectionKey | null; openSettings: (section: SettingsSectionKey | null) => void; chatScope: DialogScope; setChatScope: (scope: DialogScope) => void; chatCounters: ConversationCounters | null; chatScopeSwitcher: boolean; route: RouteKey; data: AppData; selectedEmployeeId: number | null; selectedAgentId: number | null; selectedKnowledgeId: number | null; selectedConversationId: number | null; selectedClientId: number | null; openClient: (clientId: number) => void; selectedChannelId: number | null; openChannel: (channelId: number) => void; selectedIntegrationId: number | null; openWebIntegration: (id: number) => void; selectedSupportPortalId: number | null; portalSettingsSection: PortalSettingsSectionKey | null; openSupportPortal: (portalId: number) => void; openPortalSettings: (portalId: number, section?: PortalSettingsSectionKey) => void; openConversation: (conversationId: number) => void; openEmployee: (employee: Employee) => void; openAgent: (agentId: number) => void; openKnowledge: (knowledgeId: number) => void; openKnowledgeEditor: (knowledgeId: number | null) => 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; onOpenSidebar: () => void; onOrganizationCreated: (identity: AuthenticatedUser, organizationPublicId: string) => void }) {
return (
<>
{route === "employees" && <EmployeesPage groups={data.groups} openEmployee={openEmployee} setRoute={setRoute} user={user} />}
{route === "employeeDetail" && selectedEmployeeId != null && <EmployeeDetailPage groups={data.groups} employeeId={selectedEmployeeId} setRoute={setRoute} />}
{route === "employeeDetail" && selectedEmployeeId == null && <EmployeesPage groups={data.groups} openEmployee={openEmployee} setRoute={setRoute} user={user} />}
{route === "profile" && <ProfilePage user={user} onUserUpdated={onUserUpdated} reload={reload} onLogout={onLogout} onBack={() => setRoute("chat")} />}
{route === "settings" && <SettingsPage user={user} onUserUpdated={onUserUpdated} reload={reload} groups={data.groups} section={settingsSection} openSection={openSettings} setRoute={setRoute} />}
{route === "settings" && <SettingsPage user={user} onUserUpdated={onUserUpdated} reload={reload} groups={data.groups} section={settingsSection} openSection={openSettings} openWebIntegration={openWebIntegration} setRoute={setRoute} />}
{route === "webIntegrationSettings" && selectedIntegrationId !== null && <WebIntegrationPage integrationId={selectedIntegrationId} onOpenSettings={() => openSettings(null)} onBack={() => openSettings("integrations")} />}
{route === "administrationAudit" && <AuditPage />}
{route === "organizationCreate" && <OrganizationCreatePage user={user} onCreated={onOrganizationCreated} onBack={() => setRoute("chat")} />}
{route === "salesClients" && <SalesClientsPage openClient={openClient} openIntegrations={() => openSettings("integrations")} />}
+1 -1
View File
@@ -174,7 +174,7 @@ export function Sidebar({
<SidebarLink activeRoutes={["employees", "employeeDetail"]} icon="team" label={t("common.operators")} route={route} routeKey="employees" setRoute={setRoute} target="nav-employees" />
<SidebarLink activeRoutes={["supportPortals", "supportPortalDetail", "supportPortalSettings"]} icon="globe" label={t("common.portals")} route={route} routeKey="supportPortals" setRoute={setRoute} />
<SidebarLink activeRoutes={["knowledge", "knowledgeDetail", "knowledgeCreate", "knowledgeEdit", "knowledgeCategories", "knowledgeImport"]} icon="book" label={t("common.knowledge_base")} route={route} routeKey="knowledge" setRoute={setRoute} target="nav-knowledge" />
<SidebarLink activeRoutes={["settings", "administrationAudit"]} icon="settings" label={t("common.settings")} route={route} routeKey="settings" setRoute={setRoute} />
<SidebarLink activeRoutes={["settings", "webIntegrationSettings", "administrationAudit"]} icon="settings" label={t("common.settings")} route={route} routeKey="settings" setRoute={setRoute} />
</nav>
) : route === "profile" ? (
/* Кадр P2: на «Профиле» у сотрудника сайдбар без навигации — фильтровать
+8
View File
@@ -9,6 +9,7 @@ const empty = {
knowledgeId: null,
clientId: null,
channelId: null,
integrationId: null,
supportPortalId: null,
portalSettingsSection: null,
settingsSection: null,
@@ -204,6 +205,13 @@ describe("channel routes", () => {
});
describe("settings routes", () => {
it("restores a web connection page from its direct URL", () => {
expect(routeFromPath("/settings/integrations/12")).toEqual({ ...empty, route: "webIntegrationSettings", integrationId: 12 });
expect(routeFromPath("/settings/integrations/12/")).toEqual({ ...empty, route: "webIntegrationSettings", integrationId: 12 });
expect(routeFromPath("/settings/integrations/nope")).toEqual({ ...empty, route: "settings", settingsSection: "integrations" });
expect(pathFromRoute("webIntegrationSettings", 12)).toBe("/settings/integrations/12");
});
it("parses the settings screen and its submenu sections", () => {
expect(routeFromPath("/settings")).toEqual({ route: "settings", ...empty });
expect(routeFromPath("/settings/groups")).toEqual({ ...empty, route: "settings", settingsSection: "groups" });
+10 -2
View File
@@ -14,6 +14,7 @@ export type RouteState = {
knowledgeId: number | null;
clientId: number | null;
channelId: number | null;
integrationId: number | null;
supportPortalId: number | null;
portalSettingsSection: PortalSettingsSectionKey | null;
settingsSection: SettingsSectionKey | null;
@@ -24,12 +25,12 @@ export function routeFromPath(pathname: string, search = ""): RouteState {
// Страница создания организации живёт вне организации: у неё ещё нет
// адреса, а человек попадает сюда из переключателя любой из своих (A1).
if (normalized === "/organizations/new") {
return { route: "organizationCreate", organizationPublicId: null, employeeId: null, agentId: null, knowledgeId: null, clientId: null, channelId: null, supportPortalId: null, portalSettingsSection: null, settingsSection: null };
return { route: "organizationCreate", organizationPublicId: null, employeeId: null, agentId: null, knowledgeId: null, clientId: null, channelId: null, integrationId: null, supportPortalId: null, portalSettingsSection: null, settingsSection: null };
}
const match = normalized.match(/^\/organizations\/([0-9a-f-]{36})(\/.*)?$/i);
const organizationPublicId = match?.[1] ?? null;
const path = match ? match[2] || "/" : normalized;
const base = { employeeId: null, agentId: null, knowledgeId: null, clientId: null, channelId: null, supportPortalId: null, portalSettingsSection: null, settingsSection: null };
const base = { employeeId: null, agentId: null, knowledgeId: null, clientId: null, channelId: null, integrationId: null, supportPortalId: null, portalSettingsSection: null, settingsSection: null };
const state = { organizationPublicId, ...base };
// Chat-first (SPEC-CHATBALLS-0031): корень и устаревшие адреса командного центра и
// разделённых чатов ведут в единый «Чат».
@@ -111,6 +112,12 @@ export function routeFromPath(pathname: string, search = ""): RouteState {
}
if (path === "/profile") return { route: "profile", ...state };
if (path === "/settings") return { route: "settings", ...state };
if (path.startsWith("/settings/integrations/")) {
const id = Number(path.slice("/settings/integrations/".length));
return Number.isInteger(id) && id > 0
? { ...state, route: "webIntegrationSettings", integrationId: id }
: { ...state, route: "settings", settingsSection: "integrations" };
}
if (path.startsWith("/settings/")) {
return { ...state, route: "settings", settingsSection: settingsSectionKey(path.slice("/settings/".length)) };
}
@@ -144,6 +151,7 @@ export function pathFromRoute(route: RouteKey, entityId: number | string | null
if (route === "knowledgeEdit") return entityId ? `${prefix}/knowledge/${entityId}/edit` : `${prefix}/knowledge/new`;
if (route === "administrationAudit") return `${prefix}/administration/audit`;
if (route === "profile") return `${prefix}/profile`;
if (route === "webIntegrationSettings") return entityId ? `${prefix}/settings/integrations/${entityId}` : `${prefix}/settings/integrations`;
// У «Настроек» вместо id — ключ раздела субменю (кадры N1–N7).
if (route === "settings") return settingsSectionKey(String(entityId)) ? `${prefix}/settings/${entityId}` : `${prefix}/settings`;
return `${prefix}/profile`;
+1
View File
@@ -7,6 +7,7 @@ export const routes: Record<RouteKey, string> = {
employees: t("common.operators"),
profile: t("common.profile"),
settings: t("common.settings"),
webIntegrationSettings: t("common.integrations"),
salesClientDetail: t("common.contact"),
salesClients: t("common.contacts"),
chat: t("common.chat"),
@@ -6,6 +6,18 @@ import { PORTAL_SETTINGS_SECTIONS } from "../features/support-portals/sections";
import { SectionMenu } from "./SectionMenu";
describe("SectionMenu", () => {
it("keeps planned sections visible without opening them", () => {
const markup = renderToStaticMarkup(
<SectionMenu
items={[{ key: "basics", label: t("portals.basics"), icon: "settings" }, { key: "fields", label: t("settings.site_data"), icon: "code", disabled: true }]}
activeKey="basics"
onSelect={() => undefined}
/>,
);
expect(markup).toContain(t("settings.site_data"));
expect(markup).toContain('disabled=""');
});
it("renders the portal sections, hint, divider, and dangerous item", () => {
const items = PORTAL_SETTINGS_SECTIONS.map((item) => ({
...item,
@@ -7,6 +7,7 @@ export type SectionMenuItem<Key extends string> = {
hint?: { text: string; tone?: "ok" | "muted" };
divider?: boolean;
danger?: boolean;
disabled?: boolean;
};
export function SectionMenu<Key extends string>({ items, activeKey, note, onSelect }: {
@@ -22,6 +23,7 @@ export function SectionMenu<Key extends string>({ items, activeKey, note, onSele
<button
className={`section-menu-item${item.key === activeKey ? " is-active" : ""}${item.danger ? " is-danger" : ""}`}
type="button"
disabled={item.disabled}
onClick={() => onSelect(item.key)}
>
<Icon name={item.icon} size={16} strokeWidth={1.9} />
@@ -32,6 +32,8 @@
}
.section-menu-item:hover { background: var(--n-9); }
.section-menu-item:disabled { cursor: default; }
.section-menu-item:disabled:hover { background: transparent; }
.section-menu-item.is-active {
background: var(--primary-bg);
+1
View File
@@ -24,6 +24,7 @@
@import "./features/ai/knowledge/styles-editor.css";
@import "./features/ai/knowledge/styles-pages.css";
@import "./features/integrations/styles.css";
@import "./features/integrations/web-integration.css";
@import "./features/administration/styles.css";
@import "./features/organizations/styles.css";
@import "./features/settings/styles.css";
+1 -1
View File
@@ -124,7 +124,7 @@ export type EmployeeAuditEvent = {
createdAt: string;
};
export type RouteKey = "administrationAudit" | "employeeDetail" | "employees" | "profile" | "settings" | "salesClientDetail" | "salesClients" | "chat" | "supportPortals" | "supportPortalDetail" | "supportPortalSettings" | "agents" | "agentDetail" | "knowledge" | "knowledgeDetail" | "knowledgeCreate" | "knowledgeEdit" | "knowledgeCategories" | "knowledgeImport" | "aiUsage" | "organizationCreate";
export type RouteKey = "administrationAudit" | "employeeDetail" | "employees" | "profile" | "settings" | "webIntegrationSettings" | "salesClientDetail" | "salesClients" | "chat" | "supportPortals" | "supportPortalDetail" | "supportPortalSettings" | "agents" | "agentDetail" | "knowledge" | "knowledgeDetail" | "knowledgeCreate" | "knowledgeEdit" | "knowledgeCategories" | "knowledgeImport" | "aiUsage" | "organizationCreate";
export type AppData = {
groups: EmployeeGroup[];
@@ -19,6 +19,7 @@ export function useRouteNavigation(
const [selectedConversationId, setSelectedConversationId] = useState<number | null>(null);
const [selectedClientId, setSelectedClientId] = useState(initialRoute.clientId);
const [selectedChannelId, setSelectedChannelId] = useState(initialRoute.channelId);
const [selectedIntegrationId, setSelectedIntegrationId] = useState(initialRoute.integrationId);
const [selectedSupportPortalId, setSelectedSupportPortalId] = useState(initialRoute.supportPortalId);
const [selectedPortalSection, setSelectedPortalSection] = useState(initialRoute.portalSettingsSection);
const [selectedSettingsSection, setSelectedSettingsSection] = useState(initialRoute.settingsSection);
@@ -31,6 +32,7 @@ export function useRouteNavigation(
setSelectedConversationId(null);
setSelectedClientId(next.clientId);
setSelectedChannelId(next.channelId);
setSelectedIntegrationId(next.integrationId);
setSelectedSupportPortalId(next.supportPortalId);
setSelectedPortalSection(next.portalSettingsSection);
setSelectedSettingsSection(next.settingsSection);
@@ -51,6 +53,7 @@ export function useRouteNavigation(
knowledgeId: (nextRoute === "knowledgeDetail" || nextRoute === "knowledgeEdit") && typeof entityId === "number" ? entityId : null,
clientId: nextRoute === "salesClientDetail" && typeof entityId === "number" ? entityId : null,
channelId: null,
integrationId: nextRoute === "webIntegrationSettings" && typeof entityId === "number" ? entityId : null,
supportPortalId: nextRoute === "supportPortalDetail" && typeof entityId === "number"
? entityId
// У настроек портала entityId — «id/раздел»: id портала и ключ раздела.
@@ -78,6 +81,7 @@ export function useRouteNavigation(
selectedConversationId,
selectedClientId,
selectedChannelId,
selectedIntegrationId,
selectedSupportPortalId,
selectedPortalSection,
selectedSettingsSection,