from __future__ import annotations

import calendar
from datetime import datetime
import json
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import traceback
import uuid

from openpyxl import Workbook


HR_DEBUG_LOG_PATH = Path(r"D:\Hermes\local\debug\hr_debug.log")


def append_hr_debug_log(message: str, payload: object | None = None) -> None:
    try:
        HR_DEBUG_LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
        line = f"[{datetime.now().isoformat(timespec='seconds')}] {message}"
        if payload is not None:
            line += " " + json.dumps(payload, ensure_ascii=False, default=str)
        with HR_DEBUG_LOG_PATH.open("a", encoding="utf-8") as handle:
            handle.write(line + "\n")
    except Exception:
        pass


HR_EMPLOYEE_COLUMNS = (
    "employee_id",
    "full_name",
    "display_name",
    "user_email",
    "department",
    "position",
    "phone",
    "email",
    "join_date",
    "shift_name",
    "shift_start",
    "shift_end",
    "break_time_out",
    "break_time_in",
    "is_active",
    "updated_at",
)

HR_ATTENDANCE_COLUMNS = (
    "work_date",
    "employee_id",
    "employee_name",
    "department",
    "check_in_at",
    "check_out_at",
    "break_out_at",
    "break_in_at",
    "worked_minutes",
    "working_day",
    "late_minutes",
    "early_leave_minutes",
    "status",
    "updated_at",
)


def _build_hr_module(self, parent: ttk.Frame) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(0, weight=1)

    notebook = ttk.Notebook(parent)
    notebook.grid(row=0, column=0, sticky="nsew")
    self.hr_notebook = notebook

    employees_tab = ttk.Frame(notebook, padding=14)
    attendance_tab = ttk.Frame(notebook, padding=14)
    notebook.add(employees_tab, text=self.t("hr_employees_tab"))
    notebook.add(attendance_tab, text=self.t("hr_attendance_tab"))

    _build_hr_employees_tab(self, employees_tab)
    _build_hr_attendance_tab(self, attendance_tab)


def _build_hr_employees_tab(self, parent: ttk.Frame) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(2, weight=1)

    toolbar = ttk.Frame(parent)
    toolbar.grid(row=0, column=0, sticky="ew")
    self.widget_text["hr_employee_new_button"] = ttk.Button(toolbar, command=self.open_hr_employee_dialog)
    self.widget_text["hr_employee_new_button"].pack(side="left")
    self.register_permission_widget("action.hr.employee.new", self.widget_text["hr_employee_new_button"])
    self.widget_text["hr_employee_edit_button"] = ttk.Button(toolbar, command=self.edit_selected_hr_employee)
    self.widget_text["hr_employee_edit_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.employee.edit", self.widget_text["hr_employee_edit_button"])
    self.widget_text["hr_employee_delete_button"] = ttk.Button(toolbar, command=self.delete_selected_hr_employee)
    self.widget_text["hr_employee_delete_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.employee.delete", self.widget_text["hr_employee_delete_button"])
    self.widget_text["hr_employee_toggle_button"] = ttk.Button(toolbar, command=self.toggle_selected_hr_employee_active)
    self.widget_text["hr_employee_toggle_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.employee.toggle_active", self.widget_text["hr_employee_toggle_button"])
    self.widget_text["hr_shared_lists_open_button"] = ttk.Button(toolbar, command=self.open_shared_lists_dialog)
    self.widget_text["hr_shared_lists_open_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.manage_shared_lists", self.widget_text["hr_shared_lists_open_button"])

    search_row = ttk.Frame(parent)
    search_row.grid(row=1, column=0, sticky="ew", pady=(12, 12))
    search_row.columnconfigure(1, weight=1)
    self.widget_text["hr_employee_search_label"] = ttk.Label(search_row)
    self.widget_text["hr_employee_search_label"].grid(row=0, column=0, sticky="w", padx=(0, 6))
    self.hr_employee_search_entry = ttk.Entry(search_row, textvariable=self.hr_employee_search_var)
    self.hr_employee_search_entry.grid(row=0, column=1, sticky="ew")
    self.hr_employee_search_entry.bind("<Return>", self.on_hr_employee_search_submit)
    self.hr_employee_search_entry.bind("<KP_Enter>", self.on_hr_employee_search_submit)

    table_frame = ttk.Frame(parent)
    table_frame.grid(row=2, column=0, sticky="nsew")
    table_frame.columnconfigure(0, weight=1)
    table_frame.rowconfigure(0, weight=1)

    self.hr_employee_tree = ttk.Treeview(table_frame, columns=HR_EMPLOYEE_COLUMNS, show="headings", selectmode="browse", style="Costing.Treeview")
    widths = {
        "employee_id": 110,
        "full_name": 170,
        "display_name": 140,
        "user_email": 180,
        "department": 130,
        "position": 140,
        "phone": 110,
        "email": 170,
        "join_date": 95,
        "shift_name": 100,
        "shift_start": 75,
        "shift_end": 75,
        "break_time_out": 90,
        "break_time_in": 90,
        "is_active": 80,
        "updated_at": 125,
    }
    for key in HR_EMPLOYEE_COLUMNS:
        anchor = "center"
        if key in {"employee_id", "full_name", "display_name", "user_email", "department", "position", "phone", "email", "shift_name"}:
            anchor = "w"
        self.hr_employee_tree.column(key, width=widths[key], anchor=anchor, stretch=False)
        self.hr_employee_tree.heading(key, text=key)
    self.hr_employee_tree.grid(row=0, column=0, sticky="nsew")
    self.hr_employee_tree.bind("<Double-1>", self.on_hr_employee_double_click)
    scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=self.hr_employee_tree.yview)
    scroll_y.grid(row=0, column=1, sticky="ns")
    scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=self.hr_employee_tree.xview)
    scroll_x.grid(row=1, column=0, sticky="ew")
    self.hr_employee_tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)

    ttk.Label(parent, textvariable=self.status_var, relief="sunken", anchor="w", padding=8).grid(
        row=3,
        column=0,
        sticky="ew",
        pady=(12, 0),
    )


