Initial commit: Masina-Dock Vehicle Management System
This commit is contained in:
commit
ae923e2c41
4999 changed files with 1607266 additions and 0 deletions
571
frontend/static/js/app.js
Normal file
571
frontend/static/js/app.js
Normal 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';
|
||||
}
|
||||
}
|
||||
450
frontend/static/js/app.js.backup-all-sections
Normal file
450
frontend/static/js/app.js.backup-all-sections
Normal 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'); }
|
||||
};
|
||||
383
frontend/static/js/app.js.backup-before-edit
Normal file
383
frontend/static/js/app.js.backup-before-edit
Normal 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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
383
frontend/static/js/app.js.backup-clean
Normal file
383
frontend/static/js/app.js.backup-clean
Normal 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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
296
frontend/static/js/translations.js
Normal file
296
frontend/static/js/translations.js
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue