Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -18,7 +18,7 @@ client = gspread.authorize(creds)
|
|
18 |
|
19 |
SHEET_URL = "https://docs.google.com/spreadsheets/d/1if4KoVQvw5ZbhknfdZbzMkcTiPfsD6bz9V3a1th-bwQ"
|
20 |
|
21 |
-
# --------------------
|
22 |
SHEET_MAP = {
|
23 |
"Alice": "https://docs.google.com/spreadsheets/d/18qFpkbE2CwVOgiB6Xz4m2Ep7ZA29p5xV",
|
24 |
"Bob": "https://docs.google.com/spreadsheets/d/1EKngyAvq_3hzQMAOVame2nO9LKPJEV0d",
|
@@ -26,13 +26,11 @@ SHEET_MAP = {
|
|
26 |
"Dave": "https://docs.google.com/spreadsheets/d/1m5e6YXxjK62vtBxYGkJSyHpHT7lnirg6"
|
27 |
}
|
28 |
|
29 |
-
def
|
30 |
if rep_name not in SHEET_MAP:
|
31 |
return pd.DataFrame([{"Error": f"No sheet available for '{rep_name}'"}])
|
32 |
-
|
33 |
-
sheet_url = SHEET_MAP[rep_name]
|
34 |
try:
|
35 |
-
sheet = client.open_by_url(
|
36 |
worksheet = sheet.get_worksheet(0)
|
37 |
data = worksheet.get_all_values()
|
38 |
if not data:
|
@@ -55,10 +53,7 @@ def load_sheet_df(name):
|
|
55 |
header = []
|
56 |
for col in raw_header:
|
57 |
counts[col] += 1
|
58 |
-
if counts[col] > 1
|
59 |
-
header.append(f"{col}_{counts[col]}")
|
60 |
-
else:
|
61 |
-
header.append(col)
|
62 |
header = normalize_columns(header)
|
63 |
return pd.DataFrame(rows, columns=header)
|
64 |
|
@@ -116,8 +111,7 @@ def search_appointments(y, m, d, rep=None):
|
|
116 |
|
117 |
# -------------------- LEADS --------------------
|
118 |
def get_leads_detail():
|
119 |
-
|
120 |
-
return df
|
121 |
|
122 |
def get_leads_summary():
|
123 |
df = get_leads_detail()
|
@@ -137,12 +131,11 @@ def compute_insights():
|
|
137 |
return vc.idxmax() if not vc.empty else "N/A"
|
138 |
return "N/A"
|
139 |
|
140 |
-
|
141 |
{"Metric": "Most Calls This Week", "Rep": top(calls, "Rep")},
|
142 |
{"Metric": "Most Appointments This Week", "Rep": top(appts, "Rep")},
|
143 |
{"Metric": "Most Leads Allocated", "Rep": top(leads, "Assigned Rep")},
|
144 |
-
]
|
145 |
-
return pd.DataFrame(data)
|
146 |
|
147 |
# -------------------- USER MANAGEMENT --------------------
|
148 |
def load_users():
|
@@ -166,221 +159,43 @@ def save_users(df):
|
|
166 |
set_with_dataframe(ws, df)
|
167 |
return "✅ Users saved!"
|
168 |
|
169 |
-
# --------------------
|
170 |
-
def get_quotes_df():
|
171 |
-
df = load_sheet_df("LiveQuotes")
|
172 |
-
df.columns = [c.strip() for c in df.columns]
|
173 |
-
return df
|
174 |
-
|
175 |
-
def rep_choices_quotes():
|
176 |
-
df = get_quotes_df()
|
177 |
-
return sorted(df["Rep"].dropna().unique().tolist()) if "Rep" in df else []
|
178 |
-
|
179 |
-
def quote_year_choices():
|
180 |
-
df = get_quotes_df()
|
181 |
-
if "Year" in df.columns:
|
182 |
-
years = sorted(df["Year"].dropna().unique().astype(str))
|
183 |
-
return years
|
184 |
-
if "Date" in df.columns:
|
185 |
-
years = pd.to_datetime(df["Date"], errors="coerce").dt.year.dropna().unique()
|
186 |
-
return sorted(years.astype(str))
|
187 |
-
return []
|
188 |
-
|
189 |
-
def quote_month_choices(year=None):
|
190 |
-
"""
|
191 |
-
Returns a sorted list of valid month strings for a given year, always at least [''].
|
192 |
-
Also prints debug output to help troubleshoot.
|
193 |
-
"""
|
194 |
-
df = get_quotes_df()
|
195 |
-
if (
|
196 |
-
year
|
197 |
-
and "Year" in df.columns
|
198 |
-
and "Month" in df.columns
|
199 |
-
and not df.empty
|
200 |
-
):
|
201 |
-
subset = df[df["Year"].astype(str) == str(year)]
|
202 |
-
if subset.empty:
|
203 |
-
print(f"[DEBUG] No quotes found for year {year}. Returning [''].")
|
204 |
-
return [""]
|
205 |
-
try:
|
206 |
-
months = pd.to_numeric(subset["Month"], errors="coerce").dropna().astype(int)
|
207 |
-
months = [str(m) for m in months if 1 <= m <= 12]
|
208 |
-
months = sorted(set(months))
|
209 |
-
result = [""] + months if months else [""]
|
210 |
-
print(f"[DEBUG] Year {year}: Months dropdown = {result}")
|
211 |
-
return result
|
212 |
-
except Exception as e:
|
213 |
-
print(f"[DEBUG] Exception in quote_month_choices for year {year}: {e}")
|
214 |
-
return [""]
|
215 |
-
print(f"[DEBUG] No valid year or columns missing. Returning [''].")
|
216 |
-
return [""]
|
217 |
-
|
218 |
-
def quotes_summary(year=None, month=None):
|
219 |
-
df = get_quotes_df()
|
220 |
-
if "Rep" not in df.columns or "Total" not in df.columns:
|
221 |
-
return pd.DataFrame([{"Error": "Missing Rep or Total column"}])
|
222 |
-
if year and "Year" in df.columns:
|
223 |
-
df = df[df["Year"].astype(str) == str(year)]
|
224 |
-
if month and "Month" in df.columns:
|
225 |
-
df = df[df["Month"].astype(str) == str(month)]
|
226 |
-
df["Total"] = pd.to_numeric(df["Total"].astype(str).str.replace(",", ""), errors="coerce")
|
227 |
-
summary = (
|
228 |
-
df.groupby("Rep")
|
229 |
-
.agg({"Document No.": "count", "Total": "sum"})
|
230 |
-
.rename(columns={"Document No.": "Total Quotes", "Total": "Total Value"})
|
231 |
-
.reset_index()
|
232 |
-
)
|
233 |
-
summary["Total Value"] = summary["Total Value"].fillna(0).round(2)
|
234 |
-
return summary
|
235 |
-
|
236 |
-
def get_rep_quotes_filtered(rep, year=None, month=None):
|
237 |
-
df = get_quotes_df()
|
238 |
-
if "Rep" not in df.columns:
|
239 |
-
return pd.DataFrame([{"Error": "Missing Rep column"}])
|
240 |
-
df = df[df["Rep"] == rep]
|
241 |
-
if year and "Year" in df.columns:
|
242 |
-
df = df[df["Year"].astype(str) == str(year)]
|
243 |
-
if month and "Month" in df.columns:
|
244 |
-
df = df[df["Month"].astype(str) == str(month)]
|
245 |
-
return df
|
246 |
-
|
247 |
-
def update_month_choices_summary(year):
|
248 |
-
months = quote_month_choices(year)
|
249 |
-
print(f"[DEBUG] update_month_choices_summary({year}) -> {months}")
|
250 |
-
return gr.Dropdown.update(choices=months, value="")
|
251 |
-
|
252 |
-
def update_month_choices(year):
|
253 |
-
months = quote_month_choices(year)
|
254 |
-
print(f"[DEBUG] update_month_choices({year}) -> {months}")
|
255 |
-
return gr.Dropdown.update(choices=months, value="")
|
256 |
-
|
257 |
-
# -------------------- UI LAYOUT --------------------
|
258 |
with gr.Blocks(title="Graffiti Admin Dashboard") as app:
|
259 |
-
gr.Markdown("#
|
260 |
|
261 |
-
# -- Calls Tab --
|
262 |
with gr.Tab("Calls Report"):
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
|
268 |
-
gr.Markdown("### Search Calls by Date")
|
269 |
-
y1, m1, d1 = gr.Textbox(label="Year"), gr.Textbox(label="Month"), gr.Textbox(label="Day")
|
270 |
-
rep_c2 = gr.Dropdown(choices=rep_choices("Calls"), label="Filter by Rep", allow_custom_value=True)
|
271 |
-
btn_c2 = gr.Button("Search")
|
272 |
-
tbl_c2 = gr.Dataframe()
|
273 |
-
btn_c2.click(search_calls, [y1, m1, d1, rep_c2], tbl_c2)
|
274 |
-
|
275 |
-
# -- Appointments Tab --
|
276 |
with gr.Tab("Appointments Report"):
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
def _load_appts(r):
|
282 |
-
df = get_appointments(r)
|
283 |
-
return df.groupby("Rep").size().reset_index(name="Count"), df
|
284 |
-
btn_a.click(_load_appts, rep_a, [sum_a, tbl_a])
|
285 |
-
|
286 |
-
gr.Markdown("### Search Appts by Date")
|
287 |
-
y2, m2, d2 = gr.Textbox(label="Year"), gr.Textbox(label="Month"), gr.Textbox(label="Day")
|
288 |
-
rep_a2 = gr.Dropdown(choices=rep_choices("Appointments"), label="Filter by Rep", allow_custom_value=True)
|
289 |
-
btn_a2 = gr.Button("Search")
|
290 |
-
sum_a2 = gr.Dataframe(label="📊 Appts by Rep")
|
291 |
-
tbl_a2 = gr.Dataframe()
|
292 |
-
def _search_appts(y,m,d,r):
|
293 |
-
df = search_appointments(y,m,d,r)
|
294 |
-
return df.groupby("Rep").size().reset_index(name="Count"), df
|
295 |
-
btn_a2.click(_search_appts, [y2,m2,d2,rep_a2], [sum_a2, tbl_a2])
|
296 |
|
297 |
-
# -- Appointed Leads --
|
298 |
with gr.Tab("Allocated Leads"):
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
# -- Quotes Tab (NEW) --
|
305 |
-
with gr.Tab("Quotes"):
|
306 |
-
gr.Markdown("### 📈 Quotes Summary by Rep")
|
307 |
-
year_qs = gr.Dropdown(choices=[""] + quote_year_choices(), label="Year (optional)", value="")
|
308 |
-
month_qs = gr.Dropdown(choices=[""], label="Month (optional, needs year)", value="")
|
309 |
-
btn_qs = gr.Button("Show Quotes Summary")
|
310 |
-
sum_qs = gr.Dataframe(label="Summary by Rep")
|
311 |
-
|
312 |
-
# Dynamic month options for summary
|
313 |
-
year_qs.change(update_month_choices_summary, year_qs, month_qs)
|
314 |
-
|
315 |
-
def quotes_summary_wrapper(year, month):
|
316 |
-
return quotes_summary(year if year else None, month if month else None)
|
317 |
-
btn_qs.click(quotes_summary_wrapper, [year_qs, month_qs], sum_qs)
|
318 |
|
319 |
-
gr.Markdown("### 🔎 View All Quotes for a Rep, Year, and Month")
|
320 |
-
rep_q = gr.Dropdown(choices=rep_choices_quotes(), label="Select Rep")
|
321 |
-
year_q = gr.Dropdown(choices=[""] + quote_year_choices(), label="Year (optional)", value="")
|
322 |
-
month_q = gr.Dropdown(choices=[""], label="Month (optional, needs year)", value="")
|
323 |
-
btn_qr = gr.Button("Show Quotes")
|
324 |
-
tbl_qr = gr.Dataframe(label="Quotes for Selection")
|
325 |
-
|
326 |
-
# Dynamic month options for rep quotes
|
327 |
-
year_q.change(update_month_choices, year_q, month_q)
|
328 |
-
|
329 |
-
def get_rep_quotes_filtered_wrapper(rep, year, month):
|
330 |
-
return get_rep_quotes_filtered(rep, year if year else None, month if month else None)
|
331 |
-
btn_qr.click(get_rep_quotes_filtered_wrapper, [rep_q, year_q, month_q], tbl_qr)
|
332 |
-
|
333 |
-
# -- Insights --
|
334 |
with gr.Tab("Insights"):
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
|
339 |
-
# -- User Management --
|
340 |
with gr.Tab("User Management"):
|
341 |
-
gr.Markdown("## 👤 Manage Users\nEdit the grid below then click **Save Users** to push back to the sheet.")
|
342 |
users_tbl = gr.Dataframe(value=load_users(), interactive=True)
|
343 |
save_btn = gr.Button("Save Users")
|
344 |
-
|
345 |
-
save_btn.click(save_users, users_tbl,
|
346 |
-
# -------------------- Self Sourced Leads --------------------
|
347 |
-
SHEET_MAP = {
|
348 |
-
"Alice": "https://docs.google.com/spreadsheets/d/18qFpkbE2CwVOgiB6Xz4m2Ep7ZA29p5xV",
|
349 |
-
"Bob": "https://docs.google.com/spreadsheets/d/1EKngyAvq_3hzQMAOVame2nO9LKPJEV0d",
|
350 |
-
"Charlie": "https://docs.google.com/spreadsheets/d/164OTu1keBC12-5XFUDXMmLOPMkdAjBOM",
|
351 |
-
"Dave": "https://docs.google.com/spreadsheets/d/1m5e6YXxjK62vtBxYGkJSyHpHT7lnirg6"
|
352 |
-
}
|
353 |
-
|
354 |
-
def load_leads_data(rep_name):
|
355 |
-
if rep_name not in SHEET_MAP:
|
356 |
-
return pd.DataFrame([{"Error": f"No sheet available for '{rep_name}'"}])
|
357 |
-
|
358 |
-
sheet_url = SHEET_MAP[rep_name]
|
359 |
-
try:
|
360 |
-
sheet = client.open_by_url(sheet_url)
|
361 |
-
worksheet = sheet.get_worksheet(0)
|
362 |
-
data = worksheet.get_all_values()
|
363 |
-
if not data:
|
364 |
-
return pd.DataFrame([{"Info": "No data available"}])
|
365 |
-
return pd.DataFrame(data[1:], columns=data[0])
|
366 |
-
except Exception as e:
|
367 |
-
return pd.DataFrame([{"Error": str(e)}])
|
368 |
-
# -- Self Sourced Leads Tab (New) --
|
369 |
-
with gr.Tab("Self Sourced Leads"):
|
370 |
-
gr.Markdown("## 🔍 Self Sourced Leads by Rep")
|
371 |
-
|
372 |
-
rep_leads = gr.Dropdown(
|
373 |
-
label="Select Rep",
|
374 |
-
choices=list(SHEET_MAP.keys()),
|
375 |
-
allow_custom_value=True
|
376 |
-
)
|
377 |
-
btn_load_leads = gr.Button("Load Leads")
|
378 |
-
tbl_leads = gr.Dataframe(label="Leads Data")
|
379 |
-
|
380 |
-
btn_load_leads.click(load_leads_data, rep_leads, tbl_leads)
|
381 |
-
|
382 |
-
app.launch()
|
383 |
|
384 |
-
|
385 |
-
|
|
|
|
|
|
|
386 |
|
|
|
|
18 |
|
19 |
SHEET_URL = "https://docs.google.com/spreadsheets/d/1if4KoVQvw5ZbhknfdZbzMkcTiPfsD6bz9V3a1th-bwQ"
|
20 |
|
21 |
+
# -------------------- SELF SOURCED LEADS CONFIG --------------------
|
22 |
SHEET_MAP = {
|
23 |
"Alice": "https://docs.google.com/spreadsheets/d/18qFpkbE2CwVOgiB6Xz4m2Ep7ZA29p5xV",
|
24 |
"Bob": "https://docs.google.com/spreadsheets/d/1EKngyAvq_3hzQMAOVame2nO9LKPJEV0d",
|
|
|
26 |
"Dave": "https://docs.google.com/spreadsheets/d/1m5e6YXxjK62vtBxYGkJSyHpHT7lnirg6"
|
27 |
}
|
28 |
|
29 |
+
def load_self_sourced_leads(rep_name):
|
30 |
if rep_name not in SHEET_MAP:
|
31 |
return pd.DataFrame([{"Error": f"No sheet available for '{rep_name}'"}])
|
|
|
|
|
32 |
try:
|
33 |
+
sheet = client.open_by_url(SHEET_MAP[rep_name])
|
34 |
worksheet = sheet.get_worksheet(0)
|
35 |
data = worksheet.get_all_values()
|
36 |
if not data:
|
|
|
53 |
header = []
|
54 |
for col in raw_header:
|
55 |
counts[col] += 1
|
56 |
+
header.append(f"{col}_{counts[col]}" if counts[col] > 1 else col)
|
|
|
|
|
|
|
57 |
header = normalize_columns(header)
|
58 |
return pd.DataFrame(rows, columns=header)
|
59 |
|
|
|
111 |
|
112 |
# -------------------- LEADS --------------------
|
113 |
def get_leads_detail():
|
114 |
+
return load_sheet_df("AllocatedLeads")
|
|
|
115 |
|
116 |
def get_leads_summary():
|
117 |
df = get_leads_detail()
|
|
|
131 |
return vc.idxmax() if not vc.empty else "N/A"
|
132 |
return "N/A"
|
133 |
|
134 |
+
return pd.DataFrame([
|
135 |
{"Metric": "Most Calls This Week", "Rep": top(calls, "Rep")},
|
136 |
{"Metric": "Most Appointments This Week", "Rep": top(appts, "Rep")},
|
137 |
{"Metric": "Most Leads Allocated", "Rep": top(leads, "Assigned Rep")},
|
138 |
+
])
|
|
|
139 |
|
140 |
# -------------------- USER MANAGEMENT --------------------
|
141 |
def load_users():
|
|
|
159 |
set_with_dataframe(ws, df)
|
160 |
return "✅ Users saved!"
|
161 |
|
162 |
+
# -------------------- GRADIO APP --------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
with gr.Blocks(title="Graffiti Admin Dashboard") as app:
|
164 |
+
gr.Markdown("# 📊 Graffiti Admin Dashboard")
|
165 |
|
|
|
166 |
with gr.Tab("Calls Report"):
|
167 |
+
rep = gr.Dropdown(choices=rep_choices("Calls"), label="Rep")
|
168 |
+
btn = gr.Button("Load This Week")
|
169 |
+
out = gr.Dataframe()
|
170 |
+
btn.click(get_calls, rep, out)
|
171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
with gr.Tab("Appointments Report"):
|
173 |
+
rep2 = gr.Dropdown(choices=rep_choices("Appointments"), label="Rep")
|
174 |
+
btn2 = gr.Button("Load This Week")
|
175 |
+
out2 = gr.Dataframe()
|
176 |
+
btn2.click(get_appointments, rep2, out2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
|
|
|
178 |
with gr.Tab("Allocated Leads"):
|
179 |
+
btn3 = gr.Button("Show Leads")
|
180 |
+
summary = gr.Dataframe()
|
181 |
+
details = gr.Dataframe()
|
182 |
+
btn3.click(lambda: (get_leads_summary(), get_leads_detail()), None, [summary, details])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
with gr.Tab("Insights"):
|
185 |
+
btn4 = gr.Button("Generate Insights")
|
186 |
+
out4 = gr.Dataframe()
|
187 |
+
btn4.click(compute_insights, None, out4)
|
188 |
|
|
|
189 |
with gr.Tab("User Management"):
|
|
|
190 |
users_tbl = gr.Dataframe(value=load_users(), interactive=True)
|
191 |
save_btn = gr.Button("Save Users")
|
192 |
+
save_msg = gr.Textbox()
|
193 |
+
save_btn.click(save_users, users_tbl, save_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
194 |
|
195 |
+
with gr.Tab("Self Sourced Leads"):
|
196 |
+
rep_s = gr.Dropdown(choices=list(SHEET_MAP.keys()), label="Rep")
|
197 |
+
btn_s = gr.Button("Load Leads")
|
198 |
+
tbl_s = gr.Dataframe()
|
199 |
+
btn_s.click(load_self_sourced_leads, rep_s, tbl_s)
|
200 |
|
201 |
+
app.launch()
|