const XSS = { escapeHtml: function (str) { if (str === null || str === undefined) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }, setTextSafe: function (element, text) { if (element) element.textContent = text; }, sanitizeUrl: function (url) { if (!url) return ''; const cleaned = String(url).replace(/[\x00-\x1F\x7F]/g, '').trim(); try { const parsed = new URL(cleaned, window.location.origin); if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.href; } catch (e) { if (cleaned.startsWith('/') && !cleaned.startsWith('//')) return cleaned; } return ''; } }; window.XSS = XSS; var SUBJECT_PALETTE = [ { bg: '#4361ee', text: '#fff' }, { bg: '#7b2d8b', text: '#fff' }, { bg: '#2d9b4e', text: '#fff' }, { bg: '#c87800', text: '#fff' }, { bg: '#0077b6', text: '#fff' }, { bg: '#c1121f', text: '#fff' }, { bg: '#457b9d', text: '#fff' }, { bg: '#588157', text: '#fff' }, { bg: '#6d4c41', text: '#fff' }, { bg: '#6a4c93', text: '#fff' }, ]; function subjectColorFor(subject) { if (!subject) return null; var hash = 0; for (var i = 0; i < subject.length; i++) { hash = (hash * 31 + subject.charCodeAt(i)) | 0; } return SUBJECT_PALETTE[Math.abs(hash) % SUBJECT_PALETTE.length]; } function applySubjectColors() { document.querySelectorAll('.subject-badge[data-subject]').forEach(function (badge) { var color = subjectColorFor(badge.dataset.subject); if (color) { badge.style.backgroundColor = color.bg; badge.style.color = color.text; } }); } window.subjectColorFor = subjectColorFor; let _pendingConfirmForm = null; function showConfirmModal(message, onOk) { const bodyEl = document.getElementById('confirmModalBody'); const okBtn = document.getElementById('confirmModalOk'); if (!bodyEl || !okBtn) { if (onOk) onOk(); return; } bodyEl.textContent = message; const handler = function () { okBtn.removeEventListener('click', handler); bootstrap.Modal.getInstance(document.getElementById('confirmModal')).hide(); if (onOk) onOk(); }; okBtn.addEventListener('click', handler); new bootstrap.Modal(document.getElementById('confirmModal')).show(); } window.showConfirmModal = showConfirmModal; function setupFormSubmitOnce(form) { form.addEventListener('submit', function () { const btn = form.querySelector('[type=submit]'); if (!btn || btn.disabled) return; btn.disabled = true; const orig = btn.innerHTML; btn.innerHTML = '処理中...'; window.addEventListener('pageshow', function () { btn.disabled = false; btn.innerHTML = orig; }, { once: true }); }); } function showCopyFeedback(message) { let el = document.getElementById('globalCopyFeedback'); if (!el) { el = document.createElement('div'); el.id = 'globalCopyFeedback'; el.className = 'copy-feedback alert alert-success shadow-sm py-2 px-3'; document.body.appendChild(el); } el.textContent = message; el.classList.add('show'); clearTimeout(el._timeout); el._timeout = setTimeout(function () { el.classList.remove('show'); }, 2000); } window.showCopyFeedback = showCopyFeedback; document.addEventListener('DOMContentLoaded', function () { const alerts = document.querySelectorAll('.alert:not(.alert-danger):not(.modal .alert)'); alerts.forEach(function (alert) { setTimeout(function () { alert.classList.add('fade'); setTimeout(function () { alert.remove(); }, 150); }, 5000); }); document.querySelectorAll('form[data-confirm]').forEach(function (form) { form.addEventListener('submit', function (e) { e.preventDefault(); const msg = form.dataset.confirm; showConfirmModal(msg, function () { form.submit(); }); }); }); document.querySelectorAll('form:not([data-confirm])').forEach(setupFormSubmitOnce); applySubjectColors(); const dueDateInput = document.getElementById('due_date'); if (dueDateInput && !dueDateInput.value) { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(23, 59, 0, 0); dueDateInput.value = tomorrow.toISOString().slice(0, 16); } initAssignmentIndex(); }); function initAssignmentIndex() { if (!document.getElementById('tableView')) return; var _countdownInterval = null; var _view = localStorage.getItem('viewMode') || 'table'; var _grouped = localStorage.getItem('grouped') === 'true'; var _kbFocusIndex = -1; function getRows() { return Array.from(document.querySelectorAll('#tableView .assignment-row')); } function updateCountdowns() { var now = new Date(); var hasUnder24h = false; getRows().forEach(function (row) { if (row.dataset.completed === 'true') return; var dueTs = row.dataset.dueTs; if (!dueTs) return; var due = new Date(parseInt(dueTs) * 1000); if (isNaN(due.getTime())) return; var diff = due - now; var countdownEl = row.querySelector('.countdown'); row.classList.remove('anxiety-danger', 'anxiety-warning', 'bg-danger-subtle'); if (countdownEl) countdownEl.className = 'countdown small fw-bold font-monospace'; if (diff < 0) { if (countdownEl) { countdownEl.textContent = '期限切れ'; countdownEl.classList.add('text-danger'); } row.classList.add('bg-danger-subtle'); return; } var days = Math.floor(diff / 86400000); var hours = Math.floor((diff % 86400000) / 3600000); var minutes = Math.floor((diff % 3600000) / 60000); var seconds = Math.floor((diff % 60000) / 1000); var remainingHours = days * 24 + hours; var text = (days > 0 ? days + '日 ' : '') + String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); if (countdownEl) countdownEl.textContent = text; if (remainingHours < 24) { hasUnder24h = true; row.classList.add('anxiety-danger'); if (countdownEl) { countdownEl.classList.add('text-danger', 'countdown-urgent'); countdownEl.innerHTML = '' + text; } } else if (days < 7) { row.classList.add('anxiety-warning'); if (countdownEl) { countdownEl.classList.add('text-dark'); countdownEl.innerHTML = '' + text; } } else { if (countdownEl) countdownEl.classList.add('text-secondary'); } }); if (_countdownInterval !== null) return; var interval = hasUnder24h ? 1000 : 60000; _countdownInterval = setTimeout(function () { _countdownInterval = null; updateCountdowns(); }, interval); } function toggleCountdown() { var cols = document.querySelectorAll('.countdown-col'); var btn = document.getElementById('toggleCountdownBtn'); var btnText = document.getElementById('countdownBtnText'); var isHidden = cols[0] && cols[0].style.display === 'none'; cols.forEach(function (col) { col.style.display = isHidden ? '' : 'none'; }); var nowHidden = !isHidden; btnText.textContent = nowHidden ? '残り表示' : '残り非表示'; btn.setAttribute('aria-pressed', nowHidden ? 'true' : 'false'); localStorage.setItem('countdownHidden', nowHidden); } window.toggleCountdown = toggleCountdown; function setView(mode) { _view = mode; localStorage.setItem('viewMode', mode); var tableView = document.getElementById('tableView'); var kanbanView = document.getElementById('kanbanView'); var tableBtn = document.getElementById('viewTableBtn'); var kanbanBtn = document.getElementById('viewKanbanBtn'); var groupBtn = document.getElementById('groupToggleBtn'); if (mode === 'kanban') { tableView.classList.add('d-none'); kanbanView.classList.remove('d-none'); tableBtn.classList.remove('active', 'btn-secondary'); tableBtn.classList.add('btn-outline-secondary'); kanbanBtn.classList.remove('btn-outline-secondary'); kanbanBtn.classList.add('active', 'btn-secondary'); groupBtn.classList.add('d-none'); buildKanban(); } else { kanbanView.classList.add('d-none'); tableView.classList.remove('d-none'); kanbanBtn.classList.remove('active', 'btn-secondary'); kanbanBtn.classList.add('btn-outline-secondary'); tableBtn.classList.remove('btn-outline-secondary'); tableBtn.classList.add('active', 'btn-secondary'); groupBtn.classList.remove('d-none'); if (_grouped) applyGrouping(); } } window.setView = setView; function buildKanban() { var cols = { overdue: [], today: [], week: [], later: [] }; var now = new Date(); var startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate()); var endOfDay = new Date(startOfDay.getTime() + 86400000); var endOfWeek = new Date(startOfDay.getTime() + 7 * 86400000); getRows().forEach(function (row) { if (row.dataset.completed === 'true') return; var ts = parseInt(row.dataset.dueTs) * 1000; var due = new Date(ts); var bucket = due < now ? 'overdue' : due < endOfDay ? 'today' : due < endOfWeek ? 'week' : 'later'; cols[bucket].push({ id: row.dataset.id, title: row.dataset.title, subject: row.dataset.subject, priority: row.dataset.priority, pinned: row.dataset.pinned === 'true', dueTs: ts }); }); var priorityColor = { high: 'danger', medium: 'warning', low: 'secondary' }; var priorityLabel = { high: '高', medium: '中', low: '低' }; var priorityText = { high: 'white', medium: 'dark', low: 'white' }; function renderCol(key, colId, countId) { var el = document.getElementById(colId); var cntEl = document.getElementById(countId); el.innerHTML = ''; cntEl.textContent = cols[key].length; if (!cols[key].length) { el.innerHTML = '
なし
'; return; } var csrf = (document.getElementById('_csrf_global') || {}).value || ''; cols[key].forEach(function (a) { var due = new Date(a.dueTs); var dateStr = due.getFullYear() + '/' + String(due.getMonth() + 1).padStart(2, '0') + '/' + String(due.getDate()).padStart(2, '0') + ' ' + String(due.getHours()).padStart(2, '0') + ':' + String(due.getMinutes()).padStart(2, '0'); var toggleForm = document.querySelector('form[data-row-id="' + a.id + '"]'); var toggleAction = toggleForm ? XSS.sanitizeUrl(toggleForm.action) : '#'; var pc = priorityColor[a.priority] || 'secondary'; var pl = priorityLabel[a.priority] || a.priority; var pt = priorityText[a.priority] || 'white'; var card = document.createElement('div'); card.className = 'kanban-card' + (a.pinned ? ' row-pinned' : ''); card.innerHTML = '