(feat): Implemented CSV loading/export, and PDF export using reportlab.

Added work description entry, total hours calculation/display.
Implemented CSV loading/export, and PDF export using reportlab.
This commit is contained in:
Blake Ridgway 2025-03-20 19:37:42 -05:00
parent f657bb0cd5
commit cc53fe9c41

View file

@ -1,49 +1,69 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import messagebox
from datetime import datetime from datetime import datetime, timedelta
import csv import csv
import os import os
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle
from reportlab.lib import colors
from reportlab.lib.units import inch
class TimeLogix(tk.Tk): class TimeLogix(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("Time Logix") self.title("TimeLogix")
self.geometry("450x350") self.geometry("500x500")
self.start_time = None self.start_time = None
self.tracking = False self.tracking = False
self.sessions = [] self.sessions = []
self.total_hours = 0
self.create_widgets() self.create_widgets()
self.load_sessions_from_csv()
self.update_total_hours()
def create_widgets(self): def create_widgets(self):
self.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12)) self.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12))
self.status_label.pack(pady=10) self.status_label.pack(pady=5)
self.start_button = tk.Button( self.start_button = tk.Button(
self, text="Start Tracking", width=20, command=self.start_tracking self, text="Start Tracking", width=20, command=self.start_tracking
) )
self.start_button.pack(pady=5) self.start_button.pack(pady=2)
self.stop_button = tk.Button( self.stop_button = tk.Button(
self, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED self, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED
) )
self.stop_button.pack(pady=5) self.stop_button.pack(pady=2)
self.export_button = tk.Button( self.description_label = tk.Label(self, text="Work Description:")
self, text="Export Sessions", width=20, command=self.export_sessions self.description_label.pack(pady=1)
self.description_entry = tk.Text(self, height=3, width=40)
self.description_entry.pack(pady=1)
self.export_csv_button = tk.Button(
self, text="Export to CSV", width=20, command=self.export_sessions_csv
) )
self.export_button.pack(pady=5) self.export_csv_button.pack(pady=2)
self.export_pdf_button = tk.Button(
self, text="Export to PDF", width=20, command=self.export_sessions_pdf
)
self.export_pdf_button.pack(pady=2)
self.total_hours_label = tk.Label(self, text="Total Hours Worked: 0.00", font=("Helvetica", 12))
self.total_hours_label.pack(pady=5)
self.log_text = tk.Text(self, height=10, state=tk.DISABLED) self.log_text = tk.Text(self, height=10, state=tk.DISABLED)
self.log_text.pack(pady=10, padx=10, fill='both', expand=True) self.log_text.pack(pady=5, padx=10, fill='both', expand=True)
def log_message(self, message): def log_message(self, message):
"""Append message to the log text widget.""" if hasattr(self, 'log_text'):
self.log_text.configure(state=tk.NORMAL) self.log_text.configure(state=tk.NORMAL)
self.log_text.insert(tk.END, f"{message}\n") self.log_text.insert(tk.END, f"{message}\n")
self.log_text.configure(state=tk.DISABLED) self.log_text.configure(state=tk.DISABLED)
self.log_text.see(tk.END) self.log_text.see(tk.END)
def start_tracking(self): def start_tracking(self):
if self.tracking: if self.tracking:
@ -66,19 +86,22 @@ class TimeLogix(tk.Tk):
end_time = datetime.now() end_time = datetime.now()
duration = end_time - self.start_time duration = end_time - self.start_time
self.sessions.append((self.start_time, end_time, duration)) description = self.description_entry.get("1.0", tk.END).strip()
self.sessions.append((self.start_time, end_time, duration, description))
self.tracking = False self.tracking = False
self.status_label.config(text="Status: Not Tracking") self.status_label.config(text="Status: Not Tracking")
decimal_hours = self.get_decimal_hours(duration) decimal_hours = self.get_decimal_hours(duration)
self.log_message( self.log_message(
f"Stopped at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}, Duration: {self.format_duration(duration)} " f"Stopped at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}, Duration: {self.format_duration(duration)} "
f"({decimal_hours:.2f} hours)" f"({decimal_hours:.2f} hours), Description: {description}"
) )
self.start_button.config(state=tk.NORMAL) self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.DISABLED)
self.description_entry.delete("1.0", tk.END)
self.update_total_hours()
def format_duration(self, duration): def format_duration(self, duration):
"""Format duration as H:MM:SS"""
total_seconds = int(duration.total_seconds()) total_seconds = int(duration.total_seconds())
hours = total_seconds // 3600 hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60 minutes = (total_seconds % 3600) // 60
@ -86,38 +109,123 @@ class TimeLogix(tk.Tk):
return f"{hours}:{minutes:02d}:{seconds:02d}" return f"{hours}:{minutes:02d}:{seconds:02d}"
def get_decimal_hours(self, duration): def get_decimal_hours(self, duration):
"""Convert a timedelta duration to decimal hours."""
total_hours = duration.total_seconds() / 3600 total_hours = duration.total_seconds() / 3600
return total_hours return total_hours
def export_sessions(self): def export_sessions_csv(self):
if not self.sessions: if not self.sessions:
messagebox.showinfo("Info", "No sessions to export.") messagebox.showinfo("Info", "No sessions to export.")
return return
filename = "working_sessions.csv" filename = "working_sessions.csv"
try: try:
file_exists = os.path.isfile(filename) with open(filename, mode="w", newline="") as csvfile:
with open(filename, mode="a", newline="") as csvfile:
writer = csv.writer(csvfile) writer = csv.writer(csvfile)
if not file_exists: writer.writerow([
writer.writerow([ "Start Time", "End Time",
"Start Time", "End Time", "Duration (H:MM:SS)", "Decimal Hours", "Description"
"Duration (H:MM:SS)", "Decimal Hours" ])
]) for start, end, duration, description in self.sessions:
for session in self.sessions:
start, end, duration = session
writer.writerow([ writer.writerow([
start.strftime("%Y-%m-%d %H:%M:%S"), start.strftime("%Y-%m-%d %H:%M:%S"),
end.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S"),
self.format_duration(duration), self.format_duration(duration),
f"{self.get_decimal_hours(duration):.2f}" f"{self.get_decimal_hours(duration):.2f}",
description
]) ])
writer.writerow(["", "", "", "Total Hours", f"{self.total_hours:.2f}"])
messagebox.showinfo("Export Successful", f"Sessions exported to {filename}") messagebox.showinfo("Export Successful", f"Sessions exported to {filename}")
self.sessions.clear()
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Failed to export: {e}") messagebox.showerror("Error", f"Failed to export: {e}")
def export_sessions_pdf(self):
if not self.sessions:
messagebox.showinfo("Info", "No sessions to export.")
return
filename = "working_sessions.pdf"
try:
doc = SimpleDocTemplate(filename, pagesize=letter)
elements = []
data = [
["Start Time", "End Time", "Duration (H:MM:SS)", "Decimal Hours", "Description"]
]
for start, end, duration, description in self.sessions:
data.append([
start.strftime("%Y-%m-%d %H:%M:%S"),
end.strftime("%Y-%m-%d %H:%M:%S"),
self.format_duration(duration),
f"{self.get_decimal_hours(duration):.2f}",
description
])
data.append(["", "", "", "Total Hours", f"{self.total_hours:.2f}"])
table = Table(data)
style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black)
])
table.setStyle(style)
elements.append(table)
doc.build(elements)
messagebox.showinfo("Export Successful", f"Sessions exported to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to export: {e}")
def load_sessions_from_csv(self):
filename = "working_sessions.csv"
if os.path.exists(filename):
try:
with open(filename, mode="r") as csvfile:
reader = csv.reader(csvfile)
header = next(reader, None)
for row in reader:
if len(row) == 5:
start_time_str, end_time_str, duration_str, decimal_hours_str, description = row
elif len(row) == 4:
start_time_str, end_time_str, duration_str, decimal_hours_str = row
description = ""
else:
print(f"Skipping row with unexpected number of columns: {len(row)}")
continue
try:
start_time = datetime.strptime(start_time_str, "%Y-%m-%d %H:%M:%S")
end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S")
duration = self.parse_duration(duration_str)
self.sessions.append((start_time, end_time, duration, description))
self.log_message(
f"Loaded: {start_time.strftime('%Y-%m-%d %H:%M:%S')} - {end_time.strftime('%Y-%m-%d %H:%M:%S')}, "
f"Duration: {duration_str}, Description: {description}"
)
except ValueError as ve:
print(f"Skipping row due to parsing error: {ve}")
except FileNotFoundError:
messagebox.showinfo("Info", "No CSV file found. Starting with a new session.")
except Exception as e:
messagebox.showerror("Error", f"Failed to load sessions from CSV: {e}")
def parse_duration(self, duration_str):
hours, minutes, seconds = map(int, duration_str.split(':'))
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
def update_total_hours(self):
total_duration = timedelta()
for start, end, duration, description in self.sessions:
total_duration += duration
self.total_hours = total_duration.total_seconds() / 3600
self.total_hours_label.config(text=f"Total Hours Worked: {self.total_hours:.2f}")
if __name__ == "__main__": if __name__ == "__main__":
app = TimeLogix() app = TimeLogix()
app.mainloop() app.mainloop()