stepolan commited on
Commit
7630609
·
1 Parent(s): 9095085

Switch to public Google Sheet backend, add privacy warnings

Browse files

- Replace service account auth with public sheet CSV reads + Apps Script writes
- No credentials needed — just a shared Google Sheet and a deployed script
- Add privacy warning on save: no real names, companies, or identifying info
- Add per-deal calculator with split deals, clawbacks, accelerator multipliers
- Support both attainment-based and deal-size commission models
- Include apps_script.js for easy Google Sheets setup

Files changed (4) hide show
  1. .streamlit/secrets.toml.example +8 -20
  2. app.py +148 -127
  3. apps_script.js +49 -0
  4. requirements.txt +1 -2
.streamlit/secrets.toml.example CHANGED
@@ -1,22 +1,10 @@
1
- # Copy this to secrets.toml and fill in your GCP service account credentials.
2
- # On Streamlit Cloud, paste these values into the app's Secrets settings.
3
  #
4
- # Steps:
5
- # 1. Go to https://console.cloud.google.com/iam-admin/serviceaccounts
6
- # 2. Create a service account (or use an existing one)
7
- # 3. Grant it "Editor" role
8
- # 4. Create a JSON key and paste the values below
9
- # 5. Enable the Google Sheets API: https://console.cloud.google.com/apis/library/sheets.googleapis.com
10
- # 6. Enable the Google Drive API: https://console.cloud.google.com/apis/library/drive.googleapis.com
11
 
12
- [gcp_service_account]
13
- type = "service_account"
14
- project_id = ""
15
- private_key_id = ""
16
- private_key = ""
17
- client_email = ""
18
- client_id = ""
19
- auth_uri = "https://accounts.google.com/o/oauth2/auth"
20
- token_uri = "https://oauth2.googleapis.com/token"
21
- auth_provider_x509_cert_url = "https://www.googleapis.com/oauth2/v1/certs"
22
- client_x509_cert_url = ""
 
1
+ # Copy this to secrets.toml (local) or paste into Streamlit Cloud Secrets.
 
2
  #
3
+ # 1. Create a Google Sheet with headers in row 1: name | author | timestamp | data_json
4
+ # 2. Share it as "Anyone with the link" → Editor
5
+ # 3. Open Extensions → Apps Script, paste the code from apps_script.js
6
+ # 4. Deploy as Web App → Execute as Me → Anyone can access
7
+ # 5. Fill in the URLs below
 
 
8
 
9
+ sheet_url = "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit"
10
+ apps_script_url = "https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec"
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -3,78 +3,73 @@ import json
3
  import datetime
4
  import pandas as pd
5
  import plotly.graph_objects as go
6
- import gspread
7
- from google.oauth2.service_account import Credentials
8
 
9
  # ---------------------------------------------------------------------------
10
- # Google Sheets Backend
11
  # ---------------------------------------------------------------------------
12
 
13
- SCOPES = [
14
- "https://www.googleapis.com/auth/spreadsheets",
15
- "https://www.googleapis.com/auth/drive",
16
- ]
17
- SHEET_NAME = "SalesComp Scenarios"
18
 
19
 
20
- @st.cache_resource
21
- def get_gsheet_client():
22
- """Authenticate with Google Sheets using Streamlit secrets."""
23
- creds_dict = dict(st.secrets["gcp_service_account"])
24
- creds = Credentials.from_service_account_info(creds_dict, scopes=SCOPES)
25
- return gspread.authorize(creds)
 
 
 
26
 
27
 
28
- def _get_worksheet():
29
- """Return the first worksheet, creating the spreadsheet if needed."""
30
- gc = get_gsheet_client()
31
- try:
32
- sh = gc.open(SHEET_NAME)
33
- except gspread.SpreadsheetNotFound:
34
- sh = gc.create(SHEET_NAME)
35
- sh.share(None, perm_type="anyone", role="reader") # link-shareable
36
- ws = sh.sheet1
37
- # Ensure header row exists
38
- if ws.row_count == 0 or ws.cell(1, 1).value != "name":
39
- ws.clear()
40
- ws.append_row(["name", "author", "timestamp", "data_json"])
41
- return ws
42
 
