enhance and fix htmx with modal integration
This commit is contained in:
parent
4815ae4f6a
commit
6c2b6b1588
@ -208,7 +208,7 @@ class Command(BaseCommand):
|
|||||||
last_name = fake.last_name()
|
last_name = fake.last_name()
|
||||||
email = fake.email()
|
email = fake.email()
|
||||||
staff = random.choice(Staff.objects.filter(dealer=dealer))
|
staff = random.choice(Staff.objects.filter(dealer=dealer))
|
||||||
|
|
||||||
make = random.choice(CarMake.objects.all())
|
make = random.choice(CarMake.objects.all())
|
||||||
model = random.choice(make.carmodel_set.all())
|
model = random.choice(make.carmodel_set.all())
|
||||||
lead = Lead.objects.create(
|
lead = Lead.objects.create(
|
||||||
|
|||||||
@ -1620,7 +1620,7 @@ class CarUpdateView(
|
|||||||
permission_required = ["inventory.change_car"]
|
permission_required = ["inventory.change_car"]
|
||||||
|
|
||||||
def get_success_url(self):
|
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):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
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_content_type=ContentType.objects.get_for_model(models.Staff),
|
||||||
related_object_id=self.request.staff.pk,
|
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
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -4771,9 +4771,10 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
|
|||||||
kwargs["data"] = finance_data
|
kwargs["data"] = finance_data
|
||||||
kwargs["invoice"] = invoice_obj
|
kwargs["invoice"] = invoice_obj
|
||||||
try:
|
try:
|
||||||
cf = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances
|
||||||
selected_items = cf.additional_services.filter(dealer=dealer)
|
selected_items = car_finances.additional_services.filter(dealer=dealer)
|
||||||
form = forms.AdditionalFinancesForm()
|
form = forms.AdditionalFinancesForm()
|
||||||
|
form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer)
|
||||||
form.initial["additional_finances"] = selected_items
|
form.initial["additional_finances"] = selected_items
|
||||||
kwargs["additionals_form"] = form
|
kwargs["additionals_form"] = form
|
||||||
except Exception as e:
|
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)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
estimate = get_object_or_404(EstimateModel, pk=pk)
|
estimate = get_object_or_404(EstimateModel, pk=pk)
|
||||||
mark = request.GET.get("mark")
|
mark = request.GET.get("mark")
|
||||||
|
print(mark)
|
||||||
if mark:
|
if mark:
|
||||||
if mark == "review":
|
if mark == "review":
|
||||||
if not estimate.can_review():
|
if not estimate.can_review():
|
||||||
@ -6485,7 +6487,7 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
|
|
||||||
if not request.is_staff:
|
if not request.is_staff:
|
||||||
messages.error(request, _("You do not have permission to schedule."))
|
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":
|
if request.method == "POST":
|
||||||
form = forms.ScheduleForm(request.POST)
|
form = forms.ScheduleForm(request.POST)
|
||||||
|
|||||||
@ -82,7 +82,7 @@
|
|||||||
{% include "plans/expiration_messages.html" %}
|
{% include "plans/expiration_messages.html" %}
|
||||||
{% block period_navigation %}
|
{% block period_navigation %}
|
||||||
{% endblock period_navigation %}
|
{% endblock period_navigation %}
|
||||||
<div id="main_content" class="fade-me-in" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
<div id="main_content" class="fade-me-in" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="innerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||||
<div id="spinner" class="htmx-indicator spinner-bg">
|
<div id="spinner" class="htmx-indicator spinner-bg">
|
||||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||||
</div>
|
</div>
|
||||||
@ -93,8 +93,8 @@
|
|||||||
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
|
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
|
||||||
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
|
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
|
||||||
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
|
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
|
||||||
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script>
|
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
|
||||||
<script src="{% static 'js/phoenix.js' %}"></script> {% endcomment %}
|
<script src="{% static 'js/phoenix.js' %}"></script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% block body %}
|
{% block body %}
|
||||||
@ -186,7 +186,25 @@ document.addEventListener('htmx:afterRequest', function(evt) {
|
|||||||
notify("error", "Unexpected Error");
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
|
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -40,6 +40,5 @@
|
|||||||
document.querySelector('#id_note').value = note
|
document.querySelector('#id_note').value = note
|
||||||
let form = document.querySelector('.add_note_form')
|
let form = document.querySelector('.add_note_form')
|
||||||
form.action = url
|
form.action = url
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -16,6 +16,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
|
<form action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
|
||||||
|
hx-select=".taskTable"
|
||||||
|
hx-target=".taskTable"
|
||||||
|
hx-on::after-request="{
|
||||||
|
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
|
||||||
|
$('#scheduleModal').modal('hide');
|
||||||
|
}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
method="post"
|
method="post"
|
||||||
class="add_schedule_form">
|
class="add_schedule_form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|||||||
@ -23,15 +23,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form action="{% url 'add_task' request.dealer.slug content_type slug %}"
|
<form action="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||||
hx-boost="true"
|
method="post"
|
||||||
hx-select-oob=".taskTable:outerHTML,#toast-container:outerHTML"
|
class="add_task_form"
|
||||||
hx-swap="none"
|
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||||
hx-on::after-request="{
|
hx-target="#your-content-container"
|
||||||
resetSubmitButton(document.querySelector('.add_task_form button[type=submit]'));
|
hx-swap="innerHTML"
|
||||||
$('#taskModal').modal('hide');
|
hx-boost="false">
|
||||||
}"
|
|
||||||
method="post"
|
|
||||||
class="add_task_form">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ staff_task_form|crispy }}
|
{{ staff_task_form|crispy }}
|
||||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||||
|
|||||||
@ -971,17 +971,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close modal after successful form submission
|
// Close modal after successful form submission
|
||||||
document.body.addEventListener('htmx:afterSwap', function(evt) {
|
/*document.body.addEventListener('htmx:afterSwap', function(evt) {
|
||||||
if (evt.detail.target.id === 'main_content') {
|
if (evt.detail.target.id === 'main_content') {
|
||||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
document.querySelectorAll('.modal').forEach(function(modal) {
|
||||||
if (modal) {
|
var modal = bootstrap.Modal.getInstance();
|
||||||
modal.hide();
|
if (modal) {
|
||||||
}
|
modal.hide();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
// Cleanup modal backdrop if needed
|
// Cleanup modal backdrop if needed
|
||||||
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
/* document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
||||||
if (evt.detail.target.id === 'main_content') {
|
if (evt.detail.target.id === 'main_content') {
|
||||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||||
backdrops.forEach(function(backdrop) {
|
backdrops.forEach(function(backdrop) {
|
||||||
|
|||||||
@ -1091,6 +1091,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'modal/delete_modal.html' %}
|
||||||
<!-- email Modal -->
|
<!-- email Modal -->
|
||||||
{% include "components/email_modal.html" %}
|
{% include "components/email_modal.html" %}
|
||||||
<!-- task Modal -->
|
<!-- task Modal -->
|
||||||
|
|||||||
@ -611,12 +611,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const csrftoken = getCookie("csrftoken");
|
const csrftoken = getCookie("csrftoken");
|
||||||
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
|
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
|
||||||
|
|
||||||
|
|
||||||
const modalBody = customCardModal.querySelector(".modal-body");
|
const modalBody = customCardModal.querySelector(".modal-body");
|
||||||
|
|
||||||
const showSpecificationButton = document.getElementById("specification-btn");
|
const showSpecificationButton = document.getElementById("specification-btn");
|
||||||
|
|||||||
@ -22,8 +22,10 @@
|
|||||||
<div class="modal-body p-4">
|
<div class="modal-body p-4">
|
||||||
<p id="deleteModalText"></p>
|
<p id="deleteModalText"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer flex justify-content-center border-top-0">
|
<div class="modal-footer flex justify-content-center border-top-0" >
|
||||||
<a id="deleteModalConfirm"
|
<a id="deleteModalConfirm"
|
||||||
|
hx-select-oob="#notesTable:outerHTML,#toast-container:outerHTML"
|
||||||
|
hx-swap="none"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-phoenix-danger w-100"
|
class="btn btn-sm btn-phoenix-danger w-100"
|
||||||
href="">{{ _("Delete") }}</a>
|
href="">{{ _("Delete") }}</a>
|
||||||
@ -31,24 +33,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
const deleteModal = document.getElementById("deleteModal");
|
|
||||||
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
|
||||||
const deleteModalMessage = document.getElementById("deleteModalText");
|
|
||||||
|
|
||||||
document.querySelectorAll(".delete-btn").forEach(button => {
|
<script>
|
||||||
button.addEventListener("click", function () {
|
// Initialize when page loads and after HTMX swaps
|
||||||
let deleteUrl = this.getAttribute("data-url");
|
document.addEventListener('DOMContentLoaded', initDeleteModals);
|
||||||
let deleteMessage = this.getAttribute("data-message");
|
document.addEventListener('htmx:afterSwap', initDeleteModals);
|
||||||
|
|
||||||
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
function initDeleteModals() {
|
||||||
confirmDeleteBtn.setAttribute("hx-boost", "true");
|
const deleteModal = document.getElementById("deleteModal");
|
||||||
confirmDeleteBtn.setAttribute("hx-select-oob", "#notesTable:outerHTML,#toast-container:outerHTML");
|
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
||||||
confirmDeleteBtn.setAttribute("hx-swap", "none");
|
const deleteModalMessage = document.getElementById("deleteModalText");
|
||||||
confirmDeleteBtn.setAttribute("hx-on::after-request", "$('#deleteModal').modal('hide');");
|
|
||||||
deleteModalMessage.innerHTML = deleteMessage;
|
// Clean up previous listeners if any
|
||||||
});
|
document.querySelectorAll(".delete-btn").forEach(btn => {
|
||||||
});
|
btn.removeEventListener("click", handleDeleteClick);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add new listeners to all delete buttons
|
||||||
|
document.querySelectorAll(".delete-btn").forEach(button => {
|
||||||
|
button.addEventListener("click", handleDeleteClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDeleteClick() {
|
||||||
|
if (!deleteModal || !confirmDeleteBtn || !deleteModalMessage) return;
|
||||||
|
|
||||||
|
const deleteUrl = this.getAttribute("data-url");
|
||||||
|
const deleteMessage = this.getAttribute("data-message") || "Are you sure you want to delete this item?";
|
||||||
|
|
||||||
|
// Update modal content
|
||||||
|
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
||||||
|
deleteModalMessage.textContent = deleteMessage; // Use textContent instead of innerHTML for security
|
||||||
|
|
||||||
|
// Process with HTMX if available
|
||||||
|
if (typeof htmx !== 'undefined') {
|
||||||
|
htmx.process(confirmDeleteBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
/*if (typeof bootstrap !== 'undefined') {
|
||||||
|
const modal = new bootstrap.Modal(deleteModal);
|
||||||
|
modal.show();
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -334,39 +334,90 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
function calculateTotals() {
|
// Initialize when page loads and after HTMX swaps
|
||||||
const table = document.getElementById('estimate-table');
|
document.addEventListener('DOMContentLoaded', initEstimateFunctions);
|
||||||
const rows = table.getElementsByTagName('tbody')[0].rows;
|
document.addEventListener('htmx:afterSwap', initEstimateFunctions);
|
||||||
let grandTotal = 0;
|
|
||||||
|
|
||||||
for (let row of rows) {
|
function initEstimateFunctions() {
|
||||||
// Ensure the row has the expected number of cells
|
// Initialize calculateTotals if estimate table exists
|
||||||
if (row.cells.length >= 5) {
|
const estimateTable = document.getElementById('estimate-table');
|
||||||
const quantity = parseFloat(row.cells[2].textContent); // Quantity column
|
if (estimateTable) {
|
||||||
const unitPrice = parseFloat(row.cells[3].textContent); // Unit Price column
|
calculateTotals();
|
||||||
|
|
||||||
if (!isNaN(quantity) && !isNaN(unitPrice)) {
|
// Optional: If you need to recalculate when table content changes
|
||||||
const total = quantity * unitPrice;
|
estimateTable.addEventListener('change', calculateTotals);
|
||||||
row.cells[4].textContent = total.toFixed(2); // Populate Total column
|
}
|
||||||
grandTotal += total; // Add to grand total
|
|
||||||
}
|
// Initialize form action setter if form exists
|
||||||
|
const confirmForm = document.getElementById('confirmForm');
|
||||||
|
if (confirmForm) {
|
||||||
|
// Remove old event listeners if any
|
||||||
|
document.querySelectorAll('[data-set-form-action]').forEach(button => {
|
||||||
|
button.removeEventListener('click', setFormActionHandler);
|
||||||
|
button.addEventListener('click', setFormActionHandler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTotals() {
|
||||||
|
const table = document.getElementById('estimate-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tbody = table.getElementsByTagName('tbody')[0];
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
const rows = tbody.rows;
|
||||||
|
let grandTotal = 0;
|
||||||
|
|
||||||
|
for (let row of rows) {
|
||||||
|
// Skip rows that don't have enough cells
|
||||||
|
if (row.cells.length < 5) continue;
|
||||||
|
|
||||||
|
// Get quantity and unit price
|
||||||
|
const quantityText = row.cells[2]?.textContent?.trim() || '0';
|
||||||
|
const unitPriceText = row.cells[3]?.textContent?.trim() || '0';
|
||||||
|
|
||||||
|
// Parse values, handling any formatting
|
||||||
|
const quantity = parseFloat(quantityText.replace(/[^0-9.-]/g, ''));
|
||||||
|
const unitPrice = parseFloat(unitPriceText.replace(/[^0-9.-]/g, ''));
|
||||||
|
|
||||||
|
if (!isNaN(quantity) && !isNaN(unitPrice)) {
|
||||||
|
const total = quantity * unitPrice;
|
||||||
|
if (row.cells[4]) {
|
||||||
|
row.cells[4].textContent = total.toFixed(2);
|
||||||
|
}
|
||||||
|
grandTotal += total;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Display the grand total
|
// Update grand total display
|
||||||
document.getElementById('grand-total').textContent = grandTotal.toFixed(2);
|
const grandTotalElement = document.getElementById('grand-total');
|
||||||
|
if (grandTotalElement) {
|
||||||
|
grandTotalElement.textContent = grandTotal.toFixed(2);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calculating totals:', error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormActionHandler(event) {
|
||||||
// Run the function on page load
|
const action = event.currentTarget.getAttribute('data-set-form-action');
|
||||||
//window.onload = calculateTotals;
|
if (action) {
|
||||||
|
setFormAction(action);
|
||||||
function setFormAction(action) {
|
|
||||||
// Get the form element
|
|
||||||
const form = document.getElementById('confirmForm');
|
|
||||||
// Set the form action with the query parameter
|
|
||||||
form.action = "{% url 'estimate_mark_as' request.dealer.slug estimate.pk %}?mark=" + action;
|
|
||||||
}
|
}
|
||||||
</script>
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -154,14 +154,14 @@
|
|||||||
<div class="card shadow-sm border-0 rounded-3">
|
<div class="card shadow-sm border-0 rounded-3">
|
||||||
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
|
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
|
||||||
<h3 class="mb-0 fs-4 text-center">
|
<h3 class="mb-0 fs-4 text-center">
|
||||||
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
|
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body bg-light-subtle">
|
<div class="card-body bg-light-subtle">
|
||||||
|
|
||||||
|
|
||||||
<form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
|
<form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="row g-3 col-12">
|
<div class="row g-3 col-12">
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
@ -202,165 +202,246 @@
|
|||||||
<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>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block customJS %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// Global variables
|
||||||
const Toast = Swal.mixin({
|
let Toast;
|
||||||
toast: true,
|
let customSelectsInitialized = false;
|
||||||
position: "top-end",
|
let formInitialized = false;
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 2000,
|
|
||||||
timerProgressBar: false,
|
|
||||||
didOpen: (toast) => {
|
|
||||||
toast.onmouseenter = Swal.stopTimer;
|
|
||||||
toast.onmouseleave = Swal.resumeTimer;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
|
|
||||||
|
|
||||||
const customSelects = document.querySelectorAll('.custom-select');
|
// Initialize when page loads and after HTMX swaps
|
||||||
|
document.addEventListener('DOMContentLoaded', initPage);
|
||||||
|
document.addEventListener('htmx:afterSwap', initPage);
|
||||||
|
|
||||||
customSelects.forEach(select => {
|
function initPage() {
|
||||||
const trigger = select.querySelector('.select-trigger');
|
initToast();
|
||||||
const optionsContainer = select.querySelector('.options-container');
|
initCustomSelects();
|
||||||
const options = select.querySelectorAll('.option');
|
initFormSubmission();
|
||||||
const nativeSelect = select.querySelector('.native-select');
|
}
|
||||||
const selectedValue = select.querySelector('.selected-value');
|
|
||||||
|
|
||||||
// Toggle dropdown
|
|
||||||
trigger.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
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');
|
|
||||||
otherSelect.querySelector('.select-trigger').classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle option selection
|
|
||||||
options.forEach(option => {
|
|
||||||
option.addEventListener('click', () => {
|
|
||||||
const value = option.getAttribute('data-value');
|
|
||||||
const image = option.getAttribute('data-image');
|
|
||||||
const text = option.querySelector('span').textContent;
|
|
||||||
|
|
||||||
// Update selected display
|
|
||||||
selectedValue.innerHTML = `
|
|
||||||
<img src="${image}" alt="${text}">
|
|
||||||
<span>${text}</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Update native select value
|
|
||||||
nativeSelect.value = value;
|
|
||||||
|
|
||||||
// Mark as selected
|
|
||||||
options.forEach(opt => opt.classList.remove('selected'));
|
|
||||||
option.classList.add('selected');
|
|
||||||
|
|
||||||
// Close dropdown
|
|
||||||
select.classList.remove('open');
|
|
||||||
trigger.classList.remove('active');
|
|
||||||
|
|
||||||
// Trigger change event
|
|
||||||
const event = new Event('change');
|
|
||||||
nativeSelect.dispatchEvent(event);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close when clicking outside
|
|
||||||
document.addEventListener('click', () => {
|
|
||||||
select.classList.remove('open');
|
|
||||||
trigger.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize with native select value
|
|
||||||
if (nativeSelect.value) {
|
|
||||||
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
|
|
||||||
if (selectedOption) {
|
|
||||||
selectedOption.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Form submission
|
|
||||||
/*const form = document.getElementById('demo-form');
|
|
||||||
form.addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const formData = new FormData(form);
|
|
||||||
alert(`Selected value: ${formData.get('car')}`);
|
|
||||||
});*/
|
|
||||||
|
|
||||||
document.getElementById('mainForm').addEventListener('submit', async function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const titleInput = document.querySelector('[name="title"]');
|
|
||||||
if (titleInput.value.length < 5) {
|
|
||||||
notify("error", "Customer Estimate Title must be at least 5 characters long.");
|
|
||||||
return; // Stop form submission
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all form data
|
|
||||||
const formData = {
|
|
||||||
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
||||||
title: document.querySelector('[name=title]').value,
|
|
||||||
customer: document.querySelector('[name=customer]').value,
|
|
||||||
item: [],
|
|
||||||
quantity: [1],
|
|
||||||
opportunity_id: "{{opportunity_id}}"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Collect multi-value fields (e.g., item[], quantity[])
|
|
||||||
document.querySelectorAll('[name="item"]').forEach(input => {
|
|
||||||
formData.item.push(input.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(formData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send data to the server using fetch
|
|
||||||
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)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse the JSON response
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Handle the response
|
|
||||||
if (data.status === "error") {
|
|
||||||
notify("error", data.message); // Display an error message
|
|
||||||
} else if (data.status === "success") {
|
|
||||||
notify("success","Estimate created successfully");
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.assign(data.url); // Redirect to the provided URL
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
notify("error","Unexpected response from the server");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
notify("error", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
}
|
||||||
{% 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 = `
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
|||||||
@ -30,31 +30,31 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="list">
|
<tbody class="list">
|
||||||
{% for extra in staff_estimates %}
|
{% for estimate in staff_estimates %}
|
||||||
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
|
||||||
<td class="align-middle product white-space-nowrap py-0 px-1">{{ extra.content_object.estimate_number }}</td>
|
<td class="align-middle product white-space-nowrap py-0 px-1">{{ estimate.estimate_number }}</td>
|
||||||
<td class="align-middle product white-space-nowrap">{{ extra.content_object.customer.customer_name }}</td>
|
<td class="align-middle product white-space-nowrap">{{ estimate.customer.customer_name }}</td>
|
||||||
<td class="align-middle product white-space-nowrap">
|
<td class="align-middle product white-space-nowrap">
|
||||||
{% if extra.content_object.status == 'draft' %}
|
{% if estimate.status == 'draft' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-warning">{% trans "Draft" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-warning">{% trans "Draft" %}</span>
|
||||||
{% elif extra.content_object.status == 'in_review' %}
|
{% elif estimate.status == 'in_review' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-info">{% trans "In Review" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-info">{% trans "In Review" %}</span>
|
||||||
{% elif extra.content_object.status == 'approved' %}
|
{% elif estimate.status == 'approved' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Approved" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Approved" %}</span>
|
||||||
{% elif extra.content_object.status == 'declined' %}
|
{% elif estimate.status == 'declined' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Declined" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Declined" %}</span>
|
||||||
{% elif extra.content_object.status == 'canceled' %}
|
{% elif estimate.status == 'canceled' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Canceled" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Canceled" %}</span>
|
||||||
{% elif extra.content_object.status == 'completed' %}
|
{% elif estimate.status == 'completed' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Completed" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Completed" %}</span>
|
||||||
{% elif extra.content_object.status == 'void' %}
|
{% elif estimate.status == 'void' %}
|
||||||
<span class="badge badge-phoenix badge-phoenix-secondary">{% trans "Void" %}</span>
|
<span class="badge badge-phoenix badge-phoenix-secondary">{% trans "Void" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle product white-space-nowrap">{{ extra.content_object.get_status_action_date }}</td>
|
<td class="align-middle product white-space-nowrap">{{ estimate.get_status_action_date }}</td>
|
||||||
<td class="align-middle product white-space-nowrap">{{ extra.content_object.created }}</td>
|
<td class="align-middle product white-space-nowrap">{{ estimate.created }}</td>
|
||||||
<td class="align-middle product white-space-nowrap">
|
<td class="align-middle product white-space-nowrap">
|
||||||
<a href="{% url 'estimate_detail' request.dealer.slug extra.content_object.pk %}"
|
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
|
||||||
class="btn btn-sm btn-phoenix-success">
|
class="btn btn-sm btn-phoenix-success">
|
||||||
<i class="fa-regular fa-eye me-1"></i>
|
<i class="fa-regular fa-eye me-1"></i>
|
||||||
{% trans "view"|capfirst %}
|
{% trans "view"|capfirst %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user