Microsoft To-Do Weekly Planner

To-Do widget that generates report of "Planned" tasks sorted by date, then by list, then by title

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name          Microsoft To-Do Weekly Planner
// @namespace     http://tampermonkey.net/
// @version       2026-01-16.20
// @description   To-Do widget that generates report of "Planned" tasks sorted by date, then by list, then by title
// @author        Donnie Hires
// @match         *://to-do.office.com/*
// @license       MIT
// @grant         none
// ==/UserScript==

(function() {
    'use strict';

    // Inject CSS for readability and specialized print formatting
    const style = document.createElement('style');
    style.innerHTML = `
        #roadmap-day-input::placeholder {
            color: #bbbbbb !important;
            opacity: 1 !important;
        }
        #roadmap-day-input::-webkit-input-placeholder { color: #bbbbbb !important; }
        #roadmap-day-input::-moz-placeholder { color: #bbbbbb !important; opacity: 1 !important; }

        @media print {
            body > *:not(#course-roadmap-overlay) {
                display: none !important;
            }
            #course-roadmap-overlay {
                position: absolute !important;
                top: 0 !important;
                left: 0 !important;
                width: 100% !important;
                height: auto !important;
                border: none !important;
                box-shadow: none !important;
                padding: 0 !important;
                margin: 0 !important;
                overflow: visible !important;
            }
            #close-report-btn, #print-report-btn {
                display: none !important;
            }
            h2 {
                -webkit-print-color-adjust: exact !important;
                print-color-adjust: exact !important;
            }
        }
    `;
    document.head.appendChild(style);

    function createButton() {
        const isPlannedTab = window.location.href.toLowerCase().includes('planned');
        let container = document.getElementById('roadmap-container');

        if (!isPlannedTab) {
            if (container) container.style.display = 'none';
            return;
        }

        if (container) {
            container.style.display = 'flex';
            return;
        }

        container = document.createElement('div');
        container.id = 'roadmap-container';
        container.style = `
            position: fixed; bottom: 20px; left: 20px; z-index: 9999999;
            display: flex; flex-direction: column; gap: 10px; background: #ffffff;
            padding: 16px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            border: 2px solid #0078d4; width: 200px; font-family: 'Segoe UI', sans-serif;
            cursor: move; user-select: none; align-items: center;
        `;

        const title = document.createElement('div');
        title.style = "font-size: 14px; font-weight: 800; color: #0078d4; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 5px;";
        title.innerText = "Planning Widget";

        const dayInput = document.createElement('input');
        dayInput.type = 'text';
        dayInput.placeholder = '# of days';
        dayInput.id = 'roadmap-day-input';
        dayInput.style = `
            width: 100%; height: 40px; border: 2px solid #0078d4; border-radius: 6px;
            text-align: center; font-size: 14px; font-weight: bold; outline: none;
            cursor: text; box-sizing: border-box; background: white; color: black;
        `;
        dayInput.onmousedown = (e) => e.stopPropagation();

        const planBtn = document.createElement('button');
        planBtn.style = `
            width: 100%; height: 45px; background: #0078d4; color: white; border: none;
            border-radius: 6px; font-weight: 700; cursor: pointer; font-size: 14px;
            display: flex; align-items: center; justify-content: center; gap: 8px;
        `;
        planBtn.innerHTML = 'Plan My Week 📅';

        planBtn.onclick = () => {
            const inputVal = document.getElementById('roadmap-day-input').value;
            const parsedDays = parseInt(inputVal);
            const now = new Date();
            now.setHours(0,0,0,0);

            if (isNaN(parsedDays)) {
                // FALLBACK: Until Saturday
                const dayOfWeek = now.getDay();
                let diff = 6 - dayOfWeek;
                if (diff < 0) diff = 6;

                // Calculate the Sunday before that Saturday
                const satDate = new Date(now);
                satDate.setDate(now.getDate() + diff);
                const sunDate = new Date(satDate);
                sunDate.setDate(satDate.getDate() - 6);

                const titleStr = `Week of ${sunDate.toLocaleDateString()} Report`;
                generateReport(diff, titleStr);
            } else {
                // CUSTOM: Range from today to last day
                const endDate = new Date(now);
                endDate.setDate(now.getDate() + parsedDays);
                const titleStr = `Custom Report: ${now.toLocaleDateString()} - ${endDate.toLocaleDateString()}`;
                generateReport(parsedDays, titleStr);
            }
        };

        container.appendChild(title);
        container.appendChild(dayInput);
        container.appendChild(planBtn);
        document.body.appendChild(container);

        let active = false, initialX, initialY;
        container.onmousedown = (e) => {
            if (e.target === container || e.target === title) {
                active = true;
                initialX = e.clientX - container.offsetLeft;
                initialY = e.clientY - container.offsetTop;
            }
        };
        document.onmousemove = (e) => {
            if (active) {
                e.preventDefault();
                container.style.bottom = 'auto';
                container.style.left = (e.clientX - initialX) + "px";
                container.style.top = (e.clientY - initialY) + "px";
            }
        };
        document.onmouseup = () => { active = false; };
    }

    function parseDate(dateStr) {
        const now = new Date();
        now.setHours(0, 0, 0, 0);
        const lower = dateStr.toLowerCase();
        if (lower.includes('today')) return new Date(now);
        if (lower.includes('tomorrow')) {
            const d = new Date(now); d.setDate(now.getDate() + 1); return d;
        }
        const clean = dateStr.replace(/^later,\s+/i, '');
        const parsed = Date.parse(clean + ", " + now.getFullYear());
        if (isNaN(parsed)) return null;
        const d = new Date(parsed); d.setHours(0, 0, 0, 0); return d;
    }

    function generateReport(limitDays, titleText) {
        const tasks = document.querySelectorAll('.taskItem');
        let reportData = {};
        const now = new Date();
        now.setHours(0, 0, 0, 0);
        const endDate = new Date(now);
        endDate.setDate(now.getDate() + limitDays);
        endDate.setHours(23, 59, 59, 999);

        tasks.forEach(task => {
            const title = task.querySelector('.taskItem-title')?.innerText || "Unknown Task";
            const listEl = task.querySelector('.taskItemInfo-title');
            let category = listEl ? listEl.innerText.replace(/^in\s+/i, '').replace(/^\(GT\)\s+/i, '').trim() : "General";
            const dateEl = task.querySelector('.taskItemInfo-date');
            let dateStr = dateEl ? dateEl.innerText.trim() : "";

            if (dateStr) {
                const dateObj = parseDate(dateStr);
                if (dateObj && dateObj >= now && dateObj <= endDate) {
                    const fullDateKey = dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });
                    if (!reportData[fullDateKey]) reportData[fullDateKey] = [];
                    reportData[fullDateKey].push({ title, category });
                }
            }
        });
        renderOverlay(reportData, titleText);
    }

    function renderOverlay(data, titleText) {
        const existing = document.getElementById('course-roadmap-overlay');
        if (existing) existing.remove();
        const overlay = document.createElement('div');
        overlay.id = 'course-roadmap-overlay';
        overlay.style = "position:fixed; top:30px; left:30px; right:30px; bottom:30px; background:white; z-index:99999999; padding:40px; border-radius:15px; border: 3px solid #0078d4; overflow-y:auto; color: black; font-family: 'Segoe UI', sans-serif; box-shadow: 0 15px 50px rgba(0,0,0,0.5);";

        const sortedDates = Object.keys(data).sort((a, b) => new Date(a) - new Date(b));
        let content = `
            <div style="float:right; display:flex; gap:12px;">
                <button id="print-report-btn" style="height:40px; width:120px; background:#0078d4; color:white; border:none; border-radius:6px; cursor:pointer; font-weight:bold; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 8px;">Print 🖨️</button>
                <button id="close-report-btn" style="height:40px; width:120px; background:#f3f2f1; border:1px solid #ccc; border-radius:6px; cursor:pointer; font-weight:bold; font-size: 14px; display: flex; align-items: center; justify-content: center; gap: 8px;">Close ❌</button>
            </div>
            <h1 style="color:#0078d4; margin-top:0; font-size: 28px; border-bottom: 2px solid #eee; padding-bottom: 10px;">${titleText}</h1>`;

        sortedDates.forEach(dateKey => {
            content += `<div style="margin-top:30px;">
                        <h2 style="background:#0078d4; color:white; padding:12px; border-radius:6px; font-size:1.3em;">${dateKey}</h2>
                        <table style="width:100%; border-collapse: collapse; font-size: 16px;">`;

            const dayTasks = data[dateKey].sort((a, b) => {
                const catCompare = a.category.localeCompare(b.category);
                return catCompare !== 0 ? catCompare : a.title.localeCompare(b.title);
            });

            dayTasks.forEach(t => {
                content += `<tr style="border-bottom: 1px solid #eee;">
                            <td style="padding:12px; width:30px;"><input type="checkbox" style="width: 18px; height: 18px;"></td>
                            <td style="padding:12px; font-weight:bold; color:#0078d4; width:150px;">[${t.category}]</td>
                            <td style="padding:12px;">${t.title}</td>
                            </tr>`;
            });
            content += `</table></div>`;
        });

        overlay.innerHTML = content;
        document.body.appendChild(overlay);
        document.getElementById('close-report-btn').onclick = () => overlay.remove();
        document.getElementById('print-report-btn').onclick = () => window.print();
    }

    setInterval(createButton, 2000);
})();