mktgtech commited on
Commit
65eaeb6
Β·
verified Β·
1 Parent(s): 2519edf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +246 -175
app.py CHANGED
@@ -13,25 +13,23 @@ from datetime import date, timedelta
13
  API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
14
  APP_PASSWORD = os.getenv("APP_PASSWORD")
15
 
16
- # If testing locally without env vars, uncomment and set manually:
17
- # API_TOKEN = "your_token_here"
18
- # APP_PASSWORD = "password"
19
-
20
  if not API_TOKEN or not APP_PASSWORD:
21
- # Fallback for safe run if secrets missing (prevents crash on immediate launch)
22
  print("⚠️ WARNING: Secrets missing. App will launch but API calls will fail.")
 
 
23
 
24
- ACCOUNT_ID = "255333"
25
  BASE_URL = "https://api.leadfeeder.com"
26
  PAGE_SIZE = 100
27
  HEADERS = {
28
  "Authorization": f"Token token={API_TOKEN}",
29
  "Accept": "application/json"
30
  }
31
- CONFIG_FILE = "campaign_rules.json"
32
 
33
  # =====================================================
34
- # 1. CAMPAIGN & CONFIG MANAGEMENT
35
  # =====================================================
36
  DEFAULT_CAMPAIGNS = {
37
  "MS Stack": [
@@ -56,132 +54,118 @@ DEFAULT_CAMPAIGNS = {
56
  }
57
 
58
  def load_campaign_rules():
59
- """Load rules from JSON or fallback to defaults."""
60
- if os.path.exists(CONFIG_FILE):
61
  try:
62
- with open(CONFIG_FILE, "r") as f:
63
  return json.load(f)
64
  except:
65
  return DEFAULT_CAMPAIGNS
66
  return DEFAULT_CAMPAIGNS
67
 
68
- def save_campaign_rules(rules):
69
- """Save rules to JSON."""
70
- with open(CONFIG_FILE, "w") as f:
71
- json.dump(rules, f, indent=4)
72
- return "βœ… Settings Saved!"
73
-
74
- def get_rules_text(rules):
75
- """Convert dict to readable text for UI."""
76
- return json.dumps(rules, indent=2)
77
-
78
- def update_rules_from_text(text):
79
- """Parse text back to dict and save."""
80
  try:
81
- new_rules = json.loads(text)
82
- save_campaign_rules(new_rules)
83
- return "βœ… Configuration updated successfully!", new_rules
 
84
  except json.JSONDecodeError:
85
- return "❌ Error: Invalid JSON format. Please check syntax.", None
86
 
87
- # =====================================================
88
- # 2. DATA PROCESSING LOGIC
89
- # =====================================================
90
  def categorize_quality(score):
 
91
  if pd.isna(score): return "Unknown"
92
- s = int(score)
93
- if 8 <= s <= 10: return "High Quality (8-10)"
94
- if 5 <= s <= 7: return "Mid Quality (5-7)"
95
- if 1 <= s <= 4: return "Low Quality (1-4)"
96
- return "Low Quality (0)"
 
 
 
97
 
98
  def assign_campaign(url, rules):
 
99
  if not url or not isinstance(url, str):
100
  return "Uncategorized"
101
 
102
  url_lower = url.lower()
103
- # Check each campaign group
104
  for campaign_name, keywords in rules.items():
105
  for kw in keywords:
106
  if kw.lower() in url_lower:
107
  return campaign_name
108
  return "Other"
109
 
110
- def process_business_logic(df):
111
- """Apply Quality Grouping and Campaign Logic to the DataFrame."""
112
- if df.empty: return df
113
 
114
- # 1. Load Rules
115
- rules = load_campaign_rules()
116
 
117
- # 2. Apply Quality Logic
118
- df["Quality Group"] = df["lead_quality_score"].apply(categorize_quality)
 
 
119
 
120
- # 3. Apply Campaign Logic (Check Landing Page mostly)
121
- # We prioritize landing_page_path, fallback to first_visit_url if available?
122
- # For now, strictly landing_page_path or blank.
123
- def get_campaign(row):
124
- # Check landing page
125
- path = row.get("landing_page_path")
126
- res = assign_campaign(path, rules)
127
- if res != "Other": return res
128
-
129
- # Optional: Check all industries if landing page fails?
130
- # ind = row.get("all_industries", "")
131
- # if ind: return assign_campaign(ind, rules)
132
-
133
- return "Other"
134
-
135
- df["Campaign"] = df.apply(get_campaign, axis=1)
136
  return df
137
 
138
  # =====================================================
139
- # 3. PRESETS (UPDATED)
140
  # =====================================================
141
  DASHBOARD_PRESETS = {
142
- "Total Visits by Campaign": ("Campaign", "total_visits", "sum"),
 
 
 
 
 
143
  "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
144
- "Lead Quality Breakdown": ("Quality Group", "company_name", "count"),
145
- "Campaign Performance (Leads)": ("Campaign", "company_name", "count"),
146
  "Total Visits by Country": ("country", "total_visits", "sum"),
147
  "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
148
  "Top Accounts by Visits": ("company_name", "total_visits", "sum"),
 
 
 
 
149
  }
150
 
151
  TREND_PRESETS = {
152
- "Total Visits Trend": ("total_visits", "sum", None),
153
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
154
- "Visits Trend by Quality": ("total_visits", "sum", "Quality Group"),
 
 
 
155
  "Active Accounts Trend": ("company_name", "count", None),
156
  "Avg Lead Quality Trend": ("lead_quality_score", "mean", None),
157
  "Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
 
 
158
  }
159
 
160
  # =====================================================
161
- # 4. API & FETCHER
162
  # =====================================================
163
  def make_request(url, params=None):
164
  retries = 3
165
  while retries > 0:
166
- try:
167
- r = requests.get(url, headers=HEADERS, params=params, timeout=45)
168
- if r.status_code == 429:
169
- print("⏳ Rate limit hit. Sleeping for 60 seconds...")
170
- time.sleep(61)
171
- retries -= 1
172
- continue
173
- r.raise_for_status()
174
- return r.json()
175
- except Exception as e:
176
- print(f"Request failed: {e}")
177
  retries -= 1
178
- time.sleep(2)
179
- raise Exception("Max retries exceeded or API Error")
 
 
180
 
181
  def fetch_basic_leads(start_date, end_date):
 
182
  page = 1
183
  rows = []
184
- print(f"πŸš€ Fetching leads {start_date} to {end_date}...")
185
 
186
  while True:
187
  try:
@@ -208,19 +192,33 @@ def fetch_basic_leads(start_date, end_date):
208
  "lead_id": lead.get("id"),
209
  "company_name": a.get("name"),
210
  "website_url": a.get("website_url"),
 
 
211
  "primary_industry": a.get("industry"),
 
 
212
  "last_visit_date": a.get("last_visit_date"),
213
  "total_visits": a.get("visits"),
214
  "lead_quality_score": a.get("quality"),
 
 
 
 
 
 
 
215
  "country": loc.get("country"),
 
216
  "city": loc.get("city"),
217
- # Init fields for enrichment
 
218
  "landing_page_path": None,
219
  "exit_page_path": None
220
  })
 
