overhaul of timelogix app
This commit is contained in:
		
							parent
							
								
									266d21b540
								
							
						
					
					
						commit
						34d7c4c9dd
					
				
					 1 changed files with 355 additions and 225 deletions
				
			
		
							
								
								
									
										580
									
								
								time_logix.py
									
										
									
									
									
								
							
							
						
						
									
										580
									
								
								time_logix.py
									
										
									
									
									
								
							|  | @ -1,271 +1,401 @@ | |||
| import tkinter as tk | ||||
| from tkinter import messagebox | ||||
| from datetime import datetime, timedelta | ||||
| from tkinter import ttk | ||||
| import time | ||||
| import datetime | ||||
| import csv | ||||
| import os | ||||
| 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.platypus import Table, TableStyle, Paragraph | ||||
| from reportlab.lib.styles import getSampleStyleSheet | ||||
| 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.tracking = False | ||||
|         self.sessions = [] | ||||
|         self.total_hours = 0 | ||||
|         self.elapsed_time = 0 | ||||
|         self.timer_id = None | ||||
|         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() | ||||
|         self.load_sessions_from_csv() | ||||
|         self.update_total_hours() | ||||
|         # UI elements | ||||
|         self.task_label = ttk.Label(root, text="Task Description:") | ||||
|         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.status_label = tk.Label(self, text="Status: Not Tracking", font=("Helvetica", 12)) | ||||
|         self.status_label.pack(pady=5) | ||||
|         self.project_label = ttk.Label(root, text="Project:") | ||||
|         self.project_label.grid(row=1, column=0, sticky=tk.W, padx=10, 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, text="Start Tracking", width=20, command=self.start_tracking | ||||
|         self.time_label = ttk.Label( | ||||
|             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, text="Stop Tracking", width=20, command=self.stop_tracking, state=tk.DISABLED | ||||
|         self.log_label = ttk.Label(root, text="Log Entries:") | ||||
|         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:") | ||||
|         self.description_label.pack(pady=1) | ||||
|         self.description_entry = tk.Text(self, height=3, width=40) | ||||
|         self.description_entry.pack(pady=1) | ||||
|         # Add Project UI | ||||
|         self.new_project_label = ttk.Label(root, text="New Project:") | ||||
|         self.new_project_label.grid(row=6, column=0, sticky=tk.W, padx=10, pady=5) | ||||
|         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, text="Export to CSV", width=20, command=self.export_sessions_csv | ||||
|         self.add_project_button = ttk.Button( | ||||
|             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( | ||||
|             self, text="Export to PDF", width=20, command=self.export_sessions_pdf | ||||
|         # Button Grouping | ||||
|         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.total_hours_label.pack(pady=5) | ||||
| 
 | ||||
|         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.export_pdf_button = ttk.Button( | ||||
|             button_frame, text="Export to PDF", command=self.export_to_pdf | ||||
|         ) | ||||
|         self.exit_button.pack(pady=5) | ||||
|         self.export_pdf_button.grid(row=0, column=1, padx=5, pady=5) | ||||
| 
 | ||||
|     def log_message(self, message): | ||||
|         if hasattr(self, 'log_text'): | ||||
|             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.exit_button = ttk.Button(  # Create Exit button | ||||
|             button_frame, text="Exit", command=self.exit_app | ||||
|         ) | ||||
|         self.log_message(f"Started at: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}") | ||||
|         self.start_button.config(state=tk.DISABLED) | ||||
|         self.stop_button.config(state=tk.NORMAL) | ||||
|         self.exit_button.grid(row=0, column=2, padx=5, pady=5)  # Place in button_frame | ||||
| 
 | ||||
|     def stop_tracking(self): | ||||
|         if not self.tracking: | ||||
|             messagebox.showwarning("Warning", "Not currently tracking!") | ||||
|             return | ||||
| 
 | ||||
|         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.total_time_button = ttk.Button( | ||||
|             root, | ||||
|             text="Calculate Total Time", | ||||
|             command=self.calculate_total_time, | ||||
|             width=20,  # Set specific width for this button | ||||
|         ) | ||||
|         self.start_button.config(state=tk.NORMAL) | ||||
|         self.stop_button.config(state=tk.DISABLED) | ||||
|         self.description_entry.delete("1.0", tk.END) | ||||
|         self.total_time_button.grid(row=9, column=0, columnspan=2, pady=5) | ||||
| 
 | ||||
|         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): | ||||
|         total_seconds = int(duration.total_seconds()) | ||||
|         hours = total_seconds // 3600 | ||||
|         minutes = (total_seconds % 3600) // 60 | ||||
|         seconds = total_seconds % 60 | ||||
|         return f"{hours}:{minutes:02d}:{seconds:02d}" | ||||
|         # Settings UI | ||||
|         self.company_name_label = ttk.Label(root, text="Company Name:") | ||||
|         self.company_name_label.grid(row=11, column=0, sticky=tk.W, padx=10, pady=5) | ||||
|         self.company_name_entry = ttk.Entry(root, width=30) | ||||
|         self.company_name_entry.grid(row=11, column=1, sticky=tk.W, padx=10, pady=5) | ||||
|         self.company_name_entry.insert(0, self.company_name) | ||||
| 
 | ||||
|     def get_decimal_hours(self, duration): | ||||
|         total_hours = duration.total_seconds() / 3600 | ||||
|         return total_hours | ||||
|         self.company_address_label = ttk.Label(root, text="Company Address:") | ||||
|         self.company_address_label.grid(row=12, column=0, sticky=tk.W, padx=10, pady=5) | ||||
|         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): | ||||
|         if not self.sessions: | ||||
|             messagebox.showinfo("Info", "No sessions to export.") | ||||
|             return | ||||
|         self.client_name_label = ttk.Label(root, text="Client Name:") | ||||
|         self.client_name_label.grid(row=13, column=0, sticky=tk.W, padx=10, pady=5) | ||||
|         self.client_name_entry = ttk.Entry(root, width=30) | ||||
|         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: | ||||
|             with open(filename, mode="w", newline="") as csvfile: | ||||
|                 writer = csv.writer(csvfile) | ||||
|                 writer.writerow([ | ||||
|                     "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}") | ||||
|             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: | ||||
|             messagebox.showerror("Error", f"Failed to export: {e}") | ||||
|             print(f"Error saving projects: {e}") | ||||
| 
 | ||||
|     def export_sessions_pdf(self): | ||||
|         if not self.sessions: | ||||
|             messagebox.showinfo("Info", "No sessions to export.") | ||||
|             return | ||||
|     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["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 | ||||
| 
 | ||||
|         filename = "working_sessions.pdf" | ||||
|     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: | ||||
|             doc = SimpleDocTemplate(filename, pagesize=letter) | ||||
|             elements = [] | ||||
|             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 | ||||
| 
 | ||||
|             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 | ||||
|                 ]) | ||||
|         # OPTIONAL: Save settings to a file here (e.g., JSON) | ||||
| 
 | ||||
|             data.append(["", "", "", "Total Hours", f"{self.total_hours:.2f}"]) | ||||
|     def toggle_timer(self): | ||||
|         if self.is_running: | ||||
|             self.stop_timer() | ||||
|         else: | ||||
|             self.start_timer() | ||||
| 
 | ||||
|             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) | ||||
|             ]) | ||||
|     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) | ||||
|             elements.append(table) | ||||
|             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 | ||||
| 
 | ||||
