AddisonSwan commited on
Commit
0b1c25a
Β·
verified Β·
1 Parent(s): 5e4713e

Upload data_app.py

Browse files
Files changed (1) hide show
  1. data_app.py +316 -85
data_app.py CHANGED
@@ -1,87 +1,318 @@
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import pandas as pd
3
- from datetime import datetime
4
- import os
5
- from huggingface_hub import HfApi
6
-
7
- st.set_page_config(page_title="πŸ”§ Service Tech Tracker", layout="wide")
8
- st.title("πŸ”§ Service Tech Fuel & Job Tracker")
9
-
10
- # ====================== HF TOKEN (Safe Handling) ======================
11
- HF_TOKEN = st.sidebar.text_input("πŸ”‘ Hugging Face Token (optional for backup)", type="password")
12
- HF_REPO = "AddisonSwan/service-tech-tracker"
13
-
14
- def backup_to_hf():
15
- if not HF_TOKEN:
16
- return
17
- try:
18
- if os.path.exists("data/mileage.csv"):
19
- api = HfApi(token=HF_TOKEN)
20
- api.upload_file(
21
- path_or_fileobj="data/mileage.csv",
22
- path_in_repo="mileage.csv",
23
- repo_id=HF_REPO,
24
- repo_type="dataset"
25
- )
26
- st.sidebar.success("βœ… Backed up to Hugging Face")
27
- except:
28
- st.sidebar.warning("Backup failed")
29
-
30
- # Create folders
31
- os.makedirs("data", exist_ok=True)
32
- os.makedirs("jobs", exist_ok=True)
33
-
34
- page = st.sidebar.radio("Go to", ["Gas & Mileage", "New Job", "All Data"])
35
-
36
- if page == "Gas & Mileage":
37
- st.subheader("β›½ Gas & Mileage Tracker")
38
-
39
- col1, col2 = st.columns(2)
40
- with col1:
41
- country = st.selectbox("Country", ["USA", "Canada", "Other"])
42
- region = st.text_input("State / Province / Region")
43
-
44
- price = st.number_input("Gas Price", value=4.50, step=0.01)
45
- miles = st.number_input("Miles Driven", value=0.0)
46
- trip_type = st.radio("Type", ["Work", "Personal"])
47
- notes = st.text_area("Notes")
48
-
49
- if st.button("πŸ’Ύ Save Entry"):
50
- new_row = pd.DataFrame([{
51
- "Date": datetime.now().strftime("%Y-%m-%d %H:%M"),
52
- "Country": country,
53
- "Region": region,
54
- "Price": price,
55
- "Miles": miles,
56
- "Type": trip_type,
57
- "Notes": notes
58
- }])
59
-
60
- try:
61
- df = pd.read_csv("data/mileage.csv")
62
- df = pd.concat([df, new_row], ignore_index=True)
63
- except:
64
- df = new_row
65
-
66
- df.to_csv("data/mileage.csv", index=False)
67
- backup_to_hf()
68
- st.success("βœ… Saved!")
69
-
70
- if os.path.exists("data/mileage.csv"):
71
- st.dataframe(pd.read_csv("data/mileage.csv"))
72
-
73
- elif page == "New Job":
74
- st.subheader("πŸ“ New Job")
75
- job_id = st.text_input("Job ID")
76
- client = st.text_input("Client")
77
- desc = st.text_area("Description")
78
- if st.button("Create Job"):
79
- os.makedirs(f"jobs/{job_id}", exist_ok=True)
80
- st.success(f"βœ… Job {job_id} created!")
81
-
82
- elif page == "All Data":
83
- st.subheader("All Records")
84
- if os.path.exists("data/mileage.csv"):
85
- st.dataframe(pd.read_csv("data/mileage.csv"))
86
-
87
- st.caption("Service Tech Tracker β€’ Local + Optional HF Backup")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==========================================
2
+ # app.py β€” FieldTech Pro | Streamlit App
3
+ # ==========================================
4
+ # Author: OpenAI GPT-5
5
+ # Date: 2026-05-09
6
+ # ==========================================
7
+
8
  import streamlit as st
