feat: Telegram message templates in OrderModal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 11:17:09 +03:00
parent 4b8028d7ae
commit 1fa97ca010
+106 -1
View File
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { X, Save, FileText, Camera, X as XIcon, Share2 } from 'lucide-react'; import { X, Save, FileText, Camera, X as XIcon, Share2, MessageSquare } from 'lucide-react';
import { useOrderPhotos } from './useOrderPhotos'; import { useOrderPhotos } from './useOrderPhotos';
import { pdf } from '@react-pdf/renderer'; import { pdf } from '@react-pdf/renderer';
import { supabase } from '../../lib/supabase'; import { supabase } from '../../lib/supabase';
@@ -9,6 +9,42 @@ import { useIsMobile } from '../../hooks/useIsMobile';
import { InvoicePDF } from './InvoicePDF'; import { InvoicePDF } from './InvoicePDF';
const STATUSES = ['новая', 'замер', 'согласование', 'монтаж', 'закрыт']; const STATUSES = ['новая', 'замер', 'согласование', 'монтаж', 'закрыт'];
const TEMPLATES = [
{
id: 'confirm',
label: 'Подтверждение визита',
text: (f) => {
const date = f.scheduled_at
? new Date(f.scheduled_at).toLocaleString('ru-RU', { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' })
: '[дата не указана]';
return `Здравствуйте, ${f.name || '[имя]'}! Подтверждаем ваш визит ${date}${f.address ? ` по адресу: ${f.address}` : ''}. Ждём вас!`;
},
},
{
id: 'reminder',
label: 'Напоминание (за день)',
text: (f) => {
const date = f.scheduled_at
? new Date(f.scheduled_at).toLocaleString('ru-RU', { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit' })
: '[дата не указана]';
return `${f.name || 'Клиент'}, напоминаем о визите завтра в ${date.split(',')[1]?.trim() || '[время]'}${f.address ? `, адрес: ${f.address}` : ''}. До встречи!`;
},
},
{
id: 'received',
label: 'Заявка получена',
text: (f) => `Здравствуйте, ${f.name || '[имя]'}! Ваша заявка принята. Менеджер свяжется с вами в ближайшее время для уточнения деталей.`,
},
{
id: 'done',
label: 'Работы завершены',
text: (f) => {
const cost = f.final_cost ? `Итоговая стоимость: ${Number(f.final_cost).toLocaleString('ru-RU')} ₽.` : '';
return `${f.name || 'Клиент'}, работы по бронированию стёкол завершены. ${cost} Спасибо за доверие!`;
},
},
];
const OBJECT_TYPES = [ const OBJECT_TYPES = [
{ value: 'industrial', label: 'Промышленный объект' }, { value: 'industrial', label: 'Промышленный объект' },
{ value: 'commercial', label: 'Коммерческая штаб-квартира' }, { value: 'commercial', label: 'Коммерческая штаб-квартира' },
@@ -58,6 +94,8 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [templatesOpen, setTemplatesOpen] = useState(false);
const [copiedTpl, setCopiedTpl] = useState(null);
useEffect(() => { useEffect(() => {
if (order?.id) { if (order?.id) {
@@ -97,6 +135,14 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
scheduled_at: f.scheduled_at === '' ? null : new Date(f.scheduled_at).toISOString(), scheduled_at: f.scheduled_at === '' ? null : new Date(f.scheduled_at).toISOString(),
}); });
const handleCopyTemplate = (tpl) => {
const text = tpl.text(form);
navigator.clipboard.writeText(text).then(() => {
setCopiedTpl(tpl.id);
setTimeout(() => setCopiedTpl(null), 2000);
});
};
const handleShare = () => { const handleShare = () => {
const url = `${window.location.origin}/track/${order.public_token}`; const url = `${window.location.origin}/track/${order.public_token}`;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
@@ -325,6 +371,65 @@ const OrderModal = ({ order, onClose, onSaved, mode = 'edit' }) => {
<textarea id="modal-notes" name="notes" value={form.notes} onChange={set} style={{ ...inputStyle, height: '72px', resize: 'vertical' }} /> <textarea id="modal-notes" name="notes" value={form.notes} onChange={set} style={{ ...inputStyle, height: '72px', resize: 'vertical' }} />
</div> </div>
{mode === 'edit' && (
<div style={{ marginTop: '14px' }}>
<button
type="button"
onClick={() => setTemplatesOpen((v) => !v)}
style={{
display: 'flex', alignItems: 'center', gap: '6px',
background: 'none', border: 'none', cursor: 'pointer',
fontSize: '0.82rem', fontWeight: 700, color: 'var(--text-muted)',
padding: '4px 0', fontFamily: 'var(--font-heading)',
textTransform: 'uppercase', letterSpacing: '0.4px',
}}
>
<MessageSquare size={14} />
Шаблоны сообщений
<span style={{ marginLeft: '2px', fontSize: '0.7rem' }}>{templatesOpen ? '▲' : '▼'}</span>
</button>
{templatesOpen && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '10px' }}>
{TEMPLATES.map((tpl) => (
<div
key={tpl.id}
style={{
background: 'var(--bg-lighter)', border: '1px solid var(--border-light)',
borderRadius: '8px', padding: '12px',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '0.75rem', fontWeight: 700, color: 'var(--text-muted)', marginBottom: '4px', textTransform: 'uppercase', letterSpacing: '0.3px' }}>
{tpl.label}
</div>
<div style={{ fontSize: '0.83rem', color: 'var(--text-main)', lineHeight: 1.5 }}>
{tpl.text(form)}
</div>
</div>
<button
type="button"
onClick={() => handleCopyTemplate(tpl)}
style={{
flexShrink: 0, padding: '6px 12px',
background: copiedTpl === tpl.id ? '#10b981' : '#4361EE',
color: '#fff', border: 'none', borderRadius: '6px',
fontSize: '0.78rem', fontWeight: 700, cursor: 'pointer',
fontFamily: 'var(--font-heading)',
transition: 'background 0.15s',
}}
>
{copiedTpl === tpl.id ? 'Скопировано!' : 'Копировать'}
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
{mode === 'edit' && ( {mode === 'edit' && (
<div style={{ marginTop: '14px' }}> <div style={{ marginTop: '14px' }}>
<label style={labelStyle}>Фотографии объекта</label> <label style={labelStyle}>Фотографии объекта</label>