rebase with marwan update

This commit is contained in:
ismail 2025-07-23 17:29:32 +03:00
parent 7a1b15bb97
commit fa111b159f
16 changed files with 224 additions and 149 deletions

View File

@ -660,7 +660,7 @@ class Car(Base):
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks")) remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage")) mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date")) receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
sold_date=models.DateTimeField(verbose_name=_("Sold Date")) sold_date=models.DateTimeField(verbose_name=_("Sold Date"),null=True,blank=True)
hash = models.CharField( hash = models.CharField(
max_length=64, blank=True, null=True, verbose_name=_("Hash") max_length=64, blank=True, null=True, verbose_name=_("Hash")
) )
@ -3361,9 +3361,9 @@ class ExtraInfo(models.Model):
) )
# qs = qs.select_related("customer","estimate","invoice") # qs = qs.select_related("customer","estimate","invoice")
data = SaleOrder.objects.filter(pk__in=[x.content_object.sale_orders.select_related("customer","estimate","invoice").first().pk for x in qs if x.content_object.sale_orders.first()]) data = SaleOrder.objects.filter(pk__in=[x.content_object.sale_orders.select_related("customer","estimate","invoice").first().pk for x in qs if x.content_object.sale_orders.first()])
return data return data
# return [ # return [
# x.content_object.sale_orders.select_related( # x.content_object.sale_orders.select_related(
# "customer", "estimate", "invoice" # "customer", "estimate", "invoice"

View File

