Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -24,11 +24,6 @@ def normalize_columns(cols):
|
|
24 |
return [c.strip().title() for c in cols]
|
25 |
|
26 |
def load_sheet_df(name):
|
27 |
-
"""
|
28 |
-
Load a sheet into a DataFrame without confusing duplicates in
|
29 |
-
the header row. We fetch all values, dedupe the first row,
|
30 |
-
then build a DataFrame.
|
31 |
-
"""
|
32 |
ws = client.open_by_url(SHEET_URL).worksheet(name)
|
33 |
data = ws.get_all_values()
|
34 |
if not data:
|
@@ -131,7 +126,6 @@ def compute_insights():
|
|
131 |
# -------------------- USER MANAGEMENT --------------------
|
132 |
def load_users():
|
133 |
df = load_sheet_df("Users")
|
134 |
-
# select & rename your columns as needed
|
135 |
want = [
|
136 |
"Id", "Email", "Name", "Business", "Role",
|
137 |
"Daily Phone Call Target", "Daily Phone Appointment Target",
|
@@ -148,9 +142,66 @@ def load_users():
|
|
148 |
def save_users(df):
|
149 |
ws = client.open_by_url(SHEET_URL).worksheet("Users")
|
150 |
ws.clear()
|
151 |
-
set_with_dataframe(ws, df)
|
152 |
return "β
Users saved!"
|
153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
154 |
# -------------------- UI LAYOUT --------------------
|
155 |
with gr.Blocks(title="Graffiti Admin Dashboard") as app:
|
156 |
gr.Markdown("# π Graffiti Admin Dashboard")
|
@@ -198,6 +249,45 @@ with gr.Blocks(title="Graffiti Admin Dashboard") as app:
|
|
198 |
det_l = gr.Dataframe(label="π Details")
|
199 |
btn_l.click(lambda: (get_leads_summary(), get_leads_detail()), None, [sum_l, det_l])
|
200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
# -- Insights --
|
202 |
with gr.Tab("Insights"):
|
203 |
btn_i = gr.Button("Generate Insights")
|
|
|
24 |
return [c.strip().title() for c in cols]
|
25 |
|
26 |
def load_sheet_df(name):
|
|
|
|
|
|
|
|
|
|
|
27 |
ws = client.open_by_url(SHEET_URL).worksheet(name)
|
28 |
data = ws.get_all_values()
|
29 |
if not data:
|
|
|
126 |
# -------------------- USER MANAGEMENT --------------------
|
127 |
def load_users():
|
128 |
df = load_sheet_df("Users")
|
|
|
129 |
want = [
|
130 |
"Id", "Email", "Name", "Business", "Role",
|
131 |
"Daily Phone Call Target", "Daily Phone Appointment Target",
|
|
|
142 |
def save_users(df):
|
143 |
ws = client.open_by_url(SHEET_URL).worksheet("Users")
|
144 |
ws.clear()
|
145 |
+
set_with_dataframe(ws, df)
|
146 |
return "β
Users saved!"
|
147 |
|
148 |
+
# -------------------- QUOTES TAB UTILS --------------------
|
149 |
+
def get_quotes_df():
|
150 |
+
df = load_sheet_df("LiveQuotes")
|
151 |
+
df.columns = [c.strip() for c in df.columns]
|
152 |
+
return df
|
153 |
+
|
154 |
+
def rep_choices_quotes():
|
155 |
+
df = get_quotes_df()
|
156 |
+
return sorted(df["Rep"].dropna().unique().tolist()) if "Rep" in df else []
|
157 |
+
|
158 |
+
def quote_year_choices():
|
159 |
+
df = get_quotes_df()
|
160 |
+
if "Year" in df.columns:
|
161 |
+
years = sorted(df["Year"].dropna().unique().astype(str))
|
162 |
+
return years
|
163 |
+
if "Date" in df.columns:
|
164 |
+
years = pd.to_datetime(df["Date"], errors="coerce").dt.year.dropna().unique()
|
165 |
+
return sorted(years.astype(str))
|
166 |
+
return []
|
167 |
+
|
168 |
+
def quote_month_choices(year=None):
|
169 |
+
df = get_quotes_df()
|
170 |
+
if year and "Year" in df.columns and "Month" in df.columns:
|
171 |
+
months = df[df["Year"].astype(str) == str(year)]["Month"].dropna().unique()
|
172 |
+
months = [str(int(m)) for m in sorted(pd.to_numeric(months, errors="coerce").dropna().unique())]
|
173 |
+
return months
|
174 |
+
return []
|
175 |
+
|
176 |
+
def quotes_summary(year=None, month=None):
|
177 |
+
df = get_quotes_df()
|
178 |
+
if "Rep" not in df.columns or "Total" not in df.columns:
|
179 |
+
return pd.DataFrame([{"Error": "Missing Rep or Total column"}])
|
180 |
+
if year and "Year" in df.columns:
|
181 |
+
df = df[df["Year"].astype(str) == str(year)]
|
182 |
+
if month and "Month" in df.columns:
|
183 |
+
df = df[df["Month"].astype(str) == str(month)]
|
184 |
+
df["Total"] = pd.to_numeric(df["Total"].astype(str).str.replace(",", ""), errors="coerce")
|
185 |
+
summary = (
|
186 |
+
df.groupby("Rep")
|
187 |
+
.agg({"Document No.": "count", "Total": "sum"})
|
188 |
+
.rename(columns={"Document No.": "Total Quotes", "Total": "Total Value"})
|
189 |
+
.reset_index()
|
190 |
+
)
|
191 |
+
summary["Total Value"] = summary["Total Value"].fillna(0).round(2)
|
192 |
+
return summary
|
193 |
+
|
194 |
+
def get_rep_quotes_filtered(rep, year=None, month=None):
|
195 |
+
df = get_quotes_df()
|
196 |
+
if "Rep" not in df.columns:
|
197 |
+
return pd.DataFrame([{"Error": "Missing Rep column"}])
|
198 |
+
df = df[df["Rep"] == rep]
|
199 |
+
if year and "Year" in df.columns:
|
200 |
+
df = df[df["Year"].astype(str) == str(year)]
|
201 |
+
if month and "Month" in df.columns:
|
202 |
+
df = df[df["Month"].astype(str) == str(month)]
|
203 |
+
return df
|
204 |
+
|
205 |
# -------------------- UI LAYOUT --------------------
|
206 |
with gr.Blocks(title="Graffiti Admin Dashboard") as app:
|
207 |
gr.Markdown("# π Graffiti Admin Dashboard")
|
|
|
249 |
det_l = gr.Dataframe(label="π Details")
|
250 |
btn_l.click(lambda: (get_leads_summary(), get_leads_detail()), None, [sum_l, det_l])
|
251 |
|
252 |
+
# -- Quotes Tab (NEW) --
|
253 |
+
with gr.Tab("Quotes"):
|
254 |
+
gr.Markdown("### π Quotes Summary by Rep")
|
255 |
+
year_qs = gr.Dropdown(choices=[""] + quote_year_choices(), label="Year (optional)", value="")
|
256 |
+
month_qs = gr.Dropdown(choices=[""], label="Month (optional, needs year)", value="")
|
257 |
+
btn_qs = gr.Button("Show Quotes Summary")
|
258 |
+
sum_qs = gr.Dataframe(label="Summary by Rep")
|
259 |
+
|
260 |
+
# Dynamic month options for summary
|
261 |
+
def update_month_choices_summary(year):
|
262 |
+
if year:
|
263 |
+
return gr.Dropdown.update(choices=[""] + quote_month_choices(year), value="")
|
264 |
+
else:
|
265 |
+
return gr.Dropdown.update(choices=[""], value="")
|
266 |
+
year_qs.change(update_month_choices_summary, year_qs, month_qs)
|
267 |
+
|
268 |
+
def quotes_summary_wrapper(year, month):
|
269 |
+
return quotes_summary(year if year else None, month if month else None)
|
270 |
+
btn_qs.click(quotes_summary_wrapper, [year_qs, month_qs], sum_qs)
|
271 |
+
|
272 |
+
gr.Markdown("### π View All Quotes for a Rep, Year, and Month")
|
273 |
+
rep_q = gr.Dropdown(choices=rep_choices_quotes(), label="Select Rep")
|
274 |
+
year_q = gr.Dropdown(choices=[""] + quote_year_choices(), label="Year (optional)", value="")
|
275 |
+
month_q = gr.Dropdown(choices=[""], label="Month (optional, needs year)", value="")
|
276 |
+
btn_qr = gr.Button("Show Quotes")
|
277 |
+
tbl_qr = gr.Dataframe(label="Quotes for Selection")
|
278 |
+
|
279 |
+
# Dynamic month options for rep quotes
|
280 |
+
def update_month_choices(year):
|
281 |
+
if year:
|
282 |
+
return gr.Dropdown.update(choices=[""] + quote_month_choices(year), value="")
|
283 |
+
else:
|
284 |
+
return gr.Dropdown.update(choices=[""], value="")
|
285 |
+
year_q.change(update_month_choices, year_q, month_q)
|
286 |
+
|
287 |
+
def get_rep_quotes_filtered_wrapper(rep, year, month):
|
288 |
+
return get_rep_quotes_filtered(rep, year if year else None, month if month else None)
|
289 |
+
btn_qr.click(get_rep_quotes_filtered_wrapper, [rep_q, year_q, month_q], tbl_qr)
|
290 |
+
|
291 |
# -- Insights --
|
292 |
with gr.Tab("Insights"):
|
293 |
btn_i = gr.Button("Generate Insights")
|