ログ閲覧画面(期間フィルター+結果バッジ)

応用例 中級

この画面パターンについて

操作ログ・ログイン履歴など、時系列データを「見るだけ」の用途に特化した閲覧画面のパターンです。 日付範囲・種別・結果の3軸フィルターと色分けバッジで「目的のログに素早くたどり着く」ことに最適化します。 検索テーブル画面との違いは、編集導線がなく日付範囲フィルターが主役で、視認性(バッジ・行の色分け)を最優先する点です。

こんな場面で使えます

  • 監査対応 — 誰がいつ何をしたかを期間指定で確認する
  • 障害・トラブル調査 — 失敗ログだけを絞り込んで原因を探す
  • 利用状況の確認 — ログイン履歴やエクスポート操作を一覧する

この画面で使っているUIコンポーネント

#パーツこの画面での役割
1日付範囲フィルター開始日〜終了日の絞り込み(片方のみ可)
2種別セレクトボックスログイン/更新/削除/エクスポートの絞り込み
3結果セレクトボックス成功/失敗/警告の絞り込み
4条件クリアボタン全条件のリセット
5読み取り専用テーブル日時降順固定の一覧表示
6結果バッジ成功=緑・失敗=赤・警告=黄の色分け
7ページネーション20件/ページ・条件維持で移動
8ヒット件数+空状態件数の常時表示・0件時の案内

実装のポイント・注意点

構造は検索テーブル画面と同じ「state → 絞り込み → ページ分割 → 描画」の単方向フローで、ソートUIを持たず日時降順に固定する分シンプルです。 日付の比較は、datetime の先頭10文字(YYYY-MM-DD)と input[type="date"] の値が同じ形式なので、 Date 変換せず文字列比較で安全に行えます(new Date('2026-06-10') がUTC解釈になるタイムゾーンの罠を避けられます)。

注意が必要なのは開始日>終了日の逆転入力で、警告を表示して絞り込みを実行しない(直前の表示を維持する)ことで、突然の0件表示でユーザーを混乱させません。 失敗・警告の行には行背景にも薄い色を付け、バッジ単体よりも一覧スキャン時の視認性を高めます(バッジ=判定、行背景=補助の役割分担)。

8個のUIコンポーネントをHTML・CSS・バニラJavaScriptのみで組み合わせており、 フレームワーク不要で画面ごとコピペして使えます。

動作サンプル

ログ閲覧画面(期間フィルター+結果バッジ)のデモ画面 動作サンプルを別ウィンドウで確認 ↗

試してみる:

  • 期間を狭めて絞り込み、件数表示とページが連動することを確認
  • 開始日を終了日より後にして、警告表示(絞り込みは実行されない)を確認
  • 結果を「失敗」にして、行背景が薄赤になる視認性の工夫を確認

そのほかの操作も自由に試してみてください。

サンプルソース

