
import tkinter as tk
import re
from datetime import date, datetime, timedelta
from tkinter import messagebox, scrolledtext

from core.constants import BASE_DIR
from core.storage import save_notes, save_todos, save_enterprise, save_reports, save_attendance, save_documents
from core.ui import ToolTip, create_button, create_card, create_dialog_window, prompt_text_dialog, show_empty


class ScrollArea:
    def __init__(self, parent, colors):
        self.colors = colors
        self.outer = tk.Frame(parent, bg=colors["card"])
        self.canvas = tk.Canvas(self.outer, bg=colors["card"], highlightthickness=0)
        self.scrollbar = tk.Scrollbar(self.outer, orient="vertical", command=self.canvas.yview)
        self.inner = tk.Frame(self.canvas, bg=colors["card"])
        self.window = self.canvas.create_window((0, 0), window=self.inner, anchor="nw")
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self._scrollbar_visible = False
        self._scroll_needed = False
        self.inner.bind("<Configure>", self._on_configure)
        self.canvas.bind("<Configure>", self._on_configure)

    def _inside(self, event):
        try:
            x1, y1 = self.outer.winfo_rootx(), self.outer.winfo_rooty()
            x2, y2 = x1 + self.outer.winfo_width(), y1 + self.outer.winfo_height()
            return x1 <= event.x_root <= x2 and y1 <= event.y_root <= y2
        except Exception:
            return False

    def _on_configure(self, event=None):
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))
        try:
            self.canvas.itemconfig(self.window, width=max(self.canvas.winfo_width() - 4, 240))
            needed = self.inner.winfo_reqheight() > self.canvas.winfo_height() + 2
            self._scroll_needed = needed
            if needed and not self._scrollbar_visible:
                self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
                self._scrollbar_visible = True
            elif not needed and self._scrollbar_visible:
                self.scrollbar.pack_forget()
                self._scrollbar_visible = False
        except Exception:
            pass

    def _wheel_linux(self, direction, event):
        if self._inside(event) and self._scroll_needed:
            self.canvas.yview_scroll(direction, "units")

    def _wheel(self, event):
        if self._inside(event) and self._scroll_needed:
            step = -1 if event.delta > 0 else 1
            self.canvas.yview_scroll(step, "units")