43
 
 
44
  def load_scenarios() -> dict:
45
- ws = _get_worksheet()
46
- records = ws.get_all_records()
47
- scenarios = {}
48
- for r in records:
49
- try:
50
- scenarios[r["name"]] = json.loads(r["data_json"])
51
- except (json.JSONDecodeError, KeyError):
52
- continue
53
- return scenarios
 
 
 
 
 
 
54
 
55
 
56
  def save_scenario(name: str, data: dict):
57
- ws = _get_worksheet()
58
- records = ws.get_all_records()
59
- row_idx = None
60
- for i, r in enumerate(records):
61
- if r["name"] == name:
62
- row_idx = i + 2 # 1-indexed + header
63
- break
64
- row = [name, data.get("author", ""), data.get("timestamp", ""), json.dumps(data)]
65
- if row_idx:
66
- ws.update(f"A{row_idx}:D{row_idx}", [row])
67
- else:
68
- ws.append_row(row)
 
69
 
70
 
71
  def delete_scenario(name: str):
72
- ws = _get_worksheet()
73
- records = ws.get_all_records()
74
- for i, r in enumerate(records):
75
- if r["name"] == name:
76
- ws.delete_rows(i + 2)
77
- return
 
78
 
79
 
80
  # ---------------------------------------------------------------------------
