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" } # ------------------ 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" sheet = client.open_by_url(sheet_url).worksheet("Calls") data = sheet.get_all_records() df = pd.DataFrame(data) # ------------------ DATA CLEANING ------------------ 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_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') 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 ALL_REPS = sorted(df['Rep Name'].dropna().unique()) # ------------------ DASHBOARD FUNCTIONS ------------------ def generate_summary(date_str): 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): reps = df[df['Date'] == date_str]['Rep Name'].dropna().unique() return gr.update(choices=sorted(reps)) def show_map(date_str, rep): 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") with gr.Tab("📊 Summary"): date_summary = gr.Dropdown(label="Select Date", choices=sorted(df['Date'].unique(), reverse=True)) 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=sorted(df['Date'].unique(), reverse=True)) 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]) 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()