const STORAGE_KEY = "toyota-kanban-tasks-v1"; const STATUSES = [ { id: "backlog", label: "\u30d0\u30c3\u30af\u30ed\u30b0", hint: "\u69cb\u60f3\u4e2d" }, { id: "ready", label: "\u6e96\u5099", hint: "\u6e96\u5099\u4e2d" }, { id: "inprogress", label: "\u4f5c\u696d", hint: "\u9032\u884c\u4e2d" }, { id: "waiting", label: "\u5f85\u6a5f", hint: "\u505c\u6b62\u4e2d" }, { id: "done", label: "\u5b8c\u4e86", hint: "\u5b8c\u4e86" } ]; const state = { tasks: [], filters: { search: "", searchRaw: "", selectedTags: [], dateFrom: "", dateTo: "", parentId: "", parentSearch: "" }, selectionMode: false, selectedIds: new Set(), showParentView: false, highlightParentId: "", activeStatus: STATUSES[0].id, parentViewSearch: "" }; const dom = {}; let initialized = false; function init() { if (initialized) return; initialized = true; cacheDom(); bindGlobalEvents(); hydrateStaticUi(); loadTasks(); renderAll(); } function cacheDom() { dom.board = document.getElementById("board"); dom.statusDock = document.getElementById("status-dock"); dom.parentView = document.getElementById("parent-view"); dom.parentClusters = document.getElementById("parent-clusters"); dom.selectionToolbar = document.getElementById("selection-toolbar"); dom.selectionCount = document.getElementById("selection-count"); dom.bulkStatusButtons = document.getElementById("bulk-status-buttons"); dom.filterReset = document.getElementById("filter-reset-button"); dom.parentFilter = document.getElementById("parent-filter-select"); dom.parentFilterSearch = document.getElementById("parent-filter-search"); dom.tagToggleContainer = document.getElementById("tag-toggle-container"); dom.parentViewSearch = document.getElementById("parent-view-search"); dom.parentViewClear = document.getElementById("parent-view-clear"); dom.searchInput = document.getElementById("search-input"); dom.dateFrom = document.getElementById("date-from"); dom.dateTo = document.getElementById("date-to"); dom.toggleGroupBtn = document.getElementById("btn-toggle-group"); dom.toggleSelectionBtn = document.getElementById("btn-toggle-selection"); dom.newTaskBtn = document.getElementById("btn-new-task"); dom.bulkDeleteBtn = document.getElementById("btn-bulk-delete"); dom.selectAllBtn = document.getElementById("btn-select-all"); dom.clearSelectionBtn = document.getElementById("btn-clear-selection"); dom.dialog = document.getElementById("task-dialog"); dom.dialogTitle = document.getElementById("dialog-title"); dom.dialogClose = document.getElementById("dialog-close"); dom.taskForm = document.getElementById("task-form"); dom.taskDeleteBtn = document.getElementById("task-delete-button"); dom.taskStatusSelect = document.getElementById("task-status"); dom.taskParentSelect = document.getElementById("task-parent"); dom.taskCardTemplate = document.getElementById("task-card-template"); } function bindGlobalEvents() { dom.newTaskBtn.addEventListener("click", () => openTaskDialog()); dom.dialogClose.addEventListener("click", closeDialog); dom.taskForm.addEventListener("submit", handleTaskSubmit); dom.taskDeleteBtn.addEventListener("click", handleDialogDelete); dom.toggleGroupBtn.addEventListener("click", toggleParentView); dom.toggleSelectionBtn.addEventListener("click", toggleSelectionMode); dom.bulkDeleteBtn.addEventListener("click", handleBulkDelete); dom.selectAllBtn.addEventListener("click", handleSelectAll); dom.clearSelectionBtn.addEventListener("click", () => { state.selectedIds.clear(); updateBulkActions(); renderAll(); }); dom.searchInput.addEventListener("input", (event) => { const raw = event.target.value.trim(); state.filters.searchRaw = raw; state.filters.search = raw.toLowerCase(); renderAll(); }); dom.dateFrom.addEventListener("change", (event) => { state.filters.dateFrom = event.target.value; renderAll(); }); dom.dateTo.addEventListener("change", (event) => { state.filters.dateTo = event.target.value; renderAll(); }); dom.parentFilter.addEventListener("change", (event) => { state.filters.parentId = event.target.value; renderAll(); }); if (dom.filterReset) { dom.filterReset.addEventListener("click", resetFilters); } if (dom.parentFilterSearch) { dom.parentFilterSearch.addEventListener("input", (event) => { state.filters.parentSearch = event.target.value.trim(); renderParentFilterOptions(); }); } if (dom.parentViewSearch) { dom.parentViewSearch.addEventListener("input", (event) => { state.parentViewSearch = event.target.value.trim(); renderParentClusters(); }); } if (dom.parentViewClear) { dom.parentViewClear.addEventListener("click", () => { state.parentViewSearch = ""; state.highlightParentId = ""; state.filters.parentId = ""; if (dom.parentViewSearch) dom.parentViewSearch.value = ""; renderAll(); }); } dom.dialog.addEventListener("close", () => { dom.taskForm.reset(); dom.taskDeleteBtn.hidden = true; dom.taskForm.dataset.editingId = ""; }); } function hydrateStaticUi() { STATUSES.forEach((status) => { const option = document.createElement("option"); option.value = status.id; option.textContent = status.label; dom.taskStatusSelect.appendChild(option); }); renderBulkStatusButtons(); } function loadTasks() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { state.tasks = parsed; return; } } } catch (error) { console.error("Failed to parse storage", error); } state.tasks = createSampleTasks(); saveTasks(); } function saveTasks() { localStorage.setItem(STORAGE_KEY, JSON.stringify(state.tasks)); } function renderAll() { document.body.classList.toggle("selection-mode", state.selectionMode); document.body.classList.toggle("parent-view-mode", state.showParentView); dom.parentView.classList.toggle("active", state.showParentView); dom.toggleGroupBtn.classList.toggle("active", state.showParentView); dom.toggleSelectionBtn.classList.toggle("active", state.selectionMode); if (dom.searchInput) dom.searchInput.value = state.filters.searchRaw; if (dom.dateFrom) dom.dateFrom.value = state.filters.dateFrom; if (dom.dateTo) dom.dateTo.value = state.filters.dateTo; if (dom.parentViewSearch) dom.parentViewSearch.value = state.parentViewSearch; const tagScopedTasks = getFilteredTasks({ includeTagFilter: false }); renderTagToggles(tagScopedTasks); const filteredTasks = getFilteredTasks(); const groupedByStatus = groupByStatus(filteredTasks); renderStatusDock(groupedByStatus); renderBoard(groupedByStatus); renderParentOptions(); renderParentFilterOptions(); renderParentClusters(); updateBulkActions(); } function renderBoard(groupedTasks) { dom.board.innerHTML = ""; const status = STATUSES.find((item) => item.id === state.activeStatus) ?? STATUSES[0]; const tasks = groupedTasks[status.id] ?? []; const column = document.createElement("section"); column.className = "column"; column.dataset.status = status.id; const header = document.createElement("header"); header.className = "column-header"; header.innerHTML = `${status.label}${tasks.length}`; const body = document.createElement("div"); body.className = "column-body"; if (tasks.length === 0) { body.classList.add("empty"); } else { tasks.forEach((task) => body.appendChild(createTaskCard(task))); } column.append(header, body); dom.board.appendChild(column); } function renderParentOptions(currentId = "") { dom.taskParentSelect.innerHTML = ""; const noneOption = document.createElement("option"); noneOption.value = ""; noneOption.textContent = "\u306a\u3057"; dom.taskParentSelect.appendChild(noneOption); state.tasks .filter((task) => task.id !== currentId && !isDescendant(task.id, currentId)) .sort((a, b) => a.title.localeCompare(b.title, "ja")) .forEach((task) => { const option = document.createElement("option"); option.value = task.id; option.textContent = task.title; dom.taskParentSelect.appendChild(option); }); } function renderParentFilterOptions() { const previous = state.filters.parentId || ""; const searchTerm = state.filters.parentSearch.trim().toLowerCase(); dom.parentFilter.innerHTML = ""; const allOption = document.createElement("option"); allOption.value = ""; allOption.textContent = "\u3059\u3079\u3066"; dom.parentFilter.appendChild(allOption); const allParents = state.tasks .filter((task) => !task.parentId) .sort((a, b) => a.title.localeCompare(b.title, "ja")); const filtered = []; allParents.forEach((task) => { if (!searchTerm || task.title.toLowerCase().includes(searchTerm)) { filtered.push(task); } }); if (previous && !filtered.some((task) => task.id === previous)) { const selected = allParents.find((task) => task.id === previous); if (selected) { filtered.unshift(selected); } } filtered.forEach((task) => { const option = document.createElement("option"); option.value = task.id; option.textContent = task.title; dom.parentFilter.appendChild(option); }); const hasSelection = previous && allParents.some((task) => task.id === previous); dom.parentFilter.value = hasSelection ? previous : ""; if (!hasSelection) { state.filters.parentId = ""; } if (dom.parentFilterSearch) { dom.parentFilterSearch.value = state.filters.parentSearch; } } function renderStatusDock(groupedTasks) { dom.statusDock.innerHTML = ""; STATUSES.forEach((status) => { const button = document.createElement("button"); button.type = "button"; button.className = "status-chip"; button.dataset.status = status.id; if (status.id === state.activeStatus) { button.classList.add("active"); } const count = groupedTasks[status.id]?.length ?? 0; button.innerHTML = `
${status.label} ${status.hint}
${count} `; button.addEventListener("click", () => { if (state.activeStatus !== status.id) { state.activeStatus = status.id; renderAll(); } }); dom.statusDock.appendChild(button); }); } function renderBulkStatusButtons() { dom.bulkStatusButtons.innerHTML = ""; STATUSES.forEach((status) => { const button = document.createElement("button"); button.type = "button"; button.className = "btn subtle"; button.textContent = status.label; button.addEventListener("click", () => handleBulkMove(status.id)); dom.bulkStatusButtons.appendChild(button); }); } function renderTagToggles(tasks = []) { const container = dom.tagToggleContainer; if (!container) return; container.innerHTML = ""; const tagSet = new Set(); tasks.forEach((task) => (task.tags ?? []).forEach((tag) => tagSet.add(tag))); const sortedTags = [...tagSet].sort((a, b) => a.localeCompare(b, "ja")); const available = new Set(sortedTags); state.filters.selectedTags = state.filters.selectedTags.filter((tag) => available.has(tag)); if (!sortedTags.length) { const placeholder = document.createElement("span"); placeholder.className = "tag-toggle-empty"; placeholder.textContent = "\u30bf\u30b0\u306f\u672a\u767b\u9332\u3067\u3059"; container.appendChild(placeholder); return; } sortedTags.forEach((tag) => { const button = document.createElement("button"); button.type = "button"; button.className = "tag-toggle"; button.textContent = tag; const isActive = state.filters.selectedTags.includes(tag); button.setAttribute("aria-pressed", isActive ? "true" : "false"); button.classList.toggle("active", isActive); button.addEventListener("click", () => toggleTagFilter(tag)); container.appendChild(button); }); } function toggleTagFilter(tag) { const list = state.filters.selectedTags; const index = list.indexOf(tag); if (index >= 0) { list.splice(index, 1); } else { list.push(tag); } renderAll(); } function createTaskCard(task) { const card = dom.taskCardTemplate.content.firstElementChild.cloneNode(true); card.dataset.id = task.id; card.dataset.status = task.status; if (state.highlightParentId && (state.highlightParentId === task.id || state.highlightParentId === task.parentId)) { card.classList.add("highlighted"); } const checkbox = card.querySelector(".task-select"); checkbox.checked = state.selectedIds.has(task.id); checkbox.style.display = state.selectionMode ? "block" : "none"; checkbox.addEventListener("change", (event) => { if (event.target.checked) { state.selectedIds.add(task.id); } else { state.selectedIds.delete(task.id); } updateBulkActions(); }); card.querySelector(".task-title").textContent = task.title; card.querySelector(".assignee").textContent = task.assignee ? `\u62c5\u5f53: ${task.assignee}` : "\u62c5\u5f53: \u672a\u5b9a"; card.querySelector(".due-date").textContent = task.dueDate ? `\u671f\u9650: ${formatDate(task.dueDate)}` : "\u671f\u9650: \u672a\u8a2d\u5b9a"; card.querySelector(".progress").textContent = task.progress || "\u9032\u6357\u30e1\u30e2\u306a\u3057"; card.querySelector(".notes").textContent = task.notes || "\u5099\u8003\u306a\u3057"; const linkEl = card.querySelector(".resource-link"); if (task.link) { linkEl.href = task.link; linkEl.textContent = task.link; linkEl.style.display = "block"; } else { linkEl.style.display = "none"; } const tagContainer = card.querySelector(".task-tags"); tagContainer.innerHTML = ""; (task.tags || []).forEach((tag) => { const tagEl = document.createElement("span"); tagEl.className = "task-tag"; tagEl.textContent = tag; tagContainer.appendChild(tagEl); }); const childrenContainer = card.querySelector(".task-children"); const children = state.tasks.filter((item) => item.parentId === task.id); if (children.length) { children.forEach((child) => { const row = document.createElement("div"); row.className = "task-child"; const statusLabel = STATUSES.find((status) => status.id === child.status)?.label ?? ""; row.innerHTML = `${child.title}${statusLabel}`; childrenContainer.appendChild(row); }); } else { childrenContainer.style.display = "none"; } const quickButtons = card.querySelector(".status-quick-buttons"); quickButtons.innerHTML = ""; STATUSES.forEach((status) => { if (status.id === task.status) return; const button = document.createElement("button"); button.type = "button"; button.textContent = status.label; button.addEventListener("click", () => { updateTask(task.id, { status: status.id }, { skipRender: true }); renderAll(); }); quickButtons.appendChild(button); }); card.querySelector('[data-action="edit"]').addEventListener("click", () => openTaskDialog(task.id)); card.querySelector('[data-action="delete"]').addEventListener("click", () => { if (confirm("\u3053\u306e\u30bf\u30b9\u30af\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\u5b50\u30bf\u30b9\u30af\u3082\u524a\u9664\u3055\u308c\u307e\u3059\u3002")) { deleteTask(task.id); renderAll(); } }); return card; } function openTaskDialog(taskId = "") { const editing = Boolean(taskId); dom.dialogTitle.textContent = editing ? "\u30bf\u30b9\u30af\u3092\u7de8\u96c6" : "\u30bf\u30b9\u30af\u3092\u8ffd\u52a0"; dom.taskDeleteBtn.hidden = !editing; dom.taskForm.dataset.editingId = editing ? taskId : ""; populateTaskForm(editing ? getTask(taskId) : null); dom.dialog.showModal(); } function closeDialog() { dom.dialog.close(); } function populateTaskForm(task) { dom.taskForm.reset(); renderParentOptions(task?.id ?? ""); dom.taskForm.elements.title.value = task?.title ?? ""; dom.taskForm.elements.assignee.value = task?.assignee ?? ""; dom.taskForm.elements.dueDate.value = task?.dueDate ?? ""; dom.taskForm.elements.link.value = task?.link ?? ""; dom.taskForm.elements.tags.value = (task?.tags ?? []).join(", "); dom.taskForm.elements.progress.value = task?.progress ?? ""; dom.taskForm.elements.notes.value = task?.notes ?? ""; dom.taskForm.elements.parentId.value = task?.parentId ?? ""; dom.taskForm.elements.status.value = task?.status ?? STATUSES[0].id; } function handleTaskSubmit(event) { event.preventDefault(); const formData = new FormData(dom.taskForm); const payload = { title: formData.get("title").trim(), assignee: formData.get("assignee").trim(), dueDate: formData.get("dueDate") || "", link: formData.get("link").trim(), tags: formData .get("tags") .split(",") .map((tag) => tag.trim()) .filter(Boolean), progress: formData.get("progress").trim(), notes: formData.get("notes").trim(), parentId: formData.get("parentId") || "", status: formData.get("status") }; const editingId = dom.taskForm.dataset.editingId; if (editingId) { updateTask(editingId, payload, { skipRender: true, skipPersist: true }); saveTasks(); } else { const newTask = { id: generateId(), createdAt: Date.now(), updatedAt: Date.now(), ...payload }; state.tasks.push(newTask); saveTasks(); } renderAll(); closeDialog(); } function handleDialogDelete() { const editingId = dom.taskForm.dataset.editingId; if (!editingId) return; if (!confirm("\u3053\u306e\u30bf\u30b9\u30af\u3092\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\u5b50\u30bf\u30b9\u30af\u3082\u524a\u9664\u3055\u308c\u307e\u3059\u3002")) return; deleteTask(editingId); renderAll(); closeDialog(); } function resetFilters() { state.filters.search = ""; state.filters.searchRaw = ""; state.filters.selectedTags = []; state.filters.dateFrom = ""; state.filters.dateTo = ""; state.filters.parentId = ""; state.filters.parentSearch = ""; state.parentViewSearch = ""; state.parentViewSearch = ""; state.highlightParentId = ""; state.selectedIds.clear(); state.selectionMode = false; state.showParentView = false; state.activeStatus = STATUSES[0].id; if (dom.searchInput) dom.searchInput.value = ""; if (dom.dateFrom) dom.dateFrom.value = ""; if (dom.dateTo) dom.dateTo.value = ""; if (dom.parentFilterSearch) dom.parentFilterSearch.value = ""; if (dom.parentViewSearch) dom.parentViewSearch.value = ""; if (dom.parentViewClear) dom.parentViewClear.disabled = true; renderAll(); } function handleBulkDelete() { if (!state.selectedIds.size) return; if (!confirm("\u9078\u629e\u3057\u305f\u30bf\u30b9\u30af\u3092\u307e\u3068\u3081\u3066\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\u5b50\u30bf\u30b9\u30af\u3082\u524a\u9664\u3055\u308c\u307e\u3059\u3002")) { return; } [...state.selectedIds].forEach((taskId) => deleteTask(taskId)); state.selectedIds.clear(); updateBulkActions(); renderAll(); } function handleBulkMove(statusId) { if (!state.selectedIds.size) return; state.selectedIds.forEach((taskId) => updateTask(taskId, { status: statusId }, { skipRender: true, skipPersist: true })); saveTasks(); renderAll(); } function handleSelectAll() { const filtered = getFilteredTasks(); filtered.forEach((task) => state.selectedIds.add(task.id)); updateBulkActions(); renderAll(); } function focusParentTasks(parentId) { const parentTask = getTask(parentId); state.filters.search = ""; state.filters.searchRaw = ""; state.filters.selectedTags = []; state.filters.dateFrom = ""; state.filters.dateTo = ""; state.filters.parentId = parentId; state.highlightParentId = parentId; state.showParentView = false; state.selectionMode = false; state.selectedIds.clear(); state.filters.parentSearch = ""; state.parentViewSearch = ""; if (parentTask) { state.activeStatus = parentTask.status; } renderAll(); } function toggleParentView() { state.showParentView = !state.showParentView; if (state.showParentView) { state.selectionMode = false; state.selectedIds.clear(); state.parentViewSearch = ""; } else { state.highlightParentId = ""; } renderAll(); } function toggleSelectionMode() { state.selectionMode = !state.selectionMode; if (!state.selectionMode) { state.selectedIds.clear(); } renderAll(); } function renderParentClusters() { dom.parentClusters.innerHTML = ""; if (!state.showParentView) return; if (dom.parentViewSearch) { dom.parentViewSearch.value = state.parentViewSearch; } if (dom.parentViewClear) { dom.parentViewClear.disabled = !state.highlightParentId; } const searchTerm = state.parentViewSearch.trim().toLowerCase(); const parents = state.tasks .filter((task) => !task.parentId) .filter((parent) => { const matchesParent = matchesTaskFilters(parent); const childMatches = state.tasks.some((child) => child.parentId === parent.id && matchesTaskFilters(child)); if (!(matchesParent || childMatches)) { return false; } if (!searchTerm) { return true; } const parentHaystack = [ parent.title, parent.assignee, parent.progress, parent.notes, parent.link, ...(parent.tags ?? []) ] .filter(Boolean) .join(" ") .toLowerCase(); if (parentHaystack.includes(searchTerm)) { return true; } return state.tasks.some((child) => { if (child.parentId !== parent.id) return false; const childHaystack = [ child.title, child.assignee, child.progress, child.notes, child.link, ...(child.tags ?? []) ] .filter(Boolean) .join(" ") .toLowerCase(); return childHaystack.includes(searchTerm); }); }) .sort((a, b) => a.title.localeCompare(b.title, "ja")); if (!parents.length) { const empty = document.createElement("div"); empty.className = "parent-empty"; empty.textContent = "条件に一致する親タスクがありません。"; dom.parentClusters.appendChild(empty); return; } parents.forEach((parent) => { const children = state.tasks.filter((task) => task.parentId === parent.id); const isHighlighted = state.highlightParentId === parent.id; let expanded = isHighlighted; const cluster = document.createElement("div"); cluster.className = "parent-cluster"; if (isHighlighted) { cluster.classList.add("highlighted"); cluster.classList.add("expanded"); } const header = document.createElement("div"); header.className = "cluster-header"; const titleBox = document.createElement("div"); titleBox.className = "cluster-title"; const title = document.createElement("h4"); title.textContent = parent.title; const meta = document.createElement("div"); meta.className = "cluster-meta"; const metaParts = []; if (parent.assignee) metaParts.push(`担当: ${parent.assignee}`); if (parent.dueDate) metaParts.push(`期限: ${formatDate(parent.dueDate)}`); metaParts.push(`子タスク ${children.length} 件`); meta.textContent = metaParts.join(" / "); titleBox.append(title, meta); const statusStrip = document.createElement("div"); statusStrip.className = "cluster-status"; STATUSES.forEach((status) => { const count = children.filter((child) => child.status === status.id).length; const pill = document.createElement("span"); pill.className = "status-pill"; pill.textContent = `${status.label} ${count}`; statusStrip.appendChild(pill); }); const actions = document.createElement("div"); actions.className = "cluster-actions"; const toggleButton = document.createElement("button"); toggleButton.type = "button"; toggleButton.className = "btn tiny ghost"; toggleButton.textContent = expanded ? "折りたたむ" : "展開"; const focusButton = document.createElement("button"); focusButton.type = "button"; focusButton.className = "btn tiny subtle"; focusButton.textContent = "ボードで表示"; actions.append(toggleButton, focusButton); header.append(titleBox, statusStrip, actions); const body = document.createElement("div"); body.className = "cluster-body"; body.hidden = !expanded; if (children.length) { children .sort((a, b) => a.title.localeCompare(b.title, "ja")) .forEach((child) => { const row = document.createElement("div"); row.className = "cluster-child-row"; const statusLabel = STATUSES.find((status) => status.id === child.status)?.label ?? ""; row.innerHTML = `${child.title}${statusLabel}`; row.addEventListener("click", (event) => { event.stopPropagation(); focusParentTasks(parent.id); }); body.appendChild(row); }); } else { const emptyChild = document.createElement("div"); emptyChild.className = "cluster-child-empty"; emptyChild.textContent = "子タスクはまだありません。"; body.appendChild(emptyChild); } toggleButton.addEventListener("click", (event) => { event.stopPropagation(); expanded = !expanded; body.hidden = !expanded; cluster.classList.toggle("expanded", expanded); toggleButton.textContent = expanded ? "折りたたむ" : "展開"; }); focusButton.addEventListener("click", (event) => { event.stopPropagation(); focusParentTasks(parent.id); }); header.addEventListener("click", (event) => { if (event.target.closest("button")) return; focusParentTasks(parent.id); }); cluster.append(header, body); dom.parentClusters.appendChild(cluster); }); } function matchesTaskFilters(task, options = {}) { const { includeTags = true } = options; const search = state.filters.search; const selectedTags = state.filters.selectedTags; const from = state.filters.dateFrom; const to = state.filters.dateTo; if (search) { const haystack = [ task.title, task.assignee, task.progress, task.notes, task.link, ...(task.tags ?? []) ] .filter(Boolean) .join(" ") .toLowerCase(); if (!haystack.includes(search)) { return false; } } if (includeTags && selectedTags.length) { const lowerTags = (task.tags ?? []).map((tag) => tag.toLowerCase()); const matchesAll = selectedTags.every((tag) => lowerTags.includes(tag.toLowerCase())); if (!matchesAll) return false; } if (from && task.dueDate && task.dueDate < from) return false; if (to && task.dueDate && task.dueDate > to) return false; return true; } function getFilteredTasks(options = {}) { const { includeTagFilter = true } = options; const parentId = state.filters.parentId; const highlight = state.highlightParentId; return state.tasks.filter((task) => { if (!matchesTaskFilters(task, { includeTags: includeTagFilter })) { return false; } if (parentId && !(task.id === parentId || task.parentId === parentId)) { return false; } if (highlight && !(task.id === highlight || task.parentId === highlight)) { return false; } return true; }); } function groupByStatus(tasks) { return tasks.reduce((acc, task) => { if (!acc[task.status]) acc[task.status] = []; acc[task.status].push(task); return acc; }, {}); } function updateTask(taskId, patch, options = {}) { const task = getTask(taskId); if (!task) return; const { skipRender = false, skipPersist = false } = options; Object.assign(task, patch, { updatedAt: Date.now() }); if (!skipPersist) { saveTasks(); } if (!skipRender) { renderAll(); } } function deleteTask(taskId) { const childIds = state.tasks.filter((task) => task.parentId === taskId).map((task) => task.id); state.tasks = state.tasks.filter((task) => task.id !== taskId); childIds.forEach((childId) => deleteTask(childId)); state.selectedIds.delete(taskId); saveTasks(); } function getTask(taskId) { return state.tasks.find((task) => task.id === taskId); } function isDescendant(candidateId, targetId) { if (!candidateId || !targetId) return false; let cursor = state.tasks.find((task) => task.id === candidateId)?.parentId; while (cursor) { if (cursor === targetId) return true; cursor = state.tasks.find((task) => task.id === cursor)?.parentId; } return false; } function updateBulkActions() { const visible = state.selectionMode; const count = state.selectedIds.size; if (dom.selectionToolbar) { dom.selectionToolbar.classList.toggle("active", visible); } if (dom.selectionCount) { dom.selectionCount.textContent = `${count}\u4EF6\u9078\u629E\u4E2D`; } dom.bulkDeleteBtn.disabled = count === 0; dom.clearSelectionBtn.disabled = count === 0; dom.bulkStatusButtons.querySelectorAll("button").forEach((button) => { button.disabled = count === 0; }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init, { once: true }); } else { init(); } function formatDate(isoDate) { if (!isoDate) return ""; const [year, month, day] = isoDate.split("-"); return `${year}/${month}/${day}`; } function generateId() { if (crypto?.randomUUID) return crypto.randomUUID(); return `task-${Date.now()}-${Math.random().toString(16).slice(2)}`; } function createSampleTasks() { const now = new Date(); const iso = (offset) => { const base = new Date(now); base.setDate(base.getDate() + offset); return base.toISOString().slice(0, 10); }; const projectId = generateId(); return [ { id: projectId, title: "\u30e9\u30a4\u30f3\u6539\u5584\u306e\u5168\u4f53\u8a2d\u8a08", assignee: "\u7530\u4e2d", dueDate: iso(7), link: "https://www.toyota.co.jp/", tags: ["\u5de5\u7a0b\u7ba1\u7406", "\u512a\u5148"], progress: "\u73fe\u72b6\u5206\u6790\u3092\u5b8c\u4e86\u3002\u6539\u5584\u6848\u306e\u521d\u7a3f\u3092\u4f5c\u6210\u6e08\u307f\u3002", notes: "\u30b3\u30b9\u30c8\u8a66\u7b97\u3092\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u3042\u308a\u3002", parentId: "", status: "ready", createdAt: Date.now(), updatedAt: Date.now() }, { id: generateId(), title: "\u6f5c\u5728\u30dc\u30c8\u30eb\u30cd\u30c3\u30af\u306e\u8abf\u67fb", assignee: "\u4f50\u85e4", dueDate: iso(3), link: "", tags: ["\u5de5\u7a0b\u7ba1\u7406", "\u8abf\u67fb"], progress: "\u30e9\u30a4\u30f3\u505c\u6b62\u30ed\u30b0\u304b\u30895\u4ef6\u306e\u5019\u88dc\u3092\u62bd\u51fa\u3002", notes: "\u73fe\u5834\u30d2\u30a2\u30ea\u30f3\u30b0\u3092\u4e88\u5b9a\u3002", parentId: projectId, status: "inprogress", createdAt: Date.now(), updatedAt: Date.now() }, { id: generateId(), title: "\u5b89\u5168\u6559\u80b2\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u898b\u76f4\u3057", assignee: "\u4e2d\u6751", dueDate: iso(12), link: "file:///\\\\server\\docs\\safety", tags: ["\u6559\u80b2", "\u5b89\u5168"], progress: "\u65e2\u5b58\u6559\u6750\u306e\u68da\u5378\u3057\u5b8c\u4e86\u3002", notes: "\u65b0\u5165\u793e\u54e1\u5411\u3051\u306e\u8ffd\u52a0\u30d1\u30fc\u30c8\u304c\u5fc5\u8981\u3002", parentId: "", status: "backlog", createdAt: Date.now(), updatedAt: Date.now() }, { id: generateId(), title: "AI\u5224\u5b9a\u30e2\u30c7\u30eb\u306e\u7cbe\u5ea6\u691c\u8a3c", assignee: "\u9234\u6728", dueDate: iso(-2), link: "", tags: ["AI", "\u54c1\u8cea"], progress: "\u30c6\u30b9\u30c8\u30c7\u30fc\u30bf\u306795%\u7cbe\u5ea6\u3002", notes: "\u73fe\u5834\u5c0e\u5165\u524d\u306b\u6700\u7d42\u78ba\u8a8d\u304c\u5fc5\u9808\u3002", parentId: "", status: "waiting", createdAt: Date.now(), updatedAt: Date.now() }, { id: generateId(), title: "\u6539\u5584\u6848\u30ec\u30d3\u30e5\u30fc\u4f1a\u8b70", assignee: "\u7530\u4e2d", dueDate: iso(1), link: "https://teams.microsoft.com/", tags: ["\u4f1a\u8b70", "\u512a\u5148"], progress: "\u30a2\u30b8\u30a7\u30f3\u30c0\u4f5c\u6210\u6e08\u307f\u3002", notes: "\u53c2\u52a0\u8005\u3078\u306e\u8cc7\u6599\u914d\u5e03\u3092\u5b8c\u4e86\u3002", parentId: projectId, status: "ready", createdAt: Date.now(), updatedAt: Date.now() } ]; }