Jarrett R. Stanley Picture

Jarrett R. Stanley

Code Snippets: My Approach & Problem Solving

This section offers a glimpse into my coding style, problem-solving techniques, and a few reusable patterns. I believe in clean, efficient, and well-documented code that addresses real-world business needs.

Outlook Attachment Scraper with Advanced Logging and Graceful Exit (Python + win32com)

This script demonstrates robust automation for managing email attachments. Key problem-solving aspects include: handling file duplicates by moving them to a dedicated folder, implementing comprehensive logging for traceability and debugging, and enabling a graceful exit mechanism via a separate thread for user control during long-running operations. This ensures data integrity and operational resilience for critical compliance and data management tasks.

import os
import win32com.client
from datetime import datetime
import uuid
import time
import threading
import logging
import traceback

# Set base folder
save_folder = r"Desired\Path\To\Save\Attachments"

# Create necessary folders
duplicate_folder = os.path.join(save_folder, "Duplicate")
log_folder = os.path.join(save_folder, "Log")

os.makedirs(duplicate_folder, exist_ok=True)
os.makedirs(log_folder, exist_ok=True)

# Configure logging
log_filename = os.path.join(log_folder, f"log_{datetime.now().strftime('%Y-%m-%d')}.txt")
logging.basicConfig(
    filename=log_filename,
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

logging.info("Script started.")

# Initialize Outlook
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
inbox = outlook.GetDefaultFolder(6)

def process_attachments():
    # Locate the 'Organized' folder
    organized_folder = None
    for folder in inbox.Folders:
        if folder.Name == "Organized":
            organized_folder = folder
            break

    # Locate the 'PM' folder
    PM_DOCS = None
    if organized_folder:
        for folder in organized_folder.Folders:
            if folder.Name == "PM_Data":
                PM_DOCS = folder
                break

    # Locate or create 'Processed' folder
    processed_folder = None
    for folder in inbox.Folders:
        if folder.Name == "Processed":
            processed_folder = folder
            break

    if not processed_folder:
        processed_folder = inbox.Folders.Add("Processed")

    if PM_DOCS:
        items_to_process = list(PM_DOCS.Items)
        processed_count = 0

        for item in items_to_process:
            if item.Class == 43 and item.Attachments.Count > 0:
                processed = False

                for attachment in item.Attachments:
                    if attachment.Type == 1 and attachment.FileName.lower().endswith(".pdf"):
                        attachment_filename = attachment.FileName
                        attachment_path = os.path.join(save_folder, attachment_filename)

                        # Create unique file name
                        unique_id = uuid.uuid4().hex[:8]
                        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                        file_name, file_extension = os.path.splitext(attachment_filename)
                        new_attachment_filename = f"{file_name}_{timestamp}_{unique_id}{file_extension}"

                        # Save to correct location
                        if os.path.exists(attachment_path):
                            # Duplicate
                            new_attachment_path = os.path.join(duplicate_folder, new_attachment_filename)
                            attachment.SaveAsFile(new_attachment_path)
                            logging.info(f"Duplicate file saved: {new_attachment_path}")
                        else:
                            # Original
                            temp_path = os.path.join(save_folder, attachment_filename)
                            attachment.SaveAsFile(temp_path)
                            new_attachment_path = os.path.join(save_folder, new_attachment_filename)
                            os.rename(temp_path, new_attachment_path)
                            logging.info(f"Processed file: {new_attachment_path}")
                            processed = True

                if processed:
                    item.Move(processed_folder)
                    processed_count += 1

        logging.info(f"Checked and processed {processed_count} emails.")
    else:
        logging.warning("Folder structure not found'.")

# Flag to stop the loop
stop_flag = False

def monitor_input():
    global stop_flag
    print("Type 'exit' to stop the script:")
    buffer = ""
    import msvcrt
    while not stop_flag:
        if msvcrt.kbhit():
            char = msvcrt.getwche()
            if char in ('\r', '\n'):
                if buffer.strip().lower() == "exit":
                    print("\nExiting...")
                    stop_flag = True
                buffer = ""
            else:
                buffer += char
        time.sleep(0.1)

# Start input monitor thread
input_thread = threading.Thread(target=monitor_input, daemon=True)
input_thread.start()

# Main loop
while not stop_flag:
    try:
        process_attachments()
    except Exception as e:
        logging.error(f"Error occurred: {str(e)}")
        logging.error(traceback.format_exc())

    for _ in range(60):  # Sleep in 10s intervals for responsiveness
        if stop_flag:
            break
        time.sleep(10)

logging.info("Script has stopped.")
print("Script has stopped.")

FMS API CLI Tool for Batch Equipment Updates (Python + `aiohttp`, `pandas`)

This CLI tool demonstrates comprehensive problem-solving for data management with an external API. It includes a flexible field mapping system for user-friendly data input, a critical dry-run mode to prevent accidental live system changes, and robust error handling with detailed logging for auditing and debugging. The use of `aiohttp` and `tqdm` also highlights efficient handling of multiple asynchronous API requests and user feedback during batch operations.

import asyncio
import aiohttp
import pandas as pd
from tqdm.asyncio import tqdm
import logging
from getpass import getpass
import os
import csv
from pathlib import Path
from datetime import datetime

# Mapping of human-readable field names to  integration names
FIELD_MAP = {
    "Accessory Last Annual PM": "accessory_last_annual_pm",
    "Accessory Next Annual PM": "accessory_next_annual_pm",
    "Accessory Last PM": "accessory_last_pm",
    "Accessory Next PM": "accessory_next_pm",
    "Chassis Last Annual PM": "Last_Annual_PM",
    "Chassis Next Annual PM Due Date": "Next_Annual_PM_Due_Date",
    "Chassis Last PM": "Last_PM",
    "Chassis Next PM Due Date": "Next_PM_Due_Date",
    "Color": "Color",
    "Engine Displacement": "Engine_Displacement",
    "Engine Family Name": "Engine_Family_Name",
    "Engine Make": "Engine_Make",
    "Engine Model": "Engine_Model",
    "Engine Notification Value": "Engine_Notification_Value",
    "Engine Serial #": "Engine_Serial_Num",
    "Engine Year": "Engine_Year",
    "GeoTab ID": "GeoTab_ID",
    "Camera IMEI": "Camera_IMEI",
    "Geotab Serial Number": "Geotab_Serial_Number",
    "GVW": "GVW",
    "License Exp": "License_Exp",
    "License Expiration": "License_Expiration",
    "License Plate": "License",
    "License State": "state",
    "License Type": "License_Type",
    "Title": "Title",
    "Toll Tag #": "Toll_Tag_Num",
    "Toll Tag Effective Date": "Toll_Tag_Effective_Date",
    "Toll Tag Expiration": "Toll_Tag_Expiration",
}

def prompt_credentials():
    print("๐Ÿ” Enter your credentials:")
    login_name = input("Login Name: ").strip()
    password = getpass("Password: ").strip()
    cust_id = input("Customer ID: ").strip()
    return login_name, password, cust_id


def prompt_field_selection():
    print("\n๐Ÿ“ Select fields to update (comma-separated numbers):")
    for i, field in enumerate(FIELD_MAP.keys(), 1):
        print(f"{i}. {field}")
    selection = input("Enter numbers (e.g. 1,3,5): ").strip()
    try:
        selected = [list(FIELD_MAP.keys())[int(i) - 1] for i in selection.split(",") if i.strip().isdigit()]
        return selected
    except (IndexError, ValueError):
        print("โš ๏ธ Invalid selection. Please enter comma-separated numbers.")
        return prompt_field_selection()


def sanitize_path(path: str) -> str:
    return path.strip().strip('"').strip("'").strip()


def prompt_excel_path():
    return sanitize_path(input('\n๐Ÿ“„ Enter path to Excel file: '))


def prompt_generate_template(selected_fields):
    answer = input("\n๐Ÿ“ Generate Excel template with required columns? (y/n): ").strip().lower()
    return answer == "y"

def get_default_template_path():
    downloads = Path.home() / "Downloads"
    downloads.mkdir(exist_ok=True)  # ensure it exists
    return str(downloads / "FMS_UPDATE_TEMPLATE.xlsx")


def create_excel_template(path, selected_fields, overwrite=False):
    headers = ['equipment_id'] + selected_fields
    df = pd.DataFrame(columns=headers)

    if Path(path).exists() and not overwrite:
        print(f"โš ๏ธ File already exists: {path}")
        confirm = input("Overwrite? (y/n): ").strip().lower()
        if confirm != 'y':
            print("๐Ÿšซ Skipping template creation.")
            return

    df.to_excel(path, index=False)
    print(f"โœ… Template saved to: {path}")


async def login(login_name, password, cust_id):
    login_url = "UPDATE_URL_HERE"
    headers = {
        'loginName': login_name,
        'password': password,
        'custId': cust_id
    }

    async with aiohttp.ClientSession() as session:
        async with session.get(login_url, headers=headers) as response:
            text = await response.text()
            if response.status != 200 or "" not in text:
                raise Exception("Login failed. Response:\n" + text)
            session_id = text.split("")[1].split("")[0]
            print("โœ… Logged in successfully.")
            return session_id

def read_excel(file_path, selected_fields):
    df = pd.read_excel(file_path)
    df.columns = [str(col).strip() for col in df.columns]  # Normalize headers

    expected_cols = ['equipment_id'] + selected_fields
    missing = [col for col in expected_cols if col not in df.columns]
    if missing:
        raise ValueError(f"โŒ Missing required columns in Excel: {missing}")

    print(f"โœ… Found {len(df)} records in Excel")

    updates = {}
    for _, row in df.iterrows():
        equipment_id = row['equipment_id']
        update_fields = {FIELD_MAP[f]: row[f] for f in selected_fields if pd.notna(row[f])}
        if update_fields:
            updates[equipment_id] = update_fields

    return updates

async def update_record(session, base_url, session_id, equipment_id, fields, log_writer, dry_run=False):
    params = {
        "sessionId": session_id,
        "objName": "Equipment1",
        "id": equipment_id,
        "useIds": "false"
    }
    params.update({k: str(v) for k, v in fields.items()})

    if dry_run:
        status = "โœ… Dry run - no update sent"
        log_writer.writerow([equipment_id, "DRY_RUN", "Success", status])
        return

    try:
        async with session.post(base_url, params=params) as response:
            text = await response.text()
            status_code = response.status
            result = "Success" if status_code == 200 else "Failed"
            log_writer.writerow([equipment_id, status_code, result, text])
    except Exception as e:
        log_writer.writerow([equipment_id, "ERROR", "Failed", str(e)])

async def send_updates(updates, session_id, dry_run=False):
    base_url = "UPDATE_URL_HERE"  # Replace with actual update URL
    log_file = "update_log.csv"
    with open(log_file, "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["equipment_id", "response_code", "status", "details"])
        async with aiohttp.ClientSession() as session:
            for equipment_id, fields in tqdm(updates.items(), desc="๐Ÿšš Sending updates"):
                await update_record(session, base_url, session_id, equipment_id, fields, writer, dry_run=dry_run)

    print(f"๐Ÿงพ Log saved to: {log_file}")

async def main():
    print("๐Ÿ” Enter your  credentials:")
    login_name = input("Login Name: ").strip()
    password = getpass("Password: ").strip()
    cust_id = input("Customer ID: ").strip()

    selected_fields = prompt_field_selection()

    if input("๐Ÿ“„ Would you like to generate an Excel template for the selected fields? (y/n): ").lower() == "y":
        template_path = get_default_template_path()
        overwrite = input("โš ๏ธ Overwrite if file exists? (y/n): ").lower() == "y"
        create_excel_template(template_path, selected_fields, overwrite=overwrite)
        # return

    dry_run = input("๐Ÿ”Ž Dry run (no updates sent)? (y/n): ").lower() == "y"

    try:
        session_id = await login(login_name, password, cust_id)
        print("โœ… Logged in successfully.")
    except Exception as e:
        print(f"โŒ Login failed: {e}")
        return

    file_path = input("๐Ÿ“„ Enter path to Excel file: ").strip().strip('"')

    try:
        updates = read_excel(file_path, selected_fields)
        if not updates:
            print("โš ๏ธ No updates to process. Check your Excel data.")
            return
        print(f"๐Ÿ“ฆ Prepared {len(updates)} records for update.")
    except Exception as e:
        print(f"โŒ Failed to read Excel file: {e}")
        return

    await send_updates(updates, session_id, dry_run=dry_run)

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\n๐Ÿ‘‹ Operation cancelled by user.")

VIN Decoder Desktop Application (Python + Tkinter)

This desktop application provides a user-friendly interface for VIN decoding by integrating with the NHTSA API. Problem-solving is evident in its clear GUI design for ease of use, robust error handling for API requests, and dynamic updating of result fields, which gracefully manages the display of varying data returned from the API. This tool simplifies a complex data retrieval process for end-users.

import tkinter as tk
import requests
import xml.dom.minidom
import tkinter.messagebox as messagebox

class VinDecoderApp:
    def __init__(self, root):
        self.root = root
        self.root.title("VIN Decoder App")

        bground = '#ffffff'
        fground = '#000000'
        tkfont =  ("Areial", 12)
        btnfont = ("Areial", 12, "bold")
        warnfont = ("Areial", 10, "bold")     

        root.configure(bg=bground)
             
        # Label for VIN entry
        self.vin_label = tk.Label(root, text="Enter VIN", font=("Areial",14,'bold'), bg=bground, fg=fground)
        self.vin_label.grid(row=0, column=0, pady=10, padx=10, columnspan=1, sticky="w")

        # Entry for VIN input
        self.vin_entry = tk.Entry(root, width=30)
        self.vin_entry.grid(row=0, column=1, pady=5, padx=10, columnspan=4, sticky="ew")

        # Fields to display VIN information
        self.result_fields = {}
        fields_column1 = ["Manufacturer", "ModelYear", "Make", "VIN", "Axles", "BodyCabType", "DisplacementCC", "EngineModel",
                          "GVWR", "PlantCountry", "VehicleType", "BrakeSystemType", "TransmissionStyle", "WheelBaseLong", "Wheels"]

        fields_column2 = ["Model", "BodyClass", "DriveType", "FuelTypePrimary", "PlantCompanyName", "PlantState", "CurbWeightLB", 
                          "WheelBaseShort", "WheelBaseType", "WheelSizeFront", "WheelSizeRear", "ErrorText"]

        for i, (field1, field2) in enumerate(zip(fields_column1, fields_column2)):
            label1 = tk.Label(root, text=field1, bg=bground, fg=fground, font=tkfont)
            label1.grid(row=i + 2, column=0, pady=8, padx=10, sticky="e")

            entry1 = tk.Entry(root, state="readonly", width=40)
            entry1.grid(row=i + 2, column=1, pady=8, padx=10, sticky="w")
            self.result_fields[field1] = entry1

            label2 = tk.Label(root, text=field2, bg=bground, fg=fground, font=tkfont)
            label2.grid(row=i + 2, column=2, pady=8, padx=10, sticky="e")

            entry2 = tk.Entry(root, state="readonly", width=40)
            entry2.grid(row=i + 2, column=3, pady=8, padx=10, sticky="w")
            self.result_fields[field2] = entry2

        # Button to fetch and display VIN information
        self.fetch_button = tk.Button(root, text="Decode Vin", font=btnfont, command=self.fetch_and_display)
        self.fetch_button.grid(row=i + 3, columnspan=2, column=0, pady=10, padx=10, sticky="ew")

        # Button to clear VIN entry and results
        self.clear_button = tk.Button(root, text="Clear", font=btnfont, command=self.clear_entries)
        self.clear_button.grid(row=i + 3, columnspan=2, column=2, pady=10, padx=10, sticky="ew")
        
        # Warning Label
        self.vin_label = tk.Label(root, font=warnfont, text="This application is designed to access VIN data from the NHTSA website.", bg=bground, fg=fground)
        self.vin_label.grid(row=i + 4, column=0, pady=10, padx=10, columnspan=4, sticky="ew")
        # Warning Label
        self.vin_label = tk.Label(root, font=warnfont, text="Please exercise caution when interpreting results, as variations in our equipment may not align precisely with the manufacturer's information.", bg=bground, fg=fground)
        self.vin_label.grid(row=i + 5, column=0, pady=10, padx=10, columnspan=4, sticky="ew")

    def fetch_and_display(self):
        self.fetch_button.configure(state="disabled")
        
        # Clear out prior entries
        self.clear_result_fields()

        vin = self.vin_entry.get().strip()
        api_url = "https://vpic.nhtsa.dot.gov/api/vehicles/decodevinvalues/"

        try:
            response = self.make_api_request(api_url + vin)
            self.update_result_fields(response)

        except requests.exceptions.RequestException as e:
            error_message = f"Error fetching data: {str(e)}"
            self.display_error_message(error_message)
        
        finally:
            # Enables the fetch button.
            self.fetch_button.configure(state="normal")

    def make_api_request(self, url):
        with requests.Session() as session:
            response = session.get(url, verify=True)
            response.raise_for_status()
            return response

    def update_result_fields(self, response):
        dom = xml.dom.minidom.parseString(response.text)

        # Update result fields with VIN information
        for field, entry in self.result_fields.items():
            elements = dom.getElementsByTagName(field)
            if elements and elements[0].firstChild:
                value = elements[0].firstChild.nodeValue.strip()
            else:
                value = "N/A"
            entry.configure(state="normal")
            entry.delete(0, tk.END)
            entry.insert(0, value)
            entry.configure(state="readonly")

    def display_error_message(self, message):
        messagebox.showinfo("Error", message)

    def clear_entries(self):
        # Clear VIN entry and result fields
        self.vin_entry.delete(0, tk.END)
        self.clear_result_fields()

    def clear_result_fields(self):
        for entry in self.result_fields.values():
            entry.configure(state="normal")
            entry.delete(0, tk.END)
            entry.configure(state="readonly")

if __name__ == "__main__":
    root = tk.Tk()
    app = VinDecoderApp(root)
    root.mainloop()

Fleet Innovation Admin Tool - Authentication and Device Management (HTML/JavaScript)

This HTML/JavaScript snippet showcases problem-solving in building an interactive web-based administration tool. It includes a clear authentication mechanism for secure API access and a structured approach to fetching and displaying device information. The code addresses user experience by providing visual feedback on authentication status and by dynamically rendering data, offering a practical solution for managing telematics devices directly from a web browser.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>FIAT</title>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link href="https://cdn.muicss.com/mui-0.10.3/css/mui.min.css" rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
    <style>
        /* Variables */
        /* Variables */
        :root {
            --primary-color: #4a90e2;
            --accent-color: #ff6e40;
            --error-color: #d32f2f;
            --success-color: #4caf50;
            --light-bg: #f5f7fa;
            --light-container: #ffffff;
            --dark-bg: #121212;
            --dark-container: #1e1e1e;
            --border-radius: 8px;
            --box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
            --transition: all 0.3s ease;
        }

        /* Base Styles */
        body {
            font-family: 'Roboto', sans-serif;
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            background-color: var(--light-bg);
            color: #333;
            line-height: 1.6;
        }

        .container {
            max-width: 900px;
            margin: 30px auto;
            padding: 25px;
            background-color: var(--light-container);
            border-radius: var(--border-radius);
            box-shadow: var(--box-shadow);
        }

        h1,
        h2 {
            color: var(--primary-color);
            text-align: center;
            margin-bottom: 25px;
        }

        .mui-textfield input,
        .mui-textfield textarea,
        .mui-btn {
            border-radius: 5px;
        }

        .mui-btn-primary {
            background-color: var(--primary-color);
        }

        .mui-btn-danger {
            background-color: var(--error-color);
        }

        .mui-btn {
            transition: var(--transition);
        }

        .mui-btn:hover {
            opacity: 0.9;
        }

        .mui-panel {
            margin-bottom: 25px;
            border-radius: var(--border-radius);
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
            padding: 20px;
            background-color: #fff;
        }

        .status-message {
            margin-top: 15px;
            padding: 10px 15px;
            border-radius: 5px;
            font-weight: bold;
            text-align: center;
        }

        .status-message.success {
            background-color: #e8f5e9;
            color: var(--success-color);
            border: 1px solid #c8e6c9;
        }

        .status-message.error {
            background-color: #ffebee;
            color: var(--error-color);
            border: 1px solid #ffcdd2;
        }

        .status-message.info {
            background-color: #e3f2fd;
            color: #2196f3;
            border: 1px solid #bbdefb;
        }

        /* QR Code */
        #qrcode {
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 10px;
            margin-top: 20px;
            border: 1px dashed #ccc;
            border-radius: 5px;
            min-height: 150px;
            box-sizing: border-box;
        }

        #qrcode img {
            max-width: 100%;
            height: auto;
        }

        /* Responsive adjustments */
        @media (max-width: 600px) {
            .container {
                margin: 15px;
                padding: 15px;
            }
        }

        /* Dark Mode Toggle (for future expansion) */
        body.dark-mode {
            background-color: var(--dark-bg);
            color: #f5f5f5;
        }

        body.dark-mode .container,
        body.dark-mode .mui-panel {
            background-color: var(--dark-container);
            color: #f5f5f5;
        }

        body.dark-mode .mui-textfield input,
        body.dark-mode .mui-textfield textarea {
            color: #f5f5f5;
            background-color: #333;
            border-color: #555;
        }

        body.dark-mode .mui-textfield label {
            color: #bbb;
        }
    </style>
