diff --git a/backend/main.py b/backend/main.py index 410b2e7..9164ea8 100644 --- a/backend/main.py +++ b/backend/main.py @@ -687,10 +687,15 @@ async def create_whatsapp_template( "meta_name": "my_template", # exact name in Meta BM "language_code": "he", "description": "optional description", - "header_text": "היי {{1}}", # raw text (for preview) + "header_type": "TEXT" or "IMAGE", # header type + "header_text": "היי {{1}}", # raw text (for preview, TEXT headers) + "header_handle": "https://...", # media URL or handle (IMAGE headers) "body_text": "{{1}} ו-{{2}} ...", # raw text (for preview) "header_param_keys": ["contact_name"], # ordered param keys for header {{N}} "body_param_keys": ["groom_name", "bride_name", ...], + "button_type": "URL", # optional button type + "button_text": "Visit Website", # optional button label + "button_url": "https://...", # optional button URL "fallbacks": { "contact_name": "חבר", ... } } """ @@ -710,10 +715,15 @@ async def create_whatsapp_template( "language_code": body.get("language_code", "he"), "friendly_name": body["friendly_name"], "description": body.get("description", ""), + "header_type": body.get("header_type", "TEXT"), "header_text": body.get("header_text", ""), + "header_handle": body.get("header_handle", ""), "body_text": body.get("body_text", ""), "header_params": body.get("header_param_keys", []), "body_params": body.get("body_param_keys", []), + "button_type": body.get("button_type", ""), + "button_text": body.get("button_text", ""), + "button_url": body.get("button_url", ""), "fallbacks": body.get("fallbacks", {}), "guest_name_key": body.get("guest_name_key", ""), } diff --git a/backend/whatsapp.py b/backend/whatsapp.py index 7d2eb48..81445f1 100644 --- a/backend/whatsapp.py +++ b/backend/whatsapp.py @@ -441,6 +441,10 @@ class WhatsAppService: tpl = get_template(template_key) meta_name = tpl["meta_name"] language_code = tpl.get("language_code", "he") + header_type = tpl.get("header_type", "TEXT") + header_handle = tpl.get("header_handle", "") + button_type = tpl.get("button_type", "") + button_url = tpl.get("button_url", "") header_values, body_values = build_params_list(template_key, params) @@ -449,18 +453,54 @@ class WhatsAppService: raise WhatsAppError(f"Invalid phone number: {to_phone}") components = [] - if header_values: + + # Build header component based on type + if header_type == "IMAGE" and header_handle: + components.append({ + "type": "header", + "parameters": [{ + "type": "image", + "image": {"link": header_handle} + }], + }) + elif header_type == "VIDEO" and header_handle: + components.append({ + "type": "header", + "parameters": [{ + "type": "video", + "video": {"link": header_handle} + }], + }) + elif header_type == "DOCUMENT" and header_handle: + components.append({ + "type": "header", + "parameters": [{ + "type": "document", + "document": {"link": header_handle} + }], + }) + elif header_type == "TEXT" and header_values: components.append({ "type": "header", "parameters": [{"type": "text", "text": str(v)} for v in header_values], }) + + # Build body component if body_values: components.append({ "type": "body", "parameters": [{"type": "text", "text": str(v)} for v in body_values], }) - # Handle url_button component if defined in template + # Handle static URL button + if button_type == "URL" and button_url: + # For URL buttons with dynamic suffixes, extract the dynamic part from params + # URL buttons in Meta templates can have a static prefix and dynamic suffix + # Example: https://example.com/{1} where {1} is replaced by a param + # If no dynamic param, the button is purely static (no parameters needed) + pass # Static URL buttons don't need parameters in the API call + + # Handle url_button component if defined in template (legacy dynamic buttons) url_btn = tpl.get("url_button", {}) if url_btn and url_btn.get("enabled"): param_key = url_btn.get("param_key", "event_id") @@ -487,7 +527,7 @@ class WhatsAppService: import json logger.info( f"[WhatsApp] send_by_template_key '{template_key}' → meta='{meta_name}' " - f"lang={language_code} to={to_e164} " + f"lang={language_code} to={to_e164} header_type={header_type} " f"header_params={header_values} body_params={body_values}" ) logger.debug( diff --git a/backend/whatsapp_templates.py b/backend/whatsapp_templates.py index f545ccf..dca4f96 100644 --- a/backend/whatsapp_templates.py +++ b/backend/whatsapp_templates.py @@ -10,9 +10,14 @@ How to add a new template: - language_code : he / he_IL / en / en_US … - friendly_name : shown in the frontend dropdown - description : optional, for documentation + - header_type : "TEXT" or "IMAGE" or "VIDEO" or "DOCUMENT" (default: "TEXT") - header_params : ordered list of variable keys sent in the HEADER component (empty list [] if the template has no header variables) + - header_handle : media handle for IMAGE/VIDEO/DOCUMENT headers (optional) - body_params : ordered list of variable keys sent in the BODY component + - button_type : "URL" or "PHONE_NUMBER" or "QUICK_REPLY" (optional) + - button_text : button label/text (optional) + - button_url : button URL (optional, for URL buttons) - fallbacks : dict {key: default_string} used when the caller doesn't provide a value for that key @@ -219,6 +224,11 @@ def list_templates_for_frontend() -> list: "header_params": tpl["header_params"], "body_text": tpl.get("body_text", ""), "header_text": tpl.get("header_text", ""), + "header_type": tpl.get("header_type", "TEXT"), + "header_handle": tpl.get("header_handle", ""), + "button_type": tpl.get("button_type", ""), + "button_text": tpl.get("button_text", ""), + "button_url": tpl.get("button_url", ""), "guest_name_key": tpl.get("guest_name_key", ""), "url_button": tpl.get("url_button", None), } diff --git a/frontend/src/components/TemplateEditor.css b/frontend/src/components/TemplateEditor.css index aa8935c..e176ed4 100644 --- a/frontend/src/components/TemplateEditor.css +++ b/frontend/src/components/TemplateEditor.css @@ -400,6 +400,62 @@ border-bottom-color: rgba(255,255,255,0.08); } +.te-bubble-header-media { + margin: -0.65rem -0.85rem 0.5rem -0.85rem; + border-radius: 10px 10px 0 0; + overflow: hidden; + max-height: 200px; +} + +.te-bubble-header-media img { + width: 100%; + height: auto; + display: block; + object-fit: cover; + max-height: 200px; +} + +.te-media-placeholder { + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%); + color: #999; + font-size: 2rem; + min-height: 120px; + padding: 2rem; +} + +[data-theme="dark"] .te-media-placeholder { + background: linear-gradient(135deg, #2a2d3d 0%, #1f2230 100%); + color: #666; +} + +.te-bubble-button { + margin: 0.6rem -0.35rem 0; + padding: 0.6rem; + text-align: center; + color: #0a7cff; + font-weight: 600; + font-size: 0.85rem; + border-top: 1px solid rgba(0,0,0,0.08); + cursor: pointer; + transition: background 0.15s; +} + +.te-bubble-button:hover { + background: rgba(0,0,0,0.03); +} + +[data-theme="dark"] .te-bubble-button { + border-top-color: rgba(255,255,255,0.08); + color: #4a9eff; +} + +[data-theme="dark"] .te-bubble-button:hover { + background: rgba(255,255,255,0.05); +} + .te-bubble-body { color: #333; white-space: pre-wrap; diff --git a/frontend/src/components/TemplateEditor.jsx b/frontend/src/components/TemplateEditor.jsx index dc86e89..942778d 100644 --- a/frontend/src/components/TemplateEditor.jsx +++ b/frontend/src/components/TemplateEditor.jsx @@ -29,8 +29,14 @@ const he = { description: 'תיאור', headerSection: 'כותרת (Header) — אופציונלי', bodySection: 'גוף ההודעה (Body)', + buttonSection: 'כפתור (Button) — אופציונלי', + headerType: 'סוג כותרת', headerText: 'טקסט הכותרת', + headerHandle: 'קישור/מזהה תמונה', bodyText: 'טקסט ההודעה', + buttonType: 'סוג כפתור', + buttonText: 'טקסט הכפתור', + buttonUrl: 'כתובת URL', paramMapping: 'מיפוי פרמטרים', preview: 'תצוגה מקדימה', save: 'שמור תבנית', @@ -41,7 +47,9 @@ const he = { builtIn: 'מובנת', chars: 'תווים', headerHint: 'השתמש ב-{{1}} לשם האורח — השאר ריק אם אין כותרת', + headerHandleHint: 'קישור לתמונה או מזהה Media שהועלה ל-Meta', bodyHint: 'השתמש ב-{{1}}, {{2}}, ... לפרמטרים — בדיוק כמו ב-Meta', + buttonUrlHint: 'כתובת URL מלאה, לדוגמה: https://invy.dvirlabs.com', keyHint: 'אותיות קטנות, מספרים ו-_ בלבד', metaHint: 'שם מדויק כפי שאושר ב-Meta Business Manager', saved: '✓ התבנית נשמרה בהצלחה!', @@ -73,7 +81,9 @@ function renderPreview(text, paramKeys) { const EMPTY_FORM = { key: '', friendlyName: '', metaName: '', language: 'he', description: '', - headerText: '', bodyText: '', + headerType: 'TEXT', headerText: '', headerHandle: '', + bodyText: '', + buttonType: '', buttonText: '', buttonUrl: '', } export default function TemplateEditor({ onBack }) { @@ -160,8 +170,13 @@ export default function TemplateEditor({ onBack }) { metaName: tpl.meta_name, language: tpl.language_code || 'he', description: tpl.description || '', + headerType: tpl.header_type || 'TEXT', headerText: tpl.header_text || '', + headerHandle: tpl.header_handle || '', bodyText: tpl.body_text || '', + buttonType: tpl.button_type || '', + buttonText: tpl.button_text || '', + buttonUrl: tpl.button_url || '', }) setEditMode(true) setEditingKey(tpl.key) @@ -189,10 +204,15 @@ export default function TemplateEditor({ onBack }) { meta_name: form.metaName.trim(), language_code: form.language, description: form.description.trim(), + header_type: form.headerType, header_text: form.headerText.trim(), + header_handle: form.headerHandle.trim(), body_text: form.bodyText.trim(), header_param_keys: headerParamKeys, body_param_keys: bodyParamKeys, + button_type: form.buttonType, + button_text: form.buttonText.trim(), + button_url: form.buttonUrl.trim(), fallbacks: Object.fromEntries(PARAM_OPTIONS.map(o => [o.key, o.sample])), guest_name_key: guestNameKey, }) @@ -293,15 +313,35 @@ export default function TemplateEditor({ onBack }) {