refactor: Create main.py as entry point, implement on close

This commit creates a dedicated entry point `main.py`, instantiates
the application components, and handles the WM_DELETE_WINDOW protocol.

- Removed window creation and mainloop from previous file.
- Created a dedicated entry point with basic error handling.
- Implemented an on_closing protocol to properly close db and exit application
- Implemented the loading of database objects.
This commit is contained in:
Blake Ridgway 2025-04-05 18:03:52 -05:00
parent 2c6c55a176
commit 1231673385
2 changed files with 22 additions and 497 deletions

22
main.py Normal file
View file

@ -0,0 +1,22 @@
import sys
from database import Database
from pdf_exporter import PDFExporter
from ui.main_window import MainWindow
def main():
db = Database()
pdf_exporter = PDFExporter()
app = MainWindow(db, pdf_exporter)
def on_closing():
db.close()
app.destroy() # Properly destroy the Tkinter window
sys.exit()
app.protocol("WM_DELETE_WINDOW", on_closing) # Handle window closing
app.mainloop()
if __name__ == "__main__":
main()

View file

@ -1,497 +0,0 @@
import customtkinter as ctk
import tkinter as tk
from tkinter import ttk
import time
import datetime
import csv
from reportlab.lib.pagesizes import letter
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 os
class TimeLogix:
def __init__(self):
self.root = ctk.CTk()
self.root.title("TimeLogix")
# Theme Configuration
ctk.set_appearance_mode("Dark") # Or "Light", "System"
ctk.set_default_color_theme("blue")
# --- Styling ---
self.font_family = "Segoe UI"
self.font_size = 12
# --- App Data ---
self.project_file = "projects.txt"
self.invoice_file = "invoice_number.txt" # File to store invoice number
self.projects = self.load_projects()
self.is_running = False
self.start_time = None
self.elapsed_time = 0
self.timer_id = None
self.log_entries = []
self.invoice_number = self.load_invoice_number() # Load invoice number
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.csv_file = "working_sessions.csv"
# --- Scrollable Frame ---
self.scrollable_frame = ctk.CTkScrollableFrame(
self.root, width=450, height=600
) # Consider making height adaptable
self.scrollable_frame.pack(fill="both", expand=True, padx=10, pady=10)
# --- UI Elements ---
self.task_label = ctk.CTkLabel(
self.scrollable_frame,
text="Task Description:",
font=(self.font_family, self.font_size),
)
self.task_label.pack(pady=(10, 2))
self.task_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
self.task_entry.pack(pady=(2, 10))
self.project_label = ctk.CTkLabel(
self.scrollable_frame,
text="Project:",
font=(self.font_family, self.font_size),
)
self.project_label.pack(pady=(10, 2))
self.project_combo = ctk.CTkComboBox(
self.scrollable_frame, values=self.projects, width=250
)
self.project_combo.pack(pady=(2, 10))
if self.projects:
self.project_combo.set(self.projects[0])
self.time_label = ctk.CTkLabel(
self.scrollable_frame, text="00:00:00", font=(self.font_family, 36)
)
self.time_label.pack(pady=(15, 20))
self.start_stop_button = ctk.CTkButton(
self.scrollable_frame,
text="Start",
command=self.toggle_timer,
corner_radius=8,
)
self.start_stop_button.pack(pady=(5, 20))
self.log_label = ctk.CTkLabel(
self.scrollable_frame,
text="Log Entries:",
font=(self.font_family, self.font_size),
)
self.log_label.pack(pady=(10, 2))
self.log_text = ctk.CTkTextbox(
self.scrollable_frame,
width=400,
height=100,
font=(self.font_family, self.font_size),
)
self.log_text.pack(pady=5, padx=10, fill="both", expand=True)
self.new_project_label = ctk.CTkLabel(
self.scrollable_frame,
text="New Project:",
font=(self.font_family, self.font_size),
)
self.new_project_label.pack(pady=(10, 2))
self.new_project_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
self.new_project_entry.pack(pady=(2, 10))
self.add_project_button = ctk.CTkButton(
self.scrollable_frame,
text="Add Project",
command=self.add_project,
corner_radius=8,
)
self.add_project_button.pack(pady=(5, 15))
# --- Button Frame ---
button_frame = ctk.CTkFrame(self.scrollable_frame)
button_frame.pack(pady=(10, 15))
self.export_csv_button = ctk.CTkButton(
button_frame,
text="Export to CSV",
command=self.export_to_csv,
corner_radius=8,
)
self.export_csv_button.pack(
side="left", padx=5, pady=5
) # Using side="left" for horizontal layout
self.export_pdf_button = ctk.CTkButton(
button_frame,
text="Export to PDF",
command=self.export_to_pdf,
corner_radius=8,
)
self.export_pdf_button.pack(side="left", padx=5, pady=5)
self.exit_button = ctk.CTkButton(
button_frame, text="Exit", command=self.exit_app, corner_radius=8
)
self.exit_button.pack(side="left", padx=5, pady=5)
self.total_time_button = ctk.CTkButton(
self.scrollable_frame,
text="Calculate Total Time",
command=self.calculate_total_time,
corner_radius=8,
)
self.total_time_button.pack(pady=(5, 15))
self.total_time_label = ctk.CTkLabel(
self.scrollable_frame,
text="Total Time: 00:00:00",
font=(self.font_family, self.font_size),
)
self.total_time_label.pack(pady=(5, 15))
# --- Settings UI ---
self.company_name_label = ctk.CTkLabel(
self.scrollable_frame,
text="Company Name:",
font=(self.font_family, self.font_size),
)
self.company_name_label.pack(pady=(10, 2))
self.company_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
self.company_name_entry.pack(pady=(2, 10))
self.company_address_label = ctk.CTkLabel(
self.scrollable_frame,
text="Company Address:",
font=(self.font_family, self.font_size),
)
self.company_address_label.pack(pady=(10, 2))
self.company_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
self.company_address_entry.pack(pady=(2, 10))
self.client_name_label = ctk.CTkLabel(
self.scrollable_frame,
text="Client Name:",
font=(self.font_family, self.font_size),
)
self.client_name_label.pack(pady=(10, 2))
self.client_name_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
self.client_name_entry.pack(pady=(2, 10))
self.client_address_label = ctk.CTkLabel(
self.scrollable_frame,
text="Client Address:",
font=(self.font_family, self.font_size),
)
self.client_address_label.pack(pady=(10, 2))
self.client_address_entry = ctk.CTkEntry(self.scrollable_frame, width=250)
self.client_address_entry.pack(pady=(2, 10))
self.hourly_rate_label = ctk.CTkLabel(
self.scrollable_frame,
text="Hourly Rate:",
font=(self.font_family, self.font_size),
)
self.hourly_rate_label.pack(pady=(10, 2))
self.hourly_rate_entry = ctk.CTkEntry(self.scrollable_frame, width=100)
self.hourly_rate_entry.pack(pady=(2, 10))
self.update_settings_button = ctk.CTkButton(
self.scrollable_frame,
text="Update Settings",
command=self.update_settings,
corner_radius=8,
)
self.update_settings_button.pack(pady=(15, 20))
self.load_log_entries()
def load_projects(self):
try:
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:
print(f"Error saving projects: {e}")
def load_invoice_number(self):
try:
with open(self.invoice_file, "r") as f:
return int(f.read().strip())
except FileNotFoundError:
return 1
except ValueError:
print("Invalid invoice number in file, resetting to 1.")
return 1
def save_invoice_number(self):
try:
with open(self.invoice_file, "w") as f:
f.write(str(self.invoice_number))
except Exception as e:
print(f"Error saving invoice number: {e}")
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.configure(values=self.projects)
self.project_combo.set(new_project)
self.save_projects()
self.new_project_entry.delete(0, tk.END)
elif new_project in self.projects:
print("Project already exists.")
else:
print("Project name cannot be empty.")
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:
self.hourly_rate = float(self.hourly_rate_entry.get())
except ValueError:
print("Invalid hourly rate. Using default.")
self.hourly_rate = 50.00
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.configure(text="Stop")
self.start_time = time.time()
self.update_timer()
def stop_timer(self):
self.is_running = False
self.start_stop_button.configure(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.configure(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.configure(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 load_log_entries(self):
if os.path.exists(self.csv_file):
try:
with open(self.csv_file, "r") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
try:
row["duration"] = float(row["duration"])
except ValueError:
# Try converting duration from hh:mm:ss format to seconds
try:
h, m, s = map(int, row["duration"].split(":"))
row["duration"] = h * 3600 + m * 60 + s
except ValueError:
print(
f"Invalid duration format: {row['duration']}. "
"Skipping this entry."
)
continue # Skip this entry if the format is invalid
self.log_entries.append(row)
self.update_log_display()
print("Loaded log entries from CSV successfully!")
except Exception as e:
print(f"Error loading log entries from CSV: {e}")
def export_to_csv(self):
try:
with open(self.csv_file, "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 ---
bill_to_y = 6.5 * inch # Starting y position for "Bill To:"
line_height = 0.2 * inch # Height for each line of text
c.setFont("Helvetica-Bold", 12)
c.drawString(inch, bill_to_y, "Bill To:") # "Bill To:" label
c.setFont("Helvetica", 10)
c.drawString(
inch, bill_to_y - line_height, self.client_name
) # Client Name
c.drawString(
inch, bill_to_y - 2 * line_height, self.client_address
) # 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
self.save_invoice_number() # Save the updated invoice number
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.configure(text=f"Total Time: {time_str}")
def exit_app(self):
self.root.destroy()
sys.exit()
if __name__ == "__main__":
app = TimeLogix()
app.root.mainloop()