File size: 10,200 Bytes
44e7320
 
106b612
44e7320
106b612
44e7320
106b612
d685333
d975ba4
cf4a816
 
 
7074721
 
106b612
44e7320
ed3b18b
dfbe477
ed3b18b
f68916e
 
 
ed3b18b
f68916e
 
 
ed3b18b
 
c959952
f68916e
ed3b18b
 
 
f68916e
 
 
 
 
 
 
 
35f7fbb
ed3b18b
35f7fbb
f68916e
 
35f7fbb
 
 
f68916e
35f7fbb
 
 
65e2013
f68916e
65e2013
0b258de
44e7320
ed3b18b
65e2013
f68916e
65e2013
44e7320
 
 
a137762
 
 
 
 
44e7320
 
 
ed3b18b
a137762
 
 
 
 
 
 
 
 
 
ed3b18b
a137762
 
44e7320
a137762
 
 
 
 
 
 
 
 
 
 
 
 
87d485d
106b612
ed3b18b
 
35f7fbb
ed3b18b
87d485d
 
ed3b18b
 
87d485d
 
 
 
 
35f7fbb
87d485d
 
ed3b18b
 
87d485d
 
44e7320
 
ed3b18b
d685333
 
106b612
 
d685333
 
 
 
 
 
4bd42b3
f68916e
 
 
d685333
f68916e
d685333
 
 
 
 
 
 
f68916e
d685333
 
 
 
 
 
 
 
 
19384fe
 
 
c959952
19384fe
 
 
 
 
 
 
 
c959952
 
 
 
 
 
 
19384fe
 
0009227
19384fe
 
 
 
0009227
 
19384fe
0009227
 
19384fe
 
 
 
 
 
 
 
 
 
 
 
0009227
19384fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106b612
 
d685333
6cca50e
106b612
6cca50e
106b612
6cca50e
d685333
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import pandas as pd
import gspread
import gradio as gr
import plotly.express as px
from oauth2client.service_account import ServiceAccountCredentials

# ------------------ AUTH ------------------
VALID_USERS = {
    "[email protected]": "Pass.123",
    "[email protected]": "Pass.123",
    "[email protected]": "Pass.123",
    "[email protected]": "Pass.123"
}

# ------------------ GOOGLE SHEET SETUP ------------------
scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
creds = ServiceAccountCredentials.from_json_keyfile_name("bid4carsappsheetdash.json", scope)
client = gspread.authorize(creds)
sheet_url = "https://docs.google.com/spreadsheets/d/1eUXhcfWd3jtNmZC6U_Dr2F7obQcK81I4YrK-fKEbkyU"

# ------------------ DATA REFRESH FUNCTION ------------------
def refresh_data():
    sheet = client.open_by_url(sheet_url).worksheet("Calls2")
    data = sheet.get_all_records()
    df = pd.DataFrame(data)

    df['Timestamp'] = pd.to_datetime(df['Date'].astype(str) + " " + df['Time'].astype(str), errors='coerce')
    df['Date'] = pd.to_datetime(df['Date'], errors='coerce').dt.date.astype(str)
    df['Time'] = pd.to_datetime(df['Time'], format='%H:%M:%S', errors='coerce').dt.time

    gps_split = df['GPS'].astype(str).str.split(',', expand=True)
    df['Latitude'] = pd.to_numeric(gps_split[0], errors='coerce')
    df['Longitude'] = pd.to_numeric(gps_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

    return df

# ------------------ SUMMARY GENERATION ------------------
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))

