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,371 @@
:root {
--bg-primary: #1a1d2e;
--bg-secondary: #2d3250;
--bg-tertiary: #424769;
--text-primary: #f6f6f6;
--text-secondary: #b8b8b8;
--accent: #676f9d;
--accent-hover: #7d87ab;
--success: #4caf50;
--warning: #ff9800;
--error: #f44336;
--urgent: #e91e63;
--border: #3d4059;
}
[data-theme="light"] {
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e8e8e8;
--text-primary: #2d3250;
--text-secondary: #666666;
--accent: #5865f2;
--accent-hover: #4752c4;
--border: #d0d0d0;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background: var(--bg-secondary);
padding: 15px 30px;
border-bottom: 2px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
}
.logo img {
width: 40px;
height: 40px;
}
.logo h1 {
font-size: 24px;
font-weight: 600;
}
nav {
display: flex;
gap: 20px;
align-items: center;
}
nav a {
color: var(--text-primary);
text-decoration: none;
padding: 8px 16px;
border-radius: 5px;
transition: background 0.3s;
}
nav a:hover, nav a.active {
background: var(--accent);
}
.theme-toggle {
background: var(--bg-tertiary);
border: none;
color: var(--text-primary);
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.theme-toggle:hover {
background: var(--accent);
}
.card {
background: var(--bg-secondary);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.vehicle-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.vehicle-card {
background: var(--bg-secondary);
border-radius: 10px;
padding: 15px;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
position: relative;
}
.vehicle-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
}
.vehicle-card.sold::after {
content: 'SOLD';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-15deg);
font-size: 48px;
font-weight: bold;
color: var(--error);
opacity: 0.7;
}
.vehicle-photo {
width: 100%;
height: 150px;
object-fit: cover;
border-radius: 8px;
margin-bottom: 10px;
}
.btn {
background: var(--accent);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.btn:hover {
background: var(--accent-hover);
}
.btn-danger {
background: var(--error);
}
.btn-success {
background: var(--success);
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: var(--bg-tertiary);
font-weight: 600;
}
tr:hover {
background: var(--bg-tertiary);
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--bg-secondary);
padding: 30px;
border-radius: 10px;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.form-group {
margin: 15px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid var(--border);
border-radius: 5px;
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 14px;
}
.kanban-board {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.kanban-column {
background: var(--bg-secondary);
border-radius: 10px;
padding: 15px;
}
.kanban-column h3 {
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid var(--accent);
}
.kanban-item {
background: var(--bg-tertiary);
padding: 12px;
border-radius: 5px;
margin: 10px 0;
cursor: move;
}
.priority-high {
border-left: 4px solid var(--error);
}
.priority-medium {
border-left: 4px solid var(--warning);
}
.priority-low {
border-left: 4px solid var(--success);
}
.urgency-very-urgent {
background: var(--urgent);
}
.urgency-urgent {
background: var(--warning);
}
.urgency-not-urgent {
background: var(--success);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.stat-card {
background: var(--bg-secondary);
padding: 20px;
border-radius: 10px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: var(--accent);
}
.stat-label {
color: var(--text-secondary);
margin-top: 5px;
}
.chart-container {
background: var(--bg-secondary);
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
.context-menu {
position: fixed;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 5px;
padding: 5px 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
z-index: 1000;
display: none;
}
.context-menu.active {
display: block;
}
.context-menu-item {
padding: 10px 20px;
cursor: pointer;
transition: background 0.2s;
}
.context-menu-item:hover {
background: var(--bg-tertiary);
}
@media (max-width: 768px) {
header {
flex-direction: column;
gap: 15px;
}
nav {
flex-wrap: wrap;
justify-content: center;
}
.vehicle-grid {
grid-template-columns: 1fr;
}
.kanban-board {
grid-template-columns: 1fr;
}
}

View file

@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200">
<defs>
<linearGradient id="carGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#676f9d;stop-opacity:1" />
<stop offset="100%" style="stop-color:#5865f2;stop-opacity:1" />
</linearGradient>
</defs>
<circle cx="100" cy="100" r="95" fill="url(#carGradient)" opacity="0.1"/>
<path d="M 50 120 L 60 90 L 140 90 L 150 120 Z" fill="url(#carGradient)" stroke="#ffffff" stroke-width="3"/>
<rect x="65" y="95" width="25" height="20" fill="#ffffff" opacity="0.7" rx="2"/>
<rect x="110" y="95" width="25" height="20" fill="#ffffff" opacity="0.7" rx="2"/>
<circle cx="70" cy="125" r="12" fill="#2d3250" stroke="#ffffff" stroke-width="3"/>
<circle cx="70" cy="125" r="6" fill="#676f9d"/>
<circle cx="130" cy="125" r="12" fill="#2d3250" stroke="#ffffff" stroke-width="3"/>
<circle cx="130" cy="125" r="6" fill="#676f9d"/>
<path d="M 50 120 L 45 120 L 45 110 L 50 110 Z" fill="url(#carGradient)"/>
<path d="M 150 120 L 155 120 L 155 110 L 150 110 Z" fill="url(#carGradient)"/>
<g transform="translate(100, 155)">
<path d="M -25 -5 L -15 5 L -5 -5" stroke="#676f9d" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M 5 -5 L 15 5 L 25 -5" stroke="#676f9d" stroke-width="3" fill="none" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,247 @@
const API_URL = window.location.origin;
let currentUser = null;
let currentTheme = 'dark';
async function apiRequest(endpoint, options = {}) {
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
credentials: 'include',
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Request failed');
}
return response.json();
}
function toggleTheme() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
if (currentUser) {
apiRequest('/api/settings/theme', {
method: 'POST',
body: JSON.stringify({ theme: currentTheme })
});
}
}
async function login(username, password) {
try {
const data = await apiRequest('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
currentUser = data.user;
currentTheme = data.user.theme;
document.documentElement.setAttribute('data-theme', currentTheme);
if (data.user.first_login) {
showChangePasswordModal();
} else {
window.location.href = '/dashboard';
}
} catch (error) {
alert(error.message);
}
}
async function register(username, email, password) {
try {
await apiRequest('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ username, email, password })
});
alert('Registration successful! Please login.');
window.location.href = '/login';
} catch (error) {
alert(error.message);
}
}
async function logout() {
try {
await apiRequest('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
} catch (error) {
alert(error.message);
}
}
async function loadVehicles() {
try {
const vehicles = await apiRequest('/api/vehicles');
displayVehicles(vehicles);
} catch (error) {
console.error('Failed to load vehicles:', error);
}
}
function displayVehicles(vehicles) {
const container = document.getElementById('vehicles-container');
container.innerHTML = vehicles.map(v => `
<div class="vehicle-card ${v.status === 'sold' ? 'sold' : ''}" onclick="viewVehicle(${v.id})">
${v.photo ? `<img src="${v.photo}" class="vehicle-photo" alt="${v.make} ${v.model}">` : ''}
<h3>${v.year} ${v.make} ${v.model}</h3>
<p>${v.vin || 'No VIN'}</p>
<p>Odometer: ${v.odometer.toLocaleString()} miles</p>
</div>
`).join('');
}
async function addVehicle(vehicleData) {
try {
await apiRequest('/api/vehicles', {
method: 'POST',
body: JSON.stringify(vehicleData)
});
loadVehicles();
closeModal('vehicle-modal');
} catch (error) {
alert(error.message);
}
}
async function loadServiceRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/service-records`);
displayServiceRecords(records);
} catch (error) {
console.error('Failed to load service records:', error);
}
}
function displayServiceRecords(records) {
const tbody = document.getElementById('service-records-tbody');
tbody.innerHTML = records.map(r => `
<tr oncontextmenu="showContextMenu(event, 'service', ${r.id})">
<td>${new Date(r.date).toLocaleDateString()}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.description}</td>
<td>$${r.cost.toFixed(2)}</td>
<td>${r.category || 'N/A'}</td>
<td>${r.notes || ''}</td>
</tr>
`).join('');
}
async function loadFuelRecords(vehicleId) {
try {
const records = await apiRequest(`/api/vehicles/${vehicleId}/fuel-records`);
displayFuelRecords(records);
calculateFuelStats(records);
} catch (error) {
console.error('Failed to load fuel records:', error);
}
}
function displayFuelRecords(records) {
const tbody = document.getElementById('fuel-records-tbody');
tbody.innerHTML = records.map(r => `
<tr>
<td>${new Date(r.date).toLocaleDateString()}</td>
<td>${r.odometer.toLocaleString()}</td>
<td>${r.fuel_amount.toFixed(2)}</td>
<td>$${r.cost.toFixed(2)}</td>
<td>${r.distance || 'N/A'}</td>
<td>${r.fuel_economy ? r.fuel_economy.toFixed(2) : 'N/A'} ${r.unit}</td>
</tr>
`).join('');
}
function calculateFuelStats(records) {
const totalCost = records.reduce((sum, r) => sum + r.cost, 0);
const avgEconomy = records.filter(r => r.fuel_economy).reduce((sum, r, _, arr) =>
sum + r.fuel_economy / arr.length, 0);
document.getElementById('total-fuel-cost').textContent = `$${totalCost.toFixed(2)}`;
document.getElementById('avg-fuel-economy').textContent = avgEconomy.toFixed(2);
}
async function loadReminders(vehicleId) {
try {
const reminders = await apiRequest(`/api/vehicles/${vehicleId}/reminders`);
displayReminders(reminders);
} catch (error) {
console.error('Failed to load reminders:', error);
}
}
function displayReminders(reminders) {
const container = document.getElementById('reminders-container');
container.innerHTML = reminders.map(r => `
<div class="kanban-item urgency-${r.urgency}">
<h4>${r.description}</h4>
<p>${r.due_date ? `Due: ${new Date(r.due_date).toLocaleDateString()}` : ''}</p>
<p>${r.due_odometer ? `At: ${r.due_odometer.toLocaleString()} miles` : ''}</p>
<p>${r.notes || ''}</p>
</div>
`).join('');
}
async function loadTodos(vehicleId) {
try {
const todos = await apiRequest(`/api/vehicles/${vehicleId}/todos`);
displayTodos(todos);
} catch (error) {
console.error('Failed to load todos:', error);
}
}
function displayTodos(todos) {
const statuses = ['planned', 'doing', 'testing', 'done'];
statuses.forEach(status => {
const column = document.getElementById(`todos-${status}`);
const filtered = todos.filter(t => t.status === status);
column.innerHTML = filtered.map(t => `
<div class="kanban-item priority-${t.priority}" draggable="true" data-id="${t.id}">
<h4>${t.description}</h4>
<p>Cost: $${t.cost.toFixed(2)}</p>
<p>${t.type || ''}</p>
<small>${t.notes || ''}</small>
</div>
`).join('');
});
}
function showContextMenu(event, type, id) {
event.preventDefault();
const menu = document.getElementById('context-menu');
menu.style.left = `${event.pageX}px`;
menu.style.top = `${event.pageY}px`;
menu.classList.add('active');
menu.dataset.type = type;
menu.dataset.id = id;
}
document.addEventListener('click', () => {
document.getElementById('context-menu')?.classList.remove('active');
});
function showModal(modalId) {
document.getElementById(modalId).classList.add('active');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('active');
}
async function exportData(type, vehicleId) {
window.location.href = `${API_URL}/api/export/${type}?vehicle_id=${vehicleId}`;
}
document.addEventListener('DOMContentLoaded', () => {
const theme = localStorage.getItem('theme') || 'dark';
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
});