4つのファイルを同じフォルダに保存し、ローカルサーバー(VS Code Live Server等)経由で index.html を開くと動作確認できます。
ファイル名:index.html / style.css / script.js + data/ フォルダに data.jsonfetch を使用しているため file:// での直接表示は動作しません(CORSエラー)。 保存時の文字コードは UTF-8 を指定してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ログ閲覧画面 サンプル</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="log-screen">
  <h1 class="screen-title">操作ログ</h1>

  <!-- ===== フィルターエリア ===== -->
  <div class="filter-panel">
    <div class="filter-field">
      <label for="dateFrom">期間</label>
      <div class="date-range">
        <input type="date" id="dateFrom" aria-label="開始日">
        <span class="date-separator">〜</span>
        <input type="date" id="dateTo" aria-label="終了日">
      </div>
    </div>
    <div class="filter-field">
      <label for="typeFilter">種別</label>
      <select id="typeFilter">
        <option value="">すべて</option>
        <option value="login">ログイン</option>
        <option value="update">更新</option>
        <option value="delete">削除</option>
        <option value="export">エクスポート</option>
      </select>
    </div>
    <div class="filter-field">
      <label for="resultFilter">結果</label>
      <select id="resultFilter">
        <option value="">すべて</option>
        <option value="success">成功</option>
        <option value="error">失敗</option>
        <option value="warning">警告</option>
      </select>
    </div>
    <button type="button" class="btn-secondary" id="clearBtn">クリア</button>
  </div>
  <p class="filter-error" id="filterError" hidden>開始日は終了日以前を指定してください</p>

  <!-- ===== 件数表示 ===== -->
  <p class="result-count" id="resultCount"></p>

  <!-- ===== ログテーブル ===== -->
  <div class="table-wrapper" id="tableWrapper">
    <table class="log-table">
      <thead>
        <tr><th>日時</th><th>種別</th><th>内容</th><th>操作者</th><th>結果</th></tr>
      </thead>
      <tbody id="tableBody"><!-- JSで生成 --></tbody>
    </table>
  </div>

  <!-- ===== 空状態(0件時に表示) ===== -->
  <div class="empty-state" id="emptyState" hidden>
    <p class="empty-icon">📋</p>
    <p class="empty-message">条件に一致するログがありません</p>
    <button type="button" class="btn-secondary" id="emptyClearBtn">条件をクリア</button>
  </div>

  <!-- ===== ページネーション ===== -->
  <nav class="pagination" id="pagination" aria-label="ページネーション"><!-- JSで生成 --></nav>
</div>

<script src="./script.js"></script>
</body>
</html>
/* ===== ログ閲覧画面 — style.css ===== */
*, *::before, *::after { box-sizing: border-box; }

:root {
  --color-primary:   #2B7FE8;
  --color-text:      #1E293B;
  --color-muted:     #64748B;
  --color-border:    #D0D7E0;
  --color-bg:        #F4F6F9;
  --color-card:      #FFFFFF;
  --color-th-bg:     #F1F5F9;
  --color-row-hover: #F4F8FE;
  /* 結果バッジ・行背景の色 */
  --color-success-fg: #166534;
  --color-success-bg: #DCFCE7;
  --color-error-fg:   #B91C1C;
  --color-error-bg:   #FEE2E2;
  --color-warning-fg: #854D0E;
  --color-warning-bg: #FEF9C3;
  --color-row-error:   #FDF2F2;
  --color-row-warning: #FDFAF0;
}

body {
  margin: 0;
  font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
}

/* ===== 画面レイアウト ===== */
.log-screen {
  max-width: 1160px;
  margin: 0 auto;
  padding: 24px 20px 48px;
}

.screen-title {
  font-size: 22px;
  margin: 0 0 16px;
}

/* ===== フィルターエリア ===== */
.filter-panel {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  padding: 16px;
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  margin-bottom: 8px;
}

.filter-field label {
  display: block;
  font-size: 12px;
  color: var(--color-muted);
  margin-bottom: 4px;
}

.date-range {
  display: flex;
  align-items: center;
  gap: 6px;
}

.date-separator {
  color: var(--color-muted);
}

.filter-field input,
.filter-field select {
  height: 38px;
  padding: 0 10px;
  font-size: 14px;
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  background: #fff;
}

.filter-field select { min-width: 130px; }

.filter-field input:focus,
.filter-field select:focus {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}

/* クリアボタンを右端に寄せる */
#clearBtn { margin-left: auto; }

