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 ;
}
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.
;
}
function History({ logs, onRefresh, onDelete }) {
const [query, setQuery] = useState('');
const filtered = logs.filter(l => JSON.stringify(l).toLowerCase().includes(query.toLowerCase()));
const exportCsv = () => {
const headers = ['date','time','shiftNumber','shiftType','fleetNumber','registration','regoStatus','gearNoise','power','bodyRattles','aircon','damageNotes','comments','photoUrl','runPrintUrl'];
const csv = [headers.join(','), ...logs.map(l => headers.map(h => JSON.stringify(l[h] || '')).join(','))].join('\n');
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
const a = document.createElement('a'); a.href = url; a.download = 'bus-drive-log.csv'; a.click(); URL.revokeObjectURL(url);
};
return Bus history
setQuery(e.target.value)} placeholder="Search fleet, rego, shift, notes..." />
{logs.length} total logs {new Set(logs.map(l=>l.fleetNumber)).size} unique buses {logs.filter(l=>l.aircon==='Bad').length} bad aircon notes
{filtered.map(log => {log.fleetNumber || 'Unknown bus'} {log.registration}
{log.date} {log.time} • {log.shiftType} {log.shiftNumber && `• Shift ${log.shiftNumber}`}
{log.vehicleDescription} {log.vehicleLocation && `• ${log.vehicleLocation}`}
Gear: {log.gearNoise || '-'}Power: {log.power || '-'}Rattles: {log.bodyRattles || '-'}Aircon: {log.aircon || '-'}
{log.damageNotes && Damage/defect: {log.damageNotes}
}{log.comments && Comments: {log.comments}
})}{filtered.length === 0 && No matching logs yet.
};
}
function FleetRegister({ vehicles }) {
const [query, setQuery] = useState('');
const filtered = vehicles.filter(v => JSON.stringify(v).toLowerCase().includes(query.toLowerCase())).slice(0, 300);
return Fleet register
Loaded from the uploaded spreadsheet. Showing up to 300 matches.
setQuery(e.target.value)} placeholder="Search fleet, rego, model, depot..." />
| Fleet | Rego | Description | Location | Status |
{filtered.map(v => | {v.fleetNumber} | {v.registration} | {v.description} | {v.location} | {v.status} |
)}
;
}
function HomeScreen({ setPage }) { return ; }
function App() {
const [page, setPage] = useState('home');
const [logs, setLogs] = useState([]);
const [error, setError] = useState('');
const fleetIndex = useMemo(() => new Map(fleetVehicles.map(v => [normaliseFleet(v.fleetNumber), v])), []);
const refresh = async () => { try { setLogs(await loadLogs()); setError(''); } catch(e) { setError(e.message); } };
useEffect(()=>{ refresh(); }, []);
const remove = async id => { if (confirm('Delete this log?')) { await deleteLog(id); await refresh(); } };
return Bus Drive Log
{supabase ? 'Supabase cloud sync enabled' : 'Local demo mode — add Supabase env vars for phone/computer sync'}
{error &&
{error}
}{page==='home' &&
} {page==='log' && } {page==='history' && } {page==='fleet' && } ;
}
createRoot(document.getElementById('root')).render();