@@ -107,13 +102,11 @@ def calc_commission_deal_rate(deals, deal_rate_tiers, attainment_pct, accel_tier
107
  deal_rate_tiers: list of (min_acv, max_acv | None, rate_pct)
108
  accel_tiers: list of (upper_attainment_pct | None, multiplier)
109
  """
110
- # Determine accelerator multiplier
111
  multiplier = 1.0
112
  for upper_pct, mult in accel_tiers:
113
  if upper_pct is None or attainment_pct <= upper_pct:
114
  multiplier = mult
115
  break
116
- # Commission per deal
117
  total = 0.0
118
  deal_results = []
119
  for deal in deals:
@@ -159,6 +152,23 @@ def calc_ramp_adjusted_quota(annual_quota, ramp_pcts, months_employed=12):
159
  st.set_page_config(page_title="Sales Comp Calculator", layout="wide")
160
  st.title("Sales Compensation Calculator")
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  # ---- Sidebar: Plan Parameters ----
163
 
164
  st.sidebar.header("Plan Parameters")
@@ -178,15 +188,19 @@ commission_mode = st.sidebar.radio(
178
  "Commission model",
179
  ["Attainment-based rates", "Deal-size rates + accelerators"],
180
  key="comm_mode",
181
- help="Plan 1 style: tiered % on revenue bands. Plan 2 style: flat % per deal size with attainment multipliers.",
182
  )
183
 
184
  # -- Attainment-based tiers --
 
 
 
 
 
185
  if commission_mode == "Attainment-based rates":
186
  with st.sidebar.expander("Commission Tiers (attainment)", expanded=True):
187
  st.caption("Marginal rates — each applies only to revenue in that band")
188
  num_tiers = st.number_input("Number of tiers", 1, 8, 4, key="num_tiers")
189
- att_tiers = []
190
  att_defaults = [(100, 4.0), (125, 5.0), (150, 6.0), (None, 7.0)]
191
  for i in range(int(num_tiers)):
192
  c1, c2 = st.columns(2)
@@ -205,7 +219,6 @@ else:
205
  with st.sidebar.expander("Deal-Size Commission Rates", expanded=True):
206
  num_deal_tiers = st.number_input("Number of deal-size tiers", 1, 6, 3, key="num_deal_tiers")
207
  deal_defaults = [(0, 29_999, 20.0), (30_000, 100_000, 40.0), (100_001, None, 40.0)]
208
- deal_rate_tiers = []
209
  for i in range(int(num_deal_tiers)):
210
  c1, c2, c3 = st.columns(3)
211
  d = deal_defaults[i] if i < len(deal_defaults) else (0, None, 40.0)
@@ -224,7 +237,6 @@ else:
224
  st.caption("Multiplier applied to deal commission based on annual attainment")
225
  num_accels = st.number_input("Number of accelerator tiers", 1, 6, 3, key="num_accels")
226
  accel_defaults = [(99, 1.0), (124, 1.3), (None, 1.5)]
227
- accel_tiers = []
228
  for i in range(int(num_accels)):
229
  c1, c2 = st.columns(2)
230
  d = accel_defaults[i] if i < len(accel_defaults) else (None, 1.0)
@@ -278,14 +290,10 @@ with tab_scenario:
278
  if commission_mode == "Attainment-based rates":
279
  commission = calc_commission_attainment(actual_revenue, effective_quota, att_tiers)
280
  else:
281
- # For total-revenue mode with deal-rate model, treat as single "blended" deal
282
- # Real per-deal calc is in Tab 2. Here we approximate with the rate for the avg deal.
283
- avg_rate = 0.0
284
  for mn, mx, rt in deal_rate_tiers:
285
  if mx is None or actual_revenue / max(1, 12) <= mx:
286
  avg_rate = rt
287
  break
288
- # Apply accelerator
289
  mult = 1.0
290
  for upper, m in accel_tiers:
291
  if upper is None or attainment_pct <= upper:
@@ -354,7 +362,6 @@ with tab_scenario:
354
  comm_pts.append(r * (avg_rate / 100) * m_)
355
  att_pts = [r / effective_quota * 100 if effective_quota > 0 else 0 for r in rev_pts]
356
 
357
- # Total comp curve (base + draw + commission)
358
  total_comp_pts = [prorated_base + draw_paid + c for c in comm_pts]
359
 
360
  fig = go.Figure()
@@ -399,15 +406,23 @@ with tab_scenario:
399
  },
400
  }
401
 
 
 
 
 
 
 
 
402
  scol1, scol2 = st.columns(2)
403
  with scol1:
404
  scenario_name = st.text_input("Scenario name", key="save_name",
405
- placeholder="e.g. 'plan-v2-jane'")
406
  if st.button("Save Scenario", type="primary"):
407
  if scenario_name.strip():
408
  try:
409
  save_scenario(scenario_name.strip(), scenario_data)
410
- st.success(f"Saved '{scenario_name.strip()}' to Google Sheets")
 
411
  st.rerun()
412
  except Exception as e:
413
  st.error(f"Failed to save: {e}")
@@ -451,64 +466,70 @@ with tab_deals:
451
 
452
  with tab_compare:
453
  st.subheader("Saved Scenarios")
454
- try:
455
- scenarios = load_scenarios()
456
- except Exception as e:
457
- st.error(f"Could not load scenarios from Google Sheets: {e}")
458
- st.info("Check your Google Sheets credentials in `.streamlit/secrets.toml`")
459
- scenarios = {}
460
-
461
- if scenarios:
462
- col_a, col_b = st.columns(2)
463
- with col_b:
464
- to_delete = st.selectbox("Delete a scenario", [""] + list(scenarios.keys()), key="del_sel")
465
- if st.button("Delete"):
466
- if to_delete:
467
- delete_scenario(to_delete)
468
- st.success(f"Deleted '{to_delete}'")
469
- st.rerun()
470
 
471
- selected = st.multiselect("Select scenarios to compare", list(scenarios.keys()),
472
- default=list(scenarios.keys())[:5], key="compare_sel")
473
- if selected:
474
- rows = []
475
- for name in selected:
476
- s = scenarios[name]
477
- r = s.get("results", {})
478
- p = s.get("params", {})
479
- inp = s.get("inputs", {})
480
- rev = inp.get("actual_revenue", 0)
481
- rows.append({
482
- "Scenario": name,
483
- "Author": s.get("author", ""),
484
- "Mode": s.get("commission_mode", ""),
485
- "Base": f"${p.get('base_salary', 0):,.0f}",
486
- "Quota": f"${p.get('annual_quota', 0):,.0f}",
487
- "Revenue": f"${rev:,.0f}",
488
- "Attainment": f"{r.get('attainment_pct', 0)}%",
489
- "Commission": f"${r.get('commission', 0):,.0f}",
490
- "Draw": f"${r.get('draw_paid', 0):,.0f}",
491
- "Total Comp": f"${r.get('total_comp', 0):,.0f}",
492
- "Gross Profit": f"${r.get('gross_profit', 0):,.0f}",
493
- "Net After Rep": f"${r.get('net_after_rep', 0):,.0f}",
494
- "Rep Cost % Rev": f"{r.get('total_cost_of_rep', 0) / rev * 100:.1f}%" if rev > 0 else "N/A",
495
- "ROI": f"{r.get('roi_on_rep', 0):.1f}x",
496
- })
497
- st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
498
-
499
- # Side-by-side bar chart
500
- fig = go.Figure()
501
- for name in selected:
502
- s = scenarios[name]
503
- r = s.get("results", {})
504
- fig.add_trace(go.Bar(
505
- name=name,
506
- x=["Commission", "Total Comp", "Gross Profit", "Net After Rep"],
507
- y=[r.get("commission", 0), r.get("total_comp", 0),
508
- r.get("gross_profit", 0), r.get("net_after_rep", 0)],
509
- ))
510
- fig.update_layout(barmode="group", height=400,
511
- margin=dict(l=0, r=0, t=30, b=0))
512
- st.plotly_chart(fig, use_container_width=True)
513
  else:
514
- st.info("No saved scenarios yet. Save one from the Scenario Calculator tab.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import datetime
4
  import pandas as pd
5
  import plotly.graph_objects as go
6
+ import requests
 
7
 
8
  # ---------------------------------------------------------------------------
9
+ # Google Sheets Backend (public sheet, no credentials)
10
  # ---------------------------------------------------------------------------
11
 
12
+ SHEET_CSV_TMPL = "https://docs.google.com/spreadsheets/d/{sheet_id}/gviz/tq?tqx=out:csv"
 
 
 
 
13
 
14
 
15
+ def _get_sheet_id():
16
+ url = st.secrets.get("sheet_url", "")
17
+ if not url:
18
+ return None
19
+ # Extract ID from URL like https://docs.google.com/spreadsheets/d/XXXXX/edit...
20
+ parts = url.split("/d/")
21
+ if len(parts) < 2:
22
+ return None
23
+ return parts[1].split("/")[0]
24
 
25
 
26
+ def _get_apps_script_url():
27
+ return st.secrets.get("apps_script_url", "")
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
 
30
+ @st.cache_data(ttl=30)
31
  def load_scenarios() -> dict:
32
+ sheet_id = _get_sheet_id()
33
+ if not sheet_id:
34
+ return {}
35
+ try:
36
+ csv_url = SHEET_CSV_TMPL.format(sheet_id=sheet_id)
37
+ df = pd.read_csv(csv_url)
38
+ scenarios = {}
39
+ for _, row in df.iterrows():
40
+ try:
41
+ scenarios[row["name"]] = json.loads(row["data_json"])
42
+ except (json.JSONDecodeError, KeyError):
43
+ continue
44
+ return scenarios
45
+ except Exception:
46
+ return {}
47
 
48
 
49
  def save_scenario(name: str, data: dict):
50
+ url = _get_apps_script_url()
51
+ if not url:
52
+ raise ValueError("Apps Script URL not configured in secrets")
53
+ payload = {
54
+ "action": "save",
55
+ "name": name,
56
+ "author": data.get("author", ""),
57
+ "timestamp": data.get("timestamp", ""),
58
+ "data_json": json.dumps(data),
59
+ }
60
+ resp = requests.post(url, json=payload, timeout=15)
61
+ if resp.status_code != 200:
62
+ raise ValueError(f"Apps Script returned {resp.status_code}")
63
 
64
 
65
  def delete_scenario(name: str):
66
+ url = _get_apps_script_url()
67
+ if not url:
68
+ raise ValueError("Apps Script URL not configured in secrets")
69
+ payload = {"action": "delete", "name": name}
70
+ resp = requests.post(url, json=payload, timeout=15)
71
+ if resp.status_code != 200:
72
+ raise ValueError(f"Apps Script returned {resp.status_code}")
73
 
74
 
75
  # ---------------------------------------------------------------------------
 
102
  deal_rate_tiers: list of (min_acv, max_acv | None, rate_pct)
103
  accel_tiers: list of (upper_attainment_pct | None, multiplier)
104
  """
 
105
  multiplier = 1.0
106
  for upper_pct, mult in accel_tiers:
107
  if upper_pct is None or attainment_pct <= upper_pct:
108
  multiplier = mult
109
  break
 
110
  total = 0.0
111
  deal_results = []
112
  for deal in deals:
 
152
  st.set_page_config(page_title="Sales Comp Calculator", layout="wide")
153
  st.title("Sales Compensation Calculator")
154
 
155
+ # Check config
156
+ sheet_ok = bool(_get_sheet_id())
157
+ script_ok = bool(_get_apps_script_url())
158
+ if not sheet_ok or not script_ok:
159
+ with st.expander("Setup required — click to configure", expanded=True):
160
+ st.warning("Add these to `.streamlit/secrets.toml` (local) or Streamlit Cloud Secrets:")
161
+ st.code('''sheet_url = "https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/edit"
162
+ apps_script_url = "https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec"''', language="toml")
163
+ st.markdown("""
164
+ **Quick setup (5 min):**
165
+ 1. Create a Google Sheet with headers in row 1: `name | author | timestamp | data_json`
166
+ 2. Share it as **"Anyone with the link" → Editor**
167
+ 3. Open **Extensions → Apps Script**, paste the script from `apps_script.js` in this repo
168
+ 4. Deploy as **Web App** → Execute as **Me** → Access **Anyone**
169
+ 5. Copy the sheet URL and web app URL into secrets
170
+ """)
171
+
172
  # ---- Sidebar: Plan Parameters ----
173
 
174
  st.sidebar.header("Plan Parameters")
 
188
  "Commission model",
189
  ["Attainment-based rates", "Deal-size rates + accelerators"],
190
  key="comm_mode",
191
+ help="Plan 1: tiered % on revenue bands. Plan 2: flat % per deal size with attainment multipliers.",
192
  )
