feat: email field for QR review replies, internal note if no email
Some checks failed
Deploy to Production / deploy (push) Has been cancelled
Some checks failed
Deploy to Production / deploy (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,7 @@ export function ReviewsPage() {
|
||||
const [tab, setTab] = useState<Tab>('pending')
|
||||
const [replyId, setReplyId] = useState<string | null>(null)
|
||||
const [replyText, setReplyText] = useState('')
|
||||
const [replyEmail, setReplyEmail] = useState('')
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
// Settings
|
||||
@@ -189,10 +190,11 @@ export function ReviewsPage() {
|
||||
|
||||
// ── Review actions ──────────────────────────────────────────────────────────
|
||||
|
||||
const sendReply = async (id: string) => {
|
||||
const sendReply = async (id: string, isQr: boolean) => {
|
||||
if (!replyText.trim() || !slug) return
|
||||
// Negative review stays private (isPublic=false), just saves the reply
|
||||
const updated = await api.reviews.update(slug, id, { reply: replyText.trim() })
|
||||
const data: { reply: string; guestEmail?: string } = { reply: replyText.trim() }
|
||||
if (isQr && replyEmail.trim()) data.guestEmail = replyEmail.trim()
|
||||
const updated = await api.reviews.update(slug, id, data)
|
||||
setReviews(prev => prev.map(r => r.id === id ? updated : r))
|
||||
setReplyId(null)
|
||||
setReplyText('')
|
||||
@@ -921,16 +923,33 @@ export function ReviewsPage() {
|
||||
)}
|
||||
{replyId === review.id && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{review.source === 'qr' && (
|
||||
<input
|
||||
type="email"
|
||||
className="input text-sm w-full"
|
||||
placeholder="Email гостя (необязательно — для отправки ответа)"
|
||||
value={replyEmail}
|
||||
onChange={e => setReplyEmail(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
<textarea
|
||||
className="input resize-none text-sm w-full" rows={2}
|
||||
placeholder="Личный ответ гостю (отправится по email)..."
|
||||
placeholder={review.source === 'qr'
|
||||
? replyEmail ? 'Ответ отправится на указанный email...' : 'Внутренняя заметка (без email — не отправится гостю)...'
|
||||
: 'Личный ответ гостю (отправится по email)...'}
|
||||
value={replyText}
|
||||
onChange={e => setReplyText(e.target.value)}
|
||||
autoFocus
|
||||
autoFocus={review.source !== 'qr'}
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => sendReply(review.id)} className="btn-primary text-xs py-1.5">Отправить гостю</button>
|
||||
<button onClick={() => setReplyId(null)} className="btn-secondary text-xs py-1.5">Отмена</button>
|
||||
<button
|
||||
onClick={() => sendReply(review.id, review.source === 'qr')}
|
||||
className="btn-primary text-xs py-1.5"
|
||||
>
|
||||
{review.source === 'qr' && replyEmail ? 'Отправить на email' : review.source === 'qr' ? 'Сохранить заметку' : 'Отправить гостю'}
|
||||
</button>
|
||||
<button onClick={() => { setReplyId(null); setReplyEmail('') }} className="btn-secondary text-xs py-1.5">Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -939,10 +958,10 @@ export function ReviewsPage() {
|
||||
{!review.isPublic && !review.reply && !review.rejectedAt && replyId !== review.id && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => { setReplyId(review.id); setReplyText('') }}
|
||||
onClick={() => { setReplyId(review.id); setReplyText(''); setReplyEmail('') }}
|
||||
className="flex items-center gap-1 px-3 py-1.5 rounded-lg bg-brand-600 text-white text-xs font-medium hover:bg-brand-700 transition-colors"
|
||||
>
|
||||
<MessageSquare size={12} />Ответить гостю
|
||||
<MessageSquare size={12} />{review.source === 'qr' ? 'Добавить заметку / ответить' : 'Ответить гостю'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => reject(review.id)}
|
||||
|
||||
Reference in New Issue
Block a user