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:
commit
a849b79a98
13 changed files with 706 additions and 355 deletions
33
.gitignore
vendored
33
.gitignore
vendored
|
|
@ -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
|
||||
10
TimeLogix_1.0/DEBIAN/control
Normal file
10
TimeLogix_1.0/DEBIAN/control
Normal 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
15
TimeLogix_1.0/DEBIAN/postinst
Executable 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
8
TimeLogix_1.0/DEBIAN/postrm
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Remove desktop entry
|
||||
rm /usr/share/applications/TimeLogix.desktop
|
||||
|
||||
exit 0
|
||||
w
|
||||
BIN
TimeLogix_1.0/opt/timelogix/TimeLogix
Executable file
BIN
TimeLogix_1.0/opt/timelogix/TimeLogix
Executable file
Binary file not shown.
0
__init__.py
Normal file
0
__init__.py
Normal file
188
database.py
Normal file
188
database.py
Normal 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
22
main.py
Normal 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
115
pdf_exporter.py
Normal 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}")
|
||||
353
time_logix.py
353
time_logix.py
|
|
@ -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
0
ui/__init__.py
Normal file
22
ui/components.py
Normal file
22
ui/components.py
Normal 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
295
ui/main_window.py
Normal 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
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue