Initial commit: Masina-Dock Vehicle Management System

This commit is contained in:
Iulian 2025-10-19 11:10:11 +01:00
commit ae923e2c41
4999 changed files with 1607266 additions and 0 deletions

571
frontend/static/js/app.js Normal file
View file

@ -0,0 +1,571 @@
const API_URL = window.location.origin;
let editingServiceRecordId = null;
let editingFuelRecordId = null;
let editingReminderId = null;
let editingVehicleId = null;
let currentUser = null;
let currentTheme = 'dark';
let userSettings = null;
function setSelectedVehicle(vehicleId) {
localStorage.setItem('selectedVehicleId', vehicleId);
}
function getSelectedVehicle() {
return localStorage.getItem('selectedVehicleId');
}
function clearSelectedVehicle() {
localStorage.removeItem('selectedVehicleId');
}
async function loadUserSettings() {
try {
const settings = await apiRequest('/api/settings');
userSettings = settings;
localStorage.setItem('userSettings', JSON.stringify(settings));
return settings;
} catch (error) {
const cached = localStorage.getItem('userSettings');
if (cached) {
userSettings = JSON.parse(cached);
return userSettings;
}
return { currency: 'GBP', unit_system: 'imperial', language: 'en' };
}
}
function getCurrencySymbol(currency) {
const symbols = {
'USD': '$',
'GBP': '£',
'RON': 'lei',
'EUR': '€'
};
return symbols[currency] || currency;
}
function formatCurrency(amount, currency = null) {
if (!currency) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
currency = settings.currency || 'GBP';
}
const numAmount = parseFloat(amount) || 0;
const symbol = getCurrencySymbol(currency);
if (currency === 'RON') {
return `${numAmount.toFixed(2)} ${symbol}`;
} else {
return `${symbol}${numAmount.toFixed(2)}`;
}
}
function formatDate(dateString) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
const lang = settings.language || 'en';
const locale = lang === 'ro' ? 'ro-RO' : 'en-GB';
return new Date(dateString).toLocaleDateString(locale);
}
async function apiRequest(endpoint, options = {}) {
try {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
...options.headers
}
});
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text);
throw new Error('Server returned non-JSON response');
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
return data;
} catch (error) {
console.error('API Request Error:', error);
throw error;
}
}
function toggleTheme() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
if (currentUser) {
apiRequest('/api/settings/theme', {
method: 'POST',
body: JSON.stringify({ theme: currentTheme })
}).catch(err => console.error('Failed to save theme:', err));
}
}
async function login(username, password) {
try {
const data = await apiRequest('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
currentUser = data.user;
currentTheme = data.user.theme;
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('userSettings', JSON.stringify(data.user));
if (data.user.must_change_credentials) {
window.location.href = '/first-login';
} else if (data.user.first_login) {
alert('Welcome! Please review your settings.');
window.location.href = '/dashboard';
} else {
window.location.href = '/dashboard';
}
} catch (error) {
alert('Login failed: ' + error.message);
}
}
async function register(username, email, password) {
try {
await apiRequest('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password })
});
alert('Registration successful! Please login.');
window.location.href = '/login';
} catch (error) {
alert('Registration failed: ' + error.message);
}
}
async function logout() {
try {
await apiRequest('/api/auth/logout', { method: 'POST' });
localStorage.clear();
sessionStorage.clear();
window.location.href = '/login';
} catch (error) {
alert('Logout failed: ' + error.message);
}
}
async function loadVehicles() {
try {
const vehicles = await apiRequest('/api/vehicles');
displayVehicles(vehicles);
} catch (error) {
console.error('Failed to load vehicles:', error);
}
}
function displayVehicles(vehicles) {
const container = document.getElementById('vehicles-container');
if (!container) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"unit_system":"imperial"}');
const unitLabel = settings.unit_system === 'metric' ? 'km' : 'miles';
container.innerHTML = vehicles.map(v => `
<div class="vehicle-card ${v.status === 'sold' ? 'sold' : ''}" onclick="viewVehicle(${v.id})">
${v.photo ? `<img src="${v.photo}" class="vehicle-photo" alt="${v.make} ${v.model}">` : ''}
<h3>${v.year} ${v.make} ${v.model}</h3>
<p>${v.vin || 'No VIN'}</p>
<p>Odometer: ${v.odometer.toLocaleString()} ${unitLabel}</p>
</div>
`).join('');
}
async function addVehicle(vehicleData) {
try {
await apiRequest('/api/vehicles', {
method: 'POST',
body: JSON.stringify(vehicleData)
});
loadVehicles();
closeModal('add-vehicle-modal');
document.getElementById('add-vehicle-form').reset();
const photoPreview = document.getElementById('photo-preview');
if (photoPreview) photoPreview.innerHTML = '';
} catch (error) {
alert('Failed to add vehicle: ' + error.message);
}
}
async function loadServiceRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
displayServiceRecords(records);
} catch (error) {
console.error('Failed to load service records:', error);
}
}
function displayServiceRecords(records) {
const tbody = document.getElementById('service-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
const vehicleId = getSelectedVehicle();
tbody.innerHTML = records.map(r => `
<tr>
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.description}</td>
<td>${r.category || 'N/A'}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.notes || ''}</td>
<td>${r.document_path ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.document_path)}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
<td>
<button onclick="editServiceRecord(${vehicleId}, ${r.id})" class="btn" style="padding: 5px 10px; font-size: 12px; margin-right: 5px;">Edit</button>
<button onclick="deleteServiceRecord(${vehicleId}, ${r.id})" class="btn" style="padding: 5px 10px; font-size: 12px; background: #dc3545;">Delete</button>
</td>
</tr>
`).join('');
}
function displayFuelRecords(records) {
const tbody = document.getElementById('fuel-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
const vehicleId = getSelectedVehicle();
tbody.innerHTML = records.map(r => `
<tr>
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.fuel_amount.toFixed(2)}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.distance || 'N/A'}</td>
<td>${r.fuel_economy ? r.fuel_economy.toFixed(2) : 'N/A'} ${r.unit}</td>
<td>${r.notes && r.notes.startsWith('attachment:') ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.notes.replace('attachment:', ''))}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
<td>
<button onclick="editFuelRecord(${vehicleId}, ${r.id})" class="btn" style="padding: 5px 10px; font-size: 12px; margin-right: 5px;">Edit</button>
<button onclick="deleteFuelRecord(${vehicleId}, ${r.id})" class="btn" style="padding: 5px 10px; font-size: 12px; background: #dc3545;">Delete</button>
</td>
</tr>
`).join('');
}
async function loadFuelRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/fuel-records`);
displayFuelRecords(records);
calculateFuelStats(records);
} catch (error) {
console.error('Failed to load fuel records:', error);
}
}
function calculateFuelStats(records) {
const totalCost = records.reduce((sum, r) => sum + r.cost, 0);
const avgEconomy = records.filter(r => r.fuel_economy).reduce((sum, r, _, arr) =>
sum + r.fuel_economy / arr.length, 0);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
const totalCostEl = document.getElementById('total-fuel-cost');
const avgEconomyEl = document.getElementById('avg-fuel-economy');
if (totalCostEl) totalCostEl.textContent = formatCurrency(totalCost, settings.currency);
if (avgEconomyEl) avgEconomyEl.textContent = avgEconomy.toFixed(2);
}
async function loadReminders(vehicleId) {
try {
const reminders = await apiRequest(`/api/vehicles/${vehicleId}/reminders`);
displayReminders(reminders);
} catch (error) {
console.error('Failed to load reminders:', error);
}
}
function displayReminders(reminders) {
const container = document.getElementById('reminders-container');
if (!container) return;
const vehicleId = getSelectedVehicle();
container.innerHTML = reminders.map(r => `
<div class="kanban-item urgency-${r.urgency}">
<h4>${r.description}</h4>
<p>${r.due_date ? `Due: ${formatDate(r.due_date)}` : ''}</p>
<p>${r.due_odometer ? `At: ${r.due_odometer.toLocaleString()}` : ''}</p>
<p>${r.notes || ''}</p>
<div style="margin-top: 10px;">
<button onclick="editReminder(${vehicleId}, ${r.id})" class="btn" style="padding: 5px 10px; font-size: 12px; margin-right: 5px;">Edit</button>
<button onclick="deleteReminder(${vehicleId}, ${r.id})" class="btn" style="padding: 5px 10px; font-size: 12px; background: #dc3545;">Delete</button>
</div>
</div>
`).join('');
}
async function loadTodos(vehicleId) {
try {
const todos = await apiRequest(`/api/vehicles/${vehicleId}/todos`);
displayTodos(todos);
} catch (error) {
console.error('Failed to load todos:', error);
}
}
function displayTodos(todos) {
const statuses = ['planned', 'doing', 'testing', 'done'];
statuses.forEach(status => {
const column = document.getElementById(`todos-${status}`);
if (!column) return;
const filtered = todos.filter(t => t.status === status);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
column.innerHTML = filtered.map(t => `
<div class="kanban-item priority-${t.priority}" draggable="true" data-id="${t.id}">
<h4>${t.description}</h4>
<p>Cost: ${formatCurrency(t.cost, settings.currency)}</p>
<p>${t.type || ''}</p>
<small>${t.notes || ''}</small>
</div>
`).join('');
});
}
function showContextMenu(event, type, id) {
event.preventDefault();
const menu = document.getElementById('context-menu');
if (!menu) return;
menu.style.left = `${event.pageX}px`;
menu.style.top = `${event.pageY}px`;
menu.classList.add('active');
menu.dataset.type = type;
menu.dataset.id = id;
}
document.addEventListener('click', () => {
const menu = document.getElementById('context-menu');
if (menu) menu.classList.remove('active');
});
function showModal(modalId) {
resetEditMode();
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'block';
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = 'none';
}
resetEditMode();
}
async function exportData(type, vehicleId) {
window.location.href = `${API_URL}/api/export/${type}?vehicle_id=${vehicleId}`;
}
function exportAllVehicleData(vehicleId) {
window.location.href = `${API_URL}/api/vehicles/${vehicleId}/export-all`;
}
function viewVehicle(vehicleId) {
setSelectedVehicle(vehicleId);
window.location.href = `/vehicle-detail?id=${vehicleId}`;
}
document.addEventListener('DOMContentLoaded', async () => {
const theme = localStorage.getItem('theme') || 'dark';
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
await loadUserSettings();
const vehicleSelect = document.getElementById('vehicle-select');
if (vehicleSelect) {
const selectedVehicleId = getSelectedVehicle();
if (selectedVehicleId) {
vehicleSelect.value = selectedVehicleId;
vehicleSelect.dispatchEvent(new Event('change'));
}
}
});
async function editRecurringExpense(vehicleId, expenseId) {
try {
const expense = await apiRequest(`/api/vehicles/${vehicleId}/recurring-expenses/${expenseId}`);
const expense_type = prompt('Expense Type:', expense.expense_type);
if (!expense_type) return;
const description = prompt('Description:', expense.description);
if (!description) return;
const amount = prompt('Amount:', expense.amount);
if (!amount) return;
const frequency = prompt('Frequency (monthly/yearly):', expense.frequency);
const notes = prompt('Notes:', expense.notes || '');
await apiRequest(`/api/vehicles/${vehicleId}/recurring-expenses/${expenseId}`, {
method: 'PUT',
body: JSON.stringify({ expense_type, description, amount: parseFloat(amount), frequency, notes })
});
alert('Tax/Expense updated successfully');
location.reload();
} catch (error) {
alert('Failed to update tax/expense: ' + error.message);
}
}
async function deleteRecurringExpense(vehicleId, expenseId) {
if (!confirm('Are you sure you want to delete this tax/expense?')) return;
try {
await apiRequest(`/api/vehicles/${vehicleId}/recurring-expenses/${expenseId}`, { method: 'DELETE' });
alert('Tax/Expense deleted successfully');
location.reload();
} catch (error) {
alert('Failed to delete tax/expense: ' + error.message);
}
}
async function editServiceRecord(vehicleId, recordId) {
try {
const record = await apiRequest(`/api/vehicles/${vehicleId}/service-records/${recordId}`);
editingServiceRecordId = recordId;
editingVehicleId = vehicleId;
document.getElementById('service-date').value = record.date;
document.getElementById('service-odometer').value = record.odometer;
document.getElementById('service-description').value = record.description;
document.getElementById('service-category').value = record.category || 'Maintenance';
document.getElementById('service-cost').value = record.cost;
document.getElementById('service-notes').value = record.notes || '';
const modal = document.getElementById('add-service-modal');
const modalTitle = modal.querySelector('h2');
if (modalTitle) modalTitle.textContent = 'Edit Service Record';
modal.style.display = 'block';
} catch (error) {
alert('Failed to load service record: ' + error.message);
}
}
async function deleteServiceRecord(vehicleId, recordId) {
if (!confirm('Are you sure you want to delete this service record?')) return;
try {
await apiRequest(`/api/vehicles/${vehicleId}/service-records/${recordId}`, { method: 'DELETE' });
alert('Service record deleted successfully');
await loadServiceRecords(vehicleId);
} catch (error) {
alert('Failed to delete service record: ' + error.message);
}
}
async function editFuelRecord(vehicleId, recordId) {
try {
const record = await apiRequest(`/api/vehicles/${vehicleId}/fuel-records/${recordId}`);
editingFuelRecordId = recordId;
editingVehicleId = vehicleId;
document.getElementById('fuel-date').value = record.date;
document.getElementById('fuel-odometer').value = record.odometer;
document.getElementById('fuel-amount').value = record.fuel_amount;
document.getElementById('fuel-cost').value = record.cost;
document.getElementById('fuel-notes').value = record.notes || '';
const modal = document.getElementById('add-fuel-modal');
const modalTitle = modal.querySelector('h2');
if (modalTitle) modalTitle.textContent = 'Edit Fuel Record';
modal.style.display = 'block';
} catch (error) {
alert('Failed to load fuel record: ' + error.message);
}
}
async function deleteFuelRecord(vehicleId, recordId) {
if (!confirm('Are you sure you want to delete this fuel record?')) return;
try {
await apiRequest(`/api/vehicles/${vehicleId}/fuel-records/${recordId}`, { method: 'DELETE' });
alert('Fuel record deleted successfully');
await loadFuelRecords(vehicleId);
} catch (error) {
alert('Failed to delete fuel record: ' + error.message);
}
}
async function editReminder(vehicleId, reminderId) {
try {
const reminder = await apiRequest(`/api/vehicles/${vehicleId}/reminders/${reminderId}`);
editingReminderId = reminderId;
editingVehicleId = vehicleId;
document.getElementById('reminder-description').value = reminder.description;
document.getElementById('reminder-urgency').value = reminder.urgency;
document.getElementById('reminder-date').value = reminder.due_date || '';
document.getElementById('reminder-odometer').value = reminder.due_odometer || '';
document.getElementById('reminder-notes').value = reminder.notes || '';
const modal = document.getElementById('add-reminder-modal');
const modalTitle = modal.querySelector('h2');
if (modalTitle) modalTitle.textContent = 'Edit Reminder';
modal.style.display = 'block';
} catch (error) {
alert('Failed to load reminder: ' + error.message);
}
}
async function deleteReminder(vehicleId, reminderId) {
if (!confirm('Are you sure you want to delete this reminder?')) return;
try {
await apiRequest(`/api/vehicles/${vehicleId}/reminders/${reminderId}`, { method: 'DELETE' });
alert('Reminder deleted successfully');
await loadTodos(vehicleId);
} catch (error) {
alert('Failed to delete reminder: ' + error.message);
}
}
function resetEditMode() {
editingServiceRecordId = null;
editingFuelRecordId = null;
editingReminderId = null;
editingVehicleId = null;
const serviceModal = document.getElementById('add-service-modal');
if (serviceModal) {
const serviceTitle = serviceModal.querySelector('h2');
if (serviceTitle) serviceTitle.textContent = 'Add Service Record';
}
const fuelModal = document.getElementById('add-fuel-modal');
if (fuelModal) {
const fuelTitle = fuelModal.querySelector('h2');
if (fuelTitle) fuelTitle.textContent = 'Add Fuel Record';
}
const reminderModal = document.getElementById('add-reminder-modal');
if (reminderModal) {
const reminderTitle = reminderModal.querySelector('h2');
if (reminderTitle) reminderTitle.textContent = 'Add Reminder';
}
}

View file

@ -0,0 +1,450 @@
const API_URL = window.location.origin;
let currentUser = null;
let currentTheme = 'dark';
let userSettings = null;
function setSelectedVehicle(vehicleId) {
localStorage.setItem('selectedVehicleId', vehicleId);
}
function getSelectedVehicle() {
return localStorage.getItem('selectedVehicleId');
}
function clearSelectedVehicle() {
localStorage.removeItem('selectedVehicleId');
}
async function loadUserSettings() {
try {
const settings = await apiRequest('/api/settings');
userSettings = settings;
localStorage.setItem('userSettings', JSON.stringify(settings));
return settings;
} catch (error) {
const cached = localStorage.getItem('userSettings');
if (cached) {
userSettings = JSON.parse(cached);
return userSettings;
}
return { currency: 'GBP', unit_system: 'imperial', language: 'en' };
}
}
function getCurrencySymbol(currency) {
const symbols = {
'USD': '$',
'GBP': '£',
'RON': 'lei',
'EUR': '€'
};
return symbols[currency] || currency;
}
function formatCurrency(amount, currency = null) {
if (!currency) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
currency = settings.currency || 'GBP';
}
const numAmount = parseFloat(amount) || 0;
const symbol = getCurrencySymbol(currency);
if (currency === 'RON') {
return `${numAmount.toFixed(2)} ${symbol}`;
} else {
return `${symbol}${numAmount.toFixed(2)}`;
}
}
function formatDate(dateString) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
const lang = settings.language || 'en';
const locale = lang === 'ro' ? 'ro-RO' : 'en-GB';
return new Date(dateString).toLocaleDateString(locale);
}
async function apiRequest(endpoint, options = {}) {
try {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
...options.headers
}
});
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text);
throw new Error('Server returned non-JSON response');
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
return data;
} catch (error) {
console.error('API Request Error:', error);
throw error;
}
}
function toggleTheme() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
if (currentUser) {
apiRequest('/api/settings/theme', {
method: 'POST',
body: JSON.stringify({ theme: currentTheme })
}).catch(err => console.error('Failed to save theme:', err));
}
}
async function login(username, password) {
try {
const data = await apiRequest('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
currentUser = data.user;
currentTheme = data.user.theme;
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('userSettings', JSON.stringify(data.user));
if (data.user.must_change_credentials) {
window.location.href = '/first-login';
} else if (data.user.first_login) {
alert('Welcome! Please review your settings.');
window.location.href = '/dashboard';
} else {
window.location.href = '/dashboard';
}
} catch (error) {
alert('Login failed: ' + error.message);
}
}
async function register(username, email, password) {
try {
await apiRequest('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password })
});
alert('Registration successful! Please login.');
window.location.href = '/login';
} catch (error) {
alert('Registration failed: ' + error.message);
}
}
async function logout() {
try {
await apiRequest('/api/auth/logout', { method: 'POST' });
localStorage.clear();
sessionStorage.clear();
window.location.href = '/login';
} catch (error) {
alert('Logout failed: ' + error.message);
}
}
async function loadVehicles() {
try {
const vehicles = await apiRequest('/api/vehicles');
displayVehicles(vehicles);
} catch (error) {
console.error('Failed to load vehicles:', error);
}
}
function displayVehicles(vehicles) {
const container = document.getElementById('vehicles-container');
if (!container) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"unit_system":"imperial"}');
const unitLabel = settings.unit_system === 'metric' ? 'km' : 'miles';
container.innerHTML = vehicles.map(v => `
<div class="vehicle-card ${v.status === 'sold' ? 'sold' : ''}" onclick="viewVehicle(${v.id})">
${v.photo ? `<img src="${v.photo}" class="vehicle-photo" alt="${v.make} ${v.model}">` : ''}
<h3>${v.year} ${v.make} ${v.model}</h3>
<p>${v.vin || 'No VIN'}</p>
<p>Odometer: ${v.odometer.toLocaleString()} ${unitLabel}</p>
</div>
`).join('');
}
async function addVehicle(vehicleData) {
try {
await apiRequest('/api/vehicles', {
method: 'POST',
body: JSON.stringify(vehicleData)
});
loadVehicles();
closeModal('add-vehicle-modal');
document.getElementById('add-vehicle-form').reset();
const photoPreview = document.getElementById('photo-preview');
if (photoPreview) photoPreview.innerHTML = '';
} catch (error) {
alert('Failed to add vehicle: ' + error.message);
}
}
async function loadServiceRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
displayServiceRecords(records);
} catch (error) {
console.error('Failed to load service records:', error);
}
}
function displayServiceRecords(records) {
const tbody = document.getElementById('service-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
tbody.innerHTML = records.map(r => `
<tr oncontextmenu="showContextMenu(event, 'service', ${r.id})">
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.description}</td>
<td>${r.category || 'N/A'}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.notes || ''}</td>
<td>${r.document_path ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.document_path)}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
</tr>
`).join('');
}
async function loadFuelRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/fuel-records`);
displayFuelRecords(records);
calculateFuelStats(records);
} catch (error) {
console.error('Failed to load fuel records:', error);
}
}
function displayFuelRecords(records) {
const tbody = document.getElementById('fuel-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
tbody.innerHTML = records.map(r => `
<tr>
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.fuel_amount.toFixed(2)}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.distance || 'N/A'}</td>
<td>${r.fuel_economy ? r.fuel_economy.toFixed(2) : 'N/A'} ${r.unit}</td>
<td>${r.notes && r.notes.startsWith('attachment:') ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.notes.replace('attachment:', ''))}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
</tr>
`).join('');
}
function calculateFuelStats(records) {
const totalCost = records.reduce((sum, r) => sum + r.cost, 0);
const avgEconomy = records.filter(r => r.fuel_economy).reduce((sum, r, _, arr) =>
sum + r.fuel_economy / arr.length, 0);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
const totalCostEl = document.getElementById('total-fuel-cost');
const avgEconomyEl = document.getElementById('avg-fuel-economy');
if (totalCostEl) totalCostEl.textContent = formatCurrency(totalCost, settings.currency);
if (avgEconomyEl) avgEconomyEl.textContent = avgEconomy.toFixed(2);
}
async function loadReminders(vehicleId) {
try {
const reminders = await apiRequest(`/api/vehicles/${vehicleId}/reminders`);
displayReminders(reminders);
} catch (error) {
console.error('Failed to load reminders:', error);
}
}
function displayReminders(reminders) {
const container = document.getElementById('reminders-container');
if (!container) return;
container.innerHTML = reminders.map(r => `
<div class="kanban-item urgency-${r.urgency}">
<h4>${r.description}</h4>
<p>${r.due_date ? `Due: ${formatDate(r.due_date)}` : ''}</p>
<p>${r.due_odometer ? `At: ${r.due_odometer.toLocaleString()}` : ''}</p>
<p>${r.notes || ''}</p>
</div>
`).join('');
}
async function loadTodos(vehicleId) {
try {
const todos = await apiRequest(`/api/vehicles/${vehicleId}/todos`);
displayTodos(todos);
} catch (error) {
console.error('Failed to load todos:', error);
}
}
function displayTodos(todos) {
const statuses = ['planned', 'doing', 'testing', 'done'];
statuses.forEach(status => {
const column = document.getElementById(`todos-${status}`);
if (!column) return;
const filtered = todos.filter(t => t.status === status);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
column.innerHTML = filtered.map(t => `
<div class="kanban-item priority-${t.priority}" draggable="true" data-id="${t.id}">
<h4>${t.description}</h4>
<p>Cost: ${formatCurrency(t.cost, settings.currency)}</p>
<p>${t.type || ''}</p>
<small>${t.notes || ''}</small>
</div>
`).join('');
});
}
function showContextMenu(event, type, id) {
event.preventDefault();
const menu = document.getElementById('context-menu');
if (!menu) return;
menu.style.left = `${event.pageX}px`;
menu.style.top = `${event.pageY}px`;
menu.classList.add('active');
menu.dataset.type = type;
menu.dataset.id = id;
}
document.addEventListener('click', () => {
const menu = document.getElementById('context-menu');
if (menu) menu.classList.remove('active');
});
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.add('active');
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.remove('active');
}
async function exportData(type, vehicleId) {
window.location.href = `${API_URL}/api/export/${type}?vehicle_id=${vehicleId}`;
}
function exportAllVehicleData(vehicleId) {
window.location.href = `${API_URL}/api/vehicles/${vehicleId}/export-all`;
}
function viewVehicle(vehicleId) {
setSelectedVehicle(vehicleId);
window.location.href = `/vehicle-detail?id=${vehicleId}`;
}
document.addEventListener('DOMContentLoaded', async () => {
const theme = localStorage.getItem('theme') || 'dark';
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
await loadUserSettings();
const vehicleSelect = document.getElementById('vehicle-select');
if (vehicleSelect) {
const selectedVehicleId = getSelectedVehicle();
if (selectedVehicleId) {
vehicleSelect.value = selectedVehicleId;
vehicleSelect.dispatchEvent(new Event('change'));
}
}
});
window.editServiceRecord = async function(id, vid) {
try {
const r = await apiRequest('/api/vehicles/'+vid+'/service-records/'+id);
document.getElementById('service-date').value = r.date.split('T')[0];
document.getElementById('service-odometer').value = r.odometer;
document.getElementById('service-description').value = r.description;
document.getElementById('service-category').value = r.category || 'Maintenance';
document.getElementById('service-cost').value = r.cost;
if(document.getElementById('service-notes')) document.getElementById('service-notes').value = r.notes || '';
showModal('add-service-modal');
window._editId = id;
window._editVid = vid;
} catch(e) { alert('Load failed'); }
};
window.deleteServiceRecord = async function(id, vid) {
if(!confirm('Delete this service record?')) return;
try {
await apiRequest('/api/vehicles/'+vid+'/service-records/'+id, {method:'DELETE'});
location.reload();
} catch(e) { alert('Delete failed'); }
};
window.editFuelRecord = async function(id, vid) {
try {
const r = await apiRequest('/api/vehicles/'+vid+'/fuel-records/'+id);
document.getElementById('fuel-date').value = r.date.split('T')[0];
document.getElementById('fuel-odometer').value = r.odometer;
document.getElementById('fuel-amount').value = r.fuel_amount;
document.getElementById('fuel-cost').value = r.cost;
document.getElementById('fuel-unit').value = r.unit;
if(document.getElementById('fuel-notes')) document.getElementById('fuel-notes').value = r.notes || '';
showModal('add-fuel-modal');
window._editId = id;
window._editVid = vid;
} catch(e) { alert('Load failed'); }
};
window.deleteFuelRecord = async function(id, vid) {
if(!confirm('Delete this fuel record?')) return;
try {
await apiRequest('/api/vehicles/'+vid+'/fuel-records/'+id, {method:'DELETE'});
location.reload();
} catch(e) { alert('Delete failed'); }
};
window.editReminder = async function(id, vid) {
try {
const r = await apiRequest('/api/vehicles/'+vid+'/reminders/'+id);
document.getElementById('reminder-description').value = r.description;
document.getElementById('reminder-urgency').value = r.urgency;
if(r.due_date) document.getElementById('reminder-due-date').value = r.due_date.split('T')[0];
if(document.getElementById('reminder-notes')) document.getElementById('reminder-notes').value = r.notes || '';
showModal('add-reminder-modal');
window._editId = id;
window._editVid = vid;
} catch(e) { alert('Load failed'); }
};
window.deleteReminder = async function(id, vid) {
if(!confirm('Delete this reminder?')) return;
try {
await apiRequest('/api/vehicles/'+vid+'/reminders/'+id, {method:'DELETE'});
location.reload();
} catch(e) { alert('Delete failed'); }
};

