Mathias Claude Opus 4.5 commited on
Commit
d378246
·
1 Parent(s): 8e9f28f

Add Prospects & Discovery columns + column auto-detection

Browse files

- Add Prospects % and Discovery % columns to dashboard (6 activity columns total)
- Show "No target" for missing activity rows
- Add column_config.json for dynamic column configuration
- Add detect_columns.py script for monthly config updates
- Add /api/reload-config and /api/column-config endpoints
- Update leaderboard scoring to include all activity types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (4) hide show
  1. app.py +132 -8
  2. column_config.json +15 -0
  3. detect_columns.py +231 -0
  4. static/index.html +17 -5
app.py CHANGED
@@ -52,15 +52,87 @@ KNOWN_ACTIVITIES = {
52
  'prospects (activated)', 'discovery', 'sql', 'sql (offer sent)'
53
  }
54
 
55
- # Week configuration: (week_number, daily_start, daily_end, target_col, percentage_col)
56
- # Each week block = 5 daily columns + target column + percentage column
57
- WEEK_CONFIGS = [
 
 
 
58
  (2, 3, 7, 8, 9), # Week 2: daily D-H (3-7), target I (8), pct J (9)
59
  (3, 10, 14, 15, 16), # Week 3: daily K-O (10-14), target P (15), pct Q (16)
60
  (4, 17, 21, 22, 23), # Week 4: daily R-V (17-21), target W (22), pct X (23)
61
  (5, 24, 28, 30, 31), # Week 5: daily Y-AC (24-28), target AE (30), pct AF (31) - extra empty col
62
  ]
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  def get_activity(row):
66
  """Extract activity type from row (column C, index 2)."""
@@ -162,7 +234,8 @@ def process_activity_row(row, case_name, gs_name, case_data):
162
  }
163
 
164
  # Extract weekly data: actual (sum of daily), target, and percentage from sheet
165
- for week_num, daily_start, daily_end, target_col, pct_col in WEEK_CONFIGS:
 
166
  actual = sum_daily(row, daily_start, daily_end)
167
  target = safe_int(row, target_col)
168
  percentage = extract_percentage(row, pct_col)
@@ -176,15 +249,16 @@ def process_activity_row(row, case_name, gs_name, case_data):
176
  if percentage is not None:
177
  case_data[key]["weeks"][week_num][f"{activity_key}Pct"] = percentage
178
 
179
- # Get monthly target and actual from columns AG (32) and AH (33)
180
- monthly_target = safe_int(row, 32)
181
- monthly_actual = safe_int(row, 33)
 
182
 
183
  # Update monthly totals
184
  if activity_key == "sql":
185
  case_data[key]["monthlyTotal"]["sql"] = monthly_actual
186
  case_data[key]["monthlyTotal"]["sqlTarget"] = monthly_target
187
- elif activity_key in ["calls", "emails", "linkedin", "prospects"]:
188
  # Activity includes all non-SQL activities
189
  case_data[key]["monthlyTotal"]["activity"] += monthly_actual
190
  case_data[key]["monthlyTotal"]["activityTarget"] += monthly_target
@@ -369,6 +443,56 @@ async def cache_status():
369
  return JSONResponse(content={"cached": False})
370
 
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  @app.get("/api/debug")
373
  async def debug_data():
374
  """Debug endpoint to see raw sheet data structure and parsed blocks."""
 
52
  'prospects (activated)', 'discovery', 'sql', 'sql (offer sent)'
53
  }
54
 
55
+ # Column configuration file path
56
+ CONFIG_FILE = "column_config.json"
57
+
58
+ # Default week configuration (fallback if config file not found)
59
+ # Format: (week_number, daily_start, daily_end, target_col, percentage_col)
60
+ DEFAULT_WEEK_CONFIGS = [
61
  (2, 3, 7, 8, 9), # Week 2: daily D-H (3-7), target I (8), pct J (9)
62
  (3, 10, 14, 15, 16), # Week 3: daily K-O (10-14), target P (15), pct Q (16)
63
  (4, 17, 21, 22, 23), # Week 4: daily R-V (17-21), target W (22), pct X (23)
64
  (5, 24, 28, 30, 31), # Week 5: daily Y-AC (24-28), target AE (30), pct AF (31) - extra empty col
65
  ]
66
 
