diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index 5a90230..4031c69 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -3486,13 +3486,26 @@ def _form_to_product(form) -> dict: pros = json.dumps([line.strip() for line in pros_raw.splitlines() if line.strip()]) cons = json.dumps([line.strip() for line in cons_raw.splitlines() if line.strip()]) + # Program-based URL vs manual URL. + # When a program is selected, product_identifier holds the ASIN/path; + # affiliate_url is cleared. Manual mode is the reverse. + program_id_str = form.get("program_id", "").strip() + program_id = int(program_id_str) if program_id_str and program_id_str != "0" else None + product_identifier = form.get("product_identifier", "").strip() + affiliate_url = form.get("affiliate_url", "").strip() + + # retailer is auto-populated from program name on save (kept for display/filter) + retailer = form.get("retailer", "").strip() + return { "slug": form.get("slug", "").strip(), "name": form.get("name", "").strip(), "brand": form.get("brand", "").strip(), "category": form.get("category", "accessory").strip(), - "retailer": form.get("retailer", "").strip(), - "affiliate_url": form.get("affiliate_url", "").strip(), + "retailer": retailer, + "program_id": program_id, + "product_identifier": product_identifier, + "affiliate_url": affiliate_url, "image_url": form.get("image_url", "").strip(), "price_cents": price_cents, "currency": "EUR", @@ -3610,14 +3623,15 @@ async def affiliate_preview(): @csrf_protect async def affiliate_new(): """Create an affiliate product.""" - from ..affiliate import get_distinct_retailers + from ..affiliate import get_all_programs, get_distinct_retailers if request.method == "POST": form = await request.form data = _form_to_product(form) - if not data["slug"] or not data["name"] or not data["affiliate_url"]: - await flash("Slug, name, and affiliate URL are required.", "error") + has_url = bool(data["program_id"] and data["product_identifier"]) or bool(data["affiliate_url"]) + if not data["slug"] or not data["name"] or not has_url: + await flash("Slug, name, and either a program+product ID or manual URL are required.", "error") return await render_template( "admin/affiliate_form.html", admin_page="affiliate", @@ -3626,6 +3640,7 @@ async def affiliate_new(): categories=AFFILIATE_CATEGORIES, statuses=AFFILIATE_STATUSES, retailers=await get_distinct_retailers(), + programs=await get_all_programs(status="active"), ) existing = await fetch_one( @@ -3642,17 +3657,27 @@ async def affiliate_new(): categories=AFFILIATE_CATEGORIES, statuses=AFFILIATE_STATUSES, retailers=await get_distinct_retailers(), + programs=await get_all_programs(status="active"), ) + # Auto-populate retailer from program name if not manually set + if data["program_id"] and not data["retailer"]: + prog = await fetch_one( + "SELECT name FROM affiliate_programs WHERE id = ?", (data["program_id"],) + ) + if prog: + data["retailer"] = prog["name"] + await execute( """INSERT INTO affiliate_products - (slug, name, brand, category, retailer, affiliate_url, image_url, - price_cents, currency, rating, pros, cons, description, cta_label, - status, language, sort_order) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (slug, name, brand, category, retailer, program_id, product_identifier, + affiliate_url, image_url, price_cents, currency, rating, pros, cons, + description, cta_label, status, language, sort_order) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( data["slug"], data["name"], data["brand"], data["category"], - data["retailer"], data["affiliate_url"], data["image_url"], + data["retailer"], data["program_id"], data["product_identifier"], + data["affiliate_url"], data["image_url"], data["price_cents"], data["currency"], data["rating"], data["pros"], data["cons"], data["description"], data["cta_label"], data["status"], data["language"], data["sort_order"], @@ -3669,6 +3694,7 @@ async def affiliate_new(): categories=AFFILIATE_CATEGORIES, statuses=AFFILIATE_STATUSES, retailers=await get_distinct_retailers(), + programs=await get_all_programs(status="active"), ) @@ -3677,7 +3703,7 @@ async def affiliate_new(): @csrf_protect async def affiliate_edit(product_id: int): """Edit an affiliate product.""" - from ..affiliate import get_distinct_retailers + from ..affiliate import get_all_programs, get_distinct_retailers product = await fetch_one("SELECT * FROM affiliate_products WHERE id = ?", (product_id,)) if not product: @@ -3688,8 +3714,9 @@ async def affiliate_edit(product_id: int): form = await request.form data = _form_to_product(form) - if not data["slug"] or not data["name"] or not data["affiliate_url"]: - await flash("Slug, name, and affiliate URL are required.", "error") + has_url = bool(data["program_id"] and data["product_identifier"]) or bool(data["affiliate_url"]) + if not data["slug"] or not data["name"] or not has_url: + await flash("Slug, name, and either a program+product ID or manual URL are required.", "error") return await render_template( "admin/affiliate_form.html", admin_page="affiliate", @@ -3699,6 +3726,7 @@ async def affiliate_edit(product_id: int): categories=AFFILIATE_CATEGORIES, statuses=AFFILIATE_STATUSES, retailers=await get_distinct_retailers(), + programs=await get_all_programs(status="active"), ) # Check slug collision only if slug or language changed @@ -3718,18 +3746,29 @@ async def affiliate_edit(product_id: int): categories=AFFILIATE_CATEGORIES, statuses=AFFILIATE_STATUSES, retailers=await get_distinct_retailers(), + programs=await get_all_programs(status="active"), ) + # Auto-populate retailer from program name if not manually set + if data["program_id"] and not data["retailer"]: + prog = await fetch_one( + "SELECT name FROM affiliate_programs WHERE id = ?", (data["program_id"],) + ) + if prog: + data["retailer"] = prog["name"] + await execute( """UPDATE affiliate_products - SET slug=?, name=?, brand=?, category=?, retailer=?, affiliate_url=?, - image_url=?, price_cents=?, currency=?, rating=?, pros=?, cons=?, + SET slug=?, name=?, brand=?, category=?, retailer=?, program_id=?, + product_identifier=?, affiliate_url=?, image_url=?, + price_cents=?, currency=?, rating=?, pros=?, cons=?, description=?, cta_label=?, status=?, language=?, sort_order=?, updated_at=datetime('now') WHERE id=?""", ( data["slug"], data["name"], data["brand"], data["category"], - data["retailer"], data["affiliate_url"], data["image_url"], + data["retailer"], data["program_id"], data["product_identifier"], + data["affiliate_url"], data["image_url"], data["price_cents"], data["currency"], data["rating"], data["pros"], data["cons"], data["description"], data["cta_label"], data["status"], data["language"], data["sort_order"], @@ -3761,6 +3800,7 @@ async def affiliate_edit(product_id: int): categories=AFFILIATE_CATEGORIES, statuses=AFFILIATE_STATUSES, retailers=await get_distinct_retailers(), + programs=await get_all_programs(status="active"), ) diff --git a/web/src/padelnomics/admin/templates/admin/affiliate_form.html b/web/src/padelnomics/admin/templates/admin/affiliate_form.html index 42ea4bf..9acd571 100644 --- a/web/src/padelnomics/admin/templates/admin/affiliate_form.html +++ b/web/src/padelnomics/admin/templates/admin/affiliate_form.html @@ -24,6 +24,20 @@ document.addEventListener('DOMContentLoaded', function() { slugInput.dataset.manual = '1'; }); } + + // Toggle program-based vs manual URL fields + function toggleProgramFields() { + var sel = document.getElementById('f-program'); + if (!sel) return; + var isManual = sel.value === '0' || sel.value === ''; + document.getElementById('f-product-id-row').style.display = isManual ? 'none' : ''; + document.getElementById('f-manual-url-row').style.display = isManual ? '' : 'none'; + } + var programSel = document.getElementById('f-program'); + if (programSel) { + programSel.addEventListener('change', toggleProgramFields); + toggleProgramFields(); + } }); {% endblock %} @@ -87,9 +101,38 @@ document.addEventListener('DOMContentLoaded', function() { - {# Retailer #} + {# Program dropdown #}
Select a program to auto-build the URL, or choose Manual for a custom link.
+ASIN, product path, or other program-specific identifier. URL is assembled at redirect time.
+Full URL with tracking params already baked in. Used as fallback if no program is set.
+Full URL with tracking params already baked in.
-