def _build_hr_attendance_tab(self, parent: ttk.Frame) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(3, weight=1)

    toolbar = ttk.Frame(parent)
    toolbar.grid(row=0, column=0, sticky="ew")
    self.widget_text["hr_check_in_button"] = ttk.Button(toolbar, command=self.hr_check_in_selected_employee)
    self.widget_text["hr_check_in_button"].pack(side="left")
    self.register_permission_widget("action.hr.attendance.check_in", self.widget_text["hr_check_in_button"])
    self.widget_text["hr_check_out_button"] = ttk.Button(toolbar, command=self.hr_check_out_selected_employee)
    self.widget_text["hr_check_out_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.attendance.check_out", self.widget_text["hr_check_out_button"])
    self.widget_text["hr_attendance_new_button"] = ttk.Button(toolbar, command=self.open_new_hr_attendance_dialog)
    self.widget_text["hr_attendance_new_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.attendance.new", self.widget_text["hr_attendance_new_button"])
    self.widget_text["hr_attendance_delete_button"] = ttk.Button(toolbar, command=self.delete_selected_hr_attendance)
    self.widget_text["hr_attendance_delete_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.attendance.delete", self.widget_text["hr_attendance_delete_button"])
    self.widget_text["hr_attendance_quick_button"] = ttk.Button(toolbar, command=self.open_hr_quick_attendance_dialog)
    self.widget_text["hr_attendance_quick_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.attendance.quick", self.widget_text["hr_attendance_quick_button"])
    self.widget_text["hr_attendance_overview_button"] = ttk.Button(toolbar, command=self.open_hr_attendance_overview_dialog)
    self.widget_text["hr_attendance_overview_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.attendance.overview", self.widget_text["hr_attendance_overview_button"])
    self.widget_text["hr_attendance_edit_button"] = ttk.Button(toolbar, command=self.edit_selected_hr_attendance)
    self.widget_text["hr_attendance_edit_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.attendance.edit", self.widget_text["hr_attendance_edit_button"])
    self.widget_text["hr_attendance_export_button"] = ttk.Button(toolbar, command=self.export_hr_attendance_report)
    self.widget_text["hr_attendance_export_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.hr.export_report", self.widget_text["hr_attendance_export_button"])

    selector_row = ttk.Frame(parent)
    selector_row.grid(row=1, column=0, sticky="ew", pady=(12, 8))
    selector_row.columnconfigure(1, weight=1)
    self.widget_text["hr_attendance_employee_label"] = ttk.Label(selector_row)
    self.widget_text["hr_attendance_employee_label"].grid(row=0, column=0, sticky="w", padx=(0, 6))
    self.hr_attendance_employee_var = tk.StringVar(value="")
    self.hr_attendance_employee_combo = ttk.Combobox(selector_row, textvariable=self.hr_attendance_employee_var, state="readonly")
    self.hr_attendance_employee_combo.grid(row=0, column=1, sticky="ew")

    search_row = ttk.Frame(parent)
    search_row.grid(row=2, column=0, sticky="ew", pady=(0, 12))
    search_row.columnconfigure(1, weight=1)
    self.widget_text["hr_attendance_search_label"] = ttk.Label(search_row)
    self.widget_text["hr_attendance_search_label"].grid(row=0, column=0, sticky="w", padx=(0, 6))
    self.hr_attendance_search_entry = ttk.Entry(search_row, textvariable=self.hr_attendance_search_var)
    self.hr_attendance_search_entry.grid(row=0, column=1, sticky="ew")
    self.hr_attendance_search_entry.bind("<Return>", self.on_hr_attendance_search_submit)
    self.hr_attendance_search_entry.bind("<KP_Enter>", self.on_hr_attendance_search_submit)

    table_frame = ttk.Frame(parent)
    table_frame.grid(row=3, column=0, sticky="nsew")
    table_frame.columnconfigure(0, weight=1)
    table_frame.rowconfigure(0, weight=1)

    self.hr_attendance_tree = ttk.Treeview(table_frame, columns=HR_ATTENDANCE_COLUMNS, show="headings", selectmode="browse", style="Costing.Treeview")
    widths = {
        "work_date": 95,
        "employee_id": 105,
        "employee_name": 170,
        "department": 130,
        "check_in_at": 135,
        "check_out_at": 135,
        "break_out_at": 135,
        "break_in_at": 135,
        "worked_minutes": 95,
        "working_day": 80,
        "late_minutes": 85,
        "early_leave_minutes": 105,
        "status": 95,
        "updated_at": 125,
    }
    for key in HR_ATTENDANCE_COLUMNS:
        anchor = "center"
        if key in {"employee_id", "employee_name", "department", "status"}:
            anchor = "w"
        self.hr_attendance_tree.column(key, width=widths[key], anchor=anchor, stretch=False)
        self.hr_attendance_tree.heading(key, text=key)
    self.hr_attendance_tree.grid(row=0, column=0, sticky="nsew")
    scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=self.hr_attendance_tree.yview)
    scroll_y.grid(row=0, column=1, sticky="ns")
    scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=self.hr_attendance_tree.xview)
    scroll_x.grid(row=1, column=0, sticky="ew")
    self.hr_attendance_tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)

    ttk.Label(parent, textvariable=self.status_var, relief="sunken", anchor="w", padding=8).grid(
        row=4,
        column=0,
        sticky="ew",
        pady=(12, 0),
    )


def refresh_hr_module(self) -> None:
    self.hr_employee_rows = self.load_hr_employees_from_source()
    self.hr_attendance_rows = self.load_hr_attendance_records_from_source()
    self.refresh_hr_attendance_employee_selector()
    self.apply_hr_employee_filter()
    self.apply_hr_attendance_filter()
    self.update_hr_module_status()


def apply_hr_employee_filter(self) -> None:
    query = str(getattr(self, "hr_employee_search_committed_value", self.hr_employee_search_var.get()) or "").strip().lower()
    self.filtered_hr_employee_rows = []
    for row in self.hr_employee_rows:
        haystack = " ".join(
            [
                str(row.get("employee_id", "")),
                str(row.get("full_name", "")),
                str(row.get("display_name", "")),
                str(row.get("user_email", "")),
                str(row.get("department", "")),
                str(row.get("position", "")),
            ]
        ).lower()
        if not query or query in haystack:
            self.filtered_hr_employee_rows.append(row)
    self.refresh_hr_employee_tree()
    self.update_hr_module_status()


def apply_hr_attendance_filter(self) -> None:
    query = str(getattr(self, "hr_attendance_search_committed_value", self.hr_attendance_search_var.get()) or "").strip().lower()
    self.filtered_hr_attendance_rows = []
    for row in self.hr_attendance_rows:
        haystack = " ".join(
            [
                str(row.get("work_date", "")),
                str(row.get("employee_id", "")),
                str(row.get("employee_name", "")),
                str(row.get("department", "")),
                str(row.get("status", "")),
            ]
        ).lower()
        if not query or query in haystack:
            self.filtered_hr_attendance_rows.append(row)
    self.refresh_hr_attendance_tree()
    self.update_hr_module_status()


def commit_hr_employee_search_query(self, value: object | None = None, *, apply_filter: bool = True) -> None:
    committed_value = self.hr_employee_search_var.get() if value is None else value
    self.hr_employee_search_committed_value = str(committed_value or "").strip()
    if apply_filter:
        self.apply_hr_employee_filter()


def on_hr_employee_search_submit(self, _event: object | None = None) -> str:
    self.commit_hr_employee_search_query()
    return "break"


def commit_hr_attendance_search_query(self, value: object | None = None, *, apply_filter: bool = True) -> None:
    committed_value = self.hr_attendance_search_var.get() if value is None else value
    self.hr_attendance_search_committed_value = str(committed_value or "").strip()
    if apply_filter:
        self.apply_hr_attendance_filter()


def on_hr_attendance_search_submit(self, _event: object | None = None) -> str:
    self.commit_hr_attendance_search_query()
    return "break"