221
  page += 1
222
  except Exception as e:
223
- print(f"Error page {page}: {e}")
224
  break
225
 
226
  df = pd.DataFrame(rows)
@@ -229,190 +227,263 @@ def fetch_basic_leads(start_date, end_date):
229
  return df
230
 
231
  def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
 
232
  if df.empty: return df
233
  target_df = df.head(max_rows) if max_rows else df
234
  total = len(target_df)
235
 
236
- print(f"πŸ•΅οΈ Enriching {total} rows...")
237
 
238
  for index, row in target_df.iterrows():
239
- # Skip if already has landing page
240
- if row.get("landing_page_path"): continue
241
 
242
  lead_id = row["lead_id"]
243
  try:
244
- # We fetch 1 visit to get landing page
245
  visit_data = make_request(
246
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
247
- params={"start_date": start_date, "end_date": end_date, "page[size]": 1}
 
 
 
248
  )
249
  visits = visit_data.get("data", [])
 
 
 
 
250
  if visits:
251
- attrs = visits[0].get("attributes", {})
252
- landing = attrs.get("landing_page_path") or attrs.get("landing_page_url")
253
 
254
- # Fallback to visit route if top level attr is missing
255
- if not landing:
256
- route = attrs.get("visit_route", [])
257
- if route:
258
- landing = route[0].get("page_path")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  df.at[index, "landing_page_path"] = landing
261
- except:
262
- pass
 
263
 
264
  if max_rows and index % 5 == 0:
265
- progress(index / total, desc="Enriching Visits...")
266
 
267
  return df
268
 
269
  # =====================================================
270
- # 5. WRAPPERS
271
  # =====================================================
272
- def load_and_process(start, end):
273
- # 1. Fetch Basic
274
  df = fetch_basic_leads(start, end)
275
  if df.empty: return df, pd.DataFrame(), "⚠️ No data found."
276
 
277
- # 2. Enrich (Preview 20 rows for speed in UI)
278
- # We MUST enrich landing pages to get Campaigns working
279
  df_preview = df.copy()
280
  df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=20)
