import customtkinter as ctk from database import Database from tkinter import messagebox from utils import parse_float, format_float from gamification import GamificationManager class DailyLogFrame(ctk.CTkFrame): def __init__(self, master, db: Database, date_str: str): super().__init__(master) self.db = db self.date_str = date_str self.gm = GamificationManager(self.db) self.setup_ui() self.load_data() def setup_ui(self): self.grid_columnconfigure((0, 1), weight=1) # Header self.header = ctk.CTkLabel(self, text=f"Daily Log: {self.date_str}", font=ctk.CTkFont(size=20, weight="bold")) self.header.grid(row=0, column=0, columnspan=2, pady=10, sticky="w") # Sleep Section self.sleep_frame = ctk.CTkFrame(self) self.sleep_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=10) ctk.CTkLabel(self.sleep_frame, text="Sleep Duration (Hours):").grid(row=0, column=0, padx=10, pady=10) self.sleep_entry = ctk.CTkEntry(self.sleep_frame, placeholder_text="e.g. 7,5") self.sleep_entry.grid(row=0, column=1, padx=10, pady=10) ctk.CTkLabel(self.sleep_frame, text="Sleep Quality (1-10):").grid(row=0, column=2, padx=10, pady=10) self.sleep_quality_slider = ctk.CTkSlider(self.sleep_frame, from_=1, to=10, number_of_steps=9) self.sleep_quality_slider.grid(row=0, column=3, padx=10, pady=10) self.sleep_quality_label = ctk.CTkLabel(self.sleep_frame, text="5") self.sleep_quality_label.grid(row=0, column=4, padx=5) self.sleep_quality_slider.configure(command=lambda val: self.sleep_quality_label.configure(text=str(int(val)))) # Body Stats Section self.body_frame = ctk.CTkFrame(self) self.body_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=10) ctk.CTkLabel(self.body_frame, text="Weight (kg):").grid(row=0, column=0, padx=10, pady=10) self.weight_entry = ctk.CTkEntry(self.body_frame, placeholder_text="0,0") self.weight_entry.grid(row=0, column=1, padx=10, pady=10) ctk.CTkLabel(self.body_frame, text="Waist (cm):").grid(row=0, column=2, padx=10, pady=10) self.waist_entry = ctk.CTkEntry(self.body_frame, placeholder_text="0,0") self.waist_entry.grid(row=0, column=3, padx=10, pady=10) # Notes / Mood self.notes_frame = ctk.CTkFrame(self) self.notes_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=10) ctk.CTkLabel(self.notes_frame, text="Mood/Energy (1-10):").grid(row=0, column=0, padx=10, pady=10) self.mood_slider = ctk.CTkSlider(self.notes_frame, from_=1, to=10, number_of_steps=9) self.mood_slider.grid(row=0, column=1, padx=10, pady=10, sticky="ew") self.mood_label = ctk.CTkLabel(self.notes_frame, text="5") self.mood_label.grid(row=0, column=2, padx=5) self.mood_slider.configure(command=lambda val: self.mood_label.configure(text=str(int(val)))) ctk.CTkLabel(self.notes_frame, text="Notes:").grid(row=1, column=0, padx=10, pady=10, sticky="n") self.notes_entry = ctk.CTkTextbox(self.notes_frame, height=100) self.notes_entry.grid(row=1, column=1, columnspan=2, padx=10, pady=10, sticky="ew") # Save Button self.save_btn = ctk.CTkButton(self, text="Save Daily Log", command=self.save_log) self.save_btn.grid(row=4, column=0, columnspan=2, pady=20) def load_data(self): log = self.db.get_daily_log(self.date_str) if log: if log['sleep_hours'] is not None: self.sleep_entry.insert(0, format_float(log['sleep_hours'])) if log['sleep_quality']: self.sleep_quality_slider.set(log['sleep_quality']) self.sleep_quality_label.configure(text=str(log['sleep_quality'])) if log['weight'] is not None: self.weight_entry.insert(0, format_float(log['weight'])) if log['waist'] is not None: self.waist_entry.insert(0, format_float(log['waist'])) if log['mood']: self.mood_slider.set(log['mood']) self.mood_label.configure(text=str(log['mood'])) if log['notes']: self.notes_entry.insert("0.0", log['notes']) def save_log(self): # We allow entries even if some fields are empty (parse_float handles this) # However, if they typed something invalid, we should catch it s_val = self.sleep_entry.get() w_val = self.weight_entry.get() wa_val = self.waist_entry.get() sleep = parse_float(s_val) if s_val else None weight = parse_float(w_val) if w_val else None waist = parse_float(wa_val) if wa_val else None # Validation check: if they entered text but it parsed to None if (s_val and sleep is None) or (w_val and weight is None) or (wa_val and waist is None): messagebox.showerror("Error", "Please enter valid numbers (e.g. 75.5 or 75,5)") return data = { 'sleep_hours': sleep, 'sleep_quality': int(self.sleep_quality_slider.get()), 'weight': weight, 'waist': waist, 'mood': int(self.mood_slider.get()), 'notes': self.notes_entry.get("0.0", "end").strip() } self.db.save_daily_log(self.date_str, data) self.save_btn.configure(text="Saved! (+20 XP)", fg_color="green") self.db.add_xp(20) self.gm.check_achievements() # Cancel previous timer if exists if hasattr(self, '_reset_btn_id'): self.after_cancel(self._reset_btn_id) self._reset_btn_id = self.after(2000, self.reset_save_button) def reset_save_button(self): if self.winfo_exists(): self.save_btn.configure(text="Save Daily Log", fg_color=["#3B8ED0", "#1F6AA5"]) def destroy(self): if hasattr(self, '_reset_btn_id'): self.after_cancel(self._reset_btn_id) super().destroy()