Nominatim API を使って現在地の住所を取得し、出勤/退勤時に Firestore に一緒に保存していきます。
✅出退勤管理ページ改善内容
navigator.geolocation
で緯度・経度を取得- Nominatim APIで逆ジオコーディング(緯度経度 → 住所)を取得
- Firestore
attendance
コレクションに保存:uid, type, timestamp, location: { latitude, longitude, address }
📝 出退勤管理ページ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>
<script src="https://www.gstatic.com/firebasejs/10.11.0/firebase-firestore-compat.js"></script>
</head>
<body>
<h1>出退勤管理アプリ</h1>
<button onclick="recordTime('出勤')">出勤</button>
<button onclick="recordTime('退勤')">退勤</button>
<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初期化
firebase.initializeApp(firebaseConfig);
const auth = firebase.auth();
const db = firebase.firestore();
// 出勤・退勤を記録する関数
function recordTime(type) {
const resultEl = document.getElementById('result');
const user = auth.currentUser;
if (!user) {
resultEl.innerText = "ユーザーがログインしていません。";
return;
}
if (!navigator.geolocation) {
resultEl.innerText = "この端末では位置情報を取得できません。";
return;
}
navigator.geolocation.getCurrentPosition(async (position) => {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
let address = "住所不明";
try {
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}&accept-language=ja`);
const data = await response.json();
address = data.display_name || address;
} catch (error) {
console.warn("住所取得に失敗しました:", error);
}
const now = new Date();
const timeString = now.toLocaleString();
db.collection("attendance").add({
uid: user.uid,
type: type,
timestamp: firebase.firestore.Timestamp.fromDate(now),
location: {
latitude,
longitude,
address
}
})
.then(() => {
resultEl.innerText = `${type} を記録しました:${timeString}(住所:${address})`;
})
.catch((error) => {
resultEl.innerText = `エラー: ${error.message}`;
});
}, (error) => {
resultEl.innerText = "位置情報の取得に失敗しました:" + error.message;
});
}
// (必要に応じて)自動ログイン処理や認証監視をここに追加可能
// 例:
// auth.onAuthStateChanged(user => {
// if (user) { console.log("ログイン済み:", user.uid); }
// });
</script>
</body>
</html>
✅ 備考
- Nominatimは無料でAPIキーも不要ですが、1秒に1リクエストの制限があります。業務利用では独自サーバーや商用APIを検討してください。
- 表示される住所は正確さにばらつきがあるため、緯度・経度も併存することをおすすめします。
ご希望があれば:
- ログイン機能と連携した完成例
- 管理者ページでの住所表示実装
も追加可能ですので、お気軽にお知らせください。
✅ 管理者ページ改善内容
users
テーブルに「最新の住所」列を追加attendance
コレクションから最新の打刻データのlocation.address
を取得して表示- 出退勤履歴表示にも住所欄(
address
)を追加
これにより、社員一覧でも出退勤履歴でも住所が確認できます。
Firestore に保存されている住所は、以下のような構造になっている必要があります:
{
uid: "ユーザーのUID",
timestamp: Timestamp,
type: "出勤" または "退勤",
location: {
latitude: 35.x,
longitude: 139.x,
address: "東京都新宿区..." ← ここを表示
}
}
📝 管理者ページ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>
<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: "あなたのプロジェクト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 = "";
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();
const lastTimestamp = last.timestamp.toDate();
const now = new Date();
const isSameDay =
lastTimestamp.getFullYear() === now.getFullYear() &&
lastTimestamp.getMonth() === now.getMonth() &&
lastTimestamp.getDate() === now.getDate();
if (last.type === "出勤" && isSameDay) {
status = "出勤中";
} else {
status = "退勤済み";
}
}
} catch (e) {
console.warn("出退勤データ取得エラー:", e);
}
const row = `
<tr>
<td><a href="#" onclick="loadHistory('${uid}', '${user.name}'); return false;">${user.name}</a></td>
<td>${user.email}</td>
<td>${status}</td>
</tr>
`;
userList.innerHTML += row;
}
} catch (error) {
console.error("ユーザーデータ取得エラー:", error);
}
}
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 address = data.location?.address || "取得なし";
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>
<td>${address}</td>
</tr>
`;
tbody.innerHTML += row;
}
});
}
window.onload = loadUsers;
</script>
</body>
</html>