View file

@ -0,0 +1,383 @@
const API_URL = window.location.origin;
let currentUser = null;
let currentTheme = 'dark';
let userSettings = null;
function setSelectedVehicle(vehicleId) {
localStorage.setItem('selectedVehicleId', vehicleId);
}
function getSelectedVehicle() {
return localStorage.getItem('selectedVehicleId');
}
function clearSelectedVehicle() {
localStorage.removeItem('selectedVehicleId');
}
async function loadUserSettings() {
try {
const settings = await apiRequest('/api/settings');
userSettings = settings;
localStorage.setItem('userSettings', JSON.stringify(settings));
return settings;
} catch (error) {
const cached = localStorage.getItem('userSettings');
if (cached) {
userSettings = JSON.parse(cached);
return userSettings;
}
return { currency: 'GBP', unit_system: 'imperial', language: 'en' };
}
}
function getCurrencySymbol(currency) {
const symbols = {
'USD': '$',
'GBP': '£',
'RON': 'lei',
'EUR': '€'
};
return symbols[currency] || currency;
}
function formatCurrency(amount, currency = null) {
if (!currency) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
currency = settings.currency || 'GBP';
}
const numAmount = parseFloat(amount) || 0;
const symbol = getCurrencySymbol(currency);
if (currency === 'RON') {
return `${numAmount.toFixed(2)} ${symbol}`;
} else {
return `${symbol}${numAmount.toFixed(2)}`;
}
}
function formatDate(dateString) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
const lang = settings.language || 'en';
const locale = lang === 'ro' ? 'ro-RO' : 'en-GB';
return new Date(dateString).toLocaleDateString(locale);
}
async function apiRequest(endpoint, options = {}) {
try {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
...options.headers
}
});
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text);
throw new Error('Server returned non-JSON response');
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
return data;
} catch (error) {
console.error('API Request Error:', error);
throw error;
}
}
function toggleTheme() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
if (currentUser) {
apiRequest('/api/settings/theme', {
method: 'POST',
body: JSON.stringify({ theme: currentTheme })
}).catch(err => console.error('Failed to save theme:', err));
}
}
async function login(username, password) {
try {
const data = await apiRequest('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
currentUser = data.user;
currentTheme = data.user.theme;
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('userSettings', JSON.stringify(data.user));
if (data.user.must_change_credentials) {
window.location.href = '/first-login';
} else if (data.user.first_login) {
alert('Welcome! Please review your settings.');
window.location.href = '/dashboard';
} else {
window.location.href = '/dashboard';
}
} catch (error) {
alert('Login failed: ' + error.message);
}
}
async function register(username, email, password) {
try {
await apiRequest('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password })
});
alert('Registration successful! Please login.');
window.location.href = '/login';
} catch (error) {
alert('Registration failed: ' + error.message);
}
}
async function logout() {
try {
await apiRequest('/api/auth/logout', { method: 'POST' });
localStorage.clear();
sessionStorage.clear();
window.location.href = '/login';
} catch (error) {
alert('Logout failed: ' + error.message);
}
}
async function loadVehicles() {
try {
const vehicles = await apiRequest('/api/vehicles');
displayVehicles(vehicles);
} catch (error) {
console.error('Failed to load vehicles:', error);
}
}
function displayVehicles(vehicles) {
const container = document.getElementById('vehicles-container');
if (!container) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"unit_system":"imperial"}');
const unitLabel = settings.unit_system === 'metric' ? 'km' : 'miles';
container.innerHTML = vehicles.map(v => `
<div class="vehicle-card ${v.status === 'sold' ? 'sold' : ''}" onclick="viewVehicle(${v.id})">
${v.photo ? `<img src="${v.photo}" class="vehicle-photo" alt="${v.make} ${v.model}">` : ''}
<h3>${v.year} ${v.make} ${v.model}</h3>
<p>${v.vin || 'No VIN'}</p>
<p>Odometer: ${v.odometer.toLocaleString()} ${unitLabel}</p>
</div>
`).join('');
}
async function addVehicle(vehicleData) {
try {
await apiRequest('/api/vehicles', {
method: 'POST',
body: JSON.stringify(vehicleData)
});
loadVehicles();
closeModal('add-vehicle-modal');
document.getElementById('add-vehicle-form').reset();
const photoPreview = document.getElementById('photo-preview');
if (photoPreview) photoPreview.innerHTML = '';
} catch (error) {
alert('Failed to add vehicle: ' + error.message);
}
}
async function loadServiceRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
displayServiceRecords(records);
} catch (error) {
console.error('Failed to load service records:', error);
}
}
function displayServiceRecords(records) {
const tbody = document.getElementById('service-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
tbody.innerHTML = records.map(r => `
<tr oncontextmenu="showContextMenu(event, 'service', ${r.id})">
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.description}</td>
<td>${r.category || 'N/A'}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.notes || ''}</td>
<td>${r.document_path ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.document_path)}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
</tr>
`).join('');
}
async function loadFuelRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/fuel-records`);
displayFuelRecords(records);
calculateFuelStats(records);
} catch (error) {
console.error('Failed to load fuel records:', error);
}
}
function displayFuelRecords(records) {
const tbody = document.getElementById('fuel-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
tbody.innerHTML = records.map(r => `
<tr>
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.fuel_amount.toFixed(2)}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.distance || 'N/A'}</td>
<td>${r.fuel_economy ? r.fuel_economy.toFixed(2) : 'N/A'} ${r.unit}</td>
<td>${r.notes && r.notes.startsWith('attachment:') ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.notes.replace('attachment:', ''))}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
</tr>
`).join('');
}
function calculateFuelStats(records) {
const totalCost = records.reduce((sum, r) => sum + r.cost, 0);
const avgEconomy = records.filter(r => r.fuel_economy).reduce((sum, r, _, arr) =>
sum + r.fuel_economy / arr.length, 0);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
const totalCostEl = document.getElementById('total-fuel-cost');
const avgEconomyEl = document.getElementById('avg-fuel-economy');
if (totalCostEl) totalCostEl.textContent = formatCurrency(totalCost, settings.currency);
if (avgEconomyEl) avgEconomyEl.textContent = avgEconomy.toFixed(2);
}
async function loadReminders(vehicleId) {
try {
const reminders = await apiRequest(`/api/vehicles/${vehicleId}/reminders`);
displayReminders(reminders);
} catch (error) {
console.error('Failed to load reminders:', error);
}
}
function displayReminders(reminders) {
const container = document.getElementById('reminders-container');
if (!container) return;
container.innerHTML = reminders.map(r => `
<div class="kanban-item urgency-${r.urgency}">
<h4>${r.description}</h4>
<p>${r.due_date ? `Due: ${formatDate(r.due_date)}` : ''}</p>
<p>${r.due_odometer ? `At: ${r.due_odometer.toLocaleString()}` : ''}</p>
<p>${r.notes || ''}</p>
</div>
`).join('');
}
async function loadTodos(vehicleId) {
try {
const todos = await apiRequest(`/api/vehicles/${vehicleId}/todos`);
displayTodos(todos);
} catch (error) {
console.error('Failed to load todos:', error);
}
}
function displayTodos(todos) {
const statuses = ['planned', 'doing', 'testing', 'done'];
statuses.forEach(status => {
const column = document.getElementById(`todos-${status}`);
if (!column) return;
const filtered = todos.filter(t => t.status === status);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
column.innerHTML = filtered.map(t => `
<div class="kanban-item priority-${t.priority}" draggable="true" data-id="${t.id}">
<h4>${t.description}</h4>
<p>Cost: ${formatCurrency(t.cost, settings.currency)}</p>
<p>${t.type || ''}</p>
<small>${t.notes || ''}</small>
</div>
`).join('');
});
}
function showContextMenu(event, type, id) {
event.preventDefault();
const menu = document.getElementById('context-menu');
if (!menu) return;
menu.style.left = `${event.pageX}px`;
menu.style.top = `${event.pageY}px`;
menu.classList.add('active');
menu.dataset.type = type;
menu.dataset.id = id;
}
document.addEventListener('click', () => {
const menu = document.getElementById('context-menu');
if (menu) menu.classList.remove('active');
});
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.add('active');
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.remove('active');
}
async function exportData(type, vehicleId) {
window.location.href = `${API_URL}/api/export/${type}?vehicle_id=${vehicleId}`;
}
function exportAllVehicleData(vehicleId) {
window.location.href = `${API_URL}/api/vehicles/${vehicleId}/export-all`;
}
function viewVehicle(vehicleId) {
setSelectedVehicle(vehicleId);
window.location.href = `/vehicle-detail?id=${vehicleId}`;
}
document.addEventListener('DOMContentLoaded', async () => {
const theme = localStorage.getItem('theme') || 'dark';
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
await loadUserSettings();
const vehicleSelect = document.getElementById('vehicle-select');
if (vehicleSelect) {
const selectedVehicleId = getSelectedVehicle();
if (selectedVehicleId) {
vehicleSelect.value = selectedVehicleId;
vehicleSelect.dispatchEvent(new Event('change'));
}
}
});

View file

@ -0,0 +1,383 @@
const API_URL = window.location.origin;
let currentUser = null;
let currentTheme = 'dark';
let userSettings = null;
function setSelectedVehicle(vehicleId) {
localStorage.setItem('selectedVehicleId', vehicleId);
}
function getSelectedVehicle() {
return localStorage.getItem('selectedVehicleId');
}
function clearSelectedVehicle() {
localStorage.removeItem('selectedVehicleId');
}
async function loadUserSettings() {
try {
const settings = await apiRequest('/api/settings');
userSettings = settings;
localStorage.setItem('userSettings', JSON.stringify(settings));
return settings;
} catch (error) {
const cached = localStorage.getItem('userSettings');
if (cached) {
userSettings = JSON.parse(cached);
return userSettings;
}
return { currency: 'GBP', unit_system: 'imperial', language: 'en' };
}
}
function getCurrencySymbol(currency) {
const symbols = {
'USD': '$',
'GBP': '£',
'RON': 'lei',
'EUR': '€'
};
return symbols[currency] || currency;
}
function formatCurrency(amount, currency = null) {
if (!currency) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
currency = settings.currency || 'GBP';
}
const numAmount = parseFloat(amount) || 0;
const symbol = getCurrencySymbol(currency);
if (currency === 'RON') {
return `${numAmount.toFixed(2)} ${symbol}`;
} else {
return `${symbol}${numAmount.toFixed(2)}`;
}
}
function formatDate(dateString) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
const lang = settings.language || 'en';
const locale = lang === 'ro' ? 'ro-RO' : 'en-GB';
return new Date(dateString).toLocaleDateString(locale);
}
async function apiRequest(endpoint, options = {}) {
try {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
...options.headers
}
});
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const text = await response.text();
console.error('Non-JSON response:', text);
throw new Error('Server returned non-JSON response');
}
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
return data;
} catch (error) {
console.error('API Request Error:', error);
throw error;
}
}
function toggleTheme() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
if (currentUser) {
apiRequest('/api/settings/theme', {
method: 'POST',
body: JSON.stringify({ theme: currentTheme })
}).catch(err => console.error('Failed to save theme:', err));
}
}
async function login(username, password) {
try {
const data = await apiRequest('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
currentUser = data.user;
currentTheme = data.user.theme;
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('userSettings', JSON.stringify(data.user));
if (data.user.must_change_credentials) {
window.location.href = '/first-login';
} else if (data.user.first_login) {
alert('Welcome! Please review your settings.');
window.location.href = '/dashboard';
} else {
window.location.href = '/dashboard';
}
} catch (error) {
alert('Login failed: ' + error.message);
}
}
async function register(username, email, password) {
try {
await apiRequest('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password })
});
alert('Registration successful! Please login.');
window.location.href = '/login';
} catch (error) {
alert('Registration failed: ' + error.message);
}
}
async function logout() {
try {
await apiRequest('/api/auth/logout', { method: 'POST' });
localStorage.clear();
sessionStorage.clear();
window.location.href = '/login';
} catch (error) {
alert('Logout failed: ' + error.message);
}
}
async function loadVehicles() {
try {
const vehicles = await apiRequest('/api/vehicles');
displayVehicles(vehicles);
} catch (error) {
console.error('Failed to load vehicles:', error);
}
}
function displayVehicles(vehicles) {
const container = document.getElementById('vehicles-container');
if (!container) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"unit_system":"imperial"}');
const unitLabel = settings.unit_system === 'metric' ? 'km' : 'miles';
container.innerHTML = vehicles.map(v => `
<div class="vehicle-card ${v.status === 'sold' ? 'sold' : ''}" onclick="viewVehicle(${v.id})">
${v.photo ? `<img src="${v.photo}" class="vehicle-photo" alt="${v.make} ${v.model}">` : ''}
<h3>${v.year} ${v.make} ${v.model}</h3>
<p>${v.vin || 'No VIN'}</p>
<p>Odometer: ${v.odometer.toLocaleString()} ${unitLabel}</p>
</div>
`).join('');
}
async function addVehicle(vehicleData) {
try {
await apiRequest('/api/vehicles', {
method: 'POST',
body: JSON.stringify(vehicleData)
});
loadVehicles();
closeModal('add-vehicle-modal');
document.getElementById('add-vehicle-form').reset();
const photoPreview = document.getElementById('photo-preview');
if (photoPreview) photoPreview.innerHTML = '';
} catch (error) {
alert('Failed to add vehicle: ' + error.message);
}
}
async function loadServiceRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
displayServiceRecords(records);
} catch (error) {
console.error('Failed to load service records:', error);
}
}
function displayServiceRecords(records) {
const tbody = document.getElementById('service-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
tbody.innerHTML = records.map(r => `
<tr oncontextmenu="showContextMenu(event, 'service', ${r.id})">
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.description}</td>
<td>${r.category || 'N/A'}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.notes || ''}</td>
<td>${r.document_path ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.document_path)}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
</tr>
`).join('');
}
async function loadFuelRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/fuel-records`);
displayFuelRecords(records);
calculateFuelStats(records);
} catch (error) {
console.error('Failed to load fuel records:', error);
}
}
function displayFuelRecords(records) {
const tbody = document.getElementById('fuel-records-tbody');
if (!tbody) return;
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
tbody.innerHTML = records.map(r => `
<tr>
<td>${formatDate(r.date)}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.fuel_amount.toFixed(2)}</td>
<td>${formatCurrency(r.cost, settings.currency)}</td>
<td>${r.distance || 'N/A'}</td>
<td>${r.fuel_economy ? r.fuel_economy.toFixed(2) : 'N/A'} ${r.unit}</td>
<td>${r.notes && r.notes.startsWith('attachment:') ? `<a href="/api/attachments/download?path=${encodeURIComponent(r.notes.replace('attachment:', ''))}" class="btn" style="padding: 5px 10px; font-size: 12px;">Download</a>` : 'None'}</td>
</tr>
`).join('');
}
function calculateFuelStats(records) {
const totalCost = records.reduce((sum, r) => sum + r.cost, 0);
const avgEconomy = records.filter(r => r.fuel_economy).reduce((sum, r, _, arr) =>
sum + r.fuel_economy / arr.length, 0);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
const totalCostEl = document.getElementById('total-fuel-cost');
const avgEconomyEl = document.getElementById('avg-fuel-economy');
if (totalCostEl) totalCostEl.textContent = formatCurrency(totalCost, settings.currency);
if (avgEconomyEl) avgEconomyEl.textContent = avgEconomy.toFixed(2);
}
async function loadReminders(vehicleId) {
try {
const reminders = await apiRequest(`/api/vehicles/${vehicleId}/reminders`);
displayReminders(reminders);
} catch (error) {
console.error('Failed to load reminders:', error);
}
}
function displayReminders(reminders) {
const container = document.getElementById('reminders-container');
if (!container) return;
container.innerHTML = reminders.map(r => `
<div class="kanban-item urgency-${r.urgency}">
<h4>${r.description}</h4>
<p>${r.due_date ? `Due: ${formatDate(r.due_date)}` : ''}</p>
<p>${r.due_odometer ? `At: ${r.due_odometer.toLocaleString()}` : ''}</p>
<p>${r.notes || ''}</p>
</div>
`).join('');
}
async function loadTodos(vehicleId) {
try {
const todos = await apiRequest(`/api/vehicles/${vehicleId}/todos`);
displayTodos(todos);
} catch (error) {
console.error('Failed to load todos:', error);
}
}
function displayTodos(todos) {
const statuses = ['planned', 'doing', 'testing', 'done'];
statuses.forEach(status => {
const column = document.getElementById(`todos-${status}`);
if (!column) return;
const filtered = todos.filter(t => t.status === status);
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
column.innerHTML = filtered.map(t => `
<div class="kanban-item priority-${t.priority}" draggable="true" data-id="${t.id}">
<h4>${t.description}</h4>
<p>Cost: ${formatCurrency(t.cost, settings.currency)}</p>
<p>${t.type || ''}</p>
<small>${t.notes || ''}</small>
</div>
`).join('');
});
}
function showContextMenu(event, type, id) {
event.preventDefault();
const menu = document.getElementById('context-menu');
if (!menu) return;
menu.style.left = `${event.pageX}px`;
menu.style.top = `${event.pageY}px`;
menu.classList.add('active');
menu.dataset.type = type;
menu.dataset.id = id;
}
document.addEventListener('click', () => {
const menu = document.getElementById('context-menu');
if (menu) menu.classList.remove('active');
});
function showModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.add('active');
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.classList.remove('active');
}
async function exportData(type, vehicleId) {
window.location.href = `${API_URL}/api/export/${type}?vehicle_id=${vehicleId}`;
}
function exportAllVehicleData(vehicleId) {
window.location.href = `${API_URL}/api/vehicles/${vehicleId}/export-all`;
}
function viewVehicle(vehicleId) {
setSelectedVehicle(vehicleId);
window.location.href = `/vehicle-detail?id=${vehicleId}`;
}
document.addEventListener('DOMContentLoaded', async () => {
const theme = localStorage.getItem('theme') || 'dark';
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
await loadUserSettings();
const vehicleSelect = document.getElementById('vehicle-select');
if (vehicleSelect) {
const selectedVehicleId = getSelectedVehicle();
if (selectedVehicleId) {
vehicleSelect.value = selectedVehicleId;
vehicleSelect.dispatchEvent(new Event('change'));
}
}
});

