Set the dynamic fileds for each template

This commit is contained in:
dvirlabs 2026-02-25 03:24:00 +02:00
parent 1fcfcd7ee4
commit a0f0528477
10 changed files with 332 additions and 261 deletions

View File

@ -1 +1,32 @@
{}
{
"wedding_invitation_by_vered": {
"meta_name": "wedding_invitation_by_vered",
"language_code": "he",
"friendly_name": "wedding_invitation_by_vered",
"description": "This template design be Vered",
"header_text": "",
"body_text": "היי {{1}},\nאנחנו שמחים ומתרגשים להזמין אותך לחתונה שלנו 🤍🥂\n\nנשמח מאוד לראותכם ביום {{2}} ה-{{3}} ב\"{{4}}\", {{5}}.\n\n{{6}} קבלת פנים 🍸\n{{7}} חופה 🤵🏻💍👰🏻‍♀️\n{{8}} ארוחה וריקודים 🕺🏻💃🏻\n\nמתרגשים לחגוג איתכם,\n{{9}} ו{{10}}\n👰🏻🤍🤵🏻♂",
"header_params": [],
"body_params": [
"אורח",
"שני",
"15/06",
"הרמוניה בגן",
"בכנות",
"18:15",
"19:15",
"20:00",
"ורד",
"דביר"
],
"fallbacks": {
"contact_name": "דוד",
"groom_name": "דוד",
"bride_name": "ורד",
"venue": "אולם הגן",
"event_date": "15/06",
"event_time": "18:30",
"guest_link": "https://invy.dvirlabs.com/guest"
}
}
}

View File

