transfer_data / kanban /script.js
Okoge-keys's picture
Upload 3 files
bebebd9 verified
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 = `<span>${status.label}</span><span class="column-count">${tasks.length}</span>`;
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 = `
<div class="status-chip-text">
<strong>${status.label}</strong>
<span>${status.hint}</span>
</div>
<span class="status-chip-count">${count}</span>
`;
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 = `<span>${child.title}</span><span>${statusLabel}</span>`;
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 = `<span class="child-title">${child.title}</span><span class="child-status">${statusLabel}</span>`;
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()
}
];
}