diff --git a/.gitignore b/.gitignore index 0dad42b..c1b3842 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,33 @@ -venv +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +TimeLogix.spec +TimeLogix.app/ + +# SQLite Database +timelogix.db + +#pyinstaller +*.onefile +*.app + +# Environments +.env +venv/ +ENV/ +env/ + +# General +*~ +*.swp +*.DS_Store *.csv *.pdf -projects.txt +*.txt +*.db +*.deb \ No newline at end of file diff --git a/TimeLogix_1.0/DEBIAN/control b/TimeLogix_1.0/DEBIAN/control new file mode 100644 index 0000000..d6d3a6a --- /dev/null +++ b/TimeLogix_1.0/DEBIAN/control @@ -0,0 +1,10 @@ +Package: timelogix +Version: 1.0 +Architecture: amd64 +Maintainer: Blake Ridgway +Description: A simple time tracking application. + Long description of the application. You can + add multiple lines here. +Depends: python3, python3-tk +Section: utils +Priority: optional diff --git a/TimeLogix_1.0/DEBIAN/postinst b/TimeLogix_1.0/DEBIAN/postinst new file mode 100755 index 0000000..9713712 --- /dev/null +++ b/TimeLogix_1.0/DEBIAN/postinst @@ -0,0 +1,15 @@ +#!/bin/sh +set -e + +cat < /usr/share/applications/TimeLogix.desktop +[Desktop Entry] +Name=TimeLogix +Comment=Time tracking application +Exec=/opt/timelogix/TimeLogix +#Icon=/opt/timelogix/icon.ico # If you have an icon +Terminal=false +Type=Application +Categories=Utility; +EOF + +exit 0 diff --git a/TimeLogix_1.0/DEBIAN/postrm b/TimeLogix_1.0/DEBIAN/postrm new file mode 100755 index 0000000..dc67d0c --- /dev/null +++ b/TimeLogix_1.0/DEBIAN/postrm @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +# Remove desktop entry +rm /usr/share/applications/TimeLogix.desktop + +exit 0 +w diff --git a/TimeLogix_1.0/opt/timelogix/TimeLogix b/TimeLogix_1.0/opt/timelogix/TimeLogix new file mode 100755 index 0000000..9a92fc9 Binary files /dev/null and b/TimeLogix_1.0/opt/timelogix/TimeLogix differ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database.py b/database.py new file mode 100644 index 0000000..eafa312 --- /dev/null +++ b/database.py @@ -0,0 +1,188 @@ +# timelogix/database.py +import sqlite3 + + +class Database: + def __init__(self, db_file="timelogix.db"): + self.db_file = db_file + self.conn = None # Initialize conn to None + self.cursor = None # Initialize cursor to None + try: + self.conn = sqlite3.connect(self.db_file) + self.cursor = self.conn.cursor() + self.create_tables() + except sqlite3.Error as e: + print(f"Database error: {e}") + # Handle the error appropriately, e.g., exit or disable DB features + + def create_tables(self): + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE + ) + """ + ) + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS log_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task TEXT, + project_id INTEGER, + start_time TEXT, + end_time TEXT, + duration REAL, + FOREIGN KEY (project_id) REFERENCES projects (id) + ) + """ + ) + self.cursor.execute( + """ + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_name TEXT, + company_address TEXT, + client_name TEXT, + client_address TEXT, + hourly_rate REAL, + invoice_number INTEGER + ) + """ + ) + # Check if settings exist, if not create + self.cursor.execute("SELECT COUNT(*) FROM settings") + if self.cursor.fetchone()[0] == 0: + self.cursor.execute( + """ + INSERT INTO settings (company_name, company_address, client_name, client_address, hourly_rate, invoice_number) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + "Your Company Name", # Default company name + "123 Main St, Anytown, USA", # Default company address + "Client Name", # Default client name + "Client Address", # Default client address + 60.00, # Default hourly rate + 1, + ), + ) # Start invoice number at 1 + self.conn.commit() + + def load_projects(self): + self.cursor.execute("SELECT name FROM projects") + projects = [row[0] for row in self.cursor.fetchall()] + return projects + + def save_projects(self, projects): + for project in projects: + try: + self.cursor.execute( + "INSERT INTO projects (name) VALUES (?)", (project,) + ) + except sqlite3.IntegrityError: + pass # Ignore if project already exists + self.conn.commit() + + def load_invoice_number(self): + self.cursor.execute("SELECT invoice_number FROM settings") + invoice_number = self.cursor.fetchone()[0] + return invoice_number + + def save_invoice_number(self, invoice_number): + self.cursor.execute("UPDATE settings SET invoice_number = ?", (invoice_number,)) + self.conn.commit() + + def add_project(self, project_name): + try: + self.cursor.execute( + "INSERT INTO projects (name) VALUES (?)", (project_name,) + ) + self.conn.commit() + return True + except sqlite3.IntegrityError: + return False # Project already exists + + def get_project_id(self, project_name): + self.cursor.execute("SELECT id FROM projects WHERE name = ?", (project_name,)) + result = self.cursor.fetchone() + if result: + return result[0] + else: + return None + + def insert_log_entry(self, task, project_id, start_time, end_time, duration): + self.cursor.execute( + """ + INSERT INTO log_entries (task, project_id, start_time, end_time, duration) + VALUES (?, ?, ?, ?, ?) + """, + (task, project_id, start_time, end_time, duration), + ) + self.conn.commit() + + def load_log_entries(self): + self.cursor.execute( + """ + SELECT log_entries.task, projects.name, log_entries.start_time, + log_entries.end_time, log_entries.duration + FROM log_entries + JOIN projects ON log_entries.project_id = projects.id + """ + ) + rows = self.cursor.fetchall() + log_entries = [] + for row in rows: + task, project, start_time, end_time, duration = row + entry = { + "task": task, + "project": project, + "start_time": start_time, + "end_time": end_time, + "duration": duration, + } + log_entries.append(entry) + return log_entries + + def update_settings( + self, company_name, company_address, client_name, client_address, hourly_rate + ): + self.cursor.execute( + """ + UPDATE settings SET + company_name = ?, company_address = ?, client_name = ?, + client_address = ?, hourly_rate = ? + """, + (company_name, company_address, client_name, client_address, hourly_rate), + ) + self.conn.commit() + + def load_settings(self): + self.cursor.execute("SELECT * FROM settings") + settings = self.cursor.fetchone() + if settings: + ( + _, + company_name, + company_address, + client_name, + client_address, + hourly_rate, + invoice_number, + ) = settings + return { + "company_name": company_name, + "company_address": company_address, + "client_name": client_name, + "client_address": client_address, + "hourly_rate": hourly_rate, + "invoice_number": invoice_number, + } + else: + return None + + def close(self): + if self.conn: + self.conn.close() + self.conn = None + self.cursor = None diff --git a/main.py b/main.py new file mode 100644 index 0000000..ab0cf65 --- /dev/null +++ b/main.py @@ -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() diff --git a/pdf_exporter.py b/pdf_exporter.py new file mode 100644 index 0000000..f1d59ad --- /dev/null +++ b/pdf_exporter.py @@ -0,0 +1,115 @@ +# timelogix/pdf_exporter.py +import datetime +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas +from reportlab.lib import colors +from reportlab.platypus import Table, TableStyle +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import inch + + +class PDFExporter: + def __init__(self): + 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.invoice_number = 1 + + def export_to_pdf(self, log_entries): + 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 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}!") + + except Exception as e: + print(f"Error exporting to PDF: {e}") diff --git a/time_logix.py b/time_logix.py deleted file mode 100644 index a47d77c..0000000 --- a/time_logix.py +++ /dev/null @@ -1,353 +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 - - -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.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 = 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 - - # --- 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)) - - 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 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 export_to_csv(self): - try: - with open("working_sessions.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.configure(text=f"Total Time: {time_str}") - - def exit_app(self): - self.root.destroy() - sys.exit() - - -if __name__ == "__main__": - app = TimeLogix() - app.root.mainloop() diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/components.py b/ui/components.py new file mode 100644 index 0000000..26008ff --- /dev/null +++ b/ui/components.py @@ -0,0 +1,22 @@ +# timelogix/ui/components.py +import customtkinter as ctk + + +def create_label(parent, text, font_family, font_size): + return ctk.CTkLabel(parent, text=text, font=(font_family, font_size)) + + +def create_entry(parent, width): + return ctk.CTkEntry(parent, width=width) + + +def create_button(parent, text, command): + return ctk.CTkButton(parent, text=text, command=command, corner_radius=8) + + +def create_combo(parent, values, width): + return ctk.CTkComboBox(parent, values=values, width=width) + + +def create_text_box(parent, width, height, font): + return ctk.CTkTextbox(parent, width=width, height=height, font=font) diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 0000000..006c7f2 --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,295 @@ +# timelogix/ui/main_window.py +import customtkinter as ctk +import tkinter as tk +from tkinter import ttk +import time +import csv +import datetime +from .components import create_label, create_entry, create_button, create_combo, create_text_box + + +class MainWindow(ctk.CTk): + def __init__(self, db, pdf_exporter): + super().__init__() + self.db = db + self.pdf_exporter = pdf_exporter + self.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.is_running = False + self.start_time = None + self.elapsed_time = 0 + self.timer_id = None + + # Load settings from the database + settings = self.db.load_settings() + if settings: + self.company_name = settings["company_name"] + self.company_address = settings["company_address"] + self.client_name = settings["client_name"] + self.client_address = settings["client_address"] + self.hourly_rate = settings["hourly_rate"] + self.invoice_number = settings["invoice_number"] + else: + # Provide default values if settings are not found + 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.invoice_number = 1 + + + # UI Components + self.scrollable_frame = ctk.CTkScrollableFrame(self, width=450, height=600) + self.scrollable_frame.pack(fill="both", expand=True, padx=10, pady=10) + + self.task_label = create_label(self.scrollable_frame, "Task Description:", self.font_family, self.font_size) + self.task_label.pack(pady=(10, 2)) + self.task_entry = create_entry(self.scrollable_frame, width=250) + self.task_entry.pack(pady=(2, 10)) + + self.project_label = create_label(self.scrollable_frame, "Project:", self.font_family, self.font_size) + self.project_label.pack(pady=(10, 2)) + self.projects = self.db.load_projects() + self.project_combo = create_combo(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 = create_label(self.scrollable_frame, "00:00:00", self.font_family, 36) + self.time_label.pack(pady=(15, 20)) + self.start_stop_button = create_button(self.scrollable_frame, "Start", self.toggle_timer) + self.start_stop_button.pack(pady=(5, 20)) + + self.log_label = create_label(self.scrollable_frame, "Log Entries:", self.font_family, self.font_size) + self.log_label.pack(pady=(10, 2)) + self.log_text = create_text_box(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 = create_label(self.scrollable_frame, "New Project:", self.font_family, self.font_size) + self.new_project_label.pack(pady=(10, 2)) + self.new_project_entry = create_entry(self.scrollable_frame, width=250) + self.new_project_entry.pack(pady=(2, 10)) + self.add_project_button = create_button(self.scrollable_frame, "Add Project", self.add_project) + self.add_project_button.pack(pady=(5, 15)) + + button_frame = ctk.CTkFrame(self.scrollable_frame) + button_frame.pack(pady=(10, 15)) + self.export_csv_button = create_button(button_frame, "Export to CSV", self.export_to_csv) + self.export_csv_button.pack(side="left", padx=5, pady=5) + self.export_pdf_button = create_button(button_frame, "Export to PDF", self.export_to_pdf) + self.export_pdf_button.pack(side="left", padx=5, pady=5) + self.exit_button = create_button(button_frame, "Exit", self.exit_app) + self.exit_button.pack(side="left", padx=5, pady=5) + + self.total_time_button = create_button(self.scrollable_frame, "Calculate Total Time", self.calculate_total_time) + self.total_time_button.pack(pady=(5, 15)) + self.total_time_label = create_label(self.scrollable_frame, "Total Time: 00:00:00", self.font_family, self.font_size) + self.total_time_label.pack(pady=(5, 15)) + + # Settings UI + self.company_name_label = create_label(self.scrollable_frame, "Company Name:", self.font_family, self.font_size) + self.company_name_label.pack(pady=(10, 2)) + self.company_name_entry = create_entry(self.scrollable_frame, width=250) + self.company_name_entry.pack(pady=(2, 10)) + self.company_name_entry.insert(0, self.company_name) # Initial value + + self.company_address_label = create_label(self.scrollable_frame, "Company Address:", self.font_family, self.font_size) + self.company_address_label.pack(pady=(10, 2)) + self.company_address_entry = create_entry(self.scrollable_frame, width=250) + self.company_address_entry.pack(pady=(2, 10)) + self.company_address_entry.insert(0, self.company_address) # Initial value + + self.client_name_label = create_label(self.scrollable_frame, "Client Name:", self.font_family, self.font_size) + self.client_name_label.pack(pady=(10, 2)) + self.client_name_entry = create_entry(self.scrollable_frame, width=250) + self.client_name_entry.pack(pady=(2, 10)) + self.client_name_entry.insert(0, self.client_name) # Initial value + + self.client_address_label = create_label(self.scrollable_frame, "Client Address:", self.font_family, self.font_size) + self.client_address_label.pack(pady=(10, 2)) + self.client_address_entry = create_entry(self.scrollable_frame, width=250) + self.client_address_entry.pack(pady=(2, 10)) + self.client_address_entry.insert(0, self.client_address) # Initial value + + self.hourly_rate_label = create_label(self.scrollable_frame, "Hourly Rate:", self.font_family, self.font_size) + self.hourly_rate_label.pack(pady=(10, 2)) + self.hourly_rate_entry = create_entry(self.scrollable_frame, width=100) + self.hourly_rate_entry.pack(pady=(2, 10)) + self.hourly_rate_entry.insert(0, str(self.hourly_rate)) # Initial value + + self.update_settings_button = create_button(self.scrollable_frame, "Update Settings", self.update_settings) + self.update_settings_button.pack(pady=(15, 20)) + + + self.load_log_entries() # Load log entries after UI is set up + + 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.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.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) + + # Get project ID from the database + project_id = self.db.get_project_id(project) + if not project_id: + print("Project not found in the database.") + return + + self.db.insert_log_entry( + task_description, + project_id, + start_time_str.strftime("%Y-%m-%d %H:%M:%S"), + end_time.strftime("%Y-%m-%d %H:%M:%S"), + duration, + ) + + self.load_log_entries() # Refresh log entries + 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): + self.log_entries = self.db.load_log_entries() + self.update_log_display() + + def add_project(self): + new_project = self.new_project_entry.get().strip() + if new_project and new_project not in self.projects: + if self.db.add_project(new_project): + self.projects.append(new_project) + self.project_combo.configure(values=self.projects) + self.project_combo.set(new_project) + self.new_project_entry.delete(0, tk.END) + else: + print("Project already exists in the database.") + elif new_project in self.projects: + print("Project already exists.") + else: + print("Project name cannot be empty.") + + def update_settings(self): + company_name = self.company_name_entry.get() + company_address = self.company_address_entry.get() + client_name = self.client_name_entry.get() + client_address = self.client_address_entry.get() + try: + hourly_rate = float(self.hourly_rate_entry.get()) + except ValueError: + print("Invalid hourly rate. Using default.") + hourly_rate = 50.00 + + self.db.update_settings( + company_name, company_address, client_name, client_address, hourly_rate + ) + + # Update local settings + self.company_name = company_name + self.company_address = company_address + self.client_name = client_name + self.client_address = client_address + self.hourly_rate = hourly_rate + + def export_to_csv(self): + try: + with open("working_sessions.csv", "w", newline="", encoding='utf-8') 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: + # Load settings and increment invoice number + settings = self.db.load_settings() + if settings: + self.company_name = settings["company_name"] + self.company_address = settings["company_address"] + self.client_name = settings["client_name"] + self.client_address = settings["client_address"] + self.hourly_rate = settings["hourly_rate"] + self.invoice_number = settings["invoice_number"] + + self.pdf_exporter.company_name = self.company_name + self.pdf_exporter.company_address = self.company_address + self.pdf_exporter.client_name = self.client_name + self.pdf_exporter.client_address = self.client_address + self.pdf_exporter.hourly_rate = self.hourly_rate + self.pdf_exporter.invoice_number = self.invoice_number + + self.pdf_exporter.export_to_pdf(self.log_entries) + + # Increment and save the invoice number + self.invoice_number += 1 + self.db.save_invoice_number(self.invoice_number) + self.db.conn.commit() + 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.destroy() # Correctly destroy the main window and all children + +