def refresh_hr_employee_tree(self) -> None:
    if not hasattr(self, "hr_employee_tree"):
        return
    for item_id in self.hr_employee_tree.get_children():
        self.hr_employee_tree.delete(item_id)
    for index, row in enumerate(self.filtered_hr_employee_rows, start=1):
        values = tuple(
            self.display_value("Yes" if row.get("is_active") else "No") if key == "is_active" else self.display_value(row.get(key))
            for key in HR_EMPLOYEE_COLUMNS
        )
        self.hr_employee_tree.insert("", "end", str(index), values=values, tags=("odd_row" if index % 2 else "even_row",))


def refresh_hr_attendance_tree(self) -> None:
    if not hasattr(self, "hr_attendance_tree"):
        return
    for item_id in self.hr_attendance_tree.get_children():
        self.hr_attendance_tree.delete(item_id)
    for index, row in enumerate(self.filtered_hr_attendance_rows, start=1):
        values = tuple(self.display_value(row.get(key)) for key in HR_ATTENDANCE_COLUMNS)
        self.hr_attendance_tree.insert("", "end", str(index), values=values, tags=("odd_row" if index % 2 else "even_row",))


def get_selected_hr_employee(self) -> dict | None:
    selection = self.hr_employee_tree.selection() if hasattr(self, "hr_employee_tree") else ()
    if not selection:
        return None
    index = int(selection[0]) - 1
    if 0 <= index < len(self.filtered_hr_employee_rows):
        return self.filtered_hr_employee_rows[index]
    return None


def get_selected_hr_attendance(self) -> dict | None:
    selection = self.hr_attendance_tree.selection() if hasattr(self, "hr_attendance_tree") else ()
    if not selection:
        return None
    index = int(selection[0]) - 1
    if 0 <= index < len(self.filtered_hr_attendance_rows):
        return self.filtered_hr_attendance_rows[index]
    return None


def build_hr_user_account_options(self) -> list[dict]:
    options: list[dict] = []
    for user in self.fetch_users_from_source():
        email = str(user.get("email", "")).strip()
        if not email:
            continue
        display_name = str(user.get("display_name", "")).strip()
        role = str(user.get("role", "")).strip()
        label = f"{email} - {display_name}" if display_name else email
        options.append({"email": email, "display_name": display_name, "role": role, "label": label})
    options.sort(key=lambda item: item["label"].lower())
    return options


def refresh_hr_attendance_employee_selector(self) -> None:
    if not hasattr(self, "hr_attendance_employee_combo"):
        return
    options: list[str] = []
    current_user_email = str(getattr(self, "current_user", {}).get("email", "")).strip().lower()
    current_value = str(self.hr_attendance_employee_var.get()).strip()
    selected_value = ""
    for row in self.hr_employee_rows:
        employee_id = str(row.get("employee_id", "")).strip()
        full_name = str(row.get("full_name", "")).strip()
        label = employee_id
        if full_name:
            label = f"{employee_id} - {full_name}"
        if label:
            options.append(label)
        if current_value and label == current_value:
            selected_value = label
        if not selected_value and current_user_email and str(row.get("user_email", "")).strip().lower() == current_user_email:
            selected_value = label
    self.hr_attendance_employee_combo.configure(values=options)
    self.hr_attendance_employee_var.set(selected_value if selected_value in options else "")


