Files
IdP/apps/frontend/components/id/messaging-settings-section.tsx
2026-06-24 16:53:06 +03:00

213 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { Loader2, Mail, MessageSquare, Save, Send, ToggleLeft, ToggleRight } from 'lucide-react';
import { useMemo, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { PhoneInput, phoneCountries } from '@/components/ui/phone-input';
import { apiFetch, buildSystemSettingPayload, SystemSetting } from '@/lib/api';
import { getSettingMeta, isSettingVisible, MESSAGING_SETTING_KEYS } from '@/lib/system-settings-catalog';
function parseBoolean(value: string) {
return ['true', '1', 'yes'].includes(value.trim().toLowerCase());
}
interface MessagingSettingsSectionProps {
settings: SystemSetting[];
token: string | null;
savingKey: string | null;
onUpdateValue: (key: string, value: string) => void;
onSave: (setting: SystemSetting) => Promise<void>;
showToast: (message: string) => void;
}
function getSettingValue(settings: SystemSetting[], key: string) {
return settings.find((item) => item.key === key)?.value ?? '';
}
function shouldShowMessagingField(setting: SystemSetting, settings: SystemSetting[]) {
if (!isSettingVisible(setting, settings)) return false;
const provider = getSettingValue(settings, 'SMS_PROVIDER');
if (setting.key === 'SMS_API_KEY') return provider === 'smsru';
if (setting.key === 'SMS_LOGIN' || setting.key === 'SMS_PASSWORD') return provider === 'smsc';
const emailProvider = getSettingValue(settings, 'EMAIL_PROVIDER');
if (setting.key.startsWith('EMAIL_SMTP_') || setting.key.startsWith('EMAIL_FROM_')) {
return parseBoolean(getSettingValue(settings, 'EMAIL_ENABLED')) && emailProvider === 'smtp';
}
if (setting.key === 'EMAIL_PROVIDER') {
return parseBoolean(getSettingValue(settings, 'EMAIL_ENABLED'));
}
if (setting.key === 'SMS_PROVIDER') {
return parseBoolean(getSettingValue(settings, 'SMS_ENABLED'));
}
return true;
}
export function MessagingSettingsSection({
settings,
token,
savingKey,
onUpdateValue,
onSave,
showToast
}: MessagingSettingsSectionProps) {
const [testEmail, setTestEmail] = useState('');
const [testPhone, setTestPhone] = useState('');
const [testPhoneCountry, setTestPhoneCountry] = useState(phoneCountries[0]);
const [testingChannel, setTestingChannel] = useState<'email' | 'sms' | null>(null);
const messagingSettings = useMemo(
() => settings.filter((item) => MESSAGING_SETTING_KEYS.includes(item.key)),
[settings]
);
async function sendTest(channel: 'email' | 'sms') {
if (!token) return;
const target = channel === 'email' ? testEmail.trim() : `${testPhoneCountry.code}${testPhone}`;
if (!target) {
showToast(channel === 'email' ? 'Укажите email для теста' : 'Укажите номер телефона для теста');
return;
}
setTestingChannel(channel);
try {
const response = await apiFetch<{ message?: string }>(
'/admin/settings/messaging/test',
{
method: 'POST',
body: JSON.stringify({ channel, target })
},
token
);
showToast(response.message ?? 'Тестовое сообщение отправлено');
} catch (error) {
showToast(error instanceof Error ? error.message : 'Не удалось отправить тестовое сообщение');
} finally {
setTestingChannel(null);
}
}
const emailEnabled = parseBoolean(getSettingValue(messagingSettings, 'EMAIL_ENABLED'));
const smsEnabled = parseBoolean(getSettingValue(messagingSettings, 'SMS_ENABLED'));
return (
<section className="mb-10">
<div className="mb-4 flex items-center gap-3">
<MessageSquare className="h-6 w-6" />
<div>
<h3 className="text-xl font-medium">Email и SMS провайдеры</h3>
<p className="mt-1 text-sm text-[#667085]">
Настройте SMTP и SMS OTP-коды при входе и регистрации будут отправляться автоматически.
</p>
</div>
</div>
<div className="space-y-3">
{messagingSettings.filter((setting) => shouldShowMessagingField(setting, messagingSettings)).map((setting) => {
const meta = getSettingMeta(setting.key);
const label = meta?.label ?? setting.key;
const type = meta?.type ?? 'text';
return (
<div key={setting.key} className="rounded-[24px] bg-[#f4f5f8] p-5">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="min-w-0 flex-1">
<div className="font-semibold">{label}</div>
<p className="mt-1 text-sm text-[#667085]">{meta?.hint ?? setting.description ?? setting.key}</p>
</div>
<div className="flex flex-wrap items-center gap-3">
{type === 'boolean' ? (
<button
type="button"
aria-label={label}
onClick={() => onUpdateValue(setting.key, parseBoolean(setting.value) ? 'false' : 'true')}
>
{parseBoolean(setting.value) ? <ToggleRight className="h-9 w-9 text-green-600" /> : <ToggleLeft className="h-9 w-9 text-[#a8adbc]" />}
</button>
) : type === 'select' ? (
<select
className="h-10 rounded-xl border border-[#e4e7ec] bg-white px-3 text-sm outline-none focus:border-[#98a2b3]"
value={setting.value}
onChange={(event) => onUpdateValue(setting.key, event.target.value)}
>
{(meta?.options ?? []).map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
) : (
<div className="flex items-center gap-2">
<Input
type={setting.isSecret ? 'password' : type === 'number' ? 'number' : 'text'}
value={setting.value}
className={setting.isSecret ? 'w-[240px] bg-white' : 'w-[220px] bg-white'}
onChange={(event) => onUpdateValue(setting.key, event.target.value)}
/>
{meta?.unit ? <span className="text-sm text-[#667085]">{meta.unit}</span> : null}
</div>
)}
<Button disabled={savingKey === setting.key} onClick={() => void onSave(setting)}>
{savingKey === setting.key ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
Сохранить
</Button>
</div>
</div>
</div>
);
})}
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
<div className="rounded-[24px] border border-[#e4e7ec] bg-white p-5">
<div className="flex items-center gap-2 font-semibold">
<Mail className="h-5 w-5" />
Тест email
</div>
<p className="mt-2 text-sm text-[#667085]">
{emailEnabled ? 'Отправит тестовый OTP-код через SMTP.' : 'Сначала включите Email и сохраните настройки SMTP.'}
</p>
<Input
className="mt-4 bg-[#f9fafb]"
type="email"
placeholder="admin@example.com"
value={testEmail}
onChange={(event) => setTestEmail(event.target.value)}
/>
<Button className="mt-3" disabled={!emailEnabled || testingChannel === 'email'} onClick={() => void sendTest('email')}>
{testingChannel === 'email' ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
Отправить тест
</Button>
</div>
<div className="rounded-[24px] border border-[#e4e7ec] bg-white p-5">
<div className="flex items-center gap-2 font-semibold">
<MessageSquare className="h-5 w-5" />
Тест SMS
</div>
<p className="mt-2 text-sm text-[#667085]">
{smsEnabled ? 'Отправит тестовый OTP-код через SMS-провайдера.' : 'Сначала включите SMS и сохраните ключ API.'}
</p>
<div className="mt-4">
<PhoneInput
country={testPhoneCountry}
value={testPhone}
onCountryChange={setTestPhoneCountry}
onValueChange={setTestPhone}
/>
</div>
<Button className="mt-3" disabled={!smsEnabled || testingChannel === 'sms'} onClick={() => void sendTest('sms')}>
{testingChannel === 'sms' ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
Отправить тест
</Button>
</div>
</div>
</section>
);
}