Merge pull request 'Very Big PR' (#2) from enhancements into main

Reviewed-on: https://brew.bsd.cafe/blake/time_logix/pulls/2
This commit is contained in:
blake 2025-04-06 02:49:35 +02:00
commit a849b79a98
13 changed files with 706 additions and 355 deletions

33
.gitignore vendored
View file

@ -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

View file

@ -0,0 +1,10 @@
Package: timelogix
Version: 1.0
Architecture: amd64
Maintainer: Blake Ridgway <blake@blakeridgway.com>
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

15
TimeLogix_1.0/DEBIAN/postinst Executable file
View file

@ -0,0 +1,15 @@
#!/bin/sh
set -e
cat <<EOF > /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

8
TimeLogix_1.0/DEBIAN/postrm Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Remove desktop entry
rm /usr/share/applications/TimeLogix.desktop
exit 0
w

Binary file not shown.

0
__init__.py Normal file
View file

188
database.py Normal file
View file

@ -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

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()

115
pdf_exporter.py Normal file
View file

@ -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}")

View file

@ -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()

0
ui/__init__.py Normal file
View file

22
ui/components.py Normal file
View file

@ -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)

295
ui/main_window.py Normal file
View file

@ -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