diff --git a/backend/custom_templates.json b/backend/custom_templates.json index 9e26dfe..6afe969 100644 --- a/backend/custom_templates.json +++ b/backend/custom_templates.json @@ -1 +1,32 @@ -{} \ No newline at end of file +{ + "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" + } + } +} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 1fcbe3b..24c0285 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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() + 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( diff --git a/backend/schemas.py b/backend/schemas.py index fa2bf0f..b4cf410 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -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 diff --git a/backend/whatsapp_templates.py b/backend/whatsapp_templates.py index 473ae2b..00ade83 100644 --- a/backend/whatsapp_templates.py +++ b/backend/whatsapp_templates.py @@ -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", ""), } diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index 843535a..e04616a 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -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 } diff --git a/frontend/src/components/EventForm.css b/frontend/src/components/EventForm.css index ae81831..f9f8758 100644 --- a/frontend/src/components/EventForm.css +++ b/frontend/src/components/EventForm.css @@ -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 { diff --git a/frontend/src/components/GuestList.jsx b/frontend/src/components/GuestList.jsx index 7e65cb0..374051b 100644 --- a/frontend/src/components/GuestList.jsx +++ b/frontend/src/components/GuestList.jsx @@ -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 diff --git a/frontend/src/components/TemplateEditor.jsx b/frontend/src/components/TemplateEditor.jsx index 3b3fe23..9405678 100644 --- a/frontend/src/components/TemplateEditor.jsx +++ b/frontend/src/components/TemplateEditor.jsx @@ -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 }) {
👤 {he.autoGuest}
+ )} -