GoogleのAIアシスタント Gemini
ChatGPTはGoogle検索の代わりに使っていましたがGeminiはGoogleWorkplaceに統合されるようになってからGメールで推敲など使用することがありましたが、今回GeminiのCanvasを使ってみて、ちょっとしたWebアプリを作ってみたのでコードも含め公開します。
Webアプリ名:Mileage Log
モバイルでの使用が想定の車の走行距離を記録して管理するWebアプリで下記の機能があります。
- ライトモード・ダークモード・システム設定を選択できる外観モード設定
- 車両は何台でも追加可能(車両毎に走行距離を記録できる)
- 出発時は、出発地のGPS座標・出発地・出発時の走行距離を入力
- 到着時は、到着地のGPS座標・到着地・到着時の走行距離・用件を入力
- 走行履歴の閲覧(年月別及び車両で絞り込み可能)
- Excelにエクスポート(後からPCで編集できるように)
- データはブラウザに保管する仕様(キャッシュを削除するとデータが消えます)
作ってみた感想
これはすごい便利だね、アイデアがあれば色んなアプリができそう。
HTMLコード
GeminiのCanvasで対話しながら作ったコードです。
良ければコピーして使用してください。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mileage Log</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Excel出力ライブラリを追加 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
/* 簡単なアニメーション効果 */
.screen {
display: none;
animation: fadeIn 0.5s;
}
.screen.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* NEW STYLISH BUTTONS */
.btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem; /* rounded-xl */
font-weight: bold;
color: white;
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
transition: all 0.15s ease-out;
border: none;
cursor: pointer;
border-bottom: 4px solid;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
-webkit-tap-highlight-color: transparent; /* For mobile tap */
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
.btn:active {
transform: translateY(0);
box-shadow: inset 0 3px 5px rgba(0,0,0,0.25);
border-bottom-width: 0px;
padding-top: calc(0.75rem + 4px); /* Maintain size when border is removed */
}
.btn-sm {
padding: 0.5rem 0.75rem;
font-size: 0.875rem; /* text-sm */
border-bottom-width: 3px;
}
.btn-sm:active {
padding-top: calc(0.5rem + 3px);
}
.btn-icon {
width: auto;
padding: 0.5rem;
border-radius: 9999px;
}
/* --- Modern Adaptive Button Colors (Light Theme) --- */
.btn-primary {
background: linear-gradient(to top, #3b82f6, #60a5fa); /* blue-500, blue-400 */
border-color: #2563eb; /* blue-600 */
}
.btn-secondary {
background: linear-gradient(to top, #e5e7eb, #f9fafb); /* gray-200, gray-50 */
border-color: #d1d5db; /* gray-300 */
color: #374151; /* gray-700 */
text-shadow: none;
}
.btn-danger {
background: linear-gradient(to top, #ef4444, #f87171); /* red-500, red-400 */
border-color: #dc2626; /* red-600 */
}
.btn-green {
background: linear-gradient(to top, #16a34a, #4ade80); /* green-600, green-400 */
border-color: #15803d; /* green-700 */
}
.btn[disabled] {
background: linear-gradient(to top, #9ca3af, #d1d5db);
border-color: #9ca3af;
cursor: not-allowed;
box-shadow: none;
transform: translateY(0);
}
/* NEW 3D INPUT FIELD */
.input-field {
margin-top: 0.25rem;
display: block;
width: 100%;
padding: 0.75rem;
background-color: #ffffff;
border: 1px solid #d1d5db; /* border-gray-300 */
border-radius: 0.5rem; /* rounded-lg */
font-size: 0.875rem; /* text-sm */
color: #1f2937;
/* Creates the 3D "inset" effect */
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease-in-out, border-color 0.2s ease-in-out;
-webkit-appearance: none; /* remove default iOS styling */
}
.input-field::placeholder {
color: #9ca3af; /* placeholder-gray-400 */
}
.input-field:focus {
outline: none;
border-color: #3b82f6; /* focus:border-blue-500 */
/* Add a glow effect on focus */
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
.label { @apply block text-sm font-medium text-gray-700; }
/* スピナー */
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* --- Modern Header --- */
.header-bg {
background: linear-gradient(to right, #3b82f6, #60a5fa);
}
/* ダークモード対応 */
html.dark body {
background-color: #111827; /* Tailwind gray-900 */
}
html.dark .header-bg {
background: linear-gradient(to right, #1e3a8a, #2563eb);
}
html.dark #app, html.dark .bg-white {
background-color: #1f2937; /* Tailwind gray-800 */
}
html.dark h2, html.dark .font-semibold, html.dark .font-bold, html.dark .text-gray-800 {
color: #f9fafb; /* Tailwind gray-50 */
}
html.dark .text-gray-700, html.dark .text-gray-600, html.dark .label {
color: #d1d5db; /* Tailwind gray-300 */
}
html.dark .text-gray-500 {
color: #9ca3af; /* Tailwind gray-400 */
}
html.dark .bg-gray-50 {
background-color: #374151; /* Tailwind gray-700 */
}
html.dark .input-field, html.dark select.input-field {
background-color: #4b5563; /* gray-600 */
border-color: #6b7280; /* gray-500 */
color: #f9fafb; /* gray-50 */
}
html.dark .input-field::placeholder {
color: #9ca3af; /* gray-400 */
}
html.dark .input-field:focus {
background-color: #4b5563;
border-color: #60a5fa; /* blue-400 */
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.4);
}
html.dark #start-location-display, html.dark #end-location-display, html.dark .bg-gray-200 {
background-color: #4b5563; /* gray-600 */
color: #e5e7eb; /* gray-200 */
}
html.dark .bg-blue-50 {
background-color: #1e3a8a; /* A darker blue */
}
html.dark .text-blue-800, html.dark .text-blue-700 {
color: #bfdbfe; /* A lighter blue */
}
html.dark #message-modal > div, html.dark #confirm-modal > div {
background-color: #374151; /* gray-700 */
}
/* --- Modern Adaptive Button Colors (Dark Theme) --- */
html.dark .btn-primary {
background: linear-gradient(to top, #60a5fa, #93c5fd); /* blue-400, blue-300 */
border-color: #3b82f6; /* blue-500 */
color: #1e40af; /* blue-800 */
text-shadow: none;
}
html.dark .btn-secondary {
background: linear-gradient(to top, #4b5563, #6b7280); /* gray-600, gray-500 */
border-color: #374151; /* gray-700 */
color: #f9fafb; /* gray-50 */
}
html.dark .btn-danger {
background: linear-gradient(to top, #f87171, #fca5a5); /* red-400, red-300 */
border-color: #ef4444; /* red-500 */
color: #7f1d1d; /* red-900 */
text-shadow: none;
}
html.dark .btn-green {
background: linear-gradient(to top, #22c55e, #86efac); /* green-500, green-300 */
border-color: #16a34a; /* green-600 */
color: #064e3b; /* green-900 */
text-shadow: none;
}
</style>
</head>
<body class="bg-gray-100 font-sans">
<div id="app" class="max-w-md mx-auto bg-white shadow-lg min-h-screen">
<!-- Header -->
<header class="flex items-center justify-between text-white p-4 sticky top-0 z-20 shadow-md header-bg">
<button id="settings-btn" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/20 transition" title="設定">
<i class="fas fa-cog"></i>
</button>
<div class="text-center">
<h1 class="text-xl font-bold">Mileage Log</h1>
<p id="current-vehicle-display" class="text-sm mt-1"></p>
</div>
<button id="reload-btn" class="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/20 transition" title="リロード">
<i class="fas fa-sync-alt"></i>
</button>
</header>
<!-- Main Content -->
<main class="p-4">
<!-- 1. 車両選択画面 -->
<div id="vehicle-selection-screen" class="screen active">
<h2 class="text-lg font-semibold mb-4 text-gray-800">車両を選択してください</h2>
<div id="vehicle-list" class="space-y-3 mb-4">
<!-- 車両リストがここに挿入されます -->
</div>
<div class="space-y-3">
<button id="show-add-vehicle-screen-btn" class="btn btn-primary"><i class="fas fa-plus mr-2"></i>新しい車両を追加</button>
<button id="show-logs-btn" class="btn btn-secondary"><i class="fas fa-history mr-2"></i>走行履歴を確認</button>
</div>
</div>
<!-- 2. 車両追加画面 -->
<div id="add-vehicle-screen" class="screen">
<h2 class="text-lg font-semibold mb-4 text-gray-800">新しい車両を追加</h2>
<form id="add-vehicle-form">
<div>
<label for="vehicle-name" class="label">車両名 (例: プリウス)</label>
<input type="text" id="vehicle-name" class="input-field" required>
</div>
<div class="mt-4">
<label for="vehicle-license" class="label">ナンバープレート (例: 品川 300 あ 12-34)</label>
<input type="text" id="vehicle-license" class="input-field" required>
</div>
<div class="mt-6 space-y-3">
<button type="submit" class="btn btn-primary"><i class="fas fa-check mr-2"></i>追加</button>
<button type="button" id="cancel-add-vehicle-btn" class="btn btn-secondary"><i class="fas fa-times mr-2"></i>キャンセル</button>
</div>
</form>
</div>
<!-- 3. 走行記録画面 -->
<div id="trip-screen" class="screen">
<!-- 出発記録エリア -->
<div id="start-trip-section">
<h2 class="text-lg font-semibold mb-2 text-gray-800">出発記録</h2>
<div class="bg-gray-50 p-4 rounded-lg shadow-inner">
<p class="text-sm text-gray-600 mb-4">出発情報を入力してください。</p>
<button id="get-location-start-btn" class="btn btn-primary mb-3"><i class="fas fa-map-marker-alt mr-2"></i>出発地のGPS座標を取得</button>
<div id="start-location-loader" class="hidden my-2"><div class="loader mx-auto"></div></div>
<div>
<label class="label">出発地GPS座標</label>
<div id="start-location-display" class="mt-2 p-3 bg-gray-200 rounded-lg text-center text-sm text-gray-700">位置情報未取得</div>
</div>
<div class="mt-4">
<label for="start-location-manual" class="label">出発地 (手入力)</label>
<input type="text" id="start-location-manual" class="input-field" placeholder="例: 本社" required>
</div>
<div class="mt-4">
<label for="start-odometer" class="label">出発時の走行距離 (km)</label>
<input type="number" id="start-odometer" class="input-field" placeholder="数値を入力" required>
</div>
<div class="mt-6 space-y-3">
<button id="start-trip-btn" class="btn btn-primary"><i class="fas fa-play-circle mr-2"></i>出発記録を開始</button>
<button id="back-to-vehicle-selection-btn" class="btn btn-secondary"><i class="fas fa-chevron-left mr-2"></i>車両選択に戻る</button>
</div>
</div>
</div>
<!-- 到着記録エリア -->
<div id="end-trip-section" class="hidden">
<h2 class="text-lg font-semibold mb-2 text-gray-800">到着記録</h2>
<div class="bg-blue-50 p-4 rounded-lg border border-blue-200 mb-4">
<h3 class="font-semibold text-blue-800">現在の走行情報</h3>
<p class="text-sm text-blue-700"><strong>出発地:</strong> <span id="active-trip-start-location"></span></p>
<p class="text-sm text-blue-700"><strong>出発時刻:</strong> <span id="active-trip-start-time"></span></p>
<p class="text-sm text-blue-700"><strong>出発時距離:</strong> <span id="active-trip-start-odometer"></span> km</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg shadow-inner">
<p class="text-sm text-gray-600 mb-4">到着情報を入力してください。</p>
<button id="get-location-end-btn" class="btn btn-primary mb-3"><i class="fas fa-map-marker-alt mr-2"></i>到着地のGPS座標を取得</button>
<div id="end-location-loader" class="hidden my-2"><div class="loader mx-auto"></div></div>
<div>
<label class="label">到着地GPS座標</label>
<div id="end-location-display" class="mt-2 p-3 bg-gray-200 rounded-lg text-center text-sm text-gray-700">位置情報未取得</div>
</div>
<div class="mt-4">
<label for="end-location-manual" class="label">到着地 (手入力)</label>
<input type="text" id="end-location-manual" class="input-field" placeholder="例: B社 現場" required>
</div>
<div class="mt-4">
<label for="purpose" class="label">用件</label>
<input type="text" id="purpose" class="input-field" placeholder="例: A社訪問" required>
</div>
<div class="mt-4">
<label for="end-odometer" class="label">到着時の走行距離 (km)</label>
<input type="number" id="end-odometer" class="input-field" placeholder="数値を入力" required>
</div>
<div class="mt-6 space-y-3">
<button id="end-trip-btn" class="btn btn-primary"><i class="fas fa-check-circle mr-2"></i>走行を完了する</button>
<button id="cancel-trip-btn" class="btn btn-danger"><i class="fas fa-times-circle mr-2"></i>この走行をキャンセル</button>
</div>
</div>
</div>
</div>
<!-- 4. 走行履歴画面 -->
<div id="logs-screen" class="screen">
<h2 class="text-lg font-semibold text-gray-800 mb-4">走行履歴</h2>
<div class="grid grid-cols-2 gap-2 mb-4">
<div>
<label for="log-year-filter" class="label">年:</label>
<select id="log-year-filter" class="input-field"></select>
</div>
<div>
<label for="log-month-filter" class="label">月:</label>
<select id="log-month-filter" class="input-field"></select>
</div>
</div>
<div class="mb-4">
<label for="vehicle-filter" class="label">車両で絞り込み:</label>
<select id="vehicle-filter" class="input-field">
<!-- Options will be populated by JS -->
</select>
</div>
<div id="logs-list" class="space-y-3">
<!-- ログリストがここに挿入されます -->
</div>
<div class="mt-6 space-y-3">
<button id="export-excel-btn" class="btn btn-green"><i class="fas fa-file-excel mr-2"></i>Excelエクスポート</button>
<button id="back-to-main-from-logs-btn" class="btn btn-secondary"><i class="fas fa-chevron-left mr-2"></i>戻る</button>
</div>
</div>
<!-- 5. 設定画面 -->
<div id="settings-screen" class="screen">
<h2 class="text-lg font-semibold mb-4 text-gray-800">設定</h2>
<div class="space-y-4">
<div>
<label class="label">外観モード</label>
<div class="mt-2 grid grid-cols-3 gap-2" id="theme-selector">
<button data-theme="light" class="theme-btn btn btn-secondary"><i class="fas fa-sun mr-2"></i>ライト</button>
<button data-theme="dark" class="theme-btn btn btn-secondary"><i class="fas fa-moon mr-2"></i>ダーク</button>
<button data-theme="system" class="theme-btn btn btn-secondary"><i class="fas fa-desktop mr-2"></i>システム</button>
</div>
</div>
</div>
<div class="mt-8">
<button id="back-to-main-from-settings-btn" class="btn btn-secondary"><i class="fas fa-chevron-left mr-2"></i>戻る</button>
</div>
</div>
<!-- 6. 履歴編集画面 -->
<div id="edit-log-screen" class="screen">
<h2 class="text-lg font-semibold mb-4 text-gray-800">走行履歴を編集</h2>
<form id="edit-log-form" class="space-y-4">
<input type="hidden" id="edit-log-id">
<div>
<label for="edit-start-datetime" class="label">出発日時</label>
<input type="datetime-local" id="edit-start-datetime" class="input-field" required>
</div>
<div>
<label for="edit-start-location-manual" class="label">出発地</label>
<input type="text" id="edit-start-location-manual" class="input-field" required>
</div>
<div>
<label for="edit-start-odometer" class="label">出発時 走行距離 (km)</label>
<input type="number" id="edit-start-odometer" class="input-field" required>
</div>
<div>
<label for="edit-end-datetime" class="label">到着日時</label>
<input type="datetime-local" id="edit-end-datetime" class="input-field">
</div>
<div>
<label for="edit-end-location-manual" class="label">到着地</label>
<input type="text" id="edit-end-location-manual" class="input-field">
</div>
<div>
<label for="edit-end-odometer" class="label">到着時 走行距離 (km)</label>
<input type="number" id="edit-end-odometer" class="input-field">
</div>
<div>
<label for="edit-purpose" class="label">用件</label>
<input type="text" id="edit-purpose" class="input-field">
</div>
<div class="mt-6 space-y-3">
<button type="submit" class="btn btn-primary"><i class="fas fa-save mr-2"></i>保存</button>
<button type="button" id="cancel-edit-log-btn" class="btn btn-secondary"><i class="fas fa-times mr-2"></i>キャンセル</button>
</div>
</form>
</div>
</main>
<!-- メッセージ表示用モーダル -->
<div id="message-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white p-6 rounded-lg shadow-xl max-w-sm mx-auto text-center">
<p id="message-text" class="text-gray-800 mb-4"></p>
<button id="close-message-btn" class="btn btn-primary">閉じる</button>
</div>
</div>
<!-- 確認モーダル -->
<div id="confirm-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
<div class="bg-white p-6 rounded-lg shadow-xl max-w-sm mx-auto text-center">
<p id="confirm-text" class="text-gray-800 mb-4"></p>
<div class="w-full space-y-3">
<button id="confirm-ok-btn" class="btn btn-danger">はい</button>
<button id="confirm-cancel-btn" class="btn btn-secondary">いいえ</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- DOM要素の取得 ---
const screens = document.querySelectorAll('.screen');
const vehicleSelectionScreen = document.getElementById('vehicle-selection-screen');
const addVehicleScreen = document.getElementById('add-vehicle-screen');
const tripScreen = document.getElementById('trip-screen');
const logsScreen = document.getElementById('logs-screen');
const settingsScreen = document.getElementById('settings-screen');
const editLogScreen = document.getElementById('edit-log-screen');
const vehicleList = document.getElementById('vehicle-list');
const addVehicleForm = document.getElementById('add-vehicle-form');
const currentVehicleDisplay = document.getElementById('current-vehicle-display');
const startTripSection = document.getElementById('start-trip-section');
const endTripSection = document.getElementById('end-trip-section');
const messageModal = document.getElementById('message-modal');
const messageText = document.getElementById('message-text');
const confirmModal = document.getElementById('confirm-modal');
const confirmText = document.getElementById('confirm-text');
const confirmOkBtn = document.getElementById('confirm-ok-btn');
const confirmCancelBtn = document.getElementById('confirm-cancel-btn');
const vehicleFilter = document.getElementById('vehicle-filter');
const logYearFilter = document.getElementById('log-year-filter');
const logMonthFilter = document.getElementById('log-month-filter');
const reloadBtn = document.getElementById('reload-btn');
const settingsBtn = document.getElementById('settings-btn');
const backToMainFromSettingsBtn = document.getElementById('back-to-main-from-settings-btn');
const themeSelector = document.getElementById('theme-selector');
const editLogForm = document.getElementById('edit-log-form');
const cancelEditLogBtn = document.getElementById('cancel-edit-log-btn');
// --- 状態管理変数 ---
let vehicles = JSON.parse(localStorage.getItem('vehicles')) || [];
let logs = JSON.parse(localStorage.getItem('logs')) || [];
let selectedVehicle = JSON.parse(sessionStorage.getItem('selectedVehicle')) || null;
let activeTrip = JSON.parse(sessionStorage.getItem('activeTrip')) || null;
let startCoords = null;
let endCoords = null;
let currentScreenId = 'vehicle-selection-screen';
// --- コールバック変数 ---
let onConfirmOkCallback = () => {};
let onMessageModalCloseCallback = () => {};
// --- 関数定義 ---
// 画面切り替え
const showScreen = (screenId) => {
screens.forEach(screen => screen.classList.remove('active'));
const newScreen = document.getElementById(screenId)
if (newScreen) {
newScreen.classList.add('active');
currentScreenId = screenId;
}
window.scrollTo(0, 0); // 画面上部にスクロール
};
// メッセージ表示
const showMessage = (message, callback) => {
messageText.textContent = message;
onMessageModalCloseCallback = callback || (() => {});
messageModal.classList.remove('hidden');
};
// 確認モーダル表示
const showConfirm = (message, callback) => {
confirmText.textContent = message;
onConfirmOkCallback = callback;
confirmModal.classList.remove('hidden');
};
// データ保存
const saveData = () => {
localStorage.setItem('vehicles', JSON.stringify(vehicles));
localStorage.setItem('logs', JSON.stringify(logs));
sessionStorage.setItem('selectedVehicle', JSON.stringify(selectedVehicle));
sessionStorage.setItem('activeTrip', JSON.stringify(activeTrip));
};
// 車両リストのレンダリング
const renderVehicleList = () => {
vehicleList.innerHTML = '';
if (vehicles.length === 0) {
vehicleList.innerHTML = `<p class="text-center text-gray-500">車両が登録されていません。<br>下のボタンから追加してください。</p>`;
} else {
vehicles.forEach(vehicle => {
const div = document.createElement('div');
div.className = 'p-4 border rounded-lg shadow-sm bg-white flex justify-between items-center';
div.innerHTML = `
<div class="flex-grow cursor-pointer">
<p class="font-bold text-gray-800">${vehicle.name}</p>
<p class="text-sm text-gray-600">${vehicle.license}</p>
</div>
<button class="delete-vehicle-btn btn btn-sm btn-icon btn-danger ml-4" data-id="${vehicle.id}">
<i class="fas fa-trash-alt"></i>
</button>
`;
div.querySelector('.flex-grow').addEventListener('click', () => selectVehicle(vehicle));
vehicleList.appendChild(div);
});
}
};
// 車両選択
const selectVehicle = (vehicle) => {
selectedVehicle = vehicle;
currentVehicleDisplay.textContent = `選択中の車両: ${selectedVehicle.name}`;
saveData();
showScreen('trip-screen');
updateTripScreenUI();
};
// 走行記録画面のUI更新
const updateTripScreenUI = () => {
if (activeTrip) {
startTripSection.classList.add('hidden');
endTripSection.classList.remove('hidden');
const date = new Date(activeTrip.startTime);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const dayOfWeek = ['日', '月', '火', '水', '木', '金', '土'][date.getDay()];
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const formattedStartDateTime = `${year}年${month}月${day}日(${dayOfWeek}) ${hours}:${minutes}`;
document.getElementById('active-trip-start-location').textContent = activeTrip.startLocationManual;
document.getElementById('active-trip-start-time').textContent = formattedStartDateTime;
document.getElementById('active-trip-start-odometer').textContent = activeTrip.startOdometer;
} else {
startTripSection.classList.remove('hidden');
endTripSection.classList.add('hidden');
// フォームをリセット
startCoords = null;
endCoords = null;
document.getElementById('start-location-display').textContent = '位置情報未取得';
document.getElementById('start-location-manual').value = '';
document.getElementById('start-odometer').value = '';
}
window.scrollTo(0, 0); // 画面が更新されたときに一番上にスクロール
};
// 位置情報取得
const getLocation = async (type) => {
const loaderId = type === 'start' ? 'start-location-loader' : 'end-location-loader';
const displayId = type === 'start' ? 'start-location-display' : 'end-location-display';
const loader = document.getElementById(loaderId);
const display = document.getElementById(displayId);
if (!navigator.geolocation) {
showMessage('お使いのブラウザは位置情報取得に対応していません。');
return;
}
loader.classList.remove('hidden');
display.textContent = "GPS測位中...";
try {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
});
});
const coords = {
lat: position.coords.latitude,
lon: position.coords.longitude
};
if (type === 'start') {
startCoords = coords;
} else {
endCoords = coords;
}
display.innerHTML = `緯度: ${coords.lat.toFixed(5)}, 経度: ${coords.lon.toFixed(5)} <a href="https://www.google.com/maps?q=${coords.lat},${coords.lon}" target="_blank" class="text-blue-500 ml-2"><i class="fas fa-external-link-alt"></i>地図で見る</a>`;
} catch (error) {
console.error("Geolocation API Error:", error);
let errorMessage = '位置情報の取得に失敗しました。';
if (error.code === 1) errorMessage = '位置情報の利用が許可されていません。ブラウザの設定を確認してください。';
if (error.code === 2) errorMessage = '位置情報を特定できませんでした。場所を移動して再度お試しください。';
if (error.code === 3) errorMessage = '位置情報の取得がタイムアウトしました。';
showMessage(errorMessage);
display.textContent = '取得失敗';
} finally {
loader.classList.add('hidden');
}
};
const populateVehicleFilter = () => {
vehicleFilter.innerHTML = '<option value="all">すべての車両</option>';
vehicles.forEach(vehicle => {
const option = document.createElement('option');
option.value = vehicle.id;
option.textContent = `${vehicle.name} (${vehicle.license})`;
vehicleFilter.appendChild(option);
});
};
const populateDateFilters = () => {
const years = [...new Set(logs.map(log => new Date(log.startTime).getFullYear()))];
if (years.length === 0) {
const currentYear = new Date().getFullYear();
if (!years.includes(currentYear)) {
years.push(currentYear);
}
}
logYearFilter.innerHTML = '';
years.sort((a, b) => b - a).forEach(year => {
const option = document.createElement('option');
option.value = year;
option.textContent = `${year}年`;
logYearFilter.appendChild(option);
});
logMonthFilter.innerHTML = '';
for (let i = 1; i <= 12; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = `${i}月`;
logMonthFilter.appendChild(option);
}
const now = new Date();
logYearFilter.value = now.getFullYear();
logMonthFilter.value = now.getMonth() + 1;
};
const renderLogs = () => {
const logsList = document.getElementById('logs-list');
logsList.innerHTML = '';
const selectedVehicleId = vehicleFilter.value;
const selectedYear = parseInt(logYearFilter.value, 10);
const selectedMonth = parseInt(logMonthFilter.value, 10);
let tempLogs = logs;
if (selectedVehicleId !== 'all') {
tempLogs = tempLogs.filter(log => String(log.vehicleId) === selectedVehicleId);
}
if (!isNaN(selectedYear) && !isNaN(selectedMonth)) {
tempLogs = tempLogs.filter(log => {
const date = new Date(log.startTime);
return date.getFullYear() === selectedYear && (date.getMonth() + 1) === selectedMonth;
});
}
if (tempLogs.length === 0) {
logsList.innerHTML = '<p class="text-center text-gray-500">走行履歴はありません。</p>';
return;
}
const sortedLogs = tempLogs.slice().sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
sortedLogs.forEach(log => {
const vehicle = vehicles.find(v => v.id === log.vehicleId);
const distance = (log.endOdometer && log.startOdometer) ? (log.endOdometer - log.startOdometer) : 0;
const div = document.createElement('div');
div.className = 'bg-white p-4 rounded-lg shadow-sm border relative';
const startLocationText = log.startLocationManual || (log.startCoords ? `${log.startCoords.lat.toFixed(5)}, ${log.startCoords.lon.toFixed(5)}` : (log.startLocation || '情報なし'));
const startMapLink = log.startCoords ? `<a href="https://www.google.com/maps?q=${log.startCoords.lat},${log.startCoords.lon}" target="_blank" class="text-blue-500 ml-1 text-xs">(地図)</a>` : '';
const endLocationText = log.endLocationManual || (log.endCoords ? `${log.endCoords.lat.toFixed(5)}, ${log.endCoords.lon.toFixed(5)}` : (log.endLocation || '情報なし'));
const endMapLink = log.endCoords ? `<a href="https://www.google.com/maps?q=${log.endCoords.lat},${log.endCoords.lon}" target="_blank" class="text-blue-500 ml-1 text-xs">(地図)</a>` : '';
const date = new Date(log.startTime);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const dayOfWeek = ['日', '月', '火', '水', '木', '金', '土'][date.getDay()];
const formattedDate = `${year}年${month}月${day}日(${dayOfWeek})`;
const startHours = String(date.getHours()).padStart(2, '0');
const startMinutes = String(date.getMinutes()).padStart(2, '0');
const formattedStartDateTime = `${formattedDate} ${startHours}:${startMinutes}`;
let formattedEndDateTime = 'N/A';
if (log.endTime) {
const endDate = new Date(log.endTime);
const endYear = endDate.getFullYear();
const endMonth = endDate.getMonth() + 1;
const endDay = endDate.getDate();
const endDayOfWeek = ['日', '月', '火', '水', '木', '金', '土'][endDate.getDay()];
const endHours = String(endDate.getHours()).padStart(2, '0');
const endMinutes = String(endDate.getMinutes()).padStart(2, '0');
formattedEndDateTime = `${endYear}年${endMonth}月${endDay}日(${endDayOfWeek}) ${endHours}:${endMinutes}`;
}
div.innerHTML = `
<div class="absolute top-2 right-2 flex space-x-2">
<button class="edit-log-btn btn btn-sm btn-icon btn-secondary" data-id="${log.id}">
<i class="fas fa-pencil-alt fa-xs"></i>
</button>
<button class="delete-log-btn btn btn-sm btn-icon btn-danger" data-id="${log.id}">
<i class="fas fa-trash-alt fa-xs"></i>
</button>
</div>
<div class="flex justify-between items-start">
<div>
<p class="font-bold text-lg text-gray-800">${formattedDate}</p>
<p class="text-base text-gray-600">${vehicle ? `${vehicle.name} (${vehicle.license})` : '不明な車両'}</p>
</div>
<p class="text-xl font-bold text-blue-600 mr-20">${distance} km</p>
</div>
<div class="mt-2 text-base text-gray-700">
<p><strong>用件:</strong> ${log.purpose || '記載なし'}</p>
<p><i class="fas fa-arrow-up text-green-500 mr-2"></i><strong>出発:</strong> ${startLocationText} ${startMapLink}</p>
<p class="text-sm text-gray-500 ml-5">${formattedStartDateTime} / ${log.startOdometer || 'N/A'} km</p>
<p><i class="fas fa-arrow-down text-red-500 mr-2"></i><strong>到着:</strong> ${endLocationText} ${endMapLink}</p>
<p class="text-sm text-gray-500 ml-5">${formattedEndDateTime} / ${log.endOdometer || 'N/A'} km</p>
</div>
`;
logsList.appendChild(div);
});
};
// Excelエクスポート
const exportXLSX = () => {
if (logs.length === 0) {
showMessage('エクスポートするデータがありません。');
return;
}
const header = ["車両名", "ナンバー", "出発日時", "出発地", "出発緯度", "出発経度", "出発時距離(km)", "到着日時", "到着地", "到着緯度", "到着経度", "到着時距離(km)", "走行距離(km)", "用件"];
const data = logs.map(log => {
const vehicle = vehicles.find(v => v.id === log.vehicleId) || {};
const distance = log.endOdometer - log.startOdometer;
return [
vehicle.name || '',
vehicle.license || '',
new Date(log.startTime).toLocaleString('ja-JP'),
log.startLocationManual || '',
log.startCoords ? log.startCoords.lat : '',
log.startCoords ? log.startCoords.lon : '',
log.startOdometer,
log.endTime ? new Date(log.endTime).toLocaleString('ja-JP') : '',
log.endLocationManual || '',
log.endCoords ? log.endCoords.lat : '',
log.endCoords ? log.endCoords.lon : '',
log.endOdometer,
distance,
log.purpose || ''
];
});
const ws = XLSX.utils.aoa_to_sheet([header, ...data]);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "走行履歴");
XLSX.writeFile(wb, `mileage_log_${new Date().toISOString().slice(0,10)}.xlsx`);
};
// 車両削除ハンドラ
const handleVehicleDelete = (vehicleId) => {
const vehicle = vehicles.find(v => v.id === vehicleId);
if (!vehicle) return;
showConfirm(`車両「${vehicle.name}」と関連するすべての走行履歴を削除します。よろしいですか?`, () => {
vehicles = vehicles.filter(v => v.id !== vehicleId);
logs = logs.filter(log => log.vehicleId !== vehicleId);
saveData();
renderVehicleList();
showMessage('車両を削除しました。');
});
};
// 走行履歴削除ハンドラ
const handleLogDelete = (logId) => {
showConfirm('この走行履歴を削除します。よろしいですか?', () => {
logs = logs.filter(log => log.id !== logId);
saveData();
renderLogs();
showMessage('走行履歴を削除しました。');
});
};
// --- 履歴編集関連の関数 ---
const toDateTimeLocal = (isoString) => {
if (!isoString) return '';
const date = new Date(isoString);
const offset = date.getTimezoneOffset() * 60000;
const localDate = new Date(date.getTime() - offset);
return localDate.toISOString().slice(0, 16);
};
const handleLogEdit = (logId) => {
const log = logs.find(l => l.id === logId);
if (!log) return;
document.getElementById('edit-log-id').value = log.id;
document.getElementById('edit-start-datetime').value = toDateTimeLocal(log.startTime);
document.getElementById('edit-start-location-manual').value = log.startLocationManual || '';
document.getElementById('edit-start-odometer').value = log.startOdometer || '';
document.getElementById('edit-end-datetime').value = toDateTimeLocal(log.endTime);
document.getElementById('edit-end-location-manual').value = log.endLocationManual || '';
document.getElementById('edit-end-odometer').value = log.endOdometer || '';
document.getElementById('edit-purpose').value = log.purpose || '';
showScreen('edit-log-screen');
};
// --- 外観モード関連の関数 ---
const updateThemeButtons = (selectedTheme) => {
document.querySelectorAll('.theme-btn').forEach(button => {
button.classList.remove('btn-primary', 'btn-secondary');
if (button.dataset.theme === selectedTheme) {
button.classList.add('btn-primary');
} else {
button.classList.add('btn-secondary');
}
});
};
const applyTheme = (theme) => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else { // system
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
updateThemeButtons(theme);
};
const systemThemeListener = (e) => {
const currentTheme = localStorage.getItem('theme') || 'system';
if (currentTheme === 'system') {
if (e.matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
};
// --- イベントリスナー ---
reloadBtn.addEventListener('click', () => {
location.reload();
});
settingsBtn.addEventListener('click', () => {
const activeScreen = document.querySelector('.screen.active');
if (activeScreen) {
sessionStorage.setItem('lastScreen', activeScreen.id);
}
showScreen('settings-screen');
});
backToMainFromSettingsBtn.addEventListener('click', () => {
const lastScreen = sessionStorage.getItem('lastScreen') || 'vehicle-selection-screen';
showScreen(lastScreen);
});
themeSelector.addEventListener('click', (e) => {
const themeBtn = e.target.closest('.theme-btn');
if (themeBtn) {
const selectedTheme = themeBtn.dataset.theme;
localStorage.setItem('theme', selectedTheme);
applyTheme(selectedTheme);
}
});
// モーダル閉じる
document.getElementById('close-message-btn').addEventListener('click', () => {
messageModal.classList.add('hidden');
onMessageModalCloseCallback();
});
// 確認モーダルボタン
confirmOkBtn.addEventListener('click', () => {
confirmModal.classList.add('hidden');
if (typeof onConfirmOkCallback === 'function') {
onConfirmOkCallback();
}
});
confirmCancelBtn.addEventListener('click', () => {
confirmModal.classList.add('hidden');
});
// 車両追加画面表示
document.getElementById('show-add-vehicle-screen-btn').addEventListener('click', () => {
showScreen('add-vehicle-screen');
});
// 車両追加キャンセル
document.getElementById('cancel-add-vehicle-btn').addEventListener('click', () => {
addVehicleForm.reset();
showScreen('vehicle-selection-screen');
});
// 車両追加フォーム送信
addVehicleForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('vehicle-name').value.trim();
const license = document.getElementById('vehicle-license').value.trim();
if (name && license) {
const newVehicle = { id: Date.now(), name, license };
vehicles.push(newVehicle);
saveData();
renderVehicleList();
addVehicleForm.reset();
showScreen('vehicle-selection-screen');
}
});
// 走行履歴画面表示
document.getElementById('show-logs-btn').addEventListener('click', () => {
populateDateFilters();
populateVehicleFilter();
renderLogs();
showScreen('logs-screen');
});
// 走行履歴から戻る
document.getElementById('back-to-main-from-logs-btn').addEventListener('click', () => {
showScreen('vehicle-selection-screen');
});
// 車両選択に戻るボタン
document.getElementById('back-to-vehicle-selection-btn').addEventListener('click', () => {
selectedVehicle = null;
sessionStorage.removeItem('selectedVehicle');
currentVehicleDisplay.textContent = '車両が選択されていません';
saveData();
showScreen('vehicle-selection-screen');
});
// Excelエクスポートボタン
document.getElementById('export-excel-btn').addEventListener('click', exportXLSX);
// 位置情報取得ボタン
document.getElementById('get-location-start-btn').addEventListener('click', () => getLocation('start'));
document.getElementById('get-location-end-btn').addEventListener('click', () => getLocation('end'));
// 絞り込みフィルター
vehicleFilter.addEventListener('change', renderLogs);
logYearFilter.addEventListener('change', renderLogs);
logMonthFilter.addEventListener('change', renderLogs);
// イベント委任による削除・編集ボタンの処理
vehicleList.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.delete-vehicle-btn');
if (deleteBtn) {
const vehicleId = parseInt(deleteBtn.dataset.id, 10);
handleVehicleDelete(vehicleId);
}
});
document.getElementById('logs-list').addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.delete-log-btn');
if (deleteBtn) {
const logId = parseInt(deleteBtn.dataset.id, 10);
handleLogDelete(logId);
}
const editBtn = e.target.closest('.edit-log-btn');
if(editBtn) {
const logId = parseInt(editBtn.dataset.id, 10);
handleLogEdit(logId);
}
});
// 出発記録開始
document.getElementById('start-trip-btn').addEventListener('click', async () => {
const startLocationManual = document.getElementById('start-location-manual').value.trim();
const startOdometer = document.getElementById('start-odometer').value;
if (!startCoords || !startLocationManual || !startOdometer) {
showMessage('すべての項目を入力・取得してください。');
return;
}
activeTrip = {
id: Date.now(), // 削除機能のためにユニークIDを付与
vehicleId: selectedVehicle.id,
startTime: new Date().toISOString(),
startCoords: startCoords,
startLocationManual: startLocationManual,
startOdometer: parseInt(startOdometer, 10),
};
saveData();
showMessage('出発記録を開始しました。', () => {
updateTripScreenUI();
});
});
// 走行完了
document.getElementById('end-trip-btn').addEventListener('click', async () => {
const endLocationManual = document.getElementById('end-location-manual').value.trim();
const purpose = document.getElementById('purpose').value.trim();
const endOdometer = document.getElementById('end-odometer').value;
if (!endCoords || !endLocationManual || !purpose || !endOdometer) {
showMessage('すべての項目を入力・取得してください。');
return;
}
const endOdometerValue = parseInt(endOdometer, 10);
if (endOdometerValue <= activeTrip.startOdometer) {
showMessage('到着時の走行距離は、出発時より大きい数値を入力してください。');
return;
}
const completedTrip = {
...activeTrip,
endTime: new Date().toISOString(),
endCoords: endCoords,
endLocationManual: endLocationManual,
endOdometer: endOdometerValue,
purpose,
};
logs.push(completedTrip);
activeTrip = null;
saveData();
showMessage('走行記録を完了しました。', () => {
// フォームをリセット
document.getElementById('end-location-manual').value = '';
document.getElementById('purpose').value = '';
document.getElementById('end-odometer').value = '';
document.getElementById('end-location-display').textContent = '位置情報未取得';
showScreen('vehicle-selection-screen');
});
});
// 走行キャンセル
document.getElementById('cancel-trip-btn').addEventListener('click', () => {
showConfirm('現在の走行記録を本当にキャンセルしますか?', () => {
activeTrip = null;
saveData();
updateTripScreenUI();
showMessage('走行をキャンセルしました。');
});
});
// 履歴編集フォームのイベント
editLogForm.addEventListener('submit', (e) => {
e.preventDefault();
const logId = parseInt(document.getElementById('edit-log-id').value, 10);
const logIndex = logs.findIndex(l => l.id === logId);
if (logIndex > -1) {
const startOdometer = parseInt(document.getElementById('edit-start-odometer').value, 10);
const endOdometer = parseInt(document.getElementById('edit-end-odometer').value, 10);
if (endOdometer && startOdometer >= endOdometer) {
showMessage('到着時の走行距離は、出発時より大きい数値を入力してください。');
return;
}
logs[logIndex] = {
...logs[logIndex],
startTime: new Date(document.getElementById('edit-start-datetime').value).toISOString(),
startLocationManual: document.getElementById('edit-start-location-manual').value,
startOdometer: startOdometer,
endTime: document.getElementById('edit-end-datetime').value ? new Date(document.getElementById('edit-end-datetime').value).toISOString() : null,
endLocationManual: document.getElementById('edit-end-location-manual').value,
endOdometer: endOdometer,
purpose: document.getElementById('edit-purpose').value,
};
saveData();
renderLogs();
showScreen('logs-screen');
showMessage('走行履歴を更新しました。');
}
});
cancelEditLogBtn.addEventListener('click', () => {
showScreen('logs-screen');
});
// --- 初期化処理 ---
const initialize = () => {
// Theme initialization
const theme = localStorage.getItem('theme') || 'system';
applyTheme(theme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', systemThemeListener);
renderVehicleList();
if (selectedVehicle) {
currentVehicleDisplay.textContent = `選択中の車両: ${selectedVehicle.name}`;
showScreen('trip-screen');
updateTripScreenUI();
} else {
currentVehicleDisplay.textContent = '車両が選択されていません';
showScreen('vehicle-selection-screen');
}
};
initialize();
});
</script>
</body>
</html>
最近のお気に入りの曲
なんと、元ANTHEMの坂本英三さんが同じく元ANTHEMのマッド大内さんが居るバンド「SUPERBLOOD」に加入して新曲PVを公開しました。
ANTHEMの時とは違う英三さんで『めっちゃ、カッチョイイ!!』
サビに入る時のキックも「カッチョいい」