Redmine.JS

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>タスク管理プロトタイプ</title>
  <style>
    body { font-family: sans-serif; margin: 20px; }
    table { border-collapse: collapse; width: 100%; margin-top: 20px; }
    th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
    button { margin: 5px; }
  </style>
  <!-- Excelインポート用：SheetJS -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
</head>
<body>
  <h1>タスク管理プロトタイプ</h1>
  
  <!-- タスクの登録エリア -->
  <h2>タスク登録</h2>
  <form id="taskForm">
    <label>タイトル：<input type="text" id="title" required></label><br>
    <label>説明：<input type="text" id="description"></label><br>
    <label>ステータス：
      <select id="status">
        <option value="未着手">未着手</option>
        <option value="進行中">進行中</option>
        <option value="完了">完了</option>
      </select>
    </label>
    <button type="submit">タスク追加</button>
  </form>
  
  <!-- Excelファイルからインポート -->
  <h2>Excelインポート</h2>
  <input type="file" id="excelFile" accept=".xlsx, .xls" />
  <button id="importExcelBtn">Excelからインポート</button>
  
  <!-- エクスポート -->
  <h2>エクスポート</h2>
  <button id="exportJsonBtn">JSON出力</button>
  <button id="exportCsvBtn">CSV出力</button>
  
  <!-- タスク一覧表示 -->
  <h2>タスク一覧</h2>
  <table id="taskTable">
    <thead>
      <tr>
        <th>ID</th>
        <th>タイトル</th>
        <th>説明</th>
        <th>ステータス</th>
      </tr>
    </thead>
    <tbody>
      <!-- タスクデータが動的に追加される -->
    </tbody>
  </table>
  
  <script>
    // 内部状態のタスクデータは配列で管理（シンプルな自動インクリメントID）
    let tasks = [];
    let nextTaskId = 1;
    
    // タスクの追加関数
    function addTask(title, description, status) {
      const task = { id: nextTaskId++, title, description, status };
      tasks.push(task);
      renderTasks();
    }
    
    // タスク一覧をテーブルに描画
    function renderTasks() {
      const tbody = document.querySelector("#taskTable tbody");
      tbody.innerHTML = "";
      tasks.forEach(task => {
        const tr = document.createElement("tr");
        tr.innerHTML = `<td>${task.id}</td>
                        <td>${task.title}</td>
                        <td>${task.description}</td>
                        <td>${task.status}</td>`;
        tbody.appendChild(tr);
      });
    }
    
    // フォーム送信イベント：タスク追加
    document.getElementById("taskForm").addEventListener("submit", function(event){
      event.preventDefault();
      const title = document.getElementById("title").value;
      const description = document.getElementById("description").value;
      const status = document.getElementById("status").value;
      addTask(title, description, status);
      this.reset();
    });
    
    // Excelからタスクデータのインポート（前提：１列目にタイトル、２列目に説明、３列目にステータス）
    document.getElementById("importExcelBtn").addEventListener("click", function(){
      const fileInput = document.getElementById("excelFile");
      if (!fileInput.files.length) {
        alert("Excelファイルを選択してください");
        return;
      }
      const file = fileInput.files[0];
      const reader = new FileReader();
      reader.onload = function(e) {
        const data = e.target.result;
        // 拡張オプションを追加して文字化け対策
        const workbook = XLSX.read(data, {
          type: "array",
          codepage: 65001,  // UTF-8エンコーディングを指定
          cellStyles: true,
          cellNF: true,
          cellDates: true
        });
        // シート1を選択してデータを取得
        const firstSheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[firstSheetName];
        // オプション：raw: trueでセルの生の値を取得、header: 1で全体を配列として取得
        const rows = XLSX.utils.sheet_to_json(worksheet, {
          header: 1,
          raw: false, // 文字列として値を取得
          defval: ""  // 未定義セルの代替値
        });
        
        console.log("インポートデータ:", rows); // デバッグ用
        
        // ヘッダーがある場合は行番号を調整する
        rows.forEach((row, index) => {
          // 例：row[0]：タイトル, row[1]：説明, row[2]：ステータス
          if (index === 0) return; // ヘッダー行をスキップ
          if(row[0]) {
            // データが文字化けしていないか確認
            const title = String(row[0] || "");
            const desc = String(row[1] || "");
            const status = String(row[2] || "未着手");
            
            console.log(`インポート行 ${index}:`, title, desc, status); // デバッグ用
            addTask(title, desc, status);
          }
        });
      };
      reader.readAsArrayBuffer(file); // バイナリではなくArrayBufferとして読み込む
    });
    
    // JSON出力
    document.getElementById("exportJsonBtn").addEventListener("click", function(){
      if (tasks.length === 0) {
        alert("出力するタスクがありません");
        return;
      }
      const jsonStr = JSON.stringify(tasks, null, 2);
      const blob = new Blob([jsonStr], { type: "application/json" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "tasks_" + new Date().toISOString().replace(/[:.-]/g, "") + ".json";
      a.click();
      URL.revokeObjectURL(url);
    });
    
    // CSV出力（日本語対応のカンマ区切り）
    document.getElementById("exportCsvBtn").addEventListener("click", function(){
      if (tasks.length === 0) {
        alert("出力するタスクがありません");
        return;
      }
      
      // ヘッダー行
      let csv = "ID,タイトル,説明,ステータス\n";
      
      // データ行（ダブルクォートでエスケープを確実に行う）
      tasks.forEach(task => {
        // ダブルクォート内のダブルクォートを二重にエスケープ
        const title = task.title.replace(/"/g, '""');
        const description = task.description.replace(/"/g, '""');
        const status = task.status.replace(/"/g, '""');
        
        csv += `${task.id},"${title}","${description}","${status}"\n`;
      });
      
      // 重要: CSVの先頭にUTF-8 BOMを追加
      // これによりExcelが日本語を正しく認識できる
      const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
      
      // 文字列をUTF-8エンコードのUint8Arrayに変換
      const encoder = new TextEncoder();
      const csvData = encoder.encode(csv);
      
      // BOMとCSVデータを結合
      const combinedArray = new Uint8Array(bom.length + csvData.length);
      combinedArray.set(bom, 0);
      combinedArray.set(csvData, bom.length);
      
      // 適切なMIMEタイプを指定
      const blob = new Blob([combinedArray], { type: "text/csv;charset=utf-8" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "tasks_" + new Date().toISOString().replace(/[:.-]/g, "") + ".csv";
      a.click();
      URL.revokeObjectURL(url);
    });
    
    // 初期描画
    renderTasks();
  </script>
</body>
</html>
```