/* ============ Mileage ============ */
function Mileage({ search }) {
const { mileage, clients, settings } = useStore();
const [editing, setEditing] = React.useState(null);
const [range, setRange] = React.useState('all'); // all | mtd | ytd
const today = new Date();
const rangeStart = React.useMemo(() => {
if (range === 'mtd') return new Date(today.getFullYear(), today.getMonth(), 1).toISOString().slice(0,10);
if (range === 'ytd') return new Date(today.getFullYear(), 0, 1).toISOString().slice(0,10);
return '0000-01-01';
}, [range]);
const filtered = React.useMemo(() => {
const q = (search || '').toLowerCase().trim();
return [...mileage]
.filter(m => m.date >= rangeStart)
.filter(m => {
if (!q) return true;
const client = clients.find(c => c.id === m.clientId);
const hay = `${m.purpose} ${m.from} ${m.to} ${client?.name || ''}`.toLowerCase();
return hay.includes(q);
})
.sort((a,b) => b.date.localeCompare(a.date) || (b.createdAt||'').localeCompare(a.createdAt||''));
}, [mileage, clients, rangeStart, search]);
const tripMiles = (m) => (parseFloat(m.miles)||0) * (m.roundTrip ? 2 : 1);
const totalMiles = filtered.reduce((s,m) => s + tripMiles(m), 0);
const totalDeduction = totalMiles * (settings.mileageRate || 0);
const tripCount = filtered.length;
const avgPerTrip = tripCount ? totalMiles / tripCount : 0;
return (
m.roundTrip).length} delta={`of ${tripCount} entries`} iconName="arrowRt" iconColor="var(--orange)" />
{filtered.length === 0 ? (
setEditing({ _new: true })}>Log trip} />
) : (
| № |
Date |
Purpose |
From → To |
Client |
Miles |
R/T |
Deduction |
|
{filtered.map((m, i) => {
const client = clients.find(c => c.id === m.clientId);
const miles = tripMiles(m);
const ded = miles * (settings.mileageRate || 0);
return (
setEditing(m)}>
| {String(i+1).padStart(2,'0')} |
{fmt.dateShort(m.date)} |
{m.purpose || —} |
{m.from || '—'}
{m.to || '—'}
|
{client ? {client.name} : —}
|
{miles.toFixed(1)} |
{m.roundTrip ? R/T : One-way} |
{fmt.money(ded, 2)} |
|
);
})}
| Totals |
{totalMiles.toFixed(1)} |
|
{fmt.money(totalDeduction, 2)} |
|
)}
{editing &&
setEditing(null)} />}
);
}
function MileageEditor({ draft, clients, settings, onClose }) {
const isNew = !!draft._new;
const [m, setM] = React.useState(isNew ? {
date: todayISO(), clientId: null, purpose: '', from: settings.baseAddress || 'Office', to: '',
miles: 0, roundTrip: true, notes: '',
} : draft);
const toast = useToast();
const set = (patch) => setM(prev => ({ ...prev, ...patch }));
const totalMiles = (parseFloat(m.miles)||0) * (m.roundTrip ? 2 : 1);
const deduction = totalMiles * (settings.mileageRate || 0);
const save = () => {
if (isNew) { Store.Mileage.add(m); toast('Trip logged'); }
else { Store.Mileage.update(draft.id, m); toast('Trip updated'); }
onClose();
};
const del = () => {
if (confirm('Delete this trip?')) { Store.Mileage.remove(draft.id); toast('Trip deleted'); onClose(); }
};
return (
{!isNew && }
>}>
set({ date: e.target.value })}/>
set({ purpose: e.target.value })} placeholder="Loan signing, courthouse run…"/>
set({ from: e.target.value })} placeholder="Office"/>
set({ to: e.target.value })} placeholder="Client address"/>
set({ miles: parseFloat(e.target.value)||0 })}/>
set({ roundTrip: v === 'rt' })} options={[
{ value: 'rt', label: 'Round trip' },
{ value: 'ow', label: 'One-way' },
]}/>
Total distance
{totalMiles.toFixed(1)} mi
Tax deduction
{fmt.money(deduction, 2)}
);
}
function exportMileageCSV(list, clients, settings) {
const headers = ['Date','Purpose','From','To','OneWayMiles','RoundTrip','TotalMiles','Deduction','Client','Notes'];
const rows = list.map(m => {
const client = clients.find(c => c.id === m.clientId);
const total = (parseFloat(m.miles)||0) * (m.roundTrip ? 2 : 1);
const ded = total * (settings.mileageRate||0);
return [m.date, m.purpose, m.from, m.to, m.miles, m.roundTrip ? 'Yes' : 'No', total.toFixed(1), ded.toFixed(2), client?.name || '', m.notes];
});
downloadCSV('mileage', [headers, ...rows]);
}
function downloadCSV(name, rows) {
const csv = rows.map(r => r.map(v => `"${String(v ?? '').replace(/"/g,'""')}"`).join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `${name}-${todayISO()}.csv`; a.click();
URL.revokeObjectURL(url);
}
window.Mileage = Mileage;
window.exportMileageCSV = exportMileageCSV;
window.downloadCSV = downloadCSV;