193
 
194
  # -- Attainment-based tiers --
195
+ att_tiers = []
196
+ deal_rate_tiers = []
197
+ accel_tiers = []
198
+ avg_rate = 0.0
199
+
200
  if commission_mode == "Attainment-based rates":
201
  with st.sidebar.expander("Commission Tiers (attainment)", expanded=True):
202
  st.caption("Marginal rates — each applies only to revenue in that band")
203
  num_tiers = st.number_input("Number of tiers", 1, 8, 4, key="num_tiers")
 
204
  att_defaults = [(100, 4.0), (125, 5.0), (150, 6.0), (None, 7.0)]
205
  for i in range(int(num_tiers)):
206
  c1, c2 = st.columns(2)
 
219
  with st.sidebar.expander("Deal-Size Commission Rates", expanded=True):
220
  num_deal_tiers = st.number_input("Number of deal-size tiers", 1, 6, 3, key="num_deal_tiers")
221
  deal_defaults = [(0, 29_999, 20.0), (30_000, 100_000, 40.0), (100_001, None, 40.0)]
 
222
  for i in range(int(num_deal_tiers)):
223
  c1, c2, c3 = st.columns(3)
224
  d = deal_defaults[i] if i < len(deal_defaults) else (0, None, 40.0)
 
237
  st.caption("Multiplier applied to deal commission based on annual attainment")