|             doc.build(elements) | ||||
|             messagebox.showinfo("Export Successful", f"Sessions exported to {filename}") | ||||
|         except Exception as e: | ||||
|             messagebox.showerror("Error", f"Failed to export: {e}") | ||||
|             print(f"Error exporting to PDF: {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 | ||||
|     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}") | ||||
| 
 | ||||
|                     for row in reader: | ||||
|                         if not row:  | ||||
|                             print(f"Skipping empty row: {row_number}") | ||||
|                             row_number += 1 | ||||
|                             continue | ||||
|     def exit_app(self):  # Define the exit_app method | ||||
|         self.root.destroy()  # Close the Tkinter window | ||||
|         sys.exit() | ||||
| 
 | ||||
|                         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: | ||||
|                 messagebox.showinfo("Info", "No CSV file found. Starting with a new session.") | ||||
|             except Exception as e: | ||||
|                 messagebox.showerror("Error", f"Failed to load sessions from CSV: {e}") | ||||
|                | ||||
|     def parse_duration(self, duration_str): | ||||
|         hours, minutes, seconds = map(int, duration_str.split(':')) | ||||
|         return timedelta(hours=hours, minutes=minutes, seconds=seconds) | ||||
| 
 | ||||
|     def update_total_hours(self): | ||||
|         total_duration = timedelta() | ||||
|         for start, end, duration, description in self.sessions: | ||||
|             total_duration += duration | ||||
| 
 | ||||
|         self.total_hours = total_duration.total_seconds() / 3600 | ||||
|         self.total_hours_label.config(text=f"Total Hours Worked: {self.total_hours:.2f}") | ||||
| 
 | ||||
|     def exit_app(self): | ||||
|         self.destroy() | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     app = TimeLogix() | ||||
|     app.mainloop() | ||||
|     root = tk.Tk() | ||||
|     app = TimeLogix(root) | ||||
|     root.mainloop() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Blake Ridgway
						Blake Ridgway