class HomePage:
    def __init__(self, app, parent):
        self.app = app
        self.colors = app.colors
        self.frame = tk.Frame(parent, bg=self.colors["bg"])
        self.notice_page = 0
        self.docs_page = 0
        self.page_size = 5
        self._build()

    def _hero_button(self, parent, text, command, filled=False):
        bg = self.colors["primary"] if filled else self.colors["card_soft"]
        fg = "white" if filled else self.colors["text"]
        return tk.Button(
            parent, text=text, command=command, bg=bg, fg=fg,
            activebackground=self.colors["primary_dark"] if filled else self.colors["primary_light"],
            activeforeground="white" if filled else self.colors["primary"],
            relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 10, "bold"),
            padx=16, pady=9, cursor="hand2", highlightthickness=0
        )

    def _display_user_name(self):
        try:
            raw = str(self.app.current_user_display_name() or "NexaOffice").strip()
        except Exception:
            raw = str(self.app.user_settings.get("username") or "NexaOffice").strip()
        return raw or "NexaOffice"

    def _daily_encouragement(self):
        items = self.app.t("home_daily_encouragements")
        if not isinstance(items, list) or not items:
            items = [self.app.t("home_welcome_default_encouragement")]
        return items[date.today().toordinal() % len(items)]

    def _time_greeting_key(self):
        hour = datetime.now().hour
        if 5 <= hour < 11:
            return "home_good_morning"
        if 11 <= hour < 14:
            return "home_good_noon"
        if 14 <= hour < 18:
            return "home_good_afternoon"
        return "home_good_evening"

    def _update_home_focus_status(self):
        """Show a lightweight task pressure signal in the home hero badge.

        This keeps the home page more useful without adding another heavy widget:
        overdue tasks get a red badge, today's tasks get an orange badge, and
        otherwise the badge stays green/ready.
        """
        if not hasattr(self, "hero_status"):
            return
        try:
            today = date.today()
            overdue = 0
            due_today = 0
            for item in self.app.todos.get("items", []):
                if not isinstance(item, dict) or item.get("done"):
                    continue
                due = self._task_date_text(item.get("due_date"))
                if not due:
                    continue
                due_day = datetime.strptime(due, "%Y-%m-%d").date()
                if due_day < today:
                    overdue += 1
                elif due_day == today:
                    due_today += 1
            if overdue:
                self.hero_status.config(text=self.app.t("home_focus_overdue").format(count=overdue), fg=self.colors["danger"], bg=self.colors["danger_light"])
            elif due_today:
                self.hero_status.config(text=self.app.t("home_focus_today").format(count=due_today), fg=self.colors["warning"], bg=self.colors["warning_light"])
            else:
                self.hero_status.config(text=self.app.t("status_ready"), fg=self.colors["success"], bg=self.colors["success_light"])
        except Exception:
            self.hero_status.config(text=self.app.t("status_ready"), fg=self.colors["success"], bg=self.colors["success_light"])

    def _update_home_greeting(self):
        if not hasattr(self, "hero_title"):
            return
        name = self._display_user_name()
        greeting = self.app.t(self._time_greeting_key()).format(name=name)
        self.hero_title.config(text=greeting)
        self.hero_subtitle.config(text=self._daily_encouragement())
        self._update_home_focus_status()

    def _section(self, parent, title_key, subtitle_key=None, nav=None, scroll=False, open_command=None):
        """Build a home card section.

        The "open all" action intentionally lives in the bottom-right corner of
        each card. This keeps the section header clean, especially when the card
        also has a pager or inline input controls.
        """
        header = tk.Frame(parent, bg=self.colors["card"])
        header.pack(fill=tk.X, padx=18, pady=(16, 10))
        accent = tk.Frame(header, bg=self.colors["primary"], width=4, height=20)
        accent.pack(side=tk.LEFT, padx=(0, 10))
        title = tk.Label(header, text=self.app.t(title_key), font=("Microsoft YaHei UI", 13, "bold"), fg=self.colors["text"], bg=self.colors["card"])
        title.pack(side=tk.LEFT)
        subtitle = None
        if subtitle_key:
            subtitle = tk.Label(header, text=self.app.t(subtitle_key), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card"])
            subtitle.pack(side=tk.RIGHT)
        if nav:
            nav(header)

        def add_footer():
            if not open_command:
                return
            footer = tk.Frame(parent, bg=self.colors["card"])
            footer.pack(fill=tk.X, padx=16, pady=(0, 14))
            tk.Button(
                footer,
                text=self.app.t("home_open_all"),
                command=open_command,
                relief=tk.FLAT,
                bd=0,
                bg=self.colors["card_soft"],
                fg=self.colors["primary"],
                activebackground=self.colors["primary_light"],
                activeforeground=self.colors["primary"],
                font=("Microsoft YaHei UI", 9, "bold"),
                cursor="hand2",
                padx=10,
                pady=5,
            ).pack(side=tk.RIGHT)

        if scroll:
            area = ScrollArea(parent, self.colors)
            area.outer.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 8 if open_command else 16))
            add_footer()
            return title, subtitle, area.inner
        body = tk.Frame(parent, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 8 if open_command else 16))
        add_footer()
        return title, subtitle, body

    def _build(self):
        # The page header already shows "NexaOffice + subtitle".
        # Keep this card only for status and quick actions to avoid duplicated titles.
        self.hero_card = create_card(self.frame, self.colors, fill=tk.X, pady=(0, 14))
        hero = self.hero_card
        hero_inner = tk.Frame(hero, bg=self.colors["card"])
        hero_inner.pack(fill=tk.BOTH, expand=True, padx=22, pady=18)
        self.hero_inner = hero_inner

        hero_top = tk.Frame(hero_inner, bg=self.colors["card"])
        hero_top.pack(fill=tk.X, pady=(0, 12))
        self.hero_title = tk.Label(
            hero_top,
            text="",
            font=("Microsoft YaHei UI", 19, "bold"),
            fg=self.colors["text"],
            bg=self.colors["card"],
        )
        self.hero_title.pack(side=tk.LEFT)
        self.hero_status = tk.Label(
            hero_top,
            text=self.app.t("status_ready"),
            font=("Microsoft YaHei UI", 9, "bold"),
            fg=self.colors["success"],
            bg=self.colors["success_light"],
            padx=10,
            pady=5,
        )
        self.hero_status.pack(side=tk.LEFT, padx=12)
        self.hero_subtitle = tk.Label(
            hero_top,
            text="",
            font=("Microsoft YaHei UI", 10),
            fg=self.colors["text_secondary"],
            bg=self.colors["card"],
        )
        self.hero_subtitle.pack(side=tk.LEFT, padx=(0, 8))

        self.metrics_row = tk.Frame(hero_inner, bg=self.colors["card"])
        self.metrics_row.pack(fill=tk.X, pady=(0, 12))
        self._build_hero_metrics()

        # Quick actions are now consolidated in the portal below and in Ctrl+K.
        # Avoid showing the same shortcuts twice on the home page.
        self.action_row = tk.Frame(hero_inner, bg=self.colors["card"])

        # Portal + update row. The update card is intentionally compact so it
        # informs users after login without taking over the whole home page.
        self.portal_update_row = tk.Frame(self.frame, bg=self.colors["bg"])
        self.portal_update_row.pack(fill=tk.X, pady=(0, 14))
        self.portal_update_row.grid_columnconfigure(0, weight=7, uniform="home_top")
        self.portal_update_row.grid_columnconfigure(1, weight=3, uniform="home_top")

        self.launcher_card = create_card(self.portal_update_row, self.colors)
        self.launcher_card.grid(row=0, column=0, sticky="nsew", padx=(0, 7))
        launcher_head = tk.Frame(self.launcher_card, bg=self.colors["card"])
        launcher_head.pack(fill=tk.X, padx=18, pady=(14, 6))
        self.launcher_title = tk.Label(launcher_head, text=self.app.t("home_portal_title"), font=("Microsoft YaHei UI", 13, "bold"), fg=self.colors["text"], bg=self.colors["card"])
        self.launcher_title.pack(side=tk.LEFT)
        self.launcher_hint = tk.Label(launcher_head, text=self.app.t("home_portal_hint"), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card"])
        self.launcher_hint.pack(side=tk.RIGHT)
        self.launcher_row = tk.Frame(self.launcher_card, bg=self.colors["card"])
        self.launcher_row.pack(fill=tk.X, padx=18, pady=(0, 12))
        self._build_module_launcher()

        self.update_card = create_card(self.portal_update_row, self.colors)
        self.update_card.grid(row=0, column=1, sticky="nsew", padx=(7, 0))
        update_head = tk.Frame(self.update_card, bg=self.colors["card"])
        update_head.pack(fill=tk.X, padx=16, pady=(14, 6))
        self.update_title = tk.Label(update_head, text=self.app.t("home_latest_update"), font=("Microsoft YaHei UI", 13, "bold"), fg=self.colors["text"], bg=self.colors["card"])
        self.update_title.pack(side=tk.LEFT)
        self.update_body = tk.Frame(self.update_card, bg=self.colors["card"])
        self.update_body.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 8))
        update_footer = tk.Frame(self.update_card, bg=self.colors["card"])
        update_footer.pack(fill=tk.X, padx=16, pady=(0, 12))
        self.update_open_btn = tk.Button(update_footer, text=self.app.t("home_update_open_history"), command=self.open_update_history, relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["primary"], activebackground=self.colors["primary_light"], activeforeground=self.colors["primary"], font=("Microsoft YaHei UI", 9, "bold"), cursor="hand2", padx=10, pady=5)
        self.update_open_btn.pack(side=tk.RIGHT)
        self._build_latest_update_card()

        # The old workspace-focus card duplicated the top metrics and the
        # notification area.  Keep the home page simple: overview at the top,
        # portal shortcuts, then the four work lists below.
        self.mode_card = None

        self.grid = tk.Frame(self.frame, bg=self.colors["bg"])
        grid = self.grid
        grid.pack(fill=tk.BOTH, expand=True)
        for c in range(3):
            grid.grid_columnconfigure(c, weight=1, uniform="home")
        grid.grid_rowconfigure(0, weight=1)
        grid.grid_rowconfigure(1, weight=1)

        self.events_card = create_card(grid, self.colors)
        self.events_card.grid(row=0, column=0, sticky="nsew", padx=(0, 7), pady=(0, 7))
        self.todos_card = create_card(grid, self.colors)
        self.todos_card.grid(row=0, column=1, sticky="nsew", padx=7, pady=(0, 7))
        self.notifications_card = create_card(grid, self.colors)
        self.notifications_card.grid(row=0, column=2, sticky="nsew", padx=(7, 0), pady=(0, 7))
        self.docs_card = create_card(grid, self.colors)
        self.docs_card.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=0, pady=(7, 0))

        self.events_title, self.events_subtitle, self.events_list = self._section(self.events_card, "home_upcoming", "upcoming_days", scroll=True, open_command=self.open_all_events)
        self.todos_title, _, self.todos_list = self._section(self.todos_card, "home_todos", None, scroll=True, open_command=self.open_all_tasks)
        self._build_todo_input(self.todos_title.master)
        self.notifications_title, _, self.notifications_list = self._section(self.notifications_card, "notification_center", None, self._notice_nav, scroll=True, open_command=self.open_all_notifications)
        self.docs_title, _, self.docs_list = self._section(self.docs_card, "home_documents", None, self._docs_nav, scroll=True, open_command=self.open_all_docs)
        self.overview_list = None

    def on_app_resize(self, width, height):
        try:
            width = max(int(width), 1220)
            height = max(int(height), 740)
        except Exception:
            width, height = 1440, 900
        try:
            compact = height < 820 or width < 1380
            hero_pad = (14, 12) if compact else (20, 16)
            if hasattr(self, "hero_inner"):
                self.hero_inner.pack_configure(padx=hero_pad[0], pady=hero_pad[1])
            self.hero_title.configure(font=("Microsoft YaHei UI", 17 if compact else 19, "bold"))
            self.hero_subtitle.configure(font=("Microsoft YaHei UI", 9 if compact else 10))
            self.hero_status.configure(padx=9 if compact else 11, pady=4 if compact else 5)
            for btn in self.action_row.winfo_children():
                try:
                    btn.configure(font=("Microsoft YaHei UI", 9 if compact else 10, "bold"), padx=14 if compact else 16, pady=8 if compact else 9)
                except Exception:
                    pass
            if hasattr(self, "launcher_row"):
                for item in self.launcher_row.winfo_children():
                    try:
                        item.grid_configure(padx=(0, 6 if compact else 10), pady=(0, 6 if compact else 8))
                    except Exception:
                        pass
                for label in getattr(self, "_launcher_labels", []):
                    try:
                        label.configure(font=("Microsoft YaHei UI", 8 if compact else 9, "bold"), wraplength=108 if compact else 116)
                    except Exception:
                        pass
                for hint in getattr(self, "_launcher_hint_labels", []):
                    try:
                        hint.configure(font=("Microsoft YaHei UI", 7 if compact else 8), wraplength=112 if compact else 122)
                    except Exception:
                        pass
            if hasattr(self, "portal_update_row"):
                top_gap = 5 if compact else 7
                self.launcher_card.grid_configure(padx=(0, top_gap))
                self.update_card.grid_configure(padx=(top_gap, 0))
            for card in getattr(self, "metrics_row", tk.Frame()).winfo_children():
                try:
                    card.pack_configure(padx=(0, 8 if compact else 10))
                except Exception:
                    pass
            # Card spacing: tighter on laptop screens, more breathable on wide screens.
            gap = 5 if compact else 8
            self.events_card.grid_configure(padx=(0, gap), pady=(0, gap))
            self.todos_card.grid_configure(padx=gap, pady=(0, gap))
            self.notifications_card.grid_configure(padx=(gap, 0), pady=(0, gap))
            self.docs_card.grid_configure(padx=0, pady=(gap, 0))
        except Exception:
            pass

    def _notice_nav(self, parent):
        self.notice_page_label = tk.Label(parent, text="", fg=self.colors["text_secondary"], bg=self.colors["card"], font=("Microsoft YaHei UI", 8))
        self.notice_page_label.pack(side=tk.RIGHT, padx=(8, 0))
        tk.Button(parent, text="›", command=lambda: self._turn_notice(1), relief=tk.FLAT, bd=0, bg=self.colors["card"], fg=self.colors["primary"], cursor="hand2", font=("Microsoft YaHei UI", 12, "bold")).pack(side=tk.RIGHT)
        tk.Button(parent, text="‹", command=lambda: self._turn_notice(-1), relief=tk.FLAT, bd=0, bg=self.colors["card"], fg=self.colors["primary"], cursor="hand2", font=("Microsoft YaHei UI", 12, "bold")).pack(side=tk.RIGHT)

    def _docs_nav(self, parent):
        self.docs_page_label = tk.Label(parent, text="", fg=self.colors["text_secondary"], bg=self.colors["card"], font=("Microsoft YaHei UI", 8))
        self.docs_page_label.pack(side=tk.RIGHT, padx=(8, 0))
        tk.Button(parent, text="›", command=lambda: self._turn_docs(1), relief=tk.FLAT, bd=0, bg=self.colors["card"], fg=self.colors["primary"], cursor="hand2", font=("Microsoft YaHei UI", 12, "bold")).pack(side=tk.RIGHT)
        tk.Button(parent, text="‹", command=lambda: self._turn_docs(-1), relief=tk.FLAT, bd=0, bg=self.colors["card"], fg=self.colors["primary"], cursor="hand2", font=("Microsoft YaHei UI", 12, "bold")).pack(side=tk.RIGHT)

    def _quick_defs(self):
        return [
            ("focus", "◎", "hero_open_focus", self.open_focus_planner),
            ("calendar", "C", "hero_open_calendar", lambda: self.app.switch_page("calendar")),
            ("task", "✓", "hero_new_todo", self.focus_todo),
            ("note", "N", "hero_new_note", lambda: self.app.switch_page("files")),
            ("chat", "M", "hero_open_chat", lambda: self.app.switch_page("chat")),
            ("cloud", "D", "hero_open_cloud", lambda: self.app.switch_page("cloud")),
            ("enterprise", "E", "hero_open_enterprise", lambda: self.app.switch_page("enterprise")),
            ("ai", "AI", "hero_open_ai", lambda: self.app.switch_page("ai")),
        ]

    def _enabled_quick_defs(self):
        settings = self.app.get_quick_actions()
        unavailable_in_mode = set()
        if hasattr(self.app, "is_page_available"):
            for name, page in (("chat", "chat"), ("cloud", "cloud"), ("enterprise", "enterprise")):
                if not self.app.is_page_available(page):
                    unavailable_in_mode.add(name)
        return [
            (name, icon, key, command)
            for name, icon, key, command in self._quick_defs()
            if settings.get(name, True) and name not in unavailable_in_mode
        ][:10]

    def _quick_label(self, name, default_key):
        return self.app.get_quick_label(name, self.app.t(default_key))

    def _module_launcher_defs(self):
        return [
            ("◎", "home_app_focus", "home_app_focus_hint", self.open_focus_planner, "#8b5cf6"),
            ("25", "home_app_timer", "home_app_timer_hint", self.app.show_focus_timer, "#ef4444"),
            ("✓", "home_app_approval", "home_app_approval_hint", self.open_approval_center, "#f59e0b"),
            ("⏱", "home_app_attendance", "home_app_attendance_hint", self.open_attendance_center, "#0ea5e9"),
            ("▣", "home_app_report", "home_app_report_hint", self.open_work_report, "#14b8a6"),
            ("◷", "home_app_time", "home_app_time_hint", lambda: self.app.switch_page("calendar"), "#2563eb"),
            ("D", "home_app_knowledge", "home_app_knowledge_hint", lambda: self.app.switch_page("files"), "#3b82f6"),
            ("☑", "home_app_meeting", "home_app_meeting_hint", self.open_meeting_assistant, "#10b981"),
            ("☁", "home_app_cloud", "home_app_cloud_hint", lambda: self.app.switch_page("cloud"), "#3b82f6"),
            ("?", "home_app_survey", "home_app_survey_hint", self.open_survey_assistant, "#64748b"),
            ("▥", "home_app_vote", "home_app_vote_hint", self.open_vote_assistant, "#2563eb"),
            ("⋯", "home_app_all", "home_app_all_hint", self.open_app_center, "#94a3b8"),
        ]

    def _launcher_visible_defs(self):
        """Home portal shortcuts vary by product mode.

        Personal mode keeps personal productivity entries and hides company-only
        collaboration shortcuts. Enterprise mode keeps the full set.
        """
        defs = self._module_launcher_defs()
        if getattr(self.app, "is_enterprise_mode", lambda: True)():
            return defs
        # Personal edition keeps only personal productivity / personal files.
        # Company collaboration, cloud/team, approval, survey and vote stay hidden
        # until the user explicitly joins or creates an enterprise.
        hidden_keys = {"home_app_approval", "home_app_survey", "home_app_vote", "home_app_cloud", "home_app_attendance", "home_app_report", "home_app_meeting"}
        return [item for item in defs if item[1] not in hidden_keys]

    def _task_now_text(self):
        return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    def _task_date_text(self, value):
        value = str(value or "").strip()[:10]
        try:
            datetime.strptime(value, "%Y-%m-%d")
            return value
        except Exception:
            return ""

    def _parse_task_text(self, raw_text):
        """Create task metadata from quick text.

        Supported lightweight syntax:
        - Prefix ! for high priority, !! for urgent.
        - Add @YYYY-MM-DD to set a due date.
        - Words such as today/tomorrow/今天/明天/hari ini/besok also set due date.
        The visible task text is cleaned so the list stays readable.
        """
        original = str(raw_text or "").strip()
        text = original
        priority = "normal"
        if text.startswith("!!"):
            priority = "urgent"
            text = text[2:].strip()
        elif text.startswith("!"):
            priority = "high"
            text = text[1:].strip()

        due_date = ""
        match = re.search(r"(?:^|\s)@(\d{4}-\d{2}-\d{2})(?=\s|$)", text)
        if match:
            due_date = self._task_date_text(match.group(1))
            text = (text[:match.start()] + " " + text[match.end():]).strip()

        tags = []
        for tag in re.findall(r"(?<!\S)#([\w\u4e00-\u9fff\-]{1,24})", text):
            if tag not in tags:
                tags.append(tag)
        text = re.sub(r"(?<!\S)#[\w\u4e00-\u9fff\-]{1,24}", " ", text).strip()

        assignee = ""
        owner_match = re.search(r"(?:^|\s)\+([\w\u4e00-\u9fff\-]{1,24})(?=\s|$)", text)
        if owner_match:
            assignee = owner_match.group(1)
            text = (text[:owner_match.start()] + " " + text[owner_match.end():]).strip()

        lowered = text.lower()
        keyword_dates = [
            (("今天", "今日", "today", "hari ini"), 0),
            (("明天", "tomorrow", "besok"), 1),
        ]
        if not due_date:
            for words, offset in keyword_dates:
                if any(word in lowered for word in words):
                    due_date = (date.today() + timedelta(days=offset)).strftime("%Y-%m-%d")
                    break

        now = self._task_now_text()
        return {
            "text": text or original,
            "done": False,
            "priority": priority,
            "due_date": due_date,
            "tags": tags,
            "assignee": assignee,
            "created_at": now,
            "updated_at": now,
            "scope": self.app.current_data_scope() if hasattr(self.app, "current_data_scope") else "personal",
        }

    def _make_todo_item(self, raw_text):
        if hasattr(self.app, "make_todo_item"):
            return self.app.make_todo_item(raw_text)
        item = self._parse_task_text(raw_text)
        if not item.get("text"):
            item["text"] = str(raw_text or "").strip()
        return item

    def _set_todo_done(self, idx, done):
        if not (0 <= idx < len(self.app.todos.get("items", []))):
            return False
        item = self.app.todos["items"][idx]
        if not isinstance(item, dict):
            item = {"text": str(item), "done": False}
            self.app.todos["items"][idx] = item
        done = bool(done)
        item["done"] = done
        item["updated_at"] = self._task_now_text()
        if done:
            item["completed_at"] = item.get("completed_at") or item["updated_at"]
        else:
            item["completed_at"] = ""
        return True

    def _update_todo_text(self, idx, raw_text):
        if not (0 <= idx < len(self.app.todos.get("items", []))):
            return False
        item = self.app.todos["items"][idx]
        if not isinstance(item, dict):
            item = {"text": str(item), "done": False}
            self.app.todos["items"][idx] = item
        parsed = self._parse_task_text(raw_text)
        item["text"] = parsed.get("text") or item.get("text", "")
        item["priority"] = parsed.get("priority") or item.get("priority", "normal")
        if parsed.get("due_date"):
            item["due_date"] = parsed["due_date"]
        if parsed.get("tags"):
            item["tags"] = parsed["tags"]
        if parsed.get("assignee"):
            item["assignee"] = parsed["assignee"]
        item["updated_at"] = self._task_now_text()
        item.setdefault("created_at", item["updated_at"])
        return True

    def _task_due_bucket(self, item):
        due = self._task_date_text(item.get("due_date"))
        if not due:
            return 4
        try:
            due_day = datetime.strptime(due, "%Y-%m-%d").date()
        except Exception:
            return 4
        today = date.today()
        if due_day < today:
            return 0
        if due_day == today:
            return 1
        if due_day == today + timedelta(days=1):
            return 2
        return 3

    def _task_is_overdue(self, item):
        if not isinstance(item, dict) or item.get("done"):
            return False
        due = self._task_date_text(item.get("due_date"))
        if not due:
            return False
        try:
            return datetime.strptime(due, "%Y-%m-%d").date() < date.today()
        except Exception:
            return False

    def _task_is_due_today(self, item):
        if not isinstance(item, dict) or item.get("done"):
            return False
        due = self._task_date_text(item.get("due_date"))
        if not due:
            return False
        try:
            return datetime.strptime(due, "%Y-%m-%d").date() == date.today()
        except Exception:
            return False

    def _task_filter_match(self, item, mode):
        if not isinstance(item, dict):
            item = {"text": str(item), "done": False}
        if mode == "pending":
            return not item.get("done")
        if mode == "done":
            return bool(item.get("done"))
        if mode == "today":
            return self._task_is_due_today(item)
        if mode == "overdue":
            return self._task_is_overdue(item)
        if mode == "priority":
            return not item.get("done") and item.get("priority") in ("urgent", "high")
        return True

    def _task_sort_key(self, idx, item):
        if hasattr(self.app, "task_sort_key"):
            return self.app.task_sort_key(idx, item)
        if not isinstance(item, dict):
            item = {"text": str(item), "done": False}
        done_bucket = 1 if item.get("done") else 0
        priority_bucket = {"urgent": 0, "high": 1, "normal": 2, "low": 3}.get(item.get("priority", "normal"), 2)
        time_key = str(item.get("due_date") or item.get("created_at") or item.get("updated_at") or "9999")
        return (done_bucket, self._task_due_bucket(item), priority_bucket, time_key, idx)

    def _ordered_todo_items(self):
        if hasattr(self.app, "ordered_todos"):
            return self.app.ordered_todos()
        return sorted(enumerate(self.app.todos.get("items", [])), key=lambda pair: self._task_sort_key(pair[0], pair[1]))

    def _task_meta_parts(self, item):
        if not isinstance(item, dict):
            return []
        parts = []
        priority = item.get("priority", "normal")
        if priority in ("urgent", "high"):
            parts.append(self.app.t(f"task_priority_{priority}"))
        due = self._task_date_text(item.get("due_date"))
        if due:
            try:
                due_day = datetime.strptime(due, "%Y-%m-%d").date()
                today = date.today()
                if due_day < today and not item.get("done"):
                    parts.append(self.app.t("task_meta_overdue").format(date=due))
                elif due_day == today:
                    parts.append(self.app.t("task_meta_due_today"))
                elif due_day == today + timedelta(days=1):
                    parts.append(self.app.t("task_meta_due_tomorrow"))
                else:
                    parts.append(self.app.t("task_meta_due").format(date=due))
            except Exception:
                parts.append(self.app.t("task_meta_due").format(date=due))
        assignee = str(item.get("assignee") or "").strip()
        if assignee:
            parts.append(self.app.t("task_meta_assignee").format(name=assignee))
        tags = [str(tag).strip() for tag in item.get("tags", []) if str(tag).strip()]
        if tags:
            parts.append(self.app.t("task_meta_tags").format(tags=", ".join("#" + tag for tag in tags[:3])))
        if item.get("done") and item.get("completed_at"):
            parts.append(self.app.t("task_meta_completed").format(time=str(item.get("completed_at"))[:16]))
        return parts

    def _render_task_text_block(self, parent, item, wraplength=480):
        box = tk.Frame(parent, bg=self.colors["card_soft"])
        box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4, pady=8)
        tk.Label(
            box,
            text=item.get("text", "") if isinstance(item, dict) else str(item),
            font=("Microsoft YaHei UI", 10, "overstrike") if isinstance(item, dict) and item.get("done") else ("Microsoft YaHei UI", 10),
            fg=self.colors["todo_done"] if isinstance(item, dict) and item.get("done") else self.colors["text"],
            bg=self.colors["card_soft"],
            wraplength=wraplength,
            justify=tk.LEFT,
        ).pack(anchor="w", fill=tk.X)
        meta = " · ".join(self._task_meta_parts(item))
        if meta:
            tk.Label(box, text=meta, font=("Microsoft YaHei UI", 8), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=wraplength, justify=tk.LEFT).pack(anchor="w", pady=(3, 0))

    def _top_pending_task_lines(self, limit=5):
        rows = []
        for _idx, item in self._ordered_todo_items():
            if not isinstance(item, dict) or item.get("done"):
                continue
            meta = " / ".join(self._task_meta_parts(item))
            text = str(item.get("text", "")).strip()
            rows.append(f"- {text}" + (f" ({meta})" if meta else ""))
            if len(rows) >= limit:
                break
        return "\n".join(rows) if rows else f"- {self.app.t('report_no_focus_tasks')}"

    def _build_module_launcher(self):
        if not hasattr(self, "launcher_row"):
            return
        for child in self.launcher_row.winfo_children():
            child.destroy()
        self._launcher_labels = []
        self._launcher_items = []
        defs = self._launcher_visible_defs()
        cols = 5 if len(defs) > 5 else max(len(defs), 1)
        for c in range(cols):
            self.launcher_row.grid_columnconfigure(c, weight=1, uniform="launcher")
        self._launcher_hint_labels = []
        for i, (icon, title_key, hint_key, command, color) in enumerate(defs):
            row = i // cols
            col = i % cols
            item = tk.Frame(self.launcher_row, bg=self.colors["card_soft"], cursor="hand2", highlightthickness=1, highlightbackground=self.colors["border"])
            item.grid(row=row, column=col, sticky="nsew", padx=(0, 10), pady=(0, 8))
            inner = tk.Frame(item, bg=self.colors["card_soft"])
            inner.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
            icon_box = tk.Frame(inner, bg=color, width=42, height=42, highlightthickness=0)
            icon_box.pack(anchor="w")
            icon_box.pack_propagate(False)
            tk.Label(icon_box, text=icon, fg="white", bg=color, font=("Microsoft YaHei UI", 14, "bold")).pack(expand=True)
            label = tk.Label(inner, text=self.app.t(title_key), fg=self.colors["text"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 9, "bold"), wraplength=116, justify=tk.LEFT, anchor="w")
            label.pack(anchor="w", pady=(8, 2), fill=tk.X)
            hint = tk.Label(inner, text=self.app.t(hint_key), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 8), wraplength=122, justify=tk.LEFT, anchor="w")
            hint.pack(anchor="w", fill=tk.X)
            self._launcher_labels.append(label)
            self._launcher_hint_labels.append(hint)
            self._launcher_items.append((item, inner, icon_box, label, hint))
            self._bind_card(item, lambda e, cmd=command: cmd())
            self._bind_card(inner, lambda e, cmd=command: cmd())
            ToolTip(item, lambda key=hint_key: self.app.t(key))

    def _dialog_window(self, title, width=760, height=560, modal=True, resizable=True):
        win = create_dialog_window(
            self.app.root,
            title,
            self.colors,
            width=width,
            height=height,
            modal=modal,
            resizable=resizable,
            min_width=max(420, int(width * 0.72)),
            min_height=max(300, int(height * 0.68)),
        )
        try:
            if hasattr(self.app, "_set_window_icon"):
                self.app._set_window_icon(win)
        except Exception:
            pass
        return win

    def _focus_scope_items(self):
        current_scope = self.app.current_data_scope() if hasattr(self.app, "current_data_scope") else "personal"
        items = []
        for idx, item in enumerate(self.app.todos.get("items", [])):
            if item.get("done"):
                continue
            scope = str(item.get("scope", current_scope) or current_scope)
            if scope not in (current_scope, ""):
                continue
            items.append((idx, item))
        return items

    def _today_note_lines(self):
        today_key = date.today().strftime("%Y-%m-%d")
        note = self.app.notes.get(today_key, {})
        if not isinstance(note, dict):
            return []
        text = str(note.get("text", "") or "").strip()
        if not text:
            return []
        lines = [line.strip() for line in text.splitlines() if line.strip()]
        if note.get("time"):
            return [f"{note.get('time')}  {line}" for line in lines[:4]]
        return lines[:4]

    def _focus_plan_text(self):
        today_key = date.today().strftime("%Y-%m-%d")
        pending = self._focus_scope_items()
        top_tasks = [item.get("text", "").strip() for _, item in pending[:5] if item.get("text", "").strip()]
        event_lines = self._today_note_lines()
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        recent_docs = []
        try:
            recent_docs = [doc.get("title", "") for _, doc in sorted(docs_source.items(), key=lambda x: x[1].get("updated", ""), reverse=True)[:3] if doc.get("title")]
        except Exception:
            recent_docs = []
        if not top_tasks:
            top_tasks = [self.app.t("focus_no_pending_tasks")]
        if not event_lines:
            event_lines = [self.app.t("focus_no_today_schedule")]
        if not recent_docs:
            recent_docs = [self.app.t("focus_no_recent_docs")]
        task_lines = "\n".join(f"- [ ] {text}" for text in top_tasks)
        event_text = "\n".join(f"- {line}" for line in event_lines)
        doc_text = "\n".join(f"- {title}" for title in recent_docs)
        return self.app.t("focus_plan_template").format(
            date=today_key,
            name=self._display_user_name(),
            tasks=task_lines,
            schedule=event_text,
            docs=doc_text,
            mode=self.app.scope_label() if hasattr(self.app, "scope_label") else "",
        )

    def open_focus_planner(self):
        win = self._dialog_window(self.app.t("focus_planner_title"), 860, 700)
        win.minsize(720, 560)
        header = tk.Frame(win, bg=self.colors["card"])
        header.pack(fill=tk.X, padx=22, pady=(18, 8))
        tk.Label(header, text=self.app.t("focus_planner_title"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w")
        tk.Label(header, text=self.app.t("focus_planner_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=780, justify=tk.LEFT).pack(anchor="w", pady=(6, 0))

        summary = tk.Frame(win, bg=self.colors["card"])
        summary.pack(fill=tk.X, padx=22, pady=(0, 10))
        pending = len(self._focus_scope_items())
        today_events = len(self._today_note_lines())
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        stats = [
            (str(pending), self.app.t("focus_stat_pending")),
            (str(today_events), self.app.t("focus_stat_today_schedule")),
            (str(len(docs_source)), self.app.t("focus_stat_documents")),
        ]
        for value, label in stats:
            box = tk.Frame(summary, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
            box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
            tk.Label(box, text=value, font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["primary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=14, pady=(10, 0))
            tk.Label(box, text=label, font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=14, pady=(0, 10))

        editor = scrolledtext.ScrolledText(win, wrap=tk.WORD, font=("Microsoft YaHei UI", 10), relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], insertbackground=self.colors["text"])
        editor.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 12))
        editor.insert("1.0", self._focus_plan_text())

        footer = tk.Frame(win, bg=self.colors["card"])
        footer.pack(fill=tk.X, padx=22, pady=(0, 18))

        def content():
            return editor.get("1.0", tk.END).strip()

        def copy_plan():
            text = content()
            if not text:
                return
            self.app.root.clipboard_clear()
            self.app.root.clipboard_append(text)
            messagebox.showinfo(self.app.t("success_title"), self.app.t("copied_to_clipboard"))

        def save_to_today():
            text = content()
            if not text:
                return
            today_key = date.today().strftime("%Y-%m-%d")
            old_note = self.app.notes.get(today_key, {}) if isinstance(self.app.notes.get(today_key, {}), dict) else {}
            old_text = str(old_note.get("text", "") or "").rstrip()
            marker = self.app.t("focus_saved_marker")
            merged = (old_text + "\n\n" if old_text else "") + marker + "\n" + text
            self.app.notes[today_key] = {
                "text": merged,
                "time": old_note.get("time") or datetime.now().strftime("%H:%M"),
                "image": old_note.get("image"),
                "category": old_note.get("category") or "focus",
            }
            save_notes(self.app.notes)
            self.app.refresh_home()
            self.app.refresh_notification_badge()
            messagebox.showinfo(self.app.t("success_title"), self.app.t("focus_saved_to_today"))

        def save_as_doc():
            text = content()
            if not text:
                return
            doc_id = f"doc-focus-{datetime.now().strftime('%Y%m%d%H%M%S')}"
            title = self.app.t("focus_doc_title").format(date=date.today().strftime("%Y-%m-%d"))
            if isinstance(self.app.documents, dict):
                self.app.documents[doc_id] = {
                    "title": title,
                    "type": "doc",
                    "content": text,
                    "drawing": [],
                    "scope": self.app.current_data_scope() if hasattr(self.app, "current_data_scope") else "personal",
                    "owner": self._display_user_name(),
                    "updated": datetime.now().strftime("%Y-%m-%d %H:%M"),
                }
            save_documents(self.app.documents)
            self.app.refresh_home()
            messagebox.showinfo(self.app.t("success_title"), self.app.t("focus_saved_as_doc"))

        def add_top_tasks_to_todo():
            text = content()
            lines = []
            for line in text.splitlines():
                raw = line.strip()
                if raw.startswith("- [ ]"):
                    value = raw.replace("- [ ]", "", 1).strip()
                    if value and value != self.app.t("focus_no_pending_tasks"):
                        lines.append(value)
            existing = {str(item.get("text", "")).strip() for item in self.app.todos.get("items", [])}
            added = 0
            for line in lines[:8]:
                if line not in existing:
                    self.app.todos.setdefault("items", []).append(self._make_todo_item(line))
                    added += 1
            if added:
                save_todos(self.app.todos)
                self.refresh()
                self.app.refresh_notification_badge()
            messagebox.showinfo(self.app.t("success_title"), self.app.t("focus_tasks_added").format(count=added))

        create_button(footer, self.app.t("focus_add_tasks"), add_top_tasks_to_todo, self.colors).pack(side=tk.LEFT)
        create_button(footer, self.app.t("copy_report"), copy_plan, self.colors).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(footer, self.app.t("focus_save_today"), save_to_today, self.colors, filled=True).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(footer, self.app.t("save_as_document"), save_as_doc, self.colors).pack(side=tk.RIGHT)
        win.bind("<Control-s>", lambda e: save_to_today())
        win.bind("<Control-S>", lambda e: save_to_today())
        win.bind("<Control-Return>", lambda e: save_to_today())
        editor.focus_set()

    def open_app_center(self):
        win = self._dialog_window(self.app.t("home_app_center_title"), 820, 620)
        head = tk.Frame(win, bg=self.colors["card"])
        head.pack(fill=tk.X, padx=22, pady=(18, 12))
        tk.Label(head, text=self.app.t("home_app_center_title"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w")
        tk.Label(head, text=self.app.t("home_app_center_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"]).pack(anchor="w", pady=(4, 0))
        body = tk.Frame(win, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 22))
        apps = self._launcher_visible_defs()[:-1]
        for i, (icon, title_key, hint_key, command, color) in enumerate(apps):
            row = i // 3
            col = i % 3
            card = tk.Frame(body, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
            card.grid(row=row, column=col, sticky="nsew", padx=8, pady=8)
            body.grid_columnconfigure(col, weight=1)
            icon_box = tk.Frame(card, bg=color, width=42, height=42)
            icon_box.pack(anchor="w", padx=14, pady=(14, 8))
            icon_box.pack_propagate(False)
            tk.Label(icon_box, text=icon, bg=color, fg="white", font=("Microsoft YaHei UI", 13, "bold")).pack(expand=True)
            tk.Label(card, text=self.app.t(title_key), bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 12, "bold")).pack(anchor="w", padx=14)
            tk.Label(card, text=self.app.t(hint_key), bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=210, justify=tk.LEFT).pack(anchor="w", padx=14, pady=(4, 14))
            self._bind_card(card, lambda e, cmd=command, w=win: (w.destroy(), cmd()))

    def open_approval_center(self):
        pending = [(idx, item) for idx, item in enumerate(self.app.todos.get("items", [])) if not item.get("done")]
        win = self._dialog_window(self.app.t("approval_center_title"), 720, 560)
        tk.Label(win, text=self.app.t("approval_center_title"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w", padx=22, pady=(18, 6))
        tk.Label(win, text=self.app.t("approval_center_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=650, justify=tk.LEFT).pack(anchor="w", padx=22, pady=(0, 12))
        area = ScrollArea(win, self.colors)
        area.outer.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 12))
        if not pending:
            show_empty(area.inner, self.app.t("approval_empty"), self.colors)
        else:
            selected = {}
            for idx, item in pending:
                row = tk.Frame(area.inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
                row.pack(fill=tk.X, pady=5)
                var = tk.BooleanVar(value=True)
                selected[idx] = var
                tk.Checkbutton(row, variable=var, bg=self.colors["card_soft"], activebackground=self.colors["card_soft"], selectcolor=self.colors["card"]).pack(side=tk.LEFT, padx=10, pady=10)
                tk.Label(row, text=item.get("text", ""), bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10), wraplength=560, justify=tk.LEFT).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4, pady=10)
            def approve_selected():
                for idx, var in selected.items():
                    if var.get() and 0 <= idx < len(self.app.todos.get("items", [])):
                        self._set_todo_done(idx, True)
                save_todos(self.app.todos)
                self.refresh()
                win.destroy()
                messagebox.showinfo(self.app.t("success_title"), self.app.t("approval_done"))
            create_button(win, self.app.t("approval_done_btn"), approve_selected, self.colors, filled=True).pack(fill=tk.X, padx=22, pady=(0, 18))

    def _report_summary_values(self, period_days=1):
        today = date.today()
        start = today - timedelta(days=period_days - 1)
        start_key = start.strftime("%Y-%m-%d")
        end_key = today.strftime("%Y-%m-%d")
        todos = [item for item in self.app.todos.get("items", []) if isinstance(item, dict)]
        period_done = [item for item in todos if item.get("done") and start_key <= str(item.get("completed_at") or item.get("updated_at") or item.get("created_at") or "")[:10] <= end_key]
        period_created = [item for item in todos if start_key <= str(item.get("created_at") or "")[:10] <= end_key]
        pending = sum(1 for item in todos if not item.get("done"))
        done = len(period_done)
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        events = []
        for offset in range(period_days):
            ds = (start + timedelta(days=offset)).strftime("%Y-%m-%d")
            if ds in self.app.notes:
                events.append((ds, self.app.notes.get(ds, {})))
        return today, start, pending, done, docs_source, events, len(period_created)

    def _build_report_text(self, report_type):
        is_weekly = report_type == "weekly"
        today, start, pending, done, docs_source, events, created = self._report_summary_values(7 if is_weekly else 1)
        period = f"{start.strftime('%Y-%m-%d')} ~ {today.strftime('%Y-%m-%d')}" if is_weekly else today.strftime("%Y-%m-%d")
        event_lines = []
        for ds, item in events[:8]:
            text = str(item.get("text", "")).splitlines()[0] if isinstance(item, dict) else str(item)
            event_lines.append(f"- {ds} {item.get('time', '') if isinstance(item, dict) else ''} {text}".strip())
        if not event_lines:
            event_lines.append(f"- {self.app.t('report_no_events')}")
        total_tasks = max(created, done + pending)
        completion_rate = f"{done}/{total_tasks} ({int(done * 100 / total_tasks) if total_tasks else 0}%)"
        username = self._display_user_name()
        today_key = today.strftime("%Y-%m-%d")
        attendance_item = next((i for i in self.app.attendance.get("items", []) if i.get("date") == today_key and i.get("username") == username), {})
        attendance_line = self.app.t("attendance_row_text").format(check_in=attendance_item.get("check_in") or "--:--", check_out=attendance_item.get("check_out") or "--:--")
        attendance_line += " · " + self.app.t("attendance_duration_label") + " " + self._format_duration(self._attendance_duration_minutes(attendance_item))
        return self.app.t("daily_weekly_report_template").format(
            report_type=self.app.t("weekly_report") if is_weekly else self.app.t("daily_report"),
            name=username,
            period=period,
            done=done,
            pending=pending,
            completion_rate=completion_rate,
            attendance_line=attendance_line,
            docs=len(docs_source),
            events=len(events),
            event_lines="\n".join(event_lines),
            focus_task_lines=self._top_pending_task_lines(),
            tomorrow_focus=self.app.t("report_default_focus"),
        )

    def open_work_report(self):
        win = self._dialog_window(self.app.t("report_center_title"), 820, 680)
        tk.Label(win, text=self.app.t("report_center_title"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w", padx=22, pady=(18, 6))
        tk.Label(win, text=self.app.t("report_center_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=740, justify=tk.LEFT).pack(anchor="w", padx=22, pady=(0, 12))

        top = tk.Frame(win, bg=self.colors["card"])
        top.pack(fill=tk.X, padx=22, pady=(0, 10))
        report_type = tk.StringVar(value="daily")
        title_var = tk.StringVar(value=self.app.t("daily_report"))
        period_var = tk.StringVar(value=date.today().strftime("%Y-%m-%d"))
        def refresh_template():
            title_var.set(self.app.t("weekly_report") if report_type.get() == "weekly" else self.app.t("daily_report"))
            period_var.set((date.today() - timedelta(days=6)).strftime("%Y-%m-%d") + " ~ " + date.today().strftime("%Y-%m-%d") if report_type.get() == "weekly" else date.today().strftime("%Y-%m-%d"))
            box.delete("1.0", tk.END)
            box.insert("1.0", self._build_report_text(report_type.get()))
        for text_key, value in (("daily_report", "daily"), ("weekly_report", "weekly")):
            tk.Radiobutton(top, text=self.app.t(text_key), variable=report_type, value=value, command=refresh_template, bg=self.colors["card"], activebackground=self.colors["card"], selectcolor=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold")).pack(side=tk.LEFT, padx=(0, 12))
        tk.Label(top, text=self.app.t("report_period"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9)).pack(side=tk.LEFT, padx=(18, 6))
        tk.Entry(top, textvariable=period_var, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10), width=24).pack(side=tk.LEFT, ipady=6)

        title_row = tk.Frame(win, bg=self.colors["card"])
        title_row.pack(fill=tk.X, padx=22, pady=(0, 8))
        tk.Label(title_row, text=self.app.t("report_title_label"), bg=self.colors["card"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9)).pack(side=tk.LEFT)
        tk.Entry(title_row, textvariable=title_var, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10)).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(10, 0), ipady=6)

        box = scrolledtext.ScrolledText(win, wrap=tk.WORD, font=("Microsoft YaHei UI", 10), relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"])
        box.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 12))
        box.insert("1.0", self._build_report_text("daily"))

        actions = tk.Frame(win, bg=self.colors["card"])
        actions.pack(fill=tk.X, padx=22, pady=(0, 18))
        def copy_report():
            self.app.root.clipboard_clear()
            self.app.root.clipboard_append(box.get("1.0", tk.END).strip())
            messagebox.showinfo(self.app.t("success_title"), self.app.t("copied_to_clipboard"))
        def save_report():
            item = {
                "id": datetime.now().strftime("report-%Y%m%d%H%M%S"),
                "type": report_type.get(),
                "title": title_var.get().strip() or self.app.t("daily_report"),
                "period": period_var.get().strip(),
                "content": box.get("1.0", tk.END).strip(),
                "author": self._display_user_name(),
                "created": datetime.now().strftime("%Y-%m-%d %H:%M"),
            }
            self.app.reports.setdefault("items", []).insert(0, item)
            save_reports(self.app.reports)
            messagebox.showinfo(self.app.t("success_title"), self.app.t("report_saved"))
        def save_as_doc():
            content = box.get("1.0", tk.END).strip()
            if not content:
                return
            doc_id = f"doc-report-{datetime.now().strftime('%Y%m%d%H%M%S')}"
            self.app.documents[doc_id] = {
                "title": title_var.get().strip() or self.app.t("daily_report"),
                "type": "doc",
                "content": content,
                "drawing": [],
                "scope": self.app.current_data_scope() if hasattr(self.app, "current_data_scope") else "personal",
                "owner": self._display_user_name(),
                "updated": datetime.now().strftime("%Y-%m-%d %H:%M"),
            }
            save_documents(self.app.documents)
            self.app.refresh_home()
            messagebox.showinfo(self.app.t("success_title"), self.app.t("report_doc_saved"))
        create_button(actions, self.app.t("report_history"), self.open_report_history, self.colors).pack(side=tk.LEFT)
        create_button(actions, self.app.t("save_as_document"), save_as_doc, self.colors).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(actions, self.app.t("save_report"), save_report, self.colors, filled=True).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(actions, self.app.t("copy_report"), copy_report, self.colors).pack(side=tk.RIGHT)
        win.bind("<Control-Return>", lambda e: save_report())
        win.bind("<Control-s>", lambda e: save_report())
        win.bind("<Control-S>", lambda e: save_report())

    def open_report_history(self):
        win = self._dialog_window(self.app.t("report_history"), 800, 590)
        tk.Label(win, text=self.app.t("report_history"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w", padx=22, pady=(18, 4))
        tk.Label(win, text=self.app.t("report_history_hint"), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=480, justify=tk.LEFT).pack(anchor="w", padx=22, pady=(0, 10))
        area = ScrollArea(win, self.colors)
        area.outer.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 18))
        items = list(self.app.reports.get("items", []))
        if not items:
            show_empty(area.inner, self.app.t("report_history_empty"), self.colors)
            return

        def view_report(item):
            dlg = self._dialog_window(item.get("title") or self.app.t("report_history"), 760, 560)
            box = scrolledtext.ScrolledText(dlg, wrap=tk.WORD, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10))
            box.pack(fill=tk.BOTH, expand=True, padx=18, pady=18)
            box.insert("1.0", item.get("content", ""))
            box.configure(state=tk.DISABLED)

        def copy_report_item(item):
            self.app.root.clipboard_clear()
            self.app.root.clipboard_append(item.get("content", ""))
            messagebox.showinfo(self.app.t("success_title"), self.app.t("copied_to_clipboard"))

        def delete_report_item(item):
            if not messagebox.askyesno(self.app.t("confirm_title"), self.app.t("report_delete_confirm")):
                return
            rid = item.get("id")
            self.app.reports["items"] = [r for r in self.app.reports.get("items", []) if r.get("id") != rid]
            save_reports(self.app.reports)
            win.destroy()
            self.open_report_history()

        def save_report_item_as_doc(item):
            doc_id = f"doc-report-history-{datetime.now().strftime('%Y%m%d%H%M%S')}"
            doc = {
                "title": item.get("title") or self.app.t("daily_report"),
                "type": "doc",
                "content": item.get("content", ""),
                "drawing": [],
                "scope": self.app.current_data_scope() if hasattr(self.app, "current_data_scope") else "personal",
                "owner": self._display_user_name(),
                "updated": datetime.now().strftime("%Y-%m-%d %H:%M"),
            }
            if isinstance(self.app.documents, dict):
                self.app.documents[doc_id] = doc
            else:
                doc["id"] = doc_id
                self.app.documents.append(doc)
            save_documents(self.app.documents)
            try:
                self.app.refresh_home()
            except Exception:
                pass
            messagebox.showinfo(self.app.t("success_title"), self.app.t("report_doc_saved"))

        for item in items[:80]:
            card = tk.Frame(area.inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
            card.pack(fill=tk.X, pady=6)
            top = tk.Frame(card, bg=self.colors["card_soft"])
            top.pack(fill=tk.X, padx=12, pady=(10, 2))
            tk.Label(top, text=f"{item.get('title','')} · {item.get('period','')}", bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold")).pack(side=tk.LEFT, fill=tk.X, expand=True, anchor="w")
            tk.Button(top, text=self.app.t("report_view"), command=lambda it=item: view_report(it), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 8, "bold"), cursor="hand2").pack(side=tk.RIGHT, padx=(6, 0))
            tk.Button(top, text=self.app.t("report_delete"), command=lambda it=item: delete_report_item(it), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["danger"], font=("Microsoft YaHei UI", 8, "bold"), cursor="hand2").pack(side=tk.RIGHT, padx=(6, 0))
            tk.Button(top, text=self.app.t("save_as_document"), command=lambda it=item: save_report_item_as_doc(it), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 8, "bold"), cursor="hand2").pack(side=tk.RIGHT, padx=(6, 0))
            tk.Button(top, text=self.app.t("copy_report"), command=lambda it=item: copy_report_item(it), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 8, "bold"), cursor="hand2").pack(side=tk.RIGHT)
            tk.Label(card, text=item.get("content", "")[:260], bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9), wraplength=700, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(0, 10))

    def _attendance_today(self):
        today = date.today().strftime("%Y-%m-%d")
        username = self._display_user_name()
        items = self.app.attendance.setdefault("items", [])
        for item in items:
            if item.get("date") == today and item.get("username") == username:
                return item
        item = {"date": today, "username": username, "check_in": "", "check_out": "", "status": ""}
        items.insert(0, item)
        return item

    def _attendance_duration_minutes(self, item):
        try:
            start = item.get("check_in") or ""
            end = item.get("check_out") or ""
            if not start or not end:
                return 0
            t1 = datetime.strptime(start, "%H:%M")
            t2 = datetime.strptime(end, "%H:%M")
            minutes = int((t2 - t1).total_seconds() // 60)
            return max(minutes, 0)
        except Exception:
            return 0

    def _format_duration(self, minutes):
        if minutes <= 0:
            return "--"
        h, m = divmod(minutes, 60)
        return f"{h}h {m:02d}m"

    def _attendance_month_summary(self):
        username = self._display_user_name()
        prefix = date.today().strftime("%Y-%m")
        records = [i for i in self.app.attendance.get("items", []) if i.get("username") == username and str(i.get("date", "")).startswith(prefix)]
        complete = [i for i in records if i.get("check_in") and i.get("check_out")]
        total_minutes = sum(self._attendance_duration_minutes(i) for i in complete)
        return {
            "month": prefix,
            "records": records,
            "record_count": len(records),
            "complete_count": len(complete),
            "total_minutes": total_minutes,
            "avg_minutes": int(total_minutes / len(complete)) if complete else 0,
        }

    def _build_attendance_report_text(self):
        summary = self._attendance_month_summary()
        lines = [
            self.app.t("attendance_month_report_title").format(month=summary["month"]),
            "",
            self.app.t("attendance_records_count").format(count=summary["record_count"]),
            self.app.t("attendance_complete_days").format(count=summary["complete_count"]),
            self.app.t("attendance_work_hours").format(hours=self._format_duration(summary["total_minutes"])),
            self.app.t("attendance_avg_hours").format(hours=self._format_duration(summary["avg_minutes"])),
            "",
            self.app.t("attendance_recent"),
        ]
        for item in summary["records"][:31]:
            lines.append(f"- {item.get('date', '')}: {self.app.t('attendance_row_text').format(check_in=item.get('check_in') or '--:--', check_out=item.get('check_out') or '--:--')} · {self.app.t('attendance_duration_label')} {self._format_duration(self._attendance_duration_minutes(item))}")
        return "\n".join(lines)

    def _save_attendance_report_as_doc(self):
        content = self._build_attendance_report_text()
        title = self.app.t("attendance_month_report_title").format(month=date.today().strftime("%Y-%m"))
        doc_id = f"doc-attendance-{datetime.now().strftime('%Y%m%d%H%M%S')}"
        doc = {
            "title": title,
            "type": "doc",
            "content": content,
            "drawing": [],
            "scope": self.app.current_data_scope() if hasattr(self.app, "current_data_scope") else "personal",
            "owner": self._display_user_name(),
            "updated": datetime.now().strftime("%Y-%m-%d %H:%M"),
        }
        if isinstance(self.app.documents, dict):
            self.app.documents[doc_id] = doc
        else:
            doc["id"] = doc_id
            self.app.documents.append(doc)
        save_documents(self.app.documents)
        try:
            self.app.refresh_home()
        except Exception:
            pass
        messagebox.showinfo(self.app.t("success_title"), self.app.t("attendance_exported_doc"))

    def open_attendance_center(self):
        win = self._dialog_window(self.app.t("attendance_title"), 720, 560)
        tk.Label(win, text=self.app.t("attendance_title"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w", padx=22, pady=(18, 6))
        tk.Label(win, text=self.app.t("attendance_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=650, justify=tk.LEFT).pack(anchor="w", padx=22, pady=(0, 12))
        month_summary = self._attendance_month_summary()
        summary_card = tk.Frame(win, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
        summary_card.pack(fill=tk.X, padx=22, pady=(0, 12))
        summary_inner = tk.Frame(summary_card, bg=self.colors["card_soft"])
        summary_inner.pack(fill=tk.X, padx=14, pady=12)
        summary_items = [
            (self.app.t("attendance_records_short"), str(month_summary["record_count"])),
            (self.app.t("attendance_complete_short"), str(month_summary["complete_count"])),
            (self.app.t("attendance_total_short"), self._format_duration(month_summary["total_minutes"])),
            (self.app.t("attendance_avg_short"), self._format_duration(month_summary["avg_minutes"])),
        ]
        for label_text, value_text in summary_items:
            item_box = tk.Frame(summary_inner, bg=self.colors["card_soft"])
            item_box.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
            tk.Label(item_box, text=value_text, bg=self.colors["card_soft"], fg=self.colors["primary"], font=("Microsoft YaHei UI", 14, "bold")).pack(anchor="w")
            tk.Label(item_box, text=label_text, bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 8)).pack(anchor="w")
        today_item = self._attendance_today()
        status = tk.Frame(win, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
        status.pack(fill=tk.X, padx=22, pady=(0, 12))
        status_label = tk.Label(status, text="", bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 12, "bold"))
        status_label.pack(anchor="w", padx=16, pady=(14, 4))
        hint_label = tk.Label(status, text="", bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9))
        hint_label.pack(anchor="w", padx=16, pady=(0, 14))
        def refresh_status():
            status_label.config(text=self.app.t("attendance_today_status").format(check_in=today_item.get("check_in") or "--:--", check_out=today_item.get("check_out") or "--:--"))
            if today_item.get("check_in") and today_item.get("check_out"):
                hint_label.config(text=self.app.t("attendance_done_hint"))
            elif today_item.get("check_in"):
                hint_label.config(text=self.app.t("attendance_checked_in_hint"))
            else:
                hint_label.config(text=self.app.t("attendance_not_started_hint"))
        def mark(kind):
            now = datetime.now().strftime("%H:%M")
            if kind == "in":
                today_item["check_in"] = today_item.get("check_in") or now
            else:
                today_item["check_out"] = now
            today_item["status"] = "done" if today_item.get("check_in") and today_item.get("check_out") else "working"
            save_attendance(self.app.attendance)
            refresh_status()
            self.refresh()
            messagebox.showinfo(self.app.t("success_title"), self.app.t("attendance_saved"))
        btns = tk.Frame(win, bg=self.colors["card"])
        btns.pack(fill=tk.X, padx=22, pady=(0, 12))
        create_button(btns, self.app.t("attendance_check_in"), lambda: mark("in"), self.colors, filled=True).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        create_button(btns, self.app.t("attendance_check_out"), lambda: mark("out"), self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        def clear_today_attendance():
            if not messagebox.askyesno(self.app.t("confirm_title"), self.app.t("attendance_clear_confirm")):
                return
            today_item["check_in"] = ""
            today_item["check_out"] = ""
            today_item["status"] = ""
            save_attendance(self.app.attendance)
            refresh_status()
            self.refresh()
        create_button(btns, self.app.t("attendance_clear_today"), clear_today_attendance, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10))
        create_button(btns, self.app.t("attendance_export_month_report"), self._save_attendance_report_as_doc, self.colors).pack(side=tk.LEFT, fill=tk.X, expand=True)
        refresh_status()

        tk.Label(win, text=self.app.t("attendance_recent"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 12, "bold")).pack(anchor="w", padx=22, pady=(8, 6))
        area = ScrollArea(win, self.colors)
        area.outer.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 18))
        username = self._display_user_name()
        recent = [i for i in self.app.attendance.get("items", []) if i.get("username") == username][:14]
        if not recent:
            show_empty(area.inner, self.app.t("attendance_empty"), self.colors)
        else:
            for item in recent:
                row = tk.Frame(area.inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
                row.pack(fill=tk.X, pady=5)
                tk.Label(row, text=item.get("date", ""), bg=self.colors["card_soft"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold"), width=16, anchor="w").pack(side=tk.LEFT, padx=12, pady=10)
                info_text = self.app.t("attendance_row_text").format(check_in=item.get("check_in") or "--:--", check_out=item.get("check_out") or "--:--")
                info_text += " · " + self.app.t("attendance_duration_label") + " " + self._format_duration(self._attendance_duration_minutes(item))
                tk.Label(row, text=info_text, bg=self.colors["card_soft"], fg=self.colors["text_secondary"], font=("Microsoft YaHei UI", 9)).pack(side=tk.LEFT, padx=6)

    def open_meeting_assistant(self):
        win = self._dialog_window(self.app.t("meeting_assistant_title"), 620, 520)
        tk.Label(win, text=self.app.t("meeting_assistant_title"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(anchor="w", padx=22, pady=(18, 8))
        form = tk.Frame(win, bg=self.colors["card"])
        form.pack(fill=tk.X, padx=22, pady=(0, 12))
        date_var = tk.StringVar(value=date.today().strftime("%Y-%m-%d"))
        time_var = tk.StringVar(value="09:00")
        title_var = tk.StringVar(value=self.app.t("meeting_default_title"))
        def row(label, var):
            line = tk.Frame(form, bg=self.colors["card"])
            line.pack(fill=tk.X, pady=6)
            tk.Label(line, text=label, width=12, anchor="w", bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold")).pack(side=tk.LEFT)
            ent = tk.Entry(line, textvariable=var, font=("Microsoft YaHei UI", 10), relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"])
            ent.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=7)
            return ent
        row(self.app.t("date_label"), date_var)
        row(self.app.t("time_label"), time_var)
        row(self.app.t("meeting_title_label"), title_var)
        tk.Label(win, text=self.app.t("meeting_agenda_label"), bg=self.colors["card"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10, "bold")).pack(anchor="w", padx=22)
        agenda = scrolledtext.ScrolledText(win, height=8, wrap=tk.WORD, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], font=("Microsoft YaHei UI", 10))
        agenda.pack(fill=tk.BOTH, expand=True, padx=22, pady=(6, 12))
        agenda.insert("1.0", self.app.t("meeting_agenda_placeholder"))
        def save_meeting():
            ds = date_var.get().strip()
            tm = time_var.get().strip()
            try:
                datetime.strptime(ds, "%Y-%m-%d")
                datetime.strptime(tm, "%H:%M")
            except Exception:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("invalid_date_or_time"))
                return
            text = f"{title_var.get().strip()}\n{agenda.get('1.0', tk.END).strip()}".strip()
            if ds in self.app.notes and self.app.notes[ds].get("text"):
                old_note = self.app.notes.get(ds, {})
                merged = str(old_note.get("text", "")).rstrip() + "\n\n" + text
                self.app.notes[ds] = {"text": merged, "time": old_note.get("time") or tm, "image": old_note.get("image"), "category": old_note.get("category") or "meeting"}
            else:
                self.app.notes[ds] = {"text": text, "time": tm, "image": None, "category": "meeting"}
            save_notes(self.app.notes)
            self.app.refresh_home()
            win.destroy()
            self.app.open_calendar_date(ds)
        create_button(win, self.app.t("save_meeting_btn"), save_meeting, self.colors, filled=True).pack(fill=tk.X, padx=22, pady=(0, 18))

    def _add_enterprise_notification(self, title_key, content):
        self.app.enterprise.setdefault("notifications", []).insert(0, {
            "title": self.app.t(title_key),
            "content": content,
            "time": datetime.now().strftime("%Y-%m-%d %H:%M"),
            "kind": "company",
        })
        save_enterprise(self.app.enterprise)
        self.refresh()

    def open_survey_assistant(self):
        text = prompt_text_dialog(
            self.app.root,
            self.app.t("survey_title"),
            self.app.t("survey_prompt"),
            self.colors,
            confirm_text=self.app.t("confirm_btn"),
            cancel_text=self.app.t("cancel"),
            width=540,
            height=250,
        )
        if text and text.strip():
            self._add_enterprise_notification("survey_title", text.strip())
            messagebox.showinfo(self.app.t("success_title"), self.app.t("survey_created"))

    def open_vote_assistant(self):
        text = prompt_text_dialog(
            self.app.root,
            self.app.t("vote_title"),
            self.app.t("vote_prompt"),
            self.colors,
            confirm_text=self.app.t("confirm_btn"),
            cancel_text=self.app.t("cancel"),
            width=540,
            height=250,
        )
        if text and text.strip():
            self._add_enterprise_notification("vote_title", text.strip())
            messagebox.showinfo(self.app.t("success_title"), self.app.t("vote_created"))

    def _metric_card(self, parent, value, title, hint, command=None):
        card = tk.Frame(parent, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
        card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10), pady=(0, 2))
        tk.Label(card, text=str(value), font=("Microsoft YaHei UI", 20, "bold"), fg=self.colors["primary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=14, pady=(10, 0))
        tk.Label(card, text=title, font=("Microsoft YaHei UI", 9, "bold"), fg=self.colors["text"], bg=self.colors["card_soft"], anchor="w").pack(anchor="w", padx=14)
        tk.Label(card, text=hint, font=("Microsoft YaHei UI", 8), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=205, justify=tk.LEFT, anchor="w").pack(anchor="w", padx=14, pady=(2, 10))
        if command:
            self._bind_card(card, command)
        return card

    def _mode_stat_items(self):
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        role_text = self.app.t("admin_badge") if self.app.is_admin() else self.app.t("normal_staff")
        if getattr(self.app, "is_enterprise_mode", lambda: False)():
            company_name = str(self.app.enterprise.get("company_name") or self.app.t("company_not_created")).strip()
            team_count = len(self.app.enterprise.get("employees", []))
            notice_count = len(self.app.enterprise.get("notifications", []))
            return [
                (self.app.t("mode_enterprise"), self.app.t("home_mode_stat_mode")),
                (role_text, self.app.t("home_mode_stat_role")),
                (company_name, self.app.t("home_mode_stat_company")),
                (str(team_count), self.app.t("home_mode_stat_team")),
            ]
        pending = sum(1 for item in self.app.todos.get("items", []) if not item.get("done"))
        upcoming = len(self._collect_events(days=30))
        return [
            (self.app.t("mode_personal"), self.app.t("home_mode_stat_mode")),
            (role_text, self.app.t("home_mode_stat_role")),
            (str(pending), self.app.t("home_mode_stat_focus")),
            (str(upcoming), self.app.t("home_mode_stat_events")),
        ]

    def _mode_action_defs(self):
        if getattr(self.app, "is_enterprise_mode", lambda: False)():
            primary = (self.app.t("home_mode_action_open_company"), lambda: self.app.switch_page("enterprise"), True)
            secondary = (self.app.t("home_mode_action_open_chat"), lambda: self.app.switch_page("chat") if self.app.is_page_available("chat") else self.app.switch_page("enterprise"), False)
            return primary, secondary
        if not getattr(self.app, "has_enterprise_account", lambda: False)():
            primary = (self.app.t("join_enterprise_btn"), self.app.show_join_enterprise_dialog, True)
            secondary = (self.app.t("create_enterprise_btn"), self.app.show_create_enterprise_dialog, False)
            return primary, secondary
        primary = (self.app.t("home_mode_action_switch_enterprise"), lambda: self.app.set_app_mode("enterprise"), True)
        secondary = (self.app.t("home_mode_action_open_docs"), lambda: self.app.switch_page("files"), False)
        return primary, secondary

    def _build_mode_focus_card(self):
        if hasattr(self, "mode_card"):
            for child in self.mode_card.winfo_children():
                child.destroy()
        shell = tk.Frame(self.mode_card, bg=self.colors["card"])
        shell.pack(fill=tk.BOTH, expand=True, padx=18, pady=16)
        self.mode_shell = shell
        left = tk.Frame(shell, bg=self.colors["card"])
        left.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.mode_title = tk.Label(left, text="", font=("Microsoft YaHei UI", 14, "bold"), fg=self.colors["text"], bg=self.colors["card"])
        self.mode_title.pack(anchor="w")
        self.mode_subtitle = tk.Label(left, text="", font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=760, justify=tk.LEFT)
        self.mode_subtitle.pack(anchor="w", pady=(4, 12))
        self.mode_stats_row = tk.Frame(left, bg=self.colors["card"])
        self.mode_stats_row.pack(fill=tk.X)

        right = tk.Frame(shell, bg=self.colors["card"])
        right.pack(side=tk.RIGHT, padx=(16, 0), fill=tk.Y)
        self.mode_pill = tk.Label(right, text="", font=("Microsoft YaHei UI", 9, "bold"), fg=self.colors["primary"], bg=self.colors["primary_light"], padx=12, pady=6)
        self.mode_pill.pack(anchor="e")
        self.mode_action_wrap = tk.Frame(right, bg=self.colors["card"])
        self.mode_action_wrap.pack(anchor="e", pady=(12, 0))
        self._refresh_mode_focus_card()

    def _refresh_mode_focus_card(self):
        if not hasattr(self, "mode_stats_row"):
            return
        enterprise_mode = getattr(self.app, "is_enterprise_mode", lambda: False)()
        self.mode_title.config(text=self.app.t("home_mode_enterprise_title") if enterprise_mode else self.app.t("home_mode_personal_title"))
        self.mode_subtitle.config(text=self.app.t("home_mode_enterprise_hint") if enterprise_mode else self.app.t("home_mode_personal_hint"))
        self.mode_pill.config(text=self.app.t("mode_enterprise") if enterprise_mode else self.app.t("mode_personal"))
        for child in self.mode_stats_row.winfo_children():
            child.destroy()
        stats = self._mode_stat_items()
        for i, (value, label_text) in enumerate(stats):
            box = tk.Frame(self.mode_stats_row, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
            box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10 if i < len(stats) - 1 else 0))
            tk.Label(box, text=value, font=("Microsoft YaHei UI", 12, "bold"), fg=self.colors["primary"], bg=self.colors["card_soft"], wraplength=150, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(10, 0))
            tk.Label(box, text=label_text, font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(3, 10))
        for child in self.mode_action_wrap.winfo_children():
            child.destroy()
        primary, secondary = self._mode_action_defs()
        create_button(self.mode_action_wrap, primary[0], primary[1], self.colors, filled=primary[2]).pack(fill=tk.X)
        create_button(self.mode_action_wrap, secondary[0], secondary[1], self.colors, filled=secondary[2]).pack(fill=tk.X, pady=(8, 0))

    def _build_hero_metrics(self):
        if not hasattr(self, "metrics_row"):
            return
        for child in self.metrics_row.winfo_children():
            child.destroy()
        pending = sum(1 for item in self.app.todos.get("items", []) if not item.get("done"))
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        notification_count = len(self.app.get_home_notifications()) if hasattr(self.app, "get_home_notifications") else 0
        metrics = [
            (len(self.app.notes), self.app.t("overview_schedule"), self.app.t("jump_to_calendar"), lambda e: self.app.switch_page("calendar")),
            (pending, self.app.t("overview_pending_tasks"), self.app.t("jump_to_tasks"), lambda e: self.focus_todo()),
            (len(docs_source), self.app.t("overview_documents"), self.app.t("jump_to_documents"), lambda e: self.app.switch_page("files")),
        ]
        if getattr(self.app, "is_enterprise_mode", lambda: False)():
            metrics.append((len(self.app.enterprise.get("notifications", [])), self.app.t("notification_center"), self.app.t("notification_center"), lambda e: self.app.switch_page("enterprise")))
        else:
            metrics.append((notification_count, self.app.t("notification_center"), self.app.t("jump_to_calendar"), lambda e: self.open_all_notifications()))
        for value, title, hint, command in metrics:
            self._metric_card(self.metrics_row, value, title, hint, command)

    def _build_hero_actions(self):
        for child in self.action_row.winfo_children():
            child.destroy()
        for idx, (name, icon, key, command) in enumerate(self._enabled_quick_defs()):
            btn = self._hero_button(self.action_row, self._quick_label(name, key), command, filled=False)
            btn.pack(side=tk.LEFT, padx=(0, 10), pady=(0, 8))

    def _build_todo_input(self, header):
        self.todo_box = tk.Frame(header, bg=self.colors["bg"], highlightthickness=1, highlightbackground=self.colors["border"])
        self.todo_box.pack(side=tk.RIGHT)
        tk.Frame(self.todo_box, width=12, bg=self.colors["bg"]).pack(side=tk.LEFT)
        self.todo_input = tk.Entry(self.todo_box, font=("Microsoft YaHei UI", 10), width=24, relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text_secondary"])
        self.todo_input.pack(side=tk.LEFT, ipady=7, padx=(0, 6))
        self.todo_input.insert(0, self.app.t("new_task_placeholder"))
        self.todo_input.bind("<FocusIn>", self._todo_focus_in)
        self.todo_input.bind("<KeyPress>", self._todo_focus_in)
        self.todo_input.bind("<FocusOut>", self._todo_focus_out)
        self.todo_input.bind("<Return>", lambda e: self.add_todo())
        self.todo_add_btn = tk.Button(self.todo_box, text=self.app.t("home_add_todo"), command=self.add_todo, bg=self.colors["primary"], fg="white", activebackground=self.colors["primary_dark"], activeforeground="white", relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 10, "bold"), padx=12, pady=8, cursor="hand2")
        self.todo_add_btn.pack(side=tk.LEFT)
        self.todo_batch_btn = tk.Button(self.todo_box, text=self.app.t("home_batch_add_todo"), command=self.open_batch_todo_dialog, bg=self.colors["card_soft"], fg=self.colors["primary"], activebackground=self.colors["primary_light"], activeforeground=self.colors["primary"], relief=tk.FLAT, bd=0, font=("Microsoft YaHei UI", 10, "bold"), padx=12, pady=8, cursor="hand2")
        self.todo_batch_btn.pack(side=tk.LEFT, padx=(8, 0))

    def _todo_focus_in(self, event=None):
        if self.todo_input.get() == self.app.t("new_task_placeholder") or self.todo_input.get() in self.app.all_values("new_task_placeholder"):
            self.todo_input.delete(0, tk.END)
            self.todo_input.config(fg=self.colors["text"])

    def _todo_focus_out(self, event=None):
        if not self.todo_input.get().strip():
            self.todo_input.insert(0, self.app.t("new_task_placeholder"))
            self.todo_input.config(fg=self.colors["text_secondary"])

    def focus_todo(self):
        self.app.switch_page("home")
        self._todo_focus_in()
        self.todo_input.focus_set()

    def set_status(self, text, fg, bg):
        self.hero_status.config(text=text, fg=fg, bg=bg)

    def refresh(self):
        self._update_home_greeting()
        self._build_hero_metrics()
        self._build_module_launcher()
        self._build_latest_update_card()
        for box in [self.events_list, self.todos_list, self.notifications_list, self.docs_list]:
            for child in box.winfo_children():
                child.destroy()
        self._render_events()
        self._render_todos()
        self._render_notifications()
        self._render_docs()

    def update_language(self):
        # Rebuild the full page so every header, button, subtitle, pager,
        # and open-all control refreshes correctly in the selected language.
        for child in list(self.frame.winfo_children()):
            child.destroy()
        self._build()
        self.refresh()
        try:
            self.on_app_resize(self.app.root.winfo_width(), self.app.root.winfo_height())
        except Exception:
            pass

    def _update_history_path(self):
        lang_suffix = {"zh_CN": "CN", "en_US": "EN", "id_ID": "ID"}.get(getattr(self.app, "lang_code", "zh_CN"), "CN")
        localized = BASE_DIR / "docs" / f"UPDATE_HISTORY_{lang_suffix}.md"
        if localized.exists():
            return localized
        return BASE_DIR / "docs" / "UPDATE_HISTORY_CN.md"

    def _read_latest_update_items(self, limit=3):
        """Return a compact, localized launch-note list for backward compatibility.

        v2026.06.20.104 keeps this method for older cards, but the visible
        update card now uses structured sections so users can quickly see what
        was fixed, what was improved, and what is planned next.
        """
        items = self.app.t("home_current_release_items")
        if isinstance(items, list) and items:
            return [(self.app.t("home_current_release"), item) for item in items[:limit]]
        return [(self.app.t("home_current_release"), self.app.t("home_update_no_detail"))]

    def _truncate_update_text(self, text, max_len=78):
        raw = " ".join(str(text or "").split())
        if len(raw) <= max_len:
            return raw
        return raw[: max_len - 1].rstrip() + "…"

    def _version_sort_key(self, text):
        match = re.search(r"v(\d{4})\.(\d{2})\.(\d{2})\.(\d{2,3})", str(text or ""))
        if not match:
            return (0, 0, 0, 0)
        return tuple(int(part) for part in match.groups())

    def _parse_update_history_sections(self, limit_versions=6, max_items_per_version=3):
        path = self._update_history_path()
        if not path.exists():
            return []
        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
        except Exception:
            return []

        heading_re = re.compile(r"^##\s+(.*)$")
        flat_re = re.compile(r"^-\s*(v\d{4}\.\d{2}\.\d{2}\.\d{2,3})\s*(?:[:：-]\s*)(.*)$")
        entries = []
        current = None

        def flush_current():
            nonlocal current
            if current:
                entries.append(current)
                current = None

        for raw_line in content.splitlines():
            stripped = raw_line.strip()
            if not stripped:
                continue
            heading_match = heading_re.match(stripped)
            flat_match = flat_re.match(stripped)
            if heading_match:
                flush_current()
                current = {"title": heading_match.group(1).strip(), "items": []}
                continue
            if flat_match:
                flush_current()
                entries.append({
                    "title": flat_match.group(1).strip(),
                    "items": [flat_match.group(2).strip()] if flat_match.group(2).strip() else [],
                })
                continue
            if stripped.startswith("- ") and current is not None:
                if len(current["items"]) < max_items_per_version:
                    current["items"].append(stripped[2:].strip())
        flush_current()
        if not entries:
            return []

        entries.sort(key=lambda entry: self._version_sort_key(entry.get("title", "")), reverse=True)
        deduped = []
        seen = set()
        for entry in entries:
            title = entry.get("title", "")
            version_match = re.search(r"v\d{4}\.\d{2}\.\d{2}\.\d{2,3}", title)
            dedupe_key = version_match.group(0) if version_match else title
            if dedupe_key in seen:
                continue
            seen.add(dedupe_key)
            deduped.append({"title": title, "items": entry.get("items", [])[:max_items_per_version]})
            if len(deduped) >= limit_versions:
                break
        return self._ensure_current_release_first(deduped, limit_versions)

    def _ensure_current_release_first(self, entries, limit_versions=6):
        current = str(self.app_version_text() or "").strip()
        if not current:
            return entries[:limit_versions]
        normalized = []
        for entry in entries:
            title = str(entry.get("title", ""))
            if current in title:
                normalized.insert(0, entry)
            else:
                normalized.append(entry)
        if not any(current in str(entry.get("title", "")) for entry in normalized):
            summary_items = []
            for _title, items, _accent in self._latest_update_sections():
                if items:
                    summary_items.append(str(items[0]))
            normalized.insert(0, {"title": current, "items": summary_items[:3] or [self.app.t("home_update_no_detail")]})
        seen_versions = set()
        result = []
        for entry in normalized:
            version_match = re.search(r"v\d{4}\.\d{2}\.\d{2}\.\d{2,3}", str(entry.get("title", "")))
            key = version_match.group(0) if version_match else str(entry.get("title", ""))
            if key in seen_versions:
                continue
            seen_versions.add(key)
            result.append(entry)
            if len(result) >= limit_versions:
                break
        return result

    def _latest_update_sections(self):
        """Structured update summary shown after login.

        The old flat list became hard to understand after many rapid versions.
        This keeps the card short and predictable: fixed / improved / next.
        """
        section_specs = [
            ("home_release_fixed_title", "home_release_fixed_items", self.colors["success"]),
            ("home_release_improved_title", "home_release_improved_items", self.colors["primary"]),
            ("home_release_next_title", "home_release_next_items", self.colors["warning"]),
        ]
        sections = []
        for title_key, items_key, color in section_specs:
            title = self.app.t(title_key)
            items = self.app.t(items_key)
            if not isinstance(items, list) or not items:
                continue
            sections.append((title, items[:3], color))
        if not sections:
            sections.append((self.app.t("home_current_release"), [self.app.t("home_update_no_detail")], self.colors["primary"]))
        return sections

    def app_version_text(self):
        return getattr(self.app, "app_version", "NexaOffice")

    def _build_latest_update_card(self):
        if not hasattr(self, "update_body"):
            return
        for child in self.update_body.winfo_children():
            child.destroy()
        box = tk.Frame(
            self.update_body,
            bg=self.colors["card"],
            highlightthickness=1,
            highlightbackground=self.colors["border"],
            bd=0,
        )
        box.pack(fill=tk.BOTH, expand=True)

        top = tk.Frame(box, bg=self.colors["card"])
        top.pack(fill=tk.X, padx=14, pady=(12, 6))

        left_meta = tk.Frame(top, bg=self.colors["card"])
        left_meta.pack(side=tk.LEFT, fill=tk.X, expand=True)
        tk.Label(
            left_meta,
            text=self.app.t("home_current_release"),
            font=("Microsoft YaHei UI", 10, "bold"),
            fg=self.colors["text"],
            bg=self.colors["card"],
        ).pack(anchor="w")
        tk.Label(
            left_meta,
            text=self._truncate_update_text(self.app.t("home_current_release_hint"), 64),
            fg=self.colors["text_secondary"],
            bg=self.colors["card"],
            font=("Microsoft YaHei UI", 8),
            wraplength=248,
            justify=tk.LEFT,
        ).pack(anchor="w", pady=(3, 0))

        badge = tk.Frame(top, bg=self.colors["primary_light"], highlightthickness=1, highlightbackground=self.colors["border"])
        badge.pack(side=tk.RIGHT, anchor="n")
        tk.Label(
            badge,
            text=self.app_version_text(),
            font=("Microsoft YaHei UI", 8, "bold"),
            fg=self.colors["primary"],
            bg=self.colors["primary_light"],
            padx=10,
            pady=5,
        ).pack()

        body = tk.Frame(box, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=12, pady=(0, 10))
        for title, items, accent in self._latest_update_sections():
            section = tk.Frame(
                body,
                bg=self.colors["card_soft"],
                highlightthickness=1,
                highlightbackground=self.colors["border"],
                bd=0,
            )
            section.pack(fill=tk.X, pady=(0, 6))

            head = tk.Frame(section, bg=self.colors["card_soft"])
            head.pack(fill=tk.X, padx=10, pady=(7, 4))
            tk.Label(
                head,
                text=title,
                fg=accent,
                bg=self.colors["card_soft"],
                font=("Microsoft YaHei UI", 8, "bold"),
            ).pack(side=tk.LEFT)

            for summary in items[:1]:
                line = tk.Frame(section, bg=self.colors["card_soft"])
                line.pack(fill=tk.X, padx=10, pady=(0, 7))
                tk.Label(
                    line,
                    text="•",
                    fg=accent,
                    bg=self.colors["card_soft"],
                    font=("Microsoft YaHei UI", 9, "bold"),
                ).pack(side=tk.LEFT, anchor="n")
                tk.Label(
                    line,
                    text=self._truncate_update_text(summary, 54),
                    fg=self.colors["text_secondary"],
                    bg=self.colors["card_soft"],
                    font=("Microsoft YaHei UI", 8),
                    wraplength=246,
                    justify=tk.LEFT,
                ).pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True, anchor="w")

        recent = self._parse_update_history_sections(limit_versions=2, max_items_per_version=1)
        if recent:
            preview = tk.Frame(
                body,
                bg=self.colors["card_soft"],
                highlightthickness=1,
                highlightbackground=self.colors["border"],
                bd=0,
            )
            preview.pack(fill=tk.BOTH, expand=True, pady=(2, 0))
            tk.Label(
                preview,
                text=self.app.t("home_update_open_history"),
                fg=self.colors["text"],
                bg=self.colors["card_soft"],
                font=("Microsoft YaHei UI", 8, "bold"),
            ).pack(anchor="w", padx=10, pady=(7, 3))
            for entry in recent:
                row = tk.Frame(preview, bg=self.colors["card_soft"])
                row.pack(fill=tk.X, padx=10, pady=(0, 4))
                tk.Label(row, text=entry["title"], fg=self.colors["primary"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 8, "bold")).pack(anchor="w")
                if entry.get("items"):
                    tk.Label(row, text=self._truncate_update_text(entry["items"][0], 58), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 8), wraplength=246, justify=tk.LEFT).pack(anchor="w", pady=(1, 0))

    def open_update_history(self):
        history = self._parse_update_history_sections(limit_versions=6, max_items_per_version=1)
        win = self._dialog_window(self.app.t("home_latest_update"), 660, 520)

        head = tk.Frame(win, bg=self.colors["card"])
        head.pack(fill=tk.X, padx=16, pady=(14, 8))
        tk.Label(head, text=self.app.t("home_latest_update"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)
        badge = tk.Frame(head, bg=self.colors["primary_light"], highlightthickness=1, highlightbackground=self.colors["border"])
        badge.pack(side=tk.RIGHT)
        tk.Label(badge, text=self.app_version_text(), font=("Microsoft YaHei UI", 8, "bold"), fg=self.colors["primary"], bg=self.colors["primary_light"], padx=10, pady=5).pack()

        summary_wrap = tk.Frame(win, bg=self.colors["card"])
        summary_wrap.pack(fill=tk.X, padx=14, pady=(0, 8))
        summary_wrap.grid_columnconfigure((0, 1, 2), weight=1)
        for idx, (title, items, accent) in enumerate(self._latest_update_sections()[:3]):
            card = tk.Frame(summary_wrap, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], bd=0)
            card.grid(row=0, column=idx, sticky="nsew", padx=(0 if idx == 0 else 6, 0), pady=0)
            tk.Label(card, text=title, fg=accent, bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 9, "bold")).pack(anchor="w", padx=10, pady=(8, 3))
            for summary in items[:1]:
                row = tk.Frame(card, bg=self.colors["card_soft"])
                row.pack(fill=tk.X, padx=10, pady=(0, 6))
                tk.Label(row, text="•", fg=accent, bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, anchor="n")
                tk.Label(row, text=self._truncate_update_text(summary, 46), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], font=("Microsoft YaHei UI", 8), wraplength=158, justify=tk.LEFT).pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)

        body = tk.Frame(win, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 12))
        canvas = tk.Canvas(body, bg=self.colors["bg"], highlightthickness=0)
        scrollbar = tk.Scrollbar(body, orient=tk.VERTICAL, command=canvas.yview)
        inner = tk.Frame(canvas, bg=self.colors["bg"])
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        window_id = canvas.create_window((0, 0), window=inner, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        canvas.bind("<Configure>", lambda e: canvas.itemconfig(window_id, width=max(e.width - 8, 420)))

        if not history:
            tk.Label(inner, text=self.app.t("home_update_no_history"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["bg"]).pack(anchor="w", padx=12, pady=12)
            return

        for index, entry in enumerate(history):
            is_current = index == 0 and self.app_version_text() in str(entry.get("title", ""))
            card_bg = self.colors["primary_light"] if is_current else self.colors["card"]
            card = tk.Frame(inner, bg=card_bg, highlightthickness=1, highlightbackground=self.colors["border"], bd=0)
            card.pack(fill=tk.X, padx=(0, 2), pady=(0, 8))
            top = tk.Frame(card, bg=card_bg)
            top.pack(fill=tk.X, padx=12, pady=(8, 5))
            tk.Label(top, text=entry["title"], font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["text"], bg=card_bg).pack(side=tk.LEFT)
            if is_current:
                tk.Label(top, text=self.app.t("home_current_release"), font=("Microsoft YaHei UI", 8, "bold"), fg=self.colors["primary"], bg=card_bg).pack(side=tk.RIGHT)
            for item in entry.get("items", [])[:2]:
                row = tk.Frame(card, bg=card_bg)
                row.pack(fill=tk.X, padx=14, pady=(0, 5))
                tk.Label(row, text="•", fg=self.colors["primary"], bg=card_bg, font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, anchor="n")
                tk.Label(row, text=self._truncate_update_text(item, 88), font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=card_bg, wraplength=480, justify=tk.LEFT).pack(side=tk.LEFT, padx=(6, 0), fill=tk.X, expand=True)

    def _collect_events(self, days=90):
        today = date.today()
        upcoming = []
        for ds, note in self.app.notes.items():
            try:
                d = datetime.strptime(ds, "%Y-%m-%d").date()
                if 0 <= (d - today).days <= days and self.app.matches_search(ds, note.get("time"), note.get("text")):
                    upcoming.append((ds, note))
            except Exception:
                pass
        upcoming.sort(key=lambda item: item[0])
        return upcoming

    def _open_full_list_popup(self, title, rows, action=None):
        win = self._dialog_window(title, 760, 620)
        head = tk.Frame(win, bg=self.colors["card"])
        head.pack(fill=tk.X, padx=22, pady=(18, 10))
        tk.Label(head, text=title, font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)
        canvas = tk.Canvas(win, bg=self.colors["card"], highlightthickness=0)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        bar = tk.Scrollbar(win, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=bar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(22, 0), pady=(0, 22))
        bar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 18), pady=(0, 22))
        window_id = canvas.create_window((0, 0), window=inner, anchor="nw")
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.bind("<Configure>", lambda e: canvas.itemconfig(window_id, width=max(e.width - 8, 420)))
        if not rows:
            show_empty(inner, self.app.t("home_no_all_items"), self.colors)
            return
        for primary, secondary, payload in rows:
            row = tk.Frame(inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
            row.pack(fill=tk.X, pady=5)
            tk.Label(row, text=primary, font=("Microsoft YaHei UI", 11, "bold"), fg=self.colors["text"], bg=self.colors["card_soft"], wraplength=650, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(10, 2))
            if secondary:
                tk.Label(row, text=secondary, font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=650, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(0, 10))
            else:
                tk.Frame(row, height=8, bg=self.colors["card_soft"]).pack()
            if action:
                self._bind_card(row, lambda e, value=payload, w=win: [w.destroy(), action(value)])

    def open_all_events(self):
        rows = []
        for ds, note in self._collect_events(days=365):
            try:
                d = datetime.strptime(ds, "%Y-%m-%d").date()
                rows.append((self.app.format_date(d), f"◷ {note.get('time', '--:--')}   {note.get('text', '')}", ds))
            except Exception:
                pass
        self._open_full_list_popup(self.app.t("home_upcoming"), rows, lambda ds: self.app.open_calendar_date(ds))

    def open_all_notifications(self):
        if hasattr(self.app, "show_notification_center"):
            self.app.show_notification_center()
            return
        rows = [(n.get("title", ""), f"{n.get('time', '')} · {n.get('content', '')}", n) for n in self.app.get_home_notifications()]
        self._open_full_list_popup(self.app.t("notification_center"), rows, lambda _: self.app.switch_page("enterprise"))

    def open_all_docs(self):
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        docs = sorted(docs_source.items(), key=lambda x: x[1].get("updated", ""), reverse=True)
        rows = [(self.app.display_doc_title(doc.get("title", "")), doc.get("updated", ""), did) for did, doc in docs]
        self._open_full_list_popup(self.app.t("home_documents"), rows, lambda did: self.app.open_document(did))

    def open_all_overview(self):
        self.app.switch_page("home")
        rows = [
            (self.app.t("overview_schedule"), self.app.t("jump_to_calendar"), "calendar"),
            (self.app.t("overview_pending_tasks"), self.app.t("jump_to_tasks"), "tasks"),
            (self.app.t("overview_documents"), self.app.t("jump_to_documents"), "files"),
        ]
        if not hasattr(self.app, "is_page_available") or self.app.is_page_available("enterprise"):
            rows.append((self.app.t("notification_center"), self.app.t("notification_center"), "enterprise"))
        self._open_full_list_popup(self.app.t("home_overview"), rows, lambda value: self.focus_todo() if value == "tasks" else self.app.switch_page(value))

    def open_all_tasks(self):
        win = self._dialog_window(self.app.t("home_todos"), 840, 680)
        top = tk.Frame(win, bg=self.colors["card"])
        top.pack(fill=tk.X, padx=22, pady=(18, 10))
        tk.Label(top, text=self.app.t("home_todos"), font=("Microsoft YaHei UI", 18, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)
        toolbar = tk.Frame(win, bg=self.colors["card"])
        toolbar.pack(fill=tk.X, padx=22, pady=(0, 10))
        filter_var = tk.StringVar(value="all")
        selected_vars = {}
        list_frame = tk.Frame(win, bg=self.colors["card"])
        list_frame.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 12))
        canvas = tk.Canvas(list_frame, bg=self.colors["card"], highlightthickness=0)
        inner = tk.Frame(canvas, bg=self.colors["card"])
        bar = tk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=bar.set)
        canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        bar.pack(side=tk.RIGHT, fill=tk.Y)
        window_id = canvas.create_window((0, 0), window=inner, anchor="nw")
        inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.bind("<Configure>", lambda e: canvas.itemconfig(window_id, width=max(e.width - 8, 420)))

        def render():
            for child in inner.winfo_children():
                child.destroy()
            selected_vars.clear()
            items = [(i, item) for i, item in self._ordered_todo_items() if self._task_filter_match(item, filter_var.get())]
            if not items:
                show_empty(inner, self.app.empty_text("home_no_todos"), self.colors)
                return
            for idx, item in items:
                row = tk.Frame(inner, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"])
                row.pack(fill=tk.X, pady=5)
                var = tk.BooleanVar(value=False)
                selected_vars[idx] = var
                tk.Checkbutton(row, variable=var, bg=self.colors["card_soft"], activebackground=self.colors["card_soft"], selectcolor=self.colors["card"]).pack(side=tk.LEFT, padx=8)
                state = self.app.t("task_status_done") if item.get("done") else self.app.t("task_status_pending")
                self._render_task_text_block(row, item, wraplength=560)
                tk.Label(row, text=state, font=("Microsoft YaHei UI", 8, "bold"), fg=self.colors["success"] if item.get("done") else self.colors["warning"], bg=self.colors["card_soft"]).pack(side=tk.RIGHT, padx=10)

        def selected_indices():
            return [idx for idx, var in selected_vars.items() if var.get()]

        def mark_selected(done):
            for idx in selected_indices():
                if 0 <= idx < len(self.app.todos.get("items", [])):
                    self._set_todo_done(idx, done)
            save_todos(self.app.todos)
            self.refresh()
            render()

        def delete_selected():
            indices = sorted(selected_indices(), reverse=True)
            if not indices:
                return
            if not messagebox.askyesno(self.app.t("confirm_title"), self.app.t("task_batch_delete_confirm")):
                return
            for idx in indices:
                if 0 <= idx < len(self.app.todos.get("items", [])):
                    self.app.todos["items"].pop(idx)
            save_todos(self.app.todos)
            self.refresh()
            render()

        def edit_selected():
            indices = selected_indices()
            if len(indices) != 1:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("task_select_one_hint"))
                return
            idx = indices[0]
            current = self.app.todos["items"][idx].get("text", "")
            new_text = prompt_text_dialog(
                win,
                self.app.t("edit_task_title"),
                self.app.t("task_text"),
                self.colors,
                initialvalue=current,
                confirm_text=self.app.t("save"),
                cancel_text=self.app.t("cancel"),
                width=540,
                height=250,
            )
            if new_text and new_text.strip():
                self._update_todo_text(idx, new_text.strip())
                save_todos(self.app.todos)
                self.refresh()
                render()

        for key, label in (("all", "task_filter_all"), ("pending", "task_filter_pending"), ("today", "task_filter_today"), ("overdue", "task_filter_overdue"), ("priority", "task_filter_priority"), ("done", "task_filter_done")):
            tk.Radiobutton(toolbar, text=self.app.t(label), variable=filter_var, value=key, command=render, bg=self.colors["card"], fg=self.colors["text"], activebackground=self.colors["card"], selectcolor=self.colors["card"], font=("Microsoft YaHei UI", 9, "bold")).pack(side=tk.LEFT, padx=(0, 8))
        create_button(toolbar, self.app.t("task_mark_done"), lambda: mark_selected(True), self.colors).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(toolbar, self.app.t("task_mark_pending"), lambda: mark_selected(False), self.colors).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(toolbar, self.app.t("task_batch_edit"), edit_selected, self.colors).pack(side=tk.RIGHT, padx=(8, 0))
        create_button(toolbar, self.app.t("task_batch_delete"), delete_selected, self.colors).pack(side=tk.RIGHT)
        render()

    def _render_events(self):
        upcoming = self._collect_events(days=30)
        if not upcoming:
            show_empty(self.events_list, self.app.empty_text("home_no_events"), self.colors)
            return
        for ds, note in upcoming[:6]:
            d = datetime.strptime(ds, "%Y-%m-%d").date()
            item = self._list_item(self.events_list)
            tk.Label(item, text=self.app.format_date(d), font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["text"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(10, 0))
            tk.Label(item, text=f"◷ {note.get('time', '--:--')}   {note.get('text', '')[:60]}", font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=330, justify=tk.LEFT).pack(anchor="w", padx=12, pady=(4, 10))
            self._bind_card(item, lambda e, date_key=ds: self.app.open_calendar_date(date_key))

    def _render_todos(self):
        items = [(i, item) for i, item in self._ordered_todo_items() if self.app.matches_search(item.get("text"), "done" if item.get("done") else "", item.get("priority"), item.get("due_date"))]
        if not items:
            show_empty(self.todos_list, self.app.empty_text("home_no_todos"), self.colors)
            return
        for idx, item in items[:7]:
            row = self._list_item(self.todos_list)
            var = tk.BooleanVar(value=item.get("done", False))
            cb = tk.Checkbutton(row, variable=var, bg=self.colors["card_soft"], activebackground=self.colors["card_soft"], selectcolor=self.colors["card"], command=lambda real_idx=idx, v=var: self.toggle_todo(real_idx, v.get()))
            cb.pack(side=tk.LEFT, padx=10, pady=7)
            self._render_task_text_block(row, item, wraplength=255)
            tk.Button(row, text="✎", command=lambda real_idx=idx: self.edit_todo(real_idx), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["primary"], cursor="hand2", font=("Microsoft YaHei UI", 10, "bold")).pack(side=tk.RIGHT, padx=(0, 6))
            tk.Button(row, text="×", command=lambda real_idx=idx: self.delete_todo(real_idx), relief=tk.FLAT, bd=0, bg=self.colors["card_soft"], fg=self.colors["danger"], cursor="hand2", font=("Microsoft YaHei UI", 11, "bold")).pack(side=tk.RIGHT, padx=10)

    def _paged(self, items, page):
        total = max(1, (len(items) + self.page_size - 1) // self.page_size)
        page = max(0, min(page, total - 1))
        return items[page * self.page_size:(page + 1) * self.page_size], page, total

    def _render_notifications(self):
        items = [n for n in self.app.get_home_notifications() if self.app.matches_search(n.get("title"), n.get("content"), n.get("time"))]
        page_items, self.notice_page, total = self._paged(items, self.notice_page)
        self.notice_page_label.config(text=f"{self.notice_page + 1}/{total}")
        if not items:
            show_empty(self.notifications_list, self.app.t("notification_empty"), self.colors)
            return
        for n in page_items:
            row = self._list_item(self.notifications_list)
            fg = self.colors["warning"] if n.get("kind") == "holiday" else self.colors["primary"]
            tk.Label(row, text=n.get("title", "")[:48], font=("Microsoft YaHei UI", 9, "bold"), fg=fg, bg=self.colors["card_soft"], wraplength=310, justify=tk.LEFT).pack(anchor="w", padx=10, pady=(8, 0))
            tk.Label(row, text=f"{n.get('time', '')} · {n.get('content', '')[:120]}", font=("Microsoft YaHei UI", 8), fg=self.colors["text_secondary"], bg=self.colors["card_soft"], wraplength=310, justify=tk.LEFT).pack(anchor="w", padx=10, pady=(4, 8))
            if hasattr(self.app, "_open_notification_target"):
                self._bind_card(row, lambda e, item=n: self.app._open_notification_target(item))
            elif n.get("kind") == "holiday" and n.get("date"):
                self._bind_card(row, lambda e, ds=n.get("date"): self.app.open_calendar_date(ds))
            elif not hasattr(self.app, "is_page_available") or self.app.is_page_available("enterprise"):
                self._bind_card(row, lambda e: self.app.switch_page("enterprise"))

    def _render_docs(self):
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        docs = [(did, doc) for did, doc in sorted(docs_source.items(), key=lambda x: x[1].get("updated", ""), reverse=True) if self.app.matches_search(doc.get("title"), doc.get("content"), doc.get("updated"), doc.get("attachment", {}).get("original_name", ""))]
        page_items, self.docs_page, total = self._paged(docs, self.docs_page)
        self.docs_page_label.config(text=f"{self.docs_page + 1}/{total}")
        if not docs:
            show_empty(self.docs_list, self.app.empty_text("home_no_documents"), self.colors)
            return
        for did, doc in page_items:
            item = self._list_item(self.docs_list)
            tk.Label(item, text=self.app.display_doc_title(doc.get("title", ""))[:32], font=("Microsoft YaHei UI", 10, "bold"), fg=self.colors["text"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(10, 0))
            second = doc.get("updated", "")
            if doc.get("attachment"):
                second = f"📎 {doc['attachment'].get('original_name', '')}"
            tk.Label(item, text=second, font=("Microsoft YaHei UI", 8), fg=self.colors["text_secondary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=12, pady=(4, 10))
            self._bind_card(item, lambda e, doc_id=did: self.app.open_document(doc_id))

    def _turn_notice(self, step):
        self.notice_page = max(0, self.notice_page + step)
        self.refresh()

    def _turn_docs(self, step):
        self.docs_page = max(0, self.docs_page + step)
        self.refresh()

    def _overview_stat_card(self, parent, value, label, on_click, tooltip):
        card = tk.Frame(parent, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
        card.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10), pady=(2, 2))
        tk.Label(card, text=str(value), font=("Microsoft YaHei UI", 22, "bold"), fg=self.colors["primary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=16, pady=(12, 2))
        tk.Label(card, text=label, font=("Microsoft YaHei UI", 9), fg=self.colors["text_secondary"], bg=self.colors["card_soft"]).pack(anchor="w", padx=16, pady=(0, 12))
        self._bind_card(card, on_click)
        ToolTip(card, lambda: tooltip)
        return card

    def _render_overview(self):
        pending = sum(1 for item in self.app.todos.get("items", []) if not item.get("done"))
        stats_row = tk.Frame(self.overview_list, bg=self.colors["card"])
        stats_row.pack(fill=tk.X)
        self._overview_stat_card(stats_row, len(self.app.notes), self.app.t("overview_schedule"), lambda e: self.app.switch_page("calendar"), self.app.t("jump_to_calendar"))
        self._overview_stat_card(stats_row, pending, self.app.t("overview_pending_tasks"), lambda e: self.focus_todo(), self.app.t("jump_to_tasks"))
        docs_source = self.app.visible_documents() if hasattr(self.app, "visible_documents") else self.app.documents
        self._overview_stat_card(stats_row, len(docs_source), self.app.t("overview_documents"), lambda e: self.app.switch_page("files"), self.app.t("jump_to_documents"))
        if not hasattr(self.app, "is_page_available") or self.app.is_page_available("enterprise"):
            self._overview_stat_card(stats_row, len(self.app.enterprise.get("notifications", [])), self.app.t("enterprise_stats_notices"), lambda e: self.app.switch_page("enterprise"), self.app.t("notification_center"))

    def _list_item(self, parent):
        item = tk.Frame(parent, bg=self.colors["card_soft"], highlightthickness=1, highlightbackground=self.colors["border"], cursor="hand2")
        item.pack(fill=tk.X, pady=5)
        return item

    def _bind_card(self, widget, command):
        widget.bind("<Button-1>", command)
        for child in widget.winfo_children():
            self._bind_card(child, command)

    def open_batch_todo_dialog(self):
        dlg = self._dialog_window(self.app.t("home_batch_add_todo"), 680, 500)
        dlg.minsize(560, 420)

        top = tk.Frame(dlg, bg=self.colors["card"])
        top.pack(fill=tk.X, padx=22, pady=(18, 8))
        tk.Label(top, text=self.app.t("home_batch_add_todo"), font=("Microsoft YaHei UI", 16, "bold"), fg=self.colors["text"], bg=self.colors["card"]).pack(side=tk.LEFT)

        footer = tk.Frame(dlg, bg=self.colors["card"])
        footer.pack(fill=tk.X, side=tk.BOTTOM, padx=22, pady=(8, 18))

        body = tk.Frame(dlg, bg=self.colors["card"])
        body.pack(fill=tk.BOTH, expand=True, padx=22, pady=(0, 0))
        tk.Label(body, text=self.app.t("home_batch_add_todo_hint"), font=("Microsoft YaHei UI", 10), fg=self.colors["text_secondary"], bg=self.colors["card"], wraplength=480, justify=tk.LEFT).pack(anchor="w")
        editor = scrolledtext.ScrolledText(body, wrap=tk.WORD, font=("Microsoft YaHei UI", 11), relief=tk.FLAT, bd=0, bg=self.colors["bg"], fg=self.colors["text"], height=12)
        editor.pack(fill=tk.BOTH, expand=True, pady=(12, 0))

        def confirm(event=None):
            lines = [line.strip() for line in editor.get("1.0", tk.END).splitlines() if line.strip()]
            if not lines:
                messagebox.showwarning(self.app.t("warning_title"), self.app.t("home_batch_add_todo_hint"))
                return "break"
            self.app.todos.setdefault("items", [])
            for line in lines:
                self.app.todos["items"].append(self._make_todo_item(line))
            save_todos(self.app.todos)
            dlg.destroy()
            self.refresh()
            self.app.set_status("status_saved")
            return "break"

        cancel_btn = create_button(footer, self.app.t("schedule_cancel"), dlg.destroy, self.colors)
        cancel_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10), ipady=2)
        confirm_btn = create_button(footer, self.app.t("home_batch_confirm_visible"), confirm, self.colors, filled=True)
        confirm_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=2)
        # Extra shortcut so batch adding still works even if the user resizes the dialog.
        dlg.bind("<Control-Return>", confirm)
        editor.focus_set()

    def add_todo(self):
        text = self.todo_input.get().strip()
        if not text or text == self.app.t("new_task_placeholder") or text in self.app.all_values("new_task_placeholder"):
            return
        self.app.todos.setdefault("items", []).append(self._make_todo_item(text))
        save_todos(self.app.todos)
        self.todo_input.delete(0, tk.END)
        self.todo_input.insert(0, self.app.t("new_task_placeholder"))
        self.todo_input.config(fg=self.colors["text_secondary"])
        self.refresh()
        self.app.set_status("status_saved")

    def edit_todo(self, idx):
        if not (0 <= idx < len(self.app.todos.get("items", []))):
            return
        current = self.app.todos["items"][idx].get("text", "")
        new_text = prompt_text_dialog(
            self.app.root,
            self.app.t("edit_task_title"),
            self.app.t("task_text"),
            self.colors,
            initialvalue=current,
            confirm_text=self.app.t("save"),
            cancel_text=self.app.t("cancel"),
            width=540,
            height=250,
        )
        if new_text is None:
            return
        new_text = new_text.strip()
        if not new_text:
            return
        self._update_todo_text(idx, new_text)
        save_todos(self.app.todos)
        self.refresh()
        self.app.set_status("status_saved")

    def toggle_todo(self, idx, done):
        if 0 <= idx < len(self.app.todos.get("items", [])):
            self._set_todo_done(idx, done)
            save_todos(self.app.todos)
            self.refresh()
            self.app.set_status("status_saved")

    def delete_todo(self, idx):
        if 0 <= idx < len(self.app.todos.get("items", [])):
            self.app.todos["items"].pop(idx)
            save_todos(self.app.todos)
            self.refresh()
            self.app.set_status("status_saved")