281
 
282
- # 3. Apply Business Logic (Campaigns + Quality)
283
- df_preview = process_business_logic(df_preview)
284
 
285
- # Note: Main DF is NOT enriched yet (too slow), so Campaign column will be empty/Other
286
- # until user clicks "Download" or we force enrichment.
287
- # For better UI, we can try to enrich ALL if size < 200, else warn.
288
- if len(df) < 300:
289
- df = enrich_leads_with_visits(df, start, end) # Enrich all if small
290
- df = process_business_logic(df)
291
- status = f"βœ… Loaded & Enriched {len(df)} companies."
292
- else:
293
- # Just process logic on what we have (mostly empty landing pages)
294
- df = process_business_logic(df)
295
- status = f"βœ… Loaded {len(df)} companies. (Preview enriched only. Download for full data)."
296
-
297
  return df, df_preview.head(20), status
298
 
299
  def download_full_excel(df, start, end):
300
  if df is None or df.empty: return None
301
- print("⏳ Full Enrichment for Export...")
 
 
 
302
 
303
- # Ensure all rows have visits
304
- enriched = enrich_leads_with_visits(df.copy(), start, end)
305
- # Re-apply logic now that we have landing pages
306
- final_df = process_business_logic(enriched)
307
 
308
  path = "/tmp/leadfeeder_campaign_data.xlsx"
309
  final_df.to_excel(path, index=False)
310
  return path
311
 
 
 
 
 
 
 
 
 
 
 
312
  # =====================================================
313
- # 6. CHARTING
314
  # =====================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  def build_dashboard(df, preset, top_n):
316
  if df is None or df.empty: return px.bar(title="No Data")
317
 
 
 
 
 
 
 
 
318
  dim, metric, agg = DASHBOARD_PRESETS[preset]
319
 
320
- # Check if column exists (Campaign might be missing if not enriched)
321
- if dim not in df.columns:
322
- return px.bar(title=f"Column '{dim}' not available. Try Downloading Full Data first.")
323
-
324
  if agg == "count":
325
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
326
  else:
327
  grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
328
 
329
- fig = px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset, color=dim)
330
- return fig
331
 
332
- def build_trend(df, preset, grain):
333
- if df is None or df.empty: return px.line(title="No Data")
334
 
335
  metric, agg, segment = TREND_PRESETS[preset]
336
- df_t = df.dropna(subset=["last_visit_date"]).copy()
 
 
 
 
 
 
 
 
 
337
 
 
 
 
 
 
 
 
 
338
  if grain == "Weekly":
339
- df_t["period"] = df_t["last_visit_date"].dt.to_period("W").astype(str)
 
 
340
  else:
341
- df_t["period"] = df_t["last_visit_date"].dt.date
342
 
343
  val = metric
344
  if agg == "count":
345
- df_t["_v"] = 1
346
  val = "_v"
347
 
348
- if segment and segment in df_t.columns:
349
- ts = df_t.groupby(["period", segment])[val].agg(agg).reset_index()
350
- return px.line(ts, x="period", y=val, color=segment, title=preset)
351
 
352
- ts = df_t.groupby("period")[val].agg(agg).reset_index()
353
- return px.line(ts, x="period", y=val, markers=True, title=preset)
354
 
355
  # =====================================================
356
- # 7. UI LAYOUT
357
  # =====================================================
358
- with gr.Blocks(title="Leadfeeder Campaign Analytics") as demo:
359
- gr.Markdown("## πŸš€ Leadfeeder Analytics + Campaign Manager")
360
 
361
- # Auth
362
  with gr.Row():
363
- pwd_input = gr.Textbox(type="password", label="App Password")
364
- auth_btn = gr.Button("Login")
 
365
 
366
- # State
367
- df_state = gr.State()
368
 
369
  with gr.Tabs():
370
  # --- TAB 1: DATA ---
371
  with gr.Tab("πŸ“‹ Data & Report"):
372
  with gr.Row():
373
- start_dt = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
374
- end_dt = gr.Textbox(label="End Date", value=date.today().isoformat())
375
 
376
  with gr.Row():
377
- btn_load = gr.Button("1. Load Data", variant="primary")
378
- btn_dl = gr.Button("2. Download Excel (Full Analysis)")
379
- file_out = gr.File(label="Exported File")
380
-
381
- status_txt = gr.Markdown("Ready.")
382
- table = gr.Dataframe(label="Preview Data", interactive=True)
383
-
384
- btn_load.click(load_and_process, [start_dt, end_dt], [df_state, table, status_txt])
385
- btn_dl.click(download_full_excel, [df_state, start_dt, end_dt], file_out)
386
 
 
 
 
 
