Spaces:
Running
Running
| # ========================================== | |
| # app.py β FieldTech Pro | Streamlit App | |
| # ========================================== | |
| # Author: OpenAI GPT-5 | |
| # Date: 2026-05-09 | |
| # ========================================== | |
| import streamlit as st | |
| import pandas as pd | |
| import plotly.express as px | |
| import altair as alt | |
| from datetime import datetime, timedelta | |
| import os, json, uuid, time | |
| from io import BytesIO | |
| from PIL import Image | |
| import base64 | |
| from weasyprint import HTML | |
| # ========================================== | |
| # INITIAL SETUP & FOLDERS | |
| # ========================================== | |
| APP_NAME = "FieldTech Pro β International Service Technician Tracker" | |
| BASE_DIR = os.path.join(os.getcwd(), "FieldTechPro_data") | |
| os.makedirs(BASE_DIR, exist_ok=True) | |
| for sub in ["projects/active", "projects/finished", "media"]: | |
| os.makedirs(os.path.join(BASE_DIR, sub), exist_ok=True) | |
| # ========================================== | |
| # PAGE CONFIG & STYLES | |
| # ========================================== | |
| st.set_page_config(page_title=APP_NAME, layout="wide", page_icon="π§°") | |
| st.markdown( | |
| """ | |
| <style> | |
| body {font-family: 'Inter', sans-serif; background-color: #f8f9fb;} | |
| .big-button button {font-size:18px !important; padding:15px 25px !important;} | |
| .card { | |
| background-color: white; | |
| border-radius: 8px; | |
| padding: 20px; | |
| box-shadow: 0 2px 6px rgba(0,0,0,0.1); | |
| text-align: center; | |
| } | |
| .metric-title {font-weight: 600; font-size: 18px;} | |
| .metric-value {font-size: 22px; color: #0d6efd;} | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| # ========================================== | |
| # HELPERS | |
| # ========================================== | |
| def gen_project_id() -> str: | |
| return f"FT-{datetime.now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:4]}" | |
| def get_active_projects(): | |
| path = os.path.join(BASE_DIR, "projects/active") | |
| files = [f for f in os.listdir(path) if f.endswith(".json")] | |
| return [os.path.splitext(f)[0] for f in files] | |
| def load_project(project_id): | |
| pj = os.path.join(BASE_DIR, "projects/active", f"{project_id}.json") | |
| if not os.path.exists(pj): | |
| pj = os.path.join(BASE_DIR, "projects/finished", f"{project_id}.json") | |
| if not os.path.exists(pj): | |
| return None | |
| with open(pj, "r") as f: | |
| return json.load(f) | |
| def save_project(project): | |
| pid = project["project_id"] | |
| folder = "active" if project.get("status") != "completed" else "finished" | |
| path_json = os.path.join(BASE_DIR, f"projects/{folder}/{pid}.json") | |
| with open(path_json, "w") as f: | |
| json.dump(project, f, indent=4) | |
| def move_to_finished(project): | |
| project["status"] = "completed" | |
| save_project(project) | |
| active_path = os.path.join(BASE_DIR, "projects/active", f"{project['project_id']}.json") | |
| finished_path = os.path.join(BASE_DIR, "projects/finished", f"{project['project_id']}.json") | |
| if os.path.exists(active_path): | |
| os.rename(active_path, finished_path) | |
| def ensure_media_folder(project_id): | |
| path = os.path.join(BASE_DIR, "media", project_id) | |
| os.makedirs(path, exist_ok=True) | |
| return path | |
| def add_expense(project, category, amount, currency="USD"): | |
| exp = project.get("expenses", []) | |
| exp.append({"date": datetime.now().isoformat(), "category": category, "amount": float(amount), "currency": currency}) | |
| project["expenses"] = exp | |
| def fake_ocr_receipt(image): | |
| # Placeholder for Hugging Face OCR | |
| # In production, call Donut or TrOCR model here. | |
| return pd.DataFrame([ | |
| {"Date": datetime.now().strftime("%Y-%m-%d"), "Merchant": "ACME Tools", "Total": 245.50, "Currency": "USD", "Item": "Replacement Kit"} | |
| ]) | |
| def fake_video_transcribe(video_bytes): | |
| # Placeholder transcription (replace with actual model call) | |
| return "Technician performed diagnostics, replaced fuse, verified operation, and closed service ticket." | |
| # ========================================== | |
| # SIDEBAR NAVIGATION | |
| # ========================================== | |
| menu = st.sidebar.radio( | |
| "Navigation", | |
| ["π Home","β New Project","π Existing Projects","π Prospect","π Documentation","π Reports"] | |
| ) | |
| # ========================================== | |
| # HOME | |
| # ========================================== | |
| if menu == "π Home": | |
| st.title("π Dashboard") | |
| col1, col2, col3, col4 = st.columns(4) | |
| col1.markdown(f"<div class='card'><div class='metric-title'>Active Trips</div><div class='metric-value'>{len(get_active_projects())}</div></div>", unsafe_allow_html=True) | |
| col2.markdown("<div class='card'><div class='metric-title'>Billable Hours (Week)</div><div class='metric-value'>42.5</div></div>", unsafe_allow_html=True) | |
| col3.markdown("<div class='card'><div class='metric-title'>Expenses Pending</div><div class='metric-value'>$560</div></div>", unsafe_allow_html=True) | |
| col4.markdown("<div class='card'><div class='metric-title'>Completed Projects</div><div class='metric-value'>12</div></div>", unsafe_allow_html=True) | |
| # Charts | |
| st.subheader("Hours Trend") | |
| df_hours = pd.DataFrame({ | |
| "Day": [d.strftime("%a") for d in [datetime.now() - timedelta(days=i) for i in range(6,-1,-1)]], | |
| "Hours": [6,7,8,9,7,5,6] | |
| }) | |
| fig = px.line(df_hours, x="Day", y="Hours", markers=True) | |
| st.plotly_chart(fig, use_container_width=True) | |
| st.subheader("Expense Breakdown") | |
| exp_df = pd.DataFrame({"Category":["Travel","Meals","Tools","Hotels"],"Amount":[300,120,250,400]}) | |
| chart = alt.Chart(exp_df).mark_arc(innerRadius=50).encode(theta="Amount", color="Category") | |
| st.altair_chart(chart, use_container_width=True) | |
| st.markdown("<div class='big-button'>", unsafe_allow_html=True) | |
| st.button("β Start New Project") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # ========================================== | |
| # NEW PROJECT | |
| # ========================================== | |
| elif menu == "β New Project": | |
| st.title("β New Project") | |
| if "current_project" not in st.session_state: | |
| st.session_state.current_project = {"project_id": gen_project_id(), "status": "active"} | |
| proj = st.session_state.current_project | |
| ensure_media_folder(proj["project_id"]) | |
| st.subheader("Project Header") | |
| c1, c2, c3, c4 = st.columns(4) | |
| proj["client"] = c1.text_input("Client Name", proj.get("client","")) | |
| proj["location"] = c2.text_input("Site/Location", proj.get("location","")) | |
| proj["country"] = c3.text_input("Country", proj.get("country","")) | |
| proj["technician"] = c4.text_input("Technician Name", proj.get("technician","")) | |
| proj["start_date"] = st.date_input("Start Date", proj.get("start_date", datetime.now().date())) | |
| proj["end_date"] = st.date_input("End Date", proj.get("end_date", datetime.now().date())) | |
| st.markdown("---") | |
| st.subheader("Trip Log & Travel") | |
| colA, colB = st.columns(2) | |
| proj["travel_type"] = colA.selectbox("Travel Type", ["Road","Air","Train","Mixed"], index=0) | |
| proj["distance_km"] = colB.number_input("Distance (km)", value=float(proj.get("distance_km",0.0))) | |
| proj["distance_miles"] = round(proj["distance_km"] * 0.621, 2) | |
| st.caption(f"β {proj['distance_miles']} miles") | |
| with st.expander("Labor Hours"): | |
| proj["task_category"] = st.selectbox("Task Category", ["Diagnostic","Repair","Testing","Training","Waiting"]) | |
| proj["hours_worked"] = st.number_input("Hours Worked", value=float(proj.get("hours_worked",0.0))) | |
| if proj["hours_worked"] > 8: | |
| st.warning("Overtime detected!") | |
| with st.expander("Hotel & Accommodation"): | |
| proj["hotel_rate"] = st.number_input("Nightly Rate (USD)", value=float(proj.get("hotel_rate",0.0))) | |
| proj["nights"] = st.number_input("Nights", value=int(proj.get("nights",0))) | |
| proj["hotel_total"] = proj["hotel_rate"] * proj["nights"] | |
| st.caption(f"Total: ${proj['hotel_total']:.2f}") | |
| with st.expander("Expenses"): | |
| cat = st.selectbox("Category", ["Travel","Meal","Tools","Other"]) | |
| amt = st.number_input("Amount", min_value=0.0) | |
| cur = st.text_input("Currency", "USD") | |
| if st.button("Add Expense"): | |
| add_expense(proj, cat, amt, cur) | |
| save_project(proj) | |
| if proj.get("expenses"): | |
| st.table(pd.DataFrame(proj["expenses"])) | |
| with st.expander("Media Capture"): | |
| img = st.camera_input("Take Photo") | |
| if img: | |
| img_path = os.path.join(ensure_media_folder(proj["project_id"]), f"photo_{int(time.time())}.jpg") | |
| Image.open(img).save(img_path) | |
| st.success("Photo saved.") | |
| vid = st.file_uploader("Upload Video", type=["mp4","mov"]) | |
| if vid: | |
| vid_path = os.path.join(ensure_media_folder(proj["project_id"]), vid.name) | |
| open(vid_path,"wb").write(vid.read()) | |
| st.success("Video uploaded.") | |
| proj["notes"] = st.text_area("Notes / Voice-to-text field") | |
| # Auto-Save | |
| if int(time.time()) % 30 == 0: | |
| save_project(proj) | |
| st.markdown("---") | |
| col_end1, col_end2 = st.columns(2) | |
| if col_end1.button("πΎ Save & Continue Later"): | |
| save_project(proj) | |
| st.success("Project saved.") | |
| if col_end2.button("β Mark Project Complete"): | |
| move_to_finished(proj) | |
| st.success("Project marked complete and moved to finished folder.") | |
| # ========================================== | |
| # EXISTING PROJECTS | |
| # ========================================== | |
| elif menu == "π Existing Projects": | |
| st.title("π Existing Projects") | |
| active = get_active_projects() | |
| if active: | |
| sel = st.selectbox("Select Project", active) | |
| data = load_project(sel) | |
| st.json(data) | |
| else: | |
| st.info("No active projects found.") | |
| # ========================================== | |
| # PROSPECT | |
| # ========================================== | |
| elif menu == "π Prospect": | |
| st.title("π Prospect Capture") | |
| new_client = st.text_input("Prospective Client") | |
| loc = st.text_input("Location") | |
| service = st.text_area("Service Summary") | |
| if st.button("Save Prospect"): | |
| dfp = pd.DataFrame([{"Client":new_client, "Location":loc, "Service":service, "Date":datetime.now().isoformat()}]) | |
| dfp.to_csv(os.path.join(BASE_DIR,"prospects.csv"), mode="a", header=not os.path.exists(os.path.join(BASE_DIR,"prospects.csv")), index=False) | |
| st.success("Prospect saved.") | |
| # ========================================== | |
| # DOCUMENTATION (OCR + TRANSCRIPTION) | |
| # ========================================== | |
| elif menu == "π Documentation": | |
| st.title("π Documentation & Media") | |
| pid_list = get_active_projects() + [f for f in os.listdir(os.path.join(BASE_DIR,"projects/finished")) if f.endswith(".json")] | |
| pid_list = [os.path.splitext(f)[0] for f in pid_list] | |
| sel_project = st.selectbox("Select Project", pid_list) | |
| if sel_project: | |
| path_media = ensure_media_folder(sel_project) | |
| st.subheader("Media Gallery") | |
| imgs = [f for f in os.listdir(path_media) if f.lower().endswith(".jpg")] | |
| for img_path in imgs: | |
| st.image(os.path.join(path_media,img_path), width=250) | |
| st.subheader("Receipt OCR") | |
| receipt = st.file_uploader("Upload Receipt Image", type=["jpg","png"]) | |
| if receipt: | |
| st.image(receipt) | |
| data = fake_ocr_receipt(receipt) | |
| st.table(data) | |
| if st.button("Add Extracted Data to Expenses"): | |
| proj = load_project(sel_project) | |
| for _, r in data.iterrows(): | |
| add_expense(proj, "Receipt", r["Total"], r["Currency"]) | |
| save_project(proj) | |
| st.success("Extracted data added.") | |
| st.subheader("Video Transcription") | |
| video = st.file_uploader("Upload Video for Transcription", type=["mp4","mov"]) | |
| if video: | |
| text = fake_video_transcribe(video.read()) | |
| st.text_area("Transcribed Text", text) | |
| # ========================================== | |
| # REPORTS | |
| # ========================================== | |
| elif menu == "π Reports": | |
| st.title("π Reports") | |
| fins = [f for f in os.listdir(os.path.join(BASE_DIR,"projects/finished")) if f.endswith(".json")] | |
| if not fins: | |
| st.info("No completed projects yet.") | |
| else: | |
| sel = st.selectbox("Select Finished Project", [os.path.splitext(f)[0] for f in fins]) | |
| proj = load_project(sel) | |
| st.json(proj) | |
| if st.button("π Generate Full Service Report"): | |
| # create HTML report | |
| html = f""" | |
| <h1>Service Report</h1> | |
| <h3>{proj.get('client','')}</h3> | |
| <p>Project ID: {proj['project_id']} | {proj.get('location','')}</p> | |
| <h4>Summary</h4> | |
| <ul> | |
| <li>Technician: {proj.get('technician','')}</li> | |
| <li>Start: {proj.get('start_date')}</li> | |
| <li>End: {proj.get('end_date')}</li> | |
| </ul> | |
| <h4>Expenses</h4> | |
| {pd.DataFrame(proj.get('expenses',[])).to_html(index=False)} | |
| <h4>Notes</h4> | |
| <p>{proj.get('notes','')}</p> | |
| """ | |
| pdf_bytes = HTML(string=html).write_pdf() | |
| st.download_button("Download Report PDF", pdf_bytes, file_name=f"{proj['project_id']}_report.pdf") | |