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
from tkinter import messagebox
from datetime import datetime, timedelta
from tkinter import ttk
import time
import datetime
import csv
import os
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.platypus import Table, TableStyle, Paragraph
from reportlab.lib.styles import getSampleStyleSheet
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.tracking = False
self.sessions = []
self.total_hours = 0
self.elapsed_time = 0
self.timer_id = None
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()
self.load_sessions_from_csv()
self.update_total_hours()
# UI elements
self.task_label = ttk.Label(root, text="Task Description:")
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.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12))
self.status_label.pack(pady=5)
self.project_label = ttk.Label(root, text="Project:")
self.project_label.grid(row=1, column=0, sticky=tk.W, padx=10, 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, text="Start Tracking", width=20, command=self.start_tracking
self.time_label = ttk.Label(
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, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED
self.log_label = ttk.Label(root, text="Log Entries:")
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:")
self.description_label.pack(pady=1)
self.description_entry = tk.Text(self, height=3, width=40)
self.description_entry.pack(pady=1)
# Add Project UI
self.new_project_label = ttk.Label(root, text="New Project:")
self.new_project_label.grid(row=6, column=0, sticky=tk.W, padx=10, pady=5)
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, text="Export to CSV", width=20, command=self.export_sessions_csv
self.add_project_button = ttk.Button(
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(
self, text="Export to PDF", width=20, command=self.export_sessions_pdf
# Button Grouping
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.total_hours_label.pack(pady=5)
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.export_pdf_button = ttk.Button(
button_frame, text="Export to PDF", command=self.export_to_pdf
)
self.exit_button.pack(pady=5)
self.export_pdf_button.grid(row=0, column=1, padx=5, pady=5)
def log_message(self, message):
if hasattr(self, 'log_text'):
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.exit_button = ttk.Button( # Create Exit button
button_frame, text="Exit", command=self.exit_app
)
self.log_message(f"Started at: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.exit_button.grid(row=0, column=2, padx=5, pady=5) # Place in button_frame
def stop_tracking(self):
if not self.tracking:
messagebox.showwarning("Warning", "Not currently tracking!")
return
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.total_time_button = ttk.Button(
root,
text="Calculate Total Time",
command=self.calculate_total_time,
width=20, # Set specific width for this button
)
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.description_entry.delete("1.0", tk.END)
self.total_time_button.grid(row=9, column=0, columnspan=2, pady=5)
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):
total_seconds = int(duration.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
return f"{hours}:{minutes:02d}:{seconds:02d}"
# Settings UI
self.company_name_label = ttk.Label(root, text="Company Name:")
self.company_name_label.grid(row=11, column=0, sticky=tk.W, padx=10, pady=5)
self.company_name_entry = ttk.Entry(root, width=30)
self.company_name_entry.grid(row=11, column=1, sticky=tk.W, padx=10, pady=5)
self.company_name_entry.insert(0, self.company_name)
def get_decimal_hours(self, duration):
total_hours = duration.total_seconds() / 3600
return total_hours
self.company_address_label = ttk.Label(root, text="Company Address:")
self.company_address_label.grid(row=12, column=0, sticky=tk.W, padx=10, pady=5)
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):
if not self.sessions:
messagebox.showinfo("Info", "No sessions to export.")
return
self.client_name_label = ttk.Label(root, text="Client Name:")
self.client_name_label.grid(row=13, column=0, sticky=tk.W, padx=10, pady=5)
self.client_name_entry = ttk.Entry(root, width=30)
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:
with open(filename, mode="w", newline="") as csvfile:
writer = csv.writer(csvfile)
writer.writerow([
"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}")
with open(self.project_file, "r") as f:
projects = [line.strip() for line in f]
return projects
except FileNotFoundError:
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:
messagebox.showerror("Error", f"Failed to export: {e}")
print(f"Error saving projects: {e}")
def export_sessions_pdf(self):
if not self.sessions:
messagebox.showinfo("Info", "No sessions to export.")
return
def add_project(self):
new_project = self.new_project_entry.get().strip()
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
filename = "working_sessions.pdf"
def update_settings(self):
self.company_name = self.company_name_entry.get()
self.company_address = self.company_address_entry.get()
self.client_name = self.client_name_entry.get()
self.client_address = self.client_address_entry.get()
try:
doc = SimpleDocTemplate(filename, pagesize=letter)
elements = []
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
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
])
# OPTIONAL: Save settings to a file here (e.g., JSON)
data.append(["", "", "", "Total Hours", f"{self.total_hours:.2f}"])
def toggle_timer(self):
if self.is_running:
self.stop_timer()
else:
self.start_timer()
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)
])
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)
elements.append(table)
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
doc.build(elements)
messagebox.showinfo("Export Successful", f"Sessions exported to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to export: {e}")
print(f"Error exporting to PDF: {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
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}")
for row in reader:
if not row:
print(f"Skipping empty row: {row_number}")
row_number += 1
continue
def exit_app(self): # Define the exit_app method
self.root.destroy() # Close the Tkinter window
sys.exit()
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:
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}")
def exit_app(self):
self.destroy()
if __name__ == "__main__":
app = TimeLogix()
app.mainloop()
root = tk.Tk()
app = TimeLogix(root)
root.mainloop()