387
  # --- TAB 2: DASHBOARD ---
388
  with gr.Tab("πŸ“Š Dashboard"):
 
 
 
389
  with gr.Row():
390
- dash_preset = gr.Dropdown(list(DASHBOARD_PRESETS.keys()), label="Chart Type", value="Total Visits by Campaign")
391
- top_n = gr.Slider(5, 50, 10, label="Top N Items")
392
- dash_plot = gr.Plot()
393
- gr.Button("Refresh Chart").click(build_dashboard, [df_state, dash_preset, top_n], dash_plot)
394
 
 
 
 
395
  # --- TAB 3: TRENDS ---
396
  with gr.Tab("πŸ“ˆ Trends"):
397
  with gr.Row():
398
- trend_preset = gr.Dropdown(list(TREND_PRESETS.keys()), label="Trend Type", value="Visits Trend by Campaign")
399
- grain_opt = gr.Radio(["Daily", "Weekly"], value="Daily", label="Granularity")
400
- trend_plot = gr.Plot()
401
- gr.Button("Show Trend").click(build_trend, [df_state, trend_preset, grain_opt], trend_plot)
402
 
403
- # --- TAB 4: SETTINGS (CAMPAIGN MANAGER) ---
404
- with gr.Tab("βš™οΈ Campaign Settings"):
405
- gr.Markdown("### Manage Campaign Keywords")
406
- gr.Markdown("Edit the JSON structure below to add/remove campaigns or keywords. Changes apply on next 'Load Data'.")
407
 
408
- # Load initial settings
409
- init_rules = get_rules_text(load_campaign_rules())
 
 
 
 
 
410
 
411
- json_editor = gr.Code(value=init_rules, language="json", label="Campaign Configuration (JSON)")
412
- save_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
 
 
413
  config_status = gr.Markdown()
414
 
415
- save_btn.click(update_rules_from_text, json_editor, [config_status, gr.State()])
 
 
 
 
 
 
 
 
416
 
417
  if __name__ == "__main__":
418
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
13
  API_TOKEN = os.getenv("LEADFEEDER_API_TOKEN")
14
  APP_PASSWORD = os.getenv("APP_PASSWORD")
15
 
16
+ # Fail-safe for local testing if secrets aren't set
 
 
 
17
  if not API_TOKEN or not APP_PASSWORD:
 
18
  print("⚠️ WARNING: Secrets missing. App will launch but API calls will fail.")
19
+ API_TOKEN = "PLACEHOLDER"
20
+ APP_PASSWORD = "password"
21
 
22
+ ACCOUNT_ID = "255333" # PREMIUM account
23
  BASE_URL = "https://api.leadfeeder.com"
24
  PAGE_SIZE = 100
25
  HEADERS = {
26
  "Authorization": f"Token token={API_TOKEN}",
27
  "Accept": "application/json"
28
  }
29
+ CAMPAIGN_CONFIG_FILE = "campaign_rules.json"
30
 
31
  # =====================================================
32
+ # 1. CAMPAIGN MANAGER & UTILS
33
  # =====================================================