238
  num_accels = st.number_input("Number of accelerator tiers", 1, 6, 3, key="num_accels")
239
  accel_defaults = [(99, 1.0), (124, 1.3), (None, 1.5)]
 
240
  for i in range(int(num_accels)):
241
  c1, c2 = st.columns(2)
242
  d = accel_defaults[i] if i < len(accel_defaults) else (None, 1.0)
 
290
  if commission_mode == "Attainment-based rates":
291
  commission = calc_commission_attainment(actual_revenue, effective_quota, att_tiers)
292
  else:
 
 
 
293
  for mn, mx, rt in deal_rate_tiers:
294
  if mx is None or actual_revenue / max(1, 12) <= mx:
295
  avg_rate = rt
296
  break
 
297
  mult = 1.0
298
  for upper, m in accel_tiers:
299
  if upper is None or attainment_pct <= upper:
 
362
  comm_pts.append(r * (avg_rate / 100) * m_)
363
  att_pts = [r / effective_quota * 100 if effective_quota > 0 else 0 for r in rev_pts]
364
 
 
365
  total_comp_pts = [prorated_base + draw_paid + c for c in comm_pts]
366
 
367
  fig = go.Figure()
 
406
  },
407
  }
408
 
409
+ st.warning(
410
+ "**Privacy:** Saved scenarios are stored in a shared Google Sheet. "
411
+ "Do NOT use real names, company names, or any identifying information "
412
+ "in scenario names or the 'Your name' field. Use generic labels "
413
+ "(e.g. 'Plan A - aggressive', 'Option 2 - conservative').",
414
+ icon="🔒",
415
+ )
416
  scol1, scol2 = st.columns(2)