def open_hr_employee_dialog(self, source_row: dict | None = None) -> None:
    if source_row is None and not self.is_action_allowed("action.hr.employee.new"):
        return
    if source_row is not None and not self.is_action_allowed("action.hr.employee.edit"):
        return
    initial = dict(source_row) if isinstance(source_row, dict) else self.default_hr_employee_record()
    is_edit = bool(source_row)
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("hr_employee_edit_title") if is_edit else self.t("hr_employee_new_title"))
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.minsize(620, 640)

    frame = ttk.Frame(dialog, padding=18)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(1, weight=1)

    account_options = self.build_hr_user_account_options()
    account_by_label = {item["label"]: item for item in account_options}
    linked_user = next((item for item in account_options if item["email"] == str(initial.get("user_email", "")).strip()), None)

    fields = {
        "employee_id": tk.StringVar(value=str(initial.get("employee_id", "")).strip()),
        "full_name": tk.StringVar(value=str(initial.get("full_name", "")).strip()),
        "display_name": tk.StringVar(value=str(initial.get("display_name", initial.get("nick_name", ""))).strip()),
        "department": tk.StringVar(value=str(initial.get("department", "")).strip()),
        "position": tk.StringVar(value=str(initial.get("position", "")).strip()),
        "phone": tk.StringVar(value=str(initial.get("phone", "")).strip()),
        "email": tk.StringVar(value=str(initial.get("email", "")).strip()),
        "join_date": tk.StringVar(value=str(initial.get("join_date", "")).strip()),
        "shift_name": tk.StringVar(value=str(initial.get("shift_name", "Office")).strip()),
        "shift_start": tk.StringVar(value=str(initial.get("shift_start", "08:00")).strip()),
        "shift_end": tk.StringVar(value=str(initial.get("shift_end", "17:00")).strip()),
        "break_time_out": tk.StringVar(value=str(initial.get("break_time_out", "12:00")).strip()),
        "break_time_in": tk.StringVar(value=str(initial.get("break_time_in", "13:00")).strip()),
        "grace_minutes": tk.StringVar(value=str(initial.get("grace_minutes", 5)).strip()),
        "user_account": tk.StringVar(value=linked_user["label"] if linked_user else ""),
        "is_active": tk.BooleanVar(value=bool(initial.get("is_active", True))),
    }

    row_index = 0
    label_map = [
        ("employee_id", "hr_employee_id_label"),
        ("full_name", "hr_full_name_label"),
        ("display_name", "hr_display_name_label"),
        ("user_account", "hr_linked_account_label"),
        ("department", "hr_department_label"),
        ("position", "hr_position_label"),
        ("phone", "hr_phone_label"),
        ("email", "hr_email_label"),
        ("join_date", "hr_join_date_label"),
        ("shift_name", "hr_shift_name_label"),
        ("shift_start", "hr_shift_start_label"),
        ("shift_end", "hr_shift_end_label"),
        ("break_time_out", "hr_break_time_out_label"),
        ("break_time_in", "hr_break_time_in_label"),
        ("grace_minutes", "hr_grace_minutes_label"),
    ]
    for key, text_key in label_map:
        ttk.Label(frame, text=self.t(text_key)).grid(row=row_index, column=0, sticky="w", padx=(0, 12), pady=5)
        if key == "user_account":
            combo = ttk.Combobox(frame, textvariable=fields[key], state="readonly", values=[item["label"] for item in account_options])
            combo.grid(row=row_index, column=1, sticky="ew", pady=5)

            def on_account_change(_event: object | None = None) -> None:
                option = account_by_label.get(fields["user_account"].get())
                if option is None:
                    return
                if not fields["full_name"].get().strip():
                    fields["full_name"].set(option.get("display_name", ""))
                if not fields["email"].get().strip():
                    fields["email"].set(option.get("email", ""))

            combo.bind("<<ComboboxSelected>>", on_account_change)
        else:
            ttk.Entry(frame, textvariable=fields[key]).grid(row=row_index, column=1, sticky="ew", pady=5)
        row_index += 1

    ttk.Checkbutton(frame, text=self.t("hr_employee_active_label"), variable=fields["is_active"]).grid(row=row_index, column=1, sticky="w", pady=(4, 8))
    row_index += 1

    ttk.Label(frame, text=self.t("hr_note_label")).grid(row=row_index, column=0, sticky="nw", padx=(0, 12), pady=(8, 5))
    note_widget = tk.Text(frame, height=5, wrap="word")
    note_widget.grid(row=row_index, column=1, sticky="ew", pady=(8, 5))
    note_widget.insert("1.0", str(initial.get("note", "")).strip())
    row_index += 1

    status_var = tk.StringVar(value="")
    ttk.Label(frame, textvariable=status_var, foreground="#8a4b1f").grid(row=row_index, column=0, columnspan=2, sticky="w", pady=(10, 0))
    row_index += 1

    def save_employee() -> None:
        employee_id = fields["employee_id"].get().strip()
        append_hr_debug_log(
            "employee-dialog-save-click",
            {
                "employee_id": employee_id,
                "is_edit": is_edit,
                "display_name": fields["display_name"].get().strip(),
                "full_name": fields["full_name"].get().strip(),
                "current_rows": len(getattr(self, "hr_employee_rows", []) or []),
            },
        )
        if not employee_id:
            status_var.set(self.t("hr_employee_id_required"))
            append_hr_debug_log("employee-dialog-save-blocked", {"reason": "missing_employee_id"})
            return
        duplicate = next((row for row in self.hr_employee_rows if str(row.get("employee_id", "")).strip() == employee_id), None)
        if duplicate is not None and not is_edit:
            status_var.set(self.t("hr_employee_exists"))
            append_hr_debug_log("employee-dialog-save-blocked", {"reason": "duplicate_employee_id", "employee_id": employee_id})
            return
        try:
            grace_minutes = int(fields["grace_minutes"].get().strip() or "0")
        except ValueError:
            status_var.set(self.t("hr_grace_minutes_invalid"))
            append_hr_debug_log(
                "employee-dialog-save-blocked",
                {"reason": "invalid_grace_minutes", "value": fields["grace_minutes"].get().strip()},
            )
            return
        linked = account_by_label.get(fields["user_account"].get())
        payload = self.default_hr_employee_record()
        payload.update(
            {
                "employee_id": employee_id,
                "user_email": linked.get("email", "") if linked else "",
                "full_name": fields["full_name"].get().strip(),
                "display_name": fields["display_name"].get().strip(),
                "department": fields["department"].get().strip(),
                "position": fields["position"].get().strip(),
                "phone": fields["phone"].get().strip(),
                "email": fields["email"].get().strip(),
                "join_date": fields["join_date"].get().strip(),
                "shift_name": fields["shift_name"].get().strip() or "Office",
                "shift_start": fields["shift_start"].get().strip() or "08:00",
                "shift_end": fields["shift_end"].get().strip() or "17:00",
                "break_time_out": fields["break_time_out"].get().strip() or "12:00",
                "break_time_in": fields["break_time_in"].get().strip() or "13:00",
                "grace_minutes": grace_minutes,
                "is_active": bool(fields["is_active"].get()),
                "note": note_widget.get("1.0", "end").strip(),
                "created_at": str(initial.get("created_at", "")).strip(),
                "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            }
        )
        append_hr_debug_log(
            "employee-dialog-save-payload",
            {
                "employee_id": payload.get("employee_id"),
                "full_name": payload.get("full_name"),
                "display_name": payload.get("display_name"),
                "user_email": payload.get("user_email"),
                "is_active": payload.get("is_active"),
            },
        )
        try:
            saved = self.save_hr_employee_to_source(payload)
        except Exception as exc:
            append_hr_debug_log(
                "employee-dialog-save-exception",
                {"message": str(exc), "traceback": traceback.format_exc()},
            )
            status_var.set(f"{self.t('hr_save_failed')} {exc}")
            return
        if not isinstance(saved, dict):
            append_hr_debug_log("employee-dialog-save-failed", {"saved_type": type(saved).__name__, "saved": saved})
            status_var.set(self.t("hr_save_failed"))
            return
        append_hr_debug_log(
            "employee-dialog-save-success",
            {
                "employee_id": saved.get("employee_id"),
                "display_name": saved.get("display_name"),
                "rows_before_refresh": len(getattr(self, "hr_employee_rows", []) or []),
            },
        )
        self.refresh_hr_module()
        self.set_status(self.t("hr_employee_saved_status", employee_id=employee_id))
        dialog.destroy()

    actions = ttk.Frame(frame)
    actions.grid(row=row_index, column=0, columnspan=2, sticky="e", pady=(14, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(0, 8))
    save_button = ttk.Button(actions, text=self.t("collection_list_save"), command=save_employee)
    save_button.pack(side="left")
    self.register_permission_widget("action.hr.employee.edit" if is_edit else "action.hr.employee.new", save_button)

    self.center_dialog(dialog, min_width=620, min_height=640)


def edit_selected_hr_employee(self) -> None:
    row = self.get_selected_hr_employee()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_employee_warning"))
        return
    self.open_hr_employee_dialog(row)


def on_hr_employee_double_click(self, _event: object | None = None) -> str:
    self.edit_selected_hr_employee()
    return "break"


def delete_selected_hr_employee(self) -> None:
    if not self.is_action_allowed("action.hr.employee.delete"):
        return
    row = self.get_selected_hr_employee()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_employee_warning"))
        return
    employee_id = str(row.get("employee_id", "")).strip()
    full_name = str(row.get("full_name", "")).strip()
    confirm_text = self.t("hr_employee_delete_confirm", employee_id=employee_id, full_name=full_name)
    if not messagebox.askyesno(self.t("warning_title"), confirm_text):
        append_hr_debug_log("employee-delete-cancelled", {"employee_id": employee_id})
        return
    remaining_rows = [
        item for item in self.hr_employee_rows
        if str(item.get("employee_id", "")).strip() != employee_id
    ]
    append_hr_debug_log(
        "employee-delete-start",
        {"employee_id": employee_id, "before_count": len(self.hr_employee_rows), "after_count": len(remaining_rows)},
    )
    try:
        saved_rows = self.save_hr_employees_to_source(remaining_rows)
    except Exception as exc:
        append_hr_debug_log(
            "employee-delete-exception",
            {"employee_id": employee_id, "message": str(exc), "traceback": traceback.format_exc()},
        )
        messagebox.showwarning(self.t("warning_title"), f"{self.t('hr_employee_delete_failed')} {exc}")
        return
    self.hr_employee_rows = list(saved_rows)
    self.refresh_hr_attendance_employee_selector()
    self.apply_hr_employee_filter()
    self.set_status(self.t("hr_employee_deleted", employee_id=employee_id))
    append_hr_debug_log("employee-delete-success", {"employee_id": employee_id, "saved_count": len(saved_rows)})


def toggle_selected_hr_employee_active(self) -> None:
    if not self.is_action_allowed("action.hr.employee.toggle_active"):
        return
    row = self.get_selected_hr_employee()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_employee_warning"))
        return
    payload = dict(row)
    payload["is_active"] = not bool(row.get("is_active", True))
    payload["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    saved = self.save_hr_employee_to_source(payload)
    if isinstance(saved, dict):
        self.refresh_hr_module()
        self.set_status(self.t("hr_employee_saved_status", employee_id=str(saved.get("employee_id", "")).strip()))


def get_target_hr_employee_for_attendance(self) -> dict | None:
    selected_label = str(getattr(self, "hr_attendance_employee_var", tk.StringVar(value="")).get()).strip()
    if selected_label:
        employee_id = selected_label.split(" - ", 1)[0].strip()
        matched = next(
            (row for row in self.hr_employee_rows if str(row.get("employee_id", "")).strip() == employee_id),
            None,
        )
        if matched is not None:
            return matched
    selected = self.get_selected_hr_employee()
    if selected is not None:
        return selected
    current_user_email = str(getattr(self, "current_user", {}).get("email", "")).strip().lower()
    if current_user_email:
        return next(
            (row for row in self.hr_employee_rows if str(row.get("user_email", "")).strip().lower() == current_user_email),
            None,
        )
    return None


def build_manual_hr_attendance_payload(self, employee_row: dict, work_date: str, *, full_shift: bool = False) -> dict:
    payload = self.default_hr_attendance_record()
    employee_id = str(employee_row.get("employee_id", "")).strip()
    normalized_work_date = str(work_date or "").strip()
    actor_email = str(getattr(self, "current_user", {}).get("email", "")).strip()
    now_text = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    payload.update(
        {
            "attendance_id": uuid.uuid4().hex,
            "employee_id": employee_id,
            "user_email": str(employee_row.get("user_email", "")).strip(),
            "employee_name": str(employee_row.get("full_name", "")).strip(),
            "department": str(employee_row.get("department", "")).strip(),
            "position": str(employee_row.get("position", "")).strip(),
            "work_date": normalized_work_date,
            "shift_name": str(employee_row.get("shift_name", "Office") or "Office").strip(),
            "shift_start": str(employee_row.get("shift_start", "08:00") or "08:00").strip(),
            "shift_end": str(employee_row.get("shift_end", "17:00") or "17:00").strip(),
            "break_time_out": str(employee_row.get("break_time_out", "12:00") or "12:00").strip(),
            "break_time_in": str(employee_row.get("break_time_in", "13:00") or "13:00").strip(),
            "grace_minutes": int(employee_row.get("grace_minutes", 5) or 0),
            "status": "Absent",
            "manual_adjusted": True,
            "created_by": actor_email,
            "updated_by": actor_email,
            "created_at": now_text,
            "updated_at": now_text,
        }
    )
    if full_shift and normalized_work_date:
        shift_start = str(payload.get("shift_start", "08:00")).strip() or "08:00"
        shift_end = str(payload.get("shift_end", "17:00")).strip() or "17:00"
        payload["check_in_at"] = f"{normalized_work_date} {shift_start}:00"
        payload["check_out_at"] = f"{normalized_work_date} {shift_end}:00"
        payload["break_out_at"] = f"{normalized_work_date} {str(payload.get('break_time_out', '12:00')).strip() or '12:00'}:00"
        payload["break_in_at"] = f"{normalized_work_date} {str(payload.get('break_time_in', '13:00')).strip() or '13:00'}:00"
        payload["status"] = "Present"
        payload["note"] = self.t("hr_quick_attendance_note")
    return payload


def hr_check_in_selected_employee(self) -> None:
    if not self.is_action_allowed("action.hr.attendance.check_in"):
        return
    employee = self.get_target_hr_employee_for_attendance()
    if employee is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_employee_warning"))
        return
    try:
        self.hr_attendance_check_in_to_source(employee)
    except Exception as exc:
        messagebox.showwarning(self.t("warning_title"), str(exc))
        return
    self.refresh_hr_module()
    self.set_status(self.t("hr_check_in_status", employee_id=str(employee.get("employee_id", "")).strip()))


def hr_check_out_selected_employee(self) -> None:
    if not self.is_action_allowed("action.hr.attendance.check_out"):
        return
    employee = self.get_target_hr_employee_for_attendance()
    if employee is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_employee_warning"))
        return
    try:
        self.hr_attendance_check_out_to_source(employee)
    except Exception as exc:
        messagebox.showwarning(self.t("warning_title"), str(exc))
        return
    self.refresh_hr_module()
    self.set_status(self.t("hr_check_out_status", employee_id=str(employee.get("employee_id", "")).strip()))


def open_new_hr_attendance_dialog(self) -> None:
    if not self.is_action_allowed("action.hr.attendance.new"):
        return
    employee = self.get_target_hr_employee_for_attendance()
    if employee is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_attendance_employee_required"))
        return
    initial = self.build_manual_hr_attendance_payload(employee, datetime.now().strftime("%Y-%m-%d"))
    self.open_hr_attendance_dialog(initial, is_new=True)


def open_hr_attendance_dialog(self, source_row: dict, *, is_new: bool = False) -> None:
    if is_new:
        if not self.is_action_allowed("action.hr.attendance.new"):
            return
    elif not self.is_action_allowed("action.hr.attendance.edit"):
        return
    initial = dict(source_row)
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("hr_attendance_new_title") if is_new else self.t("hr_attendance_edit_title"))
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.minsize(640, 460)

    frame = ttk.Frame(dialog, padding=18)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(1, weight=1)

    fields = {
        "work_date": tk.StringVar(value=str(initial.get("work_date", "")).strip()),
        "employee_id": tk.StringVar(value=str(initial.get("employee_id", "")).strip()),
        "employee_name": tk.StringVar(value=str(initial.get("employee_name", "")).strip()),
        "department": tk.StringVar(value=str(initial.get("department", "")).strip()),
        "check_in_at": tk.StringVar(value=str(initial.get("check_in_at", "")).strip()),
        "check_out_at": tk.StringVar(value=str(initial.get("check_out_at", "")).strip()),
        "break_out_at": tk.StringVar(value=str(initial.get("break_out_at", "")).strip()),
        "break_in_at": tk.StringVar(value=str(initial.get("break_in_at", "")).strip()),
        "status": tk.StringVar(value=str(initial.get("status", "")).strip()),
    }
    label_map = [
        ("work_date", "hr_work_date_label"),
        ("employee_id", "hr_employee_id_label"),
        ("employee_name", "hr_full_name_label"),
        ("department", "hr_department_label"),
        ("check_in_at", "hr_check_in_label"),
        ("check_out_at", "hr_check_out_label"),
        ("break_out_at", "hr_break_out_label"),
        ("break_in_at", "hr_break_in_label"),
        ("status", "hr_status_label"),
    ]
    for row_index, (key, text_key) in enumerate(label_map):
        ttk.Label(frame, text=self.t(text_key)).grid(row=row_index, column=0, sticky="w", padx=(0, 12), pady=5)
        ttk.Entry(frame, textvariable=fields[key]).grid(row=row_index, column=1, sticky="ew", pady=5)

    ttk.Label(frame, text=self.t("hr_note_label")).grid(row=len(label_map), column=0, sticky="nw", padx=(0, 12), pady=(8, 5))
    note_widget = tk.Text(frame, height=5, wrap="word")
    note_widget.grid(row=len(label_map), column=1, sticky="ew", pady=(8, 5))
    note_widget.insert("1.0", str(initial.get("note", "")).strip())

    def save_attendance() -> None:
        payload = dict(initial)
        for key, variable in fields.items():
            payload[key] = variable.get().strip()
        work_date = str(payload.get("work_date", "")).strip()
        employee_id = str(payload.get("employee_id", "")).strip()
        if not employee_id:
            messagebox.showwarning(self.t("warning_title"), self.t("hr_attendance_employee_required"))
            return
        if not work_date:
            messagebox.showwarning(self.t("warning_title"), self.t("hr_work_date_required"))
            return
        if is_new:
            duplicate = next(
                (
                    row
                    for row in self.hr_attendance_rows
                    if str(row.get("employee_id", "")).strip() == employee_id
                    and str(row.get("work_date", "")).strip() == work_date
                ),
                None,
            )
            if duplicate is not None:
                messagebox.showwarning(self.t("warning_title"), self.t("hr_attendance_exists"))
                return
        payload["note"] = note_widget.get("1.0", "end").strip()
        payload["manual_adjusted"] = True
        payload["updated_by"] = str(getattr(self, "current_user", {}).get("email", "")).strip()
        payload["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        saved = self.save_hr_attendance_record_to_source(payload)
        if isinstance(saved, dict):
            self.refresh_hr_module()
            self.set_status(self.t("hr_attendance_saved_status", employee_id=employee_id, work_date=work_date))
            dialog.destroy()

    actions = ttk.Frame(frame)
    actions.grid(row=len(label_map) + 1, column=0, columnspan=2, sticky="e", pady=(14, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(0, 8))
    ttk.Button(actions, text=self.t("collection_list_save"), command=save_attendance).pack(side="left")

    self.center_dialog(dialog, min_width=640, min_height=460)


def edit_selected_hr_attendance(self) -> None:
    row = self.get_selected_hr_attendance()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_attendance_warning"))
        return
    self.open_hr_attendance_dialog(row, is_new=False)


def delete_selected_hr_attendance(self) -> None:
    if not self.is_action_allowed("action.hr.attendance.delete"):
        return
    row = self.get_selected_hr_attendance()
    if row is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_select_attendance_warning"))
        return
    attendance_id = str(row.get("attendance_id", "")).strip()
    employee_id = str(row.get("employee_id", "")).strip()
    work_date = str(row.get("work_date", "")).strip()
    remaining_rows = [
        item for item in self.hr_attendance_rows
        if str(item.get("attendance_id", "")).strip() != attendance_id
    ]
    self.save_hr_attendance_records_to_source(remaining_rows)
    self.refresh_hr_module()
    self.set_status(self.t("hr_attendance_deleted", employee_id=employee_id, work_date=work_date))


def calculate_hr_worked_hours(self, row: dict) -> float:
    work_date = str(row.get("work_date", "")).strip()

    def parse_datetime_value(value: object) -> datetime | None:
        text = str(value or "").strip()
        if not text:
            return None
        for pattern in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M:%S", "%H:%M"):
            try:
                parsed = datetime.strptime(text, pattern)
                if pattern == "%H:%M" and work_date:
                    return datetime.strptime(f"{work_date} {text}", "%Y-%m-%d %H:%M")
                return parsed
            except Exception:
                continue
        return None

    check_in_dt = parse_datetime_value(row.get("check_in_at"))
    check_out_dt = parse_datetime_value(row.get("check_out_at"))
    break_out_dt = parse_datetime_value(row.get("break_out_at"))
    break_in_dt = parse_datetime_value(row.get("break_in_at"))
    if check_in_dt and check_out_dt:
        worked_minutes = max(0, int((check_out_dt - check_in_dt).total_seconds() // 60))
        if break_out_dt and break_in_dt:
            worked_minutes -= max(0, int((break_in_dt - break_out_dt).total_seconds() // 60))
        return max(0.0, worked_minutes / 60.0)
    return max(0.0, float(row.get("worked_minutes", 0) or 0) / 60.0)


def summarize_hr_attendance_month(self, employee_row: dict, year: int, month: int) -> dict[str, dict]:
    employee_id = str(employee_row.get("employee_id", "")).strip()
    summary: dict[str, dict] = {}
    for row in self.hr_attendance_rows:
        if str(row.get("employee_id", "")).strip() != employee_id:
            continue
        work_date = str(row.get("work_date", "")).strip()
        if not work_date.startswith(f"{year:04d}-{month:02d}-"):
            continue
        worked_hours = self.calculate_hr_worked_hours(row)
        missing_hours = max(0.0, 8.0 - worked_hours)
        color_key = "neutral"
        if worked_hours >= 8.0:
            color_key = "full"
        elif (10.0 / 60.0) <= missing_hours < 4:
            color_key = "short_yellow"
        elif 4 <= missing_hours <= 8:
            color_key = "short_red"
        summary[work_date] = {
            "row": dict(row),
            "worked_hours": worked_hours,
            "missing_hours": missing_hours,
            "color_key": color_key,
        }
    return summary


def open_hr_calendar_multi_select(
    self,
    *,
    parent: tk.Toplevel,
    title: str,
    employee_row: dict,
    initial_year: int,
    initial_month: int,
    selected_dates: set[str],
    overview_only: bool = False,
) -> None:
    owner_had_grab = False
    try:
        owner_had_grab = parent.grab_current() == parent
    except Exception:
        owner_had_grab = False
    if owner_had_grab:
        try:
            parent.grab_release()
        except Exception:
            pass

    picker = tk.Toplevel(parent)
    picker.title(title)
    picker.transient(parent)
    picker.resizable(False, False)
    picker.attributes("-topmost", True)
    picker.protocol("WM_DELETE_WINDOW", picker.destroy)

    container = ttk.Frame(picker, padding=10)
    container.pack(fill="both", expand=True)
    header = ttk.Frame(container)
    header.pack(fill="x")
    title_var = tk.StringVar(value="")
    selection_var = tk.StringVar(value="")
    ttk.Button(header, text="<", width=3, command=lambda: shift_month(-1)).pack(side="left")
    ttk.Label(header, textvariable=title_var, anchor="center", font=("Segoe UI", 10, "bold")).pack(side="left", expand=True)
    ttk.Button(header, text=">", width=3, command=lambda: shift_month(1)).pack(side="right")
    if not overview_only:
        ttk.Label(container, textvariable=selection_var, style="Muted.TLabel").pack(anchor="w", pady=(8, 0))
    else:
        ttk.Label(container, text=self.t("hr_overview_note"), style="Muted.TLabel").pack(anchor="w", pady=(8, 0))

    calendar_frame = ttk.Frame(container)
    calendar_frame.pack(fill="both", expand=True, pady=(8, 8))

    state = {"year": initial_year, "month": initial_month}

    def border_palette(color_key: str, is_today: bool, is_selected: bool) -> tuple[str, str]:
        if overview_only:
            if color_key == "full":
                return ("#4c8b4a", "#202020")
            if color_key == "short_yellow":
                return ("#c99700", "#202020")
            if color_key == "short_red":
                return ("#c75858", "#202020")
        if is_selected:
            return ("#4c8b4a", "#202020")
        if is_today:
            return ("#c99700", "#202020")
        return ("#404040", "#202020")

    def toggle_date(day: int) -> None:
        if overview_only:
            return
        work_date = f"{state['year']:04d}-{state['month']:02d}-{day:02d}"
        if work_date in selected_dates:
            selected_dates.remove(work_date)
        else:
            selected_dates.add(work_date)
        render_calendar()

    def shift_month(offset: int) -> None:
        month = state["month"] + offset
        year = state["year"]
        if month < 1:
            month = 12
            year -= 1
        elif month > 12:
            month = 1
            year += 1
        state["year"] = year
        state["month"] = month
        render_calendar()

    def render_calendar() -> None:
        today = datetime.now()
        month_summary = self.summarize_hr_attendance_month(employee_row, state["year"], state["month"])
        for child in calendar_frame.winfo_children():
            child.destroy()
        title_var.set(f"{calendar.month_name[state['month']]} {state['year']}")
        if not overview_only:
            selection_var.set(self.t("hr_quick_attendance_selected", count=len(selected_dates)))
        for col_index, label in enumerate(("Mo", "Tu", "We", "Th", "Fr", "Sa", "Su")):
            ttk.Label(calendar_frame, text=label, anchor="center", width=4).grid(row=0, column=col_index, padx=1, pady=1)
        for row_index, week in enumerate(calendar.monthcalendar(state["year"], state["month"]), start=1):
            for col_index, day in enumerate(week):
                if day == 0:
                    ttk.Label(calendar_frame, text="", width=4).grid(row=row_index, column=col_index, padx=1, pady=1)
                    continue
                work_date = f"{state['year']:04d}-{state['month']:02d}-{day:02d}"
                is_today = state["year"] == today.year and state["month"] == today.month and day == today.day
                is_selected = work_date in selected_dates
                summary = month_summary.get(work_date, {})
                border_color, fg = border_palette(str(summary.get("color_key", "neutral")), is_today, is_selected)
                border_width = 2 if (is_today or is_selected or summary) else 1
                wrapper = tk.Frame(calendar_frame, bg=border_color, padx=border_width, pady=border_width)
                wrapper.grid(row=row_index, column=col_index, padx=1, pady=1)
                button = tk.Button(
                    wrapper,
                    text=str(day),
                    width=4,
                    relief="flat",
                    borderwidth=0,
                    bg="#ffffff",
                    fg=fg,
                    activebackground="#f5f5f5",
                    activeforeground=fg,
                    command=(lambda selected_day=day: toggle_date(selected_day)),
                )
                if overview_only:
                    button.configure(command=lambda: None)
                button.pack()

    actions = ttk.Frame(container)
    actions.pack(fill="x")
    if not overview_only:
        ttk.Button(actions, text="Clear", command=lambda: clear_current_month()).pack(side="left")
    ttk.Button(actions, text=self.t("close"), command=picker.destroy).pack(side="right")

    def clear_current_month() -> None:
        month_prefix = f"{state['year']:04d}-{state['month']:02d}-"
        selected_dates.difference_update({item for item in selected_dates if item.startswith(month_prefix)})
        render_calendar()

    picker.update_idletasks()
    x = parent.winfo_rootx() + 20
    y = parent.winfo_rooty() + 50
    picker.geometry(f"+{x}+{y}")
    try:
        picker.grab_set()
    except Exception:
        pass
    render_calendar()
    picker.wait_window()
    if owner_had_grab:
        try:
            if parent.winfo_exists():
                parent.grab_set()
        except Exception:
            pass


def open_hr_quick_attendance_dialog(self) -> None:
    if not self.is_action_allowed("action.hr.attendance.quick"):
        return
    employee = self.get_target_hr_employee_for_attendance()
    if employee is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_attendance_employee_required"))
        return

    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("hr_quick_attendance_title"))
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.minsize(520, 220)

    frame = ttk.Frame(dialog, padding=18)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)

    now = datetime.now()
    year_var = tk.StringVar(value=str(now.year))
    month_var = tk.StringVar(value=str(now.month))
    selected_dates: set[str] = set()
    info_label = ttk.Label(frame, text=self.t("hr_quick_attendance_note"), style="Muted.TLabel")
    info_label.grid(row=1, column=0, sticky="w", pady=(8, 0))

    top = ttk.Frame(frame)
    top.grid(row=0, column=0, sticky="ew")
    ttk.Label(top, text=self.t("hr_attendance_employee_label")).pack(side="left")
    ttk.Label(top, text=f"{str(employee.get('employee_id', '')).strip()} - {str(employee.get('full_name', '')).strip()}").pack(side="left", padx=(8, 24))
    ttk.Label(top, text=self.t("hr_quick_attendance_month_label")).pack(side="left")
    month_combo = ttk.Combobox(top, textvariable=month_var, width=4, state="readonly", values=[str(index) for index in range(1, 13)])
    month_combo.pack(side="left", padx=(8, 16))
    ttk.Label(top, text=self.t("hr_quick_attendance_year_label")).pack(side="left")
    year_combo = ttk.Combobox(top, textvariable=year_var, width=6, state="readonly", values=[str(now.year - 1), str(now.year), str(now.year + 1)])
    year_combo.pack(side="left", padx=(8, 0))
    summary_var = tk.StringVar(value=self.t("hr_quick_attendance_selected", count=0))
    ttk.Label(frame, textvariable=summary_var, style="FieldLabel.TLabel").grid(row=2, column=0, sticky="w", pady=(12, 0))
    ttk.Button(
        frame,
        text=self.t("hr_quick_attendance_days_label"),
        command=lambda: open_picker(),
    ).grid(row=3, column=0, sticky="w", pady=(12, 0))

    def open_picker() -> None:
        self.open_hr_calendar_multi_select(
            parent=dialog,
            title=self.t("hr_quick_attendance_title"),
            employee_row=employee,
            initial_year=int(year_var.get().strip() or now.year),
            initial_month=int(month_var.get().strip() or now.month),
            selected_dates=selected_dates,
            overview_only=False,
        )
        summary_var.set(self.t("hr_quick_attendance_selected", count=len(selected_dates)))

    def save_quick_attendance() -> None:
        if not selected_dates:
            dialog.destroy()
            return
        merged_rows = list(self.hr_attendance_rows)
        existing_map = {
            (str(row.get("employee_id", "")).strip(), str(row.get("work_date", "")).strip()): dict(row)
            for row in merged_rows
        }
        employee_id = str(employee.get("employee_id", "")).strip()
        for work_date in sorted(selected_dates):
            payload = self.build_manual_hr_attendance_payload(employee, work_date, full_shift=True)
            existing = existing_map.get((employee_id, work_date))
            if existing is not None:
                payload["attendance_id"] = str(existing.get("attendance_id", "")).strip() or payload["attendance_id"]
                payload["created_at"] = str(existing.get("created_at", "")).strip() or payload["created_at"]
            merged_rows = [
                row for row in merged_rows
                if not (
                    str(row.get("employee_id", "")).strip() == employee_id
                    and str(row.get("work_date", "")).strip() == work_date
                )
            ]
            merged_rows.append(payload)
        self.save_hr_attendance_records_to_source(merged_rows)
        self.refresh_hr_module()
        self.set_status(self.t("hr_quick_attendance_saved", employee_id=employee_id))
        dialog.destroy()

    actions = ttk.Frame(frame)
    actions.grid(row=4, column=0, sticky="e", pady=(18, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(0, 8))
    ttk.Button(actions, text=self.t("collection_list_save"), command=save_quick_attendance).pack(side="left")

    self.center_dialog(dialog, min_width=520, min_height=220)


def open_hr_attendance_overview_dialog(self) -> None:
    if not self.is_action_allowed("action.hr.attendance.overview"):
        return
    employee = self.get_target_hr_employee_for_attendance()
    if employee is None:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_attendance_employee_required"))
        return
    now = datetime.now()
    year = now.year
    month = now.month
    selected_dates: set[str] = set()
    dialog = tk.Toplevel(self.root)
    dialog.title(self.t("hr_overview_title"))
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.minsize(420, 150)
    frame = ttk.Frame(dialog, padding=18)
    frame.pack(fill="both", expand=True)
    ttk.Label(
        frame,
        text=f"{self.t('hr_attendance_employee_label')}: {str(employee.get('employee_id', '')).strip()} - {str(employee.get('full_name', '')).strip()}",
    ).pack(anchor="w")
    ttk.Label(frame, text=self.t("hr_overview_note"), style="Muted.TLabel").pack(anchor="w", pady=(8, 0))
    ttk.Button(
        frame,
        text=self.t("hr_attendance_overview_button"),
        command=lambda: self.open_hr_calendar_multi_select(
            parent=dialog,
            title=self.t("hr_overview_title"),
            employee_row=employee,
            initial_year=year,
            initial_month=month,
            selected_dates=selected_dates,
            overview_only=True,
        ),
    ).pack(anchor="w", pady=(14, 0))
    ttk.Button(frame, text=self.t("close"), command=dialog.destroy).pack(anchor="e", pady=(18, 0))
    self.center_dialog(dialog, min_width=420, min_height=150)


def export_hr_attendance_report(self) -> None:
    if not self.is_action_allowed("action.hr.export_report"):
        return
    rows = list(self.filtered_hr_attendance_rows)
    if not rows:
        messagebox.showwarning(self.t("warning_title"), self.t("hr_no_attendance_rows"))
        return
    save_path = filedialog.asksaveasfilename(
        title=self.t("hr_export_title"),
        defaultextension=".xlsx",
        filetypes=[("Excel files", "*.xlsx")],
        initialfile=f"hr_attendance_{datetime.now().strftime('%Y%m%d%H%M')}.xlsx",
    )
    if not save_path:
        return
    workbook = Workbook()
    worksheet = workbook.active
    worksheet.title = "Attendance"
    headers = [
        self.t("hr_work_date_label"),
        self.t("hr_employee_id_label"),
        self.t("hr_full_name_label"),
        self.t("hr_department_label"),
        self.t("hr_check_in_label"),
        self.t("hr_check_out_label"),
        self.t("hr_worked_minutes_label"),
        self.t("hr_working_day_label"),
        self.t("hr_late_minutes_label"),
        self.t("hr_early_leave_minutes_label"),
        self.t("hr_status_label"),
    ]
    for column_index, header in enumerate(headers, start=1):
        worksheet.cell(row=1, column=column_index, value=header)
    for row_index, row in enumerate(rows, start=2):
        values = [
            row.get("work_date"),
            row.get("employee_id"),
            row.get("employee_name"),
            row.get("department"),
            row.get("check_in_at"),
            row.get("check_out_at"),
            row.get("worked_minutes"),
            row.get("working_day"),
            row.get("late_minutes"),
            row.get("early_leave_minutes"),
            row.get("status"),
        ]
        for column_index, value in enumerate(values, start=1):
            worksheet.cell(row=row_index, column=column_index, value=self.display_value(value))
    workbook.save(save_path)
    self.set_status(self.t("hr_export_done"))


def update_hr_module_status(self) -> None:
    employee_total = len(getattr(self, "hr_employee_rows", []) or [])
    employee_filtered = len(getattr(self, "filtered_hr_employee_rows", []) or [])
    attendance_total = len(getattr(self, "hr_attendance_rows", []) or [])
    attendance_filtered = len(getattr(self, "filtered_hr_attendance_rows", []) or [])
    self.set_status(
        self.t(
            "hr_status_summary",
            employee_total=employee_total,
            employee_filtered=employee_filtered,
            attendance_total=attendance_total,
            attendance_filtered=attendance_filtered,
        )
    )


def bind_hr_module(app_cls: type) -> None:
    method_map = {
        "_build_hr_module": _build_hr_module,
        "refresh_hr_module": refresh_hr_module,
        "commit_hr_employee_search_query": commit_hr_employee_search_query,
        "on_hr_employee_search_submit": on_hr_employee_search_submit,
        "commit_hr_attendance_search_query": commit_hr_attendance_search_query,
        "on_hr_attendance_search_submit": on_hr_attendance_search_submit,
        "apply_hr_employee_filter": apply_hr_employee_filter,
        "apply_hr_attendance_filter": apply_hr_attendance_filter,
        "refresh_hr_employee_tree": refresh_hr_employee_tree,
        "refresh_hr_attendance_tree": refresh_hr_attendance_tree,
        "get_selected_hr_employee": get_selected_hr_employee,
        "get_selected_hr_attendance": get_selected_hr_attendance,
        "build_hr_user_account_options": build_hr_user_account_options,
        "refresh_hr_attendance_employee_selector": refresh_hr_attendance_employee_selector,
        "open_hr_employee_dialog": open_hr_employee_dialog,
        "edit_selected_hr_employee": edit_selected_hr_employee,
        "on_hr_employee_double_click": on_hr_employee_double_click,
        "delete_selected_hr_employee": delete_selected_hr_employee,
        "toggle_selected_hr_employee_active": toggle_selected_hr_employee_active,
        "get_target_hr_employee_for_attendance": get_target_hr_employee_for_attendance,
        "build_manual_hr_attendance_payload": build_manual_hr_attendance_payload,
        "hr_check_in_selected_employee": hr_check_in_selected_employee,
        "hr_check_out_selected_employee": hr_check_out_selected_employee,
        "open_new_hr_attendance_dialog": open_new_hr_attendance_dialog,
        "open_hr_attendance_dialog": open_hr_attendance_dialog,
        "edit_selected_hr_attendance": edit_selected_hr_attendance,
        "delete_selected_hr_attendance": delete_selected_hr_attendance,
        "calculate_hr_worked_hours": calculate_hr_worked_hours,
        "summarize_hr_attendance_month": summarize_hr_attendance_month,
        "open_hr_calendar_multi_select": open_hr_calendar_multi_select,
        "open_hr_quick_attendance_dialog": open_hr_quick_attendance_dialog,
        "open_hr_attendance_overview_dialog": open_hr_attendance_overview_dialog,
        "export_hr_attendance_report": export_hr_attendance_report,
        "update_hr_module_status": update_hr_module_status,
    }
    for method_name, function in method_map.items():
        setattr(app_cls, method_name, function)
