Spaces:
Sleeping
Sleeping
| import pandas as pd | |
| import gspread | |
| import gradio as gr | |
| import plotly.express as px | |
| from oauth2client.service_account import ServiceAccountCredentials | |
| # ------------------ AUTH ------------------ | |
| VALID_USERS = { | |
| "andrew@lortechnologies.com": "Pass.123", | |
| "phonnie@carfind.co.za": "Pass.123", | |
| "monique@carfind.co.za": "Pass.123", | |
| "charne@carfind.co.za": "Pass.123" | |
| } | |
| # ------------------ GOOGLE SHEET SETUP ------------------ | |
| scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"] | |
| creds = ServiceAccountCredentials.from_json_keyfile_name("tough-star.json", scope) | |
| client = gspread.authorize(creds) | |
| sheet_url = "https://docs.google.com/spreadsheets/d/1bpeFS6yihb6niCavpwjWmVEypaSkGxONGg2jZfKX_sA" | |
| # ------------------ DATA REFRESH FUNCTION ------------------ | |
| def refresh_data(): | |
| sheet = client.open_by_url(sheet_url).worksheet("Calls") | |
| data = sheet.get_all_records() | |
| df = pd.DataFrame(data) | |
| # Timestamp parsing | |
| df['Timestamp'] = pd.to_datetime(df['Timestamp'], dayfirst=True, errors='coerce') | |
| df['Date'] = df['Timestamp'].dt.date.astype(str) | |
| df['Time'] = df['Timestamp'].dt.time | |
| # Location parsing | |
| location_split = df['Location'].str.split(',', expand=True) | |
| df['Latitude'] = pd.to_numeric(location_split[0], errors='coerce') | |
| df['Longitude'] = pd.to_numeric(location_split[1], errors='coerce') | |
| # Data cleaning | |
| df = df.dropna(subset=['Date', 'Rep Name', 'Latitude', 'Longitude']) | |
| df = df[(df['Latitude'] != 0) & (df['Longitude'] != 0)] | |
| df = df.sort_values(by=['Rep Name', 'Timestamp']) | |
| df['Time Diff (min)'] = df.groupby(['Rep Name', 'Date'])['Timestamp'].diff().dt.total_seconds().div(60).fillna(0) | |
| df['Visit Order'] = df.groupby(['Rep Name', 'Date']).cumcount() + 1 | |
| return df | |
| # ------------------ DEALER ESCALATIONS DATA FUNCTION ------------------ | |
| def get_dealer_escalations(): | |
| dealers_sheet = client.open_by_url(sheet_url).worksheet("Dealers") | |
| dealers_data = dealers_sheet.get_all_records() | |
| dealers_df = pd.DataFrame(dealers_data) | |
| # Standardize column names (in case of different casing/spacing) | |
| dealers_df.columns = [c.strip() for c in dealers_df.columns] | |
| # Filter for rows where Escalate Dealer == 'yes' (case-insensitive) | |
| mask = dealers_df['Escalate Dealer'].str.strip() == 'Yes' | |
| filtered_df = dealers_df.loc[mask, [ | |
| 'Dealership Name', | |
| 'Rep Name', | |
| 'Escalate Dealer', | |
| 'Escalation Comment' | |
| ]] | |
| # Optional: Sort by Rep Name and Dealership Name | |
| filtered_df = filtered_df.sort_values(by=['Rep Name', 'Dealership Name']) | |
| # If there are no escalations, show a friendly empty DataFrame | |
| if filtered_df.empty: | |
| filtered_df = pd.DataFrame( | |
| [["No dealer escalations found.", "", "", ""]], | |
| columns=['Dealership Name', 'Rep Name', 'Escalate Dealer', 'Escalation Comment'] | |
| ) | |
| return filtered_df | |
| # ------------------ DASHBOARD FUNCTIONS ------------------ | |
| def generate_summary(date_str): | |
| df = refresh_data() | |
| all_reps = sorted(df['Rep Name'].dropna().unique()) | |
| day_df = df[df['Date'] == date_str] | |
| active = day_df.groupby('Rep Name').size().reset_index(name='Total Visits') | |
| active_list = active['Rep Name'].tolist() | |
| inactive_list = [rep for rep in all_reps if rep not in active_list] | |
| inactive_df = pd.DataFrame({'Inactive Reps': inactive_list}) | |
| return active, inactive_df | |
| def get_reps(date_str): | |
| df = refresh_data() | |
| reps = df[df['Date'] == date_str]['Rep Name'].dropna().unique() | |
| return gr.update(choices=sorted(reps)) | |
| def show_map(date_str, rep): | |
| df = refresh_data() | |
| subset = df[(df['Date'] == date_str) & (df['Rep Name'] == rep)] | |
| if subset.empty: | |
| return "No valid data", None | |
| subset = subset.sort_values(by='Timestamp').copy() | |
| subset['Visit Order'] = range(1, len(subset) + 1) | |
| center_lat = subset['Latitude'].mean() | |
| center_lon = subset['Longitude'].mean() | |
| fig = px.line_mapbox( | |
| subset, | |
| lat="Latitude", lon="Longitude", | |
| hover_name="Dealership Name", | |
| hover_data={"Time": True, "Time Diff (min)": True, "Visit Order": True}, | |
| height=700, | |
| zoom=13, | |
| center={"lat": center_lat, "lon": center_lon} | |
| ) | |
| scatter = px.scatter_mapbox( | |
| subset, | |
| lat="Latitude", lon="Longitude", | |
| color="Visit Order", | |
| hover_name="Dealership Name", | |
| hover_data=["Time", "Time Diff (min)"], | |
| color_continuous_scale="Bluered" | |
| ) | |
| for trace in scatter.data: | |
| fig.add_trace(trace) | |
| fig.add_trace(px.scatter_mapbox( | |
| pd.DataFrame([subset.iloc[0]]), | |
| lat="Latitude", lon="Longitude", | |
| text=["Start"], color_discrete_sequence=["green"]).data[0]) | |
| fig.add_trace(px.scatter_mapbox( | |
| pd.DataFrame([subset.iloc[-1]]), | |
| lat="Latitude", lon="Longitude", | |
| text=["End"], color_discrete_sequence=["red"]).data[0]) | |
| fig.update_layout(mapbox_style="open-street-map", title=f"๐ {rep}'s Route on {date_str}") | |
| table = subset[[ | |
| 'Visit Order', 'Dealership Name', 'Time', 'Time Diff (min)', | |
| 'Type of call', 'Sales or service' | |
| ]].rename(columns={ | |
| 'Dealership Name': '๐งญ Dealer', | |
| 'Time': '๐ Time', | |
| 'Time Diff (min)': 'โฑ๏ธ Time Spent', | |
| 'Type of call': '๐ Call Type', | |
| 'Sales or service': '๐ผ Category' | |
| }) | |
| total_time = round(table['โฑ๏ธ Time Spent'].sum(), 2) | |
| summary_row = pd.DataFrame([{ | |
| 'Visit Order': '', | |
| '๐งญ Dealer': f"๐งฎ Total Time: {total_time} min", | |
| '๐ Time': '', | |
| 'โฑ๏ธ Time Spent': '', | |
| '๐ Call Type': '', | |
| '๐ผ Category': '' | |
| }]) | |
| table = pd.concat([table, summary_row], ignore_index=True) | |
| return table, fig | |
| # ------------------ GRADIO APP ------------------ | |
| with gr.Blocks() as app: | |
| with gr.Row(): | |
| with gr.Column(visible=True) as login_ui: | |
| gr.Markdown("## ๐ Login Required") | |
| email = gr.Textbox(label="Email") | |
| password = gr.Textbox(label="Password", type="password") | |
| login_btn = gr.Button("Login") | |
| login_msg = gr.Markdown("") | |
| with gr.Column(visible=False) as main_ui: | |
| gr.Markdown("## ๐๏ธ Carfind Rep Tracker") | |
| df_initial = refresh_data() | |
| unique_dates = sorted(df_initial['Date'].unique(), reverse=True) | |
| with gr.Tab("๐ Summary"): | |
| date_summary = gr.Dropdown(label="Select Date", choices=unique_dates) | |
| active_table = gr.Dataframe(label="โ Active Reps (with total visits)") | |
| inactive_table = gr.Dataframe(label="โ ๏ธ Inactive Reps") | |
| date_summary.change(fn=generate_summary, inputs=date_summary, outputs=[active_table, inactive_table]) | |
| with gr.Tab("๐ค KAM's"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| date_picker = gr.Dropdown(label="Select Date", choices=unique_dates) | |
| rep_picker = gr.Dropdown(label="Select Rep") | |
| btn = gr.Button("Show Route") | |
| with gr.Column(scale=2): | |
| table = gr.Dataframe(label="Call Table") | |
| map_plot = gr.Plot(label="Map") | |
| date_picker.change(fn=get_reps, inputs=date_picker, outputs=rep_picker) | |
| btn.click(fn=show_map, inputs=[date_picker, rep_picker], outputs=[table, map_plot]) | |
| with gr.Tab("๐จ Dealer Escalations"): | |
| gr.Markdown("### ๐จ Dealer Escalations (Only showing escalated dealers)") | |
| escalations_df = gr.Dataframe(value=get_dealer_escalations, label="Escalated Dealers", interactive=False) | |
| refresh_btn = gr.Button("๐ Refresh Escalations") | |
| # Refreshes the dataframe on button click | |
| refresh_btn.click(fn=get_dealer_escalations, outputs=escalations_df) | |
| def do_login(user, pw): | |
| if VALID_USERS.get(user) == pw: | |
| return gr.update(visible=False), gr.update(visible=True), "" | |
| else: | |
| return gr.update(visible=True), gr.update(visible=False), "โ Invalid email or password." | |
| login_btn.click(fn=do_login, inputs=[email, password], outputs=[login_ui, main_ui, login_msg]) | |
| app.launch() |