@ -1251,7 +1251,7 @@ urlpatterns = [
name="po-action-mark-as-void", name="po-action-mark-as-void",
), ),
# reports # reports
path( path(
"<slug:dealer_slug>/purchase-report/", "<slug:dealer_slug>/purchase-report/",
views.purchase_report_view, views.purchase_report_view,

View File

@ -1494,7 +1494,7 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
form_class = forms.CarFinanceForm form_class = forms.CarFinanceForm
template_name = "inventory/car_finance_form.html" template_name = "inventory/car_finance_form.html"
permission_required = ["inventory.add_carfinance"] permission_required = ["inventory.add_carfinance"]
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"]) self.car = get_object_or_404(models.Car, slug=self.kwargs["slug"])
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -4320,8 +4320,8 @@ def sales_list_view(request, dealer_slug):
item transactions specific to the user's entity. item transactions specific to the user's entity.
:rtype: HttpResponse :rtype: HttpResponse
""" """
dealer = get_object_or_404(models.Dealer, slug=dealer_slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
staff = getattr(request.user.staffmember, "staff", None) staff = getattr(request.user.staffmember, "staff", None)
qs = [] qs = []
try: try:
@ -4459,9 +4459,9 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
if search_query: if search_query:
print("inside") print("inside")
queryset = queryset.filter( queryset = queryset.filter(
Q(estimate_number__icontains=search_query) Q(estimate_number__icontains=search_query)
).distinct() ).distinct()
return queryset return queryset
@ -6104,6 +6104,7 @@ def update_lead_actions(request, dealer_slug):
current_action = request.POST.get("current_action") current_action = request.POST.get("current_action")
next_action = request.POST.get("next_action") next_action = request.POST.get("next_action")
next_action_date = request.POST.get("next_action_date", None) next_action_date = request.POST.get("next_action_date", None)
lead = models.Lead.objects.get(id=lead_id)
if not all([lead_id, current_action, next_action]): if not all([lead_id, current_action, next_action]):
# Log for missing required fields # Log for missing required fields
@ -6111,12 +6112,17 @@ def update_lead_actions(request, dealer_slug):
f"User {user_username} submitted incomplete data to update lead actions " f"User {user_username} submitted incomplete data to update lead actions "
f"for dealer '{dealer_slug}'. Missing fields: lead_id='{lead_id}', current_action='{current_action}', next_action='{next_action}'." f"for dealer '{dealer_slug}'. Missing fields: lead_id='{lead_id}', current_action='{current_action}', next_action='{next_action}'."
) )
return JsonResponse( messages.error(
{"success": False, "message": "All fields are required"}, status=400 request,
_("All fields are required")
) )
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
# return JsonResponse(
# {"success": False, "message": "All fields are required"}, status=400
# )
# Get the lead # Get the lead
lead = models.Lead.objects.get(id=lead_id)
# Update lead fields # Update lead fields
@ -6146,9 +6152,14 @@ def update_lead_actions(request, dealer_slug):
f"submitted invalid date format ('{next_action_date}') " f"submitted invalid date format ('{next_action_date}') "
f"for Lead ID: {lead.pk}. Error: {ve}" f"for Lead ID: {lead.pk}. Error: {ve}"
) )
return JsonResponse( messages.error(
{"success": False, "message": "Invalid date format"}, status=400 request,
_("Invalid date format")
) )
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
# return JsonResponse(
# {"success": False, "message": "Invalid date format"}, status=400
# )
# Save the lead # Save the lead
lead.save() lead.save()
# --- Logging for successful update (main try block success) --- # --- Logging for successful update (main try block success) ---
@ -6156,9 +6167,14 @@ def update_lead_actions(request, dealer_slug):
f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). " f"User {user_username} successfully updated Lead ID: {lead.pk} ('{lead.slug}'). "
f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'." f"New Status: '{lead.status}', Next Action: '{lead.next_action}', Next Action Date: '{lead.next_action_date}'."
) )
return JsonResponse( messages.success(
{"success": True, "message": "Actions updated successfully"} request,
_("Actions updated successfully")
) )
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
# return JsonResponse(
# {"success": True, "message": "Actions updated successfully"}
# )
except models.Lead.DoesNotExist: except models.Lead.DoesNotExist:
# --- Logging for Lead not found --- # --- Logging for Lead not found ---
@ -6166,7 +6182,12 @@ def update_lead_actions(request, dealer_slug):
f"User {user_username} attempted to update non-existent Lead with ID: '{lead_id}' " f"User {user_username} attempted to update non-existent Lead with ID: '{lead_id}' "
f"for dealer '{dealer_slug}'. Returning 404." f"for dealer '{dealer_slug}'. Returning 404."
) )
return JsonResponse({"success": False, "message": "Lead not found"}, status=404) messages.error(
request,
_("Lead not found")
)
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
# return JsonResponse({"success": False, "message": "Lead not found"}, status=404)
except Exception as e: except Exception as e:
involved_lead_id = request.POST.get("lead_id", "N/A") involved_lead_id = request.POST.get("lead_id", "N/A")
logger.error( logger.error(
@ -6174,7 +6195,12 @@ def update_lead_actions(request, dealer_slug):
f"for dealer '{dealer_slug}'. Error: {e}", f"for dealer '{dealer_slug}'. Error: {e}",
exc_info=True, # CRUCIAL: Includes the full traceback exc_info=True, # CRUCIAL: Includes the full traceback
) )
return JsonResponse({"success": False, "message": str(e)}, status=500) messages.error(
request,
_("An error occurred while updating lead actions")
)
return redirect("lead_detail", dealer_slug=dealer_slug, slug=lead.slug)
# return JsonResponse({"success": False, "message": str(e)}, status=500)
class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
@ -6558,7 +6584,7 @@ def lead_transfer(request, dealer_slug, slug):
messages.success(request, _("Lead transferred successfully")) messages.success(request, _("Lead transferred successfully"))
else: else:
messages.error(request, f"Invalid form data: {str(form.errors)}") messages.error(request, f"Invalid form data: {str(form.errors)}")
return redirect("lead_list", dealer_slug=dealer.slug) return redirect("lead_detail", dealer_slug=dealer.slug ,slug=lead.slug)
@login_required @login_required
@ -10399,7 +10425,7 @@ def upload_cars(request, dealer_slug, pk=None):
form = forms.CSVUploadForm() form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.all() form.fields["vendor"].queryset = dealer.vendors.all()
print(request)
return render( return render(
request, request,
"csv_upload.html", "csv_upload.html",
@ -10451,16 +10477,16 @@ def purchase_report_view(request,dealer_slug):
pos = request.entity.get_purchase_orders() pos = request.entity.get_purchase_orders()
data = [] data = []
total_po_amount=0 total_po_amount=0
total_po_cars=0 total_po_cars=0
for po in pos: for po in pos:
items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()] items = [{"total":x.total_amount,"q":x.quantity} for x in po.get_itemtxs_data()[0].all()]
po_amount=0 po_amount=0
po_quantity=0 po_quantity=0
for item in items: for item in items:
po_amount+=item["total"] po_amount+=item["total"]
po_quantity+=item["q"] po_quantity+=item["q"]
total_po_amount+=po_amount total_po_amount+=po_amount
total_po_cars+=po_quantity total_po_cars+=po_quantity
bills=po.get_po_bill_queryset() bills=po.get_po_bill_queryset()
@ -10468,7 +10494,7 @@ def purchase_report_view(request,dealer_slug):
vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A"
data.append({"po_number":po.po_number,"po_created":po.created,"po_status":po.po_status,"po_fulfilled_date":po.date_fulfilled,"po_amount":po_amount, data.append({"po_number":po.po_number,"po_created":po.created,"po_status":po.po_status,"po_fulfilled_date":po.date_fulfilled,"po_amount":po_amount,
"po_quantity":po_quantity,"vendors_str":vendors_str}) "po_quantity":po_quantity,"vendors_str":vendors_str})
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
context={ context={
"dealer":request.entity.name, "dealer":request.entity.name,
@ -10479,23 +10505,23 @@ def purchase_report_view(request,dealer_slug):
"current_time":current_time "current_time":current_time
} }
return render(request,'ledger/reports/purchase_report.html',context) return render(request,'ledger/reports/purchase_report.html',context)
def purchase_report_csv_export(request,dealer_slug): def purchase_report_csv_export(request,dealer_slug):
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S") current_time = timezone.now().strftime("%Y-%m-%d_%H%M%S")
filename = f"purchase_report_{dealer_slug}_{current_time}.csv" filename = f"purchase_report_{dealer_slug}_{current_time}.csv"
response['Content-Disposition'] = f'attachment; filename="{filename}"' response['Content-Disposition'] = f'attachment; filename="{filename}"'
writer = csv.writer(response) writer = csv.writer(response)
header = [ header = [
'PO Number', 'PO Number',
'Created Date', 'Created Date',
@ -10507,7 +10533,7 @@ def purchase_report_csv_export(request,dealer_slug):
] ]
writer.writerow(header) writer.writerow(header)
pos = request.entity.get_purchase_orders() pos = request.entity.get_purchase_orders()
for po in pos: for po in pos:
po_amount = 0 po_amount = 0
po_quantity = 0 po_quantity = 0
@ -10516,10 +10542,10 @@ def purchase_report_csv_export(request,dealer_slug):
for item in items: for item in items:
po_amount += item["total"] po_amount += item["total"]
po_quantity += item["q"] po_quantity += item["q"]
bills = po.get_po_bill_queryset() bills = po.get_po_bill_queryset()
vendors = set([bill.vendor.vendor_name for bill in bills ]) vendors = set([bill.vendor.vendor_name for bill in bills ])
vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A" vendors_str = ", ".join(sorted(list(vendors))) if vendors else "N/A"
writer.writerow([ writer.writerow([
@ -10527,7 +10553,7 @@ def purchase_report_csv_export(request,dealer_slug):
po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '', po.created.strftime("%Y-%m-%d %H:%M:%S") if po.created else '',
po.get_po_status_display(), po.get_po_status_display(),
po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else '', po.date_fulfilled.strftime("%Y-%m-%d") if po.date_fulfilled else '',
f"{po_amount:.2f}", f"{po_amount:.2f}",
po_quantity, po_quantity,
vendors_str vendors_str
]) ])
@ -10540,13 +10566,13 @@ def car_sale_report_view(request,dealer_slug):
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
context={'cars_sold':cars_sold,'current_time':current_time } context={'cars_sold':cars_sold,'current_time':current_time }
return render(request,'ledger/reports/car_sale_report.html',context) return render(request,'ledger/reports/car_sale_report.html',context)
def car_sale_report_csv_export(request,dealer_slug): def car_sale_report_csv_export(request,dealer_slug):
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S") current_time = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
filename = f"sales_report_{dealer_slug}_{current_time}.csv" filename = f"sales_report_{dealer_slug}_{current_time}.csv"
response['Content-Disposition'] = f'attachment; filename="{filename}"' response['Content-Disposition'] = f'attachment; filename="{filename}"'
@ -10555,7 +10581,7 @@ def car_sale_report_csv_export(request,dealer_slug):
header=[ header=[
'Make', 'Make',
'VIN', 'VIN',
'Model', 'Model',
'Year', 'Year',
'Serie', 'Serie',
@ -10572,16 +10598,16 @@ def car_sale_report_csv_export(request,dealer_slug):
'Invoice Number', 'Invoice Number',
] ]
writer.writerow(header) writer.writerow(header)
dealer=get_object_or_404(models.Dealer,slug=dealer_slug) dealer=get_object_or_404(models.Dealer,slug=dealer_slug)
cars_sold=models.Car.objects.filter(dealer=dealer,status='sold') cars_sold=models.Car.objects.filter(dealer=dealer,status='sold')
for car in cars_sold: for car in cars_sold:
writer.writerow([ writer.writerow([
car.vin, car.vin,
car.id_car_make.name, car.id_car_make.name,
car.id_car_model.name, car.id_car_model.name,
car.year, car.year,
car.id_car_serie.name, car.id_car_serie.name,
car.id_car_trim.name, car.id_car_trim.name,
car.mileage, car.mileage,
car.stock_type, car.stock_type,
@ -10595,5 +10621,4 @@ def car_sale_report_csv_export(request,dealer_slug):
car.item_model.invoicemodel_set.first().invoice_number car.item_model.invoicemodel_set.first().invoice_number
]) ])
return response return response

View File

@ -132,3 +132,45 @@ html[dir="rtl"] .form-icon-container .form-control {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
#spinner {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
#spinner-bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
opacity: 0;
transition: opacity 500ms ease-in;
z-index: 5;
}
#spinner-bg.htmx-request {
opacity: .8;
}
/* .fade-me-in.htmx-added {
opacity: 0;
}
.fade-me-in {
opacity: .9;
transition: opacity 300ms ease-out;
} */
#main_content.fade-me-in:not(.modal):not(.modal *) {
opacity: 1;
transition: opacity 300ms ease-out;
}
#main_content.fade-me-in.htmx-added:not(.modal):not(.modal *) {
opacity: 0;
}