34
  DEFAULT_CAMPAIGNS = {
35
  "MS Stack": [
 
54
  }
55
 
56
  def load_campaign_rules():
57
+ """Load campaign groups from JSON file or use defaults."""
58
+ if os.path.exists(CAMPAIGN_CONFIG_FILE):
59
  try:
60
+ with open(CAMPAIGN_CONFIG_FILE, "r") as f:
61
  return json.load(f)
62
  except:
63
  return DEFAULT_CAMPAIGNS
64
  return DEFAULT_CAMPAIGNS
65
 
66
+ def save_campaign_rules(rules_text):
67
+ """Parse JSON text from UI and save to file."""
 
 
 
 
 
 
 
 
 
 
68
  try:
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! (Reload Data to apply changes)", rules
73
  except json.JSONDecodeError:
74
+ return "❌ Error: Invalid JSON format.", None
75
 
 
 
 
76
  def categorize_quality(score):
77
+ """Classify Lead Quality Score into High/Mid/Low."""
78
  if pd.isna(score): return "Unknown"
79
+ try:
80
+ s = int(score)
81
+ if 8 <= s <= 10: return "High Quality (8-10)"
82
+ if 5 <= s <= 7: return "Mid Quality (5-7)"
83
+ if 1 <= s <= 4: return "Low Quality (1-4)"
84
+ return "Low Quality (0)"
85
+ except:
86
+ return "Unknown"
87
 
88
  def assign_campaign(url, rules):
89
+ """Match URL against campaign keywords."""
90
  if not url or not isinstance(url, str):
91
  return "Uncategorized"
92
 
93
  url_lower = url.lower()
 
94
  for campaign_name, keywords in rules.items():
95
  for kw in keywords:
96
  if kw.lower() in url_lower:
97
  return campaign_name
98
  return "Other"
99
 
100
+ def apply_business_logic(df):
101
+ """Apply Campaign and Quality logic to the DataFrame."""
102
+ if df is None or df.empty: return df
103
 
104
+ # 1. Apply Quality Logic
105
+ df["Quality_Group"] = df["lead_quality_score"].apply(categorize_quality)
106
 
107
+ # 2. Apply Campaign Logic
108
+ rules = load_campaign_rules()
109
+ # We use landing_page_path primarily.
110
+ df["Campaign"] = df["landing_page_path"].apply(lambda x: assign_campaign(x, rules))
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  return df
113
 
114
  # =====================================================
115
+ # 2. PRESETS (Original + New)
116
  # =====================================================
117
  DASHBOARD_PRESETS = {
118
+ # -- NEW PRESETS --
119
+ "Visits by Campaign": ("Campaign", "total_visits", "sum"),
120
+ "Leads by Campaign": ("Campaign", "company_name", "count"),
121
+ "Leads by Quality Group": ("Quality_Group", "company_name", "count"),
122
+
123
+ # -- ORIGINAL PRESETS --
124
  "Total Visits by Industry": ("primary_industry", "total_visits", "sum"),
 
 
125
  "Total Visits by Country": ("country", "total_visits", "sum"),
126
  "Avg Quality by Industry": ("primary_industry", "lead_quality_score", "mean"),
127
  "Top Accounts by Visits": ("company_name", "total_visits", "sum"),
128
+ "Companies by Country": ("country", "company_name", "count"),
129
+ "Visits by City": ("city", "total_visits", "sum"),
130
+ "Accounts by Assignee": ("assignee", "company_name", "count"),
131
+ "Visits by Assignee": ("assignee", "total_visits", "sum"),
132
  }
133
 
134
  TREND_PRESETS = {
135
+ # -- NEW TRENDS --
136
  "Visits Trend by Campaign": ("total_visits", "sum", "Campaign"),
137
+ "Visits Trend by Quality": ("total_visits", "sum", "Quality_Group"),
138
+
139
+ # -- ORIGINAL TRENDS --
140
+ "Total Visits Trend": ("total_visits", "sum", None),
141
  "Active Accounts Trend": ("company_name", "count", None),
142
  "Avg Lead Quality Trend": ("lead_quality_score", "mean", None),
143
  "Visits Trend by Industry": ("total_visits", "sum", "primary_industry"),
144
+ "Visits Trend by Country": ("total_visits", "sum", "country"),
145
+ "Visits Trend by Assignee": ("total_visits", "sum", "assignee"),
146
  }
147
 
148
  # =====================================================
149
+ # 3. API HANDLING
150
  # =====================================================
151
  def make_request(url, params=None):
152
  retries = 3
153
  while retries > 0:
154
+ r = requests.get(url, headers=HEADERS, params=params, timeout=45)
155
+ if r.status_code == 429:
156
+ print("⏳ Rate limit hit. Sleeping for 60 seconds...")
157
+ time.sleep(61)
 
 
 
 
 
 
 
158
  retries -= 1
159
+ continue
160
+ r.raise_for_status()
161
+ return r.json()
162
+ raise Exception("Max retries exceeded")
163
 
164
  def fetch_basic_leads(start_date, end_date):
165
+ """ Step 1: Fetch ALL company details (fast fetch). """
166
  page = 1
167
  rows = []
168
+ print(f"πŸš€ Fetching full company list for {start_date} to {end_date}...")
169
 
170
  while True:
171
  try:
 
192
  "lead_id": lead.get("id"),
193
  "company_name": a.get("name"),
194
  "website_url": a.get("website_url"),
195
+ "phone": a.get("phone"),
196
+ "business_id": a.get("business_id"),
197
  "primary_industry": a.get("industry"),
198
+ "all_industries": ", ".join([i.get("name") for i in a.get("industries", [])]) if a.get("industries") else None,
199
+ "first_visit_date": a.get("first_visit_date"),
200
  "last_visit_date": a.get("last_visit_date"),
201
  "total_visits": a.get("visits"),
202
  "lead_quality_score": a.get("quality"),
203
+ "revenue": a.get("revenue"),
204
+ "employee_count": a.get("employee_count"),
205
+ "assignee": a.get("assignee"),
206
+ "emailed_to": a.get("emailed_to"),
207
+ "crm_organization_id": a.get("crm_organization_id"),
208
+ "tags": ", ".join(a.get("tags", [])) if a.get("tags") else None,
209
+ "linkedin_url": a.get("linkedin_url"),
210
  "country": loc.get("country"),
211
+ "region": loc.get("region"),
212
  "city": loc.get("city"),
213
+ "leadfeeder_url": a.get("view_in_leadfeeder"),
214
+ # Placeholders for enrichment
215
  "landing_page_path": None,
216
  "exit_page_path": None
217
  })
