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

【Webアプリ】出勤管理アプリ⑩出退勤ページ機能拡張

「打刻」「各種申請(休日、残業、遅刻、休日出勤)」「設定(ログアウト)」の3つのタブで構成していく。

出退勤ページ機能拡張

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>出退勤管理</title>
    <script src="https://www.gstatic.com/firebasejs/10.11.0/firebase-app-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/10.11.0/firebase-auth-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/10.11.0/firebase-firestore-compat.js"></script>
    <style>
        .tab { display: none; }
        .active { display: block; }
        .section-content { display: none; margin: 10px 0; padding: 10px; border: 1px solid #ccc; }
    </style>
</head>
<body>
    <h1>出退勤管理アプリ</h1>

    <nav>
        <button onclick="showTab('tab-clock')">打刻</button>
        <button onclick="showTab('tab-application')">申請</button>
        <button onclick="showTab('tab-settings')">設定</button>
    </nav>

    <!-- 打刻タブ -->
    <div id="tab-clock" class="tab active">
        <h2>打刻</h2>
        <button onclick="recordTime('出勤')">出勤</button>
        <button onclick="recordTime('退勤')">退勤</button>
        <p id="currentTime"></p>
        <p id="result"></p>
    </div>

    <!-- 申請タブ -->
    <div id="tab-application" class="tab">
        <h2>申請</h2>

        <button onclick="toggleSection('holidaySection')">休日申請</button>
        <div id="holidaySection" class="section-content">
            <label>日付: <input type="date" id="holidayDate"></label><br>
            <label><input type="radio" name="holidayType" value="終日" checked>終日</label>
            <label><input type="radio" name="holidayType" value="午前休">午前休</label>
            <label><input type="radio" name="holidayType" value="午後休">午後休</label><br>
            <button onclick="submitHoliday()">休日申請</button>
        </div>

        <button onclick="toggleSection('lateSection')">遅刻申請</button>
        <div id="lateSection" class="section-content">
            <label>日付: <input type="date" id="lateDate"></label><br>
            <label>就業開始時間: <input type="time" id="scheduledStart"></label><br>
            <label>遅刻時間: <input type="time" id="lateTime"></label><br>
            <button onclick="submitLate()">遅刻申請</button>
        </div>

        <button onclick="toggleSection('overtimeSection')">残業申請</button>
        <div id="overtimeSection" class="section-content">
            <label>日付: <input type="date" id="overtimeDate"></label><br>
            <label>開始時刻: <input type="time" id="overtimeStart"></label>
            <label>終了時刻: <input type="time" id="overtimeEnd"></label><br>
            <p>理由:</p>
            <label><input type="checkbox" name="overtimeReason" value="時間内困難">時間内困難</label>
            <label><input type="checkbox" name="overtimeReason" value="突発対応">突発対応</label>
            <label><input type="checkbox" name="overtimeReason" value="報告書">報告書</label><br>
            <label>その他の理由:<br>
                <textarea id="overtimeOtherReason" rows="2" cols="30" placeholder="その他の理由を入力"></textarea>
            </label><br>
            <button onclick="submitOvertime()">残業申請</button>
        </div>

        <button onclick="toggleSection('workOnHolidaySection')">休日出勤申請</button>
        <div id="workOnHolidaySection" class="section-content">
            <label>出勤日: <input type="date" id="workOnHolidayDate"></label><br>
            <label>開始時間: <input type="time" id="workOnHolidayStart"></label>
            <label>終了時間: <input type="time" id="workOnHolidayEnd"></label><br>
            <label>業務内容:<br>
                <textarea id="workOnHolidayTask" rows="2" cols="30" placeholder="例: 定期システムメンテナンスなど"></textarea>
            </label><br>
            <button onclick="submitWorkOnHoliday()">休日出勤申請</button>
        </div>

        <p id="requestResult"></p>
    </div>

    <!-- 設定タブ -->
    <div id="tab-settings" class="tab">
        <h2>設定</h2>
        <button onclick="auth.signOut().then(() => alert('ログアウトしました'))">ログアウト</button>
    </div>

    <script>
        const firebaseConfig = {
            apiKey: "あなたのAPIキー",
            authDomain: "あなたのプロジェクトID.firebaseapp.com",
            projectId: "あなたのプロジェクトID",
            storageBucket: "あなたのプロジェクトID.appspot.com",
            messagingSenderId: "送信者ID",
            appId: "アプリID"
        };

        firebase.initializeApp(firebaseConfig);
        const auth = firebase.auth();
        const db = firebase.firestore();

        function showTab(tabId) {
            document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
            document.getElementById(tabId).classList.add('active');
        }

        function toggleSection(id) {
            const section = document.getElementById(id);
            section.style.display = section.style.display === 'block' ? 'none' : 'block';
        }

        function updateTime() {
            const now = new Date();
            document.getElementById('currentTime').innerText = `現在時刻: ${now.toLocaleString()}`;
        }
        setInterval(updateTime, 1000);

        function recordTime(type) {
            const user = auth.currentUser;
            if (!user) {
                document.getElementById("result").innerText = "ログインしていません。";
                return;
            }
            const now = new Date();
            db.collection("attendance").add({
                uid: user.uid,
                type: type,
                timestamp: firebase.firestore.Timestamp.fromDate(now)
            }).then(() => {
                document.getElementById("result").innerText = `${type}打刻しました (${now.toLocaleString()})`;
            }).catch((error) => {
                document.getElementById("result").innerText = `エラー: ${error.message}`;
            });
        }

        function submitHoliday() {
            const user = auth.currentUser;
            const date = document.getElementById("holidayDate").value;
            const type = document.querySelector('input[name="holidayType"]:checked').value;
            const now = new Date();

            if (!user || !date || !type) {
                document.getElementById("requestResult").innerText = "休日申請の入力に不足があります。";
                return;
            }

            db.collection("applications").add({
                uid: user.uid,
                type: "休日申請",
                date: date,
                holidayType: type,
                requestedAt: firebase.firestore.Timestamp.fromDate(now),
                status: "申請中"
            }).then(() => {
                document.getElementById("requestResult").innerText = `休日申請を送信しました(${date} - ${type})`;
            });
        }

        function submitLate() {
            const user = auth.currentUser;
            const date = document.getElementById("lateDate").value;
            const scheduledStart = document.getElementById("scheduledStart").value;
            const lateTime = document.getElementById("lateTime").value;
            const now = new Date();
            if (!user || !date || !scheduledStart || !lateTime) {
                document.getElementById("requestResult").innerText = "遅刻申請の入力に不足があります。";
                return;
            }
            db.collection("applications").add({
                uid: user.uid,
                type: "遅刻申請",
                date: date,
                scheduledStart: scheduledStart,
                lateTime: lateTime,
                requestedAt: firebase.firestore.Timestamp.fromDate(now),
                status: "申請中"
            }).then(() => {
                document.getElementById("requestResult").innerText = `遅刻申請を送信しました(${date} ${scheduledStart}→${lateTime})`;
            });
        }

        function submitOvertime() {
            const user = auth.currentUser;
            const date = document.getElementById("overtimeDate").value;
            const start = document.getElementById("overtimeStart").value;
            const end = document.getElementById("overtimeEnd").value;
            const reasons = Array.from(document.querySelectorAll('input[name="overtimeReason"]:checked')).map(el => el.value);
            const otherReason = document.getElementById("overtimeOtherReason").value;
            const now = new Date();
            if (!user || !date || !start || !end) {
                document.getElementById("requestResult").innerText = "残業申請の入力に不足があります。";
                return;
            }
            db.collection("applications").add({
                uid: user.uid,
                type: "残業申請",
                date: date,
                startTime: start,
                endTime: end,
                reasons: reasons,
                otherReason: otherReason,
                requestedAt: firebase.firestore.Timestamp.fromDate(now),
                status: "申請中"
            }).then(() => {
                document.getElementById("requestResult").innerText = `残業申請を送信しました(${date} ${start}~${end})`;
            });
        }

        function submitWorkOnHoliday() {
            const user = auth.currentUser;
            const date = document.getElementById("workOnHolidayDate").value;
            const start = document.getElementById("workOnHolidayStart").value;
            const end = document.getElementById("workOnHolidayEnd").value;
            const task = document.getElementById("workOnHolidayTask").value;
            const now = new Date();
            if (!user || !date || !start || !end || !task) {
                document.getElementById("requestResult").innerText = "休日出勤申請の入力に不足があります。";
                return;
            }
            db.collection("applications").add({
                uid: user.uid,
                type: "休日出勤申請",
                date: date,
                startTime: start,
                endTime: end,
                task: task,
                requestedAt: firebase.firestore.Timestamp.fromDate(now),
                status: "申請中"
            }).then(() => {
                document.getElementById("requestResult").innerText = `休日出勤申請を送信しました(${date} ${start}~${end})`;
            });
        }
    </script>
</body>
</html>

🧱 構造の全体概要

以下に構成と主要機能をわかりやすく解説します。

機能内容
打刻タブ出勤・退勤ボタン、現在時刻の表示
申請タブ各種申請(休日、遅刻、残業、休日出勤)をボタンで表示・非表示切替可能
設定タブFirebase認証によるログアウト機能
Firebase連携認証(firebase.auth())と Firestore(firebase.firestore())利用
JavaScript機能タブ切替・現在時刻更新・各申請内容送信

🔁 タブ機能の解説

<nav>
    <button onclick="showTab('tab-clock')">打刻</button>
    <button onclick="showTab('tab-application')">申請</button>
    <button onclick="showTab('tab-settings')">設定</button>
</nav>
  • showTab(tabId) 関数で、押したボタンに対応する <div id="tab-xxx"> のみを表示。
  • class="tab active" で表示中のタブを切り替えます。

⏰ 打刻タブの中身

<button onclick="recordTime('出勤')">出勤</button>
<button onclick="recordTime('退勤')">退勤</button>
<p id="currentTime"></p>
  • ボタンで出勤/退勤処理(※recordTime()の中身は未定義。追加が必要)。
  • setInterval(updateTime, 1000) により、1秒ごとに現在日時を更新し #currentTime に表示。

📄 申請タブ:申請の種類ごとにボタンで表示切り替え

共通点

  • 各申請には「ボタン」「入力フォーム」「送信ボタン」「処理関数」があります。
  • toggleSection(id) 関数で表示/非表示を切り替えます。

🗓 1. 休日申請

<input type="date" id="holidayDate">
<input type="radio" name="holidayType" value="終日">
...
<button onclick="submitHoliday()">休日申請</button>
  • 日付選択と「終日・午前休・午後休」から選択
  • submitHoliday() 関数は未定義(追加が必要)

⏱ 2. 遅刻申請

<input type="date" id="lateDate">
<input type="time" id="scheduledStart">
<input type="time" id="lateTime">
<button onclick="submitLate()">遅刻申請</button>
  • 遅刻の 予定出勤時刻と 実際の出勤時刻 を入力
  • submitLate() 関数によりFirestoreへ申請内容を保存
db.collection("applications").add({
    uid: user.uid,
    type: "遅刻申請",
    date: date,
    scheduledStart: scheduledStart,
    lateTime: lateTime,
    requestedAt: firebase.firestore.Timestamp.fromDate(now),
    status: "申請中"
})


⏰ 3. 残業申請

<input type="date" id="overtimeDate">
<input type="time" id="overtimeStart">
<input type="time" id="overtimeEnd">
  • 残業の 日付・時間範囲を入力
  • 理由は複数のチェックボックスと自由入力欄
  • submitOvertime() 関数は未定義(追加が必要)

💼 4. 休日出勤申請

<input type="date" id="workOnHolidayDate">
<input type="time" id="workOnHolidayStart">
<input type="time" id="workOnHolidayEnd">
<textarea id="workOnHolidayTask">
  • 日付・勤務時間帯・業務内容を入力
  • submitWorkOnHoliday() 関数は未定義(追加が必要)

⚙️ 設定タブ

<button onclick="auth.signOut().then(() => alert('ログアウトしました'))">ログアウト</button>
  • Firebase Authentication のログアウト処理を実行