Add Telegram bot notifications for managers in settings
In Settings → Notifications, new «Telegram-уведомления» section: - Bot token input (from @BotFather) - Step-by-step instructions: manager sends /start to bot, gets Chat ID, admin adds it - Copyable bot invite link - Event checkboxes: new booking, cancellation, check-in, check-out, channel error, payment - Manager list with avatar initials, name, Chat ID, delete button - Add manager form: name + Chat ID input - Test message button (shown when at least one manager is added) - Full section only visible after token is entered Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Users, Plus, X as XIcon } from 'lucide-react'
|
||||
import { Save, Building2, Bell, Shield, Globe, CreditCard, BedDouble, Mail, MessageSquare, Users, Plus, X as XIcon, Send, CheckCircle2, Copy } from 'lucide-react'
|
||||
import { MOCK_HOTELS } from '../data/mockData'
|
||||
import { cn, PLAN_COLORS, PLAN_LABELS } from '../lib/utils'
|
||||
import { Badge } from '../components/ui/Badge'
|
||||
@@ -80,6 +80,42 @@ export function SettingsPage() {
|
||||
const [smsApiKey, setSmsApiKey] = useState('')
|
||||
const [smsSender, setSmsSender] = useState('')
|
||||
|
||||
// Telegram settings
|
||||
const [tgBotToken, setTgBotToken] = useState('')
|
||||
const [tgManagers, setTgManagers] = useState<{ id: string; name: string; chatId: string }[]>([])
|
||||
const [newTgName, setNewTgName] = useState('')
|
||||
const [newTgChatId, setNewTgChatId] = useState('')
|
||||
const [tgEvents, setTgEvents] = useState({
|
||||
newBooking: true,
|
||||
cancellation: true,
|
||||
checkIn: true,
|
||||
checkOut: false,
|
||||
channelError: true,
|
||||
payment: false,
|
||||
})
|
||||
const [tgTestSent, setTgTestSent] = useState(false)
|
||||
const [copiedToken, setCopiedToken] = useState(false)
|
||||
|
||||
const tgBotConfigured = tgBotToken.length > 10
|
||||
|
||||
const addTgManager = () => {
|
||||
if (!newTgName.trim() || !newTgChatId.trim()) return
|
||||
setTgManagers(prev => [...prev, { id: `tg-${Date.now()}`, name: newTgName.trim(), chatId: newTgChatId.trim() }])
|
||||
setNewTgName('')
|
||||
setNewTgChatId('')
|
||||
}
|
||||
|
||||
const sendTgTest = () => {
|
||||
setTgTestSent(true)
|
||||
setTimeout(() => setTgTestSent(false), 3000)
|
||||
}
|
||||
|
||||
const copyBotLink = () => {
|
||||
navigator.clipboard.writeText('https://t.me/HotelSyncBot?start=connect')
|
||||
setCopiedToken(true)
|
||||
setTimeout(() => setCopiedToken(false), 2000)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
@@ -436,6 +472,150 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Telegram */}
|
||||
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Send size={15} className="text-sky-500" />
|
||||
<h3 className="text-sm font-semibold text-slate-800 dark:text-slate-200">Telegram-уведомления для менеджеров</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 mb-3">
|
||||
Менеджеры получают уведомления о бронированиях прямо в Telegram. Каждый менеджер должен написать боту команду <code className="bg-slate-100 dark:bg-slate-700 px-1 rounded">/start</code> и сообщить свой Chat ID.
|
||||
</p>
|
||||
|
||||
{/* How to connect — step instructions */}
|
||||
<div className="rounded-xl bg-sky-50 dark:bg-sky-900/20 border border-sky-200 dark:border-sky-800 p-3.5 mb-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-sky-700 dark:text-sky-300">Как подключить менеджера:</p>
|
||||
<ol className="text-xs text-sky-700 dark:text-sky-300 space-y-1 list-decimal list-inside">
|
||||
<li>Менеджер открывает Telegram и переходит в бота</li>
|
||||
<li>Пишет <code className="bg-sky-100 dark:bg-sky-900 px-1 rounded">/start</code> — бот ответит его Chat ID</li>
|
||||
<li>Менеджер сообщает Chat ID администратору — тот добавляет его ниже</li>
|
||||
</ol>
|
||||
<button
|
||||
onClick={copyBotLink}
|
||||
className="flex items-center gap-1.5 text-xs font-medium text-sky-600 dark:text-sky-400 hover:underline"
|
||||
>
|
||||
{copiedToken ? <CheckCircle2 size={13} className="text-emerald-500" /> : <Copy size={13} />}
|
||||
{copiedToken ? 'Скопировано!' : 'Скопировать ссылку на бота'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bot token */}
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-medium text-slate-600 dark:text-slate-400 mb-1">
|
||||
Токен бота
|
||||
<span className="ml-1 text-slate-400 font-normal">(получить у @BotFather в Telegram)</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
className="input text-sm font-mono"
|
||||
placeholder="123456789:AAExx..."
|
||||
value={tgBotToken}
|
||||
onChange={e => setTgBotToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Events to notify */}
|
||||
{tgBotConfigured && (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-slate-600 dark:text-slate-400 mb-2">Уведомлять о событиях:</p>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
{([
|
||||
{ key: 'newBooking' as const, label: 'Новое бронирование' },
|
||||
{ key: 'cancellation' as const, label: 'Отмена брони' },
|
||||
{ key: 'checkIn' as const, label: 'Заезд гостя' },
|
||||
{ key: 'checkOut' as const, label: 'Выезд гостя' },
|
||||
{ key: 'channelError' as const, label: 'Ошибка канала' },
|
||||
{ key: 'payment' as const, label: 'Платёж / оплата' },
|
||||
]).map(ev => (
|
||||
<label key={ev.key} className="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-brand-600"
|
||||
checked={tgEvents[ev.key]}
|
||||
onChange={() => setTgEvents(p => ({ ...p, [ev.key]: !p[ev.key] }))}
|
||||
/>
|
||||
<span className="text-slate-700 dark:text-slate-300">{ev.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manager list */}
|
||||
<div className="mb-3">
|
||||
<p className="text-xs font-medium text-slate-600 dark:text-slate-400 mb-2">
|
||||
Менеджеры — получатели уведомлений
|
||||
{tgManagers.length > 0 && <span className="ml-1 text-slate-400">({tgManagers.length})</span>}
|
||||
</p>
|
||||
<div className="space-y-1.5 mb-2">
|
||||
{tgManagers.length === 0 && (
|
||||
<p className="text-xs text-slate-400 py-2 text-center rounded-lg bg-slate-50 dark:bg-slate-700/30">
|
||||
Менеджеры ещё не добавлены
|
||||
</p>
|
||||
)}
|
||||
{tgManagers.map(m => (
|
||||
<div key={m.id} className="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-50 dark:bg-slate-700/40 text-sm">
|
||||
<div className="w-7 h-7 rounded-full bg-sky-100 dark:bg-sky-900/50 flex items-center justify-center shrink-0">
|
||||
<span className="text-xs font-bold text-sky-600 dark:text-sky-400">{m.name[0]?.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-900 dark:text-slate-100 truncate">{m.name}</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 font-mono">Chat ID: {m.chatId}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setTgManagers(prev => prev.filter(x => x.id !== m.id))}
|
||||
className="p-1 rounded text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||
>
|
||||
<XIcon size={13} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add manager */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
className="input text-sm flex-1"
|
||||
placeholder="Имя менеджера"
|
||||
value={newTgName}
|
||||
onChange={e => setNewTgName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input text-sm w-32 font-mono"
|
||||
placeholder="Chat ID"
|
||||
value={newTgChatId}
|
||||
onChange={e => setNewTgChatId(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') addTgManager() }}
|
||||
/>
|
||||
<button onClick={addTgManager} className="btn-secondary px-3">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test button */}
|
||||
{tgManagers.length > 0 && (
|
||||
<button
|
||||
onClick={sendTgTest}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-sm font-medium px-3 py-2 rounded-lg border transition-colors',
|
||||
tgTestSent
|
||||
? 'bg-emerald-50 dark:bg-emerald-900/20 border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-400'
|
||||
: 'bg-white dark:bg-slate-700 border-slate-200 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:border-sky-400 hover:text-sky-600',
|
||||
)}
|
||||
>
|
||||
{tgTestSent
|
||||
? <><CheckCircle2 size={14} /> Тестовое сообщение отправлено!</>
|
||||
: <><Send size={14} /> Отправить тестовое сообщение</>
|
||||
}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SMS */}
|
||||
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
|
||||
Reference in New Issue
Block a user