update the car form js

This commit is contained in:
ismail 2025-07-30 12:54:36 +03:00
parent 2aa3b8f86e
commit 9e373128a9
11 changed files with 535 additions and 186 deletions

View File

@ -1,4 +1,4 @@
from datetime import timezone
from django.utils import timezone
import logging
from .models import Dealer
from django.core.exceptions import ImproperlyConfigured, ValidationError
@ -325,7 +325,8 @@ class BasePurchaseOrderActionActionView(
try:
if po_model.can_fulfill():
po_model.mark_as_fulfilled()
# po_model.date_fulfilled = timezone.now()
if po_model.is_fulfilled():
po_model.date_fulfilled = timezone.now().date()
po_model.save()
messages.add_message(
request,

View File

@ -947,7 +947,7 @@ def create_po_item_upload(sender, instance, created, **kwargs):
if instance.po_status == "fulfilled":
for item in instance.get_itemtxs_data()[0]:
dealer = models.Dealer.objects.get(entity=instance.entity)
models.PoItemsUploaded.objects.create(
models.PoItemsUploaded.objects.get_or_create(
dealer=dealer, po=instance, item=item, status="fulfilled"
)

View File

@ -6578,7 +6578,8 @@ def schedule_event(request, dealer_slug, content_type, slug):
)
messages.success(request, _("Appointment Created Successfully"))
return redirect(request.META.get("HTTP_REFERER"))
return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug)
else:
# Log for invalid form data
logger.warning(
@ -9640,9 +9641,11 @@ def add_task(request, dealer_slug, content_type, slug):
f"for {content_type} ID: {obj.slug} (Dealer: {dealer_slug}). Errors: {form.errors.as_json()}"
)
messages.error(request, _("Task form is not valid"))
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
@login_required
@permission_required("inventory.change_tasks", raise_exception=True)
def update_task(request, dealer_slug, pk):
@ -9673,7 +9676,6 @@ def update_schedule(request, dealer_slug, pk):
@permission_required("inventory.add_notes", raise_exception=True)
def add_note(request, dealer_slug, content_type, slug):
# Get user information for logging
print("hi")
user_username = (
request.user.username if request.user.is_authenticated else "anonymous"
)
@ -9732,7 +9734,6 @@ def add_note(request, dealer_slug, content_type, slug):
@permission_required("inventory.change_notes", raise_exception=True)
def update_note(request, dealer_slug, pk):
note = get_object_or_404(models.Notes, pk=pk)
print(note)
lead = get_object_or_404(models.Lead, pk=note.content_object.id)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST":

View File

@ -156,7 +156,7 @@
document.getElementById('global-indicator')
];
});*/
let Toast = Swal.mixin({
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,

View File

@ -15,7 +15,7 @@
</button>
</div>
<div class="modal-body">
<form action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
<form id="scheduleForm" action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
hx-select=".taskTable"
hx-target=".taskTable"
hx-on::after-request="{

View File

@ -28,7 +28,7 @@
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-target="#your-content-container"
hx-swap="innerHTML"
hx-boost="false">
hx-boost="true">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>

View File

@ -325,7 +325,10 @@
<div class="tab-pane fade"
id="tab-activity"
hx-get="{% url 'lead_detail' request.dealer.slug lead.slug %}"
hx-trigger="htmx:afterRequest from:#taskForm,#noteForm"
hx-trigger="htmx:afterRequest from:#noteForm, htmx:afterRequest from:#scheduleForm"
hx-select="#tab-activity"
hx-target="this"
hx-swap="outerHTML"
role="tabpanel"
aria-labelledby="activity-tab">
<div class="mb-1 d-flex justify-content-between align-items-center">
@ -826,19 +829,7 @@
let form = document.querySelector('.add_note_form')
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
}
/*let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});*/
// Display Django messages
{% if messages %}
{% for message in messages %}
Toast.fire({
@ -859,90 +850,7 @@
modal.show();
}
/*document.getElementById('actionTrackingForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Show loading indicator
Swal.fire({
toast: true,
icon: 'info',
text: 'Please wait...',
allowOutsideClick: false,
position: "top-end",
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
fetch("{% url 'update_lead_actions' request.dealer.slug %}", {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
Swal.close();
if (data.success) {
// Success notification
Swal.fire({
toast: true,
icon: 'success',
position: "top-end",
text: data.message || 'Actions updated successfully',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
}).then(() => {
location.reload(); // Refresh after user clicks OK
});
} else {
// Error notification
Swal.fire({
toast: true,
icon: 'error',
position: "top-end",
text: data.message || 'Failed to update actions',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
}
})
.catch(error => {
Swal.close();
console.error('Error:', error);
Swal.fire({
toast: true,
icon: 'error',
position: "top-end",
text: 'An unexpected error occurred',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
});
});*/
// Helper function for notifications
function notify(tag, msg) {
Toast.fire({
icon: tag,

View File

@ -13,7 +13,7 @@
</h2>
<!-- Action Tracking Modal -->
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
@ -29,7 +29,7 @@
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
{% if page_obj.object_list %}
@ -205,7 +205,7 @@
</small>
</div>
</td>
<td class="align-middle white-space-nowrap text-end">
{% if user == lead.staff.user or request.is_dealer %}
<div class="btn-reveal-trigger position-static">
@ -253,7 +253,7 @@
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
@ -262,70 +262,4 @@
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %}
{% endblock %}
{% block customJS %}
<script>
// Initialize SweetAlert Toast for general messages
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
// Display Django messages
{% if messages %}
{% for message in messages %}
Toast.fire({
icon: "{{ message.tags }}",
titleText: "{{ message|safe }}"
});
{% endfor %}
{% endif %}
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction;
document.getElementById('nextActionDate').value = nextActionDate;
modal.show();
}
document.getElementById('actionTrackingForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Show loading indicator
Swal.fire({
toast: true,
icon: 'info',
text: 'Please wait...',
allowOutsideClick: false,
position: "top-end",
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
});
// Helper function for notifications
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
</script>
{% endblock customJS %}
{% endblock %}

View File

@ -1020,6 +1020,11 @@
</div>
<div class="tab-pane fade"
id="tab-activity"
hx-get="{% url 'lead_detail' request.dealer.slug lead.slug %}"
hx-trigger="htmx:afterRequest from:"
hx-select="#tab-activity"
hx-target="this"
hx-swap="outerHTML"
role="tabpanel"
aria-labelledby="activity-tab">
<h2 class="mb-4">Activity</h2>

View File

@ -307,13 +307,13 @@
<div class="parent-wrapper label-1">
<ul class="nav collapse parent" data-bs-parent="#navbarVerticalCollapse" id="nv-reports">
<li class="nav-item">
{% if request.user.is_authenticated and request.is_dealer %}
{% comment %} {% if request.user.is_authenticated and request.is_dealer %}
<a class="nav-link" href="{% url 'entity-dashboard' request.dealer.entity.slug %}">
{% elif request.user.is_authenticated and request.is_staff %}
<a class="nav-link" href="{% url 'entity-dashboard' request.user.staffmember.staff.dealer.entity.slug %}">
{% else %}
<a class="nav-link" href="#">
{% endif %}
{% endif %} {% endcomment %}
<div class="d-flex align-items-center">
{% comment %} <i class="fa-solid fa-chart-line"></i><span class="nav-link-text">{% trans 'Dashboard'|capfirst %}</span> {% endcomment %}
</div>

View File

@ -331,18 +331,18 @@
{% endblock content %}
{% block customJS %}
<script>
// Global variables
/*
// Global variables
let codeReader;
let currentStream = null;
const csrfToken = getCookie("csrftoken");
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
// Initialize when page loads and after HTMX swaps
document.addEventListener('DOMContentLoaded', initPage);
document.addEventListener('htmx:afterSwap', initPage);
document.addEventListener('htmx:afterRequest', initPage);
function initPage() {
let codeReader;
let currentStream = null;
const csrfToken = getCookie("csrftoken");
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
// Get DOM elements
const elements = {
vinInput: document.getElementById("{{ form.vin.id_for_label }}"),
@ -758,6 +758,506 @@ function notify(tag, msg) {
icon: tag,
titleText: msg,
});
}
}/
*/
// Helper function to get CSRF token from cookies
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// SweetAlert loading and notification functions
function showLoading() {
Swal.fire({
title: "{% trans 'Please Wait' %}",
text: "{% trans 'Loading' %}...",
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
});
}
function hideLoading() {
Swal.close();
}
function notify(tag, msg) {
Swal.fire({
icon: tag,
titleText: msg,
});
}
// Flag to control programmatic changes vs. user-initiated changes
let isProgrammaticChange = false;
// Main initialization function for VIN Decoder logic
function initVinDecoder() {
// Get CSRF token
const csrfToken = getCookie("csrftoken");
// Get DOM elements
const vinInput = document.getElementById("{{ form.vin.id_for_label }}");
const decodeVinBtn = document.getElementById("decodeVinBtn");
const makeSelect = document.getElementById("{{ form.id_car_make.id_for_label }}");
const modelSelect = document.getElementById("{{ form.id_car_model.id_for_label }}");
const yearSelect = document.getElementById("{{ form.year.id_for_label }}");
const serieSelect = document.getElementById("{{ form.id_car_serie.id_for_label }}");
const trimSelect = document.getElementById("{{ form.id_car_trim.id_for_label }}");
const equipmentSelect = document.getElementById("equipment_id");
const showSpecificationButton = document.getElementById("specification-btn");
const showEquipmentButton = document.getElementById("options-btn");
const specificationsContent = document.getElementById("specificationsContent");
const optionsContent = document.getElementById("optionsContent");
const generationContainer = document.getElementById("generation-div");
const closeButton = document.querySelector(".btn-close");
const scanVinBtn = document.getElementById("scan-vin-btn");
const videoElement = document.getElementById("video");
const resultDisplay = document.getElementById("result");
const fallbackButton = document.getElementById("ocr-fallback-btn");
// Define AJAX URL
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
// ZXing and OCR setup
let codeReader = new ZXing.BrowserMultiFormatReader();
let currentStream = null;
// --- Helper Functions for VIN Decoder Logic ---
function closeModal() {
stopScanner();
try {
const scannerModal = document.getElementById("scannerModal");
if (scannerModal) {
document.activeElement.blur();
scannerModal.setAttribute("inert", "true");
const modalInstance = bootstrap.Modal.getInstance(scannerModal);
if (modalInstance) modalInstance.hide();
if (scanVinBtn) scanVinBtn.focus();
}
} catch (err) {
console.error("Error closing scanner modal:", err);
}
}
async function decodeVin() {
const vinNumber = vinInput.value.trim();
if (vinNumber.length !== 17) {
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
return;
}
showLoading();
try {
const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
if (data.success) {
hideLoading();
await updateFields(data.data);
} else {
hideLoading();
Swal.fire("{% trans 'error' %}", data.error);
}
} catch (error) {
console.error("Error decoding VIN:", error);
hideLoading();
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}");
}
}
/**
* Orchestrates the updates of fields and subsequent dropdowns based on VIN data.
* Sets `isProgrammaticChange` to prevent recursive event firing.
*/
async function updateFields(vinData) {
console.log(vinData);
isProgrammaticChange = true; // Set flag to prevent immediate cascade
try {
if (vinData.make_id) {
makeSelect.value = vinData.make_id;
document.getElementById("make-check").innerHTML = "&#10003;";
// Loading models will trigger its change listener, but `isProgrammaticChange`
// will prevent immediate cascade to series.
await loadModels(vinData.make_id);
}
if (vinData.model_id) {
modelSelect.value = vinData.model_id;
document.getElementById("model-check").innerHTML = "&#10003;";
// Manually trigger the next step: load series
await loadSeries(vinData.model_id, vinData.year, vinData.serie_id); // Pass expected serie_id for selection
}
if (vinData.year) {
yearSelect.value = vinData.year;
document.getElementById("year-check").innerHTML = "&#10003;";
}
} finally {
isProgrammaticChange = false; // Reset flag after programmatic updates are done
}
}
async function startScanner() {
codeReader
.decodeFromVideoDevice(null, videoElement, async (result, err) => {
let res = await result;
if (result) {
vinInput.value = result.text;
closeModal();
await decodeVin();
}
})
.catch(console.error);
}
function captureAndOCR() {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
Tesseract.recognize(canvas.toDataURL("image/png"), "eng")
.then(({ data: { text } }) => {
const vin = text.match(/[A-HJ-NPR-Z0-9]{17}/);
if (vin) vinInput.value = vin[0];
closeModal();
decodeVin();
})
.catch((err) => console.error("OCR Error:", err));
}
function stopScanner() {
if (currentStream) {
currentStream.getTracks().forEach((track) => track.stop());
currentStream = null;
}
codeReader.reset();
}
function resetDropdown(dropdown, placeholder) {
dropdown.innerHTML = `<option value="">${placeholder}</option>`;
}
async function loadModels(makeId) {
resetDropdown(modelSelect, '{% trans "Select" %}');
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((model) => {
const option = document.createElement("option");
option.value = model.id_car_model;
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
modelSelect.appendChild(option);
});
}
/**
* Loads series and optionally selects a specific serie_id, then triggers trim loading.
* @param {string} modelId
* @param {string} year
* @param {string} [selectSerieId] - Optional ID of the serie to select after loading.
*/
async function loadSeries(modelId, year, selectSerieId = null) {
resetDropdown(serieSelect, '{% trans "Select" %}');
resetDropdown(trimSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
optionsContent.innerHTML = ""; // Clear options content too
showSpecificationButton.disabled = true;
showEquipmentButton.disabled = true;
const response = await fetch(`${ajaxUrl}?action=get_series&model_id=${modelId}&year=${year}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
console.log(data);
data.forEach((serie) => {
const option = document.createElement("option");
option.value = serie.id_car_serie;
option.textContent = document.documentElement.lang === "en" ? serie.name : serie.name;
serieSelect.appendChild(option);
});
if (selectSerieId && serieSelect.querySelector(`option[value="${selectSerieId}"]`)) {
serieSelect.value = selectSerieId;
const selectedSerie = data.find(s => s.id_car_serie === selectSerieId);
if (selectedSerie) {
generationContainer.innerHTML = selectedSerie.generation_name;
}
// If a specific series was selected, now manually load its trims
await loadTrims(selectSerieId, modelId, null); // Pass null for selectTrimId, as we only have serie_id from VIN
} else if (data.length > 0) {
// If no specific serie to select, but there are series, select the first one
serieSelect.value = data[0].id_car_serie;
generationContainer.innerHTML = data[0].generation_name || '';
await loadTrims(data[0].id_car_serie, modelId, null);
} else {
generationContainer.innerHTML = '';
}
}
/**
* Loads trims and optionally selects a specific trim_id, then triggers specs/equipment loading.
* @param {string} serie_id
* @param {string} model_id
* @param {string} [selectTrimId] - Optional ID of the trim to select after loading.
*/
async function loadTrims(serie_id, model_id, selectTrimId = null) {
resetDropdown(trimSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
optionsContent.innerHTML = "";
showSpecificationButton.disabled = true;
showEquipmentButton.disabled = true;
const response = await fetch(`${ajaxUrl}?action=get_trims&serie_id=${serie_id}&model_id=${model_id}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
}
});
const data = await response.json();
data.forEach((trim) => {
const option = document.createElement("option");
option.value = trim.id_car_trim;
option.textContent = document.documentElement.lang === "en" ? trim.name : trim.name;
trimSelect.appendChild(option);
});
if (selectTrimId && trimSelect.querySelector(`option[value="${selectTrimId}"]`)) {
trimSelect.value = selectTrimId;
// If a specific trim was selected, now manually load its specs and equipment
await loadSpecifications(selectTrimId);
await loadEquipment(selectTrimId);
} else if (data.length > 0) {
// If no specific trim to select, but there are trims, select the first one
trimSelect.value = data[0].id_car_trim;
await loadSpecifications(data[0].id_car_trim);
await loadEquipment(data[0].id_car_trim);
}
}
async function loadEquipment(trimId){
resetDropdown(equipmentSelect, '{% trans "Select" %}');
optionsContent.innerHTML = ""; // Clear options content
showEquipmentButton.disabled = !trimId; // Enable/disable based on trimId presence
if (!trimId) return; // No trim ID, no equipment to load
const response = await fetch(`${ajaxUrl}?action=get_equipments&trim_id=${trimId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
data.forEach((equipment) => {
const option = document.createElement('option');
option.value = equipment.id_car_equipment;
option.textContent = equipment.name;
equipmentSelect.appendChild(option);
});
}
async function loadSpecifications(trimId) {
specificationsContent.innerHTML = "";
showSpecificationButton.disabled = !trimId; // Enable/disable based on trimId presence
if (!trimId) return; // No trim ID, no specifications to load
const response = await fetch(`${ajaxUrl}?action=get_specifications&trim_id=${trimId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((spec) => {
const parentDiv = document.createElement("div");
parentDiv.innerHTML = `<strong>${spec.parent_name}</strong>`;
spec.specifications.forEach((s) => {
const specDiv = document.createElement("div");
specDiv.innerHTML = `• ${s.s_name}: ${s.s_value} ${s.s_unit}`;
parentDiv.appendChild(specDiv);
});
specificationsContent.appendChild(parentDiv);
});
}
async function loadOptions(equipmentId) {
optionsContent.innerHTML = "";
if (!equipmentId) return; // No equipment ID, no options to load
const response = await fetch(`${ajaxUrl}?action=get_options&equipment_id=${equipmentId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((parent) => {
const parentDiv = document.createElement("div");
parentDiv.innerHTML = `<strong>${parent.parent_name}</strong>`;
parent.options.forEach((option) => {
const optDiv = document.createElement("div");
optDiv.innerHTML = `• ${option.option_name}`;
parentDiv.appendChild(optDiv);
});
optionsContent.appendChild(parentDiv);
});
}
// --- Event Listeners ---
if (decodeVinBtn) {
decodeVinBtn.addEventListener("click", decodeVin);
}
if (scanVinBtn) {
scanVinBtn.addEventListener("click", () => {
resultDisplay.textContent = "";
startScanner();
});
}
if (fallbackButton) {
fallbackButton.addEventListener("click", () => {
captureAndOCR();
});
}
// Modified modelSelect listener: ONLY triggers loadSeries.
// The chain will be managed by explicit calls in loadSeries/loadTrims.
if (modelSelect) {
modelSelect.addEventListener("change", async (e) => {
if (isProgrammaticChange) return; // Prevent user-initiated event during programmatic updates
const selectedModelId = e.target.value;
const selectedYear = yearSelect.value;
if (selectedModelId) {
await loadSeries(selectedModelId, selectedYear);
} else {
// Reset all dependent dropdowns if model is cleared
resetDropdown(serieSelect, '{% trans "Select" %}');
resetDropdown(trimSelect, '{% trans "Select" %}');
resetDropdown(equipmentSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
optionsContent.innerHTML = "";
generationContainer.innerHTML = '';
showSpecificationButton.disabled = true;
showEquipmentButton.disabled = true;
}
});
}
// Modified serieSelect listener: ONLY triggers loadTrims.
// The chain will be managed by explicit calls in loadTrims.
if (serieSelect) {
serieSelect.addEventListener("change", async () => {
if (isProgrammaticChange) return; // Prevent user-initiated event during programmatic updates
const serie_id = serieSelect.value;
const model_id = modelSelect.value; // Get model_id from modelSelect
if (serie_id && model_id) {
await loadTrims(serie_id, model_id); // Only load trims
} else {
// Reset dependent dropdowns and content
resetDropdown(trimSelect, '{% trans "Select" %}');
resetDropdown(equipmentSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
optionsContent.innerHTML = "";
showSpecificationButton.disabled = true;
showEquipmentButton.disabled = true;
}
// Update generation text immediately on user change
const currentSerie = serieSelect.options[serieSelect.selectedIndex];
generationContainer.innerHTML = currentSerie ? currentSerie.dataset.generationName || '' : ''; // Assuming you add data-generation-name to options later
});
}
// trimSelect listener: Triggers loadSpecifications and loadEquipment
if (trimSelect) {
trimSelect.addEventListener("change", async () => {
if (isProgrammaticChange) return; // Prevent user-initiated event during programmatic updates
const trimId = trimSelect.value;
if (trimId) {
await loadSpecifications(trimId);
await loadEquipment(trimId);
} else {
// Reset dependent content and equipment dropdown
specificationsContent.innerHTML = "";
optionsContent.innerHTML = "";
resetDropdown(equipmentSelect, '{% trans "Select" %}');
showSpecificationButton.disabled = true;
showEquipmentButton.disabled = true;
}
});
}
// equipmentSelect listener: Triggers loadOptions
if (equipmentSelect) {
equipmentSelect.addEventListener("change", async () => {
if (isProgrammaticChange) return; // Prevent user-initiated event during programmatic updates
const equipmentId = equipmentSelect.value;
if (equipmentId) {
await loadOptions(equipmentId);
} else {
optionsContent.innerHTML = "";
}
});
}
if (closeButton) {
closeButton.addEventListener("click", closeModal);
}
if (makeSelect) {
makeSelect.addEventListener("change", (e) => {
// When make changes, only load models. No need to reset programmatically
// because modelSelect's own listener will handle the cascade if a model is selected.
loadModels(e.target.value);
// However, clear everything downstream immediately for user clarity.
resetDropdown(modelSelect, '{% trans "Select" %}');
resetDropdown(serieSelect, '{% trans "Select" %}');
resetDropdown(trimSelect, '{% trans "Select" %}');
resetDropdown(equipmentSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
optionsContent.innerHTML = "";
generationContainer.innerHTML = '';
showSpecificationButton.disabled = true;
showEquipmentButton.disabled = true;
});
}
}
// Initialize the VIN decoder when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initVinDecoder);
// Reinitialize after HTMX swaps
document.addEventListener('htmx:afterSwap', initVinDecoder);
</script>
{% endblock customJS %}