overhaul of timelogix app
This commit is contained in:
		
							parent
							
								
									266d21b540
								
							
						
					
					
						commit
						34d7c4c9dd
					
				
					 1 changed files with 355 additions and 225 deletions
				
			
		
							
								
								
									
										594
									
								
								time_logix.py
									
										
									
									
									
								
							
							
						
						
									
										594
									
								
								time_logix.py
									
										
									
									
									
								
							|  | @ -1,271 +1,401 @@ | ||||||
| import tkinter as tk | import tkinter as tk | ||||||
| from tkinter import messagebox | from tkinter import ttk | ||||||
| from datetime import datetime, timedelta | import time | ||||||
|  | import datetime | ||||||
| import csv | import csv | ||||||
| import os |  | ||||||
| from reportlab.lib.pagesizes import letter | from reportlab.lib.pagesizes import letter | ||||||
| from reportlab.platypus import SimpleDocTemplate, Table, TableStyle | from reportlab.pdfgen import canvas | ||||||
| from reportlab.lib import colors | from reportlab.lib import colors | ||||||
|  | from reportlab.platypus import Table, TableStyle, Paragraph | ||||||
|  | from reportlab.lib.styles import getSampleStyleSheet | ||||||
| from reportlab.lib.units import inch | from reportlab.lib.units import inch | ||||||
|  | import sys  # Import the sys module | ||||||
| 
 | 
 | ||||||
| class TimeLogix(tk.Tk): |  | ||||||
|     def __init__(self): |  | ||||||
|         super().__init__() |  | ||||||
|         self.title("TimeLogix") |  | ||||||
|         self.geometry("500x500") |  | ||||||
| 
 | 
 | ||||||
|  | class TimeLogix: | ||||||
|  |     def __init__(self, root): | ||||||
|  |         self.root = root | ||||||
|  |         self.root.title("TimeLogix Application") | ||||||
|  | 
 | ||||||
|  |         # Styling | ||||||
|  |         self.bg_color = "#f0f0f0"  # Light gray background | ||||||
|  |         self.font_family = "Segoe UI"  # Modern font | ||||||
|  |         self.font_size = 10 | ||||||
|  |         self.button_bg = "#4CAF50"  # Green button background | ||||||
|  |         self.button_fg = "white"  # White button text | ||||||
|  |         self.button_width = 15  # Consistent button width | ||||||
|  | 
 | ||||||
|  |         self.root.configure(bg=self.bg_color) | ||||||
|  | 
 | ||||||
|  |         style = ttk.Style() | ||||||
|  |         style.configure( | ||||||
|  |             "TLabel", background=self.bg_color, font=(self.font_family, self.font_size) | ||||||
|  |         ) | ||||||
|  |         style.configure( | ||||||
|  |             "TButton", | ||||||
|  |             background=self.button_bg, | ||||||
|  |             foreground=self.button_fg, | ||||||
|  |             font=(self.font_family, self.font_size), | ||||||
|  |             padding=5, | ||||||
|  |             width=self.button_width,  # Set button width here | ||||||
|  |         ) | ||||||
|  |         style.configure( | ||||||
|  |             "TCombobox", background="white", font=(self.font_family, self.font_size) | ||||||
|  |         ) | ||||||
|  |         style.configure( | ||||||
|  |             "TEntry", background="white", font=(self.font_family, self.font_size) | ||||||
|  |         ) | ||||||
|  |         style.configure("My.TFrame", background=self.bg_color)  # Frame style | ||||||
|  | 
 | ||||||
|  |         self.is_running = False | ||||||
|         self.start_time = None |         self.start_time = None | ||||||
|         self.tracking = False |         self.elapsed_time = 0 | ||||||
|         self.sessions = [] |         self.timer_id = None | ||||||
|         self.total_hours = 0 |         self.log_entries = [] | ||||||
|  |         self.project_file = "projects.txt" | ||||||
|  |         self.projects = self.load_projects() | ||||||
|  |         self.invoice_number = 1 | ||||||
|  |         self.company_name = "Your Company Name" | ||||||
|  |         self.company_address = "123 Main St, Anytown, USA" | ||||||
|  |         self.client_name = "Client Name" | ||||||
|  |         self.client_address = "Client Address" | ||||||
|  |         self.hourly_rate = 60.00 | ||||||
| 
 | 
 | ||||||
