基本的なコードは(12)までで完成。
あとは配置や操作性、装飾を微調整し、完成系が下記となります。
Contents
login.html
<!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>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>勤怠管理アプリ</h1>
<input type="email" id="email" placeholder="メールアドレス">
<input type="password" id="password" placeholder="パスワード">
<button onclick="login()">ログイン</button>
<p id="message"></p>
<script>
// Firebase 設定
const firebaseConfig = {
apiKey: "あなたのAPIキー",
authDomain: "あなたのプロジェクトID.firebaseapp.com",
projectId: "あなたのプロジェクトID",
storageBucket: "あなたのプロジェクトID.appspot.com",
messagingSenderId: "送信者ID",
appId: "アプリID"
};
// Firebase初期化
firebase.initializeApp(firebaseConfig);
const auth = firebase.auth();
// ログイン関数
function login() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
auth.signInWithEmailAndPassword(email, password)
.then(() => {
document.getElementById('message').innerText = "ログイン成功!";
// ログイン後にindex.htmlに移動する
window.location.href = "index.html";
})
.catch((error) => {
document.getElementById('message').innerText = "ログイン失敗:" + error.message;
});
}
</script>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>勤怠管理アプリ</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<link rel="stylesheet" href="style.css">
<style>
#currentTime {
font-size: 1.4em;
font-weight: bold;
color: #3333cc;
background: linear-gradient(to right, #e0f7ff, #f0f4ff);
padding: 12px 20px;
border-radius: 12px;
margin-top: 15px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
display: inline-block;
}
#currentTime .time-part {
font-size: 2em;
color: #0055cc;
font-family: 'Orbitron', sans-serif;
}
</style>
</head>
<body>
<h1>勤怠管理アプリ</h1>
<nav>
<button onclick="showTab('tab-clock')"><i class="fa-solid fa-clock icon"></i> 打刻</button>
<button onclick="showTab('tab-application')"><i class="fa-solid fa-file-signature icon"></i> 申請</button>
<button onclick="showTab('tab-settings')"><i class="fa-solid fa-gear icon"></i> 設定</button>
</nav>
<div id="tab-clock" class="tab active">
<h2>打刻</h2>
<button onclick="recordTime('出勤')">出勤</button>
<button onclick="recordTime('退勤')">退勤</button>
<br><br>
<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>
<label>申請理由:<br>
<textarea id="holidayReason" rows="2" cols="30" placeholder="有給休暇以外の申請はここに申請内容を明記する"></textarea>
</label><br>
<button onclick="submitHoliday()">申請</button>
<p id="holidayResult" class="result-message"></p>
</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>
<p id="lateResult" class="result-message"></p>
</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>
<p id="overtimeResult" class="result-message"></p>
</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>
<p id="workOnHolidayResult" class="result-message"></p>
</div>
</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();
const dateStr = now.toLocaleDateString(); // 例: 2025/6/8
const timeStr = now.toLocaleTimeString(); // 例: 14:23:11
// 時間部分だけを太く大きく強調
document.getElementById('currentTime').innerHTML =
`現在時刻: <span class="time-part">${timeStr}</span> (${dateStr})`;
}
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();
const resultEl = document.getElementById("holidayResult");
if (!user || !date || !type) {
resultEl.innerText = "休日申請の入力に不足があります。";
resultEl.style.color = "red";
return;
}
db.collection("applications").add({
uid: user.uid,
type: "休日申請",
date: date,
holidayType: type,
requestedAt: firebase.firestore.Timestamp.fromDate(now),
status: "申請中"
}).then(() => {
resultEl.innerText = `✅ 休日申請完了(${date} - ${type})`;
resultEl.style.color = "green";
}).catch((error) => {
resultEl.innerText = `❌ 申請失敗: ${error.message}`;
resultEl.style.color = "red";
});
}
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();
const resultEl = document.getElementById("lateResult");
if (!user || !date || !scheduledStart || !lateTime) {
resultEl.innerText = "遅刻申請の入力に不足があります。";
resultEl.style.color = "red";
return;
}
db.collection("applications").add({
uid: user.uid,
type: "遅刻申請",
date: date,
scheduledStart: scheduledStart,
lateTime: lateTime,
requestedAt: firebase.firestore.Timestamp.fromDate(now),
status: "申請中"
}).then(() => {
resultEl.innerText = `✅ 遅刻申請完了(${date} ${scheduledStart}→${lateTime})`;
resultEl.style.color = "green";
}).catch((error) => {
resultEl.innerText = `❌ 申請失敗: ${error.message}`;
resultEl.style.color = "red";
});
}
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();
const resultEl = document.getElementById("overtimeResult");
if (!user || !date || !start || !end) {
resultEl.innerText = "残業申請の入力に不足があります。";
resultEl.style.color = "red";
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(() => {
resultEl.innerText = `✅ 残業申請完了(${date} ${start}~${end})`;
resultEl.style.color = "green";
}).catch((error) => {
resultEl.innerText = `❌ 申請失敗: ${error.message}`;
resultEl.style.color = "red";
});
}
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();
const resultEl = document.getElementById("workOnHolidayResult");
if (!user || !date || !start || !end || !task) {
resultEl.innerText = "休日出勤申請の入力に不足があります。";
resultEl.style.color = "red";
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(() => {
resultEl.innerText = `✅ 休日出勤申請完了(${date} ${start}~${end})`;
resultEl.style.color = "green";
}).catch((error) => {
resultEl.innerText = `❌ 申請失敗: ${error.message}`;
resultEl.style.color = "red";
});
}
// ▼ 自動ログアウト処理 ▼
// 何分で自動ログアウトするか(例: 15分 = 15 * 60 * 1000 ミリ秒)
const AUTO_LOGOUT_TIME = 15 * 60 * 1000;
let logoutTimer;
// ログアウト処理
function autoLogout() {
auth.signOut().then(() => {
alert("15分間操作がなかったため、自動ログアウトしました。");
window.location.href = "login.html"; // ログインページへリダイレクト
});
}
// タイマーリセット処理
function resetLogoutTimer() {
if (logoutTimer) clearTimeout(logoutTimer);
logoutTimer = setTimeout(autoLogout, AUTO_LOGOUT_TIME);
}
// ユーザー操作を検知してタイマーをリセット
["click", "mousemove", "keydown", "scroll", "touchstart"].forEach(event => {
document.addEventListener(event, resetLogoutTimer);
});
// サインイン状態確認後、タイマー起動
auth.onAuthStateChanged(user => {
if (user) {
resetLogoutTimer(); // 初回起動
}
});
</script>
</body>
</html>
admin_login.html
<!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>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>管理者ログインページ</h1>
<input type="email" id="adminEmail" placeholder="メールアドレス"><br>
<input type="password" id="adminPassword" placeholder="パスワード"><br>
<button onclick="adminLogin()">ログイン</button>
<p id="adminResult"></p>
<script>
// Firebase設定
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();
// 管理者ログイン関数
function adminLogin() {
const email = document.getElementById('adminEmail').value;
const password = document.getElementById('adminPassword').value;
auth.signInWithEmailAndPassword(email, password)
.then((userCredential) => {
// 管理者かどうかチェック(メールアドレスで判断)
if (email === "⚪︎⚪︎⚪︎⚪︎⚪︎@⚪︎⚪︎⚪︎.com") { // ここを書き換える!
window.location.href = "admin_attendance.html"; // 管理者ページに移動
} else {
document.getElementById('adminResult').innerText = "管理者アカウントではありません。";
auth.signOut(); // すぐログアウト
}
})
.catch((error) => {
document.getElementById('adminResult').innerText = "ログインエラー:" + error.message;
});
}
</script>
</body>
</html>
admin_attendance.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>出勤状況一覧</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>出勤状況一覧</h1>
<a href="admin_requests.html">▶ 申請管理ページへ</a>
<table border="1">
<thead>
<tr>
<th>名前</th>
<th>メールアドレス</th>
<th>出勤状況</th>
<th>詳細</th>
</tr>
</thead>
<tbody id="userList"></tbody>
</table>
<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: "あなたのプロジェクトID.firebaseapp.com",
projectId: "あなたのプロジェクトID",
storageBucket: "あなたのプロジェクトID.appspot.com",
messagingSenderId: "送信者ID",
appId: "アプリID"
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
async function loadUsers() {
const userList = document.getElementById("userList");
userList.innerHTML = "";
const usersSnapshot = await getDocs(collection(db, "users"));
for (const docSnap of usersSnapshot.docs) {
const user = docSnap.data();
const uid = docSnap.id;
let status = "不明";
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();
const lastTimestamp = last.timestamp.toDate();
const now = new Date();
const isSameDay =
lastTimestamp.getFullYear() === now.getFullYear() &&
lastTimestamp.getMonth() === now.getMonth() &&
lastTimestamp.getDate() === now.getDate();
status = (last.type === "出勤" && isSameDay) ? "出勤中" : "退勤済み";
}
const detailLink = `<a href="user_detail.html?uid=${uid}&name=${encodeURIComponent(user.name)}" target="_blank">詳細</a>`;
const row = `
<tr>
<td>${user.name}</td>
<td>${user.email}</td>
<td>${status}</td>
<td>${detailLink}</td>
</tr>
`;
userList.innerHTML += row;
}
}
window.onload = () => {
loadUsers();
};
</script>
</body>
</html>
admin_requests.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>申請管理(申請中のみ表示)</title>
<style>
body {
font-family: "Helvetica Neue", sans-serif;
background-color: #f2f4f8;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
}
.application-card {
border: 1px solid #ccc;
border-left: 6px solid #007BFF;
border-radius: 8px;
background-color: #fff;
margin: 20px auto;
padding: 20px;
max-width: 600px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.application-header {
font-size: 1.3em;
font-weight: bold;
margin-bottom: 10px;
color: #2c3e50;
}
.application-content {
font-size: 0.95em;
margin-bottom: 16px;
color: #444;
}
.application-buttons button {
margin-right: 10px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
}
.approve-button {
background-color: #28a745;
color: white;
}
.reject-button {
background-color: #dc3545;
color: white;
}
.application-footer {
font-size: 0.85em;
color: #888;
}
</style>
</head>
<body>
<h1>申請管理ページ(申請中)</h1>
<a href="admin_attendance.html">▶ 出勤状況一覧ページへ</a>
<div id="applicationList"></div>
<!-- Firebase SDK -->
<script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-firestore-compat.js"></script>
<script>
const firebaseConfig = {
apiKey: "あなたのAPIキー",
authDomain: "あなたのプロジェクトID.firebaseapp.com",
projectId: "あなたのプロジェクトID",
storageBucket: "あなたのプロジェクトID.appspot.com",
messagingSenderId: "送信者ID",
appId: "アプリID"
};
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
async function loadApplications() {
const usersSnapshot = await db.collection("users").get();
const userMap = {};
usersSnapshot.forEach(doc => {
userMap[doc.id] = doc.data().name || doc.data().displayName || "不明";
});
const querySnapshot = await db.collection("applications")
.where("status", "==", "申請中")
.orderBy("requestedAt", "desc")
.get();
const container = document.getElementById("applicationList");
container.innerHTML = "";
querySnapshot.forEach(doc => {
const data = doc.data();
const userName = userMap[data.uid] || "不明";
renderApplication(doc, userName);
});
}
function renderApplication(doc, userName) {
const data = doc.data();
const card = document.createElement("div");
card.className = "application-card";
const header = document.createElement("div");
header.className = "application-header";
header.textContent = `${userName}(${data.type})`;
const content = document.createElement("div");
content.className = "application-content";
content.innerHTML = `
<strong>日付:</strong> ${data.date || "未入力"}<br>
<strong>詳細:</strong> ${formatDetails(data)}<br>
<strong>ステータス:</strong> ${data.status}
`;
const buttons = document.createElement("div");
buttons.className = "application-buttons";
const approveBtn = document.createElement("button");
approveBtn.className = "approve-button";
approveBtn.textContent = "許可";
approveBtn.onclick = () => updateApplicationStatus(doc.id, "許可");
const rejectBtn = document.createElement("button");
rejectBtn.className = "reject-button";
rejectBtn.textContent = "却下";
rejectBtn.onclick = () => updateApplicationStatus(doc.id, "却下");
buttons.appendChild(approveBtn);
buttons.appendChild(rejectBtn);
const footer = document.createElement("div");
footer.className = "application-footer";
footer.textContent = `申請日時: ${data.requestedAt?.toDate().toLocaleString() || "不明"}`;
card.appendChild(header);
card.appendChild(content);
card.appendChild(buttons);
card.appendChild(footer);
document.getElementById("applicationList").appendChild(card);
}
function formatDetails(data) {
switch (data.type) {
case "休日申請":
return `種類: ${data.holidayType || ""}`;
case "遅刻申請":
return `予定: ${data.scheduledStart} → 実際: ${data.lateTime}`;
case "残業申請":
return `時間: ${data.startTime} ~ ${data.endTime}<br>理由: ${(data.reasons || []).join(", ")} ${data.otherReason || ""}`;
case "休日出勤申請":
return `時間: ${data.startTime} ~ ${data.endTime}<br>内容: ${data.task}`;
default:
return "詳細情報なし";
}
}
async function updateApplicationStatus(docId, newStatus) {
try {
await db.collection("applications").doc(docId).update({
status: newStatus
});
alert(`ステータスを「${newStatus}」に更新しました`);
loadApplications();
} catch (error) {
console.error("ステータス更新エラー:", error);
alert("更新に失敗しました");
}
}
loadApplications();
</script>
</body>
</html>
user_detail.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>ユーザー詳細 </title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>ユーザー詳細ページ</h1>
<button onclick="location.href = 'admin_attendance.html'">戻る</button>
<section>
<h2>ユーザー情報</h2>
<p><strong>名前:</strong> <span id="userName">-</span></p>
<p><strong>メール:</strong> <span id="userEmail">-</span></p>
<p><strong>UID:</strong> <span id="userId">-</span></p>
</section>
<section>
<h2>出退勤履歴</h2>
<ul id="attendanceList"></ul>
</section>
<section>
<h2>申請履歴</h2>
<ul id="applicationList"></ul>
</section>
<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();
auth.onAuthStateChanged(currentUser => {
if (!currentUser) {
alert("ログインしていません。ログインページに戻ります。");
location.href = 'admin-login.html';
return;
}
const params = new URLSearchParams(location.search);
const uid = params.get("uid");
const name = decodeURIComponent(params.get("name") || "");
if (!uid) {
alert("UIDが指定されていません。");
return;
}
document.getElementById("userName").innerText = name || "(不明)";
document.getElementById("userId").innerText = uid;
// Firestoreからメールアドレス取得
db.collection("users").doc(uid).get().then(doc => {
if (doc.exists) {
document.getElementById("userEmail").innerText = doc.data().email || "(不明)";
} else {
document.getElementById("userEmail").innerText = "(データなし)";
}
});
// 出退勤履歴取得
db.collection("attendance")
.where("uid", "==", uid)
.orderBy("timestamp", "desc")
.limit(30)
.get()
.then(snapshot => {
const list = document.getElementById("attendanceList");
list.innerHTML = "";
snapshot.forEach(doc => {
const data = doc.data();
const date = data.timestamp?.toDate().toLocaleString() || "(不明)";
const li = document.createElement("li");
li.textContent = `${data.type}:${date}`;
list.appendChild(li);
});
});
// 申請履歴取得
db.collection("applications")
.where("uid", "==", uid)
.orderBy("requestedAt", "desc")
.limit(30)
.get()
.then(snapshot => {
const list = document.getElementById("applicationList");
list.innerHTML = "";
snapshot.forEach(doc => {
const data = doc.data();
const date = data.date || "-";
const type = data.type || "-";
const status = data.status || "-";
const li = document.createElement("li");
li.textContent = `${type}(${date}) - ステータス: ${status}`;
list.appendChild(li);
});
});
});
</script>
</body>
</html>
qr-attendance.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>QRコード出退勤打刻</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-firestore-compat.js"></script>
<script src="https://unpkg.com/@zxing/library@latest"></script>
<style>
body {
font-family: sans-serif;
text-align: center;
padding: 20px;
}
video {
border: 1px solid #ccc;
width: 300px;
height: 200px;
}
button {
margin: 10px;
padding: 10px 20px;
font-size: 16px;
}
#result {
margin-top: 20px;
font-size: 16px;
color: green;
}
#clock {
font-size: 18px;
margin-bottom: 10px;
color: #555;
}
</style>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>すまログ(仮) QRコード打刻</h1>
<div id="clock">--:--:--</div>
<video id="preview" autoplay muted playsinline></video>
<div>
<button onclick="setType('出勤')">出勤モード</button>
<button onclick="setType('退勤')">退勤モード</button>
</div>
<p id="result">モードを選択してください</p>
<script>
// Firebase 設定
const firebaseConfig = {
apiKey: "あなたのAPIキー",
authDomain: "あなたのプロジェクトID.firebaseapp.com",
projectId: "あなたのプロジェクトID",
storageBucket: "あなたのプロジェクトID.appspot.com",
messagingSenderId: "送信者ID",
appId: "アプリID"
};
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
let currentType = null;
let codeReader = null;
let cameraTimeout = null; // タイマー用変数
// 時計表示
function updateClock() {
const now = new Date();
const timeStr = now.toLocaleString('ja-JP', { hour12: false });
document.getElementById("clock").textContent = timeStr;
}
setInterval(updateClock, 1000);
updateClock();
function setType(type) {
// 前回のカメラを停止
if (codeReader) {
codeReader.reset();
}
if (cameraTimeout) {
clearTimeout(cameraTimeout);
}
currentType = type;
document.getElementById("result").innerText = `${type}モードです。QRコードをかざしてください。`;
startCamera();
}
function startCamera() {
const previewElement = document.getElementById('preview');
codeReader = new ZXing.BrowserQRCodeReader();
// 1分後に自動停止
cameraTimeout = setTimeout(() => {
codeReader.reset();
document.getElementById("result").innerText = "カメラは1分後に自動停止しました。再度モードを選択してください。";
}, 60000);
codeReader.decodeOnceFromVideoDevice(undefined, previewElement)
.then(async (result) => {
clearTimeout(cameraTimeout);
const uid = result.text;
const nowDate = new Date();
let latitude = null;
let longitude = null;
let address = "住所不明";
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(async (pos) => {
latitude = pos.coords.latitude;
longitude = pos.coords.longitude;
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ja`);
const data = await res.json();
address = data.display_name || address;
} catch (e) {
console.warn("住所取得に失敗:", e);
}
saveAttendance(uid, nowDate, latitude, longitude, address);
}, (err) => {
console.warn("位置情報取得失敗:", err);
saveAttendance(uid, nowDate, latitude, longitude, address);
});
} else {
saveAttendance(uid, nowDate, latitude, longitude, address);
}
document.getElementById("result").innerText = `${uid} の ${currentType} を記録中...`;
codeReader.reset();
})
.catch((err) => {
clearTimeout(cameraTimeout);
console.error("QRコードの読み取りに失敗:", err);
document.getElementById("result").innerText = "QRコードの読み取りに失敗しました。もう一度試してください。";
});
}
function saveAttendance(uid, now, latitude, longitude, address) {
db.collection("attendance").add({
uid: uid,
type: currentType,
timestamp: firebase.firestore.Timestamp.fromDate(now),
location: {
latitude,
longitude,
address
}
})
.then(() => {
document.getElementById("result").innerText = `${uid} の ${currentType} を記録しました(${now.toLocaleString()})`;
})
.catch((error) => {
document.getElementById("result").innerText = `記録に失敗: ${error.message}`;
});
}
</script>
</body>
</html>
style.css
/* style.css */
* {
box-sizing: border-box;
font-family: 'Segoe UI', 'Helvetica Neue', sans-serif;
scroll-behavior: smooth;
}
body {
background: linear-gradient(to right, #f0f4ff, #e8f1ff);
color: #2c3e50;
margin: 0;
padding: 0;
line-height: 1.6;
}
h1 {
text-align: center;
background: linear-gradient(to right, #007bff, #0056b3);
color: #fff;
margin: 0;
padding: 1.5rem;
font-size: 2rem;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.4);
}
nav {
display: flex;
overflow-x: auto;
background-color: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
padding: 10px;
gap: 12px;
justify-content: center;
border-bottom: 1px solid #ccc;
}
nav button {
background: linear-gradient(to bottom right, #007bff, #3399ff);
color: #fff;
border: none;
padding: 10px 18px;
font-size: 1rem;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.2);
transition: background 0.3s, transform 0.2s;
flex-shrink: 0;
}
nav button:hover {
background: #0056b3;
transform: translateY(-2px);
}
.tab {
display: none;
padding: 1.5rem;
animation: fadeIn 0.3s ease-in-out;
}
.tab.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.section-content {
margin: 20px auto;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 16px rgba(0,0,0,0.05);
max-width: 800px;
}
input, select, textarea {
margin-top: 6px;
margin-bottom: 14px;
padding: 10px;
border-radius: 6px;
border: 1px solid #ccc;
width: 100%;
font-size: 1rem;
transition: border-color 0.3s;
}
input:focus, select:focus, textarea:focus {
border-color: #007bff;
outline: none;
}
button {
background: linear-gradient(to bottom right, #007bff, #3399ff);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
box-shadow: 0 3px 6px rgba(0,123,255,0.2);
transition: background 0.3s, transform 0.2s;
}
button:hover {
background: #0056b3;
transform: scale(1.02);
}
#requestResult, #result {
margin-top: 1rem;
font-weight: bold;
color: #007bff;
}
.icon {
font-size: 1.3rem;
}
label {
display: block;
margin-bottom: 6px;
font-weight: bold;
color: #333;
}
@media screen and (max-width: 600px) {
h1 {
font-size: 1.5rem;
padding: 1rem;
}
nav {
justify-content: flex-start;
padding: 8px;
}
nav button {
font-size: 0.9rem;
padding: 8px 14px;
}
.section-content {
padding: 16px;
margin: 10px;
}
input, select, textarea, button {
font-size: 1rem;
}
}