218
+ print(f"βœ… Page {page} loaded. Rows: {len(rows)}")
219
  page += 1
220
  except Exception as e:
221
+ print(f"Error on page {page}: {e}")
222
  break
223
 
224
  df = pd.DataFrame(rows)
 
227
  return df
228
 
229
  def enrich_leads_with_visits(df, start_date, end_date, max_rows=None, progress=gr.Progress()):
230
+ """ Step 2: Deep enrich with visit_route parsing for Exit/Landing Page. """
231
  if df.empty: return df
232
  target_df = df.head(max_rows) if max_rows else df
233
  total = len(target_df)
234
 
235
+ print(f"πŸ•΅οΈ Deep enriching {total} rows ({start_date} to {end_date})...")
236
 
237
  for index, row in target_df.iterrows():
238
+ if row.get("landing_page_path") and row.get("exit_page_path"): continue
 
239
 
240
  lead_id = row["lead_id"]
241
  try:
 
242
  visit_data = make_request(
243
  f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits",
244
+ params={
245
+ "start_date": start_date, "end_date": end_date,
246
+ "page[size]": 1, "include": "page_views"
247
+ }
248
  )
249
  visits = visit_data.get("data", [])
250
+ included = visit_data.get("included", [])
251
+ landing = None
252
+ exit_p = None
253
+
254
  if visits:
255
+ visit = visits[0]
256
+ v_attrs = visit.get("attributes", {})
257
 
258
+ # A. Direct Attributes
259
+ landing = v_attrs.get("landing_page_path") or v_attrs.get("landing_page_url")
260
+
261
+ # B. Parse 'visit_route'
262
+ visit_route = v_attrs.get("visit_route", [])
263
+ if visit_route and isinstance(visit_route, list):
264
+ last_step = visit_route[-1]
265
+ if not exit_p: exit_p = last_step.get("page_path") or last_step.get("page_url")
266
+ if not landing:
267
+ first_step = visit_route[0]
268
+ landing = first_step.get("page_path") or first_step.get("page_url")
269
+
270
+ # C. Deep Link via 'included' Page Views
271
+ if not landing or not exit_p:
272
+ pv_map = {p["id"]: p["attributes"] for p in included if p["type"] == "page_views"}
273
+ pv_ids = [r["id"] for r in visit.get("relationships", {}).get("page_views", {}).get("data", [])]
274
+
275
+ if pv_ids:
276
+ if not landing:
277
+ first_pv = pv_map.get(pv_ids[0])
278
+ if first_pv: landing = first_pv.get("url") or first_pv.get("path")
279
+ if not exit_p:
280
+ last_pv = pv_map.get(pv_ids[-1])
281
+ if last_pv: exit_p = last_pv.get("url") or last_pv.get("path")
282
 
283
  df.at[index, "landing_page_path"] = landing
284
+ df.at[index, "exit_page_path"] = exit_p
285
+ except Exception as e:
286
+ print(f"Failed to enrich lead {lead_id}: {e}")
287
 
288
  if max_rows and index % 5 == 0:
289
+ progress(index / total, desc="Enriching...")
290
 
291
  return df
292
 
293
  # =====================================================
294
+ # 4. MAIN FUNCTIONS
295
  # =====================================================
296
+ def load_preview(start, end):
297
+ # 1. Fetch
298
  df = fetch_basic_leads(start, end)
299
  if df.empty: return df, pd.DataFrame(), "⚠️ No data found."
300
 
301
+ # 2. Enrich top 20 for preview speed
 
302
  df_preview = df.copy()
303
  df_preview = enrich_leads_with_visits(df_preview, start, end, max_rows=20)
304
 
305
+ # 3. Apply New Business Logic (Campaign/Quality)
306
+ df_preview = apply_business_logic(df_preview)
307
 
308
+ status = f"βœ… Loaded **{len(df):,} companies**. Preview shows top 20 rows enriched."
 
 
 
 
 
 
 
 
 
 
 
309
  return df, df_preview.head(20), status
310
 
311
  def download_full_excel(df, start, end):
312
  if df is None or df.empty: return None
313
+ print("⏳ Starting full enrichment for Excel export...")
314
+
315
+ # 1. Enrich ALL rows (Time consuming but necessary for Campaign logic)
316
+ enriched_df = enrich_leads_with_visits(df.copy(), start, end)
317
 
