Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1 |
import pandas as pd
|
2 |
import gspread
|
3 |
import gradio as gr
|
4 |
-
import plotly.express as px
|
5 |
from oauth2client.service_account import ServiceAccountCredentials
|
6 |
|
7 |
# ------------------ AUTH ------------------
|
@@ -14,114 +13,41 @@ VALID_USERS = {
|
|
14 |
|
15 |
# ------------------ GOOGLE SHEET SETUP ------------------
|
16 |
scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
|
17 |
-
creds = ServiceAccountCredentials.from_json_keyfile_name("
|
18 |
client = gspread.authorize(creds)
|
19 |
-
sheet_url = "https://docs.google.com/spreadsheets/d/
|
20 |
|
21 |
-
# ------------------
|
22 |
-
def
|
23 |
-
sheet = client.open_by_url(sheet_url).worksheet(
|
24 |
data = sheet.get_all_records()
|
25 |
df = pd.DataFrame(data)
|
|
|
26 |
|
27 |
-
|
|
|
|
|
28 |
df['Date'] = pd.to_datetime(df['Date'], errors='coerce').dt.date.astype(str)
|
29 |
-
df[
|
30 |
-
|
31 |
-
gps_split = df['GPS'].astype(str).str.split(',', expand=True)
|
32 |
-
df['Latitude'] = pd.to_numeric(gps_split[0], errors='coerce')
|
33 |
-
df['Longitude'] = pd.to_numeric(gps_split[1], errors='coerce')
|
34 |
-
|
35 |
-
df = df.dropna(subset=['Date', 'Rep Name', 'Latitude', 'Longitude'])
|
36 |
-
df = df[(df['Latitude'] != 0) & (df['Longitude'] != 0)]
|
37 |
-
df = df.sort_values(by=['Rep Name', 'Timestamp'])
|
38 |
-
df['Time Diff (min)'] = df.groupby(['Rep Name', 'Date'])['Timestamp'].diff().dt.total_seconds().div(60).fillna(0)
|
39 |
-
df['Visit Order'] = df.groupby(['Rep Name', 'Date']).cumcount() + 1
|
40 |
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
# ------------------ MAP & TABLE VIEW ------------------
|
60 |
-
def show_map(date_str, rep):
|
61 |
-
df = refresh_data()
|
62 |
-
subset = df[(df['Date'] == date_str) & (df['Rep Name'] == rep)]
|
63 |
-
if subset.empty:
|
64 |
-
return "No valid data", None
|
65 |
-
|
66 |
-
subset = subset.sort_values(by='Timestamp').copy()
|
67 |
-
subset['Visit Order'] = range(1, len(subset) + 1)
|
68 |
-
center_lat = subset['Latitude'].mean()
|
69 |
-
center_lon = subset['Longitude'].mean()
|
70 |
-
|
71 |
-
fig = px.line_mapbox(
|
72 |
-
subset,
|
73 |
-
lat="Latitude", lon="Longitude",
|
74 |
-
hover_name="Dealership",
|
75 |
-
hover_data={"Time": True, "Time Diff (min)": True, "Visit Order": True},
|
76 |
-
height=700,
|
77 |
-
zoom=13,
|
78 |
-
center={"lat": center_lat, "lon": center_lon}
|
79 |
-
)
|
80 |
-
|
81 |
-
scatter = px.scatter_mapbox(
|
82 |
-
subset,
|
83 |
-
lat="Latitude", lon="Longitude",
|
84 |
-
color="Visit Order",
|
85 |
-
hover_name="Dealership",
|
86 |
-
hover_data=["Time", "Time Diff (min)"],
|
87 |
-
color_continuous_scale="Bluered"
|
88 |
-
)
|
89 |
-
for trace in scatter.data:
|
90 |
-
fig.add_trace(trace)
|
91 |
-
|
92 |
-
fig.add_trace(px.scatter_mapbox(
|
93 |
-
pd.DataFrame([subset.iloc[0]]),
|
94 |
-
lat="Latitude", lon="Longitude",
|
95 |
-
text=["Start"], color_discrete_sequence=["green"]).data[0])
|
96 |
-
fig.add_trace(px.scatter_mapbox(
|
97 |
-
pd.DataFrame([subset.iloc[-1]]),
|
98 |
-
lat="Latitude", lon="Longitude",
|
99 |
-
text=["End"], color_discrete_sequence=["red"]).data[0])
|
100 |
-
|
101 |
-
fig.update_layout(mapbox_style="open-street-map", title=f"{rep}'s Route on {date_str}")
|
102 |
-
|
103 |
-
table = subset[[
|
104 |
-
'Visit Order', 'Dealership', 'Time', 'Time Diff (min)',
|
105 |
-
'Interaction Type', 'Product'
|
106 |
-
]].rename(columns={
|
107 |
-
'Dealership': 'Dealer',
|
108 |
-
'Time': 'Time',
|
109 |
-
'Time Diff (min)': 'Time Spent (min)',
|
110 |
-
'Interaction Type': 'Interaction',
|
111 |
-
'Product': 'Product Type'
|
112 |
-
})
|
113 |
-
|
114 |
-
total_time = round(table['Time Spent (min)'].sum(), 2)
|
115 |
-
summary_row = pd.DataFrame([{
|
116 |
-
'Visit Order': '',
|
117 |
-
'Dealer': f"Total Time: {total_time} min",
|
118 |
-
'Time': '',
|
119 |
-
'Time Spent (min)': '',
|
120 |
-
'Interaction': '',
|
121 |
-
'Product Type': ''
|
122 |
-
}])
|
123 |
-
table = pd.concat([table, summary_row], ignore_index=True)
|
124 |
-
return table, fig
|
125 |
|
126 |
# ------------------ GRADIO UI ------------------
|
127 |
with gr.Blocks() as app:
|
@@ -134,89 +60,31 @@ with gr.Blocks() as app:
|
|
134 |
login_msg = gr.Markdown("")
|
135 |
|
136 |
with gr.Column(visible=False) as main_ui:
|
137 |
-
gr.Markdown("## ๐๏ธ
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
report_type = gr.Radio(choices=["Daily", "Weekly", "Monthly"], label="Report Type", value="Daily")
|
163 |
-
report_date = gr.Textbox(label="Enter Date (YYYY-MM-DD)", placeholder="2025-05-27")
|
164 |
-
download_btn = gr.Button("๐ฅ Download CSV")
|
165 |
-
|
166 |
-
report_table = gr.Dataframe(label="๐ Report Summary")
|
167 |
-
report_info = gr.Markdown()
|
168 |
-
download_file = gr.File(label="๐ Download Link")
|
169 |
-
|
170 |
-
def generate_report(report_type, report_date):
|
171 |
-
df = refresh_data()
|
172 |
-
try:
|
173 |
-
date_obj = pd.to_datetime(str(report_date), errors='coerce')
|
174 |
-
except:
|
175 |
-
return pd.DataFrame(), "โ ๏ธ Invalid date format.", None
|
176 |
-
|
177 |
-
if pd.isnull(date_obj):
|
178 |
-
return pd.DataFrame(), "โ ๏ธ Date could not be parsed.", None
|
179 |
-
|
180 |
-
if report_type == "Daily":
|
181 |
-
title = f"๐ Report for {date_obj.strftime('%B %d, %Y')}"
|
182 |
-
mask = df['Date'] == str(date_obj.date())
|
183 |
-
elif report_type == "Weekly":
|
184 |
-
start = date_obj - pd.Timedelta(days=date_obj.weekday())
|
185 |
-
end = start + pd.Timedelta(days=6)
|
186 |
-
title = f"๐ Week of {start.strftime('%b %d')} โ {end.strftime('%b %d')}, {start.year}"
|
187 |
-
mask = (pd.to_datetime(df['Date']) >= start) & (pd.to_datetime(df['Date']) <= end)
|
188 |
-
elif report_type == "Monthly":
|
189 |
-
title = f"๐ Report for {date_obj.strftime('%B %Y')}"
|
190 |
-
mask = pd.to_datetime(df['Date']).dt.to_period("M") == pd.to_datetime(date_obj).to_period("M")
|
191 |
-
|
192 |
-
filtered = df[mask]
|
193 |
-
|
194 |
-
if filtered.empty:
|
195 |
-
return pd.DataFrame(), "โ ๏ธ No data found for that range.", None
|
196 |
-
|
197 |
-
summary = filtered[[
|
198 |
-
'Date', 'Rep Name', 'Dealership', 'Time',
|
199 |
-
'Interaction Type', 'Product', 'Time Diff (min)'
|
200 |
-
]].sort_values(by=["Rep Name", "Date"])
|
201 |
-
|
202 |
-
insights = f"""
|
203 |
-
### {title}
|
204 |
-
### ๐ Insights:
|
205 |
-
- **Total Visits:** {len(filtered)}
|
206 |
-
- **Unique Reps:** {filtered['Rep Name'].nunique()}
|
207 |
-
- **Most Active Rep:** {filtered['Rep Name'].value_counts().idxmax()}
|
208 |
-
- **Most Visited Dealership:** {filtered['Dealership'].value_counts().idxmax()}
|
209 |
-
- **Avg Time Between Visits:** {round(filtered['Time Diff (min)'].mean(), 2)} min
|
210 |
-
"""
|
211 |
-
|
212 |
-
filename = f"Bid4Cars_Report_{report_type}_{date_obj.strftime('%Y-%m-%d')}.csv".replace(" ", "_")
|
213 |
-
summary.to_csv(filename, index=False)
|
214 |
-
|
215 |
-
return summary, insights, filename
|
216 |
-
|
217 |
-
report_date.change(fn=generate_report, inputs=[report_type, report_date], outputs=[report_table, report_info, download_file])
|
218 |
-
report_type.change(fn=generate_report, inputs=[report_type, report_date], outputs=[report_table, report_info, download_file])
|
219 |
-
download_btn.click(fn=generate_report, inputs=[report_type, report_date], outputs=[report_table, report_info, download_file])
|
220 |
|
221 |
def do_login(user, pw):
|
222 |
if VALID_USERS.get(user) == pw:
|
@@ -226,4 +94,4 @@ with gr.Blocks() as app:
|
|
226 |
|
227 |
login_btn.click(fn=do_login, inputs=[email, password], outputs=[login_ui, main_ui, login_msg])
|
228 |
|
229 |
-
app.launch()
|
|
|
1 |
import pandas as pd
|
2 |
import gspread
|
3 |
import gradio as gr
|
|
|
4 |
from oauth2client.service_account import ServiceAccountCredentials
|
5 |
|
6 |
# ------------------ AUTH ------------------
|
|
|
13 |
|
14 |
# ------------------ GOOGLE SHEET SETUP ------------------
|
15 |
scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
|
16 |
+
creds = ServiceAccountCredentials.from_json_keyfile_name("deep-mile-461309-t8-0e90103411e0.json", scope)
|
17 |
client = gspread.authorize(creds)
|
18 |
+
sheet_url = "https://docs.google.com/spreadsheets/d/1if4KoVQvw5ZbhknfdZbzMkcTiPfsD6bz9V3a1th-bwQ"
|
19 |
|
20 |
+
# ------------------ SHEET REFRESH FUNCTIONS ------------------
|
21 |
+
def load_sheet(sheet_name):
|
22 |
+
sheet = client.open_by_url(sheet_url).worksheet(sheet_name)
|
23 |
data = sheet.get_all_records()
|
24 |
df = pd.DataFrame(data)
|
25 |
+
return df
|
26 |
|
27 |
+
# ------------------ REPORTS TAB ------------------
|
28 |
+
def filter_calls_by_date(date):
|
29 |
+
df = load_sheet("Calls")
|
30 |
df['Date'] = pd.to_datetime(df['Date'], errors='coerce').dt.date.astype(str)
|
31 |
+
return df[df['Date'] == date]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
+
def filter_appointments_by_date(date):
|
34 |
+
df = load_sheet("Appointments")
|
35 |
+
df['Date'] = pd.to_datetime(df['Date'], errors='coerce').dt.date.astype(str)
|
36 |
+
return df[df['Date'] == date]
|
37 |
+
|
38 |
+
# ------------------ APPOINTED LEADS ------------------
|
39 |
+
def appointed_leads_table():
|
40 |
+
df = load_sheet("Appointed Leads")
|
41 |
+
grouped = df.groupby('Rep')['Customer Name'].apply(list).reset_index()
|
42 |
+
return grouped
|
43 |
+
|
44 |
+
# ------------------ INTERACTIVE QUERY VIEW ------------------
|
45 |
+
def search_table(sheet_name, field, keyword):
|
46 |
+
df = load_sheet(sheet_name)
|
47 |
+
if field not in df.columns:
|
48 |
+
return pd.DataFrame(), "Field not found."
|
49 |
+
results = df[df[field].astype(str).str.contains(keyword, case=False, na=False)]
|
50 |
+
return results, f"Found {len(results)} results."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
# ------------------ GRADIO UI ------------------
|
53 |
with gr.Blocks() as app:
|
|
|
60 |
login_msg = gr.Markdown("")
|
61 |
|
62 |
with gr.Column(visible=False) as main_ui:
|
63 |
+
gr.Markdown("## ๐๏ธ Graffiti Admin Dashboard")
|
64 |
+
|
65 |
+
with gr.Tab("๐
Calls Report"):
|
66 |
+
calls_date = gr.Textbox(label="Enter Date (YYYY-MM-DD)")
|
67 |
+
calls_table = gr.Dataframe()
|
68 |
+
calls_date.change(lambda d: filter_calls_by_date(d), inputs=calls_date, outputs=calls_table)
|
69 |
+
|
70 |
+
with gr.Tab("๐
Appointments Report"):
|
71 |
+
appt_date = gr.Textbox(label="Enter Date (YYYY-MM-DD)")
|
72 |
+
appt_table = gr.Dataframe()
|
73 |
+
appt_date.change(lambda d: filter_appointments_by_date(d), inputs=appt_date, outputs=appt_table)
|
74 |
+
|
75 |
+
with gr.Tab("๐ง Appointed Leads"):
|
76 |
+
leads_btn = gr.Button("View Appointed Leads")
|
77 |
+
leads_output = gr.Dataframe()
|
78 |
+
leads_btn.click(fn=appointed_leads_table, outputs=leads_output)
|
79 |
+
|
80 |
+
with gr.Tab("๐ Query Live Sheets"):
|
81 |
+
sheet_choice = gr.Dropdown(choices=["LiveQuotes", "LiveCustomer", "LiveJobBags"], label="Select Sheet")
|
82 |
+
field_input = gr.Textbox(label="Field (column name)")
|
83 |
+
keyword_input = gr.Textbox(label="Keyword to search")
|
84 |
+
query_btn = gr.Button("Search")
|
85 |
+
query_table = gr.Dataframe()
|
86 |
+
query_info = gr.Markdown()
|
87 |
+
query_btn.click(fn=search_table, inputs=[sheet_choice, field_input, keyword_input], outputs=[query_table, query_info])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
|
89 |
def do_login(user, pw):
|
90 |
if VALID_USERS.get(user) == pw:
|
|
|
94 |
|
95 |
login_btn.click(fn=do_login, inputs=[email, password], outputs=[login_ui, main_ui, login_msg])
|
96 |
|
97 |
+
app.launch()
|