67
+ # Default monthly column configuration (fallback)
68
+ DEFAULT_MONTHLY_CONFIG = {"target_col": 32, "actual_col": 33}
69
+
70
+ # Global column configuration (loaded at startup, can be reloaded)
71
+ _column_config = {"weeks": None, "monthly": None, "loaded_at": None}
72
+
73
+
74
+ def load_column_config(force_reload=False):
75
+ """
76
+ Load column configuration from JSON file.
77
+ Falls back to hardcoded defaults if file not found or invalid.
78
+ """
79
+ global _column_config
80
+
81
+ if _column_config["weeks"] is not None and not force_reload:
82
+ return _column_config
83
+
84
+ try:
85
+ with open(CONFIG_FILE, "r") as f:
86
+ config = json.load(f)
87
+
88
+ # Convert weeks config to tuple format
89
+ weeks = []
90
+ for w in config.get("weeks", []):
91
+ weeks.append((
92
+ w["week_num"],
93
+ w["daily_start"],
94
+ w["daily_end"],
95
+ w["target_col"],
96
+ w["pct_col"]
97
+ ))
98
+
99
+ monthly = config.get("monthly", DEFAULT_MONTHLY_CONFIG)
100
+
101
+ _column_config["weeks"] = weeks if weeks else DEFAULT_WEEK_CONFIGS
102
+ _column_config["monthly"] = monthly
103
+ _column_config["loaded_at"] = datetime.now().isoformat()
104
+ print(f"Column config loaded from {CONFIG_FILE}: {len(weeks)} weeks")
105
+
106
+ except FileNotFoundError:
107
+ print(f"Config file {CONFIG_FILE} not found, using defaults")
108
+ _column_config["weeks"] = DEFAULT_WEEK_CONFIGS
109
+ _column_config["monthly"] = DEFAULT_MONTHLY_CONFIG
110
+ _column_config["loaded_at"] = datetime.now().isoformat()
111
+
112
+ except Exception as e:
113
+ print(f"Error loading config: {e}, using defaults")
114
+ _column_config["weeks"] = DEFAULT_WEEK_CONFIGS
115
+ _column_config["monthly"] = DEFAULT_MONTHLY_CONFIG
116
+ _column_config["loaded_at"] = datetime.now().isoformat()
117
+
118
+ return _column_config
119
+
120
+
121
+ def get_week_configs():
122
+ """Get the current week configurations."""
123
+ config = load_column_config()
124
+ return config["weeks"]
125
+
126
+
127
+ def get_monthly_config():
128
+ """Get the current monthly column configuration."""
129
+ config = load_column_config()
130
+ return config["monthly"]
131
+
132
+
133
+ # Load config at module initialization
134
+ load_column_config()
135
+
136
 
137
  def get_activity(row):
138
  """Extract activity type from row (column C, index 2)."""
 
234
  }
235
 
236
  # Extract weekly data: actual (sum of daily), target, and percentage from sheet
237
+ week_configs = get_week_configs()
238
+ for week_num, daily_start, daily_end, target_col, pct_col in week_configs:
239
  actual = sum_daily(row, daily_start, daily_end)
240
  target = safe_int(row, target_col)
241
  percentage = extract_percentage(row, pct_col)
 
249
  if percentage is not None:
250
  case_data[key]["weeks"][week_num][f"{activity_key}Pct"] = percentage
251
 
252
+ # Get monthly target and actual from config
253
+ monthly_config = get_monthly_config()
254
+ monthly_target = safe_int(row, monthly_config["target_col"])
255
+ monthly_actual = safe_int(row, monthly_config["actual_col"])
256
 
257
  # Update monthly totals
258
  if activity_key == "sql":
259
  case_data[key]["monthlyTotal"]["sql"] = monthly_actual
260
  case_data[key]["monthlyTotal"]["sqlTarget"] = monthly_target
261
+ elif activity_key in ["calls", "emails", "linkedin", "prospects", "discovery"]:
262
  # Activity includes all non-SQL activities
263
  case_data[key]["monthlyTotal"]["activity"] += monthly_actual
264
  case_data[key]["monthlyTotal"]["activityTarget"] += monthly_target
 
443
  return JSONResponse(content={"cached": False})
444
 
445
 