1
static/spinner.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='25' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='-.4'></animate></rect><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='85' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='-.2'></animate></rect><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='145' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='0'></animate></rect></svg>

View File

@ -13,7 +13,7 @@
<p class="text-body-tertiary">{{ _("Are you sure you want to sign out?") }}</p> <p class="text-body-tertiary">{{ _("Are you sure you want to sign out?") }}</p>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<form method="post" action="{% url 'account_logout' %}"> <form hx-boost="false" method="post" action="{% url 'account_logout' %}">
{% csrf_token %} {% csrf_token %}
{{ redirect_field }} {{ redirect_field }}
<div class="d-grid gap-2 mt-3"> <div class="d-grid gap-2 mt-3">

View File

@ -69,7 +69,7 @@
<script src="{% static 'js/main.js' %}"></script> <script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/jquery.min.js' %}"></script>
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %} {% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
{% block customCSS %}{% endblock %} {% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
</head> </head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> <body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
{% include "toast-alert.html" %} {% include "toast-alert.html" %}
@ -81,10 +81,20 @@
{% 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" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML" 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="outerHTML transition:false" hx-select-oob="#toast-container" hx-history-elt>
{% block content %} <div id="spinner" class="htmx-indicator spinner-bg">
{% endblock content %} <img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
<script src="{% static 'vendors/popper/popper.min.js' %}"></script> </div>
{% block customCSS %}{% endblock %}
{% block content %}{% endblock content %}
{% block customJS %}{% endblock %}
{% 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 %}
</div> </div>
{% block body %} {% block body %}
{% endblock body %} {% endblock body %}
@ -136,7 +146,16 @@
let datePickers = document.querySelectorAll("[id^='djl-datepicker']") let datePickers = document.querySelectorAll("[id^='djl-datepicker']")
datePickers.forEach(dp => djLedger.getCalendar(dp.attributes.id.value, dateNavigationUrl)) datePickers.forEach(dp => djLedger.getCalendar(dp.attributes.id.value, dateNavigationUrl))
{% endif %} {% endif %}
/*document.body.addEventListener('htmx:configRequest', function(evt) {
evt.detail.indicators = [
...(evt.detail.indicators || []),
document.getElementById('global-indicator')
];
});*/
</script> </script>
{% block customJS %}{% endblock %} {% comment %} {% block customJS %}{% endblock %} {% endcomment %}
</body> </body>
</html> </html>

View File

@ -1,16 +1,16 @@
{% load i18n crispy_forms_tags %} {% load i18n crispy_forms_tags %}
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true"> <div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl"> <div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0"> <div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4> <h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close"> <button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<span class="fas fa-times"></span> <span class="fas fa-times"></span>
</button> </button>
</div> </div>
<div id="emailModalBody" class="modal-body"> <div id="emailModalBody" class="modal-body">
<h1>hi</h1> <h1>hi</h1>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -16,6 +16,13 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form action="{% url 'add_note' request.dealer.slug content_type slug %}" <form action="{% url 'add_note' request.dealer.slug content_type slug %}"
hx-select="#notesTable"
hx-target="#notesTable"
hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_note_form button[type=submit]'));
$('#noteModal').modal('hide');
}"
hx-swap="outerHTML"
method="post" method="post"
class="add_note_form"> class="add_note_form">
{% csrf_token %} {% csrf_token %}
@ -33,5 +40,6 @@
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>

View File

@ -23,6 +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-select=".taskTable"
hx-target=".taskTable"
hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_task_form button[type=submit]'));
$('#taskModal').modal('hide');
}"
method="post" method="post"
class="add_task_form"> class="add_task_form">
{% csrf_token %} {% csrf_token %}

View File

@ -28,6 +28,7 @@
border-bottom: 28px solid transparent; border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6; border-left: 20px solid #dee2e6;
} }
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
@ -68,7 +69,7 @@
<p>{{ lead.email|capfirst }}</p> <p>{{ lead.email|capfirst }}</p>
</div> </div>
<div class="col-6 col-sm-auto flex-1"> <div class="col-6 col-sm-auto flex-1">
<h5 class="text-body-highlight mb-0 text-end"> <h5 id="leadStatus" class="text-body-highlight mb-0 text-end">
{{ _("Status") }} {{ _("Status") }}
{% if lead.status == "new" %} {% if lead.status == "new" %}
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{ _("New") }}</span><span class="fa fa-bell ms-1"></span></span> <span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{ _("New") }}</span><span class="fa fa-bell ms-1"></span></span>
@ -103,7 +104,7 @@
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-body"> <div class="card-body">
<div class="row align-items-center g-3 text-center text-xxl-start"> <div id="assignedTo" class="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center"> <div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5> <h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@ -219,7 +220,7 @@
</div> </div>
</div> </div>
<div class="col-md-7 col-lg-7 col-xl-8"> <div class="col-md-7 col-lg-7 col-xl-8">
<div class="d-flex w-100 gap-5"> <div id="currentStage" class="d-flex w-100 gap-5">
<div class="kanban-header bg-success w-50 text-white fw-bold"> <div class="kanban-header bg-success w-50 text-white fw-bold">
<i class="fa-solid fa-circle-check me-2"></i>{{ lead.status|capfirst }} <i class="fa-solid fa-circle-check me-2"></i>{{ lead.status|capfirst }}
<br> <br>
@ -303,6 +304,11 @@
<div class="modal-content"> <div class="modal-content">
<form class="modal-content" <form class="modal-content"
action="{% url 'lead_transfer' request.dealer.slug lead.slug %}" action="{% url 'lead_transfer' request.dealer.slug lead.slug %}"
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{
resetSubmitButton(document.querySelector('#exampleModal button[type=submit]'));
$('#exampleModal').modal('hide');}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
<div class="modal-header"> <div class="modal-header">
@ -471,7 +477,7 @@
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"></th> <th class="align-middle pe-0 text-end" scope="col" style="width:10%;"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="notesTable">
{% for note in notes %} {% for note in notes %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{ note.note }}</td> <td class="align-middle text-start fw-bold text-body-tertiary ps-1">{{ note.note }}</td>
@ -793,7 +799,7 @@
style="min-width:100px">Completed</th> style="min-width:100px">Completed</th>
</tr> </tr>
</thead> </thead>
<tbody class="list" id="all-tasks-table-body"> <tbody class="list taskTable" id="all-tasks-table-body">
{% for task in schedules %} {% for task in schedules %}
{% include "partials/task.html" %} {% include "partials/task.html" %}
{% endfor %} {% endfor %}
@ -862,7 +868,9 @@
{% endif %} {% endif %}
function openActionModal(leadId, currentAction, nextAction, nextActionDate) { function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false');
document.getElementById('leadId').value = leadId; document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction; document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction; document.getElementById('nextAction').value = nextAction;
@ -870,7 +878,7 @@
modal.show(); modal.show();
} }
document.getElementById('actionTrackingForm').addEventListener('submit', function(e) { /*document.getElementById('actionTrackingForm').addEventListener('submit', function(e) {
e.preventDefault(); e.preventDefault();
const formData = new FormData(this); const formData = new FormData(this);
@ -951,7 +959,7 @@
} }
}); });
}); });
}); });*/
// Helper function for notifications // Helper function for notifications
function notify(tag, msg) { function notify(tag, msg) {
@ -961,5 +969,25 @@
}); });
} }
</script> // Close modal after successful form submission
{% endblock customJS %} 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();
}
}
});
// Cleanup modal backdrop if needed
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();
});
}
});
</script>
{% endblock customJS %}