</head>

<body>
    <div class="mui-appbar">
        <div class="mui-container">
            <table width="100%">
                <tr style="vertical-align: middle;">
                    <td class="mui--appbar-height">
                        <a href="index.html" style="color: white; text-decoration: none; font-size: 24px; font-weight: bold;">
                            FIAT
                        </a>
                    </td>
                </tr>
            </table>
        </div>
    </div>
    <div class="mui-container">
        <!-- Authentication Section -->
        <div class="mui-panel">
            <h2>๐Ÿ”‘ Authenticate</h2>
            <div class="mui-textfield">
                <input type="password" id="api-key" placeholder="Enter API Key">
                <label for="api-key">API Key</label>
            </div>
            <button class="mui-btn mui-btn-primary" onclick="authenticate()">Authenticate</button>
            <div id="auth-status" class="status-message"></div>
        </div>

        <!-- Device Information Section -->
        <div class="mui-panel">
            <h2>๐Ÿ“Š

Telematics Map for Asset Visualization (HTML/JavaScript)

This HTML/JavaScript code demonstrates a solution for visualizing asset locations on a map using the Leaflet.js library and integrating with an API to fetch telematics data. The problem solved here is presenting complex geographical data in an intuitive and interactive way, allowing users to quickly grasp the distribution and status of assets. The dynamic loading of data and the mapping functionality are key features.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Telematics Map</title>
    <!-- Leaflet CSS -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
    <style>
        body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
        #map { height: 80vh; width: 100%; }
        #controls {
            padding: 10px;
            background: white;
            position: absolute;
            top: 10px;
            right: 10px;
            z-index: 1000;
            border-radius: 5px;
            box-shadow: 0 0 15px rgba(0,0,0,0.2);
            display: flex;
            gap: 10px;
            align-items: center;
        }
        select, button {
            padding: 8px;
            border-radius: 4px;
            border: 1px solid #ccc;
            font-size: 14px;
        }
        button {
            background-color: #007bff;
            color: white;
            cursor: pointer;
            border: none;
        }
        button:hover {
            background-color: #0056b3;
        }
        .loading-spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            width: 20px;
            height: 20px;
            animation: spin 2s linear infinite;
            display: none; /* Hidden by default */
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .marker-popup {
            font-family: Arial, sans-serif;
            font-size: 14px;
        }
        .marker-popup h4 {
            margin: 0 0 5px 0;
            color: #007bff;
        }
        .marker-popup p {
            margin: 0 0 3px 0;
        }
        .marker-popup strong {
            color: #333;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <div id="controls">
        <label for="unit-select">Select Unit:</label>
        <select id="unit-select">
            <option value="">All Units</option>
            <!-- Options will be loaded dynamically -->
        </select>
        <button onclick="fetchTelematicsData()">Refresh Map</button>
        <div id="loading-spinner" class="loading-spinner"></div>
    </div>

    <!-- Leaflet JavaScript -->
    <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
    <script>
        let map;
        let markers = L.featureGroup();
        let allUnitsData = []; // To store all fetched data

        // Initialize map
        function initMap() {
            map = L.map('map').setView([39.8283, -98.5795], 4); // Centered on USA

            L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
            }).addTo(map);

            markers.addTo(map);
        }

        // Fetch telematics data from API
        async function fetchTelematicsData() {
            const spinner = document.getElementById('loading-spinner');
            spinner.style.display = 'block'; // Show spinner

            const unitSelect = document.getElementById('unit-select');
            const selectedUnit = unitSelect.value;

            try {
                // Replace with your actual API endpoint and API key handling
                const response = await fetch('YOUR_TELEMATICS_API_ENDPOINT', {
                    headers: {
                        'Authorization': 'Bearer YOUR_API_KEY' // Or whatever your auth method is
                    }
                });
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const data = await response.json();
                allUnitsData = data; // Store all data

                populateUnitDropdown(data);
                updateMap(selectedUnit ? data.filter(d => d.unitId === selectedUnit) : data);

            } catch (error) {
                console.error("Error fetching telematics data:", error);
                alert("Failed to load telematics data. Please check the console for details.");
            } finally {
                spinner.style.display = 'none'; // Hide spinner
            }
        }

        // Populate the unit dropdown
        function populateUnitDropdown(data) {
            const unitSelect = document.getElementById('unit-select');
            const currentSelectedValue = unitSelect.value; // Preserve current selection
            unitSelect.innerHTML = '<option value="">All Units</option>'; // Clear existing options

            const uniqueUnits = [...new Set(data.map(d => d.unitId))].sort();
            uniqueUnits.forEach(unitId => {
                const option = document.createElement('option');
                option.value = unitId;
                option.textContent = unitId;
                unitSelect.appendChild(option);
            });

            // Restore previous selection or default to "All Units"
            if (uniqueUnits.includes(currentSelectedValue)) {
                unitSelect.value = currentSelectedValue;
            } else {
                unitSelect.value = "";
            }

            unitSelect.onchange = () => {
                const newSelectedUnit = unitSelect.value;
                updateMap(newSelectedUnit ? allUnitsData.filter(d => d.unitId === newSelectedUnit) : allUnitsData);
            };
        }

        // Update map markers
        function updateMap(data) {
            markers.clearLayers(); // Clear existing markers

            if (data.length === 0) {
                console.log("No data to display on map.");
                return;
            }

            data.forEach(item => {
                if (item.latitude && item.longitude) {
                    const popupContent = `
                        <div class="marker-popup">
                            <h4>Unit ID: ${item.unitId}</h4>
                            <p><strong>Location:</strong> ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}</p>
                            <p><strong>Speed:</strong> ${item.speed || 'N/A'} mph</p>
                            <p><strong>Last Update:</strong> ${new Date(item.timestamp).toLocaleString()}</p>
                            <p><strong>Status:</strong> ${item.status || 'N/A'}</p>
                        </div>
                    `;
                    const marker = L.marker([item.latitude, item.longitude])
                                    .bindPopup(popupContent);
                    markers.addLayer(marker);
                }
            });

            // Fit map to markers if there are any
            if (markers.getLayers().length > 0) {
                map.fitBounds(markers.getBounds());
            } else {
                // If no markers, reset view to USA
                map.setView([39.8283, -98.5795], 4);
            }
        }

        // Initialize map and fetch data on load
        document.addEventListener('DOMContentLoaded', () => {
            initMap();
            fetchTelematicsData(); // Initial data fetch
            // Set interval for refreshing data, e.g., every 5 minutes (300000 ms)
            // setInterval(fetchTelematicsData, 300000); 
        });
    </script>
</body>
</html>