From 6c2b6b15880d46cbc3734667f0bf362c159dc955 Mon Sep 17 00:00:00 2001 From: ismail Date: Thu, 24 Jul 2025 17:38:32 +0300 Subject: [PATCH] enhance and fix htmx with modal integration --- inventory/management/commands/seed1.py | 2 +- inventory/views.py | 12 +- templates/base.html | 26 +- templates/components/note_modal.html | 1 - templates/components/schedule_modal.html | 7 + templates/components/task_modal.html | 15 +- templates/crm/leads/lead_detail.html | 17 +- .../crm/opportunities/opportunity_detail.html | 1 + templates/inventory/car_detail.html | 4 +- templates/modal/delete_modal.html | 61 ++- .../sales/estimates/estimate_detail.html | 107 +++-- templates/sales/estimates/estimate_form.html | 387 +++++++++++------- templates/sales/estimates/estimate_list.html | 26 +- 13 files changed, 426 insertions(+), 240 deletions(-) diff --git a/inventory/management/commands/seed1.py b/inventory/management/commands/seed1.py index d40198f1..ee247748 100644 --- a/inventory/management/commands/seed1.py +++ b/inventory/management/commands/seed1.py @@ -208,7 +208,7 @@ class Command(BaseCommand): last_name = fake.last_name() email = fake.email() staff = random.choice(Staff.objects.filter(dealer=dealer)) - + make = random.choice(CarMake.objects.all()) model = random.choice(make.carmodel_set.all()) lead = Lead.objects.create( diff --git a/inventory/views.py b/inventory/views.py index 39fa981b..b619acf7 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -1620,7 +1620,7 @@ class CarUpdateView( permission_required = ["inventory.change_car"] def get_success_url(self): - return reverse("car_detail", kwargs={"slug": self.object.slug}) + return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug}) def get_form(self, form_class=None): form = super().get_form(form_class) @@ -4446,7 +4446,7 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): related_content_type=ContentType.objects.get_for_model(models.Staff), related_object_id=self.request.staff.pk, ) - context["staff_estimates"] = qs + context["staff_estimates"] = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs]) return context def get_queryset(self): @@ -4771,9 +4771,10 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView kwargs["data"] = finance_data kwargs["invoice"] = invoice_obj try: - cf = estimate.get_itemtxs_data()[0].first().item_model.car.finances - selected_items = cf.additional_services.filter(dealer=dealer) + car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances + selected_items = car_finances.additional_services.filter(dealer=dealer) form = forms.AdditionalFinancesForm() + form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer) form.initial["additional_finances"] = selected_items kwargs["additionals_form"] = form except Exception as e: @@ -5044,6 +5045,7 @@ def estimate_mark_as(request, dealer_slug, pk): dealer = get_object_or_404(models.Dealer, slug=dealer_slug) estimate = get_object_or_404(EstimateModel, pk=pk) mark = request.GET.get("mark") + print(mark) if mark: if mark == "review": if not estimate.can_review(): @@ -6485,7 +6487,7 @@ def schedule_event(request, dealer_slug, content_type, slug): if not request.is_staff: messages.error(request, _("You do not have permission to schedule.")) - return redirect(request.META.get("HTTP_REFERER")) + return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug) if request.method == "POST": form = forms.ScheduleForm(request.POST) diff --git a/templates/base.html b/templates/base.html index dca51f9f..6da18490 100644 --- a/templates/base.html +++ b/templates/base.html @@ -82,7 +82,7 @@ {% include "plans/expiration_messages.html" %} {% block period_navigation %} {% endblock period_navigation %} -
+
@@ -93,8 +93,8 @@ {% comment %} - - {% endcomment %} + {% endcomment %} +
{% block body %} @@ -186,7 +186,25 @@ document.addEventListener('htmx:afterRequest', function(evt) { notify("error", "Unexpected Error"); } }); - +document.body.addEventListener('htmx:beforeSwap', function(evt) { + if (evt.detail.target.id === 'main_content') { + var backdrops = document.querySelectorAll('.modal-backdrop'); + backdrops.forEach(function(backdrop) { + backdrop.remove(); + }); + } + }); + // Close modal after successful form submission + document.body.addEventListener('htmx:afterSwap', function(evt) { + if (evt.detail.target.id === 'main_content') { + document.querySelectorAll('.modal').forEach(function(m) { + var modal = bootstrap.Modal.getInstance(m); + if (modal) { + modal.hide(); + } + }); + } + }); {% comment %} {% block customJS %}{% endblock %} {% endcomment %} diff --git a/templates/components/note_modal.html b/templates/components/note_modal.html index e3f5f452..25e3002f 100644 --- a/templates/components/note_modal.html +++ b/templates/components/note_modal.html @@ -40,6 +40,5 @@ document.querySelector('#id_note').value = note let form = document.querySelector('.add_note_form') form.action = url - } diff --git a/templates/components/schedule_modal.html b/templates/components/schedule_modal.html index 511c51b6..99656bda 100644 --- a/templates/components/schedule_modal.html +++ b/templates/components/schedule_modal.html @@ -16,6 +16,13 @@
+{% include 'modal/delete_modal.html' %} {% include "components/email_modal.html" %} diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index cde3f0c0..d4e16fe1 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -611,12 +611,14 @@ {% endif %} + {% endblock %} + + {% block customJS %} diff --git a/templates/sales/estimates/estimate_detail.html b/templates/sales/estimates/estimate_detail.html index 593bb1ab..0558c309 100644 --- a/templates/sales/estimates/estimate_detail.html +++ b/templates/sales/estimates/estimate_detail.html @@ -334,39 +334,90 @@ {% endblock %} {% block customJS %} - +} + +function setFormAction(action) { + const form = document.getElementById('confirmForm'); + if (!form) return; + + const baseUrl = "{% url 'estimate_mark_as' request.dealer.slug estimate.pk %}"; + form.action = `${baseUrl}?mark=${encodeURIComponent(action)}`; + + // Optional: Submit form immediately after setting action + // form.submit(); +} + {% endblock %} diff --git a/templates/sales/estimates/estimate_form.html b/templates/sales/estimates/estimate_form.html index 0aa98273..d9cb4ef3 100644 --- a/templates/sales/estimates/estimate_form.html +++ b/templates/sales/estimates/estimate_form.html @@ -154,14 +154,14 @@

- {% trans "Create Quotation" %} + {% trans "Create Quotation" %}

- + - + {% csrf_token %}
{{ form|crispy }} @@ -202,165 +202,246 @@ {% trans "Cancel" %}
- +
- +{% endblock %} +{% block customJS %} - {% endblock %} + } +} + +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 = ` + ${text} + ${text} + `; + } + + // 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"); + } +} + + {% endblock %} diff --git a/templates/sales/estimates/estimate_list.html b/templates/sales/estimates/estimate_list.html index 0629019e..0438bed9 100644 --- a/templates/sales/estimates/estimate_list.html +++ b/templates/sales/estimates/estimate_list.html @@ -30,31 +30,31 @@ - {% for extra in staff_estimates %} + {% for estimate in staff_estimates %} - {{ extra.content_object.estimate_number }} - {{ extra.content_object.customer.customer_name }} + {{ estimate.estimate_number }} + {{ estimate.customer.customer_name }} - {% if extra.content_object.status == 'draft' %} + {% if estimate.status == 'draft' %} {% trans "Draft" %} - {% elif extra.content_object.status == 'in_review' %} + {% elif estimate.status == 'in_review' %} {% trans "In Review" %} - {% elif extra.content_object.status == 'approved' %} + {% elif estimate.status == 'approved' %} {% trans "Approved" %} - {% elif extra.content_object.status == 'declined' %} + {% elif estimate.status == 'declined' %} {% trans "Declined" %} - {% elif extra.content_object.status == 'canceled' %} + {% elif estimate.status == 'canceled' %} {% trans "Canceled" %} - {% elif extra.content_object.status == 'completed' %} + {% elif estimate.status == 'completed' %} {% trans "Completed" %} - {% elif extra.content_object.status == 'void' %} + {% elif estimate.status == 'void' %} {% trans "Void" %} {% endif %} - {{ extra.content_object.get_status_action_date }} - {{ extra.content_object.created }} + {{ estimate.get_status_action_date }} + {{ estimate.created }} - {% trans "view"|capfirst %}