Okoge-keys commited on
Commit
3b64b2c
·
verified ·
1 Parent(s): 7cf23f8

Upload 3 files

Browse files
Files changed (2) hide show
  1. kanban_v2/index.html +9 -6
  2. kanban_v2/script.js +168 -25
kanban_v2/index.html CHANGED
@@ -16,12 +16,15 @@
16
  <p class="subtitle">&#x73FE;&#x5834;&#x306E;&#x8996;&#x70B9;&#x3067;&#x78E8;&#x304D;&#x4E0A;&#x3052;&#x305F;&#x30BF;&#x30B9;&#x30AF;&#x30D5;&#x30ED;&#x30FC;</p>
17
  </div>
18
  </div>
19
- <div class="primary-controls">
20
- <button class="btn primary" id="btn-new-task">&#x65B0;&#x898F;&#x30BF;&#x30B9;&#x30AF;</button>
21
- <button class="btn ghost" id="btn-toggle-group">&#x89AA;&#x5B50;&#x30D3;&#x30E5;&#x30FC;</button>
22
- <button class="btn ghost" id="btn-toggle-selection">&#x4E00;&#x62EC;&#x64CD;&#x4F5C;</button>
23
- </div>
24
- </header>
 
 
 
25
 
26
  <section class="filters">
27
  <div class="search-box">
 
16
  <p class="subtitle">&#x73FE;&#x5834;&#x306E;&#x8996;&#x70B9;&#x3067;&#x78E8;&#x304D;&#x4E0A;&#x3052;&#x305F;&#x30BF;&#x30B9;&#x30AF;&#x30D5;&#x30ED;&#x30FC;</p>
17
  </div>
18
  </div>
19
+ <div class="primary-controls">
20
+ <button class="btn primary" id="btn-new-task">&#x65B0;&#x898F;&#x30BF;&#x30B9;&#x30AF;</button>
21
+ <button class="btn ghost" id="btn-toggle-group">&#x89AA;&#x5B50;&#x30D3;&#x30E5;&#x30FC;</button>
22
+ <button class="btn ghost" id="btn-toggle-selection">&#x4E00;&#x62EC;&#x64CD;&#x4F5C;</button>
23
+ <button class="btn ghost" id="btn-export-tasks">&#x30A8;&#x30AF;&#x30B9;&#x30DD;&#x30FC;&#x30C8;</button>
24
+ <button class="btn ghost" id="btn-import-tasks">&#x30A4;&#x30F3;&#x30DD;&#x30FC;&#x30C8;</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.dialog.addEventListener("close", () => {
133
- dom.taskForm.reset();
134
- dom.taskDeleteBtn.hidden = true;
135
- dom.taskForm.dataset.editingId = "";
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);