import React, { useEffect, useMemo, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { createClient } from '@supabase/supabase-js'; import { Bus, ClipboardList, Database, Upload, Search, Home, Camera, FileText, ExternalLink, Trash2, Download, RefreshCw } from 'lucide-react'; import fleetVehicles from './data/fleetVehicles.json'; import './styles.css'; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY; const supabase = supabaseUrl && supabaseKey ? createClient(supabaseUrl, supabaseKey) : null; const STORAGE_BUCKET = 'bus-log-uploads'; const emptyLog = { date: new Date().toISOString().slice(0, 10), time: new Date().toTimeString().slice(0, 5), shiftNumber: '', shiftType: 'Run printed shift', fleetNumber: '', registration: '', vehicleDescription: '', vehicleLocation: '', regoStatus: 'Not checked', gearNoise: '', power: '', bodyRattles: '', aircon: '', damageNotes: '', comments: '', photoName: '', photoUrl: '', runPrintName: '', runPrintUrl: '', createdAt: '' }; function normaliseFleet(value) { return String(value || '').replace(/[^0-9A-Za-z]/g, '').toUpperCase(); } function fileToDataUrl(file) { return new Promise((resolve, reject) => { if (!file) return resolve(''); const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(file); }); } async function uploadFile(file, prefix) { if (!file) return { name: '', url: '' }; if (!supabase) return { name: file.name, url: await fileToDataUrl(file) }; const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_'); const path = `${prefix}/${Date.now()}-${crypto.randomUUID()}-${safeName}`; const { error } = await supabase.storage.from(STORAGE_BUCKET).upload(path, file, { upsert: false }); if (error) throw error; const { data } = supabase.storage.from(STORAGE_BUCKET).getPublicUrl(path); return { name: file.name, url: data.publicUrl }; } async function loadLogs() { if (supabase) { const { data, error } = await supabase.from('bus_logs').select('*').order('created_at', { ascending: false }); if (error) throw error; return data.map(row => ({ id: row.id, date: row.drive_date, time: row.drive_time?.slice(0, 5) || '', shiftNumber: row.shift_number || '', shiftType: row.shift_type || '', fleetNumber: row.fleet_number || '', registration: row.registration || '', vehicleDescription: row.vehicle_description || '', vehicleLocation: row.vehicle_location || '', regoStatus: row.rego_status || 'Not checked', gearNoise: row.gear_noise || '', power: row.power || '', bodyRattles: row.body_rattles || '', aircon: row.aircon || '', damageNotes: row.damage_notes || '', comments: row.comments || '', photoName: row.photo_name || '', photoUrl: row.photo_url || '', runPrintName: row.run_print_name || '', runPrintUrl: row.run_print_url || '', createdAt: row.created_at || '' })); } return JSON.parse(localStorage.getItem('busLogs') || '[]'); } async function saveLog(log) { if (supabase) { const { error } = await supabase.from('bus_logs').insert({ drive_date: log.date, drive_time: log.time, shift_number: log.shiftNumber, shift_type: log.shiftType, fleet_number: log.fleetNumber, registration: log.registration, vehicle_description: log.vehicleDescription, vehicle_location: log.vehicleLocation, rego_status: log.regoStatus, gear_noise: log.gearNoise, power: log.power, body_rattles: log.bodyRattles, aircon: log.aircon, damage_notes: log.damageNotes, comments: log.comments, photo_name: log.photoName, photo_url: log.photoUrl, run_print_name: log.runPrintName, run_print_url: log.runPrintUrl }); if (error) throw error; return; } const logs = JSON.parse(localStorage.getItem('busLogs') || '[]'); logs.unshift({ ...log, id: crypto.randomUUID(), createdAt: new Date().toISOString() }); localStorage.setItem('busLogs', JSON.stringify(logs)); } async function deleteLog(id) { if (supabase) { const { error } = await supabase.from('bus_logs').delete().eq('id', id); if (error) throw error; return; } const logs = JSON.parse(localStorage.getItem('busLogs') || '[]').filter(l => l.id !== id); localStorage.setItem('busLogs', JSON.stringify(logs)); } function RatingGroup({ label, value, onChange, options }) { return

{label}

{options.map(opt => )}
; } function LogBus({ fleetIndex, onSaved }) { const [form, setForm] = useState(emptyLog); const [photo, setPhoto] = useState(null); const [runPrint, setRunPrint] = useState(null); const [message, setMessage] = useState(''); const [saving, setSaving] = useState(false); const set = (key, value) => setForm(prev => ({ ...prev, [key]: value })); const handleFleet = value => { const key = normaliseFleet(value); const vehicle = fleetIndex.get(key); setForm(prev => ({ ...prev, fleetNumber: value, registration: vehicle?.registration || '', vehicleDescription: vehicle?.description || '', vehicleLocation: vehicle?.location || '', regoStatus: vehicle ? 'Use QLD Rego Check' : 'Vehicle not found' })); }; const qldRegoUrl = 'https://www.service.transport.qld.gov.au/checkrego/application/VehicleSearch.xhtml'; const submit = async e => { e.preventDefault(); setSaving(true); setMessage(''); try { const uploadedPhoto = await uploadFile(photo, 'photos'); const uploadedRunPrint = form.shiftType === 'Run printed shift' ? await uploadFile(runPrint, 'run-prints') : { name: '', url: '' }; await saveLog({ ...form, photoName: uploadedPhoto.name, photoUrl: uploadedPhoto.url, runPrintName: uploadedRunPrint.name, runPrintUrl: uploadedRunPrint.url }); setForm({ ...emptyLog, date: new Date().toISOString().slice(0, 10), time: new Date().toTimeString().slice(0, 5) }); setPhoto(null); setRunPrint(null); setMessage('Saved.'); onSaved?.(); } catch (err) { setMessage(err.message || 'Could not save log.'); } finally { setSaving(false); } }; return

Log a bus

Designed for quick phone use at sign-on or sign-off.

QLD rego status: {form.regoStatus}
{form.registration && Open QLD Rego Check}Automatic QLD Rego Check lookup is not included because there is no public official API. Use the button to verify manually.
set('gearNoise',v)} options={['Quiet','Noticeable','Loud']} />set('power',v)} options={['Slow','Acceptable','Quick']} />set('bodyRattles',v)} options={['Extreme','Reasonable','Loud']} />set('aircon',v)} options={['Good','Bad']} />