Avisut commited on
Commit
36f4962
·
verified ·
1 Parent(s): 1b2973d

Update media_plan_templater.py

Browse files
Files changed (1) hide show
  1. media_plan_templater.py +213 -0
media_plan_templater.py CHANGED
@@ -344,6 +344,219 @@ def render(get_conn):
344
 
345
  package_opts = opts["packages"]
346
  rate_opts = opts["rates"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
  # Column config (Product column labeled as Package; Placement shown as text in grid)
349
  column_config = {
 
344
 
345
  package_opts = opts["packages"]
346
  rate_opts = opts["rates"]
347
+ # ─────────────────────────────────────────────────────────────────────────────
348
+ # Guided 5-row entry (independent controls, then write into the table)
349
+ # ─────────────────────────────────────────────────────────────────────────────
350
+ st.subheader("Guided entry (5 rows)")
351
+ st.caption("Select Package → Placement (dependent), pick Targeting presets, add Budget/Rate, then click **Populate table**.")
352
+
353
+ # one-time init to hold the guided rows' state
354
+ if "guided_rows" not in st.session_state:
355
+ st.session_state["guided_rows"] = [
356
+ {
357
+ "Media plan": "",
358
+ "Budget": None,
359
+ "Rate": rate_opts[0] if rate_opts else "",
360
+ "Product": "", # Package
361
+ "Placement": "",
362
+ "Details": "",
363
+ "Targeting": [], # list for multiselect, will stringify as "A OR B"
364
+ "Notes": "",
365
+ }
366
+ for _ in range(DEFAULT_INPUT_ROWS)
367
+ ]
368
+
369
+ def _placements_for(pkg: str) -> list[str]:
370
+ pkg = (pkg or "").strip()
371
+ return opts["placementMap"].get(pkg, [])
372
+
373
+ def _details_for(pkg: str, placement: str) -> str:
374
+ # PACKAGE_EXIST → bullets from extraction; else chosen placement
375
+ exists = opts["packageExistsMap"].get((pkg or "").strip(), False)
376
+ if exists:
377
+ bullets = opts["extractionMap"].get((pkg or "").strip(), [])
378
+ return "\n".join(f"• {b}" for b in bullets) if bullets else ""
379
+ return (placement or "").strip()
380
+
381
+ with st.form("guided_form_5rows", clear_on_submit=False):
382
+ for i in range(DEFAULT_INPUT_ROWS):
383
+ row_state = st.session_state["guided_rows"][i]
384
+
385
+ st.markdown(f"**Row {i+1}**")
386
+ c1, c2, c3, c4 = st.columns([1.1, 1.1, 1.2, 1.2])
387
+ with c1:
388
+ row_state["Media plan"] = st.text_input(
389
+ "Media plan",
390
+ key=f"g_media_{i}",
391
+ value=row_state["Media plan"],
392
+ placeholder="e.g., MP-01",
393
+ )
394
+ with c2:
395
+ row_state["Budget"] = st.number_input(
396
+ "Budget",
397
+ key=f"g_budget_{i}",
398
+ value=0.0 if row_state["Budget"] in (None, "") else float(row_state["Budget"]),
399
+ min_value=0.0,
400
+ step=100.0,
401
+ format="%0.2f",
402
+ )
403
+ with c3:
404
+ row_state["Rate"] = st.selectbox(
405
+ "Rate",
406
+ options=rate_opts,
407
+ key=f"g_rate_{i}",
408
+ index=max(0, rate_opts.index(row_state["Rate"])) if (row_state["Rate"] in rate_opts) else 0,
409
+ )
410
+ with c4:
411
+ row_state["Product"] = st.selectbox(
412
+ "Package",
413
+ options=[""] + package_opts,
414
+ key=f"g_pkg_{i}",
415
+ index=([""] + package_opts).index(row_state["Product"]) if row_state["Product"] in ([""] + package_opts) else 0,
416
+ help="Choose a Package first.",
417
+ )
418
+
419
+ # dependent Placement (disabled when PACKAGE_EXIST = yes)
420
+ pkg = row_state["Product"]
421
+ exists = opts["packageExistsMap"].get((pkg or "").strip(), False)
422
+ choices = _placements_for(pkg)
423
+
424
+ cc1, cc2 = st.columns([1.2, 2.0])
425
+ with cc1:
426
+ if exists:
427
+ # disable Placement when package is a bundle
428
+ st.selectbox(
429
+ "Placement",
430
+ options=["(package-defined elements; no placement selection)"],
431
+ index=0,
432
+ key=f"g_plc_disabled_{i}",
433
+ disabled=True,
434
+ help="This package defines its own elements.",
435
+ )
436
+ row_state["Placement"] = ""
437
+ else:
438
+ # allow selection when package is not a bundle
439
+ if choices:
440
+ default_ix = choices.index(row_state["Placement"]) if row_state["Placement"] in choices else 0
441
+ row_state["Placement"] = st.selectbox(
442
+ "Placement",
443
+ options=choices,
444
+ index=default_ix,
445
+ key=f"g_plc_{i}",
446
+ help="Placement options depend on the selected Package.",
447
+ )
448
+ else:
449
+ st.selectbox(
450
+ "Placement",
451
+ options=["(no placements for this package)"],
452
+ index=0,
453
+ key=f"g_plc_empty_{i}",
454
+ disabled=True,
455
+ )
456
+ row_state["Placement"] = ""
457
+
458
+ with cc2:
459
+ # Targeting multi-select (stores a list; will stringify later)
460
+ row_state["Targeting"] = st.multiselect(
461
+ "Targeting (presets)",
462
+ options=TARGETING_OPTIONS,
463
+ default=[t for t in (row_state["Targeting"] or []) if t in TARGETING_OPTIONS],
464
+ key=f"g_tgt_{i}",
465
+ help="Pick one or more. Will be saved as 'A OR B'.",
466
+ )
467
+
468
+ # Notes
469
+ row_state["Notes"] = st.text_input(
470
+ "Notes",
471
+ key=f"g_notes_{i}",
472
+ value=row_state["Notes"],
473
+ placeholder="Optional",
474
+ )
475
+
476
+ # Details preview (derived)
477
+ details_preview = _details_for(pkg, row_state["Placement"])
478
+ row_state["Details"] = details_preview
479
+ st.caption("**Details preview**")
480
+ st.text_area(
481
+ label=f"Details (auto)",
482
+ key=f"g_details_preview_{i}",
483
+ value=details_preview,
484
+ height=80,
485
+ disabled=True,
486
+ )
487
+ st.divider()
488
+
489
+ cleft, cright = st.columns([1, 1])
490
+ with cleft:
491
+ reset_guided = st.form_submit_button("Reset guided rows")
492
+ with cright:
493
+ populate = st.form_submit_button("Populate table")
494
+
495
+ if reset_guided:
496
+ st.session_state["guided_rows"] = [
497
+ {
498
+ "Media plan": "",
499
+ "Budget": None,
500
+ "Rate": rate_opts[0] if rate_opts else "",
501
+ "Product": "",
502
+ "Placement": "",
503
+ "Details": "",
504
+ "Targeting": [],
505
+ "Notes": "",
506
+ }
507
+ for _ in range(DEFAULT_INPUT_ROWS)
508
+ ]
509
+ st.experimental_rerun()
510
+
511
+ if populate:
512
+ # Validate rows & build DataFrame
513
+ issues = []
514
+ out_rows = []
515
+ for i, r in enumerate(st.session_state["guided_rows"], start=1):
516
+ pkg = (r["Product"] or "").strip()
517
+ if not pkg:
518
+ # allow entirely blank rows to be skipped
519
+ if not any((r["Media plan"], r["Budget"], r["Rate"], r["Placement"], r["Notes"], r["Targeting"])):
520
+ continue
521
+ issues.append(f"Row {i}: Package is required.")
522
+ continue
523
+
524
+ exists = opts["packageExistsMap"].get(pkg, False)
525
+ placement = (r["Placement"] or "").strip()
526
+ if not exists:
527
+ if not placement:
528
+ issues.append(f"Row {i}: Placement required for non-bundle package '{pkg}'.")
529
+ continue
530
+
531
+ # stringify targeting
532
+ tgt_str = " OR ".join(r["Targeting"]) if r.get("Targeting") else ""
533
+
534
+ out_rows.append({
535
+ "Media plan": r["Media plan"],
536
+ "Budget": r["Budget"],
537
+ "Rate": r["Rate"],
538
+ "Product": pkg,
539
+ "Details": _details_for(pkg, placement),
540
+ "Targeting": tgt_str,
541
+ "Notes": r["Notes"],
542
+ "Placement": placement,
543
+ })
544
+
545
+ if issues:
546
+ st.error("Please fix the following before populating:\n- " + "\n- ".join(issues))
547
+ else:
548
+ if not out_rows:
549
+ st.warning("No rows to populate.")
550
+ else:
551
+ df_new = pd.DataFrame(out_rows, dtype="object")
552
+ # Push into the main input table (normalize + keep Placement column)
553
+ canonical = _normalize_input_table(df_new[DEFAULT_INPUT_COLUMNS])
554
+ canonical["Rate"] = df_new["Rate"].fillna("").astype(str)
555
+ if "Placement" in df_new.columns:
556
+ canonical["Placement"] = df_new["Placement"].fillna("").astype(str)
557
+ st.session_state["media_plan_inputs"] = canonical
558
+ st.success(f"Populated {len(canonical)} row(s) into the input table.")
559
+ st.dataframe(canonical, use_container_width=True)
560
 
561
  # Column config (Product column labeled as Package; Placement shown as text in grid)
562
  column_config = {