314 lines
21 KiB
TypeScript
314 lines
21 KiB
TypeScript
import { useState } from 'react';
|
||
import { RefreshCw, Trash2, Globe, UserCog, Bell, Volume2, PanelTop } from 'lucide-react';
|
||
import { useCommunicationsStore } from '../services/communicationsService';
|
||
import { Button, Badge } from '@omnichannel/ui';
|
||
import { useToast } from '../../../shared/ui';
|
||
|
||
interface SettingsViewProps {
|
||
showEmailTab: boolean;
|
||
onShowEmailTabChange: (enabled: boolean) => void;
|
||
showChannelsTab: boolean;
|
||
onShowChannelsTabChange: (enabled: boolean) => void;
|
||
orgEmailAllowed: boolean;
|
||
onOrgEmailAllowedChange: (enabled: boolean) => void;
|
||
orgChannelsAllowed: boolean;
|
||
onOrgChannelsAllowedChange: (enabled: boolean) => void;
|
||
canConnectAccounts: boolean;
|
||
onCanConnectAccountsChange: (enabled: boolean) => void;
|
||
}
|
||
|
||
export function SettingsView({ showEmailTab, onShowEmailTabChange, showChannelsTab, onShowChannelsTabChange, orgEmailAllowed, onOrgEmailAllowedChange, orgChannelsAllowed, onOrgChannelsAllowedChange, canConnectAccounts, onCanConnectAccountsChange }: SettingsViewProps) {
|
||
const { showToast } = useToast();
|
||
const {
|
||
integrations,
|
||
testIntegration,
|
||
toggleIntegration,
|
||
quickReplies,
|
||
addQuickReplyTemplate,
|
||
deleteQuickReplyTemplate,
|
||
auditLogs,
|
||
} = useCommunicationsStore();
|
||
|
||
const [newTemplateText, setNewTemplateText] = useState('');
|
||
const [testingId, setTestingId] = useState<string | null>(null);
|
||
const [settingsScope, setSettingsScope] = useState<'personal' | 'organization'>('personal');
|
||
const [notificationsEnabled, setNotificationsEnabled] = useState(() => localStorage.getItem('krona-user-notifications') !== 'false');
|
||
const [soundsEnabled, setSoundsEnabled] = useState(() => localStorage.getItem('krona-user-sounds') !== 'false');
|
||
const [compactMode, setCompactMode] = useState(() => localStorage.getItem('krona-user-compact') !== 'false');
|
||
|
||
const switchClass = (enabled: boolean) => `relative w-12 h-7 rounded-full transition-colors shrink-0 ${enabled ? 'bg-[var(--krona-primary)]' : 'bg-[var(--krona-surface-muted)]'}`;
|
||
const switchKnobClass = (enabled: boolean) => `absolute left-1 top-1 w-5 h-5 rounded-full bg-white shadow-sm transition-transform ${enabled ? 'translate-x-5' : 'translate-x-0'}`;
|
||
|
||
const handleTest = async (id: string) => {
|
||
setTestingId(id);
|
||
const result = await testIntegration(id);
|
||
setTestingId(null);
|
||
showToast({
|
||
title: 'Диагностика коннектора',
|
||
message: `Статус: ОК • Задержка: ${result.latencyMs} мс`,
|
||
type: 'success',
|
||
});
|
||
};
|
||
|
||
const handleAddTemplate = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!newTemplateText.trim()) return;
|
||
addQuickReplyTemplate(newTemplateText.trim());
|
||
setNewTemplateText('');
|
||
showToast({
|
||
title: 'Шаблон добавлен',
|
||
message: 'Быстрый ответ сохранен',
|
||
type: 'success',
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="flex-1 min-h-0 flex flex-col p-4 sm:p-6 bg-[var(--krona-bg-workspace)] overflow-y-auto space-y-6">
|
||
<div className="flex p-1 rounded-xl bg-[var(--krona-surface-muted)] self-start" role="tablist" aria-label="Тип настроек">
|
||
<button type="button" role="tab" aria-selected={settingsScope === 'personal'} onClick={() => setSettingsScope('personal')} className={`px-4 py-2 rounded-lg text-sm font-semibold transition-colors ${settingsScope === 'personal' ? 'bg-[var(--krona-surface)] text-[var(--krona-primary)] shadow-xs' : 'text-[var(--krona-text-secondary)]'}`}>Мои настройки</button>
|
||
<button type="button" role="tab" aria-selected={settingsScope === 'organization'} onClick={() => setSettingsScope('organization')} className={`px-4 py-2 rounded-lg text-sm font-semibold transition-colors ${settingsScope === 'organization' ? 'bg-[var(--krona-surface)] text-[var(--krona-primary)] shadow-xs' : 'text-[var(--krona-text-secondary)]'}`}>Организация</button>
|
||
</div>
|
||
|
||
{settingsScope === 'personal' ? <>
|
||
<section className="p-4 sm:p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<h3 className="font-bold text-base text-[var(--krona-text-primary)]">Отображение разделов</h3>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mt-0.5 mb-4">Показывайте только те способы связи, которыми пользуется организация</p>
|
||
<div className="divide-y divide-[var(--krona-border-subtle)]">
|
||
<div className="flex items-center justify-between gap-4 py-3">
|
||
<div>
|
||
<div className="font-semibold text-sm text-[var(--krona-text-primary)]">Email</div>
|
||
<div className="text-xs text-[var(--krona-text-secondary)]">Почтовые ящики и переписка</div>
|
||
</div>
|
||
<button type="button" role="switch" aria-checked={showEmailTab} disabled={!orgEmailAllowed} onClick={() => onShowEmailTabChange(!showEmailTab)} className={`${switchClass(showEmailTab)} disabled:opacity-40`} aria-label="Показывать раздел Email">
|
||
<span className={switchKnobClass(showEmailTab)} />
|
||
</button>
|
||
</div>
|
||
<div className="flex items-center justify-between gap-4 py-3">
|
||
<div>
|
||
<div className="font-semibold text-sm text-[var(--krona-text-primary)]">Каналы</div>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mt-0.5">Боты и публичные каналы организации</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
role="switch"
|
||
aria-checked={showChannelsTab}
|
||
onClick={() => onShowChannelsTabChange(!showChannelsTab)}
|
||
disabled={!orgChannelsAllowed}
|
||
className={`${switchClass(showChannelsTab)} disabled:opacity-40`}
|
||
aria-label="Показывать раздел Каналы"
|
||
>
|
||
<span className={switchKnobClass(showChannelsTab)} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section className="p-4 sm:p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs divide-y divide-[var(--krona-border-subtle)]">
|
||
<h3 className="font-bold text-base text-[var(--krona-text-primary)] pb-3">Поведение и уведомления</h3>
|
||
{[
|
||
{ label: 'Уведомления', hint: 'Новые сообщения и упоминания', icon: Bell, value: notificationsEnabled, set: setNotificationsEnabled, key: 'krona-user-notifications' },
|
||
{ label: 'Звуки', hint: 'Звуковой сигнал новых сообщений', icon: Volume2, value: soundsEnabled, set: setSoundsEnabled, key: 'krona-user-sounds' },
|
||
{ label: 'Компактный интерфейс', hint: 'Больше диалогов и сообщений на экране', icon: PanelTop, value: compactMode, set: setCompactMode, key: 'krona-user-compact' },
|
||
].map((item) => <div key={item.label} className="flex items-center justify-between gap-4 py-3"><div className="flex items-center gap-3"><item.icon className="w-4 h-4 text-[var(--krona-primary)]"/><div><div className="font-semibold text-sm text-[var(--krona-text-primary)]">{item.label}</div><div className="text-xs text-[var(--krona-text-secondary)]">{item.hint}</div></div></div><button type="button" role="switch" aria-checked={item.value} onClick={() => { const next = !item.value; item.set(next); localStorage.setItem(item.key, String(next)); }} className={switchClass(item.value)} aria-label={item.label}><span className={switchKnobClass(item.value)} /></button></div>)}
|
||
</section>
|
||
</> : <>
|
||
<section className="p-4 sm:p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<div className="flex items-center gap-2 mb-1"><UserCog className="w-5 h-5 text-[var(--krona-primary)]"/><h3 className="font-bold text-base text-[var(--krona-text-primary)]">Доступ организации</h3></div>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mb-4">Разрешения действуют для всех пользователей организации</p>
|
||
<div className="divide-y divide-[var(--krona-border-subtle)]">
|
||
<div className="flex items-center justify-between gap-4 py-3"><div><div className="font-semibold text-sm">Разрешить Email</div><div className="text-xs text-[var(--krona-text-secondary)]">Пользователи смогут подключать и показывать свои ящики</div></div><button type="button" role="switch" aria-checked={orgEmailAllowed} onClick={() => onOrgEmailAllowedChange(!orgEmailAllowed)} className={switchClass(orgEmailAllowed)} aria-label="Разрешить Email организации"><span className={switchKnobClass(orgEmailAllowed)}/></button></div>
|
||
<div className="flex items-center justify-between gap-4 py-3"><div><div className="font-semibold text-sm">Разрешить Каналы</div><div className="text-xs text-[var(--krona-text-secondary)]">Боты и публичные каналы организации</div></div><button type="button" role="switch" aria-checked={orgChannelsAllowed} onClick={() => onOrgChannelsAllowedChange(!orgChannelsAllowed)} className={switchClass(orgChannelsAllowed)} aria-label="Разрешить Каналы организации"><span className={switchKnobClass(orgChannelsAllowed)}/></button></div>
|
||
<div className="flex items-center justify-between gap-4 py-3"><div><div className="font-semibold text-sm">Пользователи подключают аккаунты</div><div className="text-xs text-[var(--krona-text-secondary)]">Разрешить добавление личных мессенджеров и почты</div></div><button type="button" role="switch" aria-checked={canConnectAccounts} onClick={() => onCanConnectAccountsChange(!canConnectAccounts)} className={switchClass(canConnectAccounts)} aria-label="Разрешить подключение аккаунтов"><span className={switchKnobClass(canConnectAccounts)}/></button></div>
|
||
</div>
|
||
</section>
|
||
<section className="p-4 sm:p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<div className="flex items-center justify-between gap-3 mb-4"><div><h3 className="font-bold text-base">Пользователи и роли</h3><p className="text-xs text-[var(--krona-text-secondary)]">Кто принимает сообщения и управляет подключениями</p></div><Button size="sm" variant="primary" onClick={() => showToast({ title: 'Добавление пользователя', message: 'Форма приглашения пользователя будет открыта здесь', type: 'info' })}>Добавить пользователя</Button></div>
|
||
<div className="space-y-2 text-sm"><div className="flex justify-between p-3 rounded-xl bg-[var(--krona-bg-workspace)]"><span><strong>Виктор</strong><small className="block text-[var(--krona-text-secondary)]">Владелец</small></span><span className="text-xs text-[var(--krona-primary)]">Все права</span></div><div className="flex justify-between p-3 rounded-xl bg-[var(--krona-bg-workspace)]"><span><strong>Мария К.</strong><small className="block text-[var(--krona-text-secondary)]">Менеджер</small></span><span className="text-xs text-[var(--krona-text-secondary)]">Чаты и почта</span></div></div>
|
||
</section>
|
||
{/* 1. Connected Messengers & Connectors */}
|
||
<section className="p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h3 className="font-bold text-base text-[var(--krona-text-primary)]">
|
||
Подключенные мессенджеры и коннекторы
|
||
</h3>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mt-0.5">
|
||
Управление внешними каналами связи и интеграциями
|
||
</p>
|
||
</div>
|
||
<span className="text-xs font-semibold text-emerald-600 flex items-center gap-1.5">
|
||
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
||
Все коннекторы активны
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{integrations.map((item) => (
|
||
<div
|
||
key={item.id}
|
||
className="p-3.5 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border-subtle)] flex flex-col sm:flex-row sm:items-center justify-between gap-3"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 rounded-xl bg-[var(--krona-surface-raised)] border border-[var(--krona-border)] flex items-center justify-center font-bold text-sm text-[var(--krona-primary)] shrink-0">
|
||
{item.title[0]}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center gap-2">
|
||
<span className="font-bold text-sm text-[var(--krona-text-primary)]">
|
||
{item.title}
|
||
</span>
|
||
<Badge variant={item.status === 'active' ? 'success' : 'danger'} size="sm">
|
||
{item.status === 'active' ? 'Подключен' : 'Отключен'}
|
||
</Badge>
|
||
</div>
|
||
<span className="text-xs text-[var(--krona-text-secondary)]">
|
||
{item.accountIdentifier} • Синхронизация: {item.lastSync} • Лимит: {item.rateLimit}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 self-end sm:self-auto">
|
||
<Button
|
||
variant="secondary"
|
||
size="sm"
|
||
isLoading={testingId === item.id}
|
||
onClick={() => handleTest(item.id)}
|
||
leftIcon={<RefreshCw className="w-3.5 h-3.5" />}
|
||
>
|
||
Тест связи
|
||
</Button>
|
||
<Button
|
||
variant={item.status === 'active' ? 'danger' : 'secondary'}
|
||
size="sm"
|
||
onClick={() => toggleIntegration(item.id)}
|
||
>
|
||
{item.status === 'active' ? 'Отключить' : 'Включить'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 2. Network & VPN Gateway (Sing-box) */}
|
||
<section className="p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h3 className="font-bold text-base text-[var(--krona-text-primary)] flex items-center gap-2">
|
||
<Globe className="w-5 h-5 text-[var(--krona-primary)]" />
|
||
Сетевой шлюз и VPN (Sing-box / Wireguard)
|
||
</h3>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mt-0.5">
|
||
Обеспечение бесперебойного доступа к международным API Telegram и WhatsApp
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2"><span className="px-2.5 py-1 rounded-full bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 font-bold text-xs">Туннель активен (14 ms)</span><Button size="sm" variant="secondary" onClick={() => showToast({ title: 'Настройка VPN', message: 'Открыта настройка единого шлюза организации', type: 'info' })}>Настроить VPN</Button></div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
|
||
<div className="p-3 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border-subtle)]">
|
||
<span className="text-[var(--krona-text-muted)] block">Протокол маршрутизации</span>
|
||
<strong className="text-sm text-[var(--krona-text-primary)] block mt-0.5">VLESS + XTLS Vision</strong>
|
||
</div>
|
||
<div className="p-3 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border-subtle)]">
|
||
<span className="text-[var(--krona-text-muted)] block">Шлюз и прокси-нода</span>
|
||
<strong className="text-sm text-[var(--krona-text-primary)] block mt-0.5">eu-west.krona.systems</strong>
|
||
</div>
|
||
<div className="p-3 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border-subtle)]">
|
||
<span className="text-[var(--krona-text-muted)] block">Uptime туннеля</span>
|
||
<strong className="text-sm text-emerald-600 block mt-0.5">99.98% (30 дней)</strong>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 3. Quick Replies Manager */}
|
||
<section className="p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<h3 className="font-bold text-base text-[var(--krona-text-primary)] mb-1">
|
||
Шаблоны быстрых ответов
|
||
</h3>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mb-4">
|
||
Шаблоны доступны операторам в один клик при общении с клиентами
|
||
</p>
|
||
|
||
<form onSubmit={handleAddTemplate} className="flex gap-2 mb-4">
|
||
<input
|
||
type="text"
|
||
value={newTemplateText}
|
||
onChange={(e) => setNewTemplateText(e.target.value)}
|
||
placeholder="Введите новый шаблон ответа..."
|
||
className="flex-1 h-10 px-3 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border)] text-sm text-[var(--krona-text-primary)] placeholder-[var(--krona-text-muted)] focus:outline-none focus:border-[var(--krona-primary)]"
|
||
/>
|
||
<Button type="submit" variant="primary" disabled={!newTemplateText.trim()}>
|
||
Добавить
|
||
</Button>
|
||
</form>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||
{quickReplies.map((tmpl, idx) => (
|
||
<div
|
||
key={idx}
|
||
className="p-3 rounded-xl bg-[var(--krona-bg-workspace)] border border-[var(--krona-border-subtle)] flex items-center justify-between gap-2 text-xs text-[var(--krona-text-primary)]"
|
||
>
|
||
<span>{tmpl}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => deleteQuickReplyTemplate(idx)}
|
||
className="text-[var(--krona-text-muted)] hover:text-red-600 p-1 transition-colors"
|
||
title="Удалить шаблон"
|
||
>
|
||
<Trash2 className="w-3.5 h-3.5" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 4. Connection & Routing Audit Log */}
|
||
<section className="p-5 rounded-2xl bg-[var(--krona-surface)] border border-[var(--krona-border-subtle)] shadow-xs">
|
||
<h3 className="font-bold text-base text-[var(--krona-text-primary)] mb-1">
|
||
Журнал подключений и событий шлюза
|
||
</h3>
|
||
<p className="text-xs text-[var(--krona-text-secondary)] mb-4">
|
||
Сквозной аудит всех входящих и исходящих сессий
|
||
</p>
|
||
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-left text-xs">
|
||
<thead>
|
||
<tr className="border-b border-[var(--krona-border)] text-[var(--krona-text-secondary)]">
|
||
<th className="py-2 px-3 font-semibold">Время</th>
|
||
<th className="py-2 px-3 font-semibold">Источник</th>
|
||
<th className="py-2 px-3 font-semibold">Событие</th>
|
||
<th className="py-2 px-3 font-semibold">Оператор / Сервис</th>
|
||
<th className="py-2 px-3 font-semibold">Статус</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-[var(--krona-border-subtle)]">
|
||
{auditLogs.map((log) => (
|
||
<tr key={log.id} className="hover:bg-[var(--krona-surface-hover)]">
|
||
<td className="py-2.5 px-3 text-[var(--krona-text-muted)] font-mono">
|
||
{log.timestamp}
|
||
</td>
|
||
<td className="py-2.5 px-3 font-semibold text-[var(--krona-text-primary)]">
|
||
{log.source}
|
||
</td>
|
||
<td className="py-2.5 px-3 text-[var(--krona-text-primary)]">
|
||
{log.event}
|
||
</td>
|
||
<td className="py-2.5 px-3 text-[var(--krona-text-secondary)]">
|
||
{log.operator}
|
||
</td>
|
||
<td className="py-2.5 px-3">
|
||
<span className="px-2 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-950 text-emerald-700 dark:text-emerald-300 font-semibold text-[10px]">
|
||
Успех
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</>}
|
||
</div>
|
||
);
|
||
}
|