Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

カンバン方式でタスク管理したいけど、インストール不要なツールが見つからないのでAIに作ってもらった

Last updated at Posted at 2025-03-18

タイトルのとおりなんですが、筆者はカンバン方式でタスクを管理するのが好きです。
イメージとしては紙の付箋に「◯◯の手順書を作成する」「××さんに△△の件を電話で依頼する」みたいな細かいタスクを思いついたときに書き出して、重要度順に並べながら一個一個タスクを潰していくような形式です。
Trelloみたいなのがわかりやすいと思うんですが、インストールが必要だと職場のPCだと使いづらいですよね。
仕方がないので、Windowsの付箋機能を使ってどうにかこうにかやりくりしていましたが、ふと『AIに作ってもらえばいいじゃん』と思い立ち、命令してみた次第です。
以下コードです。

kanban.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>カンバンタスク管理</title>
    <style>
        /* スタイル部分は変更なし(元のスタイルを維持) */
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f0f0f0;
        }

        .container {
            display: flex;
            gap: 20px;
            overflow-x: auto;
            padding: 20px 0;
        }

        .column {
            background: #ebecf0;
            border-radius: 5px;
            min-width: 300px;
            padding: 10px;
        }

        .column-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }

        .task {
            background: white;
            border-radius: 3px;
            padding: 10px;
            margin-bottom: 10px;
            cursor: move;
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
        }
        .task-content {
            margin-bottom: 10px;
        }

        .task-title {
            font-weight: bold;
            margin-bottom: 5px;
        }

        .task-memo {
            font-size: 0.9em;
            color: #666;
            white-space: pre-wrap;
            margin-bottom: 10px;
        }

        .edit-btn {
            background-color: #5aac44;
            margin-right: 5px;
        }

        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.5);
        }

        .modal-content {
            background: white;
            padding: 20px;
            width: 500px;
            margin: 100px auto;
            border-radius: 5px;
        }

        textarea {
            width: 100%;
            height: 100px;
            margin: 10px 0;
        }
        .task-placeholder {
            height: 4px;
            background: #0079bf;
            margin: 2px 0;
            visibility: hidden;
        }

        .drag-over {
            background-color: #f0f8ff;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button onclick="addColumn()">列を追加</button>
        <button onclick="exportData()">エクスポート</button>
        <input type="file" id="importFile" accept=".json" style="display: none;">
        <button onclick="document.getElementById('importFile').click()">インポート</button>
    </div>

    <div class="container" id="container"></div>

    <!-- モーダルウィンドウ -->
    <div id="taskModal" class="modal">
        <div class="modal-content">
            <h3>タスクの編集</h3>
            <input type="text" id="editTitle" style="width: 100%; margin-bottom: 10px;">
            <textarea id="editMemo"></textarea>
            <input type="color" id="editColor">
            <button onclick="saveTaskChanges()">保存</button>
            <button onclick="closeModal()">キャンセル</button>
        </div>
    </div>

    <script>
        // グローバル変数宣言
        let columns = JSON.parse(localStorage.getItem('kanban')) || [
            { id: 'col1', name: 'To Do', tasks: [] },
            { id: 'col2', name: 'Doing', tasks: [] },
            { id: 'col3', name: 'Done', tasks: [] }
        ];

        let draggedTask = null;
        let sourceColumn = null;
        let currentEditingTask = null;

        // データ保存関数
        function saveData() {
            localStorage.setItem('kanban', JSON.stringify(columns));
            render();
        }

        // レンダリング関数
        function render() {
            const container = document.getElementById('container');
            container.innerHTML = '';

            columns.forEach(column => {
                const colEl = document.createElement('div');
                colEl.className = 'column';
                colEl.dataset.columnId = column.id;
                colEl.ondragover = handleDragOver;
                colEl.ondrop = handleDrop;

                // 列ヘッダー
                const header = document.createElement('div');
                header.className = 'column-header';

                const nameInput = document.createElement('input');
                nameInput.value = column.name;
                nameInput.onchange = (e) => {
                    column.name = e.target.value;
                    saveData();
                };

                const deleteBtn = document.createElement('button');
                deleteBtn.textContent = '×';
                deleteBtn.onclick = () => {
                    columns = columns.filter(c => c.id !== column.id);
                    saveData();
                };

                header.appendChild(nameInput);
                header.appendChild(deleteBtn);

                // タスクリスト
                const taskList = document.createElement('div');
                column.tasks.forEach(task => {
                    const taskEl = document.createElement('div');
                    taskEl.className = 'task';
                    taskEl.draggable = true;
                    taskEl.dataset.taskId = task.id;
                    taskEl.style.backgroundColor = task.color;
                    taskEl.ondragstart = handleDragStart;

                    // タスク内容
                    const content = document.createElement('div');
                    content.className = 'task-content';

                    const title = document.createElement('div');
                    title.className = 'task-title';
                    title.textContent = task.title;

                    const memo = document.createElement('div');
                    memo.className = 'task-memo';
                    memo.textContent = task.memo || '';

                    // 編集ボタン
                    const editBtn = document.createElement('button');
                    editBtn.textContent = '編集';
                    editBtn.className = 'edit-btn';
                    editBtn.onclick = () => openEditModal(task, column);

                    // 削除ボタン
                    const deleteTaskBtn = document.createElement('button');
                    deleteTaskBtn.textContent = '削除';
                    deleteTaskBtn.onclick = () => {
                        column.tasks = column.tasks.filter(t => t.id !== task.id);
                        saveData();
                    };

                    content.appendChild(title);
                    content.appendChild(memo);
                    taskEl.appendChild(content);
                    taskEl.appendChild(editBtn);
                    taskEl.appendChild(deleteTaskBtn);
                    taskList.appendChild(taskEl);
                });

                // タスク追加ボタン
                const addTaskBtn = document.createElement('button');
                addTaskBtn.textContent = '+ タスク追加';
                addTaskBtn.onclick = () => {
                    const title = prompt('タスク名を入力してください');
                    if (title) {
                        column.tasks.push({
                            id: Date.now().toString(),
                            title,
                            memo: '',
                            color: '#ffffff'
                        });
                        saveData();
                    }
                };

                colEl.appendChild(header);
                colEl.appendChild(taskList);
                colEl.appendChild(addTaskBtn);
                container.appendChild(colEl);
            });
        }

        // ドラッグ&ドロップ処理
        function handleDragStart(e) {
            draggedTask = e.target.closest('.task');
            sourceColumn = draggedTask.closest('.column');
            draggedTask.classList.add('dragging');
            e.dataTransfer.effectAllowed = 'move';
        }

        function handleDragOver(e) {
            e.preventDefault();
            const targetColumn = e.target.closest('.column');
            const afterElement = getDragAfterElement(targetColumn, e.clientY);
            const taskList = targetColumn.querySelector('.task-list') || targetColumn;

            // プレースホルダーの表示
            const placeholder = taskList.querySelector('.task-placeholder');
            if (placeholder) placeholder.remove();

            if (afterElement) {
                const placeholder = document.createElement('div');
                placeholder.className = 'task-placeholder';
                taskList.insertBefore(placeholder, afterElement);
                placeholder.style.visibility = 'visible';
            } else {
                const placeholder = document.createElement('div');
                placeholder.className = 'task-placeholder';
                taskList.appendChild(placeholder);
                placeholder.style.visibility = 'visible';
            }
        }

        function handleDrop(e) {
            e.preventDefault();
            const targetColumn = e.target.closest('.column');
            const placeholder = targetColumn.querySelector('.task-placeholder');
            if (placeholder) placeholder.remove();

            if (!targetColumn) return;

            const sourceCol = columns.find(c => c.id === sourceColumn.dataset.columnId);
            const targetCol = columns.find(c => c.id === targetColumn.dataset.columnId);
            const taskId = draggedTask.dataset.taskId;
            const task = sourceCol.tasks.find(t => t.id === taskId);

            // 挿入位置の決定
            const afterElement = getDragAfterElement(targetColumn, e.clientY);
            const insertIndex = afterElement 
                ? targetCol.tasks.findIndex(t => t.id === afterElement.dataset.taskId)
                : targetCol.tasks.length;

            if (sourceCol === targetCol) {
                // 同じ列内の移動
                sourceCol.tasks = sourceCol.tasks.filter(t => t.id !== taskId);
                targetCol.tasks.splice(insertIndex, 0, task);
            } else {
                // 別の列への移動
                sourceCol.tasks = sourceCol.tasks.filter(t => t.id !== taskId);
                targetCol.tasks.splice(insertIndex, 0, task);
            }

            draggedTask.classList.remove('dragging');
            saveData();
        }

        // 挿入位置検出用ヘルパー関数
        function getDragAfterElement(container, y) {
            const tasks = [...container.querySelectorAll('.task:not(.dragging)')];

            return tasks.reduce((closest, task) => {
                const box = task.getBoundingClientRect();
                const offset = y - box.top - box.height / 2;
                
                if (offset < 0 && offset > closest.offset) {
                    return { offset, element: task };
                } else {
                    return closest;
                }
            }, { offset: Number.NEGATIVE_INFINITY }).element;
        }

        // モーダル関連関数
        function openEditModal(task, column) {
            currentEditingTask = task;
            document.getElementById('editTitle').value = task.title;
            document.getElementById('editMemo').value = task.memo || '';
            document.getElementById('editColor').value = task.color;
            document.getElementById('taskModal').style.display = 'block';
        }

        function closeModal() {
            document.getElementById('taskModal').style.display = 'none';
        }

        function saveTaskChanges() {
            if (!currentEditingTask) return;
            
            const newTitle = document.getElementById('editTitle').value.trim();
            if (!newTitle) {
                alert('タイトルは必須です');
                return;
            }

            currentEditingTask.title = newTitle;
            currentEditingTask.memo = document.getElementById('editMemo').value;
            currentEditingTask.color = document.getElementById('editColor').value;
            saveData();
            closeModal();
        }

        // その他の関数
        function addColumn() {
            const name = prompt('新しい列の名前を入力してください');
            if (name) {
                columns.push({
                    id: Date.now().toString(),
                    name,
                    tasks: []
                });
                saveData();
            }
        }

        function exportData() {
            const data = JSON.stringify(columns);
            const blob = new Blob([data], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'kanban-data.json';
            a.click();
            URL.revokeObjectURL(url);
        }

        document.getElementById('importFile').addEventListener('change', function(e) {
            const file = e.target.files[0];
            const reader = new FileReader();
            reader.onload = function() {
                columns = JSON.parse(reader.result);
                saveData();
            };
            reader.readAsText(file);
        });

        // 初期表示
        render();
    </script>
</body>
</html>

メモ帳に貼り付けて拡張子をhtmlにすればぱぱっと使える、というのを想定して作りました。
あくまで自分用です。
バグとかあったら勝手に修正すると思います。
エクスポート・インポート機能は「ブラウザのキャッシュ削除したときにタスクが全部消えたら嫌だな・・・」という思いからつけました。
ちなみにコーディングはDeepSeekR1です。API安いんですよね。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Comments

nmcr5575
@nmcr5575

個人用なので、クオリティの保証はできません。

AIに作ってもらったコードのクォリティが気になるなら、完成したコードを更にAIにリファクタリングしてもらえばいいと思います。
リファクタリングしたポイントなども教えてくれるので、勉強にもなるでしょう。

1

Let's comment your feelings that are more than good

Qiita Conference 2025 will be held!: 4/23(wed) - 4/25(Fri)

Qiita Conference is the largest tech conference in Qiita!

Keynote Speaker

ymrl、Masanobu Naruse, Takeshi Kano, Junichi Ito, uhyo, Hiroshi Tokumaru, MinoDriven, Minorun, Hiroyuki Sakuraba, tenntenn, drken, konifar

View event details

Being held Article posting campaign

0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address