mktgtech commited on
Commit
e120aa8
ยท
verified ยท
1 Parent(s): cab434d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +42 -127
app.py CHANGED
@@ -16,13 +16,12 @@ from datetime import date, timedelta
16
  API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
17
  APP_PASSWORD = os.getenv("APP_PASSWORD")
18
 
19
- # Fail-safe for local testing if secrets aren't set
20
  if not API_TOKEN or not APP_PASSWORD:
21
  print("โš ๏ธ WARNING: Secrets missing. App will launch but API calls will fail.")
22
  API_TOKEN = "PLACEHOLDER"
23
  APP_PASSWORD = "password"
24
 
25
- ACCOUNT_ID = "255333" # PREMIUM account
26
  BASE_URL = "https://api.leadfeeder.com"
27
  PAGE_SIZE = 100
28
  HEADERS = {
@@ -32,11 +31,10 @@ HEADERS = {
32
  CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
33
 
34
  # =====================================================
35
- # 1. CAMPAIGN MANAGER & BUSINESS LOGIC
36
  # =====================================================
37
- # UPDATED: Renamed "MS Stack" to "MS Tech" as requested
38
  DEFAULT_CAMPAIGNS = {
39
- "MS Stack": [
40
  "microsoft", "power", "dynamics", "crm", "365", "xamarin", "sharepoint",
41
  "dot", ".net", "asp.net", "azure", "copilot", "real-estate-website-development",
42
  "manufacturing-production", "legacy-hrms", "healthcare-appointment", "eld-fleet",
@@ -71,9 +69,9 @@ def save_campaign_rules(rules_text):
71
  rules = json.loads(rules_text)
72
  with open(CAMPAIGN_CONFIG_FILE, "w") as f:
73
  json.dump(rules, f, indent=4)
74
- return "โœ… Configuration Saved! (Reload Data to apply changes)", rules
75
  except json.JSONDecodeError:
76
- return "โŒ Error: Invalid JSON format.", None
77
 
78
  def categorize_quality(score):
79
  if pd.isna(score): return "Unknown"
@@ -98,29 +96,22 @@ def assign_campaign(url, rules):
98
 
99
  def apply_business_logic(df):
100
  if df is None or df.empty: return df
101
-
102
- # 1. Quality Group
103
  if "lead_quality_score" in df.columns:
104
  df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
105
 
106
- # 2. Campaign Logic (requires landing_page_path)
107
  rules = load_campaign_rules()
108
  if "landing_page_path" in df.columns:
109
  df["Campaign"] = df["landing_page_path"].apply(lambda x: assign_campaign(x, rules))
110
  else:
111
  df["Campaign"] = "Uncategorized"
112
-
113
  return df
114
 
115
  # =====================================================
116
  # 2. PRESETS
117
  # =====================================================
118
  DASHBOARD_PRESETS = {
119
- # -- NEW: TARGETED COMPARISON --
120
  "Key Campaigns Overview (MS Tech, Fintech, Adtech)": ("SPECIAL_KEY_CAMPAIGNS", None, None),
121
-
122
  "All Campaigns (Multi-Chart)": ("SPECIAL_MULTI", None, None),
123
-
124
  "Visits by Campaign": ("Campaign", "total_visits", "sum"),
125
  "Leads by Campaign": ("Campaign", "company_name", "count"),
126
  "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
@@ -132,20 +123,18 @@ TREND_PRESETS = {
132
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
133
  "Leads Trend by Campaign": ("company_name", "count", "Campaign"),
134
  "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
135
-
136
  "Total Visits Trend": ("total_visits", "sum", None),
137
  "Active Accounts Trend": ("company_name", "count", None),
138
  }
139
 
140
  # =====================================================
141
- # 3. API HANDLING (FULL DATA PRESERVED)
142
  # =====================================================
143
  def make_request(url, params=None):
144
  retries = 3
145
  while retries > 0:
146
  r = requests.get(url, headers=HEADERS, params=params, timeout=45)
147
  if r.status_code == 429:
148
- print("โณ Rate limit hit. Sleeping for 60 seconds...")
149
  time.sleep(61)
150
  retries -= 1
151
  continue
@@ -154,20 +143,14 @@ def make_request(url, params=None):
154
  raise Exception("Max retries exceeded")
155
 
156
  def fetch_basic_leads(start_date, end_date):
157
- """ Fetch ALL company details (No data points missed). """
158
  page = 1
159
  rows = []
160
  print(f"๐Ÿš€ Fetching full company list for {start_date} to {end_date}...")
161
-
162
  while True:
163
  try:
164
  js = make_request(
165
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads",
166
- params={
167
- "start_date": start_date, "end_date": end_date,
168
- "page[number]": page, "page[size]": PAGE_SIZE,
169
- "include": "location"
170
- }
171
  )
172
  data = js.get("data", [])
173
  if not data: break
@@ -208,7 +191,7 @@ def fetch_basic_leads(start_date, end_date):
208
  "region": loc.get("region"),
209
  "city": loc.get("city"),
210
  "leadfeeder_url": a.get("view_in_leadfeeder"),
211
- "landing_page_path": None, # Init for Enrichment
212
  "exit_page_path": None
213
  })
214
  print(f"โœ… Page {page} loaded. Rows: {len(rows)}")
@@ -223,25 +206,18 @@ def fetch_basic_leads(start_date, end_date):
223
  return df
224
 
225
  def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
226
- """ Step 2: Deep enrich (Restored full logic). """
227
  if df.empty: return df
228
  target_df = df.head(max_rows) if max_rows else df
229
  total = len(target_df)
230
 
231
  print(f"๐Ÿ•ต๏ธ Deep enriching {total} rows ({start_date} to {end_date})...")
232
-
233
  for index, row in target_df.iterrows():
234
- # Skip if already enriched
235
- if row.get("landing_page_path") and row.get("exit_page_path"): continue
236
-
237
  lead_id = row["lead_id"]
238
  try:
239
  visit_data = make_request(
240
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
241
- params={
242
- "start_date": start_date, "end_date": end_date,
243
- "page[size]": 1, "include": "page_views"
244
- }
245
  )
246
  visits = visit_data.get("data", [])
247
  included = visit_data.get("included", [])
@@ -251,11 +227,8 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
251
  if visits:
252
  visit = visits[0]
253
  v_attrs = visit.get("attributes", {})
254
-
255
- # A. Direct Attributes
256
  landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
257
 
258
- # B. Parse 'visit_route'
259
  visit_route = v_attrs.get("visit_route", [])
260
  if visit_route and isinstance(visit_route, list):
261
  last_step = visit_route[-1]
@@ -264,11 +237,9 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
264
  first_step = visit_route[0]
265
  landing = first_step.get("page_path") or first_step.get("page_url")
266
 
267
- # C. Deep Link via 'included' Page Views
268
  if not landing or not exit_p:
269
  pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
270
  pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
271
-
272
  if pv_ids:
273
  if not landing:
274
  first_pv = pv_map.get(pv_ids[0])
@@ -281,10 +252,9 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
281
  df.at[index, "exit_page_path"] = exit_p
282
  except Exception as e:
283
  print(f"Failed to enrich lead {lead_id}: {e}")
284
-
285
  if max_rows and index % 5 == 0:
286
  progress(index / total, desc="Enriching...")
287
-
288
  return df
289
 
290
  # =====================================================
@@ -293,22 +263,16 @@ def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=g
293
  def load_preview(start, end):
294
  df = fetch_basic_leads(start, end)
295
  if df.empty: return df, pd.DataFrame(), "โš ๏ธ No data found."
296
-
297
- # Enrich top 50 for better preview categorization
298
  df_preview = df.copy()
299
  df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=50)
300
  df_preview = apply_business_logic(df_preview)
301
-
302
- status = f"โœ… Loaded **{len(df):,} companies**. Preview shows top 50 rows enriched."
303
- return df, df_preview.head(50), status
304
 
305
  def download_full_excel(df, start, end):
306
  if df is None or df.empty: return None
307
  print("โณ Starting full enrichment for Excel export...")
308
-
309
  enriched_df = enrich_leads_with_visits(df.copy(), start, end)
310
  final_df = apply_business_logic(enriched_df)
311
-
312
  path = "/tmp/leadfeeder_campaign_data.xlsx"
313
  final_df.to_excel(path, index=False)
314
  return path
@@ -324,14 +288,11 @@ def inspect_raw_json(lead_id, start_date, end_date):
324
  return str(e)
325
 
326
  # =====================================================
327
- # 5. CHART ENGINES (IMPROVED UI)
328
  # =====================================================
329
  def build_kpis(df):
330
  if df is None or df.empty: return 0, 0, 0, 0, 0, 0, 0
331
-
332
- if "Quality_Group" not in df.columns:
333
- df = apply_business_logic(df)
334
-
335
  return (
336
  len(df),
337
  df["total_visits"].gt(0).sum(),
@@ -346,99 +307,50 @@ def build_dashboard(df, preset, top_n):
346
  if df is None or df.empty: return px.bar(title="No Data")
347
  if "Campaign" not in df.columns: df = apply_business_logic(df)
348
 
349
- # --- 1. NEW KEY CAMPAIGNS COMPARISON (MS Tech, Fintech, Adtech) ---
350
  if preset == "Key Campaigns Overview (MS Tech, Fintech, Adtech)":
351
- target_campaigns = ["MS Tech", "Fintech", "Adtech"]
 
 
352
 
353
- # Filter Data
354
- filtered_df = df[df["Campaign"].isin(target_campaigns)].copy()
355
 
356
- if filtered_df.empty:
357
- return px.bar(title="No data found for MS Tech, Fintech, or Adtech. Try 'Download Excel' to enrich more rows.")
358
-
359
- # Create Aggregates
360
- visits = filtered_df.groupby("Campaign")["total_visits"].sum().reset_index()
361
- leads = filtered_df.groupby("Campaign")["company_name"].count().reset_index()
362
-
363
- # Subplots: Left=Visits, Right=Leads
364
- fig = make_subplots(rows=1, cols=2, subplot_titles=("Total Visits", "Total Leads (Count)"))
365
-
366
- fig.add_trace(
367
- go.Bar(x=visits["Campaign"], y=visits["total_visits"], name="Visits", marker_color='indigo'),
368
- row=1, col=1
369
- )
370
- fig.add_trace(
371
- go.Bar(x=leads["Campaign"], y=leads["company_name"], name="Leads", marker_color='teal'),
372
- row=1, col=2
373
- )
374
  fig.update_layout(title_text="Key Campaigns Comparison", showlegend=False)
375
  return fig
376
 
377
- # --- 2. ALL CAMPAIGNS MULTI-CHART ---
378
  if preset == "All Campaigns (Multi-Chart)":
379
- campaigns = df["Campaign"].dropna().unique().tolist()
380
- if not campaigns: return px.bar(title="No Campaigns Found")
381
-
382
- # Sort so key campaigns appear first if possible, or A-Z
383
- campaigns.sort()
384
-
385
- num_cams = len(campaigns)
386
- cols = 2
387
- rows = math.ceil(num_cams / cols)
388
-
389
- fig = make_subplots(rows=rows, cols=cols, subplot_titles=campaigns, vertical_spacing=0.15)
390
-
391
  for i, camp in enumerate(campaigns):
392
- r = (i // cols) + 1
393
- c = (i % cols) + 1
394
-
395
  camp_df = df[df["Campaign"] == camp]
396
- grouped = camp_df.groupby("company_name")["total_visits"].sum().reset_index()
397
- grouped = grouped.sort_values("total_visits", ascending=False).head(top_n)
398
-
399
  fig.add_trace(go.Bar(x=grouped["company_name"], y=grouped["total_visits"], name=camp), row=r, col=c)
400
-
401
- fig.update_layout(title_text=f"Top Companies by Campaign (All {num_cams})", height=350 * rows, showlegend=False)
402
  return fig
403
 
404
- # --- STANDARD CHARTS ---
405
  dim, metric, agg = DASHBOARD_PRESETS[preset]
406
  if agg == "count":
407
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
408
  else:
409
  grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
410
-
411
  return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset, color=dim)
412
 
413
- def get_trend_filter_options(df, preset):
414
- if df is None or df.empty: return gr.update(choices=[], value=None, visible=False)
415
-
416
- metric, agg, segment = TREND_PRESETS[preset]
417
- if not segment: return gr.update(choices=[], value=None, visible=False)
418
- if segment not in df.columns: df = apply_business_logic(df)
419
-
420
- options = sorted(df[segment].astype(str).unique().tolist())
421
- # Default select key campaigns if available
422
- defaults = [o for o in options if o in ["MS Tech", "Fintech", "Adtech"]]
423
- if not defaults: defaults = options[:5]
424
-
425
- return gr.update(choices=options, value=defaults, visible=True, label=f"Filter {segment}")
426
-
427
  def build_trend(df, preset, grain, filter_values):
428
  if df is None or df.empty: return px.line(title="No Data")
429
-
430
  metric, agg, segment = TREND_PRESETS[preset]
431
  df_t = df.dropna(subset=["last_visit_date"]).copy()
432
 
433
  if segment:
434
  if segment not in df_t.columns: df_t = apply_business_logic(df_t)
435
- if filter_values:
436
- df_t = df_t[df_t[segment].isin(filter_values)]
437
 
438
  if grain == "Weekly":
439
  df_t["period"] = df_t["last_visit_date"].dt.to_period("W").astype(str)
440
- elif grain == "Monthly":
441
- df_t["period"] = df_t["last_visit_date"].dt.to_period("M").astype(str)
442
  else:
443
  df_t["period"] = df_t["last_visit_date"].dt.date
444
 
@@ -454,8 +366,17 @@ def build_trend(df, preset, grain, filter_values):
454
  ts = df_t.groupby("period")[val].agg(agg).reset_index()
455
  return px.line(ts, x="period", y=val, markers=True, title=preset)
456
 
 
 
 
 
 
 
 
 
 
457
  # =====================================================
458
- # 6. UI LAYOUT (STABLE & CLEAN)
459
  # =====================================================
460
  with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
461
  gr.Markdown("## ๐Ÿš€ Leadfeeder Analytics & Campaign Manager")
@@ -473,28 +394,24 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
473
  with gr.Row():
474
  start = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
475
  end = gr.Textbox(label="End Date", value=date.today().isoformat())
476
-
477
  with gr.Row():
478
  btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
479
  btn_dl = gr.Button("2. Enrich & Download Full Excel")
480
  file_dl = gr.File(label="Download Excel")
481
-
482
- table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True, height=400)
483
  btn_load.click(load_preview, [start, end], [df_state, table, status])
484
  btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
485
 
486
  # --- TAB 2: DASHBOARD ---
487
  with gr.Tab("๐Ÿ“Š Dashboard"):
488
- with gr.Row():
489
- # KPIs in a row
490
- kpis = [gr.Number(label=l) for l in ["Companies", "Active", "Visits", "Avg Quality", "CRM %", "LinkedIn %", "High Quality (8-10)"]]
491
  gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
492
 
493
  gr.Markdown("### ๐Ÿ“Š Charts")
494
  with gr.Row():
495
  preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Key Campaigns Overview (MS Tech, Fintech, Adtech)")
496
  top_n = gr.Slider(5, 50, value=10, label="Top N Items")
497
-
498
  chart = gr.Plot()
499
  gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
500
 
@@ -503,10 +420,8 @@ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
503
  with gr.Row():
504
  trend_view = gr.Dropdown(choices=list(TREND_PRESETS.keys()), label="Select Trend", value="Visits Trend by Campaign")
505
  grain = gr.Radio(["Daily", "Weekly", "Monthly"], value="Daily", label="Granularity")
506
-
507
  filter_dropdown = gr.Dropdown(multiselect=True, visible=False, label="Filter Segments")
508
  plot = gr.Plot()
509
-
510
  trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
511
  gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
512
 
 
16
  API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
17
  APP_PASSWORD = os.getenv("APP_PASSWORD")
18
 
 
19
  if not API_TOKEN or not APP_PASSWORD:
20
  print("โš ๏ธ WARNING: Secrets missing. App will launch but API calls will fail.")
21
  API_TOKEN = "PLACEHOLDER"
22
  APP_PASSWORD = "password"
23
 
24
+ ACCOUNT_ID = "255333"
25
  BASE_URL = "https://api.leadfeeder.com"
26
  PAGE_SIZE = 100
27
  HEADERS = {
 
31
  CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
32
 
33
  # =====================================================
34
+ # 1. CAMPAIGN MANAGER & LOGIC
35
  # =====================================================
 
36
  DEFAULT_CAMPAIGNS = {
37
+ "MS Tech": [
38
  "microsoft", "power", "dynamics", "crm", "365", "xamarin", "sharepoint",
39
  "dot", ".net", "asp.net", "azure", "copilot", "real-estate-website-development",
40
  "manufacturing-production", "legacy-hrms", "healthcare-appointment", "eld-fleet",
 
69
  rules = json.loads(rules_text)
70
  with open(CAMPAIGN_CONFIG_FILE, "w") as f:
71
  json.dump(rules, f, indent=4)
72
+ return "โœ… Configuration Saved!", rules
73
  except json.JSONDecodeError:
74
+ return "โŒ Error: Invalid JSON.", None
75
 
76
  def categorize_quality(score):
77
  if pd.isna(score): return "Unknown"
 
96
 
97
  def apply_business_logic(df):
98
  if df is None or df.empty: return df
 
 
99
  if "lead_quality_score" in df.columns:
100
  df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
101
 
 
102
  rules = load_campaign_rules()
103
  if "landing_page_path" in df.columns:
104
  df["Campaign"] = df["landing_page_path"].apply(lambda x: assign_campaign(x, rules))
105
  else:
106
  df["Campaign"] = "Uncategorized"
 
107
  return df
108
 
109
  # =====================================================
110
  # 2. PRESETS
111
  # =====================================================
112
  DASHBOARD_PRESETS = {
 
113
  "Key Campaigns Overview (MS Tech, Fintech, Adtech)": ("SPECIAL_KEY_CAMPAIGNS", None, None),
 
114
  "All Campaigns (Multi-Chart)": ("SPECIAL_MULTI", None, None),
 
115
  "Visits by Campaign": ("Campaign", "total_visits", "sum"),
116
  "Leads by Campaign": ("Campaign", "company_name", "count"),
117
  "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
 
123
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
124
  "Leads Trend by Campaign": ("company_name", "count", "Campaign"),
125
  "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
 
126
  "Total Visits Trend": ("total_visits", "sum", None),
127
  "Active Accounts Trend": ("company_name", "count", None),
128
  }
129
 
130
  # =====================================================
131
+ # 3. API HANDLING
132
  # =====================================================
133
  def make_request(url, params=None):
134
  retries = 3
135
  while retries > 0:
136
  r = requests.get(url, headers=HEADERS, params=params, timeout=45)
137
  if r.status_code == 429:
 
138
  time.sleep(61)
139
  retries -= 1
140
  continue
 
143
  raise Exception("Max retries exceeded")
144
 
145
  def fetch_basic_leads(start_date, end_date):
 
146
  page = 1
147
  rows = []
148
  print(f"๐Ÿš€ Fetching full company list for {start_date} to {end_date}...")
 
149
  while True:
150
  try:
151
  js = make_request(
152
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads",
153
+ params={"start_date": start_date, "end_date": end_date, "page[number]": page, "page[size]": PAGE_SIZE, "include": "location"}
 
 
 
 
154
  )
155
  data = js.get("data", [])
156
  if not data: break
 
191
  "region": loc.get("region"),
192
  "city": loc.get("city"),
193
  "leadfeeder_url": a.get("view_in_leadfeeder"),
194
+ "landing_page_path": None,
195
  "exit_page_path": None
196
  })
197
  print(f"โœ… Page {page} loaded. Rows: {len(rows)}")
 
206
  return df
207
 
208
  def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
 
209
  if df.empty: return df
210
  target_df = df.head(max_rows) if max_rows else df
211
  total = len(target_df)
212
 
213
  print(f"๐Ÿ•ต๏ธ Deep enriching {total} rows ({start_date} to {end_date})...")
 
214
  for index, row in target_df.iterrows():
215
+ if row.get("landing_page_path"): continue
 
 
216
  lead_id = row["lead_id"]
217
  try:
218
  visit_data = make_request(
219
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
220
+ params={"start_date": start_date, "end_date": end_date, "page[size]": 1, "include": "page_views"}
 
 
 
221
  )
222
  visits = visit_data.get("data", [])
223
  included = visit_data.get("included", [])
 
227
  if visits:
228
  visit = visits[0]
229
  v_attrs = visit.get("attributes", {})
 
 
230
  landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
231
 
 
232
  visit_route = v_attrs.get("visit_route", [])
233
  if visit_route and isinstance(visit_route, list):
234
  last_step = visit_route[-1]
 
237
  first_step = visit_route[0]
238
  landing = first_step.get("page_path") or first_step.get("page_url")
239
 
 
240
  if not landing or not exit_p:
241
  pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
242
  pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
 
243
  if pv_ids:
244
  if not landing:
245
  first_pv = pv_map.get(pv_ids[0])
 
252
  df.at[index, "exit_page_path"] = exit_p
253
  except Exception as e:
254
  print(f"Failed to enrich lead {lead_id}: {e}")
255
+
256
  if max_rows and index % 5 == 0:
257
  progress(index / total, desc="Enriching...")
 
258
  return df
259
 
260
  # =====================================================
 
263
  def load_preview(start, end):
264
  df = fetch_basic_leads(start, end)
265
  if df.empty: return df, pd.DataFrame(), "โš ๏ธ No data found."
 
 
266
  df_preview = df.copy()
267
  df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=50)
268
  df_preview = apply_business_logic(df_preview)
269
+ return df, df_preview.head(50), f"โœ… Loaded {len(df):,} companies. Preview top 50 enriched."
 
 
270
 
271
  def download_full_excel(df, start, end):
272
  if df is None or df.empty: return None
273
  print("โณ Starting full enrichment for Excel export...")
 
274
  enriched_df = enrich_leads_with_visits(df.copy(), start, end)
275
  final_df = apply_business_logic(enriched_df)
 
276
  path = "/tmp/leadfeeder_campaign_data.xlsx"
277
  final_df.to_excel(path, index=False)
278
  return path
 
288
  return str(e)
289
 
290
  # =====================================================
291
+ # 5. CHART ENGINES
292
  # =====================================================
293
  def build_kpis(df):
294
  if df is None or df.empty: return 0, 0, 0, 0, 0, 0, 0
295
+ if "Quality_Group" not in df.columns: df = apply_business_logic(df)
 
 
 
296
  return (
297
  len(df),
298
  df["total_visits"].gt(0).sum(),
 
307
  if df is None or df.empty: return px.bar(title="No Data")
308
  if "Campaign" not in df.columns: df = apply_business_logic(df)
309
 
 
310
  if preset == "Key Campaigns Overview (MS Tech, Fintech, Adtech)":
311
+ target = ["MS Tech", "Fintech", "Adtech"]
312
+ filtered = df[df["Campaign"].isin(target)].copy()
313
+ if filtered.empty: return px.bar(title="No Data for Key Campaigns")
314
 
315
+ visits = filtered.groupby("Campaign")["total_visits"].sum().reset_index()
316
+ leads = filtered.groupby("Campaign")["company_name"].count().reset_index()
317
 
318
+ fig = make_subplots(rows=1, cols=2, subplot_titles=("Total Visits", "Total Leads"))
319
+ fig.add_trace(go.Bar(x=visits["Campaign"], y=visits["total_visits"], name="Visits"), row=1, col=1)
320
+ fig.add_trace(go.Bar(x=leads["Campaign"], y=leads["company_name"], name="Leads"), row=1, col=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  fig.update_layout(title_text="Key Campaigns Comparison", showlegend=False)
322
  return fig
323
 
 
324
  if preset == "All Campaigns (Multi-Chart)":
325
+ campaigns = sorted(df["Campaign"].dropna().unique().tolist())
326
+ rows = math.ceil(len(campaigns) / 2)
327
+ fig = make_subplots(rows=rows, cols=2, subplot_titles=campaigns, vertical_spacing=0.15)
 
 
 
 
 
 
 
 
 
328
  for i, camp in enumerate(campaigns):
329
+ r, c = (i // 2) + 1, (i % 2) + 1
 
 
330
  camp_df = df[df["Campaign"] == camp]
331
+ grouped = camp_df.groupby("company_name")["total_visits"].sum().reset_index().sort_values("total_visits", ascending=False).head(top_n)
 
 
332
  fig.add_trace(go.Bar(x=grouped["company_name"], y=grouped["total_visits"], name=camp), row=r, col=c)
333
+ fig.update_layout(height=350*rows, showlegend=False, title_text="Top Companies by Campaign")
 
334
  return fig
335
 
 
336
  dim, metric, agg = DASHBOARD_PRESETS[preset]
337
  if agg == "count":
338
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
339
  else:
340
  grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
 
341
  return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset, color=dim)
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  def build_trend(df, preset, grain, filter_values):
344
  if df is None or df.empty: return px.line(title="No Data")
 
345
  metric, agg, segment = TREND_PRESETS[preset]
346
  df_t = df.dropna(subset=["last_visit_date"]).copy()
347
 
348
  if segment:
349
  if segment not in df_t.columns: df_t = apply_business_logic(df_t)
350
+ if filter_values: df_t = df_t[df_t[segment].isin(filter_values)]
 
351
 
352
  if grain == "Weekly":
353
  df_t["period"] = df_t["last_visit_date"].dt.to_period("W").astype(str)
 
 
354
  else:
355
  df_t["period"] = df_t["last_visit_date"].dt.date
356
 
 
366
  ts = df_t.groupby("period")[val].agg(agg).reset_index()
367
  return px.line(ts, x="period", y=val, markers=True, title=preset)
368
 
369
+ def get_trend_filter_options(df, preset):
370
+ if df is None or df.empty: return gr.update(choices=[], value=None, visible=False)
371
+ metric, agg, segment = TREND_PRESETS[preset]
372
+ if not segment: return gr.update(choices=[], value=None, visible=False)
373
+ if segment not in df.columns: df = apply_business_logic(df)
374
+ options = sorted(df[segment].astype(str).unique().tolist())
375
+ defaults = [o for o in options if o in ["MS Tech", "Fintech", "Adtech"]] or options[:5]
376
+ return gr.update(choices=options, value=defaults, visible=True, label=f"Filter {segment}")
377
+
378
  # =====================================================
379
+ # 6. UI LAYOUT
380
  # =====================================================
381
  with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
382
  gr.Markdown("## ๐Ÿš€ Leadfeeder Analytics & Campaign Manager")
 
394
  with gr.Row():
395
  start = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
396
  end = gr.Textbox(label="End Date", value=date.today().isoformat())
 
397
  with gr.Row():
398
  btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
399
  btn_dl = gr.Button("2. Enrich & Download Full Excel")
400
  file_dl = gr.File(label="Download Excel")
401
+ # Removed height=400 to fix TypeError in older Gradio versions
402
+ table = gr.Dataframe(label="Preview (Top 50 Enriched)", interactive=True)
403
  btn_load.click(load_preview, [start, end], [df_state, table, status])
404
  btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
405
 
406
  # --- TAB 2: DASHBOARD ---
407
  with gr.Tab("๐Ÿ“Š Dashboard"):
408
+ kpis = [gr.Number(label=l) for l in ["Companies", "Active", "Visits", "Avg Quality", "CRM %", "LinkedIn %", "High Quality (8-10)"]]
 
 
409
  gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
410
 
411
  gr.Markdown("### ๐Ÿ“Š Charts")
412
  with gr.Row():
413
  preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart View", value="Key Campaigns Overview (MS Tech, Fintech, Adtech)")
414
  top_n = gr.Slider(5, 50, value=10, label="Top N Items")
 
415
  chart = gr.Plot()
416
  gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
417
 
 
420
  with gr.Row():
421
  trend_view = gr.Dropdown(choices=list(TREND_PRESETS.keys()), label="Select Trend", value="Visits Trend by Campaign")
422
  grain = gr.Radio(["Daily", "Weekly", "Monthly"], value="Daily", label="Granularity")
 
423
  filter_dropdown = gr.Dropdown(multiselect=True, visible=False, label="Filter Segments")
424
  plot = gr.Plot()
 
425
  trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
426
  gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
427