Initial commit: Masina-Dock Vehicle Management System
This commit is contained in:
commit
ae923e2c41
4999 changed files with 1607266 additions and 0 deletions
127
frontend/templates/dashboard.html
Normal file
127
frontend/templates/dashboard.html
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tablou de bord - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" class="active" data-translate="dashboard">Tablou de bord</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Schimba tema</button>
|
||||
<a href="/settings" class="btn" data-translate="settings">Setari</a>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Deconectare</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h2 data-translate="your_garage">Garajul tau</h2>
|
||||
<button class="btn btn-success" onclick="showModal('add-vehicle-modal')" data-translate="add_vehicle">+ Adauga vehicul</button>
|
||||
|
||||
<div id="vehicles-container" class="vehicle-grid"></div>
|
||||
</div>
|
||||
|
||||
<div id="add-vehicle-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_vehicle">Adauga vehicul</h2>
|
||||
<form id="add-vehicle-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="photo" data-translate="vehicle_photo">Fotografie vehicul</label>
|
||||
<input type="file" id="photo" name="photo" accept="image/*">
|
||||
<div id="photo-preview" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="year" data-translate="year">An</label>
|
||||
<input type="number" id="year" name="year" required min="1900" max="2099">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="make" data-translate="make">Marca</label>
|
||||
<input type="text" id="make" name="make" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="model" data-translate="model">Model</label>
|
||||
<input type="text" id="model" name="model" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vin" data-translate="vin">VIN (Optional)</label>
|
||||
<input type="text" id="vin" name="vin" maxlength="17">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="license_plate" data-translate="license_plate">Numar inmatriculare (Optional)</label>
|
||||
<input type="text" id="license_plate" name="license_plate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="odometer" data-translate="odometer">Kilometraj curent</label>
|
||||
<input type="number" id="odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_vehicle">Adauga vehicul</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-vehicle-modal')" data-translate="cancel">Anuleaza</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadVehicles();
|
||||
});
|
||||
|
||||
document.getElementById('photo').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
document.getElementById('photo-preview').innerHTML =
|
||||
`<img src="${e.target.result}" style="max-width: 200px; border-radius: 8px;">`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('add-vehicle-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let photoUrl = null;
|
||||
const photoFile = document.getElementById('photo').files[0];
|
||||
|
||||
if (photoFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('photo', photoFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/photo', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
photoUrl = data.photo_url;
|
||||
} catch (error) {
|
||||
console.error('Photo upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const vehicleData = {
|
||||
year: parseInt(document.getElementById('year').value),
|
||||
make: document.getElementById('make').value,
|
||||
model: document.getElementById('model').value,
|
||||
vin: document.getElementById('vin').value || null,
|
||||
license_plate: document.getElementById('license_plate').value || null,
|
||||
odometer: parseInt(document.getElementById('odometer').value),
|
||||
photo: photoUrl
|
||||
};
|
||||
|
||||
await addVehicle(vehicleData);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
88
frontend/templates/first_login.html
Normal file
88
frontend/templates/first_login.html
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Update Credentials - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h1>Masina-Dock</h1>
|
||||
<h2>Update Your Credentials</h2>
|
||||
<p style="color: var(--text-secondary); text-align: center; margin-bottom: 20px;">
|
||||
For security reasons, you must change your username, email, and password before continuing.
|
||||
</p>
|
||||
|
||||
<form id="update-credentials-form">
|
||||
<div class="form-group">
|
||||
<label for="new-username">New Username</label>
|
||||
<input type="text" id="new-username" name="username" required minlength="3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-email">New Email</label>
|
||||
<input type="email" id="new-email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-password">New Password</label>
|
||||
<input type="password" id="new-password" name="password" required minlength="8">
|
||||
<small style="color: var(--text-secondary);">
|
||||
Must be at least 8 characters with uppercase, lowercase, and numbers
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password">Confirm New Password</label>
|
||||
<input type="password" id="confirm-password" name="confirm_password" required minlength="8">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success" style="width: 100%;">Update Credentials</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
document.getElementById('update-credentials-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const newPassword = document.getElementById('new-password').value;
|
||||
const confirmPassword = document.getElementById('confirm-password').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('Passwords do not match!');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
username: document.getElementById('new-username').value,
|
||||
email: document.getElementById('new-email').value,
|
||||
password: newPassword
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/update-credentials', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert('Credentials updated successfully! Please login with your new credentials.');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
alert('Error: ' + (data.error || 'Failed to update credentials'));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Failed to update credentials: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
V
|
||||
217
frontend/templates/fuel.html
Normal file
217
frontend/templates/fuel.html
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Combustibil - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" class="active" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="fuel">Fuel Records</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-fuel-modal')" data-translate="add_fuel_record">+ Add Fuel Record</button>
|
||||
<button class="btn" onclick="exportFuelRecords()" data-translate="export_csv">Export CSV</button>
|
||||
|
||||
<div class="stats-grid" style="margin: 20px 0;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="total_fuel_cost">Total Fuel Cost</div>
|
||||
<div class="stat-value" id="total-fuel-cost">0.00 lei</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="average_fuel_economy">Average Fuel Economy</div>
|
||||
<div class="stat-value" id="avg-fuel-economy">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="odometer">Odometer</th>
|
||||
<th data-translate="fuel_amount">Fuel Amount</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="distance">Distance</th>
|
||||
<th data-translate="economy">Economy</th>
|
||||
<th data-translate="attachment">Attachment</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fuel-records-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-fuel-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_fuel_record">Add Fuel Record</h2>
|
||||
<form id="add-fuel-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="fuel-date" data-translate="date">Date</label>
|
||||
<input type="date" id="fuel-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-odometer" data-translate="odometer">Odometer</label>
|
||||
<input type="number" id="fuel-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-amount" data-translate="fuel_amount">Fuel Amount</label>
|
||||
<input type="number" id="fuel-amount" name="fuel_amount" step="0.01" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-unit">Unit</label>
|
||||
<select id="fuel-unit" name="unit">
|
||||
<option value="MPG">MPG (US)</option>
|
||||
<option value="UK MPG">UK MPG</option>
|
||||
<option value="L/100KM">L/100KM</option>
|
||||
<option value="KM/L">KM/L</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-cost" data-translate="cost">Total Cost</label>
|
||||
<input type="number" id="fuel-cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="fuel-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-attachment" data-translate="optional">Receipt (Optional)</label>
|
||||
<input type="file" id="fuel-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt">
|
||||
<small data-translate="supported_files">Supported: PDF, Images, Text</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Record</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-fuel-modal')" data-translate="cancel">Cancel</button>
|
||||
resetEditMode();
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Va rugam selectati un vehicul din tabloul de bord.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadFuelRecords(currentVehicleId);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Combustibil`;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
function exportFuelRecords() {
|
||||
if (currentVehicleId) {
|
||||
exportData('fuel_records', currentVehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('add-fuel-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('fuel-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('fuel-date').value,
|
||||
odometer: parseInt(document.getElementById('fuel-odometer').value),
|
||||
fuel_amount: parseFloat(document.getElementById('fuel-amount').value),
|
||||
unit: document.getElementById('fuel-unit').value,
|
||||
cost: parseFloat(document.getElementById('fuel-cost').value) || 0,
|
||||
notes: document.getElementById('fuel-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
if (editingFuelRecordId) {
|
||||
await apiRequest(`/api/vehicles/${editingVehicleId}/fuel-records/${editingFuelRecordId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
editingFuelRecordId = null;
|
||||
editingVehicleId = null;
|
||||
const modal = document.getElementById('add-fuel-modal');
|
||||
const modalTitle = modal.querySelector('h2');
|
||||
if (modalTitle) modalTitle.textContent = 'Add Fuel Record';
|
||||
} else {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/fuel-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
}
|
||||
closeModal('add-fuel-modal');
|
||||
resetEditMode();
|
||||
await loadFuelRecords(currentVehicleId);
|
||||
document.getElementById('add-fuel-form').reset();
|
||||
} catch (error) {
|
||||
alert('Adaugarea inregistrarii a esuat: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
201
frontend/templates/fuel.html.backup
Normal file
201
frontend/templates/fuel.html.backup
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Combustibil - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" class="active" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="fuel">Fuel Records</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-fuel-modal')" data-translate="add_fuel_record">+ Add Fuel Record</button>
|
||||
<button class="btn" onclick="exportFuelRecords()" data-translate="export_csv">Export CSV</button>
|
||||
|
||||
<div class="stats-grid" style="margin: 20px 0;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="total_fuel_cost">Total Fuel Cost</div>
|
||||
<div class="stat-value" id="total-fuel-cost">0.00 lei</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="average_fuel_economy">Average Fuel Economy</div>
|
||||
<div class="stat-value" id="avg-fuel-economy">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="odometer">Odometer</th>
|
||||
<th data-translate="fuel_amount">Fuel Amount</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="distance">Distance</th>
|
||||
<th data-translate="economy">Economy</th>
|
||||
<th data-translate="attachment">Attachment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fuel-records-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-fuel-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_fuel_record">Add Fuel Record</h2>
|
||||
<form id="add-fuel-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="fuel-date" data-translate="date">Date</label>
|
||||
<input type="date" id="fuel-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-odometer" data-translate="odometer">Odometer</label>
|
||||
<input type="number" id="fuel-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-amount" data-translate="fuel_amount">Fuel Amount</label>
|
||||
<input type="number" id="fuel-amount" name="fuel_amount" step="0.01" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-unit">Unit</label>
|
||||
<select id="fuel-unit" name="unit">
|
||||
<option value="MPG">MPG (US)</option>
|
||||
<option value="UK MPG">UK MPG</option>
|
||||
<option value="L/100KM">L/100KM</option>
|
||||
<option value="KM/L">KM/L</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-cost" data-translate="cost">Total Cost</label>
|
||||
<input type="number" id="fuel-cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="fuel-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fuel-attachment" data-translate="optional">Receipt (Optional)</label>
|
||||
<input type="file" id="fuel-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt">
|
||||
<small data-translate="supported_files">Supported: PDF, Images, Text</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Record</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-fuel-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Va rugam selectati un vehicul din tabloul de bord.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadFuelRecords(currentVehicleId);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Combustibil`;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
function exportFuelRecords() {
|
||||
if (currentVehicleId) {
|
||||
exportData('fuel_records', currentVehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('add-fuel-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('fuel-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = 'attachment:' + data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('fuel-date').value,
|
||||
odometer: parseInt(document.getElementById('fuel-odometer').value),
|
||||
fuel_amount: parseFloat(document.getElementById('fuel-amount').value),
|
||||
unit: document.getElementById('fuel-unit').value,
|
||||
cost: parseFloat(document.getElementById('fuel-cost').value) || 0,
|
||||
notes: attachmentPath || document.getElementById('fuel-notes').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/fuel-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-fuel-modal');
|
||||
await loadFuelRecords(currentVehicleId);
|
||||
document.getElementById('add-fuel-form').reset();
|
||||
} catch (error) {
|
||||
alert('Adaugarea inregistrarii a esuat: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
22
frontend/templates/index.html
Normal file
22
frontend/templates/index.html
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Masina-Dock - Vehicle Maintenance Tracker</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card" style="max-width: 500px; margin: 100px auto; text-align: center;">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock Logo" style="width: 120px; margin: 20px auto;">
|
||||
<h1>Welcome to Masina-Dock</h1>
|
||||
<p>Your personal vehicle maintenance and fuel economy tracker</p>
|
||||
<div style="display: flex; gap: 20px; justify-content: center; margin-top: 30px;">
|
||||
<a href="/login" class="btn">Login</a>
|
||||
<a href="/register" class="btn btn-success">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
185
frontend/templates/login.html
Normal file
185
frontend/templates/login.html
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.auth-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.auth-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.auth-card h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.auth-card h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h1>Masina-Dock</h1>
|
||||
<h2>Login</h2>
|
||||
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success" style="width: 100%;">Login</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align: center; margin-top: 20px;">
|
||||
Don't have an account? <a href="/register" style="color: var(--primary);">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-alert-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 400px; text-align: center;">
|
||||
<p id="alert-message" style="margin: 20px 0; font-size: 16px;"></p>
|
||||
<button class="btn btn-success" onclick="closeCustomAlert()" style="width: 100px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="2fa-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 400px;">
|
||||
<h2>Two-Factor Authentication</h2>
|
||||
<p>Enter the 6-digit code from your authenticator app:</p>
|
||||
<form id="2fa-form">
|
||||
<div class="form-group">
|
||||
<label for="2fa-code">Authentication Code</label>
|
||||
<input type="text" id="2fa-code" name="code" required maxlength="6" placeholder="000000" style="text-align: center; font-size: 24px; letter-spacing: 5px;">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success" style="width: 100%;">Verify</button>
|
||||
</form>
|
||||
<p style="text-align: center; margin-top: 15px; color: var(--text-secondary); font-size: 14px;">
|
||||
Lost your device? Use a backup code instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let tempUserId = null;
|
||||
|
||||
function showCustomAlert(message) {
|
||||
document.getElementById('alert-message').textContent = message;
|
||||
document.getElementById('custom-alert-modal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeCustomAlert() {
|
||||
document.getElementById('custom-alert-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
function showModal(modalId) {
|
||||
document.getElementById(modalId).classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
document.getElementById(modalId).classList.remove('active');
|
||||
}
|
||||
|
||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
if (data.requires_2fa) {
|
||||
tempUserId = data.user_id;
|
||||
showModal('2fa-modal');
|
||||
} else {
|
||||
localStorage.setItem('userSettings', JSON.stringify(data.user));
|
||||
|
||||
if (data.user.must_change_credentials) {
|
||||
window.location.href = '/first-login';
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showCustomAlert(data.error || 'Login failed');
|
||||
}
|
||||
} catch (error) {
|
||||
showCustomAlert('Login failed: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('2fa-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const code = document.getElementById('2fa-code').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/verify-2fa', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: tempUserId,
|
||||
code: code
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
localStorage.setItem('userSettings', JSON.stringify(data.user));
|
||||
|
||||
if (data.user.must_change_credentials) {
|
||||
window.location.href = '/first-login';
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
} else {
|
||||
showCustomAlert(data.error || 'Invalid 2FA code');
|
||||
document.getElementById('2fa-code').value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
showCustomAlert('Verification failed: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
163
frontend/templates/notes.html
Normal file
163
frontend/templates/notes.html
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notes - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" class="active" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="notes">Notes</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-note-modal')" data-translate="add_note">+ Add Note</button>
|
||||
|
||||
<div id="notes-container" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="add-note-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_note">Add Note</h2>
|
||||
<form id="add-note-form">
|
||||
<div class="form-group">
|
||||
<label for="note-title" data-translate="title">Title</label>
|
||||
<input type="text" id="note-title" name="title" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="note-content" data-translate="content">Content</label>
|
||||
<textarea id="note-content" name="content" rows="8" required></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Note</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-note-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadNotes();
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Notite`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Notes`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotes() {
|
||||
try {
|
||||
const records = await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`);
|
||||
const notes = records.filter(r => r.category === 'Note');
|
||||
displayNotes(notes);
|
||||
} catch (error) {
|
||||
console.error('Failed to load notes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayNotes(notes) {
|
||||
const container = document.getElementById('notes-container');
|
||||
if (!container) return;
|
||||
|
||||
if (notes.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-secondary); text-align: center; grid-column: 1 / -1;">No notes yet</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = notes.map(n => `
|
||||
<div class="card" style="padding: 20px;">
|
||||
<h3 style="margin-top: 0;">${n.description}</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 14px; margin-bottom: 15px;">${formatDate(n.date)}</p>
|
||||
<p style="white-space: pre-wrap;">${n.notes || ''}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('add-note-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
odometer: 0,
|
||||
description: document.getElementById('note-title').value,
|
||||
category: 'Note',
|
||||
cost: 0,
|
||||
notes: document.getElementById('note-content').value,
|
||||
document_path: null
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-note-modal');
|
||||
await loadNotes();
|
||||
document.getElementById('add-note-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add note: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
163
frontend/templates/notes.html.backup
Normal file
163
frontend/templates/notes.html.backup
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notes - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" class="active" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="notes">Notes</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-note-modal')" data-translate="add_note">+ Add Note</button>
|
||||
|
||||
<div id="notes-container" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="add-note-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_note">Add Note</h2>
|
||||
<form id="add-note-form">
|
||||
<div class="form-group">
|
||||
<label for="note-title" data-translate="title">Title</label>
|
||||
<input type="text" id="note-title" name="title" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="note-content" data-translate="content">Content</label>
|
||||
<textarea id="note-content" name="content" rows="8" required></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Note</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-note-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadNotes();
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Notite`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Notes`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNotes() {
|
||||
try {
|
||||
const records = await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`);
|
||||
const notes = records.filter(r => r.category === 'Note');
|
||||
displayNotes(notes);
|
||||
} catch (error) {
|
||||
console.error('Failed to load notes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayNotes(notes) {
|
||||
const container = document.getElementById('notes-container');
|
||||
if (!container) return;
|
||||
|
||||
if (notes.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-secondary); text-align: center; grid-column: 1 / -1;">No notes yet</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = notes.map(n => `
|
||||
<div class="card" style="padding: 20px;">
|
||||
<h3 style="margin-top: 0;">${n.description}</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 14px; margin-bottom: 15px;">${formatDate(n.date)}</p>
|
||||
<p style="white-space: pre-wrap;">${n.notes || ''}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('add-note-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
odometer: 0,
|
||||
description: document.getElementById('note-title').value,
|
||||
category: 'Note',
|
||||
cost: 0,
|
||||
notes: document.getElementById('note-content').value,
|
||||
document_path: null
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-note-modal');
|
||||
await loadNotes();
|
||||
document.getElementById('add-note-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add note: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
150
frontend/templates/planner.html
Normal file
150
frontend/templates/planner.html
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Planner - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
<a href="/vehicles">Vehicles</a>
|
||||
<a href="/planner" class="active">Planner</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()">🌙/☀️</button>
|
||||
<button class="btn btn-danger" onclick="logout()">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h2>Maintenance Planner</h2>
|
||||
<p>Select a vehicle to view and manage maintenance tasks</p>
|
||||
|
||||
<div class="form-group" style="max-width: 400px;">
|
||||
<label for="vehicle-select">Select Vehicle</label>
|
||||
<select id="vehicle-select" onchange="loadTodosForVehicle(this.value)">
|
||||
<option value="">-- Select a vehicle --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="planner-content" style="display: none;">
|
||||
<button class="btn btn-success" onclick="showModal('add-todo-modal')">+ Add Task</button>
|
||||
|
||||
<div class="kanban-board">
|
||||
<div class="kanban-column">
|
||||
<h3>Planned</h3>
|
||||
<div id="todos-planned"></div>
|
||||
</div>
|
||||
<div class="kanban-column">
|
||||
<h3>Doing</h3>
|
||||
<div id="todos-doing"></div>
|
||||
</div>
|
||||
<div class="kanban-column">
|
||||
<h3>Testing</h3>
|
||||
<div id="todos-testing"></div>
|
||||
</div>
|
||||
<div class="kanban-column">
|
||||
<h3>Done</h3>
|
||||
<div id="todos-done"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-todo-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Add New Task</h2>
|
||||
<form id="add-todo-form">
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<input type="text" id="description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cost">Estimated Cost</label>
|
||||
<input type="number" id="cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="priority">Priority</label>
|
||||
<select id="priority" name="priority">
|
||||
<option value="low">Low</option>
|
||||
<option value="medium" selected>Medium</option>
|
||||
<option value="high">High</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="type">Type</label>
|
||||
<input type="text" id="type" name="type" placeholder="e.g., Oil Change, Brake Service">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">Notes</label>
|
||||
<textarea id="notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success">Add Task</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-todo-modal')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const vehicles = await apiRequest('/api/vehicles');
|
||||
const select = document.getElementById('vehicle-select');
|
||||
vehicles.forEach(v => {
|
||||
const option = document.createElement('option');
|
||||
option.value = v.id;
|
||||
option.textContent = `${v.year} ${v.make} ${v.model}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
});
|
||||
|
||||
async function loadTodosForVehicle(vehicleId) {
|
||||
if (!vehicleId) {
|
||||
document.getElementById('planner-content').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
currentVehicleId = vehicleId;
|
||||
document.getElementById('planner-content').style.display = 'block';
|
||||
await loadTodos(vehicleId);
|
||||
}
|
||||
|
||||
document.getElementById('add-todo-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle first');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
description: document.getElementById('description').value,
|
||||
cost: parseFloat(document.getElementById('cost').value) || 0,
|
||||
priority: document.getElementById('priority').value,
|
||||
type: document.getElementById('type').value || null,
|
||||
notes: document.getElementById('notes').value || null,
|
||||
status: 'planned'
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/todos`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-todo-modal');
|
||||
await loadTodos(currentVehicleId);
|
||||
document.getElementById('add-todo-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add task: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
123
frontend/templates/register.html
Normal file
123
frontend/templates/register.html
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.auth-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.auth-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.auth-card h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.auth-card h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<h1>Masina-Dock</h1>
|
||||
<h2>Register for Masina-Dock</h2>
|
||||
|
||||
<form id="register-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required minlength="3" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required minlength="8">
|
||||
<small style="color: var(--text-secondary);">
|
||||
Must be at least 8 characters with uppercase, lowercase, and numbers
|
||||
</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success" style="width: 100%;">Register</button>
|
||||
</form>
|
||||
|
||||
<p style="text-align: center; margin-top: 20px;">
|
||||
Already have an account? <a href="/login" style="color: var(--primary);">Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-alert-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 400px; text-align: center;">
|
||||
<p id="alert-message" style="margin: 20px 0; font-size: 16px;"></p>
|
||||
<button class="btn btn-success" onclick="closeCustomAlert()" style="width: 100px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
function showCustomAlert(message, redirectUrl = null) {
|
||||
document.getElementById('alert-message').textContent = message;
|
||||
document.getElementById('custom-alert-modal').classList.add('active');
|
||||
|
||||
if (redirectUrl) {
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectUrl;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function closeCustomAlert() {
|
||||
document.getElementById('custom-alert-modal').classList.remove('active');
|
||||
}
|
||||
|
||||
document.getElementById('register-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value;
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: JSON.stringify({ username, email, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showCustomAlert('Registration successful! Please login.', '/login');
|
||||
} else {
|
||||
showCustomAlert('Error: ' + (data.error || 'Registration failed'));
|
||||
}
|
||||
} catch (error) {
|
||||
showCustomAlert('Registration failed: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
206
frontend/templates/reminders.html
Normal file
206
frontend/templates/reminders.html
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reminders - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" class="active" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="reminders">Reminders</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-reminder-modal')" data-translate="add_reminder">+ Add Reminder</button>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="description">Description</th>
|
||||
<th data-translate="urgency">Urgency</th>
|
||||
<th data-translate="due_date">Due Date</th>
|
||||
<th data-translate="due_odometer">Due Odometer</th>
|
||||
<th data-translate="notes">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reminders-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-reminder-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_reminder">Add Reminder</h2>
|
||||
<form id="add-reminder-form">
|
||||
<div class="form-group">
|
||||
<label for="reminder-description" data-translate="description">Description</label>
|
||||
<input type="text" id="reminder-description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-urgency" data-translate="urgency">Urgency</label>
|
||||
<select id="reminder-urgency" name="urgency">
|
||||
<option value="not_urgent">Not Urgent</option>
|
||||
<option value="moderate">Moderate</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-date" data-translate="due_date">Due Date (Optional)</label>
|
||||
<input type="date" id="reminder-date" name="due_date">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-odometer" data-translate="due_odometer">Due Odometer (Optional)</label>
|
||||
<input type="number" id="reminder-odometer" name="due_odometer" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="reminder-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Reminder</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-reminder-modal')" data-translate="cancel">Cancel</button>
|
||||
resetEditMode();
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadRemindersData();
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Memento-uri`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Reminders`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRemindersData() {
|
||||
try {
|
||||
const reminders = await apiRequest(`/api/vehicles/${currentVehicleId}/reminders`);
|
||||
displayRemindersTable(reminders);
|
||||
} catch (error) {
|
||||
console.error('Failed to load reminders:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRemindersTable(reminders) {
|
||||
const tbody = document.getElementById('reminders-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (reminders.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center;">No reminders</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = reminders.map(r => `
|
||||
<tr class="urgency-${r.urgency}">
|
||||
<td>${r.description}</td>
|
||||
<td>${r.urgency}</td>
|
||||
<td>${r.due_date ? formatDate(r.due_date) : '-'}</td>
|
||||
<td>${r.due_odometer ? r.due_odometer.toLocaleString() : '-'}</td>
|
||||
<td>${r.notes || ''}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('add-reminder-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
description: document.getElementById('reminder-description').value,
|
||||
urgency: document.getElementById('reminder-urgency').value,
|
||||
due_date: document.getElementById('reminder-date').value || null,
|
||||
due_odometer: parseInt(document.getElementById('reminder-odometer').value) || null,
|
||||
notes: document.getElementById('reminder-notes').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
if (editingReminderId) {
|
||||
await apiRequest(`/api/vehicles/${editingVehicleId}/reminders/${editingReminderId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
editingReminderId = null;
|
||||
editingVehicleId = null;
|
||||
const modal = document.getElementById('add-reminder-modal');
|
||||
const modalTitle = modal.querySelector('h2');
|
||||
if (modalTitle) modalTitle.textContent = 'Add Reminder';
|
||||
} else {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/reminders`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
}
|
||||
closeModal('add-reminder-modal');
|
||||
resetEditMode();
|
||||
await loadTodos(currentVehicleId);
|
||||
document.getElementById('add-reminder-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add reminder: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
193
frontend/templates/reminders.html.backup
Normal file
193
frontend/templates/reminders.html.backup
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reminders - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" class="active" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="reminders">Reminders</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-reminder-modal')" data-translate="add_reminder">+ Add Reminder</button>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="description">Description</th>
|
||||
<th data-translate="urgency">Urgency</th>
|
||||
<th data-translate="due_date">Due Date</th>
|
||||
<th data-translate="due_odometer">Due Odometer</th>
|
||||
<th data-translate="notes">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="reminders-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-reminder-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_reminder">Add Reminder</h2>
|
||||
<form id="add-reminder-form">
|
||||
<div class="form-group">
|
||||
<label for="reminder-description" data-translate="description">Description</label>
|
||||
<input type="text" id="reminder-description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-urgency" data-translate="urgency">Urgency</label>
|
||||
<select id="reminder-urgency" name="urgency">
|
||||
<option value="not_urgent">Not Urgent</option>
|
||||
<option value="moderate">Moderate</option>
|
||||
<option value="urgent">Urgent</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-date" data-translate="due_date">Due Date (Optional)</label>
|
||||
<input type="date" id="reminder-date" name="due_date">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-odometer" data-translate="due_odometer">Due Odometer (Optional)</label>
|
||||
<input type="number" id="reminder-odometer" name="due_odometer" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="reminder-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="reminder-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Reminder</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-reminder-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadRemindersData();
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Memento-uri`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Reminders`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRemindersData() {
|
||||
try {
|
||||
const reminders = await apiRequest(`/api/vehicles/${currentVehicleId}/reminders`);
|
||||
displayRemindersTable(reminders);
|
||||
} catch (error) {
|
||||
console.error('Failed to load reminders:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRemindersTable(reminders) {
|
||||
const tbody = document.getElementById('reminders-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (reminders.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center;">No reminders</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = reminders.map(r => `
|
||||
<tr class="urgency-${r.urgency}">
|
||||
<td>${r.description}</td>
|
||||
<td>${r.urgency}</td>
|
||||
<td>${r.due_date ? formatDate(r.due_date) : '-'}</td>
|
||||
<td>${r.due_odometer ? r.due_odometer.toLocaleString() : '-'}</td>
|
||||
<td>${r.notes || ''}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('add-reminder-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
description: document.getElementById('reminder-description').value,
|
||||
urgency: document.getElementById('reminder-urgency').value,
|
||||
due_date: document.getElementById('reminder-date').value || null,
|
||||
due_odometer: document.getElementById('reminder-odometer').value || null,
|
||||
notes: document.getElementById('reminder-notes').value || null,
|
||||
recurring: false
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/reminders`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-reminder-modal');
|
||||
await loadRemindersData();
|
||||
document.getElementById('add-reminder-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add reminder: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
220
frontend/templates/repairs.html
Normal file
220
frontend/templates/repairs.html
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Repairs - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" class="active" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="repairs">Repairs</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-repair-modal')" data-translate="add_repair">+ Add Repair</button>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="odometer">Odometer</th>
|
||||
<th data-translate="description">Description</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="notes">Notes</th>
|
||||
<th data-translate="attachment">Attachment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="repairs-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-repair-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_repair">Add Repair</h2>
|
||||
<form id="add-repair-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="repair-date" data-translate="date">Date</label>
|
||||
<input type="date" id="repair-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-odometer" data-translate="odometer">Odometer</label>
|
||||
<input type="number" id="repair-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-description" data-translate="description">Description</label>
|
||||
<input type="text" id="repair-description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-cost" data-translate="cost">Cost</label>
|
||||
<input type="number" id="repair-cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="repair-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-attachment" data-translate="optional">Attachment (Optional)</label>
|
||||
<input type="file" id="repair-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt,.doc,.docx,.xls,.xlsx">
|
||||
<small data-translate="supported_files">Supported: PDF, Images, Text, Word, Excel</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Record</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-repair-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadRepairs();
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Reparatii`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Repairs`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRepairs() {
|
||||
try {
|
||||
const records = await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`);
|
||||
const repairs = records.filter(r => r.category === 'Repair');
|
||||
displayRepairs(repairs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load repairs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRepairs(repairs) {
|
||||
const tbody = document.getElementById('repairs-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
|
||||
const currency = settings.currency || 'GBP';
|
||||
|
||||
if (repairs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">No repairs recorded</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = repairs.map(r => `
|
||||
<tr>
|
||||
<td>${formatDate(r.date)}</td>
|
||||
<td>${r.odometer.toLocaleString()}</td>
|
||||
<td>${r.description}</td>
|
||||
<td>${formatCurrency(r.cost, 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('');
|
||||
}
|
||||
|
||||
document.getElementById('add-repair-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('repair-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('repair-date').value,
|
||||
odometer: parseInt(document.getElementById('repair-odometer').value),
|
||||
description: document.getElementById('repair-description').value,
|
||||
category: 'Repair',
|
||||
cost: parseFloat(document.getElementById('repair-cost').value) || 0,
|
||||
notes: document.getElementById('repair-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-repair-modal');
|
||||
await loadRepairs();
|
||||
document.getElementById('add-repair-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add repair: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
220
frontend/templates/repairs.html.backup
Normal file
220
frontend/templates/repairs.html.backup
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Repairs - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" class="active" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="repairs">Repairs</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-repair-modal')" data-translate="add_repair">+ Add Repair</button>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="odometer">Odometer</th>
|
||||
<th data-translate="description">Description</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="notes">Notes</th>
|
||||
<th data-translate="attachment">Attachment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="repairs-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-repair-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_repair">Add Repair</h2>
|
||||
<form id="add-repair-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="repair-date" data-translate="date">Date</label>
|
||||
<input type="date" id="repair-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-odometer" data-translate="odometer">Odometer</label>
|
||||
<input type="number" id="repair-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-description" data-translate="description">Description</label>
|
||||
<input type="text" id="repair-description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-cost" data-translate="cost">Cost</label>
|
||||
<input type="number" id="repair-cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="repair-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="repair-attachment" data-translate="optional">Attachment (Optional)</label>
|
||||
<input type="file" id="repair-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt,.doc,.docx,.xls,.xlsx">
|
||||
<small data-translate="supported_files">Supported: PDF, Images, Text, Word, Excel</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Record</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-repair-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadRepairs();
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Reparatii`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Repairs`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRepairs() {
|
||||
try {
|
||||
const records = await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`);
|
||||
const repairs = records.filter(r => r.category === 'Repair');
|
||||
displayRepairs(repairs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load repairs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRepairs(repairs) {
|
||||
const tbody = document.getElementById('repairs-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
|
||||
const currency = settings.currency || 'GBP';
|
||||
|
||||
if (repairs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center;">No repairs recorded</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = repairs.map(r => `
|
||||
<tr>
|
||||
<td>${formatDate(r.date)}</td>
|
||||
<td>${r.odometer.toLocaleString()}</td>
|
||||
<td>${r.description}</td>
|
||||
<td>${formatCurrency(r.cost, 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('');
|
||||
}
|
||||
|
||||
document.getElementById('add-repair-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('repair-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('repair-date').value,
|
||||
odometer: parseInt(document.getElementById('repair-odometer').value),
|
||||
description: document.getElementById('repair-description').value,
|
||||
category: 'Repair',
|
||||
cost: parseFloat(document.getElementById('repair-cost').value) || 0,
|
||||
notes: document.getElementById('repair-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-repair-modal');
|
||||
await loadRepairs();
|
||||
document.getElementById('add-repair-form').reset();
|
||||
} catch (error) {
|
||||
alert('Failed to add repair: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
207
frontend/templates/service_records.html
Normal file
207
frontend/templates/service_records.html
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inregistrari Service - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Tablou de bord</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Prezentare generala</a>
|
||||
<a href="/service-records" class="active" data-translate="service_records">Inregistrari service</a>
|
||||
<a href="/repairs" data-translate="repairs">Reparatii</a>
|
||||
<a href="/fuel" data-translate="fuel">Combustibil</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxe</a>
|
||||
<a href="/notes" data-translate="notes">Notite</a>
|
||||
<a href="/reminders" data-translate="reminders">Memento-uri</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Schimba tema</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Deconectare</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="service_records">Inregistrari service</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Inapoi la vehicul</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-service-modal')" data-translate="add_service_record">+ Adauga inregistrare service</button>
|
||||
<button class="btn" onclick="exportServiceRecords()" data-translate="export_csv">Exporta CSV</button>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Data</th>
|
||||
<th data-translate="odometer">Kilometraj</th>
|
||||
<th data-translate="description">Descriere</th>
|
||||
<th data-translate="category">Categorie</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="notes">Notite</th>
|
||||
<th data-translate="attachment">Atasament</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="service-records-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-service-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_service_record">Adauga inregistrare service</h2>
|
||||
<form id="add-service-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="service-date" data-translate="date">Data</label>
|
||||
<input type="date" id="service-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-odometer" data-translate="odometer">Kilometraj</label>
|
||||
<input type="number" id="service-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-description" data-translate="description">Descriere</label>
|
||||
<input type="text" id="service-description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-category" data-translate="category">Categorie</label>
|
||||
<select id="service-category" name="category">
|
||||
<option value="Maintenance" data-translate="maintenance">Intretinere</option>
|
||||
<option value="Repair" data-translate="repair">Reparatie</option>
|
||||
<option value="Inspection" data-translate="inspection">Inspectie tehnica</option>
|
||||
<option value="Upgrade" data-translate="upgrade">Imbunatatire</option>
|
||||
<option value="Other" data-translate="other">Altele</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-cost" data-translate="cost">Cost</label>
|
||||
<input type="number" id="service-cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-notes" data-translate="notes">Notite</label>
|
||||
<textarea id="service-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-attachment" data-translate="optional">Atasament (Optional)</label>
|
||||
<input type="file" id="service-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt,.doc,.docx,.xls,.xlsx">
|
||||
<small data-translate="supported_files">Suportat: PDF, Imagini, Text, Word, Excel</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Adauga inregistrare</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-service-modal')" data-translate="cancel">Anuleaza</button>
|
||||
resetEditMode();
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Va rugam selectati un vehicul din tabloul de bord.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadServiceRecords(currentVehicleId);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Inregistrari service`;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
function exportServiceRecords() {
|
||||
if (currentVehicleId) {
|
||||
exportData('service_records', currentVehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('add-service-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('service-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('service-date').value,
|
||||
odometer: parseInt(document.getElementById('service-odometer').value),
|
||||
description: document.getElementById('service-description').value,
|
||||
category: document.getElementById('service-category').value,
|
||||
cost: parseFloat(document.getElementById('service-cost').value) || 0,
|
||||
notes: document.getElementById('service-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
if (editingServiceRecordId) {
|
||||
await apiRequest(`/api/vehicles/${editingVehicleId}/service-records/${editingServiceRecordId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
editingServiceRecordId = null;
|
||||
editingVehicleId = null;
|
||||
const modal = document.getElementById('add-service-modal');
|
||||
const modalTitle = modal.querySelector('h2');
|
||||
if (modalTitle) modalTitle.textContent = 'Add Service Record';
|
||||
} else {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
}
|
||||
closeModal('add-service-modal');
|
||||
resetEditMode();
|
||||
await loadServiceRecords(currentVehicleId);
|
||||
document.getElementById('add-service-form').reset();
|
||||
} catch (error) {
|
||||
alert('Adaugarea inregistrarii a esuat: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
193
frontend/templates/service_records.html.backup
Normal file
193
frontend/templates/service_records.html.backup
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Inregistrari Service - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Tablou de bord</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Prezentare generala</a>
|
||||
<a href="/service-records" class="active" data-translate="service_records">Inregistrari service</a>
|
||||
<a href="/repairs" data-translate="repairs">Reparatii</a>
|
||||
<a href="/fuel" data-translate="fuel">Combustibil</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxe</a>
|
||||
<a href="/notes" data-translate="notes">Notite</a>
|
||||
<a href="/reminders" data-translate="reminders">Memento-uri</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Schimba tema</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Deconectare</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="service_records">Inregistrari service</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Inapoi la vehicul</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-service-modal')" data-translate="add_service_record">+ Adauga inregistrare service</button>
|
||||
<button class="btn" onclick="exportServiceRecords()" data-translate="export_csv">Exporta CSV</button>
|
||||
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Data</th>
|
||||
<th data-translate="odometer">Kilometraj</th>
|
||||
<th data-translate="description">Descriere</th>
|
||||
<th data-translate="category">Categorie</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="notes">Notite</th>
|
||||
<th data-translate="attachment">Atasament</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="service-records-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-service-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_service_record">Adauga inregistrare service</h2>
|
||||
<form id="add-service-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="service-date" data-translate="date">Data</label>
|
||||
<input type="date" id="service-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-odometer" data-translate="odometer">Kilometraj</label>
|
||||
<input type="number" id="service-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-description" data-translate="description">Descriere</label>
|
||||
<input type="text" id="service-description" name="description" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-category" data-translate="category">Categorie</label>
|
||||
<select id="service-category" name="category">
|
||||
<option value="Maintenance" data-translate="maintenance">Intretinere</option>
|
||||
<option value="Repair" data-translate="repair">Reparatie</option>
|
||||
<option value="Inspection" data-translate="inspection">Inspectie tehnica</option>
|
||||
<option value="Upgrade" data-translate="upgrade">Imbunatatire</option>
|
||||
<option value="Other" data-translate="other">Altele</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-cost" data-translate="cost">Cost</label>
|
||||
<input type="number" id="service-cost" name="cost" step="0.01" min="0" value="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-notes" data-translate="notes">Notite</label>
|
||||
<textarea id="service-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="service-attachment" data-translate="optional">Atasament (Optional)</label>
|
||||
<input type="file" id="service-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt,.doc,.docx,.xls,.xlsx">
|
||||
<small data-translate="supported_files">Suportat: PDF, Imagini, Text, Word, Excel</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Adauga inregistrare</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-service-modal')" data-translate="cancel">Anuleaza</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Va rugam selectati un vehicul din tabloul de bord.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadServiceRecords(currentVehicleId);
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Inregistrari service`;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
function exportServiceRecords() {
|
||||
if (currentVehicleId) {
|
||||
exportData('service_records', currentVehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('add-service-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('service-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('service-date').value,
|
||||
odometer: parseInt(document.getElementById('service-odometer').value),
|
||||
description: document.getElementById('service-description').value,
|
||||
category: document.getElementById('service-category').value,
|
||||
cost: parseFloat(document.getElementById('service-cost').value) || 0,
|
||||
notes: document.getElementById('service-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-service-modal');
|
||||
await loadServiceRecords(currentVehicleId);
|
||||
document.getElementById('add-service-form').reset();
|
||||
} catch (error) {
|
||||
alert('Adaugarea inregistrarii a esuat: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
451
frontend/templates/settings.html
Normal file
451
frontend/templates/settings.html
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Settings - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<a href="/settings" class="btn" data-translate="settings">Settings</a>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h2 data-translate="settings">Settings</h2>
|
||||
|
||||
<form id="settings-form">
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3 data-translate="backup_restore_title">Backup & Restore</h3>
|
||||
<p data-translate="backup_restore_desc">Create a complete backup of all your data or restore from a previous backup.</p>
|
||||
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
||||
<button type="button" class="btn btn-success" onclick="createBackup()" data-translate="create_backup">Create Backup</button>
|
||||
<button type="button" class="btn" onclick="document.getElementById('restore-file').click()" data-translate="restore_backup">Restore Backup</button>
|
||||
<input type="file" id="restore-file" accept=".zip" style="display: none;" onchange="restoreBackup(this.files[0])">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3>User Profile</h3>
|
||||
<div style="display: flex; gap: 20px; align-items: center; margin-bottom: 20px;">
|
||||
<img id="user-photo-preview" src="" alt="User Photo" style="width: 100px; height: 100px; border-radius: 50%; object-fit: cover; display: none;">
|
||||
<div style="flex: 1;">
|
||||
<div class="form-group">
|
||||
<label for="user-photo">Profile Photo</label>
|
||||
<input type="file" id="user-photo" accept="image/*" onchange="previewUserPhoto(this)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile-username">Username</label>
|
||||
<input type="text" id="profile-username" name="username" minlength="3">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profile-email">Email</label>
|
||||
<input type="email" id="profile-email" name="email">
|
||||
</div>
|
||||
<button type="button" class="btn" onclick="showModal('change-password-modal')">Change Password</button>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3>Two-Factor Authentication (2FA)</h3>
|
||||
<p id="2fa-status" style="margin-bottom: 15px;"></p>
|
||||
<div id="2fa-controls"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3 data-translate="language">Language</h3>
|
||||
<div class="form-group">
|
||||
<label for="language" data-translate="select_language">Select Language</label>
|
||||
<select id="language" name="language">
|
||||
<option value="en">English</option>
|
||||
<option value="ro">Romana</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3 data-translate="units_currency">Units & Currency</h3>
|
||||
<div class="form-group">
|
||||
<label for="unit-system" data-translate="measurement_system">Measurement System</label>
|
||||
<select id="unit-system" name="unit_system">
|
||||
<option value="metric" data-translate="metric">Metric (km, litres)</option>
|
||||
<option value="imperial" data-translate="imperial">Imperial (miles, gallons)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="currency" data-translate="currency">Currency</label>
|
||||
<select id="currency" name="currency">
|
||||
<option value="USD" data-translate="us_dollar">US Dollar (USD)</option>
|
||||
<option value="GBP" data-translate="british_pound">British Pound (GBP)</option>
|
||||
<option value="RON" data-translate="romanian_leu">Romanian Leu (RON)</option>
|
||||
<option value="EUR" data-translate="euro">Euro (EUR)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<h3 data-translate="app_info">Application Info</h3>
|
||||
<p><strong data-translate="version">Version:</strong> 1.0.0</p>
|
||||
<p><strong data-translate="database">Database:</strong> SQLite</p>
|
||||
<p><strong data-translate="current_user">Current User:</strong> <span id="current-user-name"></span></p>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; justify-content: center; margin-top: 30px;">
|
||||
<button type="submit" class="btn btn-success" style="font-size: 18px; padding: 12px 40px;" data-translate="save">Save All Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="change-password-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="change_password">Change Password</h2>
|
||||
<form id="change-password-form">
|
||||
<div class="form-group">
|
||||
<label for="current-password" data-translate="current_password">Current Password</label>
|
||||
<input type="password" id="current-password" name="current_password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-password" data-translate="new_password">New Password</label>
|
||||
<input type="password" id="new-password" name="new_password" required minlength="8">
|
||||
<small style="color: var(--text-secondary);">
|
||||
Must be at least 8 characters with uppercase, lowercase, and numbers
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm-password" data-translate="confirm_password">Confirm New Password</label>
|
||||
<input type="password" id="confirm-password" name="confirm_password" required minlength="8">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="update">Update Password</button>
|
||||
<button type="button" class="btn" onclick="closeModal('change-password-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="setup-2fa-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Setup Two-Factor Authentication</h2>
|
||||
<p>Scan this QR code with your authenticator app:</p>
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<img id="qr-code-image" src="" alt="QR Code" style="max-width: 250px;">
|
||||
</div>
|
||||
<p><strong>Backup Codes:</strong></p>
|
||||
<p style="color: var(--text-secondary); font-size: 14px;">Save these codes in a safe place. You can use them if you lose access to your authenticator app.</p>
|
||||
<div id="backup-codes" style="background: var(--bg-secondary); padding: 15px; border-radius: 4px; font-family: monospace; margin-bottom: 20px;"></div>
|
||||
<form id="verify-2fa-form">
|
||||
<div class="form-group">
|
||||
<label for="verify-code">Enter verification code from your app:</label>
|
||||
<input type="text" id="verify-code" name="code" required maxlength="6" placeholder="000000">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success">Enable 2FA</button>
|
||||
<button type="button" class="btn" onclick="closeModal('setup-2fa-modal')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="disable-2fa-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Disable Two-Factor Authentication</h2>
|
||||
<p>Enter your password to disable 2FA:</p>
|
||||
<form id="disable-2fa-form">
|
||||
<div class="form-group">
|
||||
<label for="disable-2fa-password">Password</label>
|
||||
<input type="password" id="disable-2fa-password" name="password" required>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-danger">Disable 2FA</button>
|
||||
<button type="button" class="btn" onclick="closeModal('disable-2fa-modal')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="custom-alert-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 400px; text-align: center;">
|
||||
<p id="alert-message" style="margin: 20px 0; font-size: 16px;"></p>
|
||||
<button class="btn btn-success" onclick="closeCustomAlert()" style="width: 100px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let uploadedPhotoUrl = null;
|
||||
let user2faEnabled = false;
|
||||
|
||||
function showCustomAlert(message) {
|
||||
document.getElementById('alert-message').textContent = message;
|
||||
showModal('custom-alert-modal');
|
||||
}
|
||||
|
||||
function closeCustomAlert() {
|
||||
closeModal('custom-alert-modal');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await loadUserSettings();
|
||||
await load2FAStatus();
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadUserSettings() {
|
||||
try {
|
||||
const settings = await apiRequest('/api/settings');
|
||||
|
||||
document.getElementById('profile-username').value = settings.username;
|
||||
document.getElementById('profile-email').value = settings.email;
|
||||
document.getElementById('language').value = settings.language || 'en';
|
||||
document.getElementById('unit-system').value = settings.unit_system || 'metric';
|
||||
document.getElementById('currency').value = settings.currency || 'GBP';
|
||||
document.getElementById('current-user-name').textContent = settings.username;
|
||||
|
||||
if (settings.photo) {
|
||||
document.getElementById('user-photo-preview').src = settings.photo;
|
||||
document.getElementById('user-photo-preview').style.display = 'block';
|
||||
}
|
||||
|
||||
localStorage.setItem('userSettings', JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
console.error('Failed to load settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function load2FAStatus() {
|
||||
try {
|
||||
const response = await apiRequest('/api/auth/me');
|
||||
user2faEnabled = response.two_factor_enabled;
|
||||
|
||||
const statusEl = document.getElementById('2fa-status');
|
||||
const controlsEl = document.getElementById('2fa-controls');
|
||||
|
||||
if (user2faEnabled) {
|
||||
statusEl.innerHTML = '<span style="color: var(--success);">2FA is currently enabled on your account.</span>';
|
||||
controlsEl.innerHTML = '<button type="button" class="btn btn-danger" onclick="showModal(\'disable-2fa-modal\')">Disable 2FA</button>';
|
||||
} else {
|
||||
statusEl.innerHTML = '<span style="color: var(--text-secondary);">2FA is not enabled. Add an extra layer of security to your account.</span>';
|
||||
controlsEl.innerHTML = '<button type="button" class="btn btn-success" onclick="setup2FA()">Enable 2FA</button>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load 2FA status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function setup2FA() {
|
||||
try {
|
||||
const response = await apiRequest('/api/auth/setup-2fa', { method: 'POST' });
|
||||
|
||||
document.getElementById('qr-code-image').src = response.qr_code;
|
||||
|
||||
const backupCodesDiv = document.getElementById('backup-codes');
|
||||
backupCodesDiv.innerHTML = response.backup_codes.map(code => `<div>${code}</div>`).join('');
|
||||
|
||||
showModal('setup-2fa-modal');
|
||||
} catch (error) {
|
||||
showCustomAlert('Failed to setup 2FA: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('verify-2fa-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const code = document.getElementById('verify-code').value;
|
||||
|
||||
try {
|
||||
await apiRequest('/api/auth/enable-2fa', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
|
||||
closeModal('setup-2fa-modal');
|
||||
showCustomAlert('2FA enabled successfully!');
|
||||
await load2FAStatus();
|
||||
} catch (error) {
|
||||
showCustomAlert('Invalid verification code. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('disable-2fa-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.getElementById('disable-2fa-password').value;
|
||||
|
||||
try {
|
||||
await apiRequest('/api/auth/disable-2fa', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
|
||||
closeModal('disable-2fa-modal');
|
||||
showCustomAlert('2FA disabled successfully.');
|
||||
await load2FAStatus();
|
||||
document.getElementById('disable-2fa-form').reset();
|
||||
} catch (error) {
|
||||
showCustomAlert('Failed to disable 2FA: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
function previewUserPhoto(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('user-photo-preview').src = e.target.result;
|
||||
document.getElementById('user-photo-preview').style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('settings-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const photoFile = document.getElementById('user-photo').files[0];
|
||||
if (photoFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('photo', photoFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/photo', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
uploadedPhotoUrl = data.photo_url;
|
||||
} catch (error) {
|
||||
console.error('Photo upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const username = document.getElementById('profile-username').value;
|
||||
const email = document.getElementById('profile-email').value;
|
||||
const language = document.getElementById('language').value;
|
||||
const unitSystem = document.getElementById('unit-system').value;
|
||||
const currency = document.getElementById('currency').value;
|
||||
|
||||
try {
|
||||
if (uploadedPhotoUrl || username || email) {
|
||||
await apiRequest('/api/user/update-profile', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
email,
|
||||
photo: uploadedPhotoUrl
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
await apiRequest('/api/settings/language', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ language })
|
||||
});
|
||||
|
||||
await apiRequest('/api/settings/units', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
unit_system: unitSystem,
|
||||
currency: currency
|
||||
})
|
||||
});
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{}');
|
||||
settings.username = username;
|
||||
settings.email = email;
|
||||
settings.language = language;
|
||||
settings.unit_system = unitSystem;
|
||||
settings.currency = currency;
|
||||
if (uploadedPhotoUrl) settings.photo = uploadedPhotoUrl;
|
||||
localStorage.setItem('userSettings', JSON.stringify(settings));
|
||||
|
||||
showCustomAlert('All settings saved successfully. Page will reload.');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (error) {
|
||||
showCustomAlert('Failed to save settings: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
async function createBackup() {
|
||||
try {
|
||||
window.location.href = '/api/backup/create';
|
||||
} catch (error) {
|
||||
showCustomAlert('Failed to create backup: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreBackup(file) {
|
||||
if (!file) return;
|
||||
|
||||
if (!confirm('Restoring a backup will overwrite all current data. Are you sure?')) {
|
||||
document.getElementById('restore-file').value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('backup', file);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/backup/restore', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
showCustomAlert('Backup restored successfully. Page will reload.');
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
showCustomAlert('Failed to restore backup: ' + (data.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
showCustomAlert('Failed to restore backup: ' + error.message);
|
||||
} finally {
|
||||
document.getElementById('restore-file').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('change-password-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const currentPassword = document.getElementById('current-password').value;
|
||||
const newPassword = document.getElementById('new-password').value;
|
||||
const confirmPassword = document.getElementById('confirm-password').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showCustomAlert('New passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest('/api/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
showCustomAlert('Password changed successfully.');
|
||||
closeModal('change-password-modal');
|
||||
document.getElementById('change-password-form').reset();
|
||||
} catch (error) {
|
||||
showCustomAlert('Failed to change password: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
333
frontend/templates/taxes.html
Normal file
333
frontend/templates/taxes.html
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Taxes - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" class="active" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="taxes">Tax Records</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-tax-modal')" data-translate="add_tax_record">+ Add Tax Record</button>
|
||||
|
||||
<h3 style="margin-top: 30px;">Recurring Expenses</h3>
|
||||
<div id="recurring-expenses-container" class="vehicle-grid" style="margin-bottom: 30px;"></div>
|
||||
|
||||
<h3>Tax History</h3>
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="type">Type</th>
|
||||
<th data-translate="amount">Amount</th>
|
||||
<th data-translate="notes">Notes</th>
|
||||
<th data-translate="attachment">Attachment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="taxes-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-tax-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_tax_record">Add Tax Record</h2>
|
||||
<form id="add-tax-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="tax-date" data-translate="date">Date</label>
|
||||
<input type="date" id="tax-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-type" data-translate="type">Type</label>
|
||||
<select id="tax-type" name="type">
|
||||
<option value="Registration" data-translate="registration">Registration</option>
|
||||
<option value="License" data-translate="license">Licence</option>
|
||||
<option value="Road Tax" data-translate="property_tax">Road Tax</option>
|
||||
<option value="Insurance" data-translate="insurance">Insurance</option>
|
||||
<option value="MOT" data-translate="inspection">MOT</option>
|
||||
<option value="Other" data-translate="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-amount" data-translate="amount">Amount</label>
|
||||
<input type="number" id="tax-amount" name="amount" step="0.01" min="0" required>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: center;">
|
||||
<input type="checkbox" id="tax-recurring" name="recurring" style="margin: 0; width: auto;">
|
||||
<label for="tax-recurring" style="margin: 0 0 0 8px; cursor: pointer;">
|
||||
<span data-translate="recurring">Recurring Expense</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="recurring-options" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="tax-frequency" data-translate="type">Frequency</label>
|
||||
<select id="tax-frequency" name="frequency">
|
||||
<option value="monthly" data-translate="monthly">Monthly</option>
|
||||
<option value="quarterly" data-translate="quarterly">Quarterly</option>
|
||||
<option value="yearly" data-translate="yearly">Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="tax-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-attachment" data-translate="optional">Document (Optional)</label>
|
||||
<input type="file" id="tax-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt,.doc,.docx">
|
||||
<small data-translate="supported_files">Supported: PDF, Images, Text, Word</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Record</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-tax-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
let taxRecords = [];
|
||||
let recurringExpenses = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadRecurringExpenses();
|
||||
await loadTaxesForVehicle(currentVehicleId);
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
|
||||
document.getElementById('tax-recurring').addEventListener('change', function() {
|
||||
document.getElementById('recurring-options').style.display =
|
||||
this.checked ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Taxe`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Taxes`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecurringExpenses() {
|
||||
try {
|
||||
const expenses = await apiRequest(`/api/vehicles/${currentVehicleId}/recurring-expenses`);
|
||||
recurringExpenses = expenses;
|
||||
displayRecurringExpenses(expenses);
|
||||
} catch (error) {
|
||||
console.error('Failed to load recurring expenses:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRecurringExpenses(expenses) {
|
||||
const container = document.getElementById('recurring-expenses-container');
|
||||
if (!container) return;
|
||||
|
||||
if (expenses.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-secondary);">No recurring expenses set up.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = expenses.map(e => `
|
||||
<div class="card" style="padding: 20px;">
|
||||
<h3>${e.description}</h3>
|
||||
<p><strong>Type:</strong> ${e.expense_type}</p>
|
||||
<p><strong>Amount:</strong> ${formatCurrency(e.amount)}</p>
|
||||
<p><strong>Frequency:</strong> ${e.frequency}</p>
|
||||
<p><strong>Next Due:</strong> ${formatDate(e.next_due_date)}</p>
|
||||
<p style="color: var(--text-secondary); font-size: 14px;">${e.notes || ''}</p>
|
||||
<button class="btn btn-danger" onclick="cancelRecurringExpense(${e.id})" style="margin-top: 10px;">
|
||||
Cancel Recurring
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function cancelRecurringExpense(expenseId) {
|
||||
|
||||
async function editRecurringExpense(expenseId) {
|
||||
try {
|
||||
const expense = await apiRequest(`/api/vehicles/${currentVehicleId}/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/${currentVehicleId}/recurring-expenses/${expenseId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ expense_type, description, amount: parseFloat(amount), frequency, notes })
|
||||
});
|
||||
alert('Tax/Expense updated successfully');
|
||||
await loadRecurringExpenses();
|
||||
} catch (error) {
|
||||
alert('Failed to update tax/expense: ' + error.message);
|
||||
}
|
||||
}
|
||||
if (!confirm('Are you sure you want to cancel this recurring expense?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/recurring-expenses/${expenseId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
alert('Recurring expense cancelled successfully.');
|
||||
await loadRecurringExpenses();
|
||||
} catch (error) {
|
||||
alert('Failed to cancel recurring expense: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTaxesForVehicle(vehicleId) {
|
||||
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
|
||||
taxRecords = records.filter(r => r.category === 'Tax');
|
||||
|
||||
const tbody = document.getElementById('taxes-tbody');
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
|
||||
const currency = settings.currency || 'GBP';
|
||||
|
||||
tbody.innerHTML = taxRecords.map(r => `
|
||||
<tr>
|
||||
<td>${formatDate(r.date)}</td>
|
||||
<td>${r.description}</td>
|
||||
<td>${formatCurrency(r.cost, 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('');
|
||||
}
|
||||
|
||||
document.getElementById('add-tax-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('tax-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const taxType = document.getElementById('tax-type').value;
|
||||
const isRecurring = document.getElementById('tax-recurring').checked;
|
||||
|
||||
if (isRecurring && (taxType === 'Road Tax' || taxType === 'Insurance')) {
|
||||
const recurringData = {
|
||||
expense_type: taxType,
|
||||
description: taxType,
|
||||
amount: parseFloat(document.getElementById('tax-amount').value),
|
||||
frequency: document.getElementById('tax-frequency').value,
|
||||
start_date: document.getElementById('tax-date').value,
|
||||
notes: document.getElementById('tax-notes').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/recurring-expenses`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(recurringData)
|
||||
});
|
||||
await loadRecurringExpenses();
|
||||
} catch (error) {
|
||||
alert('Failed to add recurring expense: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('tax-date').value,
|
||||
odometer: 0,
|
||||
description: taxType,
|
||||
category: 'Tax',
|
||||
cost: parseFloat(document.getElementById('tax-amount').value),
|
||||
notes: document.getElementById('tax-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-tax-modal');
|
||||
await loadTaxesForVehicle(currentVehicleId);
|
||||
document.getElementById('add-tax-form').reset();
|
||||
document.getElementById('recurring-options').style.display = 'none';
|
||||
} catch (error) {
|
||||
alert('Failed to add tax record: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
310
frontend/templates/taxes.html.backup
Normal file
310
frontend/templates/taxes.html.backup
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Taxes - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" onclick="navigateToVehicle(); return false;" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" class="active" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 id="vehicle-title" data-translate="taxes">Tax Records</h2>
|
||||
<button class="btn" onclick="navigateToVehicle()" data-translate="back_to_vehicle">Back to Vehicle</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success" onclick="showModal('add-tax-modal')" data-translate="add_tax_record">+ Add Tax Record</button>
|
||||
|
||||
<h3 style="margin-top: 30px;">Recurring Expenses</h3>
|
||||
<div id="recurring-expenses-container" class="vehicle-grid" style="margin-bottom: 30px;"></div>
|
||||
|
||||
<h3>Tax History</h3>
|
||||
<div class="card" style="margin-top: 20px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="type">Type</th>
|
||||
<th data-translate="amount">Amount</th>
|
||||
<th data-translate="notes">Notes</th>
|
||||
<th data-translate="attachment">Attachment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="taxes-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="add-tax-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="add_tax_record">Add Tax Record</h2>
|
||||
<form id="add-tax-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="tax-date" data-translate="date">Date</label>
|
||||
<input type="date" id="tax-date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-type" data-translate="type">Type</label>
|
||||
<select id="tax-type" name="type">
|
||||
<option value="Registration" data-translate="registration">Registration</option>
|
||||
<option value="License" data-translate="license">Licence</option>
|
||||
<option value="Road Tax" data-translate="property_tax">Road Tax</option>
|
||||
<option value="Insurance" data-translate="insurance">Insurance</option>
|
||||
<option value="MOT" data-translate="inspection">MOT</option>
|
||||
<option value="Other" data-translate="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-amount" data-translate="amount">Amount</label>
|
||||
<input type="number" id="tax-amount" name="amount" step="0.01" min="0" required>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: center;">
|
||||
<input type="checkbox" id="tax-recurring" name="recurring" style="margin: 0; width: auto;">
|
||||
<label for="tax-recurring" style="margin: 0 0 0 8px; cursor: pointer;">
|
||||
<span data-translate="recurring">Recurring Expense</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="recurring-options" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="tax-frequency" data-translate="type">Frequency</label>
|
||||
<select id="tax-frequency" name="frequency">
|
||||
<option value="monthly" data-translate="monthly">Monthly</option>
|
||||
<option value="quarterly" data-translate="quarterly">Quarterly</option>
|
||||
<option value="yearly" data-translate="yearly">Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-notes" data-translate="notes">Notes</label>
|
||||
<textarea id="tax-notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tax-attachment" data-translate="optional">Document (Optional)</label>
|
||||
<input type="file" id="tax-attachment" name="attachment" accept=".pdf,.png,.jpg,.jpeg,.txt,.doc,.docx">
|
||||
<small data-translate="supported_files">Supported: PDF, Images, Text, Word</small>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="add_record">Add Record</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-tax-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
let taxRecords = [];
|
||||
let recurringExpenses = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
currentVehicleId = getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadVehicleInfo();
|
||||
await loadRecurringExpenses();
|
||||
await loadTaxesForVehicle(currentVehicleId);
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
|
||||
document.getElementById('tax-recurring').addEventListener('change', function() {
|
||||
document.getElementById('recurring-options').style.display =
|
||||
this.checked ? 'block' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
async function loadVehicleInfo() {
|
||||
try {
|
||||
const vehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"language":"en"}');
|
||||
const lang = settings.language || 'en';
|
||||
|
||||
if (lang === 'ro') {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Taxe`;
|
||||
} else {
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${vehicle.year} ${vehicle.make} ${vehicle.model} - Taxes`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle info:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToVehicle() {
|
||||
if (currentVehicleId) {
|
||||
window.location.href = `/vehicle-detail?id=${currentVehicleId}`;
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecurringExpenses() {
|
||||
try {
|
||||
const expenses = await apiRequest(`/api/vehicles/${currentVehicleId}/recurring-expenses`);
|
||||
recurringExpenses = expenses;
|
||||
displayRecurringExpenses(expenses);
|
||||
} catch (error) {
|
||||
console.error('Failed to load recurring expenses:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function displayRecurringExpenses(expenses) {
|
||||
const container = document.getElementById('recurring-expenses-container');
|
||||
if (!container) return;
|
||||
|
||||
if (expenses.length === 0) {
|
||||
container.innerHTML = '<p style="color: var(--text-secondary);">No recurring expenses set up.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = expenses.map(e => `
|
||||
<div class="card" style="padding: 20px;">
|
||||
<h3>${e.description}</h3>
|
||||
<p><strong>Type:</strong> ${e.expense_type}</p>
|
||||
<p><strong>Amount:</strong> ${formatCurrency(e.amount)}</p>
|
||||
<p><strong>Frequency:</strong> ${e.frequency}</p>
|
||||
<p><strong>Next Due:</strong> ${formatDate(e.next_due_date)}</p>
|
||||
<p style="color: var(--text-secondary); font-size: 14px;">${e.notes || ''}</p>
|
||||
<button class="btn btn-danger" onclick="cancelRecurringExpense(${e.id})" style="margin-top: 10px;">
|
||||
Cancel Recurring
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function cancelRecurringExpense(expenseId) {
|
||||
if (!confirm('Are you sure you want to cancel this recurring expense?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/recurring-expenses/${expenseId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
alert('Recurring expense cancelled successfully.');
|
||||
await loadRecurringExpenses();
|
||||
} catch (error) {
|
||||
alert('Failed to cancel recurring expense: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTaxesForVehicle(vehicleId) {
|
||||
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
|
||||
taxRecords = records.filter(r => r.category === 'Tax');
|
||||
|
||||
const tbody = document.getElementById('taxes-tbody');
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
|
||||
const currency = settings.currency || 'GBP';
|
||||
|
||||
tbody.innerHTML = taxRecords.map(r => `
|
||||
<tr>
|
||||
<td>${formatDate(r.date)}</td>
|
||||
<td>${r.description}</td>
|
||||
<td>${formatCurrency(r.cost, 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('');
|
||||
}
|
||||
|
||||
document.getElementById('add-tax-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let attachmentPath = null;
|
||||
const attachmentFile = document.getElementById('tax-attachment').files[0];
|
||||
|
||||
if (attachmentFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', attachmentFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/attachment', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
attachmentPath = data.file_path;
|
||||
} catch (error) {
|
||||
console.error('Attachment upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const taxType = document.getElementById('tax-type').value;
|
||||
const isRecurring = document.getElementById('tax-recurring').checked;
|
||||
|
||||
if (isRecurring && (taxType === 'Road Tax' || taxType === 'Insurance')) {
|
||||
const recurringData = {
|
||||
expense_type: taxType,
|
||||
description: taxType,
|
||||
amount: parseFloat(document.getElementById('tax-amount').value),
|
||||
frequency: document.getElementById('tax-frequency').value,
|
||||
start_date: document.getElementById('tax-date').value,
|
||||
notes: document.getElementById('tax-notes').value || null
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/recurring-expenses`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(recurringData)
|
||||
});
|
||||
await loadRecurringExpenses();
|
||||
} catch (error) {
|
||||
alert('Failed to add recurring expense: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const formData = {
|
||||
date: document.getElementById('tax-date').value,
|
||||
odometer: 0,
|
||||
description: taxType,
|
||||
category: 'Tax',
|
||||
cost: parseFloat(document.getElementById('tax-amount').value),
|
||||
notes: document.getElementById('tax-notes').value || null,
|
||||
document_path: attachmentPath
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('add-tax-modal');
|
||||
await loadTaxesForVehicle(currentVehicleId);
|
||||
document.getElementById('add-tax-form').reset();
|
||||
document.getElementById('recurring-options').style.display = 'none';
|
||||
} catch (error) {
|
||||
alert('Failed to add tax record: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
357
frontend/templates/vehicle_detail.html
Normal file
357
frontend/templates/vehicle_detail.html
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vehicle Details - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard" data-translate="dashboard">Dashboard</a>
|
||||
<a href="#" class="active" data-translate="overview">Overview</a>
|
||||
<a href="/service-records" data-translate="service_records">Service Records</a>
|
||||
<a href="/repairs" data-translate="repairs">Repairs</a>
|
||||
<a href="/fuel" data-translate="fuel">Fuel</a>
|
||||
<a href="/taxes" data-translate="taxes">Taxes</a>
|
||||
<a href="/notes" data-translate="notes">Notes</a>
|
||||
<a href="/reminders" data-translate="reminders">Reminders</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-translate="toggle_theme">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()" data-translate="logout">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px; flex-wrap: wrap; gap: 20px;">
|
||||
<div style="display: flex; gap: 20px; align-items: center;">
|
||||
<img id="vehicle-photo" src="" alt="Vehicle" style="width: 200px; height: 150px; object-fit: cover; border-radius: 8px; display: none;">
|
||||
<div>
|
||||
<h2 id="vehicle-title"></h2>
|
||||
<p id="vehicle-info" style="color: var(--text-secondary);"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button class="btn" onclick="showModal('edit-vehicle-modal')" data-translate="edit_vehicle">Edit Vehicle</button>
|
||||
<button class="btn btn-success" onclick="exportAllData()" data-translate="export_all_data">Export All Data</button>
|
||||
<button class="btn btn-danger" onclick="deleteVehicle()" data-translate="delete_vehicle">Delete Vehicle</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="last_odometer_reading">Last Odometer Reading</div>
|
||||
<div class="stat-value" id="last-odometer">0 km</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="total_distance_traveled">Total Distance Travelled</div>
|
||||
<div class="stat-value" id="total-distance">0 km</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="total_cost">Total Cost</div>
|
||||
<div class="stat-value" id="total-cost">£0.00</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="average_fuel_economy">Average Fuel Economy</div>
|
||||
<div class="stat-value" id="avg-fuel-economy">0 L/100km</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid" style="margin-top: 20px;">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="fuel_cost">Fuel Cost</div>
|
||||
<div class="stat-value" id="fuel-cost">£0.00</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="service_cost">Service Cost</div>
|
||||
<div class="stat-value" id="service-cost">£0.00</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="repairs_cost">Repairs Cost</div>
|
||||
<div class="stat-value" id="repairs-cost">£0.00</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="taxes_cost">Taxes Cost</div>
|
||||
<div class="stat-value" id="taxes-cost">£0.00</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-translate="upgrades_cost">Upgrades Cost</div>
|
||||
<div class="stat-value" id="upgrades-cost">£0.00</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 40px;" data-translate="recent_service_records">Recent Service Records</h3>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="odometer">Odometer</th>
|
||||
<th data-translate="description">Description</th>
|
||||
<th data-translate="category">Category</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recent-service-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 40px;" data-translate="recent_fuel_records">Recent Fuel Records</h3>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-translate="date">Date</th>
|
||||
<th data-translate="odometer">Odometer</th>
|
||||
<th data-translate="fuel_amount">Amount</th>
|
||||
<th data-translate="cost">Cost</th>
|
||||
<th data-translate="economy">Economy</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recent-fuel-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 40px;" data-translate="active_reminders">Active Reminders</h3>
|
||||
<div id="reminders-container" style="margin-top: 20px;"></div>
|
||||
</div>
|
||||
|
||||
<div id="edit-vehicle-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2 data-translate="edit_vehicle">Edit Vehicle</h2>
|
||||
<form id="edit-vehicle-form">
|
||||
<div class="form-group">
|
||||
<label for="edit-year" data-translate="year">Year</label>
|
||||
<input type="number" id="edit-year" name="year" required min="1900" max="2099">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-make" data-translate="make">Make</label>
|
||||
<input type="text" id="edit-make" name="make" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-model" data-translate="model">Model</label>
|
||||
<input type="text" id="edit-model" name="model" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-vin" data-translate="vin">VIN</label>
|
||||
<input type="text" id="edit-vin" name="vin" maxlength="17">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-license-plate" data-translate="license_plate">Registration Number</label>
|
||||
<input type="text" id="edit-license-plate" name="license_plate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-odometer" data-translate="odometer">Current Odometer</label>
|
||||
<input type="number" id="edit-odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success" data-translate="update">Update</button>
|
||||
<button type="button" class="btn" onclick="closeModal('edit-vehicle-modal')" data-translate="cancel">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/translations.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
let currentVehicleId = null;
|
||||
let currentVehicle = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
currentVehicleId = urlParams.get('id') || getSelectedVehicle();
|
||||
|
||||
if (!currentVehicleId) {
|
||||
alert('Please select a vehicle from the dashboard.');
|
||||
window.location.href = '/dashboard';
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedVehicle(currentVehicleId);
|
||||
await loadUserSettings();
|
||||
await loadVehicleDetails();
|
||||
await loadVehicleStats();
|
||||
await loadRecentRecords();
|
||||
await loadReminders(currentVehicleId);
|
||||
|
||||
setTimeout(() => {
|
||||
translatePage();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
async function loadVehicleDetails() {
|
||||
try {
|
||||
currentVehicle = await apiRequest(`/api/vehicles/${currentVehicleId}`);
|
||||
|
||||
document.getElementById('vehicle-title').textContent =
|
||||
`${currentVehicle.year} ${currentVehicle.make} ${currentVehicle.model}`;
|
||||
|
||||
const vinText = currentVehicle.vin || 'N/A';
|
||||
const licenseText = currentVehicle.license_plate || 'N/A';
|
||||
document.getElementById('vehicle-info').textContent =
|
||||
`VIN: ${vinText} | License: ${licenseText}`;
|
||||
|
||||
if (currentVehicle.photo) {
|
||||
const photoEl = document.getElementById('vehicle-photo');
|
||||
photoEl.src = currentVehicle.photo;
|
||||
photoEl.style.display = 'block';
|
||||
}
|
||||
|
||||
document.getElementById('edit-year').value = currentVehicle.year;
|
||||
document.getElementById('edit-make').value = currentVehicle.make;
|
||||
document.getElementById('edit-model').value = currentVehicle.model;
|
||||
document.getElementById('edit-vin').value = currentVehicle.vin || '';
|
||||
document.getElementById('edit-license-plate').value = currentVehicle.license_plate || '';
|
||||
document.getElementById('edit-odometer').value = currentVehicle.odometer;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle details:', error);
|
||||
alert('Failed to load vehicle details');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleStats() {
|
||||
try {
|
||||
const serviceRecords = await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`);
|
||||
const fuelRecords = await apiRequest(`/api/vehicles/${currentVehicleId}/fuel-records`);
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP","unit_system":"imperial"}');
|
||||
const currency = settings.currency || 'GBP';
|
||||
const unitSystem = settings.unit_system || 'imperial';
|
||||
const distanceUnit = unitSystem === 'metric' ? 'km' : 'miles';
|
||||
|
||||
document.getElementById('last-odometer').textContent =
|
||||
`${currentVehicle.odometer.toLocaleString()} ${distanceUnit}`;
|
||||
|
||||
let totalDistance = 0;
|
||||
if (fuelRecords.length > 0) {
|
||||
const sortedRecords = fuelRecords.sort((a, b) => a.odometer - b.odometer);
|
||||
totalDistance = sortedRecords[sortedRecords.length - 1].odometer - sortedRecords[0].odometer;
|
||||
}
|
||||
document.getElementById('total-distance').textContent =
|
||||
`${totalDistance.toLocaleString()} ${distanceUnit}`;
|
||||
|
||||
const fuelCost = fuelRecords.reduce((sum, r) => sum + r.cost, 0);
|
||||
const serviceCost = serviceRecords.filter(r => r.category === 'Maintenance').reduce((sum, r) => sum + r.cost, 0);
|
||||
const repairsCost = serviceRecords.filter(r => r.category === 'Repair').reduce((sum, r) => sum + r.cost, 0);
|
||||
const taxesCost = serviceRecords.filter(r => r.category === 'Tax').reduce((sum, r) => sum + r.cost, 0);
|
||||
const upgradesCost = serviceRecords.filter(r => r.category === 'Upgrade').reduce((sum, r) => sum + r.cost, 0);
|
||||
const totalCost = fuelCost + serviceCost + repairsCost + taxesCost + upgradesCost;
|
||||
|
||||
document.getElementById('total-cost').textContent = formatCurrency(totalCost, currency);
|
||||
document.getElementById('fuel-cost').textContent = formatCurrency(fuelCost, currency);
|
||||
document.getElementById('service-cost').textContent = formatCurrency(serviceCost, currency);
|
||||
document.getElementById('repairs-cost').textContent = formatCurrency(repairsCost, currency);
|
||||
document.getElementById('taxes-cost').textContent = formatCurrency(taxesCost, currency);
|
||||
document.getElementById('upgrades-cost').textContent = formatCurrency(upgradesCost, currency);
|
||||
|
||||
const avgEconomy = fuelRecords.filter(r => r.fuel_economy).reduce((sum, r, _, arr) =>
|
||||
sum + r.fuel_economy / arr.length, 0);
|
||||
const economyUnit = fuelRecords[0]?.unit || 'L/100km';
|
||||
document.getElementById('avg-fuel-economy').textContent =
|
||||
`${avgEconomy.toFixed(2)} ${economyUnit}`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load vehicle stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecentRecords() {
|
||||
try {
|
||||
const serviceRecords = await apiRequest(`/api/vehicles/${currentVehicleId}/service-records`);
|
||||
const fuelRecords = await apiRequest(`/api/vehicles/${currentVehicleId}/fuel-records`);
|
||||
|
||||
const settings = JSON.parse(localStorage.getItem('userSettings') || '{"currency":"GBP"}');
|
||||
const currency = settings.currency || 'GBP';
|
||||
|
||||
const recentService = serviceRecords.slice(0, 5);
|
||||
const serviceTbody = document.getElementById('recent-service-tbody');
|
||||
if (recentService.length === 0) {
|
||||
serviceTbody.innerHTML = '<tr><td colspan="5" style="text-align: center;">No service records</td></tr>';
|
||||
} else {
|
||||
serviceTbody.innerHTML = recentService.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, currency)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
const recentFuel = fuelRecords.slice(0, 5);
|
||||
const fuelTbody = document.getElementById('recent-fuel-tbody');
|
||||
if (recentFuel.length === 0) {
|
||||
fuelTbody.innerHTML = '<tr><td colspan="5" style="text-align: center;">No fuel records</td></tr>';
|
||||
} else {
|
||||
fuelTbody.innerHTML = recentFuel.map(r => `
|
||||
<tr>
|
||||
<td>${formatDate(r.date)}</td>
|
||||
<td>${r.odometer.toLocaleString()}</td>
|
||||
<td>${r.fuel_amount.toFixed(2)}</td>
|
||||
<td>${formatCurrency(r.cost, currency)}</td>
|
||||
<td>${r.fuel_economy ? r.fuel_economy.toFixed(2) : 'N/A'} ${r.unit}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent records:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function exportAllData() {
|
||||
if (currentVehicleId) {
|
||||
exportAllVehicleData(currentVehicleId);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVehicle() {
|
||||
if (!confirm('Are you sure you want to delete this vehicle? This will delete all associated records.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
clearSelectedVehicle();
|
||||
alert('Vehicle deleted successfully.');
|
||||
window.location.href = '/dashboard';
|
||||
} catch (error) {
|
||||
alert('Failed to delete vehicle: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('edit-vehicle-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = {
|
||||
year: parseInt(document.getElementById('edit-year').value),
|
||||
make: document.getElementById('edit-make').value,
|
||||
model: document.getElementById('edit-model').value,
|
||||
vin: document.getElementById('edit-vin').value || null,
|
||||
license_plate: document.getElementById('edit-license-plate').value || null,
|
||||
odometer: parseInt(document.getElementById('edit-odometer').value)
|
||||
};
|
||||
|
||||
try {
|
||||
await apiRequest(`/api/vehicles/${currentVehicleId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
closeModal('edit-vehicle-modal');
|
||||
await loadVehicleDetails();
|
||||
await loadVehicleStats();
|
||||
alert('Vehicle updated successfully.');
|
||||
} catch (error) {
|
||||
alert('Failed to update vehicle: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
125
frontend/templates/vehicles.html
Normal file
125
frontend/templates/vehicles.html
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vehicles - Masina-Dock</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="logo">
|
||||
<img src="/static/images/logo.svg" alt="Masina-Dock">
|
||||
<h1>Masina-Dock</h1>
|
||||
</div>
|
||||
<nav>
|
||||
<a href="/dashboard">Dashboard</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()">Toggle Theme</button>
|
||||
<button class="btn btn-danger" onclick="logout()">Logout</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<h2>All Vehicles</h2>
|
||||
<button class="btn btn-success" onclick="showModal('add-vehicle-modal')">+ Add Vehicle</button>
|
||||
|
||||
<div id="vehicles-container" class="vehicle-grid"></div>
|
||||
</div>
|
||||
|
||||
<div id="add-vehicle-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Add New Vehicle</h2>
|
||||
<form id="add-vehicle-form" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="photo">Vehicle Photo</label>
|
||||
<input type="file" id="photo" name="photo" accept="image/*">
|
||||
<div id="photo-preview" style="margin-top: 10px;"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="year">Year</label>
|
||||
<input type="number" id="year" name="year" required min="1900" max="2099">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="make">Make</label>
|
||||
<input type="text" id="make" name="make" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="model">Model</label>
|
||||
<input type="text" id="model" name="model" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vin">VIN (Optional)</label>
|
||||
<input type="text" id="vin" name="vin" maxlength="17">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="license_plate">License Plate (Optional)</label>
|
||||
<input type="text" id="license_plate" name="license_plate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="odometer">Current Odometer</label>
|
||||
<input type="number" id="odometer" name="odometer" required min="0">
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
||||
<button type="submit" class="btn btn-success">Add Vehicle</button>
|
||||
<button type="button" class="btn" onclick="closeModal('add-vehicle-modal')">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadVehicles();
|
||||
});
|
||||
|
||||
document.getElementById('photo').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
document.getElementById('photo-preview').innerHTML =
|
||||
`<img src="${e.target.result}" style="max-width: 200px; border-radius: 8px;">`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('add-vehicle-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let photoUrl = null;
|
||||
const photoFile = document.getElementById('photo').files[0];
|
||||
|
||||
if (photoFile) {
|
||||
const formData = new FormData();
|
||||
formData.append('photo', photoFile);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/upload/photo', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData
|
||||
});
|
||||
const data = await response.json();
|
||||
photoUrl = data.photo_url;
|
||||
} catch (error) {
|
||||
console.error('Photo upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const vehicleData = {
|
||||
year: parseInt(document.getElementById('year').value),
|
||||
make: document.getElementById('make').value,
|
||||
model: document.getElementById('model').value,
|
||||
vin: document.getElementById('vin').value || null,
|
||||
license_plate: document.getElementById('license_plate').value || null,
|
||||
odometer: parseInt(document.getElementById('odometer').value),
|
||||
photo: photoUrl
|
||||
};
|
||||
|
||||
await addVehicle(vehicleData);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue