Webアプリ「出退勤管理アプリ」を作ってみた

【Webアプリ】出勤管理アプリ⑦ 機能拡張

機能拡張していきます。

🔧 改善目標(機能追加)

  1. 名前をクリックするとそのユーザーの出退勤履歴を表示
  2. 履歴は年・月で絞り込めるようにする(プルダウンなどのUI付き)
  3. 表示を見やすく整理する(インデックスや表など)

✅ 改善プラン

1. HTML構造の拡張(モーダル or 下部に履歴表示エリア)

<!-- 名前をクリックしたら履歴を表示するためのエリア -->
<div id="historyContainer" style="margin-top: 20px;">
  <h2 id="historyTitle"></h2>

  <!-- 年月選択 -->
  <label for="yearSelect">年:</label>
  <select id="yearSelect"></select>
  <label for="monthSelect">月:</label>
  <select id="monthSelect"></select>

  <table border="1" id="historyTable">
    <thead>
      <tr>
        <th>日付</th>
        <th>時刻</th>
        <th>種類</th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>
</div>

2. JavaScriptの改善(履歴表示処理追加)

以下の機能を追加:

  • 名前クリックイベント
  • 年月セレクター
  • attendanceデータのフィルタ・表示
// クリックで履歴を表示
function createUserRow(user, uid, status) {
  return `
    <tr>
      <td><a href="#" onclick="loadHistory('${uid}', '${user.name}')">${user.name}</a></td>
      <td>${user.email}</td>
      <td>${status}</td>
    </tr>
  `;
}

// 履歴表示処理
async function loadHistory(uid, name) {
  document.getElementById("historyTitle").innerText = `${name} さんの出退勤履歴`;

  // 年月セレクタ生成
  await populateYearMonthSelects(uid);

  // 初期表示(最新の年月)
  const year = document.getElementById("yearSelect").value;
  const month = document.getElementById("monthSelect").value;
  displayHistory(uid, parseInt(year), parseInt(month));
}

// 年月選択肢の作成
async function populateYearMonthSelects(uid) {
  const attendanceRef = collection(db, "attendance");
  const q = query(attendanceRef, where("uid", "==", uid), orderBy("timestamp", "desc"));
  const snapshot = await getDocs(q);

  const dates = snapshot.docs.map(doc => doc.data().timestamp.toDate());
  const years = [...new Set(dates.map(d => d.getFullYear()))];
  const yearSelect = document.getElementById("yearSelect");
  const monthSelect = document.getElementById("monthSelect");

  yearSelect.innerHTML = years.map(y => `<option value="${y}">${y}</option>`).join("");

  yearSelect.onchange = () => {
    const selectedYear = parseInt(yearSelect.value);
    const months = [...new Set(dates.filter(d => d.getFullYear() === selectedYear).map(d => d.getMonth() + 1))];
    monthSelect.innerHTML = months.map(m => `<option value="${m}">${m}</option>`).join("");
    displayHistory(uid, selectedYear, parseInt(monthSelect.value));
  };

  monthSelect.onchange = () => {
    displayHistory(uid, parseInt(yearSelect.value), parseInt(monthSelect.value));
  };

  // 初期値に対して月も更新
  yearSelect.dispatchEvent(new Event('change'));
}

// 履歴テーブルを表示
async function displayHistory(uid, year, month) {
  const attendanceRef = collection(db, "attendance");
  const q = query(attendanceRef, where("uid", "==", uid), orderBy("timestamp", "desc"));
  const snapshot = await getDocs(q);
  const tbody = document.querySelector("#historyTable tbody");
  tbody.innerHTML = "";

  snapshot.docs.forEach(doc => {
    const data = doc.data();
    const ts = data.timestamp.toDate();
    if (ts.getFullYear() === year && (ts.getMonth() + 1) === month) {
      const row = `
        <tr>
          <td>${ts.getFullYear()}-${ts.getMonth() + 1}-${ts.getDate()}</td>
          <td>${ts.getHours()}:${ts.getMinutes().toString().padStart(2, "0")}</td>
          <td>${data.type}</td>
        </tr>
      `;
      tbody.innerHTML += row;
    }
  });
}

3. loadUsers() の修正

行生成部分を次のように差し替えます:

const row = createUserRow(user, uid, status);
userList.innerHTML += row;

🧪 実装後のチェックリスト

  • 名前をクリックすると履歴が表示されるか?
  • 年・月の選択で履歴が切り替わるか?
  • 履歴が存在しない場合の表示は問題ないか?