446
+ @app.post("/api/reload-config")
447
+ async def reload_config(request: Request):
448
+ """
449
+ Reload column configuration from column_config.json.
450
+ Also invalidates the data cache to force a fresh fetch with new config.
451
+ """
452
+ global _cache
453
+
454
+ # Optional: verify webhook secret if configured
455
+ if WEBHOOK_SECRET:
456
+ auth_header = request.headers.get("X-Webhook-Secret", "")
457
+ if not hmac.compare_digest(auth_header, WEBHOOK_SECRET):
458
+ raise HTTPException(status_code=401, detail="Invalid webhook secret")
459
+
460
+ # Reload the column configuration
461
+ config = load_column_config(force_reload=True)
462
+
463
+ # Invalidate data cache to use new config
464
+ _cache["data"] = None
465
+ _cache["timestamp"] = None
466
+
467
+ return JSONResponse(content={
468
+ "success": True,
469
+ "message": "Column config reloaded, cache invalidated",
470
+ "config_loaded_at": config["loaded_at"],
471
+ "weeks_count": len(config["weeks"]),
472
+ "timestamp": datetime.now().isoformat()
473
+ })
474
+
475
+
476
+ @app.get("/api/column-config")
477
+ async def get_column_config():
478
+ """Return current column configuration."""
479
+ config = load_column_config()
480
+ return JSONResponse(content={
481
+ "weeks": [
482
+ {
483
+ "week_num": w[0],
484
+ "daily_start": w[1],
485
+ "daily_end": w[2],
486
+ "target_col": w[3],
487
+ "pct_col": w[4]
488
+ }
489
+ for w in config["weeks"]
490
+ ],
491
+ "monthly": config["monthly"],
492
+ "loaded_at": config["loaded_at"]
493
+ })
494
+
495
+
496
  @app.get("/api/debug")
497
  async def debug_data():
498
  """Debug endpoint to see raw sheet data structure and parsed blocks."""