View File

@ -239,10 +239,6 @@
class="dropdown-item text-success-dark">{% trans "Edit" %}</a> class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
{% endif %} {% endif %}
{% if perms.inventory.change_lead %} {% if perms.inventory.change_lead %}
<button class="dropdown-item text-primary"
onclick="openActionModal('{{ lead.pk }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
{% trans "Update Actions" %}
</button>
{% endif %} {% endif %}
{% if not lead.opportunity %} {% if not lead.opportunity %}
{% if perms.inventory.add_opportunity %} {% if perms.inventory.add_opportunity %}
@ -335,69 +331,7 @@
} }
}); });
fetch("{% url 'update_lead_actions' request.dealer.slug %}", {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
Swal.close();
if (data.success) {
// Success notification
Swal.fire({
toast: true,
icon: 'success',
position: "top-end",
text: data.message || 'Actions updated successfully',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
}).then(() => {
location.reload(); // Refresh after user clicks OK
});
} else {
// Error notification
Swal.fire({
toast: true,
icon: 'error',
position: "top-end",
text: data.message || 'Failed to update actions',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
}
})
.catch(error => {
Swal.close();
console.error('Error:', error);
Swal.fire({
toast: true,
icon: 'error',
position: "top-end",
text: 'An unexpected error occurred',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
});
}); });
// Helper function for notifications // Helper function for notifications
function notify(tag, msg) { function notify(tag, msg) {
Toast.fire({ Toast.fire({

View File

@ -12,7 +12,15 @@
data-bs-dismiss="modal" data-bs-dismiss="modal"
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<form id="actionTrackingForm" method="post"> <form id="actionTrackingForm"
action="{% url 'update_lead_actions' lead.dealer.slug %}"
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{
resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]'));
$('#actionTrackingModal').modal('hide');
}"
method="post">
<div class="modal-body"> <div class="modal-body">
{% csrf_token %} {% csrf_token %}
<input type="hidden" id="leadId" name="lead_id"> <input type="hidden" id="leadId" name="lead_id">

View File

@ -3,7 +3,7 @@
<nav class="navbar navbar-vertical navbar-expand-lg "> <nav class="navbar navbar-vertical navbar-expand-lg ">
<div class="collapse navbar-collapse" id="navbarVerticalCollapse"> <div class="collapse navbar-collapse" id="navbarVerticalCollapse">
<div class="navbar-vertical-content d-flex flex-column"> <div class="navbar-vertical-content d-flex flex-column">
<ul class="navbar-nav flex-column" id="navbarVerticalNav" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML" hx-select-oob="#toast-container"> <ul class="navbar-nav flex-column" id="navbarVerticalNav" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML" hx-select-oob="#toast-container" hx-indicator="#spinner">
<li class="nav-item"> <li class="nav-item">
<p class="navbar-vertical-label text-primary fs-8 text-truncate">{{request.dealer|default:"Apps"}}</p> <p class="navbar-vertical-label text-primary fs-8 text-truncate">{{request.dealer|default:"Apps"}}</p>
<hr class="navbar-vertical-line" /> <hr class="navbar-vertical-line" />
@ -366,7 +366,7 @@
<i class="fas fa-car"></i><span class="nav-link-text">{% trans 'Car Sale Report'|capfirst %}</span> <i class="fas fa-car"></i><span class="nav-link-text">{% trans 'Car Sale Report'|capfirst %}</span>
</div> </div>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
@ -375,11 +375,11 @@
</li> </li>
</ul> </ul>
{# --- Support & Contact Section (New) --- #} {# --- Support & Contact Section (New) --- #}
<div class="mt-auto bg-info-subtle"> <div class="mt-auto bg-info-subtle">
<ul class="navbar-nav flex-column"> <ul class="navbar-nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#"> <a class="nav-link" href="#">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-headphones"></span></span> <span class="nav-link-icon"><span class="fas fa-headphones"></span></span>
<span class="nav-link-text">{% trans 'Haikal Support'|capfirst %}</span> <span class="nav-link-text">{% trans 'Haikal Support'|capfirst %}</span>
@ -387,7 +387,7 @@
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#"> <a class="nav-link" href="#">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-phone"></span></span> <span class="nav-link-icon"><span class="fas fa-phone"></span></span>
<span class="nav-link-text">{% trans 'Haikal Contact'|capfirst %}</span> <span class="nav-link-text">{% trans 'Haikal Contact'|capfirst %}</span>
@ -396,7 +396,7 @@
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#"> <a class="nav-link" href="#">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-robot"></span></span> <span class="nav-link-icon"><span class="fas fa-robot"></span></span>
<span class="nav-link-text">{% trans 'Haikal Bot'|capfirst %}</span> <span class="nav-link-text">{% trans 'Haikal Bot'|capfirst %}</span>

View File

@ -34,7 +34,7 @@
{% endif %} {% endif %}
<!----> <!---->
<div class="row justify-content-center mt-5 mb-3 <div class="row justify-content-center mt-5 mb-3
{% if not vendor_exists %}disabled{% endif %}"> {% if not vendor_exists %}disabled{% endif %}" hx-boost="false">
<div class="col-lg-8 col-md-10"> <div class="col-lg-8 col-md-10">
<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">

View File

@ -43,6 +43,10 @@
let deleteMessage = this.getAttribute("data-message"); let deleteMessage = this.getAttribute("data-message");
confirmDeleteBtn.setAttribute("href", deleteUrl); 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; deleteModalMessage.innerHTML = deleteMessage;
}); });
}); });