@ -796,14 +796,16 @@ async def send_wedding_invitation_bulk(
))
continue
# Format event details — form overrides take priority over DB values
# Build params — contact_name always comes from the guest record
guest_name = f"{guest.first_name} {guest.last_name}".strip() or guest.first_name or "חבר"
# Standard named params (built-in template keys) with DB fallbacks
partner1 = (request_body.partner1_name or event.partner1_name or "").strip()
partner2 = (request_body.partner2_name or event.partner2_name or "").strip()
venue = (request_body.venue or event.venue or event.location or "").strip()
event_time = (request_body.event_time or event.event_time or "").strip()
# Convert event_date: YYYY-MM-DD (from form input) → DD/MM, or use DB date
# Convert event_date YYYY-MM-DD → DD/MM if still in ISO format (backend fallback)
if request_body.event_date:
try:
from datetime import datetime as _dt
@ -814,23 +816,31 @@ async def send_wedding_invitation_bulk(
else:
event_date = event.date.strftime("%d/%m") if event.date else ""
# Build guest link
guest_link = (
request_body.guest_link
or event.guest_link
or f"https://invy.dvirlabs.com/guest?event={event_id}"
).strip()
result = await service.send_wedding_invitation(
params = {
"contact_name": guest_name, # always auto from guest
"groom_name": partner1,
"bride_name": partner2,
"venue": venue,
"event_date": event_date,
"event_time": event_time,
"guest_link": guest_link,
}
# Merge extra_params last so they fully override standard params
# (used by custom templates whose param keys differ from the built-in names)
if request_body.extra_params:
params.update(request_body.extra_params)
result = await service.send_by_template_key(
key=request_body.template_key or "wedding_invitation",
to_phone=to_phone,
guest_name=guest_name,
partner1_name=partner1,
partner2_name=partner2,
venue=venue,
event_date=event_date,
event_time=event_time,
guest_link=guest_link,
template_key=request_body.template_key,
params=params,
)
results.append(schemas.WhatsAppSendResult(

View File

@ -1,5 +1,5 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from typing import Optional, List, Dict
from datetime import datetime
from uuid import UUID
@ -190,6 +190,7 @@ class WhatsAppWeddingInviteRequest(BaseModel):
event_date: Optional[str] = None # YYYY-MM-DD or DD/MM
event_time: Optional[str] = None # HH:mm
guest_link: Optional[str] = None # RSVP link
extra_params: Optional[Dict[str, str]] = None # Custom/extra param values keyed by param name
class Config:
from_attributes = True

View File

@ -215,6 +215,8 @@ def list_templates_for_frontend() -> list:
"header_param_count": len(tpl["header_params"]),
"body_param_count": len(tpl["body_params"]),
"is_custom": key in custom_keys,
"body_params": tpl["body_params"],
"header_params": tpl["header_params"],
"body_text": tpl.get("body_text", ""),
"header_text": tpl.get("header_text", ""),
}

View File

@ -241,17 +241,19 @@ export const deleteWhatsAppTemplate = async (key) => {
return response.data
}
export const sendWhatsAppInvitationToGuests = async (eventId, guestIds, formData, templateKey = 'wedding_invitation') => {
export const sendWhatsAppInvitationToGuests = async (eventId, guestIds, formData, templateKey = 'wedding_invitation', extraParams = null) => {
const response = await api.post(`/events/${eventId}/whatsapp/invite`, {
guest_ids: guestIds,
template_key: templateKey,
// Form field overrides — take priority over DB values on the backend
// Standard named params — used by built-in templates (backend applies fallbacks)
partner1_name: formData?.partner1 || null,
partner2_name: formData?.partner2 || null,
venue: formData?.venue || null,
event_date: formData?.eventDate || null, // YYYY-MM-DD, backend converts to DD/MM
event_date: formData?.eventDate || null,
event_time: formData?.eventTime || null,
guest_link: formData?.guestLink || null,
// Custom / extra params — used by custom templates; overrides standard params
extra_params: extraParams || null,
})
return response.data
}

View File

@ -22,18 +22,19 @@
.event-form {
position: relative;
background: white;
border-radius: 8px;
background: var(--color-background-secondary);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
box-shadow: var(--shadow-heavy);
}
.event-form h2 {
margin-top: 0;
margin-bottom: 1.5rem;
color: #2c3e50;
color: var(--color-text);
font-size: 1.5rem;
}
@ -44,30 +45,39 @@
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: #2c3e50;
color: var(--color-text-secondary);
font-weight: 500;
font-size: 0.88rem;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
border: 1.5px solid var(--color-border);
border-radius: 6px;
font-size: 1rem;
font-family: inherit;
background: var(--color-background);
color: var(--color-text);
transition: border-color 0.15s, box-shadow 0.15s;
}
.form-group input::placeholder {
color: var(--color-text-light);
}
.form-group input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(82, 148, 255, 0.15);
}
.error-message {
background: #fee;
color: #c33;
background: var(--color-error-bg);
color: var(--color-danger);
border: 1px solid var(--color-danger);
padding: 0.75rem;
border-radius: 4px;
border-radius: 6px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
@ -77,6 +87,8 @@
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
padding-top: 1.25rem;
border-top: 1px solid var(--color-border);
}
.btn-cancel,
@ -91,21 +103,25 @@
}
.btn-cancel {
background: #ecf0f1;
color: #2c3e50;
background: var(--color-background-tertiary);
color: var(--color-text-secondary);
border: 1.5px solid var(--color-border);
}
.btn-cancel:hover:not(:disabled) {
background: #d5dbdb;
background: var(--color-border);
color: var(--color-text);
}
.btn-submit {
background: #3498db;
background: var(--color-primary);
color: white;
}
.btn-submit:hover:not(:disabled) {
background: #2980b9;
background: var(--color-primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-medium);
}
.btn-submit:disabled {

View File

@ -281,7 +281,8 @@ function GuestList({ eventId, onBack, onShowMembers }) {
eventId,
Array.from(selectedGuestIds),
data.formData,
data.templateKey || 'wedding_invitation'
data.templateKey || 'wedding_invitation',
data.extraParams || null
)
// Clear selection after successful send

View File

@ -61,7 +61,9 @@ function renderPreview(text, paramKeys) {
if (!text) return ''
return text.replace(/\{\{(\d+)\}\}/g, (_m, n) => {
const key = paramKeys[parseInt(n, 10) - 1]
return key ? (SAMPLE_MAP[key] || `[${key}]`) : `{{${n}}}`
if (!key) return `{{${n}}}`
// Known built-in key use sample value; custom key show the key name itself
return SAMPLE_MAP[key] || key
})
}
@ -273,18 +275,31 @@ export default function TemplateEditor({ onBack }) {
<div className="te-card te-params-card">
<h3 className="te-card-title">{he.paramMapping}</h3>
<div className="te-param-table">
{/* Shared datalist for suggestions */}
<datalist id="te-param-suggestions">
{PARAM_OPTIONS.map(o => (
<option key={o.key} value={o.key} label={o.label} />
))}
</datalist>
{hNums.map((n, i) => (
<div key={`h${n}`} className="te-param-row">
<span className="te-param-badge header-badge">{he.headerParam} {`{{${n}}}`}</span>
<span className="te-param-arrow"></span>
<select value={headerParamKeys[i] || ''} disabled={saving}
<input
type="text"
list="te-param-suggestions"
value={headerParamKeys[i] || ''}
disabled={saving}
placeholder="שם הפרמטר (חופשי)"
onChange={e => setHPK(p => p.map((k, j) => j === i ? e.target.value : k))}
className="te-param-select">
<option value=""> בחר </option>
{PARAM_OPTIONS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
</select>
className="te-param-select"
dir="ltr"
/>
<span className="te-param-sample">
{headerParamKeys[i] ? SAMPLE_MAP[headerParamKeys[i]] : ''}
{headerParamKeys[i]
? (SAMPLE_MAP[headerParamKeys[i]] || headerParamKeys[i])
: ''}
</span>
</div>
))}
@ -292,14 +307,20 @@ export default function TemplateEditor({ onBack }) {
<div key={`b${n}`} className="te-param-row">
<span className="te-param-badge body-badge">{he.bodyParam} {`{{${n}}}`}</span>
<span className="te-param-arrow"></span>
<select value={bodyParamKeys[i] || ''} disabled={saving}
<input
type="text"
list="te-param-suggestions"
value={bodyParamKeys[i] || ''}
disabled={saving}
placeholder="שם הפרמטר (חופשי)"
onChange={e => setBPK(p => p.map((k, j) => j === i ? e.target.value : k))}
className="te-param-select">
<option value=""> בחר </option>
{PARAM_OPTIONS.map(o => <option key={o.key} value={o.key}>{o.label}</option>)}
</select>
className="te-param-select"
dir="ltr"
/>
<span className="te-param-sample">
{bodyParamKeys[i] ? SAMPLE_MAP[bodyParamKeys[i]] : ''}
{bodyParamKeys[i]
? (SAMPLE_MAP[bodyParamKeys[i]] || bodyParamKeys[i])
: ''}
</span>
</div>
))}

View File

@ -137,6 +137,37 @@
opacity: 0.6;
}
/* Dynamic params grid — 2 cols for wider fields, 1 col on mobile */
.dynamic-params-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 8px;
}
/* Date / time / URL inputs span full width */
.dynamic-params-grid .form-group:has(input[type="date"]),
.dynamic-params-grid .form-group:has(input[type="time"]),
.dynamic-params-grid .form-group:has(input[type="url"]) {
grid-column: span 1;
}
.auto-param-note {
font-size: 0.82rem;
color: var(--color-text-secondary);
margin-bottom: 10px;
padding: 6px 10px;
background: var(--color-background-tertiary);
border-radius: 6px;
border-right: 3px solid var(--color-primary);
}
@media (max-width: 520px) {
.dynamic-params-grid {
grid-template-columns: 1fr;
}
}
/* Message Preview */
.message-preview {
background: var(--color-background-secondary);

View File

@ -1,47 +1,66 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useMemo } from 'react'
import { getWhatsAppTemplates, deleteWhatsAppTemplate } from '../api/api'
import './WhatsAppInviteModal.css'
// Known system parameter keys field definitions
// contact_name is always resolved per-guest on the backend; never shown as a field.
const SYSTEM_FIELDS = {
contact_name: null, // skip auto-filled from guest record
groom_name: { label: 'שם החתן', type: 'text', placeholder: 'דוד', required: true },
bride_name: { label: 'שם הכלה', type: 'text', placeholder: 'ורד', required: true },
venue: { label: 'שם האולם / מקום', type: 'text', placeholder: 'אולם הגן', required: true },
event_date: { label: 'תאריך האירוע', type: 'date', required: true },
event_time: { label: 'שעת ההתחלה (HH:mm)', type: 'time', required: true },
guest_link: { label: 'קישור RSVP', type: 'url', placeholder: 'https://...', required: false },
}
// Map system key eventData field to pre-fill from
const EVENT_PREFILL = {
groom_name: d => d?.partner1_name || '',
bride_name: d => d?.partner2_name || '',
venue: d => d?.venue || d?.location || '',
event_date: d => {
if (!d?.date) return ''
try { return new Date(d.date).toISOString().split('T')[0] } catch { return '' }
},
event_time: d => d?.event_time || '',
guest_link: d => d?.guest_link || '',
}
// Render a template's body_text replacing {{N}} with param values
function renderTemplatePreview(bodyText, bodyParams, params) {
if (!bodyText) return null
return bodyText.replace(/\{\{(\d+)\}\}/g, (_m, n) => {
const key = bodyParams?.[parseInt(n, 10) - 1]
if (!key || key === 'contact_name') return '[שם האורח]'
return params[key] || `[${key}]`
})
}
const he = {
title: 'שלח הזמנה בוואטסאפ',
templateLabel: 'סוג הודעה',
templateLoading: '...טוען תבניות',
partners: 'שמות בני הזוג',
partner1Name: 'שם החתן',
partner2Name: 'שם הכלה',
venue: 'שם האולם/מקום',
eventDate: 'תאריך האירוע',
eventTime: 'שעת ההתחלה (HH:mm)',
guestLink: 'קישור RSVP',
selectedGuests: 'אורחים שנבחרו',
guestCount: '{count} אורחים',
allFields: 'יש למלא את כל השדות החובה',
noPhone: 'אין טלפון',
noPhones: 'לא נבחר אורח עם טלפון',
allFields: 'יש למלא את כל השדות החובה',
sending: 'שולח הזמנות...',
send: 'שלח הזמנות',
cancel: 'ביטול',
close: 'סגור',
results: 'תוצאות שליחה',
succeeded: 'התוצאות הצליחו',
succeeded: צליחו',
failed: 'נכשלו',
success: 'הצליח',
error: 'שגיאה',
preview: 'תצוגה מקדימה של ההודעה',
guestFirstName: 'שם האורח',
backToList: 'חזור לרשימה'
autoGuest: '(שם האורח ממולא אוטומטית)',
paramsSection: 'פרמטרי ההודעה',
}
function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData = {}, onSend }) {
const [formData, setFormData] = useState({
partner1: '',
partner2: '',
venue: '',
eventDate: '',
eventTime: '',
guestLink: ''
})
const [params, setParams] = useState({})
const [templates, setTemplates] = useState([])
const [selectedTemplateKey, setSelectedTemplateKey] = useState('wedding_invitation')
const [templatesLoading, setTemplatesLoading] = useState(false)
@ -49,26 +68,7 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
const [results, setResults] = useState(null)
const [showResults, setShowResults] = useState(false)
// Initialize form with event data
useEffect(() => {
if (eventData) {
let eventDate = ''
if (eventData.date) {
const dateObj = new Date(eventData.date)
eventDate = dateObj.toISOString().split('T')[0]
}
setFormData({
partner1: eventData.partner1_name || '',
partner2: eventData.partner2_name || '',
venue: eventData.venue || eventData.location || '',
eventDate: eventDate,
eventTime: eventData.event_time || '',
guestLink: eventData.guest_link || ''
})
}
}, [eventData, isOpen])
// Fetch available templates when modal opens (or after saving a new template)
// Fetch templates when modal opens
const fetchTemplates = () => {
setTemplatesLoading(true)
getWhatsAppTemplates()
@ -78,14 +78,41 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
setSelectedTemplateKey(data.templates[0].key)
}
})
.catch(err => console.error('Failed to load templates:', err))
.catch(console.error)
.finally(() => setTemplatesLoading(false))
}
useEffect(() => { if (isOpen) fetchTemplates() }, [isOpen])
// Derive selected template object
const selectedTemplate = useMemo(
() => templates.find(t => t.key === selectedTemplateKey) || null,
[templates, selectedTemplateKey]
)
// Unique param keys for this template (header + body, deduplicated, skip contact_name)
const paramKeys = useMemo(() => {
if (!selectedTemplate) return []
const all = [
...(selectedTemplate.header_params || []),
...(selectedTemplate.body_params || []),
]
const seen = new Set()
return all.filter(k => {
if (k === 'contact_name' || seen.has(k)) return false
seen.add(k); return true
})
}, [selectedTemplate])
// Re-init params whenever template or eventData changes
useEffect(() => {
if (!isOpen) return
fetchTemplates()
}, [isOpen])
const initial = {}
for (const key of paramKeys) {
const prefill = EVENT_PREFILL[key]
initial[key] = prefill ? prefill(eventData) : ''
}
setParams(initial)
}, [selectedTemplateKey, paramKeys.join(','), isOpen])
const handleDeleteTemplate = async (key) => {
if (!window.confirm(`האם למחוק את התבנית "${key}"? פעולה זו אינה הפיכה.`)) return
@ -98,43 +125,51 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
}
}
const handleInputChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
}
const validateForm = () => {
// Check required fields
if (!formData.partner1 || !formData.partner2 || !formData.venue || !formData.eventDate || !formData.eventTime) {
alert(he.allFields)
const hasPhones = selectedGuests.some(g => g.phone_number || g.phone)
if (!hasPhones) { alert(he.noPhones); return false }
for (const key of paramKeys) {
const sysDef = SYSTEM_FIELDS[key]
const isRequired = sysDef ? sysDef.required : true // custom keys are required
if (isRequired && !params[key]?.trim()) {
const label = sysDef ? sysDef.label : key
alert(`יש למלא: ${label}`)
return false
}
// Check if any selected guest has a phone
const hasPhones = selectedGuests.some(guest => guest.phone_number || guest.phone)
if (!hasPhones) {
alert(he.noPhones)
return false
}
return true
}
const handleSend = async () => {
if (!validateForm()) return
setSending(true)
setResults(null)
setSending(true); setResults(null)
try {
if (onSend) {
// Build extra_params: convert event_date to DD/MM if in YYYY-MM-DD format
const extraParams = { ...params }
if (extraParams.event_date) {
try {
const [y, m, d] = extraParams.event_date.split('-')
if (y && m && d) extraParams.event_date = `${d}/${m}`
} catch {}
}
// Also provide legacy formData for backward compat
const formData = {
partner1: params.groom_name || '',
partner2: params.bride_name || '',
venue: params.venue || '',
eventDate: params.event_date || '',
eventTime: params.event_time || '',
guestLink: params.guest_link || '',
}
const result = await onSend({
formData,
guestIds: selectedGuests.map(g => g.id),
templateKey: selectedTemplateKey,
extraParams,
})
setResults(result)
setShowResults(true)
@ -158,21 +193,16 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
}
}
const handleClose = () => {
setResults(null)
setShowResults(false)
onClose()
}
const handleClose = () => { setResults(null); setShowResults(false); onClose() }
if (!isOpen) return null
// Show results screen
// Results screen
if (showResults && results) {
return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal-content whatsapp-modal" onClick={e => e.stopPropagation()}>
<h2>{he.results}</h2>
<div className="results-summary">
<div className="result-stat success">
<div className="stat-value">{results.succeeded}</div>
@ -183,44 +213,39 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
<div className="stat-label">{he.failed}</div>
</div>
</div>
<div className="results-list">
{results.results.map((result, idx) => (
<div key={idx} className={`result-item ${result.status}`}>
{results.results.map((r, idx) => (
<div key={idx} className={`result-item ${r.status}`}>
<div className="result-header">
<span className="result-name">{result.guest_name}</span>
<span className={`result-status ${result.status}`}>
{result.status === 'sent' ? he.success : he.error}
</span>
<span className="result-name">{r.guest_name}</span>
<span className={`result-status ${r.status}`}>{r.status === 'sent' ? he.success : he.error}</span>
</div>
<div className="result-phone">{result.phone}</div>
{result.error && (
<div className="result-error">{result.error}</div>
)}
<div className="result-phone">{r.phone}</div>
{r.error && <div className="result-error">{r.error}</div>}
</div>
))}
</div>
<div className="modal-buttons">
<button
className="btn-primary"
onClick={handleClose}
>
{he.close}
</button>
<button className="btn-primary" onClick={handleClose}>{he.close}</button>
</div>
</div>
</div>
)
}
// Show form screen
// Form screen
const previewText = renderTemplatePreview(
selectedTemplate?.body_text,
selectedTemplate?.body_params,
params
)
return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal-content whatsapp-modal" onClick={e => e.stopPropagation()}>
<h2>{he.title}</h2>
{/* Template selector */}
{/* ── Template selector ── */}
<div className="form-section template-selector">
<div className="form-group">
<div className="template-label-row">
@ -245,30 +270,25 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
</option>
))}
</select>
{templates.find(t => t.key === selectedTemplateKey)?.is_custom && (
{selectedTemplate?.is_custom && (
<button
className="btn-delete-template"
onClick={() => handleDeleteTemplate(selectedTemplateKey)}
disabled={sending}
title="מחק תבנית מותאמת"
>
🗑
</button>
>🗑</button>
)}
</div>
)}
{templates.find(t => t.key === selectedTemplateKey)?.description && (
<small className="template-description">
{templates.find(t => t.key === selectedTemplateKey).description}
</small>
{selectedTemplate?.description && (
<small className="template-description">{selectedTemplate.description}</small>
)}
</div>
</div>
{/* ── Guests list ── */}
<div className="guests-preview">
<div className="preview-header">
{he.selectedGuests} ({selectedGuests.length})
</div>
<div className="preview-header">{he.selectedGuests} ({selectedGuests.length})</div>
<div className="guests-list">
{selectedGuests.map((guest, idx) => (
<div key={idx} className="guest-item">
@ -281,124 +301,60 @@ function WhatsAppInviteModal({ isOpen, onClose, selectedGuests = [], eventData =
</div>
</div>
{/* Form */}
{/* ── Dynamic param form ── */}
<div className="whatsapp-form">
<div className="form-section">
<h3>{he.partners}</h3>
<div className="form-row">
<div className="form-group">
<label>{he.partner1Name} *</label>
<h3>{he.paramsSection}</h3>
{/* contact_name note */}
{(selectedTemplate?.header_params?.includes('contact_name') ||
selectedTemplate?.body_params?.includes('contact_name')) && (
<p className="auto-param-note">👤 {he.autoGuest}</p>
)}
<div className="dynamic-params-grid">
{paramKeys.map(key => {
const sysDef = SYSTEM_FIELDS[key]
if (sysDef === null) return null // explicitly skip (contact_name)
const label = sysDef?.label || key
const inputType = sysDef?.type || 'text'
const placeholder = sysDef?.placeholder || ''
const required = sysDef ? sysDef.required : true
return (
<div key={key} className="form-group">
<label>{label}{required ? ' *' : ''}</label>
<input
type="text"
name="partner1"
value={formData.partner1}
onChange={handleInputChange}
placeholder="דוד"
type={inputType}
value={params[key] || ''}
onChange={e => setParams(p => ({ ...p, [key]: e.target.value }))}
placeholder={placeholder}
disabled={sending}
dir={inputType === 'text' || inputType === 'url' ? 'rtl' : undefined}
/>
</div>
<div className="form-group">
<label>{he.partner2Name} *</label>
<input
type="text"
name="partner2"
value={formData.partner2}
onChange={handleInputChange}
placeholder="וורד"
disabled={sending}
/>
)
})}
</div>
</div>
</div>
<div className="form-section">
<div className="form-group">
<label>{he.venue} *</label>
<input
type="text"
name="venue"
value={formData.venue}
onChange={handleInputChange}
placeholder="אולם כלות..."
disabled={sending}
/>
</div>
</div>
<div className="form-section">
<div className="form-row">
<div className="form-group">
<label>{he.eventDate} *</label>
<input
type="date"
name="eventDate"
value={formData.eventDate}
onChange={handleInputChange}
disabled={sending}
/>
</div>
<div className="form-group">
<label>{he.eventTime} *</label>
<input
type="time"
name="eventTime"
value={formData.eventTime}
onChange={handleInputChange}
disabled={sending}
/>
</div>
</div>
</div>
<div className="form-section">
<div className="form-group">
<label>{he.guestLink}</label>
<input
type="url"
name="guestLink"
value={formData.guestLink}
onChange={handleInputChange}
placeholder="https://invy.example.com/guest?event=..."
disabled={sending}
/>
</div>
</div>
</div>
{/* Message Preview */}
{/* ── Message preview ── */}
<div className="message-preview">
<div className="preview-title">{he.preview}</div>
<div className="preview-content">
{`היי ${formData.partner1 ? formData.partner1 : he.guestFirstName} 🤍
זה קורה! 🎉
${formData.partner1} ו-${formData.partner2} מתחתנים ונשמח שתהיה/י איתנו ברגע המיוחד הזה
📍 האולם: "${formData.venue}"
📅 התאריך: ${formData.eventDate ? (() => { try { const [y,m,d] = formData.eventDate.split('-'); return `${d}/${m}` } catch { return formData.eventDate } })() : ''}
🕒 השעה: ${formData.eventTime || '—'}
לאישור הגעה ופרטים נוספים:
${formData.guestLink || '[קישור RSVP]'}
מתרגשים ומצפים לראותך 💞`}
{previewText
? previewText
: (selectedTemplate?.body_text || '— בחר תבנית —')}
</div>
</div>
{/* Buttons */}
{/* ── Buttons ── */}
<div className="modal-buttons">
<button
className="btn-primary"
onClick={handleSend}
disabled={sending}
>
<button className="btn-primary" onClick={handleSend} disabled={sending}>
{sending ? he.sending : he.send}
</button>
<button
className="btn-secondary"
onClick={handleClose}
disabled={sending}
>
<button className="btn-secondary" onClick={handleClose} disabled={sending}>
{he.cancel}
</button>
</div>