Upload 3 files
Browse files- kanban_v2/index.html +9 -6
- kanban_v2/script.js +168 -25
kanban_v2/index.html
CHANGED
|
@@ -16,12 +16,15 @@
|
|
| 16 |
<p class="subtitle">現場の視点で磨き上げたタスクフロー</p>
|
| 17 |
</div>
|
| 18 |
</div>
|
| 19 |
-
<div class="primary-controls">
|
| 20 |
-
<button class="btn primary" id="btn-new-task">新規タスク</button>
|
| 21 |
-
<button class="btn ghost" id="btn-toggle-group">親子ビュー</button>
|
| 22 |
-
<button class="btn ghost" id="btn-toggle-selection">一括操作</button>
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
<section class="filters">
|
| 27 |
<div class="search-box">
|
|
|
|
| 16 |
<p class="subtitle">現場の視点で磨き上げたタスクフロー</p>
|
| 17 |
</div>
|
| 18 |
</div>
|
| 19 |
+
<div class="primary-controls">
|
| 20 |
+
<button class="btn primary" id="btn-new-task">新規タスク</button>
|
| 21 |
+
<button class="btn ghost" id="btn-toggle-group">親子ビュー</button>
|
| 22 |
+
<button class="btn ghost" id="btn-toggle-selection">一括操作</button>
|
| 23 |
+
<button class="btn ghost" id="btn-export-tasks">エクスポート</button>
|
| 24 |
+
<button class="btn ghost" id="btn-import-tasks">インポート</button>
|
| 25 |
+
<input type="file" id="input-import-tasks" accept="application/json" hidden>
|
| 26 |
+
</div>
|
| 27 |
+
</header>
|
| 28 |
|
| 29 |
<section class="filters">
|
| 30 |
<div class="search-box">
|
kanban_v2/script.js
CHANGED
|
@@ -64,14 +64,17 @@ function cacheDom() {
|
|
| 64 |
dom.selectAllBtn = document.getElementById("btn-select-all");
|
| 65 |
dom.clearSelectionBtn = document.getElementById("btn-clear-selection");
|
| 66 |
dom.dialog = document.getElementById("task-dialog");
|
| 67 |
-
dom.dialogTitle = document.getElementById("dialog-title");
|
| 68 |
-
dom.dialogClose = document.getElementById("dialog-close");
|
| 69 |
-
dom.taskForm = document.getElementById("task-form");
|
| 70 |
-
dom.taskDeleteBtn = document.getElementById("task-delete-button");
|
| 71 |
-
dom.taskStatusSelect = document.getElementById("task-status");
|
| 72 |
-
dom.taskParentSelect = document.getElementById("task-parent");
|
| 73 |
-
dom.taskCardTemplate = document.getElementById("task-card-template");
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
function bindGlobalEvents() {
|
| 77 |
dom.newTaskBtn.addEventListener("click", () => openTaskDialog());
|
|
@@ -120,20 +123,30 @@ function bindGlobalEvents() {
|
|
| 120 |
renderParentClusters();
|
| 121 |
});
|
| 122 |
}
|
| 123 |
-
if (dom.parentViewClear) {
|
| 124 |
-
dom.parentViewClear.addEventListener("click", () => {
|
| 125 |
-
state.parentViewSearch = "";
|
| 126 |
-
state.highlightParentId = "";
|
| 127 |
-
state.filters.parentId = "";
|
| 128 |
-
if (dom.parentViewSearch) dom.parentViewSearch.value = "";
|
| 129 |
-
renderAll();
|
| 130 |
-
});
|
| 131 |
-
}
|
| 132 |
-
dom.
|
| 133 |
-
dom.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
|
| 139 |
function hydrateStaticUi() {
|
|
@@ -163,9 +176,139 @@ function loadTasks() {
|
|
| 163 |
saveTasks();
|
| 164 |
}
|
| 165 |
|
| 166 |
-
function saveTasks() {
|
| 167 |
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.tasks));
|
| 168 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
function renderAll() {
|
| 171 |
document.body.classList.toggle("selection-mode", state.selectionMode);
|
|
|
|
| 64 |
dom.selectAllBtn = document.getElementById("btn-select-all");
|
| 65 |
dom.clearSelectionBtn = document.getElementById("btn-clear-selection");
|
| 66 |
dom.dialog = document.getElementById("task-dialog");
|
| 67 |
+
dom.dialogTitle = document.getElementById("dialog-title");
|
| 68 |
+
dom.dialogClose = document.getElementById("dialog-close");
|
| 69 |
+
dom.taskForm = document.getElementById("task-form");
|
| 70 |
+
dom.taskDeleteBtn = document.getElementById("task-delete-button");
|
| 71 |
+
dom.taskStatusSelect = document.getElementById("task-status");
|
| 72 |
+
dom.taskParentSelect = document.getElementById("task-parent");
|
| 73 |
+
dom.taskCardTemplate = document.getElementById("task-card-template");
|
| 74 |
+
dom.exportTasksBtn = document.getElementById("btn-export-tasks");
|
| 75 |
+
dom.importTasksBtn = document.getElementById("btn-import-tasks");
|
| 76 |
+
dom.importTasksInput = document.getElementById("input-import-tasks");
|
| 77 |
+
}
|
| 78 |
|
| 79 |
function bindGlobalEvents() {
|
| 80 |
dom.newTaskBtn.addEventListener("click", () => openTaskDialog());
|
|
|
|
| 123 |
renderParentClusters();
|
| 124 |
});
|
| 125 |
}
|
| 126 |
+
if (dom.parentViewClear) {
|
| 127 |
+
dom.parentViewClear.addEventListener("click", () => {
|
| 128 |
+
state.parentViewSearch = "";
|
| 129 |
+
state.highlightParentId = "";
|
| 130 |
+
state.filters.parentId = "";
|
| 131 |
+
if (dom.parentViewSearch) dom.parentViewSearch.value = "";
|
| 132 |
+
renderAll();
|
| 133 |
+
});
|
| 134 |
+
}
|
| 135 |
+
if (dom.exportTasksBtn) {
|
| 136 |
+
dom.exportTasksBtn.addEventListener("click", handleExportTasks);
|
| 137 |
+
}
|
| 138 |
+
if (dom.importTasksBtn && dom.importTasksInput) {
|
| 139 |
+
dom.importTasksBtn.addEventListener("click", () => {
|
| 140 |
+
dom.importTasksInput.value = "";
|
| 141 |
+
dom.importTasksInput.click();
|
| 142 |
+
});
|
| 143 |
+
dom.importTasksInput.addEventListener("change", handleImportInputChange);
|
| 144 |
+
}
|
| 145 |
+
dom.dialog.addEventListener("close", () => {
|
| 146 |
+
dom.taskForm.reset();
|
| 147 |
+
dom.taskDeleteBtn.hidden = true;
|
| 148 |
+
dom.taskForm.dataset.editingId = "";
|
| 149 |
+
});
|
| 150 |
}
|
| 151 |
|
| 152 |
function hydrateStaticUi() {
|
|
|
|
| 176 |
saveTasks();
|
| 177 |
}
|
| 178 |
|
| 179 |
+
function saveTasks() {
|
| 180 |
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.tasks));
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
function handleExportTasks() {
|
| 184 |
+
try {
|
| 185 |
+
const payload = {
|
| 186 |
+
version: 1,
|
| 187 |
+
exportedAt: new Date().toISOString(),
|
| 188 |
+
tasks: state.tasks
|
| 189 |
+
};
|
| 190 |
+
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
| 191 |
+
const url = URL.createObjectURL(blob);
|
| 192 |
+
const anchor = document.createElement("a");
|
| 193 |
+
const timestamp = new Date().toISOString().replace(/[:]/g, "-").replace(/\..+/, "");
|
| 194 |
+
anchor.href = url;
|
| 195 |
+
anchor.download = `kanban-tasks-${timestamp}.json`;
|
| 196 |
+
document.body.appendChild(anchor);
|
| 197 |
+
anchor.click();
|
| 198 |
+
anchor.remove();
|
| 199 |
+
URL.revokeObjectURL(url);
|
| 200 |
+
} catch (error) {
|
| 201 |
+
console.error("Failed to export tasks", error);
|
| 202 |
+
window.alert("\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002");
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
async function handleImportInputChange(event) {
|
| 207 |
+
const input = event.target;
|
| 208 |
+
if (!(input instanceof HTMLInputElement)) return;
|
| 209 |
+
const files = input.files;
|
| 210 |
+
const file = files && files[0];
|
| 211 |
+
if (!file) return;
|
| 212 |
+
try {
|
| 213 |
+
const text = await file.text();
|
| 214 |
+
const parsed = JSON.parse(text);
|
| 215 |
+
const tasksData = Array.isArray(parsed) ? parsed : parsed?.tasks;
|
| 216 |
+
if (!Array.isArray(tasksData)) {
|
| 217 |
+
throw new Error("Invalid import payload");
|
| 218 |
+
}
|
| 219 |
+
const normalized = normalizeImportedTasks(tasksData);
|
| 220 |
+
if (!window.confirm("\u30a4\u30f3\u30dd\u30fc\u30c8\u3059\u308b\u3068\u73fe\u5728\u306e\u30c7\u30fc\u30bf\u306f\u4e0a\u66f8\u304d\u3055\u308c\u307e\u3059\u3002\u5b9f\u884c\u3057\u307e\u3059\u304b\uff1f")) {
|
| 221 |
+
return;
|
| 222 |
+
}
|
| 223 |
+
state.tasks = normalized;
|
| 224 |
+
state.selectionMode = false;
|
| 225 |
+
state.selectedIds.clear();
|
| 226 |
+
state.highlightParentId = "";
|
| 227 |
+
state.filters.search = "";
|
| 228 |
+
state.filters.searchRaw = "";
|
| 229 |
+
state.filters.selectedTags = [];
|
| 230 |
+
state.filters.dateFrom = "";
|
| 231 |
+
state.filters.dateTo = "";
|
| 232 |
+
state.filters.parentId = "";
|
| 233 |
+
state.filters.parentSearch = "";
|
| 234 |
+
state.parentViewSearch = "";
|
| 235 |
+
state.showParentView = false;
|
| 236 |
+
state.activeStatus = STATUSES[0].id;
|
| 237 |
+
saveTasks();
|
| 238 |
+
renderAll();
|
| 239 |
+
window.alert(`\u30a4\u30f3\u30dd\u30fc\u30c8\u304c\u5b8c\u4e86\u3057\u307e\u3057\u305f\u3002\uff08${normalized.length}\u4ef6\uff09`);
|
| 240 |
+
} catch (error) {
|
| 241 |
+
console.error("Failed to import tasks", error);
|
| 242 |
+
window.alert("\u30a4\u30f3\u30dd\u30fc\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30d5\u30a1\u30a4\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002");
|
| 243 |
+
} finally {
|
| 244 |
+
input.value = "";
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function normalizeImportedTasks(tasks) {
|
| 249 |
+
const fallbackStatus = STATUSES[0]?.id ?? "backlog";
|
| 250 |
+
const normalized = [];
|
| 251 |
+
const seenIds = new Set();
|
| 252 |
+
|
| 253 |
+
tasks.forEach((item, index) => {
|
| 254 |
+
if (!item || typeof item !== "object") {
|
| 255 |
+
throw new Error(`Invalid task at index ${index}`);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
const id = typeof item.id === "string" && item.id.trim() ? item.id.trim() : generateId();
|
| 259 |
+
if (seenIds.has(id)) {
|
| 260 |
+
throw new Error(`Duplicate task id found: ${id}`);
|
| 261 |
+
}
|
| 262 |
+
seenIds.add(id);
|
| 263 |
+
|
| 264 |
+
const rawTags = Array.isArray(item.tags)
|
| 265 |
+
? item.tags
|
| 266 |
+
: typeof item.tags === "string"
|
| 267 |
+
? item.tags.split(",").map((tag) => tag.trim())
|
| 268 |
+
: [];
|
| 269 |
+
|
| 270 |
+
const tags = rawTags.filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
| 271 |
+
|
| 272 |
+
normalized.push({
|
| 273 |
+
id,
|
| 274 |
+
title: typeof item.title === "string" ? item.title : "",
|
| 275 |
+
assignee: typeof item.assignee === "string" ? item.assignee : "",
|
| 276 |
+
dueDate: typeof item.dueDate === "string" ? item.dueDate : "",
|
| 277 |
+
link: typeof item.link === "string" ? item.link : "",
|
| 278 |
+
tags,
|
| 279 |
+
progress: typeof item.progress === "string" ? item.progress : "",
|
| 280 |
+
notes: typeof item.notes === "string" ? item.notes : "",
|
| 281 |
+
parentId: typeof item.parentId === "string" ? item.parentId : "",
|
| 282 |
+
status: typeof item.status === "string" && item.status ? item.status : fallbackStatus,
|
| 283 |
+
createdAt: coerceTimestamp(item.createdAt),
|
| 284 |
+
updatedAt: coerceTimestamp(item.updatedAt)
|
| 285 |
+
});
|
| 286 |
+
});
|
| 287 |
+
|
| 288 |
+
normalized.forEach((task) => {
|
| 289 |
+
if (!STATUSES.some((status) => status.id === task.status)) {
|
| 290 |
+
task.status = fallbackStatus;
|
| 291 |
+
}
|
| 292 |
+
if (task.parentId && !normalized.some((candidate) => candidate.id === task.parentId)) {
|
| 293 |
+
task.parentId = "";
|
| 294 |
+
}
|
| 295 |
+
});
|
| 296 |
+
|
| 297 |
+
return normalized;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
function coerceTimestamp(value) {
|
| 301 |
+
if (typeof value === "number" && Number.isFinite(value)) {
|
| 302 |
+
return value;
|
| 303 |
+
}
|
| 304 |
+
if (typeof value === "string") {
|
| 305 |
+
const parsed = Date.parse(value);
|
| 306 |
+
if (!Number.isNaN(parsed)) {
|
| 307 |
+
return parsed;
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
return Date.now();
|
| 311 |
+
}
|
| 312 |
|
| 313 |
function renderAll() {
|
| 314 |
document.body.classList.toggle("selection-mode", state.selectionMode);
|