# ------------------ MAP & TABLE VIEW ------------------
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",
        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",
        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', 'Time', 'Time Diff (min)', 
        'Interaction Type', 'Product'
    ]].rename(columns={
        'Dealership': '๐Ÿงญ Dealer',
        'Time': '๐Ÿ•’ Time',
        'Time Diff (min)': 'โฑ๏ธ Time Spent',
        'Interaction Type': '๐Ÿ“ž Interaction',
        'Product': '๐Ÿ’ผ Product Type'
    })

    total_time = round(table['โฑ๏ธ Time Spent'].sum(), 2)
    summary_row = pd.DataFrame([{
        'Visit Order': '',
        '๐Ÿงญ Dealer': f"๐Ÿงฎ Total Time: {total_time} min",
        '๐Ÿ•’ Time': '',
        'โฑ๏ธ Time Spent': '',
        '๐Ÿ“ž Interaction': '',
        '๐Ÿ’ผ Product Type': ''
    }])
    table = pd.concat([table, summary_row], ignore_index=True)
    return table, fig

# ------------------ GRADIO UI ------------------
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("## ๐Ÿ—‚๏ธ Bid4Cars FieldApp 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("๐Ÿ“„ Reports"):
                with gr.Row():
                    report_type = gr.Radio(choices=["Daily", "Weekly", "Monthly"], label="Report Type", value="Daily")
                    report_date = gr.Textbox(label="Select Date (YYYY-MM-DD)", placeholder="2025-05-27", type="text")
                    download_btn = gr.Button("๐Ÿ“ฅ Download CSV")

                report_table = gr.Dataframe(label="๐Ÿ“‹ Report Summary")
                report_info = gr.Markdown()
                download_file = gr.File(label="๐Ÿ“ Download Link")

                def generate_report(report_type, report_date):
                    df = refresh_data()
                    try:
                        date_obj = pd.to_datetime(report_date, errors='coerce')
                    except:
                        return pd.DataFrame(), "โš ๏ธ Invalid date format.", None

                    if pd.isnull(date_obj):
                        return pd.DataFrame(), "โš ๏ธ Date could not be parsed.", None

                    if report_type == "Daily":
                        title = f"๐Ÿ“† Report for {date_obj.strftime('%B %d, %Y')}"
                        mask = df['Date'] == str(date_obj.date())
                    elif report_type == "Weekly":
                        start = date_obj - pd.Timedelta(days=date_obj.weekday())
                        end = start + pd.Timedelta(days=6)
                        title = f"๐Ÿ“† Week of {start.strftime('%b %d')} โ€“ {end.strftime('%b %d')}, {start.year}"
                        mask = (pd.to_datetime(df['Date']) >= start) & (pd.to_datetime(df['Date']) <= end)
                    elif report_type == "Monthly":
                        title = f"๐Ÿ“† Report for {date_obj.strftime('%B %Y')}"
                        mask = pd.to_datetime(df['Date']).dt.to_period("M") == pd.to_datetime(date_obj).to_period("M")

                    filtered = df[mask]

                    if filtered.empty:
                        return pd.DataFrame(), "โš ๏ธ No data found for that range.", None

                    summary = filtered[[ 
                        'Date', 'Rep Name', 'Dealership', 'Time', 
                        'Interaction Type', 'Product', 'Time Diff (min)'
                    ]].sort_values(by=["Rep Name", "Date"])

                    insights = f"""
### {title}
### ๐Ÿ“Š Insights:
- **Total Visits:** {len(filtered)}
- **Unique Reps:** {filtered['Rep Name'].nunique()}
- **Most Active Rep:** {filtered['Rep Name'].value_counts().idxmax()}
- **Most Visited Dealership:** {filtered['Dealership'].value_counts().idxmax()}
- **Avg Time Between Visits:** {round(filtered['Time Diff (min)'].mean(), 2)} min
                    """

                    filename = f"Bid4Cars_Report_{report_type}_{report_date}.csv".replace(" ", "_")
                    summary.to_csv(filename, index=False)

                    return summary, insights, filename

                report_date.change(fn=generate_report, inputs=[report_type, report_date], outputs=[report_table, report_info, download_file])
                report_type.change(fn=generate_report, inputs=[report_type, report_date], outputs=[report_table, report_info, download_file])
                download_btn.click(fn=generate_report, inputs=[report_type, report_date], outputs=[report_table, report_info, download_file])

    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()