/* ===== ボタン ===== */
.btn-secondary {
  height: 38px;
  padding: 0 20px;
  font-size: 14px;
  color: #5A6A7A;
  background: #fff;
  border: 1.5px solid var(--color-border);
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.btn-secondary:hover {
  background: var(--color-bg);
  border-color: #9AA5B4;
}

/* ===== フィルター警告(開始日>終了日) ===== */
.filter-error {
  font-size: 13px;
  color: var(--color-error-fg);
  margin: 0 0 8px;
}

/* ===== 件数表示 ===== */
.result-count {
  font-size: 13px;
  color: var(--color-muted);
  margin: 8px 0;
}

/* ===== テーブル ===== */
/* スマホ幅では横スクロールで対応する */
.table-wrapper {
  overflow-x: auto;
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: 8px;
}

.log-table {
  width: 100%;
  min-width: 720px;
  border-collapse: collapse;
  font-size: 14px;
}

.log-table th,
.log-table td {
  padding: 10px 14px;
  text-align: left;
  border-bottom: 1px solid #E6EBF1;
}

.log-table thead th {
  background: var(--color-th-bg);
  font-size: 13px;
  color: var(--color-muted);
  white-space: nowrap;
}

/* 日時列は等幅フォントで桁を揃え、ログの可読性を上げる */
.log-table td.col-datetime {
  font-family: Consolas, "Courier New", monospace;
  white-space: nowrap;
}

.log-table tbody tr:last-child td { border-bottom: none; }

/* ===== 行の状態色(バッジより薄い色で補助的に) ===== */
.log-table tr.row-error   { background: var(--color-row-error); }
.log-table tr.row-warning { background: var(--color-row-warning); }

/* ===== 結果バッジ ===== */
.badge {
  display: inline-block;
  padding: 3px 12px;
  font-size: 12px;
  border-radius: 9999px;
  white-space: nowrap;
}

.badge-success { color: var(--color-success-fg); background: var(--color-success-bg); }
.badge-error   { color: var(--color-error-fg);   background: var(--color-error-bg); }
.badge-warning { color: var(--color-warning-fg); background: var(--color-warning-bg); }

/* ===== ページネーション ===== */
.pagination {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 16px;
}

.pagination button {
  min-width: 38px;
  height: 38px;
  padding: 0 12px;
  font-size: 14px;
  color: var(--color-text);
  background: #fff;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s;
}

.pagination button:hover:not(:disabled):not(.current) {
  background: var(--color-row-hover);
}

.pagination button.current {
  color: #fff;
  background: var(--color-primary);
  border-color: var(--color-primary);
  cursor: default;
}

.pagination button:disabled {
  color: #B6C0CC;
  background: var(--color-bg);
  cursor: not-allowed;
}

/* ===== 空状態 ===== */
.empty-state {
  padding: 48px 20px;
  text-align: center;
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: 8px;
}

.empty-icon {
  font-size: 32px;
  margin: 0 0 8px;
}

.empty-message {
  font-size: 15px;
  color: var(--color-muted);
  margin: 0 0 16px;
}

/* hidden 属性を確実に効かせる(display 指定との競合対策) */
.log-screen [hidden] { display: none !important; }

/* ===== レスポンシブ(768px以下) ===== */
@media (max-width: 768px) {
  .filter-panel {
    flex-direction: column;
    align-items: stretch;
  }

  .date-range { width: 100%; }
  .date-range input { flex: 1; }

  .filter-field select { width: 100%; }

  #clearBtn { margin-left: 0; }
}
/* =====================================================
   ログ閲覧画面のスクリプト

   仕組み:すべての操作は state(期間・種別・結果・ページ)を
   書き換えて update() を呼ぶだけ。update() が
   絞り込み → ページ分割 → 描画 を毎回やり直す。
   ソートUIはなく、初回ロード時に日時降順へ並べた配列を保持する。

   日付の絞り込みは datetime の先頭10文字(YYYY-MM-DD)と
   date input の値の文字列比較で行う(同形式のため Date 変換不要)。
   行の生成は createElement + textContent(XSS対策)。
   ===================================================== */

// ===== 設定値 =====
var PAGE_SIZE = 20;                            // 1ページの表示件数
var JSON_PATH = './data/data.json'; // データの取得元
var DATE_KEY_LENGTH = 10;                      // datetime の先頭 YYYY-MM-DD の長さ

// 種別コードと表示名の対応表
var TYPE_LABELS = {
  login:  'ログイン',
  update: '更新',
  delete: '削除',
  export: 'エクスポート'
};

// 結果コードと表示名・バッジ色クラス・行クラスの対応表
var RESULT_MAP = {
  success: { label: '成功', badgeClass: 'badge-success', rowClass: '' },
  error:   { label: '失敗', badgeClass: 'badge-error',   rowClass: 'row-error' },
  warning: { label: '警告', badgeClass: 'badge-warning', rowClass: 'row-warning' }
};

// ===== 状態管理 =====
var allLogs = [];  // fetchで取得し日時降順に並べた全データ(並び順は変更しない)

var state = {
  dateFrom: '', // 開始日(YYYY-MM-DD・'' は未指定)
  dateTo:   '', // 終了日(YYYY-MM-DD・'' は未指定)
  type:     '', // 種別('' はすべて)
  result:   '', // 結果('' はすべて)
  page:     1   // 現在のページ番号
};

// ===== DOM要素 =====
var dateFromInput = document.getElementById('dateFrom');
var dateToInput   = document.getElementById('dateTo');
var typeSelect    = document.getElementById('typeFilter');
var resultSelect  = document.getElementById('resultFilter');
var clearBtn      = document.getElementById('clearBtn');
var filterError   = document.getElementById('filterError');
var resultCount   = document.getElementById('resultCount');
var tableWrapper  = document.getElementById('tableWrapper');
var tableBody     = document.getElementById('tableBody');
var emptyState    = document.getElementById('emptyState');
var emptyClearBtn = document.getElementById('emptyClearBtn');
var pagination    = document.getElementById('pagination');

// ===== 初期化(データ読み込み) =====
fetch(JSON_PATH)
  .then(function (res) {
    if (!res.ok) { throw new Error('HTTP ' + res.status); }
    return res.json();
  })
  .then(function (data) {
    // 初回に日時降順へ並べておき、以降はこの順序を保持する(ソートUIなし)
    allLogs = data.logs.slice().sort(function (a, b) {
      if (a.datetime < b.datetime) { return 1; }
      if (a.datetime > b.datetime) { return -1; }
      return 0;
    });
    update();
  })
  .catch(function (err) {
    console.error('読み込みエラー:', err);
    resultCount.textContent = 'データを読み込めませんでした。ローカルサーバーで開いているか確認してください。';
  });

// ===== 絞り込み(イベント) =====
// 期間を変更 → 範囲の妥当性を確かめてから絞り込み再描画
dateFromInput.addEventListener('change', function () {
  applyDateChange(dateFromInput.value, state.dateTo, function () {
    state.dateFrom = dateFromInput.value;
  });
});

dateToInput.addEventListener('change', function () {
  applyDateChange(state.dateFrom, dateToInput.value, function () {
    state.dateTo = dateToInput.value;
  });
});

// 種別変更 → 即時にAND絞り込み再描画
typeSelect.addEventListener('change', function () {
  state.type = typeSelect.value;
  state.page = 1;
  update();
});

// 結果変更 → 即時にAND絞り込み再描画
resultSelect.addEventListener('change', function () {
  state.result = resultSelect.value;
  state.page = 1;
  update();
});

// クリアボタン → 全条件をリセットして全件表示に戻す
clearBtn.addEventListener('click', clearAll);

// 空状態の「条件をクリア」 → 同じく全リセットして一覧に復帰
emptyClearBtn.addEventListener('click', clearAll);

// 期間変更の共通処理:開始日>終了日なら警告だけ出して state を更新しない
// (絞り込みを実行せず直前の表示を維持し、突然の0件表示で混乱させない)
function applyDateChange(from, to, commit) {
  if (from && to && from > to) {
    filterError.hidden = false;
    return;
  }
  filterError.hidden = true;
  commit();
  state.page = 1;
  update();
}

// 入力欄と state を初期状態に戻して再描画する
function clearAll() {
  dateFromInput.value = '';
  dateToInput.value = '';
  typeSelect.value = '';
  resultSelect.value = '';
  filterError.hidden = true;
  state.dateFrom = '';
  state.dateTo = '';
  state.type = '';
  state.result = '';
  state.page = 1;
  update();
}

// ===== 更新処理(絞り込み → ページ分割 → 描画) =====
function update() {
  var filtered = applyFilter(allLogs);

  var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  // 件数が減って現在ページが範囲外になった場合は最終ページに丸める(保険)
  if (state.page > totalPages) { state.page = totalPages; }

  var start = (state.page - 1) * PAGE_SIZE;
  var pageItems = filtered.slice(start, start + PAGE_SIZE);

  renderCount(filtered.length);
  renderTable(pageItems);
  renderPagination(totalPages, filtered.length);
  toggleEmptyState(filtered.length === 0);
}

// 期間・種別・結果のAND絞り込み
function applyFilter(logs) {
  return logs.filter(function (log) {
    var dateKey = log.datetime.slice(0, DATE_KEY_LENGTH); // YYYY-MM-DD
    if (state.dateFrom && dateKey < state.dateFrom) { return false; }
    if (state.dateTo && dateKey > state.dateTo) { return false; }
    if (state.type && log.type !== state.type) { return false; }
    if (state.result && log.result !== state.result) { return false; }
    return true;
  });
}

// ===== 描画 =====
// 「全60件中 1〜20件を表示」のヒット件数表示
function renderCount(total) {
  if (total === 0) {
    resultCount.textContent = '';
    return;
  }
  var start = (state.page - 1) * PAGE_SIZE + 1;
  var end = Math.min(state.page * PAGE_SIZE, total);
  resultCount.textContent = '全' + total + '件中 ' + start + '〜' + end + '件を表示';
}

// 現在ページのデータでテーブル本体を描き直す
function renderTable(logs) {
  tableBody.textContent = '';
  logs.forEach(function (log) {
    var info = RESULT_MAP[log.result];
    var tr = document.createElement('tr');
    // 失敗・警告の行は薄い背景色を付けて一覧スキャン時の視認性を上げる
    if (info.rowClass) { tr.className = info.rowClass; }
    tr.appendChild(createCell(log.datetime, 'col-datetime'));
    tr.appendChild(createCell(TYPE_LABELS[log.type] || log.type));
    tr.appendChild(createCell(log.detail));
    tr.appendChild(createCell(log.actor));
    tr.appendChild(createResultCell(info));
    tableBody.appendChild(tr);
  });
}

// テキストセルを生成する(className は任意)
function createCell(text, className) {
  var td = document.createElement('td');
  td.textContent = text;
  if (className) { td.className = className; }
  return td;
}

// 結果バッジ入りのセルを生成する
function createResultCell(info) {
  var td = document.createElement('td');
  var badge = document.createElement('span');
  badge.className = 'badge ' + info.badgeClass;
  badge.textContent = info.label;
  td.appendChild(badge);
  return td;
}

// 「前へ」「ページ番号」「次へ」のボタン列を描き直す
function renderPagination(totalPages, totalItems) {
  pagination.textContent = '';
  if (totalItems === 0) { return; }

  pagination.appendChild(createPageButton('前へ', state.page - 1, state.page === 1, false));
  for (var i = 1; i <= totalPages; i++) {
    pagination.appendChild(createPageButton(String(i), i, false, i === state.page));
  }
  pagination.appendChild(createPageButton('次へ', state.page + 1, state.page === totalPages, false));
}

// ページ移動ボタンを生成する(クリックでそのページへ。条件は維持される)
function createPageButton(label, page, disabled, isCurrent) {
  var btn = document.createElement('button');
  btn.type = 'button';
  btn.textContent = label;
  btn.disabled = disabled;
  if (isCurrent) {
    btn.className = 'current';
    btn.setAttribute('aria-current', 'page');
  }
  btn.addEventListener('click', function () {
    if (disabled || isCurrent) { return; }
    state.page = page;
    update();
  });
  return btn;
}

// 0件時はテーブルとページネーションを隠して空状態を表示する
function toggleEmptyState(isEmpty) {
  tableWrapper.hidden = isEmpty;
  pagination.hidden = isEmpty;
  emptyState.hidden = !isEmpty;
}
{
  "logs": [
    {"id":1,"datetime":"2026-06-13 15:23","type":"login","detail":"ログアウトしました","actor":"利用者C","result":"success"},
    {"id":2,"datetime":"2026-06-13 01:37","type":"update","detail":"古いバージョンのまま更新しました","actor":"利用者D","result":"warning"},
    {"id":3,"datetime":"2026-06-12 05:45","type":"login","detail":"ログインしました","actor":"利用者D","result":"success"},
    {"id":4,"datetime":"2026-06-11 16:46","type":"delete","detail":"削除に失敗しました","actor":"利用者C","result":"error"},
    {"id":5,"datetime":"2026-06-10 22:36","type":"update","detail":"設定を変更しました","actor":"利用者B","result":"success"},
    {"id":6,"datetime":"2026-06-10 10:00","type":"login","detail":"ログインに失敗しました","actor":"利用者B","result":"error"},
    {"id":7,"datetime":"2026-06-09 15:51","type":"export","detail":"エクスポートに失敗しました","actor":"利用者B","result":"error"},
    {"id":8,"datetime":"2026-06-09 00:33","type":"delete","detail":"削除に失敗しました","actor":"利用者E","result":"error"},
    {"id":9,"datetime":"2026-06-08 09:43","type":"update","detail":"設定を変更しました","actor":"利用者E","result":"success"},
    {"id":10,"datetime":"2026-06-07 17:08","type":"delete","detail":"項目を削除しました","actor":"利用者E","result":"success"},
    {"id":11,"datetime":"2026-06-06 23:44","type":"update","detail":"データを更新しました","actor":"利用者D","result":"success"},
    {"id":12,"datetime":"2026-06-06 07:54","type":"update","detail":"データを更新しました","actor":"利用者A","result":"success"},
    {"id":13,"datetime":"2026-06-05 15:08","type":"export","detail":"CSVをエクスポートしました","actor":"利用者C","result":"success"},
    {"id":14,"datetime":"2026-06-04 21:08","type":"export","detail":"PDFを出力しました","actor":"利用者B","result":"success"},
    {"id":15,"datetime":"2026-06-04 08:02","type":"login","detail":"ログインしました","actor":"利用者D","result":"success"},
    {"id":16,"datetime":"2026-06-03 14:07","type":"delete","detail":"ファイルを削除しました","actor":"利用者C","result":"success"},
    {"id":17,"datetime":"2026-06-02 23:05","type":"export","detail":"CSVをエクスポートしました","actor":"利用者E","result":"success"},
    {"id":18,"datetime":"2026-06-02 06:17","type":"update","detail":"設定を変更しました","actor":"利用者E","result":"success"},
    {"id":19,"datetime":"2026-06-01 16:47","type":"export","detail":"CSVをエクスポートしました","actor":"利用者B","result":"success"},
    {"id":20,"datetime":"2026-05-31 22:42","type":"login","detail":"ログインしました","actor":"利用者E","result":"success"},
    {"id":21,"datetime":"2026-05-31 08:35","type":"login","detail":"ログアウトしました","actor":"利用者A","result":"success"},
    {"id":22,"datetime":"2026-05-30 13:25","type":"delete","detail":"削除に失敗しました","actor":"利用者E","result":"error"},
    {"id":23,"datetime":"2026-05-30 01:25","type":"login","detail":"普段と異なる環境からログインしました","actor":"利用者E","result":"warning"},
    {"id":24,"datetime":"2026-05-29 06:32","type":"update","detail":"データを更新しました","actor":"利用者A","result":"success"},
    {"id":25,"datetime":"2026-05-28 16:38","type":"export","detail":"CSVをエクスポートしました","actor":"利用者D","result":"success"},
    {"id":26,"datetime":"2026-05-27 21:21","type":"update","detail":"データの更新に失敗しました","actor":"利用者C","result":"error"},
    {"id":27,"datetime":"2026-05-27 07:05","type":"export","detail":"CSVをエクスポートしました","actor":"利用者D","result":"success"},
    {"id":28,"datetime":"2026-05-26 14:10","type":"login","detail":"ログアウトしました","actor":"利用者C","result":"success"},
    {"id":29,"datetime":"2026-05-25 23:38","type":"login","detail":"ログアウトしました","actor":"利用者C","result":"success"},
    {"id":30,"datetime":"2026-05-25 09:13","type":"export","detail":"PDFを出力しました","actor":"利用者E","result":"success"},
    {"id":31,"datetime":"2026-05-24 16:55","type":"update","detail":"データを更新しました","actor":"利用者A","result":"success"},
    {"id":32,"datetime":"2026-05-23 23:07","type":"export","detail":"PDFを出力しました","actor":"利用者A","result":"success"},
    {"id":33,"datetime":"2026-05-23 07:47","type":"update","detail":"プロフィールを更新しました","actor":"利用者A","result":"success"},
    {"id":34,"datetime":"2026-05-22 15:01","type":"login","detail":"ログインしました","actor":"利用者C","result":"success"},
    {"id":35,"datetime":"2026-05-22 01:23","type":"update","detail":"古いバージョンのまま更新しました","actor":"利用者E","result":"warning"},
    {"id":36,"datetime":"2026-05-21 09:19","type":"login","detail":"ログアウトしました","actor":"利用者B","result":"success"},
    {"id":37,"datetime":"2026-05-20 15:49","type":"login","detail":"ログアウトしました","actor":"利用者D","result":"success"},
    {"id":38,"datetime":"2026-05-20 02:00","type":"login","detail":"ログインしました","actor":"利用者A","result":"success"},
    {"id":39,"datetime":"2026-05-19 05:05","type":"update","detail":"データの更新に失敗しました","actor":"利用者B","result":"error"},
    {"id":40,"datetime":"2026-05-18 16:23","type":"login","detail":"普段と異なる環境からログインしました","actor":"利用者C","result":"warning"},
    {"id":41,"datetime":"2026-05-17 22:28","type":"delete","detail":"ファイルを削除しました","actor":"利用者C","result":"success"},
    {"id":42,"datetime":"2026-05-17 06:02","type":"login","detail":"ログインしました","actor":"利用者D","result":"success"},
    {"id":43,"datetime":"2026-05-16 14:23","type":"update","detail":"データを更新しました","actor":"利用者D","result":"success"},
    {"id":44,"datetime":"2026-05-16 00:25","type":"login","detail":"ログインに失敗しました","actor":"利用者B","result":"error"},
    {"id":45,"datetime":"2026-05-15 07:13","type":"update","detail":"設定を変更しました","actor":"利用者E","result":"success"},
    {"id":46,"datetime":"2026-05-14 15:28","type":"delete","detail":"多数の項目を一括削除しました","actor":"利用者A","result":"warning"},
    {"id":47,"datetime":"2026-05-13 23:22","type":"delete","detail":"ファイルを削除しました","actor":"利用者D","result":"success"},
    {"id":48,"datetime":"2026-05-13 06:34","type":"export","detail":"エクスポートに失敗しました","actor":"利用者E","result":"error"},
    {"id":49,"datetime":"2026-05-12 15:31","type":"update","detail":"プロフィールを更新しました","actor":"利用者E","result":"success"},
    {"id":50,"datetime":"2026-05-11 21:41","type":"export","detail":"PDFを出力しました","actor":"利用者C","result":"success"},
    {"id":51,"datetime":"2026-05-11 09:36","type":"login","detail":"ログインに失敗しました","actor":"利用者D","result":"error"},
    {"id":52,"datetime":"2026-05-10 17:57","type":"login","detail":"ログアウトしました","actor":"利用者E","result":"success"},
    {"id":53,"datetime":"2026-05-09 23:49","type":"export","detail":"エクスポートに失敗しました","actor":"利用者A","result":"error"},
    {"id":54,"datetime":"2026-05-09 08:18","type":"delete","detail":"削除に失敗しました","actor":"利用者B","result":"error"},
    {"id":55,"datetime":"2026-05-08 14:07","type":"update","detail":"プロフィールを更新しました","actor":"利用者E","result":"success"},
    {"id":56,"datetime":"2026-05-07 23:18","type":"update","detail":"プロフィールを更新しました","actor":"利用者C","result":"success"},
    {"id":57,"datetime":"2026-05-07 07:22","type":"login","detail":"普段と異なる環境からログインしました","actor":"利用者A","result":"warning"},
    {"id":58,"datetime":"2026-05-06 17:10","type":"update","detail":"データを更新しました","actor":"利用者B","result":"success"},
    {"id":59,"datetime":"2026-05-05 21:05","type":"update","detail":"設定を変更しました","actor":"利用者C","result":"success"},
    {"id":60,"datetime":"2026-05-05 05:21","type":"delete","detail":"ファイルを削除しました","actor":"利用者E","result":"success"}
  ]
}

AI用プロンプト

以下のプロンプトをコピーしてAIに渡すと、同様の画面パターンを生成できます。

ChatGPTやClaudeにこのプロンプトを渡すと、同様の画面をゼロから生成・カスタマイズできます。列の追加や1ページの件数変更など、要件を追記して使うのがおすすめです。

※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

# ログ閲覧画面 作成依頼

## 概要
操作ログを時系列で閲覧するための読み取り専用一覧画面を作成してください。
日付範囲・種別・結果のフィルターと、結果の色分けバッジ、ページネーションを備えます。

## 要件
- フィルター:日付範囲(開始日・終了日の date input)/種別セレクト(ログイン・更新・削除・
  エクスポート)/結果セレクト(成功・失敗・警告)。すべてAND条件
- 日付は片方だけの指定も可能。開始日が終了日より後の場合は警告を表示して絞り込まない
- テーブルは 日時・種別・内容・操作者・結果 の5列。常に日時の新しい順で表示(ソートUIは不要)
- 結果は「成功(緑)・失敗(赤)・警告(黄)」のバッジで表示し、
  失敗行は薄い赤、警告行は薄い黄色の行背景を付ける
- 日時列は等幅フォントで桁を揃える
- 1ページ20件のページネーションと「全60件中 1〜20件を表示」のヒット件数表示
- 0件時は空状態メッセージと「条件をクリア」ボタンを表示する
- フィルター変更時はページを1ページ目に戻す

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- データは fetch で ./data/data.json から読み込む(logs 配列・60件想定、
  datetime は "YYYY-MM-DD HH:mm" 形式)
- 動作確認用に data.json のサンプルデータも出力してください
  (logs 配列。フィールドは id / datetime / type / detail / actor / result。
  ページ送りを確認できるよう40件以上)
- レスポンシブ対応:必要(768px以下でフィルターを縦積み、テーブルは横スクロール)

## 動作詳細
- 状態(フィルター条件・現在ページ)は単一オブジェクトで管理し、
  操作のたびに「フィルター → ページ切り出し → 描画」の順で再描画する
- 日付の絞り込みは datetime の先頭10文字と date input の値の文字列比較で行う
- テーブル行の生成は createElement と textContent を使い、innerHTML に変数を結合しない

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。