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

240 lines
8.7 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 } 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, SystemSetting } from '@/lib/api';
import {
EMAIL_SETTING_KEYS,
isSettingVisible,
SMS_SETTING_KEYS
} from '@/lib/system-settings-catalog';
import { SettingsFieldRow } from '@/components/id/settings-field-row';
function parseBoolean(value: string) {
return ['true', '1', 'yes'].includes(value.trim().toLowerCase());
}
interface MessagingSettingsSectionProps {
settings: SystemSetting[];
token: string | null;
savingGroup: string | null;
onUpdateValue: (key: string, value: string) => void;
onSaveGroup: (groupKey: string, groupSettings: 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;
}
function MessagingBlock({
title,
description,
groupKey,
blockSettings,
allSettings,
savingGroup,
onUpdateValue,
onSaveGroup
}: {
title: string;
description: string;
groupKey: string;
blockSettings: SystemSetting[];
allSettings: SystemSetting[];
savingGroup: string | null;
onUpdateValue: (key: string, value: string) => void;
onSaveGroup: (groupKey: string, groupSettings: SystemSetting[]) => Promise<void>;
}) {
const visibleSettings = blockSettings.filter((setting) => shouldShowMessagingField(setting, allSettings));
const saving = savingGroup === groupKey;
return (
<div className="rounded-[24px] bg-[#f4f5f8] p-5">
<div className="mb-4 flex flex-col gap-3 border-b border-[#e0e3ea] pb-4 md:flex-row md:items-start md:justify-between">
<div>
<h4 className="text-lg font-medium">{title}</h4>
<p className="mt-1 text-sm text-[#667085]">{description}</p>
</div>
<Button disabled={saving || visibleSettings.length === 0} onClick={() => void onSaveGroup(groupKey, blockSettings)}>
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
Сохранить блок
</Button>
</div>
<div>
{visibleSettings.map((setting) => (
<SettingsFieldRow
key={setting.key}
setting={setting}
onChange={(value) => onUpdateValue(setting.key, value)}
/>
))}
</div>
</div>
);
}
export function MessagingSettingsSection({
settings,
token,
savingGroup,
onUpdateValue,
onSaveGroup,
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 emailSettings = useMemo(
() => settings.filter((item) => EMAIL_SETTING_KEYS.includes(item.key)),
[settings]
);
const smsSettings = useMemo(
() => settings.filter((item) => SMS_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(emailSettings, 'EMAIL_ENABLED'));
const smsEnabled = parseBoolean(getSettingValue(smsSettings, 'SMS_ENABLED'));
const emailReady =
emailEnabled ||
(Boolean(getSettingValue(emailSettings, 'EMAIL_SMTP_HOST')) &&
Boolean(getSettingValue(emailSettings, 'EMAIL_FROM_ADDRESS')));
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]">
Измените параметры блока и нажмите «Сохранить блок» все поля блока сохранятся одним действием.
</p>
</div>
</div>
<div className="space-y-4">
<MessagingBlock
title="Email (SMTP)"
description="Отправка OTP-кодов на почту через SMTP-провайдера."
groupKey="messaging-email"
blockSettings={emailSettings}
allSettings={settings}
savingGroup={savingGroup}
onUpdateValue={onUpdateValue}
onSaveGroup={onSaveGroup}
/>
<MessagingBlock
title="SMS"
description="Отправка OTP-кодов по SMS через sms.ru или smsc.ru."
groupKey="messaging-sms"
blockSettings={smsSettings}
allSettings={settings}
savingGroup={savingGroup}
onUpdateValue={onUpdateValue}
onSaveGroup={onSaveGroup}
/>
</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]">
{emailReady ? 'Отправит тестовый 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={!emailReady || 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.'}
</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>
);
}