✅ 完全統合版 HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>出退勤履歴一覧</title>
</head>
<body>
  <h1>管理者ページ</h1>

  <table border="1">
    <thead>
      <tr>
        <th>名前</th>
        <th>メールアドレス</th>
        <th>出勤状況</th>
      </tr>
    </thead>
    <tbody id="userList"></tbody>
  </table>

  <!-- 履歴表示エリア -->
  <div id="historyContainer" style="margin-top: 40px; display:none;">
    <h2 id="historyTitle"></h2>

    <div>
      <label for="yearSelect">年:</label>
      <select id="yearSelect"></select>
      <label for="monthSelect">月:</label>
      <select id="monthSelect"></select>
    </div>

    <table border="1" id="historyTable" style="margin-top: 10px;">
      <thead>
        <tr>
          <th>日付</th>
          <th>時刻</th>
          <th>種類</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </div>

  <script type="module">
    import { initializeApp } from "https://www.gstatic.com/firebasejs/10.11.0/firebase-app.js";
    import {
      getFirestore,
      collection,
      getDocs,
      query,
      where,
      orderBy,
      limit
    } from "https://www.gstatic.com/firebasejs/10.11.0/firebase-firestore.js";

    const firebaseConfig = {
        apiKey: "あなたのAPIキー",
            authDomain: "あなたのauthDomain",
            projectId: "あなたのprojectId",
            storageBucket: "あなたのstorageBucket",
            messagingSenderId: "あなたのmessagingSenderId",
            appId: "あなたのappId"
    };

    const app = initializeApp(firebaseConfig);
    const db = getFirestore(app);

    async function loadUsers() {
      const userList = document.getElementById("userList");
      userList.innerHTML = "";

      try {
        const usersSnapshot = await getDocs(collection(db, "users"));

        for (const doc of usersSnapshot.docs) {
          const user = doc.data();
          const uid = doc.id;
          let status = "不明";

          try {
            const attendanceQuery = query(
              collection(db, "attendance"),
              where("uid", "==", uid),
              orderBy("timestamp", "desc"),
              limit(1)
            );
            const attendanceSnapshot = await getDocs(attendanceQuery);

            if (!attendanceSnapshot.empty) {
              const last = attendanceSnapshot.docs[0].data();
              status = last.type === "出勤" ? "出勤中" : "退勤済み";
            }
          } catch (e) {
            console.warn("出退勤データ取得エラー:", e);
          }

          const row = createUserRow(user, uid, status);
          userList.innerHTML += row;
        }
      } catch (error) {
        console.error("ユーザーデータ取得エラー:", error);
      }
    }

    function createUserRow(user, uid, status) {
      return `
        <tr>
          <td><a href="#" onclick="loadHistory('${uid}', '${user.name}'); return false;">${user.name}</a></td>
          <td>${user.email}</td>
          <td>${status}</td>
        </tr>
      `;
    }

    window.loadHistory = async function(uid, name) {
      const historyContainer = document.getElementById("historyContainer");
      const historyTitle = document.getElementById("historyTitle");
      historyTitle.innerText = `${name} さんの出退勤履歴`;
      historyContainer.style.display = "block";

      await populateYearMonthSelects(uid);
    };

    async function populateYearMonthSelects(uid) {
      const attendanceRef = collection(db, "attendance");
      const q = query(attendanceRef, where("uid", "==", uid), orderBy("timestamp", "desc"));
      const snapshot = await getDocs(q);

      const dates = snapshot.docs.map(doc => doc.data().timestamp.toDate());
      const years = [...new Set(dates.map(d => d.getFullYear()))];
      const yearSelect = document.getElementById("yearSelect");
      const monthSelect = document.getElementById("monthSelect");

      yearSelect.innerHTML = years.map(y => `<option value="${y}">${y}</option>`).join("");

      yearSelect.onchange = () => {
        const selectedYear = parseInt(yearSelect.value);
        const months = [...new Set(dates
          .filter(d => d.getFullYear() === selectedYear)
          .map(d => d.getMonth() + 1))];
        monthSelect.innerHTML = months.map(m => `<option value="${m}">${m}</option>`).join("");
        displayHistory(uid, selectedYear, parseInt(monthSelect.value));
      };

      monthSelect.onchange = () => {
        displayHistory(uid, parseInt(yearSelect.value), parseInt(monthSelect.value));
      };

      yearSelect.dispatchEvent(new Event('change'));
    }

    async function displayHistory(uid, year, month) {
      const attendanceRef = collection(db, "attendance");
      const q = query(attendanceRef, where("uid", "==", uid), orderBy("timestamp", "desc"));
      const snapshot = await getDocs(q);
      const tbody = document.querySelector("#historyTable tbody");
      tbody.innerHTML = "";

      snapshot.docs.forEach(doc => {
        const data = doc.data();
        const ts = data.timestamp.toDate();
        if (ts.getFullYear() === year && (ts.getMonth() + 1) === month) {
          const row = `
            <tr>
              <td>${ts.getFullYear()}-${ts.getMonth() + 1}-${ts.getDate()}</td>
              <td>${ts.getHours()}:${ts.getMinutes().toString().padStart(2, "0")}</td>
              <td>${data.type}</td>
            </tr>
          `;
          tbody.innerHTML += row;
        }
      });
    }

    window.onload = loadUsers;
  </script>
</body>
</html>

🔍 機能まとめ

機能内容
🔵 出勤状況Firestoreのusersattendanceを参照し、出勤中/退勤済みを表示
🔵 名前クリック個別ユーザーの履歴を下に表示
🔵 年月プルダウン対象ユーザーの履歴から自動生成(重複除外)
🔵 履歴表示指定年月の出退勤記録を日別で一覧表示