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()
|
||||
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(
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
{% include "plans/expiration_messages.html" %}
|
||||
{% block 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">
|
||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||
</div>
|
||||
@ -93,8 +93,8 @@
|
||||
{% comment %} <script src="{% static 'vendors/feather-icons/feather.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/bootstrap/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'js/phoenix.js' %}"></script> {% endcomment %}
|
||||
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
|
||||
<script src="{% static 'js/phoenix.js' %}"></script>
|
||||
|
||||
</div>
|
||||
{% 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
|
||||
</body>
|
||||
|
||||
@ -40,6 +40,5 @@
|
||||
document.querySelector('#id_note').value = note
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = url
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -16,6 +16,13 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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"
|
||||
class="add_schedule_form">
|
||||
{% csrf_token %}
|
||||
|
||||
@ -23,15 +23,12 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
hx-boost="true"
|
||||
hx-select-oob=".taskTable:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="{
|
||||
resetSubmitButton(document.querySelector('.add_task_form button[type=submit]'));
|
||||
$('#taskModal').modal('hide');
|
||||
}"
|
||||
method="post"
|
||||
class="add_task_form">
|
||||
method="post"
|
||||
class="add_task_form"
|
||||
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
|
||||
hx-target="#your-content-container"
|
||||
hx-swap="innerHTML"
|
||||
hx-boost="false">
|
||||
{% csrf_token %}
|
||||
{{ staff_task_form|crispy }}
|
||||
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
|
||||
|
||||
@ -971,17 +971,20 @@
|
||||
}
|
||||
|
||||
// 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') {
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('.modal').forEach(function(modal) {
|
||||
var modal = bootstrap.Modal.getInstance();
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// 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') {
|
||||
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||
backdrops.forEach(function(backdrop) {
|
||||
|
||||
@ -1091,6 +1091,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
<!-- email Modal -->
|
||||
{% include "components/email_modal.html" %}
|
||||
<!-- task Modal -->
|
||||
|
||||
@ -611,12 +611,14 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const csrftoken = getCookie("csrftoken");
|
||||
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
|
||||
|
||||
|
||||
const modalBody = customCardModal.querySelector(".modal-body");
|
||||
|
||||
const showSpecificationButton = document.getElementById("specification-btn");
|
||||
|
||||
@ -22,8 +22,10 @@
|
||||
<div class="modal-body p-4">
|
||||
<p id="deleteModalText"></p>
|
||||
</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"
|
||||
hx-select-oob="#notesTable:outerHTML,#toast-container:outerHTML"
|
||||
hx-swap="none"
|
||||
type="button"
|
||||
class="btn btn-sm btn-phoenix-danger w-100"
|
||||
href="">{{ _("Delete") }}</a>
|
||||
@ -31,24 +33,47 @@
|
||||
</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 => {
|
||||
button.addEventListener("click", function () {
|
||||
let deleteUrl = this.getAttribute("data-url");
|
||||
let deleteMessage = this.getAttribute("data-message");
|
||||
<script>
|
||||
// Initialize when page loads and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', initDeleteModals);
|
||||
document.addEventListener('htmx:afterSwap', initDeleteModals);
|
||||
|
||||
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
||||
confirmDeleteBtn.setAttribute("hx-boost", "true");
|
||||
confirmDeleteBtn.setAttribute("hx-select-oob", "#notesTable:outerHTML,#toast-container:outerHTML");
|
||||
confirmDeleteBtn.setAttribute("hx-swap", "none");
|
||||
confirmDeleteBtn.setAttribute("hx-on::after-request", "$('#deleteModal').modal('hide');");
|
||||
deleteModalMessage.innerHTML = deleteMessage;
|
||||
});
|
||||
});
|
||||
function initDeleteModals() {
|
||||
const deleteModal = document.getElementById("deleteModal");
|
||||
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
||||
const deleteModalMessage = document.getElementById("deleteModalText");
|
||||
|
||||
// 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>
|
||||
|
||||
@ -334,39 +334,90 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
function calculateTotals() {
|
||||
const table = document.getElementById('estimate-table');
|
||||
const rows = table.getElementsByTagName('tbody')[0].rows;
|
||||
let grandTotal = 0;
|
||||
<script>
|
||||
// Initialize when page loads and after HTMX swaps
|
||||
document.addEventListener('DOMContentLoaded', initEstimateFunctions);
|
||||
document.addEventListener('htmx:afterSwap', initEstimateFunctions);
|
||||
|
||||
for (let row of rows) {
|
||||
// Ensure the row has the expected number of cells
|
||||
if (row.cells.length >= 5) {
|
||||
const quantity = parseFloat(row.cells[2].textContent); // Quantity column
|
||||
const unitPrice = parseFloat(row.cells[3].textContent); // Unit Price column
|
||||
function initEstimateFunctions() {
|
||||
// Initialize calculateTotals if estimate table exists
|
||||
const estimateTable = document.getElementById('estimate-table');
|
||||
if (estimateTable) {
|
||||
calculateTotals();
|
||||
|
||||
if (!isNaN(quantity) && !isNaN(unitPrice)) {
|
||||
const total = quantity * unitPrice;
|
||||
row.cells[4].textContent = total.toFixed(2); // Populate Total column
|
||||
grandTotal += total; // Add to grand total
|
||||
}
|
||||
// Optional: If you need to recalculate when table content changes
|
||||
estimateTable.addEventListener('change', calculateTotals);
|
||||
}
|
||||
|
||||
// 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
|
||||
document.getElementById('grand-total').textContent = grandTotal.toFixed(2);
|
||||
// Update grand total display
|
||||
const grandTotalElement = document.getElementById('grand-total');
|
||||
if (grandTotalElement) {
|
||||
grandTotalElement.textContent = grandTotal.toFixed(2);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error calculating totals:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Run the function on page load
|
||||
//window.onload = calculateTotals;
|
||||
|
||||
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;
|
||||
function setFormActionHandler(event) {
|
||||
const action = event.currentTarget.getAttribute('data-set-form-action');
|
||||
if (action) {
|
||||
setFormAction(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 %}
|
||||
|
||||
@ -154,14 +154,14 @@
|
||||
<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>
|
||||
{% 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 or not customer_count %}disabled{% endif %}">
|
||||
|
||||
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 col-12">
|
||||
{{ 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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const 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){Toast.fire({icon: tag,titleText: msg});}
|
||||
// Global variables
|
||||
let Toast;
|
||||
let customSelectsInitialized = false;
|
||||
let formInitialized = false;
|
||||
|
||||
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 => {
|
||||
const trigger = select.querySelector('.select-trigger');
|
||||
const optionsContainer = select.querySelector('.options-container');
|
||||
const options = select.querySelectorAll('.option');
|
||||
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 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;
|
||||
}
|
||||
});
|
||||
</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>
|
||||
</thead>
|
||||
<tbody class="list">
|
||||
{% for extra in staff_estimates %}
|
||||
{% for estimate in staff_estimates %}
|
||||
<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">{{ extra.content_object.customer.customer_name }}</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">{{ estimate.customer.customer_name }}</td>
|
||||
<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>
|
||||
{% elif extra.content_object.status == 'in_review' %}
|
||||
{% elif estimate.status == 'in_review' %}
|
||||
<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>
|
||||
{% elif extra.content_object.status == 'declined' %}
|
||||
{% elif estimate.status == 'declined' %}
|
||||
<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>
|
||||
{% elif extra.content_object.status == 'completed' %}
|
||||
{% elif estimate.status == 'completed' %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</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">{{ extra.content_object.created }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.get_status_action_date }}</td>
|
||||
<td class="align-middle product white-space-nowrap">{{ estimate.created }}</td>
|
||||
<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">
|
||||
<i class="fa-regular fa-eye me-1"></i>
|
||||
{% trans "view"|capfirst %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user