|         self.create_widgets() |         # UI elements | ||||||
|         self.load_sessions_from_csv() |         self.task_label = ttk.Label(root, text="Task Description:") | ||||||
|         self.update_total_hours() |         self.task_label.grid(row=0, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.task_entry = ttk.Entry(root, width=40) | ||||||
|  |         self.task_entry.grid(row=0, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
| 
 | 
 | ||||||
|     def create_widgets(self): |         self.project_label = ttk.Label(root, text="Project:") | ||||||
|         self.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12)) |         self.project_label.grid(row=1, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|         self.status_label.pack(pady=5) |         self.project_combo = ttk.Combobox(root, values=self.projects) | ||||||
|  |         self.project_combo.grid(row=1, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         if self.projects: | ||||||
|  |             self.project_combo.set(self.projects[0]) | ||||||
| 
 | 
 | ||||||
|         self.start_button = tk.Button( |         self.time_label = ttk.Label( | ||||||
|             self, text="Start Tracking", width=20, command=self.start_tracking |             root, text="00:00:00", font=(self.font_family, 36) | ||||||
|  |         )  # Larger time | ||||||
|  |         self.time_label.grid(row=2, column=0, columnspan=2, pady=15) | ||||||
|  | 
 | ||||||
|  |         self.start_stop_button = ttk.Button( | ||||||
|  |             root, text="Start", command=self.toggle_timer | ||||||
|         ) |         ) | ||||||
|         self.start_button.pack(pady=2) |         self.start_stop_button.grid(row=3, column=0, columnspan=2, pady=10) | ||||||
| 
 | 
 | ||||||
|         self.stop_button = tk.Button( |         self.log_label = ttk.Label(root, text="Log Entries:") | ||||||
|             self, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED |         self.log_label.grid(row=4, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|  | 
 | ||||||
|  |         self.log_text = tk.Text( | ||||||
|  |             root, | ||||||
|  |             height=10, | ||||||
|  |             width=50, | ||||||
|  |             bg="white", | ||||||
|  |             font=(self.font_family, self.font_size), | ||||||
|         ) |         ) | ||||||
|         self.stop_button.pack(pady=2) |         self.log_text.grid(row=5, column=0, columnspan=2, padx=10, pady=5) | ||||||
| 
 | 
 | ||||||
|         self.description_label = tk.Label(self, text="Work Description:") |         # Add Project UI | ||||||
|         self.description_label.pack(pady=1) |         self.new_project_label = ttk.Label(root, text="New Project:") | ||||||
|         self.description_entry = tk.Text(self, height=3, width=40) |         self.new_project_label.grid(row=6, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|         self.description_entry.pack(pady=1) |         self.new_project_entry = ttk.Entry(root, width=30) | ||||||
|  |         self.new_project_entry.grid(row=6, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
| 
 | 
 | ||||||
|         self.export_csv_button = tk.Button( |         self.add_project_button = ttk.Button( | ||||||
|             self, text="Export to CSV", width=20, command=self.export_sessions_csv |             root, text="Add Project", command=self.add_project | ||||||
|         ) |         ) | ||||||
|         self.export_csv_button.pack(pady=2) |         self.add_project_button.grid(row=7, column=0, columnspan=2, pady=5) | ||||||
| 
 | 
 | ||||||
|         self.export_pdf_button = tk.Button( |         # Button Grouping | ||||||
|             self, text="Export to PDF", width=20, command=self.export_sessions_pdf |         button_frame = ttk.Frame(root, padding=10, style="My.TFrame") | ||||||
|  |         button_frame.grid(row=8, column=0, columnspan=2, pady=10) | ||||||
|  | 
 | ||||||
|  |         self.export_csv_button = ttk.Button( | ||||||
|  |             button_frame, text="Export to CSV", command=self.export_to_csv | ||||||
|         ) |         ) | ||||||
|         self.export_pdf_button.pack(pady=2) |         self.export_csv_button.grid(row=0, column=0, padx=5, pady=5) | ||||||
| 
 | 
 | ||||||
|         self.total_hours_label = tk.Label(self, text="Total Hours Worked: 0.00", font=("Helvetica", 12)) |         self.export_pdf_button = ttk.Button( | ||||||
|         self.total_hours_label.pack(pady=5) |             button_frame, text="Export to PDF", command=self.export_to_pdf | ||||||
| 
 |  | ||||||
|         self.log_text = tk.Text(self, height=10, state=tk.DISABLED) |  | ||||||
|         self.log_text.pack(pady=5, padx=10, fill='both', expand=True) |  | ||||||
|          |  | ||||||
|         self.exit_button = tk.Button( |  | ||||||
|         self, text="Exit", width=10, command=self.exit_app |  | ||||||
|         ) |         ) | ||||||
|         self.exit_button.pack(pady=5) |         self.export_pdf_button.grid(row=0, column=1, padx=5, pady=5) | ||||||
| 
 | 
 | ||||||
|     def log_message(self, message): |         self.exit_button = ttk.Button(  # Create Exit button | ||||||
|         if hasattr(self, 'log_text'): |             button_frame, text="Exit", command=self.exit_app | ||||||
|             self.log_text.configure(state=tk.NORMAL) |  | ||||||
|             self.log_text.insert(tk.END, f"{message}\n") |  | ||||||
|             self.log_text.configure(state=tk.DISABLED) |  | ||||||
|             self.log_text.see(tk.END) |  | ||||||
| 
 |  | ||||||
|     def start_tracking(self): |  | ||||||
|         if self.tracking: |  | ||||||
|             messagebox.showwarning("Warning", "Already tracking!") |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         self.start_time = datetime.now() |  | ||||||
|         self.tracking = True |  | ||||||
|         self.status_label.config( |  | ||||||
|             text=f"Status: Tracking started at {self.start_time.strftime('%H:%M:%S')}" |  | ||||||
|         ) |         ) | ||||||
|         self.log_message(f"Started at: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") |         self.exit_button.grid(row=0, column=2, padx=5, pady=5)  # Place in button_frame | ||||||
|         self.start_button.config(state=tk.DISABLED) |  | ||||||
|         self.stop_button.config(state=tk.NORMAL) |  | ||||||
| 
 | 
 | ||||||
|     def stop_tracking(self): |         self.total_time_button = ttk.Button( | ||||||
|         if not self.tracking: |             root, | ||||||
|             messagebox.showwarning("Warning", "Not currently tracking!") |             text="Calculate Total Time", | ||||||
|             return |             command=self.calculate_total_time, | ||||||
| 
 |             width=20,  # Set specific width for this button | ||||||
|         end_time = datetime.now() |  | ||||||
|         duration = end_time - self.start_time |  | ||||||
|         description = self.description_entry.get("1.0", tk.END).strip() |  | ||||||
|         self.sessions.append((self.start_time, end_time, duration, description)) |  | ||||||
|         self.tracking = False |  | ||||||
|         self.status_label.config(text="Status: Not Tracking") |  | ||||||
|         decimal_hours = self.get_decimal_hours(duration) |  | ||||||
|         self.log_message( |  | ||||||
|             f"Stopped at: {end_time.strftime('%Y-%m-%d %H:%M:%S')}, Duration: {self.format_duration(duration)} " |  | ||||||
|             f"({decimal_hours:.2f} hours), Description: {description}" |  | ||||||
|         ) |         ) | ||||||
|         self.start_button.config(state=tk.NORMAL) |         self.total_time_button.grid(row=9, column=0, columnspan=2, pady=5) | ||||||
|         self.stop_button.config(state=tk.DISABLED) |  | ||||||
|         self.description_entry.delete("1.0", tk.END) |  | ||||||
| 
 | 
 | ||||||
|         self.update_total_hours() |         self.total_time_label = ttk.Label(root, text="Total Time: 00:00:00") | ||||||
|  |         self.total_time_label.grid(row=10, column=0, columnspan=2, pady=5) | ||||||
| 
 | 
 | ||||||
|     def format_duration(self, duration): |         # Settings UI | ||||||
|         total_seconds = int(duration.total_seconds()) |         self.company_name_label = ttk.Label(root, text="Company Name:") | ||||||
|         hours = total_seconds // 3600 |         self.company_name_label.grid(row=11, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|         minutes = (total_seconds % 3600) // 60 |         self.company_name_entry = ttk.Entry(root, width=30) | ||||||
|         seconds = total_seconds % 60 |         self.company_name_entry.grid(row=11, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
|         return f"{hours}:{minutes:02d}:{seconds:02d}" |         self.company_name_entry.insert(0, self.company_name) | ||||||
| 
 | 
 | ||||||
|     def get_decimal_hours(self, duration): |         self.company_address_label = ttk.Label(root, text="Company Address:") | ||||||
|         total_hours = duration.total_seconds() / 3600 |         self.company_address_label.grid(row=12, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|         return total_hours |         self.company_address_entry = ttk.Entry(root, width=30) | ||||||
|  |         self.company_address_entry.grid(row=12, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.company_address_entry.insert(0, self.company_address) | ||||||
| 
 | 
 | ||||||
|     def export_sessions_csv(self): |         self.client_name_label = ttk.Label(root, text="Client Name:") | ||||||
|         if not self.sessions: |         self.client_name_label.grid(row=13, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|             messagebox.showinfo("Info", "No sessions to export.") |         self.client_name_entry = ttk.Entry(root, width=30) | ||||||
|             return |         self.client_name_entry.grid(row=13, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.client_name_entry.insert(0, self.client_name) | ||||||
| 
 | 
 | ||||||
|         filename = "working_sessions.csv" |         self.client_address_label = ttk.Label(root, text="Client Address:") | ||||||
|  |         self.client_address_label.grid(row=14, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.client_address_entry = ttk.Entry(root, width=30) | ||||||
|  |         self.client_address_entry.grid(row=14, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.client_address_entry.insert(0, self.client_address) | ||||||
|  | 
 | ||||||
|  |         self.hourly_rate_label = ttk.Label(root, text="Hourly Rate:") | ||||||
|  |         self.hourly_rate_label.grid(row=15, column=0, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.hourly_rate_entry = ttk.Entry(root, width=10) | ||||||
|  |         self.hourly_rate_entry.grid(row=15, column=1, sticky=tk.W, padx=10, pady=5) | ||||||
|  |         self.hourly_rate_entry.insert(0, str(self.hourly_rate)) | ||||||
|  | 
 | ||||||
|  |         self.update_settings_button = ttk.Button( | ||||||
|  |             root, text="Update Settings", command=self.update_settings | ||||||
|  |         ) | ||||||
|  |         self.update_settings_button.grid(row=16, column=0, columnspan=2, pady=10) | ||||||
|  | 
 | ||||||
|  |         # Configure grid weights to make the layout expand | ||||||
|  |         for i in range(17): | ||||||
|  |             root.grid_rowconfigure(i, weight=1) | ||||||
|  |         root.grid_columnconfigure(0, weight=1) | ||||||
|  |         root.grid_columnconfigure(1, weight=1) | ||||||
|  | 
 | ||||||
|  |     def load_projects(self): | ||||||
|         try: |         try: | ||||||
|             with open(filename, mode="w", newline="") as csvfile: |             with open(self.project_file, "r") as f: | ||||||
|                 writer = csv.writer(csvfile) |                 projects = [line.strip() for line in f] | ||||||
|                 writer.writerow([ |             return projects | ||||||
|                     "Start Time", "End Time", |  | ||||||
|                     "Duration (H:MM:SS)", "Decimal Hours", "Description" |  | ||||||
|                 ]) |  | ||||||
|                 for start, end, duration, description in self.sessions: |  | ||||||
|                     writer.writerow([ |  | ||||||
|                         start.strftime("%Y-%m-%d %H:%M:%S"), |  | ||||||
|                         end.strftime("%Y-%m-%d %H:%M:%S"), |  | ||||||
|                         self.format_duration(duration), |  | ||||||
|                         f"{self.get_decimal_hours(duration):.2f}", |  | ||||||
|                         description |  | ||||||
|                     ]) |  | ||||||
|                 writer.writerow(["", "", "", "Total Hours", f"{self.total_hours:.2f}"]) |  | ||||||
|             messagebox.showinfo("Export Successful", f"Sessions exported to {filename}") |  | ||||||
|         except Exception as e: |  | ||||||
|             messagebox.showerror("Error", f"Failed to export: {e}") |  | ||||||
| 
 |  | ||||||
|     def export_sessions_pdf(self): |  | ||||||
|         if not self.sessions: |  | ||||||
|             messagebox.showinfo("Info", "No sessions to export.") |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         filename = "working_sessions.pdf" |  | ||||||
|         try: |  | ||||||
|             doc = SimpleDocTemplate(filename, pagesize=letter) |  | ||||||
|             elements = [] |  | ||||||
| 
 |  | ||||||
|             data = [ |  | ||||||
|                 ["Start Time", "End Time", "Duration (H:MM:SS)", "Decimal Hours", "Description"] |  | ||||||
|             ] |  | ||||||
|             for start, end, duration, description in self.sessions: |  | ||||||
|                 data.append([ |  | ||||||
|                     start.strftime("%Y-%m-%d %H:%M:%S"), |  | ||||||
|                     end.strftime("%Y-%m-%d %H:%M:%S"), |  | ||||||
|                     self.format_duration(duration), |  | ||||||
|                     f"{self.get_decimal_hours(duration):.2f}", |  | ||||||
|                     description |  | ||||||
|                 ]) |  | ||||||
| 
 |  | ||||||
|             data.append(["", "", "", "Total Hours", f"{self.total_hours:.2f}"]) |  | ||||||
| 
 |  | ||||||
|             table = Table(data) |  | ||||||
|             style = TableStyle([ |  | ||||||
|                 ('BACKGROUND', (0, 0), (-1, 0), colors.grey), |  | ||||||
|                 ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), |  | ||||||
|                 ('ALIGN', (0, 0), (-1, -1), 'CENTER'), |  | ||||||
|                 ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), |  | ||||||
|                 ('BOTTOMPADDING', (0, 0), (-1, 0), 12), |  | ||||||
|                 ('BACKGROUND', (0, 1), (-1, -1), colors.beige), |  | ||||||
|                 ('GRID', (0, 0), (-1, -1), 1, colors.black) |  | ||||||
|             ]) |  | ||||||
|             table.setStyle(style) |  | ||||||
|             elements.append(table) |  | ||||||
| 
 |  | ||||||
|             doc.build(elements) |  | ||||||
|             messagebox.showinfo("Export Successful", f"Sessions exported to {filename}") |  | ||||||
|         except Exception as e: |  | ||||||
|             messagebox.showerror("Error", f"Failed to export: {e}") |  | ||||||
| 
 |  | ||||||
|     def load_sessions_from_csv(self): |  | ||||||
|         filename = "working_sessions.csv" |  | ||||||
|         if os.path.exists(filename): |  | ||||||
|             try: |  | ||||||
|                 with open(filename, mode="r") as csvfile: |  | ||||||
|                     reader = csv.reader(csvfile) |  | ||||||
|                     header = next(reader, None) |  | ||||||
|                     row_number = 1 |  | ||||||
|                     total_hours_loaded = False |  | ||||||
| 
 |  | ||||||
|                     for row in reader: |  | ||||||
|                         if not row:  |  | ||||||
|                             print(f"Skipping empty row: {row_number}") |  | ||||||
|                             row_number += 1 |  | ||||||
|                             continue |  | ||||||
| 
 |  | ||||||
|                         if len(row) == 5: |  | ||||||
|                             start_time_str, end_time_str, duration_str, decimal_hours_str, description = row |  | ||||||
|                         elif len(row) == 4: |  | ||||||
|                             if row[0] == '' and row[1] == '' and row[2] == '' and row[3] == 'Total Hours': |  | ||||||
|                                 try: |  | ||||||
|                                     self.total_hours = float(row[4]) |  | ||||||
|                                     self.update_total_hours() |  | ||||||
|                                     total_hours_loaded = True |  | ||||||
|                                     print(f"Total hours loaded from row {row_number}: {self.total_hours}") |  | ||||||
|                                     row_number += 1 |  | ||||||
|                                     continue |  | ||||||
| 
 |  | ||||||
|                                 except ValueError: |  | ||||||
|                                     print(f"Skipping total hours row {row_number} due to parsing error") |  | ||||||
|                                     row_number += 1 |  | ||||||
|                                     continue |  | ||||||
| 
 |  | ||||||
|                             start_time_str, end_time_str, duration_str, decimal_hours_str = row |  | ||||||
|                             description = "" |  | ||||||
|                         else: |  | ||||||
|                             print(f"Skipping row with unexpected number of columns ({len(row)}): {row_number}") |  | ||||||
|                             row_number += 1 |  | ||||||
|                             continue |  | ||||||
| 
 |  | ||||||
|                         if not start_time_str or not end_time_str: |  | ||||||
|                             print(f"Skipping row with missing time data: {row_number}") |  | ||||||
|                             row_number += 1 |  | ||||||
|                             continue |  | ||||||
| 
 |  | ||||||
|                         try: |  | ||||||
|                             start_time = datetime.strptime(start_time_str, "%Y-%m-%d %H:%M:%S") |  | ||||||
|                             end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S") |  | ||||||
|                             duration = self.parse_duration(duration_str) |  | ||||||
|                             self.sessions.append((start_time, end_time, duration, description)) |  | ||||||
|                             self.log_message( |  | ||||||
|                                 f"Loaded: {start_time.strftime('%Y-%m-%d %H:%M:%S')} - {end_time.strftime('%Y-%m-%d %H:%M:%S')}, " |  | ||||||
|                                 f"Duration: {duration_str}, Description: {description}" |  | ||||||
|                             ) |  | ||||||
|                         except ValueError as ve: |  | ||||||
|                             print(f"Skipping row {row_number} due to parsing error: {ve}") |  | ||||||
|                         row_number += 1  |  | ||||||
| 
 |  | ||||||
|                     if not total_hours_loaded: |  | ||||||
|                          self.update_total_hours()  |  | ||||||
|                          print("Total hours row was not found so calculating from current rows!") |  | ||||||
|         except FileNotFoundError: |         except FileNotFoundError: | ||||||
|                 messagebox.showinfo("Info", "No CSV file found. Starting with a new session.") |             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: |         except Exception as e: | ||||||
|                 messagebox.showerror("Error", f"Failed to load sessions from CSV: {e}") |             print(f"Error saving projects: {e}") | ||||||
| 
 | 
 | ||||||
|     def parse_duration(self, duration_str): |     def add_project(self): | ||||||
|         hours, minutes, seconds = map(int, duration_str.split(':')) |         new_project = self.new_project_entry.get().strip() | ||||||
|         return timedelta(hours=hours, minutes=minutes, seconds=seconds) |         if new_project and new_project not in self.projects: | ||||||
|  |             self.projects.append(new_project) | ||||||
|  |             self.project_combo["values"] = self.projects  # Update Combobox | ||||||
|  |             self.project_combo.set(new_project)  # Set to the new project | ||||||
|  |             self.save_projects() | ||||||
|  |             self.new_project_entry.delete(0, tk.END)  # Clear the entry | ||||||
|  |         elif new_project in self.projects: | ||||||
|  |             print("Project already exists.")  # Replace with a GUI message box | ||||||
|  |         else: | ||||||
|  |             print("Project name cannot be empty.")  # Replace with a GUI message box | ||||||
| 
 | 
 | ||||||
|     def update_total_hours(self): |     def update_settings(self): | ||||||
|         total_duration = timedelta() |         self.company_name = self.company_name_entry.get() | ||||||
|         for start, end, duration, description in self.sessions: |         self.company_address = self.company_address_entry.get() | ||||||
|             total_duration += duration |         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.")  # Replace with GUI message | ||||||
|  |             self.hourly_rate = 50.00  # Revert to default, or handle the error | ||||||
| 
 | 
 | ||||||
|         self.total_hours = total_duration.total_seconds() / 3600 |         # OPTIONAL: Save settings to a file here (e.g., JSON) | ||||||
|         self.total_hours_label.config(text=f"Total Hours Worked: {self.total_hours:.2f}") | 
 | ||||||
|  |     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.config(text="Stop") | ||||||
|  |         self.start_time = time.time() | ||||||
|  |         self.update_timer() | ||||||
|  | 
 | ||||||
|  |     def stop_timer(self): | ||||||
|  |         self.is_running = False | ||||||
|  |         self.start_stop_button.config(text="Start") | ||||||
|  |         self.root.after_cancel(self.timer_id) | ||||||
|  |         self.log_time_entry() | ||||||
|  | 
 | ||||||
|  |     def update_timer(self): | ||||||
|  |         if self.is_running: | ||||||
|  |             self.elapsed_time = time.time() - self.start_time | ||||||
|  |             minutes, seconds = divmod(self.elapsed_time, 60) | ||||||
|  |             hours, minutes = divmod(minutes, 60) | ||||||
|  |             time_str = "{:02d}:{:02d}:{:02d}".format( | ||||||
|  |                 int(hours), int(minutes), int(seconds) | ||||||
|  |             ) | ||||||
|  |             self.time_label.config(text=time_str) | ||||||
|  |             self.timer_id = self.root.after(100, self.update_timer) | ||||||
|  | 
 | ||||||
|  |     def log_time_entry(self): | ||||||
|  |         end_time = datetime.datetime.now() | ||||||
|  |         task_description = self.task_entry.get() | ||||||
|  |         project = self.project_combo.get() | ||||||
|  |         duration = self.elapsed_time | ||||||
|  |         start_time_str = end_time - datetime.timedelta(seconds=duration) | ||||||
|  | 
 | ||||||
|  |         entry = { | ||||||
|  |             "task": task_description, | ||||||
|  |             "project": project, | ||||||
|  |             "start_time": start_time_str.strftime("%Y-%m-%d %H:%M:%S"), | ||||||
|  |             "end_time": end_time.strftime("%Y-%m-%d %H:%M:%S"), | ||||||
|  |             "duration": round(duration, 2), | ||||||
|  |         } | ||||||
|  |         self.log_entries.append(entry) | ||||||
|  |         self.update_log_display() | ||||||
|  |         self.elapsed_time = 0 | ||||||
|  |         self.time_label.config(text="00:00:00") | ||||||
|  | 
 | ||||||
|  |     def update_log_display(self): | ||||||
|  |         self.log_text.delete("1.0", tk.END) | ||||||
|  |         for entry in self.log_entries: | ||||||
|  |             self.log_text.insert(tk.END, f"Task: {entry['task']}\n") | ||||||
|  |             self.log_text.insert(tk.END, f"Project: {entry['project']}\n") | ||||||
|  |             self.log_text.insert(tk.END, f"Start: {entry['start_time']}\n") | ||||||
|  |             self.log_text.insert(tk.END, f"End: {entry['end_time']}\n") | ||||||
|  |             self.log_text.insert(tk.END, f"Duration: {entry['duration']} seconds\n") | ||||||
|  |             self.log_text.insert(tk.END, "-" * 20 + "\n") | ||||||
|  | 
 | ||||||
|  |     def export_to_csv(self): | ||||||
|  |         try: | ||||||
|  |             with open("time_entries.csv", "w", newline="") as csvfile: | ||||||
|  |                 fieldnames = ["task", "project", "start_time", "end_time", "duration"] | ||||||
|  |                 writer = csv.DictWriter(csvfile, fieldnames=fieldnames) | ||||||
|  |                 writer.writeheader() | ||||||
|  |                 for entry in self.log_entries: | ||||||
|  |                     writer.writerow(entry) | ||||||
|  |             print("Exported to CSV successfully!") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"Error exporting to CSV: {e}") | ||||||
|  | 
 | ||||||
|  |     def export_to_pdf(self): | ||||||
|  |         try: | ||||||
|  |             filename = f"invoice_{self.invoice_number}.pdf" | ||||||
|  |             c = canvas.Canvas(filename, pagesize=letter) | ||||||
|  |             styles = getSampleStyleSheet() | ||||||
|  | 
 | ||||||
|  |             # --- Header --- | ||||||
|  |             c.setFont("Helvetica-Bold", 16) | ||||||
|  |             c.drawString(inch, 7.5 * inch, self.company_name) | ||||||
|  |             c.setFont("Helvetica", 10) | ||||||
|  |             c.drawString(inch, 7.3 * inch, self.company_address) | ||||||
|  | 
 | ||||||
|  |             c.setFont("Helvetica-Bold", 12) | ||||||
|  |             c.drawString(4.5 * inch, 7.5 * inch, "Invoice") | ||||||
|  |             c.setFont("Helvetica", 10) | ||||||
|  |             c.drawString( | ||||||
|  |                 4.5 * inch, 7.3 * inch, f"Invoice Number: {self.invoice_number}" | ||||||
|  |             ) | ||||||
|  |             current_date = datetime.datetime.now().strftime("%Y-%m-%d") | ||||||
|  |             c.drawString(4.5 * inch, 7.1 * inch, f"Date: {current_date}") | ||||||
|  | 
 | ||||||
|  |             # --- Client Info --- | ||||||
|  |             c.setFont("Helvetica-Bold", 12) | ||||||
|  |             c.drawString(inch, 6.5 * inch, "Bill To:") | ||||||
|  |             c.setFont("Helvetica", 10) | ||||||
|  |             c.drawString(inch, 6.3 * inch, self.client_name) | ||||||
|  |             c.drawString(inch, 6.1 * inch, self.client_address) | ||||||
|  | 
 | ||||||
|  |             # --- Table --- | ||||||
|  |             data = [["Task", "Project", "Hours", "Rate", "Total"]] | ||||||
|  |             total_amount = 0 | ||||||
|  | 
 | ||||||
|  |             for entry in self.log_entries: | ||||||
|  |                 hours = entry["duration"] / 3600 | ||||||
|  |                 line_total = hours * self.hourly_rate | ||||||
|  |                 total_amount += line_total | ||||||
|  |                 data.append( | ||||||
|  |                     [ | ||||||
|  |                         entry["task"], | ||||||
|  |                         entry["project"], | ||||||
|  |                         f"{hours:.2f}", | ||||||
|  |                         f"${self.hourly_rate:.2f}", | ||||||
|  |                         f"${line_total:.2f}", | ||||||
|  |                     ] | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             table = Table(data, colWidths=[1.5 * inch, 1.5 * inch, inch, inch, inch]) | ||||||
|  |             style = TableStyle( | ||||||
|  |                 [ | ||||||
|  |                     ("BACKGROUND", (0, 0), (-1, 0), colors.grey), | ||||||
|  |                     ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke), | ||||||
|  |                     ("ALIGN", (0, 0), (-1, -1), "CENTER"), | ||||||
|  |                     ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), | ||||||
|  |                     ("BOTTOMPADDING", (0, 0), (-1, 0), 12), | ||||||
|  |                     ("BACKGROUND", (0, 1), (-1, -1), colors.beige), | ||||||
|  |                     ("GRID", (0, 0), (-1, -1), 1, colors.black), | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |             table.setStyle(style) | ||||||
|  |             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.config(text=f"Total Time: {time_str}") | ||||||
|  | 
 | ||||||
|  |     def exit_app(self):  # Define the exit_app method | ||||||
|  |         self.root.destroy()  # Close the Tkinter window | ||||||
|  |         sys.exit() | ||||||
| 
 | 
 | ||||||
|     def exit_app(self): |  | ||||||
|         self.destroy() |  | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     app = TimeLogix() |     root = tk.Tk() | ||||||
|     app.mainloop() |     app = TimeLogix(root) | ||||||
|  |     root.mainloop() | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Blake Ridgway
						Blake Ridgway