feat: Telegram message templates in OrderModal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user