433 lines
17 KiB
HTML
433 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
{% load crispy_forms_filters %}
|
|
{% load i18n static %}
|
|
{% block title %}
|
|
{{ _("Create Quotation") }}
|
|
{% endblock title %}
|
|
{% block content %}
|
|
<style>
|
|
.disabled{
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
.color-box {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
} /* Custom select styles */
|
|
.custom-select {
|
|
position: relative;
|
|
width: 100%;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.select-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.4rem 1rem;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
background-color: white;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.select-trigger:hover {
|
|
border-color: #aaa;
|
|
}
|
|
|
|
.select-trigger.active {
|
|
border-color: #4a90e2;
|
|
box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2);
|
|
}
|
|
|
|
.selected-value {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.selected-value img {
|
|
width: 30px;
|
|
height: 20px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.dropdown-icon {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.custom-select.open .dropdown-icon {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.options-container {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
width: 100%;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
border: 1px solid #ddd;
|
|
border-radius: 6px;
|
|
background-color: white;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
z-index: 100;
|
|
display: none;
|
|
}
|
|
|
|
.custom-select.open .options-container {
|
|
display: block;
|
|
animation: fadeIn 0.2s ease;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(-10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.option {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem 1rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.1s ease;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.option:hover {
|
|
background-color: #f0f7ff;
|
|
}
|
|
|
|
.option.selected {
|
|
background-color: #e6f0ff;
|
|
}
|
|
|
|
.option img {
|
|
width: 30px;
|
|
height: 20px;
|
|
object-fit: contain;
|
|
}
|
|
|
|
/* Hidden native select for form submission */
|
|
.native-select {
|
|
position: absolute;
|
|
opacity: 0;
|
|
height: 0;
|
|
width: 0;
|
|
}
|
|
</style>
|
|
<div class="row d-flex justify-content-center align-items-center mt-5 mb-3 ms-6 ps-3">
|
|
<div class="row">
|
|
<div class="col">
|
|
{% if not items %}
|
|
{% url "car_add" request.dealer.slug as create_car_url %}
|
|
{% include "message-illustration.html" with value1=no_items_message value2=no_items_button message_image="images/logos/no-content-new.jpg" url=create_car_url %}
|
|
{% endif %}
|
|
</div>
|
|
<div class="col">
|
|
{% if not customer_count %}
|
|
{% url "customer_create" request.dealer.slug as create_customer_url %}
|
|
{% include "message-illustration.html" with value1=no_customers_message value2=no_customers_button message_image="images/logos/no-content-new.jpg" url=create_customer_url %}
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<div class="col-md-10 ms-4 needs-validation {% if not items or not customer_count %}d-none{% endif %}">
|
|
<div class="card shadow-lg border-0 rounded-4 animate__animated animate__fadeInUp">
|
|
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
|
|
<h3 class="mb-0 fs-4 text-center">
|
|
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines ms-2"></i>
|
|
</h3>
|
|
</div>
|
|
<div class="card-body p-4 p-md-5">
|
|
<form id="mainForm"
|
|
method="post"
|
|
class="needs-validation {% if not items and not customer_count %}d-none{% endif %}">
|
|
{% csrf_token %}
|
|
<div class="row g-3">
|
|
{{ form|crispy }}
|
|
<div class="custom-select">
|
|
<!-- Hidden native select for form submission -->
|
|
<select class="native-select" name="item" required tabindex="-1">
|
|
<option value="">Select a car</option>
|
|
{% for item in items %}<option value="{{ item.hash }}"></option>{% endfor %}
|
|
</select>
|
|
<!-- Custom select UI -->
|
|
<div class="select-trigger">
|
|
<div class="selected-value">
|
|
<span>Select a car</span>
|
|
</div>
|
|
<i class="fas fa-chevron-down dropdown-icon"></i>
|
|
</div>
|
|
<div class="options-container">
|
|
{% for item in items %}
|
|
<div class="option"
|
|
data-value="{{ item.hash }}"
|
|
data-image="{{ item.logo }}">
|
|
<img src="{{ item.logo }}" alt="{{ item.model }}">
|
|
<span>{{ item.vin }} {{ item.make }} {{ item.model }} {{ item.serie }} {{ item.trim }} {{ item.color_name }}</span>
|
|
<div class="color-box"
|
|
style="background-color: rgb({{ item.exterior_color }})">
|
|
</div>
|
|
<div class="color-box"
|
|
style="background-color: rgb({{ item.interior_color }})">
|
|
</div>
|
|
<span style="color:gray;">({{ item.hash_count }} in stock)</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<hr class="my-2">
|
|
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
|
|
<button class="btn btn-lg btn-phoenix-primary md-me-2" type="submit">
|
|
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
|
|
</button>
|
|
<a href="{% url 'estimate_list' request.dealer.slug %}"
|
|
class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block customJS %}
|
|
<script>
|
|
// Global variables
|
|
let Toast;
|
|
let customSelectsInitialized = false;
|
|
let formInitialized = false;
|
|
|
|
// Initialize when page loads and after HTMX swaps
|
|
document.addEventListener('DOMContentLoaded', initPage);
|
|
document.addEventListener('htmx:afterSwap', initPage);
|
|
|
|
function initPage() {
|
|
initToast();
|
|
initCustomSelects();
|
|
initFormSubmission();
|
|
}
|
|
|
|
function initToast() {
|
|
if (typeof Swal !== 'undefined') {
|
|
Toast = Swal.mixin({
|
|
toast: true,
|
|
position: "top-end",
|
|
showConfirmButton: false,
|
|
timer: 2000,
|
|
timerProgressBar: false,
|
|
didOpen: (toast) => {
|
|
toast.onmouseenter = Swal.stopTimer;
|
|
toast.onmouseleave = Swal.resumeTimer;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function notify(tag, msg) {
|
|
if (Toast) {
|
|
Toast.fire({icon: tag, titleText: msg});
|
|
} else if (typeof Swal !== 'undefined') {
|
|
Swal.fire({icon: tag, titleText: msg});
|
|
} else {
|
|
console.log(`${tag}: ${msg}`);
|
|
}
|
|
}
|
|
|
|
function initCustomSelects() {
|
|
// Clean up previous listeners if any
|
|
if (customSelectsInitialized) {
|
|
document.removeEventListener('click', handleDocumentClickForSelects);
|
|
}
|
|
|
|
const customSelects = document.querySelectorAll('.custom-select');
|
|
if (!customSelects.length) return;
|
|
|
|
customSelects.forEach(select => {
|
|
const trigger = select.querySelector('.select-trigger');
|
|
const options = select.querySelectorAll('.option');
|
|
const nativeSelect = select.querySelector('.native-select');
|
|
const selectedValue = select.querySelector('.selected-value');
|
|
|
|
// Clone elements to remove old event listeners
|
|
if (trigger) trigger.replaceWith(trigger.cloneNode(true));
|
|
options.forEach(option => option.replaceWith(option.cloneNode(true)));
|
|
|
|
// Get fresh references
|
|
const newTrigger = select.querySelector('.select-trigger');
|
|
const newOptions = select.querySelectorAll('.option');
|
|
const newNativeSelect = select.querySelector('.native-select');
|
|
const newSelectedValue = select.querySelector('.selected-value');
|
|
|
|
// Add new listeners
|
|
if (newTrigger) {
|
|
newTrigger.addEventListener('click', (e) => handleSelectTriggerClick(e, select));
|
|
}
|
|
|
|
newOptions.forEach(option => {
|
|
option.addEventListener('click', () => handleOptionClick(option, select));
|
|
});
|
|
|
|
// Initialize with current value
|
|
if (newNativeSelect?.value && newSelectedValue) {
|
|
initializeSelectedOption(select);
|
|
}
|
|
});
|
|
|
|
// Add document click handler
|
|
document.addEventListener('click', handleDocumentClickForSelects);
|
|
customSelectsInitialized = true;
|
|
}
|
|
|
|
function handleSelectTriggerClick(e, select) {
|
|
e.stopPropagation();
|
|
const trigger = select.querySelector('.select-trigger');
|
|
|
|
// Toggle current select
|
|
select.classList.toggle('open');
|
|
trigger.classList.toggle('active');
|
|
|
|
// Close other open selects
|
|
document.querySelectorAll('.custom-select').forEach(otherSelect => {
|
|
if (otherSelect !== select) {
|
|
otherSelect.classList.remove('open');
|
|
const otherTrigger = otherSelect.querySelector('.select-trigger');
|
|
if (otherTrigger) otherTrigger.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleOptionClick(option, select) {
|
|
const value = option.getAttribute('data-value');
|
|
const image = option.getAttribute('data-image');
|
|
const text = option.querySelector('span')?.textContent || '';
|
|
const nativeSelect = select.querySelector('.native-select');
|
|
const selectedValue = select.querySelector('.selected-value');
|
|
const options = select.querySelectorAll('.option');
|
|
|
|
// Update display
|
|
if (selectedValue) {
|
|
selectedValue.innerHTML = `
|
|
<img src="${image}" alt="${text}">
|
|
<span>${text}</span>
|
|
`;
|
|
}
|
|
|
|
// Update native select
|
|
if (nativeSelect) {
|
|
nativeSelect.value = value;
|
|
// Trigger change event
|
|
const event = new Event('change');
|
|
nativeSelect.dispatchEvent(event);
|
|
}
|
|
|
|
// Update selected state
|
|
options.forEach(opt => opt.classList.remove('selected'));
|
|
option.classList.add('selected');
|
|
|
|
// Close dropdown
|
|
select.classList.remove('open');
|
|
const trigger = select.querySelector('.select-trigger');
|
|
if (trigger) trigger.classList.remove('active');
|
|
}
|
|
|
|
function handleDocumentClickForSelects() {
|
|
document.querySelectorAll('.custom-select').forEach(select => {
|
|
select.classList.remove('open');
|
|
const trigger = select.querySelector('.select-trigger');
|
|
if (trigger) trigger.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
function initializeSelectedOption(select) {
|
|
const nativeSelect = select.querySelector('.native-select');
|
|
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
|
|
if (selectedOption) {
|
|
handleOptionClick(selectedOption, select);
|
|
}
|
|
}
|
|
|
|
function initFormSubmission() {
|
|
const form = document.getElementById('mainForm');
|
|
if (!form) return;
|
|
|
|
// Remove old listener if exists
|
|
if (formInitialized) {
|
|
form.removeEventListener('submit', handleFormSubmit);
|
|
}
|
|
|
|
form.addEventListener('submit', handleFormSubmit);
|
|
formInitialized = true;
|
|
}
|
|
|
|
async function handleFormSubmit(e) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
|
|
// Validate title
|
|
const titleInput = form.querySelector('[name="title"]');
|
|
if (titleInput && titleInput.value.length < 5) {
|
|
notify("error", "Customer Estimate Title must be at least 5 characters long.");
|
|
return;
|
|
}
|
|
|
|
// Prepare form data
|
|
const formData = {
|
|
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]')?.value,
|
|
title: form.querySelector('[name=title]')?.value,
|
|
customer: form.querySelector('[name=customer]')?.value,
|
|
item: [],
|
|
quantity: [1],
|
|
opportunity_id: "{{opportunity_id}}"
|
|
};
|
|
|
|
// Collect items
|
|
form.querySelectorAll('[name="item"]').forEach(input => {
|
|
if (input.value) formData.item.push(input.value);
|
|
});
|
|
|
|
try {
|
|
// Submit form
|
|
const response = await fetch("{% url 'estimate_create' request.dealer.slug %}", {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': formData.csrfmiddlewaretoken,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(formData)
|
|
});
|
|
|
|
// Handle response
|
|
const data = await response.json();
|
|
handleFormResponse(data);
|
|
} catch (error) {
|
|
console.error("Form submission error:", error);
|
|
notify("error", "An error occurred while submitting the form");
|
|
}
|
|
}
|
|
|
|
function handleFormResponse(data) {
|
|
if (!data) {
|
|
notify("error", "No response from server");
|
|
return;
|
|
}
|
|
|
|
if (data.status === "error") {
|
|
notify("error", data.message || "An error occurred");
|
|
} else if (data.status === "success") {
|
|
notify("success", data.message || "Estimate created successfully");
|
|
if (data.url) {
|
|
setTimeout(() => window.location.assign(data.url), 1000);
|
|
}
|
|
} else {
|
|
notify("error", "Unexpected response from the server");
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|