overhaul of timelogix app

This commit is contained in:
Blake Ridgway 2025-03-31 10:02:47 -05:00
parent 266d21b540
commit 34d7c4c9dd

View file

@ -1,271 +1,401 @@
import tkinter as tk import tkinter as tk
from tkinter import messagebox from tkinter import ttk
from datetime import datetime, timedelta import time
import datetime
import csv import csv
import os
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle from reportlab.pdfgen import canvas
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.platypus import Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch from reportlab.lib.units import inch
import sys # Import the sys module
class TimeLogix(tk.Tk):
def __init__(self):
super().__init__()
self.title("TimeLogix")
self.geometry("500x500")
class TimeLogix:
def __init__(self, root):
self.root = root
self.root.title("TimeLogix Application")
# Styling
self.bg_color = "#f0f0f0" # Light gray background
self.font_family = "Segoe UI" # Modern font
self.font_size = 10
self.button_bg = "#4CAF50" # Green button background
self.button_fg = "white" # White button text
self.button_width = 15 # Consistent button width
self.root.configure(bg=self.bg_color)
style = ttk.Style()
style.configure(
"TLabel", background=self.bg_color, font=(self.font_family, self.font_size)
)
style.configure(
"TButton",
background=self.button_bg,
foreground=self.button_fg,
font=(self.font_family, self.font_size),
padding=5,
width=self.button_width, # Set button width here
)
style.configure(
"TCombobox", background="white", font=(self.font_family, self.font_size)
)
style.configure(
"TEntry", background="white", font=(self.font_family, self.font_size)
)
style.configure("My.TFrame", background=self.bg_color) # Frame style
self.is_running = False
self.start_time = None self.start_time = None
self.tracking = False self.elapsed_time = 0
self.sessions = [] self.timer_id = None
self.total_hours = 0 self.log_entries = []
self.project_file = "projects.txt"
self.projects = self.load_projects()
self.invoice_number = 1
self.company_name = "Your Company Name"
self.company_address = "123 Main St, Anytown, USA"
self.client_name = "Client Name"
self.client_address = "Client Address"
self.hourly_rate = 60.00
self.create_widgets() # UI elements
self.load_sessions_from_csv() self.task_label = ttk.Label(root, text="Task Description:")
self.update_total_hours() self.task_label.grid(row=0, column=0, sticky=tk.W, padx=10, pady=5)
self.task_entry = ttk.Entry(root, width=40)
self.task_entry.grid(row=0, column=1, sticky=tk.W, padx=10, pady=5)
def create_widgets(self): self.project_label = ttk.Label(root, text="Project:")
self.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12)) self.project_label.grid(row=1, column=0, sticky=tk.W, padx=10, pady=5)
self.status_label.pack(pady=5) self.project_combo = ttk.Combobox(root, values=self.projects)
self.project_combo.grid(row=1, column=1, sticky=tk.W, padx=10, pady=5)
if self.projects:
self.project_combo.set(self.projects[0])
self.start_button = tk.Button( self.time_label = ttk.Label(
self, text="Start Tracking", width=20, command=self.start_tracking root, text="00:00:00", font=(self.font_family, 36)
) # Larger time
self.time_label.grid(row=2, column=0, columnspan=2, pady=15)
self.start_stop_button = ttk.Button(
root, text="Start", command=self.toggle_timer
) )
self.start_button.pack(pady=2) self.start_stop_button.grid(row=3, column=0, columnspan=2, pady=10)
self.stop_button = tk.Button( self.log_label = ttk.Label(root, text="Log Entries:")
self, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED self.log_label.grid(row=4, column=0, sticky=tk.W, padx=10, pady=5)
self.log_text = tk.Text(
root,
height=10,
width=50,
bg="white",
font=(self.font_family, self.font_size),
) )
self.stop_button.pack(pady=2) self.log_text.grid(row=5, column=0, columnspan=2, padx=10, pady=5)
self.description_label = tk.Label(self, text="Work Description:") # Add Project UI
self.description_label.pack(pady=1) self.new_project_label = ttk.Label(root, text="New Project:")
self.description_entry = tk.Text(self, height=3, width=40) self.new_project_label.grid(row=6, column=0, sticky=tk.W, padx=10, pady=5)
self.description_entry.pack(pady=1) self.new_project_entry = ttk.Entry(root, width=30)
self.new_project_entry.grid(row=6, column=1, sticky=tk.W, padx=10, pady=5)
self.export_csv_button = tk.Button( self.add_project_button = ttk.Button(
self, text="Export to CSV", width=20, command=self.export_sessions_csv root, text="Add Project", command=self.add_project
) )
self.export_csv_button.pack(pady=2) self.add_project_button.grid(row=7, column=0, columnspan=2, pady=5)
self.export_pdf_button = tk.Button( # Button Grouping
self, text="Export to PDF", width=20, command=self.export_sessions_pdf button_frame = ttk.Frame(root, padding=10, style="My.TFrame")
button_frame.grid(row=8, column=0, columnspan=2, pady=10)
self.export_csv_button = ttk.Button(
button_frame, text="Export to CSV", command=self.export_to_csv
) )
self.export_pdf_button.pack(pady=2) self.export_csv_button.grid(row=0, column=0, padx=5, pady=5)
self.total_hours_label = tk.Label(self, text="Total Hours Worked: 0.00", font=("Helvetica", 12)) self.export_pdf_button = ttk.Button(
self.total_hours_label.pack(pady=5) button_frame, text="Export to PDF", command=self.export_to_pdf
self.log_text = tk.Text(self, height=10, state=tk.DISABLED)
self.log_text.pack(pady=5, padx=10, fill='both', expand=True)
self.exit_button = tk.Button(
self, text="Exit", width=10, command=self.exit_app
) )
self.exit_button.pack(pady=5) self.export_pdf_button.grid(row=0, column=1, padx=5, pady=5)
def log_message(self, message): self.exit_button = ttk.Button( # Create Exit button
if hasattr(self, 'log_text'): button_frame, text="Exit", command=self.exit_app
self.log_text.configure(state=tk.NORMAL)
self.log_text.insert(tk.END, f"{message}\n")
self.log_text.configure(state=tk.DISABLED)
self.log_text.see(tk.END)
def start_tracking(self):
if self.tracking:
messagebox.showwarning("Warning", "Already tracking!")
return
self.start_time = datetime.now()
self.tracking = True
self.status_label.config(
text=f"Status: Tracking started at {self.start_time.strftime('%H:%M:%S')}"
) )
self.log_message(f"Started at: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") self.exit_button.grid(row=0, column=2, padx=5, pady=5) # Place in button_frame
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
def stop_tracking(self): self.total_time_button = ttk.Button(
if not self.tracking: root,
messagebox.showwarning("Warning", "Not currently tracking!") text="Calculate Total Time",
return command=self.calculate_total_time,
width=20, # Set specific width for this button
end_time = datetime.now()
duration = end_time - self.start_time
description = self.description_entry.get("1.0", tk.END).strip()
self.sessions.append((self.start_time, end_time, duration, description))
self.tracking = False
self.status_label.config(text="Status: Not Tracking")
decimal_hours = self.get_decimal_hours(duration)
self.log_message(
f"Stopped at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}, Duration: {self.format_duration(duration)} "
f"({decimal_hours:.2f} hours), Description: {description}"
) )
self.start_button.config(state=tk.NORMAL) self.total_time_button.grid(row=9, column=0, columnspan=2, pady=5)
self.stop_button.config(state=tk.DISABLED)
self.description_entry.delete("1.0", tk.END)
self.update_total_hours() self.total_time_label = ttk.Label(root, text="Total Time: 00:00:00")
self.total_time_label.grid(row=10, column=0, columnspan=2, pady=5)
def format_duration(self, duration): # Settings UI
total_seconds = int(duration.total_seconds()) self.company_name_label = ttk.Label(root, text="Company Name:")
hours = total_seconds // 3600 self.company_name_label.grid(row=11, column=0, sticky=tk.W, padx=10, pady=5)
minutes = (total_seconds % 3600) // 60 self.company_name_entry = ttk.Entry(root, width=30)
seconds = total_seconds % 60 self.company_name_entry.grid(row=11, column=1, sticky=tk.W, padx=10, pady=5)
return f"{hours}:{minutes:02d}:{seconds:02d}" self.company_name_entry.insert(0, self.company_name)
def get_decimal_hours(self, duration): self.company_address_label = ttk.Label(root, text="Company Address:")
total_hours = duration.total_seconds() / 3600 self.company_address_label.grid(row=12, column=0, sticky=tk.W, padx=10, pady=5)
return total_hours self.company_address_entry = ttk.Entry(root, width=30)
self.company_address_entry.grid(row=12, column=1, sticky=tk.W, padx=10, pady=5)
self.company_address_entry.insert(0, self.company_address)
def export_sessions_csv(self): self.client_name_label = ttk.Label(root, text="Client Name:")
if not self.sessions: self.client_name_label.grid(row=13, column=0, sticky=tk.W, padx=10, pady=5)
messagebox.showinfo("Info", "No sessions to export.") self.client_name_entry = ttk.Entry(root, width=30)
return self.client_name_entry.grid(row=13, column=1, sticky=tk.W, padx=10, pady=5)
self.client_name_entry.insert(0, self.client_name)
filename = "working_sessions.csv" self.client_address_label = ttk.Label(root, text="Client Address:")
self.client_address_label.grid(row=14, column=0, sticky=tk.W, padx=10, pady=5)
self.client_address_entry = ttk.Entry(root, width=30)
self.client_address_entry.grid(row=14, column=1, sticky=tk.W, padx=10, pady=5)
self.client_address_entry.insert(0, self.client_address)
self.hourly_rate_label = ttk.Label(root, text="Hourly Rate:")
self.hourly_rate_label.grid(row=15, column=0, sticky=tk.W, padx=10, pady=5)
self.hourly_rate_entry = ttk.Entry(root, width=10)
self.hourly_rate_entry.grid(row=15, column=1, sticky=tk.W, padx=10, pady=5)
self.hourly_rate_entry.insert(0, str(self.hourly_rate))
self.update_settings_button = ttk.Button(
root, text="Update Settings", command=self.update_settings
)
self.update_settings_button.grid(row=16, column=0, columnspan=2, pady=10)
# Configure grid weights to make the layout expand
for i in range(17):
root.grid_rowconfigure(i, weight=1)
root.grid_columnconfigure(0, weight=1)
root.grid_columnconfigure(1, weight=1)
def load_projects(self):
try: try:
with open(filename, mode="w", newline="") as csvfile: with open(self.project_file, "r") as f:
writer = csv.writer(csvfile) projects = [line.strip() for line in f]
writer.writerow([ return projects
"Start Time", "End Time",
"Duration (H:MM:SS)", "Decimal Hours", "Description"
])
for start, end, duration, description in self.sessions:
writer.writerow([
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
])
writer.writerow(["", "", "", "Total Hours", f"{self.total_hours:.2f}"])
messagebox.showinfo("Export Successful", f"Sessions exported to {filename}")
except Exception as 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)
row_number = 1
total_hours_loaded = False
for row in reader:
if not row:
print(f"Skipping empty row: {row_number}")
row_number += 1
continue
if len(row) == 5:
start_time_str, end_time_str, duration_str, decimal_hours_str, description = row
elif len(row) == 4:
if row[0] == '' and row[1] == '' and row[2] == '' and row[3] == 'Total Hours':
try:
self.total_hours = float(row[4])
self.update_total_hours()
total_hours_loaded = True
print(f"Total hours loaded from row {row_number}: {self.total_hours}")
row_number += 1
continue
except ValueError:
print(f"Skipping total hours row {row_number} due to parsing error")
row_number += 1
continue
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)}): {row_number}")
row_number += 1
continue
if not start_time_str or not end_time_str:
print(f"Skipping row with missing time data: {row_number}")
row_number += 1
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 {row_number} due to parsing error: {ve}")
row_number += 1
if not total_hours_loaded:
self.update_total_hours()
print("Total hours row was not found so calculating from current rows!")
except FileNotFoundError: except FileNotFoundError:
messagebox.showinfo("Info", "No CSV file found. Starting with a new session.") return []
def save_projects(self):
try:
with open(self.project_file, "w") as f:
for project in self.projects:
f.write(project + "\n")
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Failed to load sessions from CSV: {e}") print(f"Error saving projects: {e}")
def parse_duration(self, duration_str): def add_project(self):
hours, minutes, seconds = map(int, duration_str.split(':')) new_project = self.new_project_entry.get().strip()
return timedelta(hours=hours, minutes=minutes, seconds=seconds) if new_project and new_project not in self.projects:
self.projects.append(new_project)
self.project_combo["values"] = self.projects # Update Combobox
self.project_combo.set(new_project) # Set to the new project
self.save_projects()
self.new_project_entry.delete(0, tk.END) # Clear the entry
elif new_project in self.projects:
print("Project already exists.") # Replace with a GUI message box
else:
print("Project name cannot be empty.") # Replace with a GUI message box
def update_total_hours(self): def update_settings(self):
total_duration = timedelta() self.company_name = self.company_name_entry.get()
for start, end, duration, description in self.sessions: self.company_address = self.company_address_entry.get()
total_duration += duration self.client_name = self.client_name_entry.get()
self.client_address = self.client_address_entry.get()
try:
self.hourly_rate = float(self.hourly_rate_entry.get())
except ValueError:
print("Invalid hourly rate. Using default.") # Replace with GUI message
self.hourly_rate = 50.00 # Revert to default, or handle the error
self.total_hours = total_duration.total_seconds() / 3600 # OPTIONAL: Save settings to a file here (e.g., JSON)
self.total_hours_label.config(text=f"Total Hours Worked: {self.total_hours:.2f}")
def toggle_timer(self):
if self.is_running:
self.stop_timer()
else:
self.start_timer()
def start_timer(self):
self.is_running = True
self.start_stop_button.config(text="Stop")
self.start_time = time.time()
self.update_timer()
def stop_timer(self):
self.is_running = False
self.start_stop_button.config(text="Start")
self.root.after_cancel(self.timer_id)
self.log_time_entry()
def update_timer(self):
if self.is_running:
self.elapsed_time = time.time() - self.start_time
minutes, seconds = divmod(self.elapsed_time, 60)
hours, minutes = divmod(minutes, 60)
time_str = "{:02d}:{:02d}:{:02d}".format(
int(hours), int(minutes), int(seconds)
)
self.time_label.config(text=time_str)
self.timer_id = self.root.after(100, self.update_timer)
def log_time_entry(self):
end_time = datetime.datetime.now()
task_description = self.task_entry.get()
project = self.project_combo.get()
duration = self.elapsed_time
start_time_str = end_time - datetime.timedelta(seconds=duration)
entry = {
"task": task_description,
"project": project,
"start_time": start_time_str.strftime("%Y-%m-%d %H:%M:%S"),
"end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"),
"duration": round(duration, 2),
}
self.log_entries.append(entry)
self.update_log_display()
self.elapsed_time = 0
self.time_label.config(text="00:00:00")
def update_log_display(self):
self.log_text.delete("1.0", tk.END)
for entry in self.log_entries:
self.log_text.insert(tk.END, f"Task: {entry['task']}\n")
self.log_text.insert(tk.END, f"Project: {entry['project']}\n")
self.log_text.insert(tk.END, f"Start: {entry['start_time']}\n")
self.log_text.insert(tk.END, f"End: {entry['end_time']}\n")
self.log_text.insert(tk.END, f"Duration: {entry['duration']} seconds\n")
self.log_text.insert(tk.END, "-" * 20 + "\n")
def export_to_csv(self):
try:
with open("time_entries.csv", "w", newline="") as csvfile:
fieldnames = ["task", "project", "start_time", "end_time", "duration"]
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for entry in self.log_entries:
writer.writerow(entry)
print("Exported to CSV successfully!")
except Exception as e:
print(f"Error exporting to CSV: {e}")
def export_to_pdf(self):
try:
filename = f"invoice_{self.invoice_number}.pdf"
c = canvas.Canvas(filename, pagesize=letter)
styles = getSampleStyleSheet()
# --- Header ---
c.setFont("Helvetica-Bold", 16)
c.drawString(inch, 7.5 * inch, self.company_name)
c.setFont("Helvetica", 10)
c.drawString(inch, 7.3 * inch, self.company_address)
c.setFont("Helvetica-Bold", 12)
c.drawString(4.5 * inch, 7.5 * inch, "Invoice")
c.setFont("Helvetica", 10)
c.drawString(
4.5 * inch, 7.3 * inch, f"Invoice Number: {self.invoice_number}"
)
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
c.drawString(4.5 * inch, 7.1 * inch, f"Date: {current_date}")
# --- Client Info ---
c.setFont("Helvetica-Bold", 12)
c.drawString(inch, 6.5 * inch, "Bill To:")
c.setFont("Helvetica", 10)
c.drawString(inch, 6.3 * inch, self.client_name)
c.drawString(inch, 6.1 * inch, self.client_address)
# --- Table ---
data = [["Task", "Project", "Hours", "Rate", "Total"]]
total_amount = 0
for entry in self.log_entries:
hours = entry["duration"] / 3600
line_total = hours * self.hourly_rate
total_amount += line_total
data.append(
[
entry["task"],
entry["project"],
f"{hours:.2f}",
f"${self.hourly_rate:.2f}",
f"${line_total:.2f}",
]
)
table = Table(data, colWidths=[1.5 * inch, 1.5 * inch, inch, inch, inch])
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)
table.wrapOn(c, letter[0] - 2 * inch, letter[1] - 2 * inch)
table.drawOn(c, inch, 4 * inch)
# --- Totals ---
c.setFont("Helvetica-Bold", 12)
c.drawString(4 * inch, 3.5 * inch, "Subtotal:")
c.setFont("Helvetica", 12)
c.drawRightString(5.5 * inch, 3.5 * inch, f"${total_amount:.2f}")
c.setFont("Helvetica-Bold", 12)
c.drawString(4 * inch, 3.3 * inch, "Tax (0%):")
c.setFont("Helvetica", 12)
c.drawRightString(5.5 * inch, 3.3 * inch, "$0.00")
c.setFont("Helvetica-Bold", 12)
c.drawString(4 * inch, 3.1 * inch, "Total:")
c.setFont("Helvetica", 12)
c.drawRightString(5.5 * inch, 3.1 * inch, f"${total_amount:.2f}")
# --- Notes ---
c.setFont("Helvetica", 10)
c.drawString(inch, 2 * inch, "Notes:")
c.drawString(inch, 1.8 * inch, "Thank you for your business!")
c.save()
print(f"Exported to PDF successfully as {filename}!")
self.invoice_number += 1
except Exception as e:
print(f"Error exporting to PDF: {e}")
def calculate_total_time(self):
total_seconds = sum(entry["duration"] for entry in self.log_entries)
minutes, seconds = divmod(total_seconds, 60)
hours, minutes = divmod(minutes, 60)
time_str = "{:02d}:{:02d}:{:02d}".format(
int(hours), int(minutes), int(seconds)
)
self.total_time_label.config(text=f"Total Time: {time_str}")
def exit_app(self): # Define the exit_app method
self.root.destroy() # Close the Tkinter window
sys.exit()
def exit_app(self):
self.destroy()
if __name__ == "__main__": if __name__ == "__main__":
app = TimeLogix() root = tk.Tk()
app.mainloop() app = TimeLogix(root)
root.mainloop()