GoogleのAIアシスタント Gemini

ChatGPTはGoogle検索の代わりに使っていましたがGeminiはGoogleWorkplaceに統合されるようになってからGメールで推敲など使用することがありましたが、今回GeminiのCanvasを使ってみて、ちょっとしたWebアプリを作ってみたのでコードも含め公開します。

Webアプリ名:Mileage Log

Mileage Log

モバイルでの使用が想定の車の走行距離を記録して管理するWebアプリで下記の機能があります。

  1. ライトモード・ダークモード・システム設定を選択できる外観モード設定
  2. 車両は何台でも追加可能(車両毎に走行距離を記録できる)
  3. 出発時は、出発地のGPS座標・出発地・出発時の走行距離を入力
  4. 到着時は、到着地のGPS座標・到着地・到着時の走行距離・用件を入力
  5. 走行履歴の閲覧(年月別及び車両で絞り込み可能)
  6. Excelにエクスポート(後からPCで編集できるように)
  7. データはブラウザに保管する仕様(キャッシュを削除するとデータが消えます)

作ってみた感想

これはすごい便利だね、アイデアがあれば色んなアプリができそう。

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の時とは違う英三さんで『めっちゃ、カッチョイイ!!』

サビに入る時のキックも「カッチョいい」 

filling station, gas, gas station Previous post 高卒の管理人が甲種危険物取扱者試験の受験資格取得するまでの道のり

コメントを残す