318
+ # 2. Apply Business Logic
319
+ final_df = apply_business_logic(enriched_df)
 
 
320
 
321
  path = "/tmp/leadfeeder_campaign_data.xlsx"
322
  final_df.to_excel(path, index=False)
323
  return path
324
 
325
+ def inspect_raw_json(lead_id, start_date, end_date):
326
+ if not lead_id: return "Please enter a Lead ID"
327
+ try:
328
+ url = f"{BASE_URL}/accounts/{ACCOUNT_ID}/leads/{lead_id}/visits"
329
+ params = {"start_date": start_date, "end_date": end_date, "page[size]": 1, "include": "page_views"}
330
+ r = requests.get(url, headers=HEADERS, params=params)
331
+ return json.dumps(r.json(), indent=2)
332
+ except Exception as e:
333
+ return str(e)
334
+
335
  # =====================================================
336
+ # 5. CHART ENGINES
337
  # =====================================================
338
+ def build_kpis(df):
339
+ if df is None or df.empty: return 0, 0, 0, 0, 0, 0
340
+ # Apply logic if not present
341
+ if "Quality_Group" not in df.columns:
342
+ df = apply_business_logic(df)
343
+
344
+ return (
345
+ len(df),
346
+ df["total_visits"].gt(0).sum(),
347
+ int(df["total_visits"].sum()),
348
+ round(df["lead_quality_score"].mean(), 2),
349
+ round(df["crm_organization_id"].notna().mean() * 100, 1),
350
+ df[df["Quality_Group"] == "High Quality (8-10)"].shape[0], # Count of High Quality
351
+ )
352
+
353
  def build_dashboard(df, preset, top_n):
354
  if df is None or df.empty: return px.bar(title="No Data")
355
 
356
+ # Ensure columns exist
357
+ if "Campaign" not in df.columns or "Quality_Group" not in df.columns:
358
+ # Warn if viewing campaign data without full enrichment
359
+ if "Campaign" in preset:
360
+ return px.bar(title="⚠️ Please Download Excel first to enrich Campaigns for all rows.")
361
+ df = apply_business_logic(df)
362
+
363
  dim, metric, agg = DASHBOARD_PRESETS[preset]
364
 
 
 
 
 
365
  if agg == "count":
366
  grouped = df.groupby(dim, dropna=False).size().reset_index(name="value")
367
  else:
368
  grouped = df.groupby(dim, dropna=False)[metric].agg(agg).reset_index(name="value")
369
 
370
+ return px.bar(grouped.sort_values("value", ascending=False).head(top_n), x=dim, y="value", title=preset, color=dim)
 
371
 
372
+ def get_trend_filter_options(df, preset):
373
+ if df is None or df.empty: return gr.update(choices=[], value=None, visible=False)
374
 
375
  metric, agg, segment = TREND_PRESETS[preset]
376
+ if not segment: return gr.update(choices=[], value=None, visible=False)
377
+
378
+ # Ensure logic applied
379
+ if segment not in df.columns: df = apply_business_logic(df)
380
+
381
+ options = sorted(df[segment].astype(str).unique().tolist())
382
+ return gr.update(choices=options, value=options[:5], visible=True, label=f"Filter {segment}")
383
+
384
+ def build_trend(df, preset, grain, filter_values):
385
+ if df is None or df.empty: return px.line(title="No Data Loaded")
386
 
387
+ metric, agg, segment = TREND_PRESETS[preset]
388
+ df = df.dropna(subset=["last_visit_date"]).copy()
389
+
390
+ if segment:
391
+ if segment not in df.columns: df = apply_business_logic(df)
392
+ if filter_values:
393
+ df = df[df[segment].isin(filter_values)]
394
+
395
  if grain == "Weekly":
396
+ df["period"] = df["last_visit_date"].dt.to_period("W").astype(str)
397
+ elif grain == "Monthly":
398
+ df["period"] = df["last_visit_date"].dt.to_period("M").astype(str)
399
  else:
400
+ df["period"] = df["last_visit_date"].dt.date
401
 
402
  val = metric
403
  if agg == "count":
404
+ df["_v"] = 1
405
  val = "_v"
406
 
407
+ if segment:
408
+ ts = df.groupby(["period", segment])[val].agg(agg).reset_index()
409
+ return px.line(ts, x="period", y=val, color=segment, template="plotly_white", title=preset)
410
 
411
+ ts = df.groupby("period")[val].agg(agg).reset_index()
412
+ return px.line(ts, x="period", y=val, markers=True, template="plotly_white", title=preset)
413
 
