+
Группы
+
Видимость диалогов и охват чата. Состав группы — на карточке сотрудника.
+
+ {groups.length === 0 &&
Групп пока нет — все видят все диалоги.
}
+ {groups.map((group) => (
+
+
+ {group.name}
+ {group.memberCount} чел.
+
+
+
+ ))}
+
+
+ setName(event.target.value)}
+ onKeyDown={(event) => { if (event.key === "Enter") void create(); }}
+ />
+
+
+ {errorText &&
{errorText}
}
+ {renaming && (
+
setRenaming(null)} footer={null} destroyOnClose>
+
+
setRenameValue(event.target.value)} />
+ {modalError &&
{modalError}
}
+
+
+
+
+
+
+ )}
+ {deleting && (
+
setDeleting(null)} footer={null} destroyOnClose>
+
+
{`«${deleting.name}» будет удалена; её диалоги станут «Без группы».`}
+ {modalError &&
{modalError}
}
+
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/internal-ui/src/features/settings/SettingsPage.tsx b/apps/internal-ui/src/features/settings/SettingsPage.tsx
index 164d517..a10a454 100644
--- a/apps/internal-ui/src/features/settings/SettingsPage.tsx
+++ b/apps/internal-ui/src/features/settings/SettingsPage.tsx
@@ -1,21 +1,90 @@
-import type { SessionUser } from "../../types";
+import type { ReactNode } from "react";
+
+import { isManager } from "../../auth/access";
+import type { EmployeeGroup, SessionUser } from "../../types";
+import { OrganizationSettingsForm } from "../administration/OrganizationSettingsForm";
+import { canManageSettings } from "../administration/model";
+import { useAdministration } from "../administration/useAdministration";
+import { IntegrationsSection } from "../integrations/IntegrationsSection";
import { ProfileAppearanceCard } from "../profile/ProfileAppearanceCard";
import { ProfileNotificationsCard } from "../profile/ProfileNotificationsCard";
import { ProfilePasswordForm } from "../profile/ProfilePasswordForm";
import { ProfileSessionsCard } from "../profile/ProfileSessionsCard";
import { ProfileTotpCard } from "../profile/ProfileTotpCard";
import { useProfilePage } from "../profile/useProfilePage";
+import { EmptyState, LoadingState } from "../../shared/ui";
+import { GroupsSettingsCard } from "./GroupsSettingsCard";
-export function SettingsPage({ user, onUserUpdated, reload }: { user: SessionUser; onUserUpdated: (user: SessionUser) => void; reload: () => void }) {
+// «Настройки» (SPEC-HUB-0031 §8.6): один экран с вертикальными секциями —
+// Организация · Группы · AI-провайдер · Интеграции · Профиль. Отдельные
+// разделы «Организация» и «Интеграции» упразднены. Сотруднику доступна
+// только профильная часть (тема, акцент, безопасность).
+
+export function SettingsPage({ user, onUserUpdated, reload, groups = [] }: { user: SessionUser; onUserUpdated: (user: SessionUser) => void; reload: () => void; groups?: EmployeeGroup[] }) {
const profilePage = useProfilePage({ user, onUserUpdated, reload });
+ const manager = isManager(user);
- return (
-
+ const profileSections = (
+ <>
+ >
+ );
+
+ if (!manager) {
+ return
{profileSections}
;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {profileSections}
);
}
+
+function SettingsSection({ title, note, children }: { title: string; note?: string; children: ReactNode }) {
+ return (
+
+
+ {title}
+ {note && {note}}
+
+ {children}
+
+ );
+}
+
+function OrganizationSection({ user, onUserUpdated }: { user: SessionUser; onUserUpdated: (user: SessionUser) => void }) {
+ const page = useAdministration({ section: "organization", user, onUserUpdated });
+ if (page.loading) return
;
+ if (!page.organization) return
;
+ return (
+
void page.save()}
+ onUploadLogo={(file) => void page.uploadLogo(file)}
+ onRemoveLogo={() => void page.removeLogo()}
+ />
+ );
+}
diff --git a/apps/internal-ui/src/features/settings/styles.css b/apps/internal-ui/src/features/settings/styles.css
new file mode 100644
index 0000000..dd7cde7
--- /dev/null
+++ b/apps/internal-ui/src/features/settings/styles.css
@@ -0,0 +1,110 @@
+/* Экран «Настройки» (SPEC-HUB-0031 §8.6): вертикальные секции. */
+.settings-section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.settings-section-head {
+ display: flex;
+ align-items: baseline;
+ gap: 10px;
+ margin-top: 8px;
+}
+
+.settings-section-head h2 {
+ margin: 0;
+ color: var(--n-1);
+ font-size: 16px;
+ font-weight: 700;
+}
+
+.settings-section-head span {
+ color: var(--n-4);
+ font-size: 12.5px;
+}
+
+.settings-section-note {
+ margin: 0;
+ color: var(--n-4);
+ font-size: 12.5px;
+}
+
+.settings-section-error {
+ color: var(--error);
+ font-size: 12.5px;
+}
+
+.settings-groups-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-group-row {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 8px 2px;
+ border-bottom: 1px solid var(--n-9);
+}
+
+.settings-group-row strong {
+ color: var(--n-1);
+ font-size: 13.5px;
+ font-weight: 600;
+}
+
+.settings-group-row small {
+ flex: 1;
+ color: var(--n-4);
+ font-size: 12px;
+}
+
+.settings-group-row button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ border: 0;
+ border-radius: 7px;
+ color: var(--n-4);
+ background: transparent;
+}
+
+.settings-group-row button:hover {
+ color: var(--n-2);
+ background: var(--n-9);
+}
+
+.settings-group-create {
+ display: flex;
+ gap: 8px;
+}
+
+.settings-group-create input,
+.settings-modal-input {
+ flex: 1;
+ height: 34px;
+ padding: 0 11px;
+ border: 1px solid var(--n-7);
+ border-radius: 8px;
+ color: var(--n-1);
+ background: var(--surface-card);
+ font-size: 13px;
+}
+
+.settings-modal-input {
+ width: 100%;
+}
+
+.integrations-section {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.integrations-section-actions {
+ display: flex;
+ justify-content: flex-end;
+}
diff --git a/apps/internal-ui/src/layout/LaunchChecklist.tsx b/apps/internal-ui/src/layout/LaunchChecklist.tsx
index b1f5b00..2ef63e5 100644
--- a/apps/internal-ui/src/layout/LaunchChecklist.tsx
+++ b/apps/internal-ui/src/layout/LaunchChecklist.tsx
@@ -26,7 +26,7 @@ type Step = {
const STEPS: Step[] = [
{ key: "agentCreated", label: "Создать агента", action: "Создать агента", route: "agents", icon: "robot" },
- { key: "connectionBound", label: "Подключить точку входа", action: "Подключить", route: "integrations", icon: "plug" },
+ { key: "connectionBound", label: "Подключить точку входа", action: "Подключить", route: "settings", icon: "plug" },
{ key: "employeeInvited", label: "Пригласить сотрудников", action: "Пригласить", route: "employees", icon: "team" },
];
diff --git a/apps/internal-ui/src/layout/ShellRouteContent.tsx b/apps/internal-ui/src/layout/ShellRouteContent.tsx
index c20d769..7dd3ca3 100644
--- a/apps/internal-ui/src/layout/ShellRouteContent.tsx
+++ b/apps/internal-ui/src/layout/ShellRouteContent.tsx
@@ -6,7 +6,6 @@ import { KnowledgeDetailPage } from "../features/ai/knowledge/KnowledgeDetailPag
import { KnowledgeCreatePage } from "../features/ai/knowledge/KnowledgeCreatePage";
import { KnowledgePage } from "../features/ai/knowledge/KnowledgePage";
import { CommandCenter } from "../features/command/CommandCenter";
-import { IntegrationsPage } from "../features/integrations/IntegrationsPage";
import { EmployeeDetailPage, EmployeesPage } from "../features/employees/EmployeesPage";
import { ProfilePage } from "../features/profile/ProfilePage";
import { SettingsPage } from "../features/settings/SettingsPage";
@@ -36,9 +35,8 @@ export function ShellRouteContent({ chatScope, setChatScope, chatCounters, chatS
{route === "employeeDetail" && currentEmployee && }
{route === "employeeDetail" && !currentEmployee && }
{route === "profile" && }
- {route === "settings" && }
- {(route === "administrationOrganization"
- || route === "administrationAudit") && (
+ {route === "settings" && }
+ {route === "administrationAudit" && (
setRoute("agents")}
openKnowledge={openKnowledge}
- openIntegrations={() => setRoute("integrations")}
+ openIntegrations={() => setRoute("settings")}
setRoute={setRoute}
onLoaded={onAgentLoaded}
/>
@@ -79,7 +77,6 @@ export function ShellRouteContent({ chatScope, setChatScope, chatCounters, chatS
{route === "aiKnowledge" && }
{route === "aiKnowledgeCreate" && }
{route === "aiKnowledgeDetail" && }
- {route === "integrations" && }
{route === "supportPortals" && }>}
{route === "supportPortalDetail" && }> setRoute("supportPortals")} />}
>
diff --git a/apps/internal-ui/src/layout/Sidebar.tsx b/apps/internal-ui/src/layout/Sidebar.tsx
index 501ae1b..fa6698b 100644
--- a/apps/internal-ui/src/layout/Sidebar.tsx
+++ b/apps/internal-ui/src/layout/Sidebar.tsx
@@ -44,8 +44,6 @@ const SERVICE_ITEMS: SidebarNavSectionItem[] = [
{ activeRoutes: ["supportOverview"], key: "supportOverview", label: "Обзор поддержки" },
{ activeRoutes: ["aiKnowledge", "aiKnowledgeCreate", "aiKnowledgeDetail"], key: "aiKnowledge", label: "Знания" },
{ activeRoutes: ["aiUsage"], key: "aiUsage", label: "Использование AI" },
- { activeRoutes: ["integrations"], key: "integrations", label: "Интеграции" },
- { activeRoutes: ["administrationOrganization"], key: "administrationOrganization", label: "Организация" },
{ activeRoutes: ["administrationAudit"], key: "administrationAudit", label: "Аудит" },
];
diff --git a/apps/internal-ui/src/router.test.ts b/apps/internal-ui/src/router.test.ts
index 55a1cf4..3ffcf8e 100644
--- a/apps/internal-ui/src/router.test.ts
+++ b/apps/internal-ui/src/router.test.ts
@@ -76,28 +76,31 @@ describe("chat route", () => {
});
describe("administration route", () => {
- it("keeps the old URL as an organization alias", () => {
+ it("sends legacy administration URLs to settings (§8.6)", () => {
expect(routeFromPath("/administration")).toEqual({
- route: "administrationOrganization",
+ route: "settings",
+ ...empty,
+ });
+ expect(routeFromPath("/integrations")).toEqual({
+ route: "settings",
...empty,
});
});
it("parses and creates administration subsection URLs", () => {
+ // Организация переехала в «Настройки» (§8.6); тарифы — устаревший адрес (ADR-HUB-0042).
expect(routeFromPath("/administration/organization")).toEqual({
- route: "administrationOrganization",
+ route: "settings",
...empty,
});
- // Устаревший адрес тарифов ведёт в организацию (ADR-HUB-0042).
expect(routeFromPath("/administration/subscription")).toEqual({
- route: "administrationOrganization",
+ route: "settings",
...empty,
});
expect(routeFromPath("/administration/audit")).toEqual({
route: "administrationAudit",
...empty,
});
- expect(pathFromRoute("administrationOrganization")).toBe("/administration/organization");
expect(pathFromRoute("administrationAudit")).toBe("/administration/audit");
});
});
diff --git a/apps/internal-ui/src/router.ts b/apps/internal-ui/src/router.ts
index 097992f..41fd068 100644
--- a/apps/internal-ui/src/router.ts
+++ b/apps/internal-ui/src/router.ts
@@ -60,10 +60,11 @@ export function routeFromPath(pathname: string, search = ""): RouteState {
return Number.isInteger(id) && id > 0 ? { ...state, route: "aiKnowledgeDetail", knowledgeId: id } : { route: "aiKnowledge", ...state };
}
if (path === "/ai/usage") return { route: "aiUsage", ...state };
- if (path === "/integrations") return { route: "integrations", ...state };
- // /administration/subscription — устаревший адрес тарифов (ADR-HUB-0042), ведёт в организацию.
+ // Устаревшие адреса: интеграции и организация переехали в «Настройки» (§8.6).
+ if (path === "/integrations") return { route: "settings", ...state };
+ // /administration/subscription — устаревший адрес тарифов (ADR-HUB-0042).
if (path === "/administration" || path === "/administration/organization" || path === "/administration/subscription") {
- return { route: "administrationOrganization", ...state };
+ return { route: "settings", ...state };
}
if (path === "/administration/audit") {
return { route: "administrationAudit", ...state };
@@ -90,8 +91,6 @@ export function pathFromRoute(route: RouteKey, entityId: number | null = null, p
if (route === "aiKnowledge") return `${prefix}/ai/knowledge`;
if (route === "aiKnowledgeCreate") return `${prefix}/ai/knowledge/new`;
if (route === "aiKnowledgeDetail") return entityId ? `${prefix}/ai/knowledge/${entityId}` : `${prefix}/ai/knowledge`;
- if (route === "integrations") return `${prefix}/integrations`;
- if (route === "administrationOrganization") return `${prefix}/administration/organization`;
if (route === "administrationAudit") return `${prefix}/administration/audit`;
if (route === "profile") return `${prefix}/profile`;
if (route === "settings") return `${prefix}/settings`;
diff --git a/apps/internal-ui/src/routes.ts b/apps/internal-ui/src/routes.ts
index 3e98578..07199fd 100644
--- a/apps/internal-ui/src/routes.ts
+++ b/apps/internal-ui/src/routes.ts
@@ -1,7 +1,6 @@
import type { RouteKey } from "./types";
export const routes: Record = {
- administrationOrganization: "Организация",
administrationAudit: "Аудит",
command: "Обзор",
employeeDetail: "Сотрудники",
@@ -20,5 +19,4 @@ export const routes: Record = {
aiKnowledgeCreate: "Создание знания",
aiKnowledgeDetail: "Знание",
aiUsage: "Использование AI",
- integrations: "Интеграции",
};
diff --git a/apps/internal-ui/src/styles.css b/apps/internal-ui/src/styles.css
index 16bd9e4..5858acd 100644
--- a/apps/internal-ui/src/styles.css
+++ b/apps/internal-ui/src/styles.css
@@ -24,5 +24,6 @@
@import "./features/ai/knowledge/styles-category.css";
@import "./features/integrations/styles.css";
@import "./features/administration/styles.css";
+@import "./features/settings/styles.css";
@import "./features/notifications/styles.css";
@import "./shared/state.css";
diff --git a/apps/internal-ui/src/types.ts b/apps/internal-ui/src/types.ts
index 3f84134..cecbff9 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" | "chat" | "supportOverview" | "supportPortals" | "supportPortalDetail" | "agents" | "agentDetail" | "aiKnowledge" | "aiKnowledgeCreate" | "aiKnowledgeDetail" | "aiUsage" | "integrations";
+export type RouteKey = "administrationAudit" | "command" | "employeeDetail" | "employees" | "profile" | "settings" | "salesClientDetail" | "salesClients" | "chat" | "supportOverview" | "supportPortals" | "supportPortalDetail" | "agents" | "agentDetail" | "aiKnowledge" | "aiKnowledgeCreate" | "aiKnowledgeDetail" | "aiUsage";
export type AppData = {
employees: Employee[];