column_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "generated_at": "2026-01-23T10:30:00Z",
3
+ "sheet_id": "1af6-2KsRqeTQxdw5KVRp2WCrM6RT7HIcl70m-GgGZB4",
4
+ "sheet_name": "DAILY - for SDR to add data\ud83c\udf1f",
5
+ "weeks": [
6
+ {"week_num": 2, "daily_start": 3, "daily_end": 7, "target_col": 8, "pct_col": 9},
7
+ {"week_num": 3, "daily_start": 10, "daily_end": 14, "target_col": 15, "pct_col": 16},
8
+ {"week_num": 4, "daily_start": 17, "daily_end": 21, "target_col": 22, "pct_col": 23},
9
+ {"week_num": 5, "daily_start": 24, "daily_end": 28, "target_col": 30, "pct_col": 31}
10
+ ],
11
+ "monthly": {
12
+ "target_col": 32,
13
+ "actual_col": 33
14
+ }
15
+ }
detect_columns.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Column Auto-Detection Script for SDR Status Tracker
4
+
5
+ Reads header rows from the Google Sheet and generates column_config.json
6
+ with detected week configurations and monthly column positions.
7
+
8
+ Usage:
9
+ python detect_columns.py # Generate config
10
+ python detect_columns.py --dry-run # Preview without writing file
11
+ """
12
+ import os
13
+ import re
14
+ import json
15
+ import argparse
16
+ from datetime import datetime
17
+ from google.oauth2 import service_account
18
+ from googleapiclient.discovery import build
19
+
20
+
21
+ # Configuration
22
+ SHEET_ID = os.environ.get("GOOGLE_SHEET_ID", "1af6-2KsRqeTQxdw5KVRp2WCrM6RT7HIcl70m-GgGZB4")
23
+ SHEET_NAME = "DAILY - for SDR to add data🌟"
24
+ CONFIG_FILE = "column_config.json"
25
+
26
+
27
+ def get_sheets_service():
28
+ """Create Google Sheets API service using service account credentials."""
29
+ creds_json = os.environ.get("GOOGLE_CREDENTIALS")
30
+ if not creds_json:
31
+ raise RuntimeError("GOOGLE_CREDENTIALS environment variable not set")
32
+
33
+ creds_dict = json.loads(creds_json)
34
+ credentials = service_account.Credentials.from_service_account_info(
35
+ creds_dict,
36
+ scopes=["https://www.googleapis.com/auth/spreadsheets.readonly"]
37
+ )
38
+ return build("sheets", "v4", credentials=credentials)
39
+
40
+
41
+ def fetch_header_rows(service):
42
+ """Fetch the first 4 rows from the sheet (headers)."""
43
+ result = service.spreadsheets().values().get(
44
+ spreadsheetId=SHEET_ID,
45
+ range=f"'{SHEET_NAME}'!A1:AZ4" # Wide range to capture all columns
46
+ ).execute()
47
+ return result.get("values", [])
48
+
49
+
50
+ def detect_week_markers(row1):
51
+ """
52
+ Detect WEEK markers from Row 1.
53
+ Returns list of (week_num, start_col) tuples.
54
+ Pattern: "WEEK 2", "WEEK 3", etc.
55
+ """
56
+ week_pattern = re.compile(r"WEEK\s*(\d+)", re.IGNORECASE)
57
+ weeks = []
58
+
59
+ for col_idx, cell in enumerate(row1):
60
+ if cell:
61
+ match = week_pattern.search(str(cell))
62
+ if match:
63
+ week_num = int(match.group(1))
64
+ weeks.append((week_num, col_idx))
65
+
66
+ return weeks
67
+
68
+
69
+ def detect_daily_columns(row4, week_start_col, next_week_start=None):
70
+ """
71
+ Detect daily columns within a week block.
72
+ Daily columns contain day-of-month numbers (1-31) in Row 4.
73
+ Returns (daily_start, daily_end, target_col, pct_col).
74
+ """
75
+ # Search range: from week_start to next_week_start (or end of row)
76
+ end_col = next_week_start if next_week_start else len(row4)
77
+
78
+ daily_start = None
79
+ daily_end = None
80
+ target_col = None
81
+ pct_col = None
82
+
83
+ for col_idx in range(week_start_col, end_col):
84
+ if col_idx >= len(row4):
85
+ break
86
+
87
+ cell = str(row4[col_idx]).strip().lower() if col_idx < len(row4) else ""
88
+
89
+ # Check if it's a day number (1-31)
90
+ if cell.isdigit() and 1 <= int(cell) <= 31:
91
+ if daily_start is None:
92
+ daily_start = col_idx
93
+ daily_end = col_idx
94
+
95
+ # Check for TARGET column
96
+ if "target" in cell or cell == "t":
97
+ target_col = col_idx
98
+
99
+ # Check for PERCENTAGE column (often contains % or "pct" or just a number)
100
+ if "%" in cell or "pct" in cell.lower():
101
+ pct_col = col_idx
102
+
103
+ # If no explicit pct_col found, it's usually right after target
104
+ if target_col is not None and pct_col is None:
105
+ pct_col = target_col + 1
106
+
107
+ return daily_start, daily_end, target_col, pct_col
108
+
109
+
110
+ def detect_monthly_columns(row4):
111
+ """
112
+ Detect monthly TARGET and ACTUAL columns.
113
+ These are typically labeled "TARGET" and "ACTUAL" (or similar) near the end.
114
+ """
115
+ target_col = None
116
+ actual_col = None
117
+
118
+ for col_idx, cell in enumerate(row4):
119
+ cell_str = str(cell).strip().lower() if cell else ""
120
+
121
+ # Look for monthly target (usually labeled differently from weekly)
122
+ if "monthly" in cell_str or (col_idx > 25 and "target" in cell_str):
123
+ if target_col is None:
124
+ target_col = col_idx
125
+
126
+ # Look for monthly actual
127
+ if "actual" in cell_str or (col_idx > 25 and col_idx == target_col + 1):
128
+ actual_col = col_idx
129
+
130
+ # Default fallback based on current known structure
131
+ if target_col is None:
132
+ target_col = 32 # Column AG
133
+ if actual_col is None:
134
+ actual_col = 33 # Column AH
135
+
136
+ return target_col, actual_col
137
+
138
+
139
+ def detect_columns():
140
+ """
141
+ Main detection function.
142
+ Returns the detected configuration as a dictionary.
143
+ """
144
+ print("Connecting to Google Sheets API...")
145
+ service = get_sheets_service()
146
+
147
+ print(f"Fetching headers from sheet: {SHEET_NAME}")
148
+ headers = fetch_header_rows(service)
149
+
150
+ if len(headers) < 4:
151
+ raise RuntimeError(f"Expected at least 4 header rows, got {len(headers)}")
152
+
153
+ row1 = headers[0] if len(headers) > 0 else []
154
+ row4 = headers[3] if len(headers) > 3 else []
155
+
156
+ print(f"Row 1 has {len(row1)} columns")
157
+ print(f"Row 4 has {len(row4)} columns")
158
+
159
+ # Detect week markers
160
+ week_markers = detect_week_markers(row1)
161
+ print(f"Detected {len(week_markers)} week markers: {week_markers}")
162
+
163
+ # Detect columns for each week
164
+ weeks_config = []
165
+ for i, (week_num, start_col) in enumerate(week_markers):
166
+ # Get next week's start column (or None for last week)
167
+ next_start = week_markers[i + 1][1] if i + 1 < len(week_markers) else None
168
+
169
+ daily_start, daily_end, target_col, pct_col = detect_daily_columns(
170
+ row4, start_col, next_start
171
+ )
172
+
173
+ week_config = {
174
+ "week_num": week_num,
175
+ "daily_start": daily_start,
176
+ "daily_end": daily_end,
177
+ "target_col": target_col,
178
+ "pct_col": pct_col
179
+ }
180
+ weeks_config.append(week_config)
181
+
182
+ print(f" Week {week_num}: daily={daily_start}-{daily_end}, target={target_col}, pct={pct_col}")
183
+
184
+ # Detect monthly columns
185
+ monthly_target, monthly_actual = detect_monthly_columns(row4)
186
+ print(f"Monthly columns: target={monthly_target}, actual={monthly_actual}")
187
+
188
+ config = {
189
+ "generated_at": datetime.utcnow().isoformat() + "Z",
190
+ "sheet_id": SHEET_ID,
191
+ "sheet_name": SHEET_NAME,
192
+ "weeks": weeks_config,
193
+ "monthly": {
194
+ "target_col": monthly_target,
195
+ "actual_col": monthly_actual
196
+ }
197
+ }
198
+
199
+ return config
200
+
201
+
202
+ def main():
203
+ parser = argparse.ArgumentParser(description="Detect column mappings from Google Sheet")
204
+ parser.add_argument("--dry-run", action="store_true", help="Preview config without writing file")
205
+ args = parser.parse_args()
206
+
207
+ try:
208
+ config = detect_columns()
209
+
210
+ print("\n" + "=" * 50)
211
+ print("Detected Configuration:")
212
+ print("=" * 50)
213
+ print(json.dumps(config, indent=2))
214
+
215
+ if args.dry_run:
216
+ print("\n[DRY RUN] Config not written to file")
217
+ else:
218
+ # Write config file
219
+ with open(CONFIG_FILE, "w") as f:
220
+ json.dump(config, f, indent=2)
221
+ print(f"\nConfig written to {CONFIG_FILE}")
222
+
223
+ return 0
224
+
225
+ except Exception as e:
226
+ print(f"Error: {e}")
227
+ return 1
228
+
229
+
230
+ if __name__ == "__main__":
231
+ exit(main())
static/index.html CHANGED
@@ -325,7 +325,7 @@
325
  <tr>
326
  <th>SDR</th>
327
  <th>Case</th>
328
- <th colspan="4" style="text-align: center; background: var(--zodiac-100);">Current Week</th>
329
  <th colspan="2" class="weekly-monthly-divider" style="text-align: center; background: var(--emerald-100);">Monthly Totals</th>
330
  </tr>
331
  <tr>
@@ -335,6 +335,8 @@
335
  <th class="progress-cell">Calls %</th>
336
  <th class="progress-cell">Emails %</th>
337
  <th class="progress-cell">LinkedIn %</th>
 
 
338
  <th class="progress-cell weekly-monthly-divider">SQL %</th>
339
  <th class="progress-cell">Activity %</th>
340
  </tr>
@@ -670,12 +672,12 @@
670
 
671
  function showLoadingState() {
672
  const tbody = document.getElementById('caseTable');
673
- tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; padding: 40px; color: var(--text-muted);">Loading data from Google Sheets...</td></tr>';
674
  }
675
 
676
  function showErrorState(message) {
677
  const tbody = document.getElementById('caseTable');
678
- tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; padding: 40px; color: var(--danger);">${message}</td></tr>`;
679
  }
680
 
681
  // Try to load from cache first for immediate display
@@ -792,13 +794,15 @@
792
  // Returns null for metrics without targets
793
  function getWeeklyProgress(c, weekNum) {
794
  const week = c.weeks[weekNum];
795
- if (!week) return { sql: null, calls: null, emails: null, linkedin: null };
796
 
797
  return {
798
  sql: week.sqlTarget > 0 ? Math.round((week.sql / week.sqlTarget) * 100) : null,
799
  calls: week.callsTarget > 0 ? Math.round((week.calls / week.callsTarget) * 100) : null,
800
  emails: week.emailsTarget > 0 ? Math.round((week.emails / week.emailsTarget) * 100) : null,
801
- linkedin: week.linkedinTarget > 0 ? Math.round((week.linkedin / week.linkedinTarget) * 100) : null
 
 
802
  };
803
  }
804
 
@@ -894,6 +898,8 @@
894
  <td class="progress-cell">${renderProgressBar(weekProgress.calls, true)}</td>
895
  <td class="progress-cell">${renderProgressBar(weekProgress.emails, true)}</td>
896
  <td class="progress-cell">${renderProgressBar(weekProgress.linkedin, true)}</td>
 
 
897
  <td class="progress-cell weekly-monthly-divider">${renderProgressBar(monthlySQLProgress, false)}</td>
898
  <td class="progress-cell">${renderProgressBar(monthlyActivityProgress, false)}</td>
899
  `;
@@ -963,6 +969,12 @@
963
  if (week.linkedinTarget > 0) {
964
  targets.push({ actual: week.linkedin || 0, target: week.linkedinTarget });
965
  }
 
 
 
 
 
 
966
  });
967
 
968
  weeklyByGS[gs] = calculateScore(targets);
 
325
  <tr>
326
  <th>SDR</th>
327
  <th>Case</th>
328
+ <th colspan="6" style="text-align: center; background: var(--zodiac-100);">Current Week</th>
329
  <th colspan="2" class="weekly-monthly-divider" style="text-align: center; background: var(--emerald-100);">Monthly Totals</th>
330
  </tr>
331
  <tr>
 
335
  <th class="progress-cell">Calls %</th>
336
  <th class="progress-cell">Emails %</th>
337
  <th class="progress-cell">LinkedIn %</th>
338
+ <th class="progress-cell">Prospects %</th>
339
+ <th class="progress-cell">Discovery %</th>
340
  <th class="progress-cell weekly-monthly-divider">SQL %</th>
341
  <th class="progress-cell">Activity %</th>
342
  </tr>
 
672
 
673
  function showLoadingState() {
674
  const tbody = document.getElementById('caseTable');
675
+ tbody.innerHTML = '<tr><td colspan="10" style="text-align: center; padding: 40px; color: var(--text-muted);">Loading data from Google Sheets...</td></tr>';
676
  }
677
 
678
  function showErrorState(message) {
679
  const tbody = document.getElementById('caseTable');
680
+ tbody.innerHTML = `<tr><td colspan="10" style="text-align: center; padding: 40px; color: var(--danger);">${message}</td></tr>`;
681
  }
682
 
683
  // Try to load from cache first for immediate display
 
794
  // Returns null for metrics without targets
795
  function getWeeklyProgress(c, weekNum) {
796
  const week = c.weeks[weekNum];
797
+ if (!week) return { sql: null, calls: null, emails: null, linkedin: null, prospects: null, discovery: null };
798
 
799
  return {
800
  sql: week.sqlTarget > 0 ? Math.round((week.sql / week.sqlTarget) * 100) : null,
801
  calls: week.callsTarget > 0 ? Math.round((week.calls / week.callsTarget) * 100) : null,
802
  emails: week.emailsTarget > 0 ? Math.round((week.emails / week.emailsTarget) * 100) : null,
803
+ linkedin: week.linkedinTarget > 0 ? Math.round((week.linkedin / week.linkedinTarget) * 100) : null,
804
+ prospects: week.prospectsTarget > 0 ? Math.round((week.prospects / week.prospectsTarget) * 100) : null,
805
+ discovery: week.discoveryTarget > 0 ? Math.round((week.discovery / week.discoveryTarget) * 100) : null
806
  };
807
  }
808
 
 
898
  <td class="progress-cell">${renderProgressBar(weekProgress.calls, true)}</td>
899
  <td class="progress-cell">${renderProgressBar(weekProgress.emails, true)}</td>
900
  <td class="progress-cell">${renderProgressBar(weekProgress.linkedin, true)}</td>
901
+ <td class="progress-cell">${renderProgressBar(weekProgress.prospects, true)}</td>
902
+ <td class="progress-cell">${renderProgressBar(weekProgress.discovery, true)}</td>
903
  <td class="progress-cell weekly-monthly-divider">${renderProgressBar(monthlySQLProgress, false)}</td>
904
  <td class="progress-cell">${renderProgressBar(monthlyActivityProgress, false)}</td>
905
  `;
 
969
  if (week.linkedinTarget > 0) {
970
  targets.push({ actual: week.linkedin || 0, target: week.linkedinTarget });
971
  }
972
+ if (week.prospectsTarget > 0) {
973
+ targets.push({ actual: week.prospects || 0, target: week.prospectsTarget });
974
+ }
975
+ if (week.discoveryTarget > 0) {
976
+ targets.push({ actual: week.discovery || 0, target: week.discoveryTarget });
977
+ }
978
  });
979
 
980
  weeklyByGS[gs] = calculateScore(targets);