WeeklyReport

Weekly report helper + task deadline monitor (overdue + near due)

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         WeeklyReport
// @namespace    vnpt-weekly-report
// @version      2026-01-27
// @description  Weekly report helper + task deadline monitor (overdue + near due)
// @author       You
// @match        https://*.cds.hcmict.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=hcmict.io
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @connect      script.google.com
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    /*********************************************************
   * CONFIG
   *********************************************************/
    const CONFIG = {
        API_URL: "https://api_cds.hcmict.io/api",
        GAS_WEBAPP_URL: "https://script.google.com/macros/s/AKfycbwM_k5QC2c9EWm1A58iJQYL8cz80R_pjROtH2xFSgHlvAcaQWYZ-SfiKuQXmTSsN4e0/exec",
        BOARD_ID: 257,
        UI_INIT_DELAY_MS: 4000,
        TASK_NOTIFY_TIMEOUT_MS: 10000,
        XLSX_CDN: "https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js",
    };


    const USER_IDS = [
        {
            user_id: 876,
            username: 'ducct.bdg',
            full_name: 'Cấn Trọng Đức',
            display_name: 'Cấn Trọng Đức (ducct.bdg)'
        },
        {
            user_id: 1081,
            username: 'nguyenqhuy.hcm',
            full_name: 'Nguyễn Quang Huy',
            display_name: 'Nguyễn Quang Huy (nguyenqhuy.hcm)'
        },
        {
            user_id: 1962,
            username: 'thangnn.vtu',
            full_name: 'Nguyễn Ngọc Thắng',
            display_name: 'Nguyễn Ngọc Thắng (thangnn.vtu)'
        },
        {
            user_id: 1958,
            username: 'vinhnq.bdg',
            full_name: 'Nguyễn Quốc Vinh',
            display_name: 'Nguyễn Quốc Vinh (vinhnq.bdg)'
        },
        {
            user_id: 1881,
            username: 'vinhdh.bdg',
            full_name: 'Đỗ Hữu Vinh',
            display_name: 'Đỗ Hữu Vinh (vinhdh.bdg)'
        },
        {
            user_id: 990,
            username: 'hieutt.vtu',
            full_name: 'Trần Trung Hiếu',
            display_name: 'Trần Trung Hiếu (hieutt.vtu)'
        },
        {
            user_id: 1374,
            username: 'nnhai.hcm',
            full_name: 'Nguyễn Ngọc Hải',
            display_name: 'Nguyễn Ngọc Hải (nnhai.hcm)'
        },
        {
            user_id: 1194,
            username: 'longbp.hcm',
            full_name: 'Bùi Phi Long',
            display_name: 'Bùi Phi Long (longbp.hcm)'
        },
        {
            user_id: 1274,
            username: 'nghianh.bdg',
            full_name: 'Nguyễn Hữu Nghĩa',
            display_name: 'Nguyễn Hữu Nghĩa (nghianh.bdg)'
        },
        {
            user_id: 1584,
            username: 'thaipv.vtu',
            full_name: 'Phan Văn Thái',
            display_name: 'Phan Văn Thái (thaipv.vtu)'
        },
        {
            user_id: 846,
            username: 'daothimyan.hcm',
            full_name: 'Đoàn Thị Mỹ An',
            display_name: 'Đoàn Thị Mỹ An (daothimyan.hcm)'
        },
        {
            user_id: 1378,
            username: 'npnam.bdg',
            full_name: 'Nguyễn Phương Nam',
            display_name: 'Nguyễn Phương Nam (npnam.bdg)'
        },
        {
            user_id: 1371,
            username: 'nmduc.hcm',
            full_name: 'Nguyễn Minh Đức',
            display_name: 'Nguyễn Minh Đức (nmduc.hcm)'
        },
        {
            user_id: 984,
            username: 'hieuhv.hcm',
            full_name: 'Huỳnh Văn Hiếu',
            display_name: 'Huỳnh Văn Hiếu (hieuhv.hcm)'
        },
        {
            user_id: 1565,
            username: 'tamnt.hcm',
            full_name: 'Nguyễn Thanh Tám',
            display_name: 'Nguyễn Thanh Tám (tamnt.hcm)'
        },
        {
            user_id: 1742,
            username: 'trangltt.bdg',
            full_name: 'Lê Thị Thùy Trang',
            display_name: 'Lê Thị Thùy Trang (trangltt.bdg)'
        },
        {
            user_id: 1133,
            username: 'kienlt.bdg',
            full_name: 'Lê Trung Kiên',
            display_name: 'Lê Trung Kiên (kienlt.bdg)'
        },
        {
            user_id: 1040,
            username: 'hungtm.vtu',
            full_name: 'Thái Mạnh Hùng',
            display_name: 'Thái Mạnh Hùng (hungtm.vtu)'
        },
        {
            user_id: 1498,
            username: 'pvanquang.hcm',
            full_name: 'Phạm Văn Quang',
            display_name: 'Phạm Văn Quang (pvanquang.hcm)'
        },
        {
            user_id: 1217,
            username: 'lupt.bdg',
            full_name: 'Phạm Thế Lữ',
            display_name: 'Phạm Thế Lữ (lupt.bdg)'
        },
        {
            user_id: 1542,
            username: 'sondt.vtu',
            full_name: 'Đặng Thái Sơn',
            display_name: 'Đặng Thái Sơn (sondt.vtu)'
        },
        {
            user_id: 1483,
            username: 'phuongph.bdg',
            full_name: 'Phạm Hoàng Phương',
            display_name: 'Phạm Hoàng Phương (phuongph.bdg)'
        },
        {
            user_id: 1961,
            username: 'duytnm.bdg',
            full_name: 'Trần Nguyễn Minh Duy',
            display_name: 'Trần Nguyễn Minh Duy (duytnm.bdg)'
        },
        {
            user_id: 1689,
            username: 'thuyan.hcm',
            full_name: 'Trần Thị Thúy An',
            display_name: 'Trần Thị Thúy An (thuyan.hcm)'
        },
        {
            user_id: 1598,
            username: 'thanhdn',
            full_name: 'Đỗ  Ngọc Thanh',
            display_name: 'Đỗ  Ngọc Thanh (thanhdn)'
        },
        {
            user_id: 905,
            username: 'duongcongtrung',
            full_name: 'Dương Công Trung',
            display_name: 'Dương Công Trung (duongcongtrung)'
        },
        {
            user_id: 1178,
            username: 'linhnd.bdg',
            full_name: 'Nguyễn Đăng Linh',
            display_name: 'Nguyễn Đăng Linh (linhnd.bdg)'
        },
        {
            user_id: 1028,
            username: 'huenv.vtu',
            full_name: 'Nguyễn Vinh Huế',
            display_name: 'Nguyễn Vinh Huế (huenv.vtu)'
        },
        {
            user_id: 1965,
            username: 'sonnct.bdg',
            full_name: 'Nguyễn Chí Trung Sơn',
            display_name: 'Nguyễn Chí Trung Sơn (sonnct.bdg)'
        },
        {
            user_id: 1673,
            username: 'thuanhm.hcm',
            full_name: 'Hồ Minh Thuần',
            display_name: 'Hồ Minh Thuần (thuanhm.hcm)'
        },
        {
            user_id: 1863,
            username: 'vangtta.hcm',
            full_name: 'Tô Thị Ánh Vàng',
            display_name: 'Tô Thị Ánh Vàng (vangtta.hcm)'
        },
        {
            user_id: 1001,
            username: 'hoangmh.vtu',
            full_name: 'Mai Huy Hoàng',
            display_name: 'Mai Huy Hoàng (hoangmh.vtu)'
        },
        {
            user_id: 1354,
            username: 'nhson1',
            full_name: 'Nguyễn Hồng Sơn',
            display_name: 'Nguyễn Hồng Sơn (nhson1)'
        },
        {
            user_id: 1048,
            username: 'huongpt.vtu',
            full_name: 'Phạm Thị Hương',
            display_name: 'Phạm Thị Hương (huongpt.vtu)'
        },
        {
            user_id: 1470,
            username: 'phuochc.hcm',
            full_name: 'Huỳnh Công Phước',
            display_name: 'Huỳnh Công Phước (phuochc.hcm)'
        },
        {
            user_id: 1276,
            username: 'nghiantt',
            full_name: 'Nguyễn Thanh Trọng Nghĩa',
            display_name: 'Nguyễn Thanh Trọng Nghĩa (nghiantt)'
        },
        {
            user_id: 1558,
            username: 'taint.hcm',
            full_name: 'Nguyễn Thành Tài',
            display_name: 'Nguyễn Thành Tài (taint.hcm)'
        },
        {
            user_id: 1758,
            username: 'trieunh.bdg',
            full_name: 'Nguyễn Hà Triều',
            display_name: 'Nguyễn Hà Triều (trieunh.bdg)'
        },
        {
            user_id: 1859,
            username: 'uyenhk.hcm',
            full_name: 'Hà Khánh Uyên',
            display_name: 'Hà Khánh Uyên (uyenhk.hcm)'
        },
        {
            user_id: 1626,
            username: 'thaomtt.vtu',
            full_name: 'Mai Thị Thanh Thảo',
            display_name: 'Mai Thị Thanh Thảo (thaomtt.vtu)'
        },
        {
            user_id: 1855,
            username: 'tvhuan.hcm',
            full_name: 'Trần Văn Huấn',
            display_name: 'Trần Văn Huấn (tvhuan.hcm)'
        },
        {
            user_id: 1118,
            username: 'khoala.vtu',
            full_name: 'Lâm Anh Khoa',
            display_name: 'Lâm Anh Khoa (khoala.vtu)'
        },
        {
            user_id: 1013,
            username: 'hoidq.hcm',
            full_name: 'Dương Quốc Hội',
            display_name: 'Dương Quốc Hội (hoidq.hcm)'
        },
        {
            user_id: 1301,
            username: 'nguyenha.hcm',
            full_name: 'Nguyễn Việt Hà',
            display_name: 'Nguyễn Việt Hà (nguyenha.hcm)'
        },
        {
            user_id: 847,
            username: 'datct.hcm',
            full_name: 'Cao Thái Đạt',
            display_name: 'Cao Thái Đạt (datct.hcm)'
        },
        {
            user_id: 1139,
            username: 'kietva.hcm',
            full_name: 'Vũ Anh Kiệt',
            display_name: 'Vũ Anh Kiệt (kietva.hcm)'
        },
        {
            user_id: 770,
            username: 'baobt',
            full_name: 'Bùi Thế Bảo',
            display_name: 'Bùi Thế Bảo (baobt)'
        },
        {
            user_id: 1164,
            username: 'lengoclinh',
            full_name: 'Lê Ngọc Linh',
            display_name: 'Lê Ngọc Linh (lengoclinh)'
        },
        {
            user_id: 1816,
            username: 'tuannha.vtu',
            full_name: 'Nguyễn Hoàng Anh Tuấn',
            display_name: 'Nguyễn Hoàng Anh Tuấn (tuannha.vtu)'
        },
        {
            user_id: 1645,
            username: 'thiennn.hcm',
            full_name: 'Nguyễn Ngọc Thiện',
            display_name: 'Nguyễn Ngọc Thiện (thiennn.hcm)'
        },
        {
            user_id: 1671,
            username: 'ththanh.hcm',
            full_name: 'Trịnh Hữu Thanh',
            display_name: 'Trịnh Hữu Thanh (ththanh.hcm)'
        },
        {
            user_id: 1897,
            username: 'vunc.vtu',
            full_name: 'Nguyễn Công Vũ',
            display_name: 'Nguyễn Công Vũ (vunc.vtu)'
        },
        {
            user_id: 1546,
            username: 'sonnn.hcm',
            full_name: 'Nguyễn Ngọc Sơn',
            display_name: 'Nguyễn Ngọc Sơn (sonnn.hcm)'
        },
        {
            user_id: 1831,
            username: 'tudq.vtu',
            full_name: 'Đặng Quang Tú',
            display_name: 'Đặng Quang Tú (tudq.vtu)'
        },
        {
            user_id: 1367,
            username: 'ninhnguyenan.hcm',
            full_name: 'Nguyễn An Ninh',
            display_name: 'Nguyễn An Ninh (ninhnguyenan.hcm)'
        },
        {
            user_id: 1964,
            username: 'minhtri.bdg',
            full_name: 'Nguyễn Minh Trí',
            display_name: 'Nguyễn Minh Trí (minhtri.bdg)'
        },
        {
            user_id: 1512,
            username: 'quanvq.hcm',
            full_name: 'Võ Quốc Quân',
            display_name: 'Võ Quốc Quân (quanvq.hcm)'
        },
        {
            user_id: 1950,
            username: 'phuongthao.hcm',
            full_name: 'Tô Thị Phương Thảo',
            display_name: 'Tô Thị Phương Thảo (phuongthao.hcm)'
        },
        {
            user_id: 1263,
            username: 'nganph.hcm',
            full_name: 'Nguyễn Phạm Hồng Nga',
            display_name: 'Nguyễn Phạm Hồng Nga (nganph.hcm)'
        },
        {
            user_id: 1446,
            username: 'phongnx.hcm',
            full_name: 'Nguyễn Xuân Phong',
            display_name: 'Nguyễn Xuân Phong (phongnx.hcm)'
        }
    ];
    /*********************************************************
   * STATE
   *********************************************************/
    const state = {
        notifiedKeys: new Set(), // prevent spam (per assignee + task + status)
    };

    /*********************************************************
   * DOM HELPERS
   *********************************************************/
    const $ = (sel, root = document) => root.querySelector(sel);

    function clearAlertBox() {
        $("#tm-alert-box")?.remove();
    }

    /*********************************************************
   * XLSX
   *********************************************************/
    function loadXLSX(cb) {
        if (window.XLSX) return cb();
        const s = document.createElement("script");
        s.src = CONFIG.XLSX_CDN;
        s.onload = cb;
        document.head.appendChild(s);
    }

    /*********************************************************
   * COOKIE / TOKEN
   *********************************************************/
    function getCookie(name) {
        const cookies = document.cookie.split(';');
        for (const c of cookies) {
            const [k, v] = c.trim().split('=');
            if (k === name) return v;
        }
        return null;
    }

    function getTokenFromCookie() {
        const raw = getCookie("VNPT-Token");
        if (!raw) throw new Error("VNPT-Token not found");
        return JSON.parse(raw).token;
    }

    function getCurrentUserUsidFromCookie() {
        try {
            const raw = getCookie("VNPT-Token");
            if (!raw) return null;
            const parsed = JSON.parse(raw);
            return parsed?.user?.usid ?? null;
        } catch {
            return null;
        }
    }

    /*********************************************************
   * DATE HELPERS
   *********************************************************/
    const pad2 = n => String(n).padStart(2, '0');
    const fmtISO = d => `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
    const fmtVN  = d => `${pad2(d.getDate())}/${pad2(d.getMonth() + 1)}/${d.getFullYear()}`;

    function startOfWeekMon(d) {
        const x = new Date(d);
        const day = x.getDay();
        const diff = day === 0 ? -6 : 1 - day;
        x.setDate(x.getDate() + diff);
        x.setHours(0, 0, 0, 0);
        return x;
    }

    function endOfWeekSun(d) {
        const m = startOfWeekMon(d);
        const s = new Date(m);
        s.setDate(m.getDate() + 6);
        s.setHours(23, 59, 59, 999);
        return s;
    }

    function toDDMMYYYY(value) {
        const [y, m, d] = value.split("-");
        return `${d}/${m}/${y}`;
    }

    function formatToDDMMYYYY(isoDateTime) {
        if (!isoDateTime) return "";
        const [date] = isoDateTime.split("T");
        const [y, m, d] = date.split("-");
        return `${d}/${m}/${y}`;
    }

    function dayStart(d = new Date()) {
        const x = new Date(d);
        x.setHours(0, 0, 0, 0);
        return x;
    }

    function parseDay(dateStr) {
        if (!dateStr) return null;
        const x = new Date(dateStr);
        x.setHours(0, 0, 0, 0);
        return x;
    }

    function getMonthParamNow() {
        const now = new Date();
        const mm = pad2(now.getMonth() + 1);
        const yyyy = now.getFullYear();
        return `${mm}%2F${yyyy}`; // "01%2F2026"
    }

    /*********************************************************
   * FETCH (API)
   *********************************************************/
    async function fetchData(url) {
        const token = getTokenFromCookie();
        const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
        const json = await res.json();
        if (!json?.data) return [];
        return typeof json.data === "string" ? JSON.parse(json.data) : json.data;
    }

    async function fetchTaskInfo(taskId) {
        const url = `${CONFIG.API_URL}/work/Task/GetTaskInfo?taskId=${taskId}&boardId=${CONFIG.BOARD_ID}&t=${Date.now()}`;
        return await fetchData(url);
    }

    async function fetchTaskDetailTable({ assigneeId, monthParam }) {
        if (!assigneeId) return [];
        const url =
              `${CONFIG.API_URL}/report/dashboard/DashboardQLCV/getTaskDetailTable` +
              `?month=${monthParam}&assignee_id=${assigneeId}&t=${Date.now()}`;
        return await fetchData(url);
    }

    /*********************************************************
   * TASK DEADLINE RULES
   *********************************************************/
    const DeadlineStatus = Object.freeze({
        OVERDUE: "OVERDUE",
        NEAR_DUE: "NEAR_DUE",
        NONE: null
    });

    function getTaskEndDate(task) {
        const endStr = task.actual_end || task.schedule_end;
        return parseDay(endStr);
    }

    function getDeadlineStatus(task) {
        const today = dayStart();
        const tomorrow = new Date(today);
        tomorrow.setDate(today.getDate() + 3);

        const end = getTaskEndDate(task);
        if (!end) return DeadlineStatus.NONE;

        if (end < today) return DeadlineStatus.OVERDUE;
        if (end.getTime() <= tomorrow.getTime() && end.getTime() > today) return DeadlineStatus.NEAR_DUE;

        return DeadlineStatus.NONE;
    }

    function makeNotifyKey(assigneeId, task, status) {
        return `${assigneeId}:${task.task_id || task.code}:${status}`;
    }

    /*********************************************************
   * ALERT UI (in your box)
   *********************************************************/
    function showAlertInBox(text, bgColor) {
        let alertBox = $("#tm-alert-box");

        if (!alertBox) {
            alertBox = document.createElement("div");
            alertBox.id = "tm-alert-box";
            alertBox.style = `
        margin-top:10px;
        padding:8px;
        border-radius:6px;
        color:#fff;
        font-size:12px;
        white-space:pre-line;
      `;
            $("#content")?.appendChild(alertBox);
        }

        alertBox.style.background = bgColor;
        alertBox.textContent = text;
    }

    function notifyTask(task, status) {
        const isOverdue = status === DeadlineStatus.OVERDUE;

        const title = isOverdue
        ? "❌ Công việc quá hạn"
        : "⚠️ Công việc sắp tới hạn (1 ngày)";

        // GM_notification({
        //     title,
        //     text: `${task.code} - ${task.type_task}`,
        //     timeout: CONFIG.TASK_NOTIFY_TIMEOUT_MS,
        //     silent: !isOverdue,
        //     onclick: () => window.focus()
        // });
    }

    function renderAndNotify(task, status) {
        let bgColor = "#334155";
        let message = "";

        if (status === DeadlineStatus.OVERDUE) {
            bgColor = "#7f1d1d";
            message =
`❌ QUÁ HẠN
📌 Mã: ${task.code}
📝 ${task.type_task}
📂 ${task.group_task}
📅 Hạn: ${task.schedule_end}`;
        }

        if (status === DeadlineStatus.NEAR_DUE) {
            bgColor = "#92400e";
            message =
`⚠️ SẮP TỚI HẠN (1 ngày)
📌 Mã: ${task.code}
📝 ${task.type_task}
📂 ${task.group_task}
📅 Hạn: ${task.schedule_end}`;
        }

        showAlertInBox(message, bgColor);
        notifyTask(task, status);
    }

    /*********************************************************
   * TASK CHECK FLOW

   *********************************************************/

    function renderAlertBox(overdueTasks, nearDueTasks) {
        if (!overdueTasks.length && !nearDueTasks.length) return;

        let box = document.getElementById("tm-alert-box");
        if (!box) {
            box = document.createElement("div");
            box.id = "tm-alert-box";
            box.style = `
            margin-top:10px;
            padding:8px;
            border-radius:6px;
            color:#fff;
            font-size:12px;
            max-height:220px;
            overflow:auto;
            white-space:pre-line;
        `;
            document.querySelector("#content")?.appendChild(box);
        }

        let html = "";

        if (overdueTasks.length) {
            box.style.background = "#7f1d1d";
            html += `❌ QUÁ HẠN (${overdueTasks.length})\n`;
            overdueTasks.forEach(t => {
                html += `• [${t.code}] ${t.type_task} (Hạn ${toDDMMYYYY(t.schedule_end)})\n`;
            });
            html += `\n`;
        }

        if (nearDueTasks.length) {
            box.style.background = "#92400e";
            html += `⚠️ SẮP TỚI HẠN (${nearDueTasks.length})\n`;
            nearDueTasks.forEach(t => {
                html += `• [${t.code}] ${t.type_task} \n(Hạn ${toDDMMYYYY(t.schedule_end)}) \n`;
            });
        }

        box.textContent = html.trim();
    }

    function notifyGroupedTasks(assigneeId, overdueTasks, nearDueTasks) {

        if (overdueTasks.length) {
            const key = `${assigneeId}_OVERDUE`;
            if (!state.notifiedKeys.has(key)) {
                state.notifiedKeys.add(key);

                GM_notification({
                    title: `❌ ${overdueTasks.length} công việc quá hạn`,
                    text: overdueTasks
                    .slice(0, 4)
                    .map(t => `[${t.code}] ${t.type_task}`)
                    .join("\n") +
                    (overdueTasks.length > 4 ? `\n… và ${overdueTasks.length - 4} task khác` : ""),
                    timeout: 12000,
                    onclick: () => window.focus()
                });
            }
        }

        if (nearDueTasks.length) {
            const key = `${assigneeId}_NEAR_DUE`;
            if (!state.notifiedKeys.has(key)) {
                state.notifiedKeys.add(key);

                GM_notification({
                    title: `⚠️ ${nearDueTasks.length} công việc sắp tới hạn`,
                    text: nearDueTasks
                    .slice(0, 4)
                    .map(t => `[${t.code}] ${t.type_task}`)
                    .join("\n") +
                    (nearDueTasks.length > 4 ? `\n… và ${nearDueTasks.length - 4} task khác` : ""),
                    timeout: 10000,
                    silent: true,
                    onclick: () => window.focus()
                });
            }
        }
    }


    function getSelectedAssigneeId() {
        const userSelect = $("#wr_user");
        if (!userSelect) return "";
        const v = userSelect.value;
        return v === "ALL" ? "" : v;
    }

    function resetNotifyCacheForUser() {
        state.notifiedKeys.clear();
    }

    async function checkCurrentTasksForSelectedUser() {
        const assigneeId = getSelectedAssigneeId();
        if (!assigneeId) {
            clearAlertBox();
            return;
        }

        clearAlertBox();

        const monthParam = getMonthParamNow();
        const tasks = await fetchTaskDetailTable({ assigneeId, monthParam });

        const overdueTasks = [];
        const nearDueTasks = [];

        for (const task of tasks) {
            if (task.status_name !== "Chưa hoàn thành") continue;

            const status = getDeadlineStatus(task);
            if (status === DeadlineStatus.OVERDUE) overdueTasks.push(task);
            if (status === DeadlineStatus.NEAR_DUE) nearDueTasks.push(task);
        }

        renderAlertBox(overdueTasks, nearDueTasks);
        notifyGroupedTasks(assigneeId, overdueTasks, nearDueTasks);
    }



    /*********************************************************
   * WEEK PICKER
   *********************************************************/
    function getWeeksOfCurrentMonth() {
        const now = new Date();
        const first = new Date(now.getFullYear(), now.getMonth(), 1);
        const last  = new Date(now.getFullYear(), now.getMonth() + 1, 0);

        let cur = startOfWeekMon(first);
        let idx = 1;
        const weeks = [];

        while (cur <= last) {
            const ws = new Date(Math.max(cur, first));
            const we = new Date(Math.min(endOfWeekSun(cur), last));
            weeks.push({
                index: idx++,
                fromISO: fmtISO(ws),
                toISO: fmtISO(we),
                label: `Tuần ${idx - 1}: ${fmtVN(ws)} → ${fmtVN(we)}`
      });
            cur.setDate(cur.getDate() + 7);
        }
        return weeks;
    }

    /*********************************************************
   * CORE DATA (Weekly Report)
   *********************************************************/
    async function getWeeklyReportData() {
        const assigneeId = getSelectedAssigneeId();

        const fromRaw = $("#wr_from")?.value;
        const toRaw   = $("#wr_to")?.value;

        const fromDate = encodeURIComponent(toDDMMYYYY(fromRaw));
        const toDate   = encodeURIComponent(toDDMMYYYY(toRaw));
        if (!fromDate || !toDate) {
            alert("❌ Thiếu fromDate / toDate");
            return [];
        }

        const url =
              `${CONFIG.API_URL}/report/dashboard/DashboardWeeklyTask/getDetailByDate` +
              `?assignee_id=${assigneeId}&fromDate=${fromDate}&toDate=${toDate}&t=${Date.now()}`;

        const data = await fetchData(url);
        const seen = new Set();
        const result = [];

        for (const item of data) {
            if (seen.has(item.task_id)) continue;
            seen.add(item.task_id);

            const taskInfo = await fetchTaskInfo(item.task_id);

            result.push({
                employee: item.account_name,
                code: item.code,
                task_name: item.task_name,
                customer: taskInfo?.customer?.customer_name || "",
                start_date: item.create_day,
                end_date: item.schedule_end,
                status: item.status,
                planned_hours: item.planned_duration_time,
                actual_hours: item.actual_execution_time,
                jira: item.jira_url
            });
        }
        return result;
    }

    /*********************************************************
   * EXCEL
   *********************************************************/
    function mapTasksToExcelRows(tasks) {
        return tasks.map((t, i) => ({
            "STT": i + 1,

            "Dự án (Nội dung công việc)":
            `[${t.code}] ${t.customer ? t.customer + " - " : ""}${t.task_name}`,

            "Loại\n1: Dựng mới\n2: Chỉnh sửa": 2,

            "Mức độ\n1: Bình thường\n2: Quan trọng\n3: Rất quan trọng": 1,

            "Thời gian bắt đầu": formatToDDMMYYYY(t.start_date) || "",

            "Thời gian kết thúc": t.end_date || "",

            "Trạng thái\n1: Hoàn thành\n2: Đang thực hiện":
            t.status === 1 ? 1 : 2,

            "Trễ\n0: Không trễ\n1: Trễ": 0,

            "Ghi chú": ""
        }));
    }

    function exportTasksToExcel(tasks) {
        if (!tasks.length) {
            alert("⚠️ Không có dữ liệu để xuất");
            return;
        }

        loadXLSX(() => {
            const rows = mapTasksToExcelRows(tasks);
            const ws = XLSX.utils.json_to_sheet(rows);

            ws["!cols"] = [
                { wch: 6 },
                { wch: 75 },
                { wch: 18 },
                { wch: 24 },
                { wch: 16 },
                { wch: 18 },
                { wch: 26 },
                { wch: 18 },
                { wch: 30 }
            ];

            const wb = XLSX.utils.book_new();
            XLSX.utils.book_append_sheet(wb, ws, "BaoCaoTuan");

            XLSX.writeFile(
                wb,
                `BaoCaoTuan_${new Date().toISOString().slice(0, 10)}.xlsx`
      );
        });
    }

    /*********************************************************
   * GAS
   *********************************************************/
    function sendToGAS(payload) {
        GM_xmlhttpRequest({
            method: "POST",
            url: CONFIG.GAS_WEBAPP_URL,
            headers: { "Content-Type": "application/json" },
            data: JSON.stringify({
                source: "tampermonkey",
                created_at: new Date().toISOString(),
                data: payload
            })
        });
    }

    /*********************************************************
   * UI
   *********************************************************/
    function createReportUI() {
        const box = document.createElement('div');
        box.dataset.collapsed = "false";
        box.style = `
      position:fixed;bottom:20px;right:20px;z-index:99999;
      background:#0f172a;color:#fff;padding:12px;border-radius:10px;
      width:300px;font-size:13px;
      box-shadow:0 10px 25px rgba(0,0,0,.4);
      transition:.3s;
    `;

        box.innerHTML = `
      <div id="collapseBtn"
        style="position:absolute;top:50%;right:-14px;
        transform:translateY(-50%);
        width:28px;height:28px;border-radius:50%;
        background:#0f172a;display:flex;
        align-items:center;justify-content:center;
        cursor:pointer;">◀</div>

      <div id="content">
        <b>📊 Báo cáo tuần</b>

        <div style="margin-top:8px;font-weight:600;">👤 Người thực hiện</div>
        <select id="wr_user" style="width:100%;margin-top:4px;"></select>

        <div style="margin-top:8px;font-weight:600;">📅 Chọn tuần</div>
        <select id="wr_week" style="width:100%;margin-top:4px;"></select>

        <input id="wr_from" type="hidden">
        <input id="wr_to" type="hidden">

        <button id="runNowBtn" style="width:100%;margin-top:8px;">📤 Xuất báo cáo</button>
        <button id="exportExcelBtn" style="width:100%;margin-top:4px;">📥 Xuất Excel</button>
      </div>
    `;

        document.body.appendChild(box);

        // collapse
        const content = box.querySelector('#content');
        const btn = box.querySelector('#collapseBtn');
        btn.onclick = () => {
            const c = box.dataset.collapsed === "true";
            content.style.display = c ? "block" : "none";
            box.style.width = c ? "300px" : "48px";
            btn.innerHTML = c ? "◀" : "▶";
            box.dataset.collapsed = String(!c);
        };

        // users
        const userSelect = $("#wr_user");
        const currentUsid = getCurrentUserUsidFromCookie();

        userSelect.innerHTML =
            `<option value="ALL">📌 Tất cả</option>` +
            USER_IDS.map(u => `<option value="${u.user_id}">${u.display_name}</option>`).join('');

        if (currentUsid && USER_IDS.some(u => String(u.user_id) === String(currentUsid))) {
            userSelect.value = String(currentUsid);
        } else {
            userSelect.value = "ALL";
        }

        // weeks
        const weeks = getWeeksOfCurrentMonth();
        const weekSelect = $("#wr_week");
        weekSelect.innerHTML = weeks.map(w => `<option value="${w.index}">${w.label}</option>`).join('');

        const todayISO = fmtISO(new Date());
        const currentWeek = weeks.find(w => todayISO >= w.fromISO && todayISO <= w.toISO);
        weekSelect.value = currentWeek ? currentWeek.index : 1;

        function applyWeek() {
            const w = weeks.find(x => String(x.index) === String(weekSelect.value));
            if (!w) return;
            $("#wr_from").value = w.fromISO;
            $("#wr_to").value = w.toISO;
        }
        weekSelect.onchange = applyWeek;
        applyWeek();

        // actions
        $("#runNowBtn").onclick = async () => {
            const data = await getWeeklyReportData();
            sendToGAS(data);
        };

        $("#exportExcelBtn").onclick = async () => {
            const data = await getWeeklyReportData();
            exportTasksToExcel(data);
        };

        // when user changes -> check again + reset notify cache
        userSelect.addEventListener("change", () => {
            resetNotifyCacheForUser();
            checkCurrentTasksForSelectedUser();
        });

    }

    /*********************************************************
   * INIT
   *********************************************************/
    setTimeout(() => {
        createReportUI();
        checkCurrentTasksForSelectedUser();
    }, CONFIG.UI_INIT_DELAY_MS);

})();