From 681aa727a884a2db58c171d7301ca16eb1641c90 Mon Sep 17 00:00:00 2001 From: Skyler Clark Date: Thu, 21 May 2026 10:35:01 -0500 Subject: [PATCH 1/6] Update README.md Obtained by dipsherlock, updated features list --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5586f15..fecb51b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ It’s designed to be lightweight, precise, and user-friendly β€” perfect for sm - πŸ• Punch In / Punch Out buttons for accurate time tracking - πŸ“Š View logged time directly in the app - πŸ“… View hour summaries: **daily**, **weekly**, and **all-time** -- πŸ“ Automatic CSV export of time logs +- πŸ“ Export time-filtered CSV reports of time logs: **weekly**, **monthly**, **yearly**, and **all-time** - πŸ–₯️ Simple Tkinter-based GUI, easy to run on any system with Python installed --- @@ -23,24 +23,35 @@ It’s designed to be lightweight, precise, and user-friendly β€” perfect for sm ### Installation Clone the repository: ```bash -git clone https://github.com/zegron/timeclock.git +git clone https://github.com/dipsherlock/timeclock.git cd timeclock +``` -Run the App +### Run the App Simply run the Python script: +```bash python time_clock.py +``` The time clock window will open, allowing you to punch in/out and view your hours. -πŸ“‚ Output +--- + +## πŸ“‚ Output -All time logs are automatically saved as CSV files in the project directory. +All time logs are automatically saved in a CSV file in the project directory. +Export Report creates a time-filtered copy of the CSV, free to edit and share without affecting the original CSV file. You can open them in Excel, Google Sheets, or any spreadsheet tool for further reporting. -πŸ§‘β€πŸ’» Author +--- + +## πŸ§‘β€πŸ’» Authors + +dipsherlock +πŸ“§ skylertclark@gmail.com zegron πŸ“§ matt@onetakemedia.net \ No newline at end of file From d568558266fd96567c112245854a8b86b30d9b8c Mon Sep 17 00:00:00 2001 From: dipsherlock Date: Thu, 21 May 2026 18:47:36 -0500 Subject: [PATCH 2/6] Export implementation, QOL improvements --- CHANGES.md | 39 +++++- time_clock.py | 320 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 340 insertions(+), 19 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d8df7c3..5594411 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,42 @@ All notable changes to this project will be documented in this file. + +--- + +## [v1.2.2] – 2026-05-19 +### Added +- **Dark and Light mode support:** Created a **Style** menu with sub-menu options **Dark Mode** and **Light Mode** +- **Alternate clock formats:** Added a **Clock Format** sub-menu to the Style menu, with options **12 Hour** and **24 Hour**. + +### Changed +- Switched date format from "Year/Month/Day" to "Month/Day/Year" app-wide. + +### Author +dipsherlock () + +--- + +## [v1.2.1] – 2026-05-17 +### Added +- **'Export Log' implementation:** + - Finished implementing **Export Log** in the File menu, including sub-menu options **This Week, This Month, This Year,** and **All Time**. + - User selects an option to save a time-filtered copy of the CSV file, with the default name "time_report_week/month/year/all_MMDDYYYY.csv". + + > **Example:** time_report_week_05172026.csv + +- **Debugging tool:** Added Developer menu, including option **Clear Log** for debugging purposes. + +### Changed +- Simplified main page by migrating buttons **View Hours** and **View Log** to the File menu. +- Updated About page to include myself as a derivative author. +- Switched wording from "**punch** in/out" to "**clock** in/out". + +### Author +dipsherlock () + +--- + ## [v1.1.5] – 2025-10-06 ### Added - **Unified version control system:** @@ -16,6 +52,7 @@ All notable changes to this project will be documented in this file. ### Author zegron () +--- ## [v1.1.4] – 2025-10-06 ### Added @@ -31,6 +68,7 @@ zegron () ### Author zegron () +--- ## [v1.1.3] – 2025-10-06 ### Added @@ -76,4 +114,3 @@ zegron () - Basic Punch In / Punch Out functionality. - CSV-based time logging. - Simple Tkinter GUI. - diff --git a/time_clock.py b/time_clock.py index 99fb775..ce041f9 100644 --- a/time_clock.py +++ b/time_clock.py @@ -1,17 +1,20 @@ import tkinter as tk -from tkinter import messagebox, Menu +from tkinter import messagebox, Menu, filedialog import csv import os from datetime import datetime, timedelta # --- App Metadata --- -APP_VERSION = "1.1.5" # Change this line only to update version everywhere -APP_NAME = f"Quick Time Clock v{APP_VERSION}" +APP_VERSION = "1.2.2" # Change this line only to update version everywhere +APP_NAME = f"Time Clock v{APP_VERSION}" AUTHOR = "zegron" EMAIL = "matt@onetakemedia.net" +AUTHOR_DER = "dipsherlock" +EMAIL_DER = "skylertclark@gmail.com" LICENSE_SNIPPET = "MIT License Β© 2025 zegron" FILENAME = "time_log.csv" +CLOCK_FORMAT = "12h" # --- Core Functions --- @@ -39,15 +42,18 @@ def log_action(action): # Detect invalid sequence if action == "Punch In" and last_action == "Punch In": - messagebox.showwarning("Warning", "You must Punch Out before punching in again.") + messagebox.showwarning("Warning", "You must Clock Out before clocking in again.") return if action == "Punch Out" and last_action != "Punch In": - messagebox.showwarning("Warning", "You must Punch In before punching out.") + messagebox.showwarning("Warning", "You must Clock In before clocking out.") return - now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + now = datetime.now().strftime("%m-%d-%Y %H:%M:%S") + file_exists = os.path.exists(FILENAME) with open(FILENAME, "a", newline="") as f: writer = csv.writer(f) + if not file_exists: + writer.writerow(["Timestamp", "Action"]) writer.writerow([now, action]) messagebox.showinfo("Logged", f"{action} at {now}") @@ -62,6 +68,151 @@ def view_log(): messagebox.showinfo("Time Log", log if log else "Log is empty.") +def export_report(period="all"): + """Export the log file to a user-selected CSV location.""" + if not os.path.exists(FILENAME): + messagebox.showwarning("Error", "No log file found.") + return + + try: + export_path = filedialog.asksaveasfilename( + defaultextension=".csv", + filetypes=[("CSV Files", "*.csv")], + initialfile=f"time_report_{period}_{datetime.now().strftime('%m%d%Y')}.csv", + title="Export Time Log" + ) + + if not export_path: + return # User cancelled + + entries = [] + + with open(FILENAME, "r", newline="") as f: + reader = csv.reader(f) + + next(reader, None) + + for row in reader: + if len(row) < 2: + continue + + timestamp, action = row + dt = datetime.strptime(timestamp, "%m-%d-%Y %H:%M:%S") + + entries.append((dt, action)) + + # Export Filter + today = datetime.now() + if period == "week": + # Sunday-start week + days_since_sunday = (today.weekday() + 1) % 7 + start_date = (today - timedelta(days=days_since_sunday)).replace(hour=0, minute=0, second=0, microsecond=0) + elif period == "month": + start_date = today.replace(day=1) + elif period == "year": + start_date = today.replace(month=1, day=1) + else: + start_date = None + + # Build sessions + sessions = [] + punch_in_time = None + + for dt, action in entries: + if action == "Punch In": + punch_in_time = dt + + elif action == "Punch Out" and punch_in_time: + sessions.append((punch_in_time, dt)) + punch_in_time = None + + if not sessions: + messagebox.showinfo("Export", "No completed sessions found.") + return + + total_hours = 0 + + # Filter sessions + filtered_sessions = [] + for start, end in sessions: + if start_date: + if start < start_date: + continue + filtered_sessions.append((start, end)) + + + # Write export + with open(export_path, "w", newline="") as f: + writer = csv.writer(f) + + # Header row + writer.writerow(["Date", "Clock In", "Clock Out", "Hours"]) + + for start, end in filtered_sessions: + hours = round((end - start).total_seconds() / 3600, 2) + total_hours += hours + + writer.writerow([ + start.strftime("%m-%d-%Y"), + start.strftime("%H:%M:%S"), + end.strftime("%H:%M:%S"), + f"{hours:.2f}" + ]) + + # Blank line + writer.writerow([]) + + # Footer / summary row + writer.writerow([ + "TOTAL", + "", + "", + f"{total_hours:.2f}" + ]) + + messagebox.showinfo( + "Export Successful", + f"Report exported successfully:\n{export_path}" + ) + + except Exception as e: + messagebox.showerror( + "Export Failed", + f"An error occurred:\n{e}" + ) + +def clear_log(): + """Delete the current log file after confirmation.""" + + if not os.path.exists(FILENAME): + messagebox.showinfo("Clear Log", "No log file exists.") + return + + confirm = messagebox.askyesno( + "Clear Log", + "This will permanently delete all logged time entries.\n\nContinue?" + ) + + if not confirm: + return + + try: + os.remove(FILENAME) + with open(FILENAME, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["Timestamp", "Action"]) + + messagebox.showinfo( + "Clear Log", + "Log file deleted successfully." + ) + + except Exception as e: + messagebox.showerror( + "Error", + f"Could not delete log file:\n{e}" + ) + def calculate_hours(): """Read the CSV file and calculate total hours.""" if not os.path.exists(FILENAME): @@ -71,9 +222,10 @@ def calculate_hours(): entries = [] with open(FILENAME, "r") as f: reader = csv.reader(f) + next(reader, None) # Skip header for row in reader: timestamp, action = row - dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") + dt = datetime.strptime(timestamp, "%m-%d-%Y %H:%M:%S") entries.append((dt, action)) sessions = [] @@ -107,7 +259,7 @@ def calculate_hours(): for day in sorted(daily_totals): summary += f"{day}: {daily_totals[day]:.2f} hours\n" - summary += "\nWeekly Totals (Sunday–Saturday):\n" + summary += "\nWeekly Totals:\n" for week in sorted(weekly_totals): summary += f"Week of {week}: {weekly_totals[week]:.2f} hours\n" @@ -121,19 +273,89 @@ def show_about(): f"{APP_NAME}\n" f"Author: {AUTHOR}\n" f"Contact: {EMAIL}\n\n" + f"Derivative Author: {AUTHOR_DER}\n" + f"Contact: {EMAIL_DER}\n\n" f"{LICENSE_SNIPPET}\n\n" "A simple Windows desktop time tracker built with Python and Tkinter." ) messagebox.showinfo("About", about_text) +def apply_theme(bg_color, fg_color, button_bg, button_fg): + """Apply colors to the whole app.""" + + # Main window + root.config(bg=bg_color) + + # Labels + clock_label.config(bg=bg_color, fg=fg_color) + version_label.config(bg=bg_color, fg=fg_color) + + # Buttons + clock_in_button.config( + bg=button_bg, + fg=button_fg, + activebackground=bg_color, + activeforeground=fg_color + ) + + clock_out_button.config( + bg=button_bg, + fg=button_fg, + activebackground=bg_color, + activeforeground=fg_color + ) + + exit_button.config( + bg=button_bg, + fg=button_fg, + activebackground=bg_color, + activeforeground=fg_color + ) + + # Menu bar + menu_bar.config( + bg=button_bg, + fg=button_fg, + activebackground=bg_color, + activeforeground=fg_color + ) + + # Dropdown menus + for menu in [file_menu, export_menu, style_menu, about_menu, dev_menu]: + menu.config( + bg=button_bg, + fg=button_fg, + activebackground=bg_color, + activeforeground=fg_color + ) + +def dark_mode(): + """Change app style to dark colors""" + apply_theme( + bg_color="#1e1e1e", + fg_color="#ffffff", + button_bg="#333333", + button_fg="white" + ) + +def light_mode(): + """Change app style to light colors""" + apply_theme( + bg_color="white", + fg_color="black", + button_bg="#f0f0f0", + button_fg="black" + ) + + def on_exit(): """Warn user if they are still punched in before exiting.""" last_action = get_last_action() if last_action == "Punch In": confirm = messagebox.askyesno( - "Still Punched In", - "You are still punched in.\nAre you sure you want to exit?" + "Still Clocked In", + "You are still clocked in.\nAre you sure you want to exit?" ) if not confirm: return # Cancel exit @@ -144,7 +366,7 @@ def on_exit(): root = tk.Tk() root.title(APP_NAME) -root.geometry("340x300") # slightly taller for footer label +root.geometry("340x200") # slightly taller for footer label # --- Digital Clock Display --- @@ -152,10 +374,27 @@ def on_exit(): clock_label.pack(pady=5) +def set_12_hour(): + """Switch clock to 12-hour format.""" + global CLOCK_FORMAT + CLOCK_FORMAT = "12h" + + +def set_24_hour(): + """Switch clock to 24-hour format.""" + global CLOCK_FORMAT + CLOCK_FORMAT = "24h" + + def update_clock(): """Update the on-screen digital clock every second.""" - current_time = datetime.now().strftime("%I:%M:%S %p") + if CLOCK_FORMAT == "12h": + current_time = datetime.now().strftime("%I:%M:%S %p") + else: + current_time = datetime.now().strftime("%H:%M:%S") + clock_label.config(text=current_time) + root.after(1000, update_clock) @@ -167,26 +406,71 @@ def update_clock(): root.config(menu=menu_bar) file_menu = Menu(menu_bar, tearoff=0) -file_menu.add_command(label="Export Log (Coming Soon)") +file_menu.add_command(label="View Hours", command=calculate_hours) +file_menu.add_command(label="View Log", command=view_log) +export_menu = Menu(file_menu, tearoff=0) +export_menu.add_command(label="This Week", command=lambda: export_report("week")) +export_menu.add_command(label="This Month", command=lambda: export_report("month")) +export_menu.add_command(label="This Year", command=lambda: export_report("year")) +export_menu.add_command(label="All Time", command=lambda: export_report("all")) +file_menu.add_separator() +file_menu.add_cascade(label="Export Report", menu=export_menu) menu_bar.add_cascade(label="File", menu=file_menu) +style_menu = Menu(menu_bar, tearoff=0) +style_menu.add_command(label="Dark Mode", command=dark_mode) +style_menu.add_command(label="Light Mode", command=light_mode) +style_menu.add_separator() +clock_menu = Menu(style_menu, tearoff=0) +clock_menu.add_command(label="12 Hour", command=set_12_hour) +clock_menu.add_command(label="24 Hour", command=set_24_hour) +style_menu.add_cascade(label="Clock Format", menu=clock_menu) +menu_bar.add_cascade(label="Style", menu=style_menu) + about_menu = Menu(menu_bar, tearoff=0) about_menu.add_command(label="About", command=show_about) menu_bar.add_cascade(label="About", menu=about_menu) +dev_menu = Menu(menu_bar, tearoff=0) +dev_menu.add_command(label="Clear Log", command=clear_log) +menu_bar.add_cascade(label="Developer", menu=dev_menu) + # --- Buttons --- -tk.Button(root, text="Punch In", width=15, command=lambda: log_action("Punch In")).pack(pady=5) -tk.Button(root, text="Punch Out", width=15, command=lambda: log_action("Punch Out")).pack(pady=5) -tk.Button(root, text="View Hours", width=15, command=calculate_hours).pack(pady=5) -tk.Button(root, text="View Log", width=15, command=view_log).pack(pady=5) -tk.Button(root, text="Exit", width=15, command=on_exit).pack(pady=5) +clock_in_button = tk.Button( + root, + text="Clock In", + width=15, + command=lambda: log_action("Punch In") +) +clock_in_button.pack(pady=5) + +clock_out_button = tk.Button( + root, + text="Clock Out", + width=15, + command=lambda: log_action("Punch Out") +) +clock_out_button.pack(pady=5) + +exit_button = tk.Button( + root, + text="Exit", + width=15, + command=on_exit +) +exit_button.pack(pady=5) + # --- Version Label (footer) --- version_label = tk.Label(root, text=f"Version {APP_VERSION}", font=("Arial", 9), fg="gray") version_label.pack(side="bottom", pady=3) +# --- Default Style --- +dark_mode() + + # --- Handle window close (X button) --- root.protocol("WM_DELETE_WINDOW", on_exit) From 4a9727c14830bebf0db138ee11b0b105ba012958 Mon Sep 17 00:00:00 2001 From: Skyler Clark Date: Thu, 21 May 2026 19:04:14 -0500 Subject: [PATCH 3/6] Update repository URL in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fecb51b..4c5f6cc 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It’s designed to be lightweight, precise, and user-friendly β€” perfect for sm ### Installation Clone the repository: ```bash -git clone https://github.com/dipsherlock/timeclock.git +git clone https://github.com/zegron/timeclock.git cd timeclock ``` @@ -54,4 +54,4 @@ dipsherlock πŸ“§ skylertclark@gmail.com zegron -πŸ“§ matt@onetakemedia.net \ No newline at end of file +πŸ“§ matt@onetakemedia.net From b47abb799248e9711e8fc45b45d970af4e5d81ca Mon Sep 17 00:00:00 2001 From: Skyler Clark Date: Thu, 21 May 2026 19:20:42 -0500 Subject: [PATCH 4/6] Update repository URL in README (dipsherlock/timeclock) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c5f6cc..22d0bc3 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It’s designed to be lightweight, precise, and user-friendly β€” perfect for sm ### Installation Clone the repository: ```bash -git clone https://github.com/zegron/timeclock.git +git clone https://github.com/dipsherlock/timeclock.git cd timeclock ``` From 7342b4824174453c6e1ef14bf0f89c64723409d5 Mon Sep 17 00:00:00 2001 From: Skyler Clark Date: Thu, 21 May 2026 20:12:55 -0500 Subject: [PATCH 5/6] Update repository url in README Commits to this branch applies to both the original repository as well as the cloned one, so I just added both url's for simplicity. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 22d0bc3..c0bd66f 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,21 @@ It’s designed to be lightweight, precise, and user-friendly β€” perfect for sm ### Installation Clone the repository: + +For latest version (v1.2.2): + ```bash git clone https://github.com/dipsherlock/timeclock.git cd timeclock ``` +For original version (v1.1.5): + +```bash +git clone https://github.com/zegron/timeclock.git +cd timeclock +``` + ### Run the App Simply run the Python script: From 927199de3e334cf00ed3ea93d3a2eb04551e69a4 Mon Sep 17 00:00:00 2001 From: Skyler Clark Date: Thu, 21 May 2026 20:17:53 -0500 Subject: [PATCH 6/6] Update repository url in README (zegron/timeclock) After merging my branch to my cloned project, changes no longer apply to both. Still figuring out GitHub... --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index c0bd66f..0e8ddc4 100644 --- a/README.md +++ b/README.md @@ -23,15 +23,6 @@ It’s designed to be lightweight, precise, and user-friendly β€” perfect for sm ### Installation Clone the repository: -For latest version (v1.2.2): - -```bash -git clone https://github.com/dipsherlock/timeclock.git -cd timeclock -``` - -For original version (v1.1.5): - ```bash git clone https://github.com/zegron/timeclock.git cd timeclock