View file

@ -0,0 +1,296 @@
const translations = {
en: {
dashboard: "Dashboard",
overview: "Overview",
service_records: "Service Records",
repairs: "Repairs",
fuel: "Fuel",
taxes: "Taxes",
notes: "Notes",
reminders: "Reminders",
settings: "Settings",
logout: "Logout",
toggle_theme: "Toggle Theme",
your_garage: "Your Garage",
add_vehicle: "Add Vehicle",
add_fuel_record: "Add Fuel Record",
add_service_record: "Add Service Record",
add_repair: "Add Repair",
add_tax_record: "Add Tax Record",
add_note: "Add Note",
add_reminder: "Add Reminder",
export_csv: "Export CSV",
export_all_data: "Export All Data",
back_to_vehicle: "Back to Vehicle",
date: "Date",
odometer: "Odometer",
description: "Description",
category: "Category",
cost: "Cost",
notes: "Notes",
attachment: "Attachment",
download: "Download",
fuel_amount: "Fuel Amount",
distance: "Distance",
economy: "Economy",
total_fuel_cost: "Total Fuel Cost",
average_fuel_economy: "Average Fuel Economy",
recurring: "Recurring",
type: "Type",
amount: "Amount",
save: "Save All Changes",
cancel: "Cancel",
edit: "Edit",
delete: "Delete",
add_record: "Add Record",
update: "Update",
create_backup: "Create Backup",
restore_backup: "Restore Backup",
language: "Language",
select_language: "Select Language",
save_language: "Save Language",
measurement_system: "Measurement System",
currency: "Currency",
user_management: "User Management",
change_password: "Change Password",
current_password: "Current Password",
new_password: "New Password",
confirm_password: "Confirm New Password",
year: "Year",
make: "Make",
model: "Model",
vin: "VIN",
license_plate: "Registration Number",
vehicle_photo: "Vehicle Photo",
edit_vehicle: "Edit Vehicle",
delete_vehicle: "Delete Vehicle",
last_odometer_reading: "Last Odometer Reading",
total_distance_traveled: "Total Distance Travelled",
total_cost: "Total Cost",
fuel_cost: "Fuel Cost",
service_cost: "Service Cost",
repairs_cost: "Repairs Cost",
taxes_cost: "Taxes Cost",
upgrades_cost: "Upgrades Cost",
recent_service_records: "Recent Service Records",
recent_fuel_records: "Recent Fuel Records",
active_reminders: "Active Reminders",
no_active_reminders: "No active reminders",
urgency: "Urgency",
due_date: "Due Date",
due_odometer: "Due Odometer",
registration: "Registration",
license: "Licence",
property_tax: "Road Tax",
insurance: "Insurance",
inspection: "MOT",
other: "Other",
maintenance: "Maintenance",
repair: "Repair",
upgrade: "Upgrade",
tax: "Tax",
note: "Note",
monthly: "Monthly",
quarterly: "Quarterly",
yearly: "Yearly",
metric: "Metric (km, litres)",
imperial: "Imperial (miles, gallons)",
us_dollar: "US Dollar (USD)",
british_pound: "British Pound (GBP)",
romanian_leu: "Romanian Leu (RON)",
euro: "Euro (EUR)",
supported_files: "Supported: PDF, Images, Text, Word, Excel",
no_file_selected: "No file selected",
upload_failed: "Upload failed",
save_successful: "Saved successfully",
delete_confirmation: "Are you sure you want to delete this?",
title: "Title",
content: "Content",
optional: "Optional",
mileage: "Mileage",
litres: "Litres",
gallons: "Gallons",
petrol: "Petrol",
diesel: "Diesel",
backup_restore_title: "Backup & Restore",
backup_restore_desc: "Create a complete backup of all your data or restore from a previous backup.",
units_currency: "Units & Currency",
manage_account: "Manage your account settings and password.",
app_info: "Application Info",
version: "Version",
database: "Database",
current_user: "Current User",
save_units: "Save Settings"
},
ro: {
dashboard: "Tablou de bord",
overview: "Prezentare generala",
service_records: "Inregistrari service",
repairs: "Reparatii",
fuel: "Combustibil",
taxes: "Taxe",
notes: "Notite",
reminders: "Memento-uri",
settings: "Setari",
logout: "Deconectare",
toggle_theme: "Schimba tema",
your_garage: "Garajul tau",
add_vehicle: "Adauga vehicul",
add_fuel_record: "Adauga alimentare",
add_service_record: "Adauga service",
add_repair: "Adauga reparatie",
add_tax_record: "Adauga taxa",
add_note: "Adauga notita",
add_reminder: "Adauga memento",
export_csv: "Exporta CSV",
export_all_data: "Exporta toate datele",
back_to_vehicle: "Inapoi la vehicul",
date: "Data",
odometer: "Kilometraj",
description: "Descriere",
category: "Categorie",
cost: "Cost",
notes: "Notite",
attachment: "Atasament",
download: "Descarca",
fuel_amount: "Cantitate combustibil",
distance: "Distanta",
economy: "Consum",
total_fuel_cost: "Cost total combustibil",
average_fuel_economy: "Consum mediu",
recurring: "Recurent",
type: "Tip",
amount: "Suma",
save: "Salveaza toate modificarile",
cancel: "Anuleaza",
edit: "Editeaza",
delete: "Sterge",
add_record: "Adauga inregistrare",
update: "Actualizeaza",
create_backup: "Creeaza backup",
restore_backup: "Restaureaza backup",
language: "Limba",
select_language: "Selecteaza limba",
save_language: "Salveaza limba",
measurement_system: "Sistem de masura",
currency: "Moneda",
user_management: "Gestionare utilizator",
change_password: "Schimba parola",
current_password: "Parola curenta",
new_password: "Parola noua",
confirm_password: "Confirma parola noua",
year: "An",
make: "Marca",
model: "Model",
vin: "VIN",
license_plate: "Numar inmatriculare",
vehicle_photo: "Fotografie vehicul",
edit_vehicle: "Editeaza vehicul",
delete_vehicle: "Sterge vehicul",
last_odometer_reading: "Ultima citire kilometraj",
total_distance_traveled: "Distanta totala parcursa",
total_cost: "Cost total",
fuel_cost: "Cost combustibil",
service_cost: "Cost service",
repairs_cost: "Cost reparatii",
taxes_cost: "Cost taxe",
upgrades_cost: "Cost imbunatatiri",
recent_service_records: "Inregistrari service recente",
recent_fuel_records: "Alimentari recente",
active_reminders: "Memento-uri active",
no_active_reminders: "Niciun memento activ",
urgency: "Urgenta",
due_date: "Data scadenta",
due_odometer: "Kilometraj scadent",
registration: "Inmatriculare",
license: "Licenta",
property_tax: "Taxa proprietate",
insurance: "Asigurare",
inspection: "Inspectie tehnica",
other: "Altele",
maintenance: "Intretinere",
repair: "Reparatie",
upgrade: "Imbunatatire",
tax: "Taxa",
note: "Notita",
monthly: "Lunar",
quarterly: "Trimestrial",
yearly: "Anual",
metric: "Metric (km, litri)",
imperial: "Imperial (mile, galoane)",
us_dollar: "Dolar american (USD)",
british_pound: "Lira sterlina (GBP)",
romanian_leu: "Leu romanesc (RON)",
euro: "Euro (EUR)",
supported_files: "Suportat: PDF, Imagini, Text, Word, Excel",
no_file_selected: "Niciun fisier selectat",
upload_failed: "Incarcare esuata",
save_successful: "Salvat cu succes",
delete_confirmation: "Sigur doriti sa stergeti?",
title: "Titlu",
content: "Continut",
optional: "Optional",
mileage: "Kilometraj",
litres: "Litri",
gallons: "Galoane",
petrol: "Benzina",
diesel: "Motorina",
backup_restore_title: "Backup & Restaurare",
backup_restore_desc: "Creaza un backup complet al datelor tale sau restaureaza dintr-un backup anterior.",
units_currency: "Unitati & Moneda",
manage_account: "Gestioneaza setarile contului si parola.",
app_info: "Informatii aplicatie",
version: "Versiune",
database: "Baza de date",
current_user: "Utilizator curent",
save_units: "Salveaza setari"
}
};
function getTranslation(key, lang = null) {
if (!lang) {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
lang = settings.language || 'en';
}
return translations[lang]?.[key] || translations['en'][key] || key;
}
function translatePage() {
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
const lang = settings.language || 'en';
document.querySelectorAll('[data-translate]').forEach(element => {
const key = element.getAttribute('data-translate');
const translation = getTranslation(key, lang);
if (element.tagName === 'INPUT' && (element.type === 'button' || element.type === 'submit')) {
element.value = translation;
} else if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
element.placeholder = translation;
} else if (element.tagName === 'OPTION') {
element.textContent = translation;
} else {
element.textContent = translation;
}
});
document.querySelectorAll('label[data-translate]').forEach(label => {
const key = label.getAttribute('data-translate');
const translation = getTranslation(key, lang);
const checkbox = label.querySelector('input[type="checkbox"]');
if (checkbox) {
label.childNodes.forEach(node => {
if (node.nodeType === 3) {
node.textContent = translation;
}
});
}
});
}
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
translatePage();
}, 100);
});