Set the dynamic fileds for each template
This commit is contained in:
parent
1fcfcd7ee4
commit
a0f0528477
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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", ""),
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user