Initial commit: Belgium Company Car Tax Calculator
- Complete refactored single-page interface - 1,326 vehicles from 35 brands (2020-2025 model years) - Advanced search with year, fuel type, and price filters - Comprehensive pension impact analysis - Multi-language support (EN/FR/NL) - Docker deployment ready - Streamlined UX with required fields first Features: - Vehicle database with BMW, Mercedes, Audi, VW, Tesla, Toyota, etc. - Real-time search with fuzzy matching - Validation for required fields and vehicle selection - Advanced calculation parameters (collapsible) - Responsive design for mobile and desktop 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
659e16dea2
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Backup and temporary files
|
||||||
|
*-backup.js
|
||||||
|
*-new.js
|
||||||
|
*-original.html
|
||||||
|
*-multilingual.html
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
vehicle-data/
|
||||||
|
consolidate-database.js
|
||||||
|
update-vehicle-database.js
|
||||||
|
autocomplete-ui.js
|
||||||
|
|
||||||
|
# Docker volumes
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Use nginx to serve the static HTML file
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy the HTML file to nginx's default serving directory
|
||||||
|
COPY tax.html /usr/share/nginx/html/index.html
|
||||||
|
|
||||||
|
# Copy JavaScript files for vehicle autocomplete system
|
||||||
|
COPY vehicle-database.js /usr/share/nginx/html/vehicle-database.js
|
||||||
|
COPY vehicle-search.js /usr/share/nginx/html/vehicle-search.js
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# nginx will start automatically with the default CMD from the base image
|
||||||
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Belgium Company Car Tax Calculator
|
||||||
|
|
||||||
|
A comprehensive tax calculator to analyze the pension impact of company cars in Belgium.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Streamlined Interface**: Single-page layout with required fields first
|
||||||
|
- **Vehicle Database**: 1,326+ vehicles from 35 brands (2020-2025)
|
||||||
|
- **Advanced Search**: Search with filters for year, fuel type, and price range
|
||||||
|
- **Pension Impact Analysis**: Calculate how company cars affect your retirement pension
|
||||||
|
- **Multi-language Support**: English, French, and Dutch
|
||||||
|
- **Comprehensive Options**: Advanced parameters for detailed calculations
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3005`
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
The application consists of:
|
||||||
|
- `tax.html` - Main application file
|
||||||
|
- `vehicle-database.js` - Vehicle data (35 brands, 1,326 variants)
|
||||||
|
- `vehicle-search.js` - Search and autocomplete functionality
|
||||||
|
- `docker-compose.yml` - Docker configuration
|
||||||
|
- `Dockerfile` - Container build instructions
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Fill Required Information**: Salary, age, retirement details
|
||||||
|
2. **Select Vehicles**: Use the integrated search to find and add company cars
|
||||||
|
3. **Calculate Impact**: Get detailed pension loss analysis
|
||||||
|
4. **Advanced Options**: Fine-tune calculation parameters
|
||||||
|
|
||||||
|
## Vehicle Database
|
||||||
|
|
||||||
|
- **35 Brands**: BMW, Mercedes, Audi, VW, Tesla, Toyota, and more
|
||||||
|
- **1,326 Variants**: Complete model variations with pricing and specifications
|
||||||
|
- **2020-2025 Model Years**: Up-to-date vehicle information
|
||||||
|
- **Complete Data**: Price, CO2 emissions, fuel type, power ratings
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Frontend**: Vanilla JavaScript, HTML5, CSS3
|
||||||
|
- **Search**: Custom fuzzy search with filtering
|
||||||
|
- **Data**: Static JSON database for fast performance
|
||||||
|
- **Deployment**: Docker with Nginx
|
||||||
|
- **Languages**: Multi-language internationalization
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private project for tax calculation purposes.
|
||||||
|
|
||||||
|
## Generated With
|
||||||
|
|
||||||
|
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
tax-calc:
|
||||||
|
build: .
|
||||||
|
container_name: tax-calc
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3005:80" # Port 3005 following the web apps allocation strategy
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.tax-calc.rule=Host(`car-tax-be.gm-tech.org`)"
|
||||||
|
- "traefik.http.routers.tax-calc.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.tax-calc.tls=true"
|
||||||
|
- "traefik.http.routers.tax-calc.tls.certresolver=myresolver"
|
||||||
|
- "traefik.http.services.tax-calc.loadbalancer.server.port=80"
|
||||||
|
- "traefik.docker.network=web"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
10877
vehicle-database.js
Normal file
10877
vehicle-database.js
Normal file
File diff suppressed because it is too large
Load Diff
701
vehicle-search.js
Normal file
701
vehicle-search.js
Normal file
@ -0,0 +1,701 @@
|
|||||||
|
// Vehicle Search System - Type-to-filter approach
|
||||||
|
// Much more user-friendly than cascading dropdowns
|
||||||
|
|
||||||
|
class VehicleSearchAutocomplete {
|
||||||
|
constructor(containerId, onSelectionChange = null) {
|
||||||
|
this.container = document.getElementById(containerId);
|
||||||
|
this.onSelectionChange = onSelectionChange;
|
||||||
|
this.selectedVehicle = null;
|
||||||
|
this.isManualMode = false;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.createSearchInterface();
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSearchInterface() {
|
||||||
|
this.container.innerHTML = `
|
||||||
|
<div class="vehicle-search-container">
|
||||||
|
<div class="search-filters" style="display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap;">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="year-filter">Year</label>
|
||||||
|
<select id="year-filter" class="filter-dropdown">
|
||||||
|
<option value="">All Years</option>
|
||||||
|
<option value="2025">2025</option>
|
||||||
|
<option value="2024">2024</option>
|
||||||
|
<option value="2023">2023</option>
|
||||||
|
<option value="2022">2022</option>
|
||||||
|
<option value="2021">2021</option>
|
||||||
|
<option value="2020">2020</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="fuel-filter">Fuel Type</label>
|
||||||
|
<select id="fuel-filter" class="filter-dropdown">
|
||||||
|
<option value="">All Fuel Types</option>
|
||||||
|
<option value="electric">⚡ Electric</option>
|
||||||
|
<option value="hybrid">🔋 Hybrid</option>
|
||||||
|
<option value="petrol">⛽ Petrol</option>
|
||||||
|
<option value="diesel">🛢️ Diesel</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-group">
|
||||||
|
<label for="price-filter">Price Range</label>
|
||||||
|
<select id="price-filter" class="filter-dropdown">
|
||||||
|
<option value="">All Prices</option>
|
||||||
|
<option value="0-30000">Under €30,000</option>
|
||||||
|
<option value="30000-50000">€30,000 - €50,000</option>
|
||||||
|
<option value="50000-75000">€50,000 - €75,000</option>
|
||||||
|
<option value="75000-100000">€75,000 - €100,000</option>
|
||||||
|
<option value="100000-999999">Above €100,000</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group" style="position: relative;">
|
||||||
|
<label for="vehicle-search-input">Search Vehicle</label>
|
||||||
|
<input type="text"
|
||||||
|
id="vehicle-search-input"
|
||||||
|
class="vehicle-search-input"
|
||||||
|
placeholder="Type to search... (e.g., BMW X1, Mercedes C200, Audi Q5)"
|
||||||
|
autocomplete="off">
|
||||||
|
<div id="vehicle-search-results" class="vehicle-search-results" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="manual-override" style="margin-top: 16px;">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="manual-override-checkbox">
|
||||||
|
<span style="margin-left: 8px; font-size: 0.9rem; color: #6c757d;">Enter vehicle details manually</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="manual-fields" class="manual-fields" style="display: none; margin-top: 16px;">
|
||||||
|
<div class="input-row">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="manual-name">Vehicle Name</label>
|
||||||
|
<input type="text" id="manual-name" placeholder="e.g., Tesla Model 3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="manual-price">List Price (€)</label>
|
||||||
|
<input type="number" id="manual-price" placeholder="50000" min="15000" max="200000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="manual-co2">CO2 Emissions (g/km)</label>
|
||||||
|
<input type="number" id="manual-co2" placeholder="120" min="0" max="300">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="manual-fuel">Fuel Type</label>
|
||||||
|
<select id="manual-fuel">
|
||||||
|
<option value="electric">Electric</option>
|
||||||
|
<option value="petrol">Petrol</option>
|
||||||
|
<option value="diesel">Diesel</option>
|
||||||
|
<option value="hybrid">Hybrid</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selected-vehicle-info" id="selected-vehicle-info" style="display: none; margin-top: 16px; padding: 16px; background: #f8f9fa; border-radius: 6px;">
|
||||||
|
<h4 style="margin-bottom: 12px; font-size: 0.95rem; font-weight: 500; color: #28a745;">✓ Selected Vehicle:</h4>
|
||||||
|
<div id="vehicle-details"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
const searchInput = document.getElementById('vehicle-search-input');
|
||||||
|
const resultsContainer = document.getElementById('vehicle-search-results');
|
||||||
|
const manualCheckbox = document.getElementById('manual-override-checkbox');
|
||||||
|
const manualFields = document.getElementById('manual-fields');
|
||||||
|
|
||||||
|
// Search input
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
this.performSearch(e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter dropdowns
|
||||||
|
const yearFilter = document.getElementById('year-filter');
|
||||||
|
const fuelFilter = document.getElementById('fuel-filter');
|
||||||
|
const priceFilter = document.getElementById('price-filter');
|
||||||
|
|
||||||
|
[yearFilter, fuelFilter, priceFilter].forEach(filter => {
|
||||||
|
filter.addEventListener('change', () => {
|
||||||
|
this.performSearch(searchInput.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Result selection
|
||||||
|
resultsContainer.addEventListener('click', (e) => {
|
||||||
|
const resultItem = e.target.closest('.search-result-item');
|
||||||
|
if (resultItem) {
|
||||||
|
const index = parseInt(resultItem.getAttribute('data-index'));
|
||||||
|
this.selectResult(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide results when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!searchInput.contains(e.target) && !resultsContainer.contains(e.target)) {
|
||||||
|
this.hideResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual override toggle
|
||||||
|
manualCheckbox.addEventListener('change', (e) => {
|
||||||
|
this.isManualMode = e.target.checked;
|
||||||
|
|
||||||
|
if (this.isManualMode) {
|
||||||
|
manualFields.style.display = 'block';
|
||||||
|
searchInput.disabled = true;
|
||||||
|
searchInput.style.background = '#f8f9fa';
|
||||||
|
this.hideResults();
|
||||||
|
this.hideSelectedInfo();
|
||||||
|
} else {
|
||||||
|
manualFields.style.display = 'none';
|
||||||
|
searchInput.disabled = false;
|
||||||
|
searchInput.style.background = 'white';
|
||||||
|
this.updateSelectedVehicleInfo();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Manual field changes
|
||||||
|
const manualFieldIds = ['manual-name', 'manual-price', 'manual-co2', 'manual-fuel'];
|
||||||
|
manualFieldIds.forEach(fieldId => {
|
||||||
|
const field = document.getElementById(fieldId);
|
||||||
|
if (field) {
|
||||||
|
field.addEventListener('input', () => {
|
||||||
|
this.notifySelectionChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
performSearch(searchTerm) {
|
||||||
|
const filters = this.getActiveFilters();
|
||||||
|
const results = VehicleSearchDB.smartSearchWithFilters(searchTerm, filters, 12);
|
||||||
|
this.displayResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveFilters() {
|
||||||
|
return {
|
||||||
|
year: document.getElementById('year-filter')?.value || '',
|
||||||
|
fuel: document.getElementById('fuel-filter')?.value || '',
|
||||||
|
priceRange: document.getElementById('price-filter')?.value || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
selectResult(index) {
|
||||||
|
if (!this.currentResults || !this.currentResults[index]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedVehicle = this.currentResults[index];
|
||||||
|
this.selectedVehicle = selectedVehicle;
|
||||||
|
|
||||||
|
// Update search input with selected vehicle
|
||||||
|
const searchInput = document.getElementById('vehicle-search-input');
|
||||||
|
searchInput.value = `${selectedVehicle.brand} ${selectedVehicle.model} ${selectedVehicle.variant.name} (${selectedVehicle.year})`;
|
||||||
|
|
||||||
|
this.hideResults();
|
||||||
|
this.updateSelectedVehicleInfo();
|
||||||
|
this.notifySelectionChange();
|
||||||
|
|
||||||
|
// Add the selected vehicle to the car list
|
||||||
|
if (window.addSelectedVehicle) {
|
||||||
|
window.addSelectedVehicle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
displayResults(results) {
|
||||||
|
const resultsContainer = document.getElementById('vehicle-search-results');
|
||||||
|
this.currentResults = results; // Store for selection
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
this.hideResults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
const variant = result.variant;
|
||||||
|
const co2Badge = variant.co2 === 0 ?
|
||||||
|
'<span style="background: #d1fae5; color: #065f46; padding: 2px 6px; border-radius: 3px; font-size: 0.75rem; margin-left: 8px;">0g CO₂</span>' :
|
||||||
|
'';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="search-result-item" data-index="${index}">
|
||||||
|
<div class="result-main">
|
||||||
|
<strong>${result.brand} ${result.model}</strong>
|
||||||
|
<span class="result-variant">${variant.name}</span>
|
||||||
|
<span class="result-year">(${result.year})</span>
|
||||||
|
${co2Badge}
|
||||||
|
</div>
|
||||||
|
<div class="result-details">
|
||||||
|
€${variant.price.toLocaleString()} • ${variant.co2}g CO₂ • ${this.getFuelDisplay(variant.fuel)} • ${variant.power}hp
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultsContainer.innerHTML = html;
|
||||||
|
resultsContainer.style.display = 'block';
|
||||||
|
|
||||||
|
// Bind click events
|
||||||
|
resultsContainer.querySelectorAll('.search-result-item').forEach((item, index) => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.selectVehicle(results[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectVehicle(result) {
|
||||||
|
const searchInput = document.getElementById('vehicle-search-input');
|
||||||
|
const vehicleName = `${result.brand} ${result.model} ${result.variant.name} (${result.year})`;
|
||||||
|
|
||||||
|
searchInput.value = vehicleName;
|
||||||
|
this.selectedVehicle = {
|
||||||
|
name: vehicleName,
|
||||||
|
price: result.variant.price,
|
||||||
|
co2: result.variant.co2,
|
||||||
|
fuel: result.variant.fuel,
|
||||||
|
power: result.variant.power,
|
||||||
|
brand: result.brand,
|
||||||
|
model: result.model,
|
||||||
|
year: result.year,
|
||||||
|
variant: result.variant
|
||||||
|
};
|
||||||
|
|
||||||
|
this.hideResults();
|
||||||
|
this.updateSelectedVehicleInfo();
|
||||||
|
this.notifySelectionChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
hideResults() {
|
||||||
|
document.getElementById('vehicle-search-results').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectedVehicleInfo() {
|
||||||
|
const infoDiv = document.getElementById('selected-vehicle-info');
|
||||||
|
const detailsDiv = document.getElementById('vehicle-details');
|
||||||
|
|
||||||
|
if (this.isManualMode || !this.selectedVehicle) {
|
||||||
|
infoDiv.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vehicle = this.selectedVehicle;
|
||||||
|
const variant = vehicle.variant;
|
||||||
|
const fuelBadge = variant.fuel === 'electric' ?
|
||||||
|
'<span style="background: #d1fae5; color: #065f46; padding: 2px 6px; border-radius: 3px; font-size: 0.75rem;">Electric</span>' :
|
||||||
|
variant.fuel === 'hybrid' ?
|
||||||
|
'<span style="background: #fef3c7; color: #92400e; padding: 2px 6px; border-radius: 3px; font-size: 0.75rem;">Hybrid</span>' :
|
||||||
|
`<span style="background: #f3f4f6; color: #374151; padding: 2px 6px; border-radius: 3px; font-size: 0.75rem;">${this.getFuelDisplay(variant.fuel)}</span>`;
|
||||||
|
|
||||||
|
detailsDiv.innerHTML = `
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; font-size: 0.9rem;">
|
||||||
|
<div><strong>Brand:</strong> ${vehicle.brand}</div>
|
||||||
|
<div><strong>Model:</strong> ${vehicle.model}</div>
|
||||||
|
<div><strong>Year:</strong> ${vehicle.year}</div>
|
||||||
|
<div><strong>Price:</strong> €${variant.price.toLocaleString()}</div>
|
||||||
|
<div><strong>CO₂:</strong> ${variant.co2}g/km</div>
|
||||||
|
<div><strong>Fuel:</strong> ${fuelBadge}</div>
|
||||||
|
<div><strong>Power:</strong> ${variant.power}hp</div>
|
||||||
|
<div><strong>Variant:</strong> ${variant.name}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
infoDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSelectedInfo() {
|
||||||
|
document.getElementById('selected-vehicle-info').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
getFuelDisplay(fuelType) {
|
||||||
|
const fuelMap = {
|
||||||
|
electric: 'Electric',
|
||||||
|
petrol: 'Petrol',
|
||||||
|
diesel: 'Diesel',
|
||||||
|
hybrid: 'Hybrid'
|
||||||
|
};
|
||||||
|
return fuelMap[fuelType] || fuelType;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedVehicleData() {
|
||||||
|
if (this.isManualMode) {
|
||||||
|
return {
|
||||||
|
name: document.getElementById('manual-name').value,
|
||||||
|
price: parseFloat(document.getElementById('manual-price').value) || 0,
|
||||||
|
co2: parseInt(document.getElementById('manual-co2').value) || 0,
|
||||||
|
fuel: document.getElementById('manual-fuel').value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.selectedVehicle;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySelectionChange() {
|
||||||
|
if (this.onSelectionChange) {
|
||||||
|
const vehicleData = this.getSelectedVehicleData();
|
||||||
|
this.onSelectionChange(vehicleData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
// Clear search input
|
||||||
|
document.getElementById('vehicle-search-input').value = '';
|
||||||
|
document.getElementById('vehicle-search-input').disabled = false;
|
||||||
|
document.getElementById('vehicle-search-input').style.background = 'white';
|
||||||
|
|
||||||
|
// Reset manual mode
|
||||||
|
document.getElementById('manual-override-checkbox').checked = false;
|
||||||
|
document.getElementById('manual-fields').style.display = 'none';
|
||||||
|
this.isManualMode = false;
|
||||||
|
|
||||||
|
// Clear manual fields
|
||||||
|
document.getElementById('manual-name').value = '';
|
||||||
|
document.getElementById('manual-price').value = '';
|
||||||
|
document.getElementById('manual-co2').value = '';
|
||||||
|
document.getElementById('manual-fuel').value = 'electric';
|
||||||
|
|
||||||
|
// Clear selected vehicle
|
||||||
|
this.selectedVehicle = null;
|
||||||
|
|
||||||
|
this.hideResults();
|
||||||
|
this.hideSelectedInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVehicleData(vehicleData) {
|
||||||
|
if (vehicleData.manual) {
|
||||||
|
document.getElementById('manual-override-checkbox').checked = true;
|
||||||
|
document.getElementById('manual-fields').style.display = 'block';
|
||||||
|
document.getElementById('manual-name').value = vehicleData.name || '';
|
||||||
|
document.getElementById('manual-price').value = vehicleData.price || '';
|
||||||
|
document.getElementById('manual-co2').value = vehicleData.co2 || '';
|
||||||
|
document.getElementById('manual-fuel').value = vehicleData.fuel || 'electric';
|
||||||
|
|
||||||
|
this.isManualMode = true;
|
||||||
|
document.getElementById('vehicle-search-input').disabled = true;
|
||||||
|
document.getElementById('vehicle-search-input').style.background = '#f8f9fa';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced Vehicle Database utilities for search
|
||||||
|
const VehicleSearchDB = {
|
||||||
|
// Get all vehicles as flat array for easier searching
|
||||||
|
getAllVehicles() {
|
||||||
|
const allVehicles = [];
|
||||||
|
|
||||||
|
Object.keys(vehicleDatabase).forEach(brand => {
|
||||||
|
Object.keys(vehicleDatabase[brand]).forEach(model => {
|
||||||
|
Object.keys(vehicleDatabase[brand][model]).forEach(year => {
|
||||||
|
vehicleDatabase[brand][model][year].forEach(variant => {
|
||||||
|
allVehicles.push({
|
||||||
|
brand,
|
||||||
|
model,
|
||||||
|
year,
|
||||||
|
variant,
|
||||||
|
searchText: `${brand} ${model} ${variant.name} ${year}`.toLowerCase()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return allVehicles;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get popular suggestions for empty search
|
||||||
|
getPopularSuggestions() {
|
||||||
|
const popular = [
|
||||||
|
// BMW best sellers
|
||||||
|
{brand: "BMW", model: "X1", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "BMW", model: "3 Series", year: "2024", variantIndex: 0},
|
||||||
|
|
||||||
|
// Mercedes popular
|
||||||
|
{brand: "Mercedes-Benz", model: "C-Class", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Mercedes-Benz", model: "GLC", year: "2024", variantIndex: 0},
|
||||||
|
|
||||||
|
// Audi favorites
|
||||||
|
{brand: "Audi", model: "Q5", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Audi", model: "Q3", year: "2024", variantIndex: 0},
|
||||||
|
|
||||||
|
// VW popular
|
||||||
|
{brand: "Volkswagen", model: "Tiguan", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Volkswagen", model: "Golf", year: "2024", variantIndex: 1},
|
||||||
|
|
||||||
|
// Tesla models
|
||||||
|
{brand: "Tesla", model: "Model Y", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Tesla", model: "Model 3", year: "2024", variantIndex: 0}
|
||||||
|
];
|
||||||
|
|
||||||
|
return popular.map(item => {
|
||||||
|
const variant = vehicleDatabase[item.brand][item.model][item.year][item.variantIndex];
|
||||||
|
return {
|
||||||
|
brand: item.brand,
|
||||||
|
model: item.model,
|
||||||
|
year: item.year,
|
||||||
|
variant
|
||||||
|
};
|
||||||
|
}).filter(item => item.variant); // Filter out any missing variants
|
||||||
|
},
|
||||||
|
|
||||||
|
// Smart search with filters
|
||||||
|
smartSearchWithFilters(searchTerm, filters = {}, limit = 12) {
|
||||||
|
if (!searchTerm || searchTerm.length < 2) {
|
||||||
|
return this.getPopularSuggestions(filters).slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.smartSearch(searchTerm, filters, limit);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Smart search with fuzzy matching
|
||||||
|
smartSearch(searchTerm, filters = {}, limit = 12) {
|
||||||
|
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
Object.keys(vehicleDatabase).forEach(brand => {
|
||||||
|
Object.keys(vehicleDatabase[brand]).forEach(model => {
|
||||||
|
Object.keys(vehicleDatabase[brand][model]).forEach(year => {
|
||||||
|
vehicleDatabase[brand][model][year].forEach(variant => {
|
||||||
|
// Apply filters first
|
||||||
|
if (!this.passesFilters(variant, year, filters)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchText = `${brand} ${model} ${variant.name}`.toLowerCase();
|
||||||
|
const fullText = `${brand} ${model} ${variant.name} ${year}`.toLowerCase();
|
||||||
|
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
// Brand exact match
|
||||||
|
if (brand.toLowerCase() === term) score += 1000;
|
||||||
|
else if (brand.toLowerCase().includes(term)) score += 500;
|
||||||
|
|
||||||
|
// Model exact match
|
||||||
|
if (model.toLowerCase() === term) score += 800;
|
||||||
|
else if (model.toLowerCase().includes(term)) score += 400;
|
||||||
|
|
||||||
|
// Variant name match
|
||||||
|
if (variant.name.toLowerCase().includes(term)) score += 300;
|
||||||
|
|
||||||
|
// Full text contains term
|
||||||
|
if (fullText.includes(term)) score += 100;
|
||||||
|
|
||||||
|
// Partial brand + model match (e.g., "bmw x1")
|
||||||
|
const brandModel = `${brand} ${model}`.toLowerCase();
|
||||||
|
if (brandModel.includes(term)) score += 600;
|
||||||
|
|
||||||
|
// Year preference (newer = better)
|
||||||
|
score += parseInt(year) * 0.1;
|
||||||
|
|
||||||
|
// Electric/hybrid bonus for environmental preference
|
||||||
|
if (variant.fuel === 'electric') score += 50;
|
||||||
|
if (variant.fuel === 'hybrid') score += 25;
|
||||||
|
|
||||||
|
if (score > 0) {
|
||||||
|
results.push({
|
||||||
|
brand,
|
||||||
|
model,
|
||||||
|
year,
|
||||||
|
variant,
|
||||||
|
score
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Check if vehicle passes all active filters
|
||||||
|
passesFilters(variant, year, filters) {
|
||||||
|
// Year filter
|
||||||
|
if (filters.year && year !== filters.year) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fuel type filter
|
||||||
|
if (filters.fuel && variant.fuel !== filters.fuel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price range filter
|
||||||
|
if (filters.priceRange && variant.price) {
|
||||||
|
const [minPrice, maxPrice] = filters.priceRange.split('-').map(Number);
|
||||||
|
if (variant.price < minPrice || variant.price > maxPrice) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get popular suggestions with filters applied
|
||||||
|
getPopularSuggestions(filters = {}) {
|
||||||
|
const popular = [
|
||||||
|
// BMW best sellers
|
||||||
|
{brand: "BMW", model: "X1", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "BMW", model: "3 Series", year: "2024", variantIndex: 0},
|
||||||
|
|
||||||
|
// Mercedes popular
|
||||||
|
{brand: "Mercedes-Benz", model: "C-Class", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Mercedes-Benz", model: "GLC", year: "2024", variantIndex: 0},
|
||||||
|
|
||||||
|
// Audi favorites
|
||||||
|
{brand: "Audi", model: "Q5", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Audi", model: "Q3", year: "2024", variantIndex: 0},
|
||||||
|
|
||||||
|
// VW popular
|
||||||
|
{brand: "Volkswagen", model: "Tiguan", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Volkswagen", model: "Golf", year: "2024", variantIndex: 1},
|
||||||
|
|
||||||
|
// Tesla models
|
||||||
|
{brand: "Tesla", model: "Model Y", year: "2024", variantIndex: 0},
|
||||||
|
{brand: "Tesla", model: "Model 3", year: "2024", variantIndex: 0}
|
||||||
|
];
|
||||||
|
|
||||||
|
return popular.map(item => {
|
||||||
|
if (!vehicleDatabase[item.brand] || !vehicleDatabase[item.brand][item.model] || !vehicleDatabase[item.brand][item.model][item.year]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const variant = vehicleDatabase[item.brand][item.model][item.year][item.variantIndex];
|
||||||
|
if (!variant) return null;
|
||||||
|
|
||||||
|
// Apply filters to popular suggestions too
|
||||||
|
if (!this.passesFilters(variant, item.year, filters)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
brand: item.brand,
|
||||||
|
model: item.model,
|
||||||
|
year: item.year,
|
||||||
|
variant
|
||||||
|
};
|
||||||
|
}).filter(item => item !== null); // Filter out any missing variants
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility functions for integration with existing tax calculator
|
||||||
|
const VehicleAutocompleteUtils = {
|
||||||
|
// Convert autocomplete selection to legacy custom car format
|
||||||
|
convertToCustomCar(vehicleData) {
|
||||||
|
// Handle both data structures: manual mode (flat) and search mode (nested)
|
||||||
|
let name, price, co2, fuel;
|
||||||
|
|
||||||
|
if (vehicleData.variant) {
|
||||||
|
// Search mode: {brand, model, year, variant}
|
||||||
|
const variant = vehicleData.variant;
|
||||||
|
name = `${vehicleData.brand} ${vehicleData.model} ${variant.name} (${vehicleData.year})`;
|
||||||
|
price = variant.price;
|
||||||
|
co2 = variant.co2;
|
||||||
|
fuel = variant.fuel;
|
||||||
|
} else {
|
||||||
|
// Manual mode: {name, price, co2, fuel}
|
||||||
|
name = vehicleData.name;
|
||||||
|
price = vehicleData.price;
|
||||||
|
co2 = vehicleData.co2;
|
||||||
|
fuel = vehicleData.fuel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
price,
|
||||||
|
co2,
|
||||||
|
fuel,
|
||||||
|
examples: vehicleData.variant ? "Selected from database" : "Manual entry"
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Validate vehicle data before adding
|
||||||
|
validateVehicleData(vehicleData) {
|
||||||
|
if (!vehicleData) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Please select a vehicle or enter all required fields"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle both data structures: manual mode (flat) and search mode (nested)
|
||||||
|
let name, price, co2, fuel;
|
||||||
|
|
||||||
|
if (vehicleData.variant) {
|
||||||
|
// Search mode: {brand, model, year, variant}
|
||||||
|
const variant = vehicleData.variant;
|
||||||
|
name = `${vehicleData.brand} ${vehicleData.model} ${variant.name} (${vehicleData.year})`;
|
||||||
|
price = variant.price;
|
||||||
|
co2 = variant.co2;
|
||||||
|
fuel = variant.fuel;
|
||||||
|
} else {
|
||||||
|
// Manual mode: {name, price, co2, fuel}
|
||||||
|
name = vehicleData.name;
|
||||||
|
price = vehicleData.price;
|
||||||
|
co2 = vehicleData.co2;
|
||||||
|
fuel = vehicleData.fuel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || !price) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Please select a vehicle or enter all required fields"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price < 15000 || price > 200000) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Price must be between €15,000 and €200,000"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (co2 < 0 || co2 > 300) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "CO2 emissions must be between 0 and 300 g/km"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get fuel type display name
|
||||||
|
getFuelTypeDisplay(fuelType, language = 'en') {
|
||||||
|
const fuelTypes = {
|
||||||
|
en: {
|
||||||
|
electric: "Electric",
|
||||||
|
petrol: "Petrol",
|
||||||
|
diesel: "Diesel",
|
||||||
|
hybrid: "Hybrid"
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
electric: "Électrique",
|
||||||
|
petrol: "Essence",
|
||||||
|
diesel: "Diesel",
|
||||||
|
hybrid: "Hybride"
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
electric: "Elektrisch",
|
||||||
|
petrol: "Benzine",
|
||||||
|
diesel: "Diesel",
|
||||||
|
hybrid: "Hybride"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return fuelTypes[language][fuelType] || fuelType;
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user