417
  with scol1:
418
  scenario_name = st.text_input("Scenario name", key="save_name",
419
+ placeholder="e.g. 'plan-a-aggressive'")
420
  if st.button("Save Scenario", type="primary"):
421
  if scenario_name.strip():
422
  try:
423
  save_scenario(scenario_name.strip(), scenario_data)
424
+ st.success(f"Saved '{scenario_name.strip()}'")
425
+ load_scenarios.clear()
426
  st.rerun()
427
  except Exception as e:
428
  st.error(f"Failed to save: {e}")
 
466
 
467
  with tab_compare:
468
  st.subheader("Saved Scenarios")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
 
470
+ if not sheet_ok:
471
+ st.warning("Configure your Google Sheet URL in secrets to enable scenario saving.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  else:
473
+ if st.button("Refresh"):
474
+ load_scenarios.clear()
475
+ st.rerun()
476
+
477
+ scenarios = load_scenarios()
478
+
479
+ if scenarios:
480
+ col_a, col_b = st.columns(2)
481
+ with col_b:
482
+ to_delete = st.selectbox("Delete a scenario", [""] + list(scenarios.keys()), key="del_sel")
483
+ if st.button("Delete"):
484
+ if to_delete:
485
+ try:
486
+ delete_scenario(to_delete)
487
+ load_scenarios.clear()
488
+ st.success(f"Deleted '{to_delete}'")
489
+ st.rerun()
490
+ except Exception as e:
491
+ st.error(f"Failed to delete: {e}")
492
+
493
+ selected = st.multiselect("Select scenarios to compare", list(scenarios.keys()),
494
+ default=list(scenarios.keys())[:5], key="compare_sel")
495
+ if selected:
496
+ rows = []
497
+ for name in selected:
498
+ s = scenarios[name]
499
+ r = s.get("results", {})
500
+ p = s.get("params", {})
501
+ inp = s.get("inputs", {})
502
+ rev = inp.get("actual_revenue", 0)
503
+ rows.append({
504
+ "Scenario": name,
505
+ "Author": s.get("author", ""),
506
+ "Mode": s.get("commission_mode", ""),
507
+ "Base": f"${p.get('base_salary', 0):,.0f}",
508
+ "Quota": f"${p.get('annual_quota', 0):,.0f}",
509
+ "Revenue": f"${rev:,.0f}",
510
+ "Attainment": f"{r.get('attainment_pct', 0)}%",
511
+ "Commission": f"${r.get('commission', 0):,.0f}",
512
+ "Draw": f"${r.get('draw_paid', 0):,.0f}",
513
+ "Total Comp": f"${r.get('total_comp', 0):,.0f}",
514
+ "Gross Profit": f"${r.get('gross_profit', 0):,.0f}",
515
+ "Net After Rep": f"${r.get('net_after_rep', 0):,.0f}",
516
+ "Rep Cost % Rev": f"{r.get('total_cost_of_rep', 0) / rev * 100:.1f}%" if rev > 0 else "N/A",
517
+ "ROI": f"{r.get('roi_on_rep', 0):.1f}x",
518
+ })
519
+ st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
520
+
521
+ fig = go.Figure()
522
+ for name in selected:
523
+ s = scenarios[name]
524
+ r = s.get("results", {})
525
+ fig.add_trace(go.Bar(
526
+ name=name,
527
+ x=["Commission", "Total Comp", "Gross Profit", "Net After Rep"],
528
+ y=[r.get("commission", 0), r.get("total_comp", 0),
529
+ r.get("gross_profit", 0), r.get("net_after_rep", 0)],
530
+ ))
531
+ fig.update_layout(barmode="group", height=400,
532
+ margin=dict(l=0, r=0, t=30, b=0))
533
+ st.plotly_chart(fig, use_container_width=True)
534
+ else:
535
+ st.info("No saved scenarios yet. Save one from the Scenario Calculator tab.")
apps_script.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Google Apps Script — paste this into Extensions > Apps Script in your Google Sheet.
2
+ // Deploy as: Web App → Execute as Me → Anyone can access.
3
+ //
4
+ // The sheet must have headers in row 1: name | author | timestamp | data_json
5
+
6
+ function doPost(e) {
7
+ var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
8
+ var data = JSON.parse(e.postData.contents);
9
+
10
+ if (data.action === "save") {
11
+ // Check if scenario already exists (update in place)
12
+ var names = sheet.getRange("A:A").getValues();
13
+ var rowIndex = -1;
14
+ for (var i = 1; i < names.length; i++) {
15
+ if (names[i][0] === data.name) {
16
+ rowIndex = i + 1; // 1-indexed
17
+ break;
18
+ }
19
+ }
20
+ var row = [data.name, data.author, data.timestamp, data.data_json];
21
+ if (rowIndex > 0) {
22
+ sheet.getRange(rowIndex, 1, 1, 4).setValues([row]);
23
+ } else {
24
+ sheet.appendRow(row);
25
+ }
26
+ return ContentService.createTextOutput(JSON.stringify({status: "ok"}))
27
+ .setMimeType(ContentService.MimeType.JSON);
28
+
29
+ } else if (data.action === "delete") {
30
+ var names = sheet.getRange("A:A").getValues();
31
+ for (var i = 1; i < names.length; i++) {
32
+ if (names[i][0] === data.name) {
33
+ sheet.deleteRow(i + 1);
34
+ break;
35
+ }
36
+ }
37
+ return ContentService.createTextOutput(JSON.stringify({status: "ok"}))
38
+ .setMimeType(ContentService.MimeType.JSON);
39
+ }
40
+
41
+ return ContentService.createTextOutput(JSON.stringify({status: "error", message: "Unknown action"}))
42
+ .setMimeType(ContentService.MimeType.JSON);
43
+ }
44
+
45
+ // Required for Apps Script web app — handles GET requests with a simple status page
46
+ function doGet(e) {
47
+ return ContentService.createTextOutput("SalesComp API is running.")
48
+ .setMimeType(ContentService.MimeType.TEXT);
49
+ }
requirements.txt CHANGED
@@ -1,5 +1,4 @@
1
  streamlit>=1.30
2
  plotly>=5.18
3
  pandas>=2.0
4
- gspread>=6.0
5
- google-auth>=2.0
 
1
  streamlit>=1.30
2
  plotly>=5.18
3
  pandas>=2.0
4
+ requests>=2.28