haikal/templates/sales/estimates/estimate_form.html
2025-07-29 13:29:56 +03:00

436 lines
14 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 justify-content-center mt-5 mb-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="Please add at least one car before creating a quotation." value2="Add car" message_image="images/empty/no_car.png" 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="Please add at least one customer before creating a quotation." value2="Add Customer" message_image="images/empty/no_estimate.png" url=create_customer_url %}
{% endif %}
</div>
<div>
<div class="col-lg-8 col-md-10 needs-validation {% if not items or not customer_count %}d-none{% endif %}">
<div class="card shadow-sm border-0 rounded-3">
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
<h3 class="mb-0 fs-4 text-center">
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
</h3>
</div>
<div class="card-body bg-light-subtle">
<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 col-12">
{{ 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.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>
{% 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 %}