diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx index 50e9b8cd..1c3f7c8d 100644 --- a/admin/src/pages/SettingsPage.tsx +++ b/admin/src/pages/SettingsPage.tsx @@ -38,6 +38,7 @@ import { AlertOutlined, MailOutlined, RetweetOutlined, + PhoneOutlined, } from '@ant-design/icons'; import { useSettingsStore } from '@/stores/settings.store'; import { api } from '@/lib/api'; @@ -614,6 +615,36 @@ export default function SettingsPage() { + + {/* SMS Notifications — only show when enableSms is on */} + , cur: Record) => prev.enableSms !== cur.enableSms}> + {({ getFieldValue }: { getFieldValue: (name: string) => unknown }) => + getFieldValue('enableSms') ? ( + + SMS Notifications} + > + + + + + + + + + + + + + + All SMS notifications respect opt-outs. Requires an active phone connection. + + + + ) : null + } + ), diff --git a/admin/src/pages/sms/SmsCampaignsPage.tsx b/admin/src/pages/sms/SmsCampaignsPage.tsx index cfa18062..15556f68 100644 --- a/admin/src/pages/sms/SmsCampaignsPage.tsx +++ b/admin/src/pages/sms/SmsCampaignsPage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from 'react'; -import { Table, Button, Modal, Form, Input, Select, InputNumber, Space, Tag, Progress, App, Typography, Popconfirm } from 'antd'; -import { PlusOutlined, PlayCircleOutlined, PauseCircleOutlined, CaretRightOutlined, DeleteOutlined } from '@ant-design/icons'; +import { Table, Button, Modal, Form, Input, Select, InputNumber, Space, Tag, Progress, App, Typography, Popconfirm, Divider, Alert } from 'antd'; +import { PlusOutlined, PlayCircleOutlined, PauseCircleOutlined, CaretRightOutlined, DeleteOutlined, EyeOutlined, SendOutlined, PhoneOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { api } from '@/lib/api'; import type { SmsCampaign, SmsContactList, SmsPaginatedResponse } from '@/types/sms'; @@ -30,6 +30,11 @@ export default function SmsCampaignsPage() { const [contactLists, setContactLists] = useState([]); const [createForm] = Form.useForm(); + // Preview & Test + const [testPreview, setTestPreview] = useState(''); + const [testPhone, setTestPhone] = useState(''); + const [testSending, setTestSending] = useState(false); + useEffect(() => { setPageHeader({ title: 'SMS Campaigns', subtitle: 'Create and manage SMS campaigns' }); }, [setPageHeader]); @@ -110,6 +115,31 @@ export default function SmsCampaignsPage() { } catch { message.error('Delete failed — only DRAFT campaigns can be deleted'); } }; + const handlePreviewTest = () => { + const template = createForm.getFieldValue('messageTemplate') || ''; + // Substitute placeholders with sample values + const preview = template + .replace(/\{name\}/g, 'Jane Doe') + .replace(/\{phone\}/g, '+1 555 000 0000'); + setTestPreview(preview); + }; + + const handleSendTestCampaign = async () => { + if (!testPhone.trim() || !testPreview.trim()) { + message.warning('Enter a test phone number and ensure the message preview is visible'); + return; + } + setTestSending(true); + try { + await api.post('/sms/messages/send', { phone: testPhone.trim(), message: testPreview }); + message.success('Test message sent'); + } catch { + message.error('Failed to send test message'); + } finally { + setTestSending(false); + } + }; + const columns: ColumnsType = [ { title: 'Name', dataIndex: 'name', ellipsis: true }, { @@ -204,7 +234,7 @@ export default function SmsCampaignsPage() { setCreateOpen(false)} + onCancel={() => { setCreateOpen(false); setTestPreview(''); }} onOk={() => createForm.submit()} width={600} > @@ -224,6 +254,54 @@ export default function SmsCampaignsPage() {