update the car form js
This commit is contained in:
parent
2aa3b8f86e
commit
9e373128a9
@ -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,
|
||||
|
||||
@ -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"
|
||||
)
|
||||
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -156,7 +156,7 @@
|
||||
document.getElementById('global-indicator')
|
||||
];
|
||||
});*/
|
||||
let Toast = Swal.mixin({
|
||||
let Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
|
||||
@ -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="{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 = "✓";
|
||||
// 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 = "✓";
|
||||
// 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 = "✓";
|
||||
}
|
||||
} 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user