414
  # =====================================================
415
+ # 6. UI LAYOUT
416
  # =====================================================
417
+ with gr.Blocks(title="Leadfeeder Campaign Pro") as demo:
418
+ gr.Markdown("## πŸš€ Leadfeeder Analytics & Campaign Manager")
419
 
 
420
  with gr.Row():
421
+ pwd = gr.Textbox(type="password", label="App Password")
422
+ gr.Button("Auth").click(lambda p: gr.Info("Success") if p==APP_PASSWORD else gr.Error("Invalid"), pwd, None)
423
+ df_state = gr.State()
424
 
425
+ status = gr.Markdown()
 
426
 
427
  with gr.Tabs():
428
  # --- TAB 1: DATA ---
429
  with gr.Tab("πŸ“‹ Data & Report"):
430
  with gr.Row():
431
+ start = gr.Textbox(label="Start Date", value=(date.today()-timedelta(days=30)).isoformat())
432
+ end = gr.Textbox(label="End Date", value=date.today().isoformat())
433
 
434
  with gr.Row():
435
+ btn_load = gr.Button("1. Load Data (Preview)", variant="primary")
436
+ btn_dl = gr.Button("2. Enrich & Download Full Excel")
437
+ file_dl = gr.File(label="Download Excel")
 
 
 
 
 
 
438
 
439
+ table = gr.Dataframe(label="Preview (Top 20 Enriched)", interactive=True)
440
+ btn_load.click(load_preview, [start, end], [df_state, table, status])
441
+ btn_dl.click(download_full_excel, [df_state, start, end], file_dl)
442
+
443
  # --- TAB 2: DASHBOARD ---
444
  with gr.Tab("πŸ“Š Dashboard"):
445
+ kpis = [gr.Number(label=l) for l in ["Total Companies", "Active Companies", "Total Visits", "Avg Quality", "CRM %", "High Quality Leads (8-10)"]]
446
+ gr.Button("Refresh KPIs").click(build_kpis, df_state, kpis)
447
+
448
  with gr.Row():
449
+ preset = gr.Dropdown(choices=list(DASHBOARD_PRESETS.keys()), label="Chart", value="Visits by Campaign")
450
+ top_n = gr.Slider(5, 50, value=10, label="Top N")
 
 
451
 
452
+ chart = gr.Plot()
453
+ gr.Button("Build View").click(build_dashboard, [df_state, preset, top_n], chart)
454
+
455
  # --- TAB 3: TRENDS ---
456
  with gr.Tab("πŸ“ˆ Trends"):
457
  with gr.Row():
458
+ trend_view = gr.Dropdown(choices=list(TREND_PRESETS.keys()), label="Select Trend", value="Visits Trend by Campaign")
459
+ grain = gr.Radio(["Daily", "Weekly", "Monthly"], value="Daily", label="Granularity")
 
 
460
 
461
+ filter_dropdown = gr.Dropdown(multiselect=True, visible=False, label="Filter Segments")
462
+ plot = gr.Plot()
 
 
463
 
464
+ trend_view.change(get_trend_filter_options, [df_state, trend_view], filter_dropdown)
465
+ gr.Button("Show Trend", variant="primary").click(build_trend, [df_state, trend_view, grain, filter_dropdown], plot)
466
+
467
+ # --- TAB 4: SETTINGS (NEW) ---
468
+ with gr.Tab("βš™οΈ Campaign Settings"):
469
+ gr.Markdown("### Manage Campaign Groups & Keywords")
470
+ gr.Markdown("Modify the JSON below to add new groups or keywords. Click 'Save' to apply changes.")
471
 
472
+ # Load current rules into text box
473
+ current_rules = json.dumps(load_campaign_rules(), indent=4)
474
+ config_editor = gr.Code(value=current_rules, language="json", label="Campaign Configuration")
475
+ save_config_btn = gr.Button("πŸ’Ύ Save Configuration")
476
  config_status = gr.Markdown()
477
 
478
+ save_config_btn.click(save_campaign_rules, config_editor, [config_status, gr.State()])
479
+
480
+ # --- TAB 5: DEBUGGER ---
481
+ with gr.Tab("πŸ› οΈ Debugger"):
482
+ gr.Markdown("Inspect raw API data for a specific lead.")
483
+ dbg_id = gr.Textbox(label="Lead ID")
484
+ dbg_btn = gr.Button("Inspect Raw JSON")
485
+ dbg_out = gr.Code(language="json")
486
+ dbg_btn.click(inspect_raw_json, [dbg_id, start, end], dbg_out)
487
 
488
  if __name__ == "__main__":
489
  demo.launch(server_name="0.0.0.0", server_port=7860)