Initial commit: Masina-Dock Vehicle Management System

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

View 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>

View 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

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>