9
  import pandas as pd
10
+ import plotly.express as px
11
+ import altair as alt
12
+ from datetime import datetime, timedelta
13
+ import os, json, uuid, time
14
+ from io import BytesIO
15
+ from PIL import Image
16
+ import base64
17
+ from weasyprint import HTML
18
+
19
+ # ==========================================
20
+ # INITIAL SETUP & FOLDERS
21
+ # ==========================================
22
+ APP_NAME = "FieldTech Pro – International Service Technician Tracker"
23
+ BASE_DIR = os.path.join(os.getcwd(), "FieldTechPro_data")
24
+
25
+ os.makedirs(BASE_DIR, exist_ok=True)
26
+ for sub in ["projects/active", "projects/finished", "media"]:
27
+ os.makedirs(os.path.join(BASE_DIR, sub), exist_ok=True)
28
+
29
+ # ==========================================
30
+ # PAGE CONFIG & STYLES
31
+ # ==========================================
32
+ st.set_page_config(page_title=APP_NAME, layout="wide", page_icon="🧰")
33
+
34
+ st.markdown(
35
+ """
36
+ <style>
37
+ body {font-family: 'Inter', sans-serif; background-color: #f8f9fb;}
38
+ .big-button button {font-size:18px !important; padding:15px 25px !important;}
39
+ .card {
40
+ background-color: white;
41
+ border-radius: 8px;
42
+ padding: 20px;
43
+ box-shadow: 0 2px 6px rgba(0,0,0,0.1);
44
+ text-align: center;
45
+ }
46
+ .metric-title {font-weight: 600; font-size: 18px;}
47
+ .metric-value {font-size: 22px; color: #0d6efd;}
48
+ </style>
49
+ """,
50
+ unsafe_allow_html=True,
51
+ )
52
+
53
+ # ==========================================
54
+ # HELPERS
55
+ # ==========================================
56
+
57
+ def gen_project_id() -> str:
58
+ return f"FT-{datetime.now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:4]}"
59
+
60
+ def get_active_projects():
61
+ path = os.path.join(BASE_DIR, "projects/active")
62
+ files = [f for f in os.listdir(path) if f.endswith(".json")]
63
+ return [os.path.splitext(f)[0] for f in files]
64
+
65
+ def load_project(project_id):
66
+ pj = os.path.join(BASE_DIR, "projects/active", f"{project_id}.json")
67
+ if not os.path.exists(pj):
68
+ pj = os.path.join(BASE_DIR, "projects/finished", f"{project_id}.json")
69
+ if not os.path.exists(pj):
70
+ return None
71
+ with open(pj, "r") as f:
72
+ return json.load(f)
73
+
74
+ def save_project(project):
75
+ pid = project["project_id"]
76
+ folder = "active" if project.get("status") != "completed" else "finished"
77
+ path_json = os.path.join(BASE_DIR, f"projects/{folder}/{pid}.json")
78
+ with open(path_json, "w") as f:
79
+ json.dump(project, f, indent=4)
80
+
81
+ def move_to_finished(project):
82
+ project["status"] = "completed"
83
+ save_project(project)
84
+ active_path = os.path.join(BASE_DIR, "projects/active", f"{project['project_id']}.json")
85
+ finished_path = os.path.join(BASE_DIR, "projects/finished", f"{project['project_id']}.json")
86
+ if os.path.exists(active_path):
87
+ os.rename(active_path, finished_path)
88
+
89
+ def ensure_media_folder(project_id):
90
+ path = os.path.join(BASE_DIR, "media", project_id)
91
+ os.makedirs(path, exist_ok=True)
92
+ return path
93
+
94
+ def add_expense(project, category, amount, currency="USD"):
95
+ exp = project.get("expenses", [])
96
+ exp.append({"date": datetime.now().isoformat(), "category": category, "amount": float(amount), "currency": currency})
97
+ project["expenses"] = exp
98
+
99
+ def fake_ocr_receipt(image):
100
+ # Placeholder for Hugging Face OCR
101
+ # In production, call Donut or TrOCR model here.
102
+ return pd.DataFrame([
103
+ {"Date": datetime.now().strftime("%Y-%m-%d"), "Merchant": "ACME Tools", "Total": 245.50, "Currency": "USD", "Item": "Replacement Kit"}
104
+ ])
105
+
106
+ def fake_video_transcribe(video_bytes):
107
+ # Placeholder transcription (replace with actual model call)
108
+ return "Technician performed diagnostics, replaced fuse, verified operation, and closed service ticket."
109
+
110
+ # ==========================================
111
+ # SIDEBAR NAVIGATION
112
+ # ==========================================
113
+ menu = st.sidebar.radio(
114
+ "Navigation",
115
+ ["🏠 Home","βž• New Project","πŸ“‚ Existing Projects","🌍 Prospect","πŸ“‘ Documentation","πŸ“Š Reports"]
116
+ )
117
+
118
+ # ==========================================
119
+ # HOME
120
+ # ==========================================
121
+ if menu == "🏠 Home":
122
+ st.title("🏠 Dashboard")
123
+
124
+ col1, col2, col3, col4 = st.columns(4)
125
+ 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)
126
+ 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)
127
+ col3.markdown("<div class='card'><div class='metric-title'>Expenses Pending</div><div class='metric-value'>$560</div></div>", unsafe_allow_html=True)
128
+ col4.markdown("<div class='card'><div class='metric-title'>Completed Projects</div><div class='metric-value'>12</div></div>", unsafe_allow_html=True)
129
+
130
+ # Charts
131
+ st.subheader("Hours Trend")
132
+ df_hours = pd.DataFrame({
133
+ "Day": [d.strftime("%a") for d in [datetime.now() - timedelta(days=i) for i in range(6,-1,-1)]],
134
+ "Hours": [6,7,8,9,7,5,6]
135
+ })
136
+ fig = px.line(df_hours, x="Day", y="Hours", markers=True)
137
+ st.plotly_chart(fig, use_container_width=True)
138
+
139
+ st.subheader("Expense Breakdown")
140
+ exp_df = pd.DataFrame({"Category":["Travel","Meals","Tools","Hotels"],"Amount":[300,120,250,400]})
141
+ chart = alt.Chart(exp_df).mark_arc(innerRadius=50).encode(theta="Amount", color="Category")
142
+ st.altair_chart(chart, use_container_width=True)
143
+
144
+ st.markdown("<div class='big-button'>", unsafe_allow_html=True)
145
+ st.button("βž• Start New Project")
146
+ st.markdown("</div>", unsafe_allow_html=True)
147
+
148
+ # ==========================================
149
+ # NEW PROJECT
150
+ # ==========================================
151
+ elif menu == "βž• New Project":
152
+ st.title("βž• New Project")
153
+ if "current_project" not in st.session_state:
154
+ st.session_state.current_project = {"project_id": gen_project_id(), "status": "active"}
155
+
156
+ proj = st.session_state.current_project
157
+ ensure_media_folder(proj["project_id"])
158
+
159
+ st.subheader("Project Header")
160
+ c1, c2, c3, c4 = st.columns(4)
161
+ proj["client"] = c1.text_input("Client Name", proj.get("client",""))
162
+ proj["location"] = c2.text_input("Site/Location", proj.get("location",""))
163
+ proj["country"] = c3.text_input("Country", proj.get("country",""))
164
+ proj["technician"] = c4.text_input("Technician Name", proj.get("technician",""))
165
+
166
+ proj["start_date"] = st.date_input("Start Date", proj.get("start_date", datetime.now().date()))
167
+ proj["end_date"] = st.date_input("End Date", proj.get("end_date", datetime.now().date()))
168
+
169
+ st.markdown("---")
170
+ st.subheader("Trip Log & Travel")
171
+ colA, colB = st.columns(2)
172
+ proj["travel_type"] = colA.selectbox("Travel Type", ["Road","Air","Train","Mixed"], index=0)
173
+ proj["distance_km"] = colB.number_input("Distance (km)", value=float(proj.get("distance_km",0.0)))
174
+ proj["distance_miles"] = round(proj["distance_km"] * 0.621, 2)
175
+ st.caption(f"β‰ˆ {proj['distance_miles']} miles")
176
+
177
+ with st.expander("Labor Hours"):
178
+ proj["task_category"] = st.selectbox("Task Category", ["Diagnostic","Repair","Testing","Training","Waiting"])
179
+ proj["hours_worked"] = st.number_input("Hours Worked", value=float(proj.get("hours_worked",0.0)))
180
+ if proj["hours_worked"] > 8:
181
+ st.warning("Overtime detected!")
182
+
183
+ with st.expander("Hotel & Accommodation"):
184
+ proj["hotel_rate"] = st.number_input("Nightly Rate (USD)", value=float(proj.get("hotel_rate",0.0)))
185
+ proj["nights"] = st.number_input("Nights", value=int(proj.get("nights",0)))
186
+ proj["hotel_total"] = proj["hotel_rate"] * proj["nights"]
187
+ st.caption(f"Total: ${proj['hotel_total']:.2f}")
188
+
189
+ with st.expander("Expenses"):
190
+ cat = st.selectbox("Category", ["Travel","Meal","Tools","Other"])
191
+ amt = st.number_input("Amount", min_value=0.0)
192
+ cur = st.text_input("Currency", "USD")
193
+ if st.button("Add Expense"):
194
+ add_expense(proj, cat, amt, cur)
195
+ save_project(proj)
196
+ if proj.get("expenses"):
197
+ st.table(pd.DataFrame(proj["expenses"]))
198
+
199
+ with st.expander("Media Capture"):
200
+ img = st.camera_input("Take Photo")
201
+ if img:
202
+ img_path = os.path.join(ensure_media_folder(proj["project_id"]), f"photo_{int(time.time())}.jpg")
203
+ Image.open(img).save(img_path)
204
+ st.success("Photo saved.")
205
+ vid = st.file_uploader("Upload Video", type=["mp4","mov"])
206
+ if vid:
207
+ vid_path = os.path.join(ensure_media_folder(proj["project_id"]), vid.name)
208
+ open(vid_path,"wb").write(vid.read())
209
+ st.success("Video uploaded.")
210
+ proj["notes"] = st.text_area("Notes / Voice-to-text field")
211
+
212
+ # Auto-Save
213
+ if int(time.time()) % 30 == 0:
214
+ save_project(proj)
215
+
216
+ st.markdown("---")
217
+ col_end1, col_end2 = st.columns(2)
218
+ if col_end1.button("πŸ’Ύ Save & Continue Later"):
219
+ save_project(proj)
220
+ st.success("Project saved.")
221
+ if col_end2.button("βœ… Mark Project Complete"):
222
+ move_to_finished(proj)
223
+ st.success("Project marked complete and moved to finished folder.")
224
+
225
+ # ==========================================
226
+ # EXISTING PROJECTS
227
+ # ==========================================
228
+ elif menu == "πŸ“‚ Existing Projects":
229
+ st.title("πŸ“‚ Existing Projects")
230
+ active = get_active_projects()
231
+ if active:
232
+ sel = st.selectbox("Select Project", active)
233
+ data = load_project(sel)
234
+ st.json(data)
235
+ else:
236
+ st.info("No active projects found.")
237
+
238
+ # ==========================================
239
+ # PROSPECT
240
+ # ==========================================
241
+ elif menu == "🌍 Prospect":
242
+ st.title("🌍 Prospect Capture")
243
+ new_client = st.text_input("Prospective Client")
244
+ loc = st.text_input("Location")
245
+ service = st.text_area("Service Summary")
246
+ if st.button("Save Prospect"):
247
+ dfp = pd.DataFrame([{"Client":new_client, "Location":loc, "Service":service, "Date":datetime.now().isoformat()}])
248
+ 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)
249
+ st.success("Prospect saved.")
250
+
251
+ # ==========================================
252
+ # DOCUMENTATION (OCR + TRANSCRIPTION)
253
+ # ==========================================
254
+ elif menu == "πŸ“‘ Documentation":
255
+ st.title("πŸ“‘ Documentation & Media")
256
+
257
+ pid_list = get_active_projects() + [f for f in os.listdir(os.path.join(BASE_DIR,"projects/finished")) if f.endswith(".json")]
258
+ pid_list = [os.path.splitext(f)[0] for f in pid_list]
259
+ sel_project = st.selectbox("Select Project", pid_list)
260
+ if sel_project:
261
+ path_media = ensure_media_folder(sel_project)
262
+ st.subheader("Media Gallery")
263
+ imgs = [f for f in os.listdir(path_media) if f.lower().endswith(".jpg")]
264
+ for img_path in imgs:
265
+ st.image(os.path.join(path_media,img_path), width=250)
266
+
267
+ st.subheader("Receipt OCR")
268
+ receipt = st.file_uploader("Upload Receipt Image", type=["jpg","png"])
269
+ if receipt:
270
+ st.image(receipt)
271
+ data = fake_ocr_receipt(receipt)
272
+ st.table(data)
273
+ if st.button("Add Extracted Data to Expenses"):
274
+ proj = load_project(sel_project)
275
+ for _, r in data.iterrows():
276
+ add_expense(proj, "Receipt", r["Total"], r["Currency"])
277
+ save_project(proj)
278
+ st.success("Extracted data added.")
279
+
280
+ st.subheader("Video Transcription")
281
+ video = st.file_uploader("Upload Video for Transcription", type=["mp4","mov"])
282
+ if video:
283
+ text = fake_video_transcribe(video.read())
284
+ st.text_area("Transcribed Text", text)
285
+
286
+ # ==========================================
287
+ # REPORTS
288
+ # ==========================================
289
+ elif menu == "πŸ“Š Reports":
290
+ st.title("πŸ“Š Reports")
291
+ fins = [f for f in os.listdir(os.path.join(BASE_DIR,"projects/finished")) if f.endswith(".json")]
292
+ if not fins:
293
+ st.info("No completed projects yet.")
294
+ else:
295
+ sel = st.selectbox("Select Finished Project", [os.path.splitext(f)[0] for f in fins])
296
+ proj = load_project(sel)
297
+ st.json(proj)
298
+
299
+ if st.button("πŸ“„ Generate Full Service Report"):
300
+ # create HTML report
301
+ html = f"""
302
+ <h1>Service Report</h1>
303
+ <h3>{proj.get('client','')}</h3>
304
+ <p>Project ID: {proj['project_id']} | {proj.get('location','')}</p>
305
+ <h4>Summary</h4>
306
+ <ul>
307
+ <li>Technician: {proj.get('technician','')}</li>
308
+ <li>Start: {proj.get('start_date')}</li>
309
+ <li>End: {proj.get('end_date')}</li>
310
+ </ul>
311
+ <h4>Expenses</h4>
312
+ {pd.DataFrame(proj.get('expenses',[])).to_html(index=False)}
313
+ <h4>Notes</h4>
314
+ <p>{proj.get('notes','')}</p>
315
+ """
316
+ pdf_bytes = HTML(string=html).write_pdf()
317
+ st.download_button("Download Report PDF", pdf_bytes, file_name=f"{proj['project_id']}_report.pdf")
318
+