Compare commits

...

15 Commits

68 changed files with 1765 additions and 1124 deletions

View File

@ -1,15 +1,15 @@
from inventory import views
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from inventory import views
# from debug_toolbar.toolbar import debug_toolbar_urls
from inventory.notifications.sse import NotificationSSEApp
# import debug_toolbar
from schema_graph.views import Schema from schema_graph.views import Schema
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from inventory.notifications.sse import NotificationSSEApp
# import debug_toolbar
# from two_factor.urls import urlpatterns as tf_urls # from two_factor.urls import urlpatterns as tf_urls
# from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [ urlpatterns = [
# path('__debug__/', include(debug_toolbar.urls)), # path('__debug__/', include(debug_toolbar.urls)),

View File

@ -440,14 +440,14 @@ class CarFinanceForm(forms.ModelForm):
model = CarFinance model = CarFinance
fields = ["cost_price","marked_price"] fields = ["cost_price","marked_price"]
def save(self, commit=True): # def save(self, commit=True):
instance = super().save() # instance = super().save()
try: # try:
instance.additional_services.set(self.cleaned_data["additional_finances"]) # instance.additional_services.set(self.cleaned_data["additional_finances"])
except KeyError: # except KeyError:
pass # pass
instance.save() # instance.save()
return instance # return instance
class CarLocationForm(forms.ModelForm): class CarLocationForm(forms.ModelForm):
@ -1603,7 +1603,7 @@ class PermissionForm(forms.ModelForm):
"django_ledger.invoicemodel", "django_ledger.invoicemodel",
"django_ledger.vendormodel", "django_ledger.vendormodel",
"django_ledger.journalentrymodel" "django_ledger.journalentrymodel"
"django_ledger.purchaseordermodel", # TODO add purchase order "django_ledger.purchaseordermodel",
] ]
permissions = cache.get( permissions = cache.get(

View File

@ -0,0 +1,28 @@
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
import datetime
from inventory.models import Dealer
from plans.models import Plan, Order,PlanPricing
User = get_user_model()
class Command(BaseCommand):
help = ""
def handle(self, *args, **options):
dealer = Dealer.objects.get(user__email="dealer6@example.com")
user = dealer.user
user.userplan.expire = datetime.datetime.now().date()
user.userplan.save()
pp = PlanPricing.objects.get(plan__name="Basic")
order = Order.objects.create(
user=user,
plan=pp.plan,
pricing=pp.pricing,
amount=pp.price,
currency="SA",
tax=15,
status=1,
)
order.complete_order()
print(user.userplan)

View File

@ -0,0 +1,72 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from django.conf import settings
from django.template.loader import render_to_string
from plans.models import UserPlan, Order
from datetime import timedelta
from django.utils.translation import activate, get_language
from django_q.tasks import async_task
import logging
from inventory.tasks import send_bilingual_reminder, handle_email_result
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Handles subscription plan maintenance tasks"
def handle(self, *args, **options):
self.stdout.write("Starting plans maintenance...")
# 1. Send expiration reminders
self.send_expiration_reminders()
# 2. Deactivate expired plans
self.deactivate_expired_plans()
# 3. Clean up old incomplete orders
self.cleanup_old_orders()
self.stdout.write("Maintenance completed!")
def send_expiration_reminders(self):
"""Queue email reminders for expiring plans"""
reminder_days = getattr(settings, 'PLANS_EXPIRATION_REMIND', [3, 7, 14])
today = timezone.now().date()
for days in reminder_days:
target_date = today + timedelta(days=days)
expiring_plans = UserPlan.objects.filter(
active=True,
expire=target_date
).select_related('user', 'plan')
self.stdout.write(f"Queuing {days}-day reminders for {expiring_plans.count()} plans")
for user_plan in expiring_plans:
# Queue email task
async_task(
send_bilingual_reminder,
user_plan.user_id,
user_plan.plan_id,
user_plan.expire,
days,
hook=handle_email_result
)
def deactivate_expired_plans(self):
"""Deactivate plans that have expired (synchronous)"""
expired_plans = UserPlan.objects.filter(
active=True,
expire__lt=timezone.now().date()
)
count = expired_plans.update(active=False)
self.stdout.write(f"Deactivated {count} expired plans")
def cleanup_old_orders(self):
"""Delete incomplete orders older than 30 days"""
cutoff = timezone.now() - timedelta(days=30)
count, _ = Order.objects.filter(
created__lt=cutoff,
status=Order.STATUS.NEW
).delete()
self.stdout.write(f"Cleaned up {count} old incomplete orders")

View File

@ -589,10 +589,11 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
verbose_name_plural = _("Additional Services") verbose_name_plural = _("Additional Services")
def __str__(self): def __str__(self):
return self.name + " - " + str(self.price) return self.name + " - " + str(self.price_)
class Car(Base): class Car(Base):
item_model = models.OneToOneField( item_model = models.OneToOneField(
ItemModel, ItemModel,
models.DO_NOTHING, models.DO_NOTHING,

View File

@ -321,7 +321,6 @@ class BasePurchaseOrderActionActionView(
f"User {user_username} attempting to call action '{self.action_name}' " f"User {user_username} attempting to call action '{self.action_name}' "
f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})." f"on Purchase Order ID: {po_model.pk} (Entity: {entity_slug})."
) )
print(self.action_name)
if self.action_name == "mark_as_fulfilled": if self.action_name == "mark_as_fulfilled":
try: try:
if po_model.can_fulfill(): if po_model.can_fulfill():

View File

@ -24,6 +24,7 @@ from . import models
from django.utils.timezone import now from django.utils.timezone import now
from django.db import transaction from django.db import transaction
from django_q.tasks import async_task from django_q.tasks import async_task
from plans.signals import order_completed, activate_user_plan
# logging # logging
import logging import logging
@ -370,8 +371,8 @@ def update_item_model_cost(sender, instance, created, **kwargs):
) )
instance.car.item_model.default_amount = instance.marked_price instance.car.item_model.default_amount = instance.marked_price
if not isinstance(instance.car.item_model.additional_info, dict): # if not isinstance(instance.car.item_model.additional_info, dict):
instance.car.item_model.additional_info = {} # instance.car.item_model.additional_info = {}
# instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()}) # instance.car.item_model.additional_info.update({"car_finance": instance.to_dict()})
# instance.car.item_model.additional_info.update( # instance.car.item_model.additional_info.update(
# { # {
@ -1153,3 +1154,10 @@ def bill_model_after_approve_notification(sender, instance, created, **kwargs):
please complete the bill payment. please complete the bill payment.
""", """,
) )
def handle_upgrade(sender, order, **kwargs):
logger.info(f"User {order.user} upgraded to {order.plan}")
order_completed.connect(handle_upgrade)

View File

@ -1,11 +1,18 @@
import logging import logging
from plans.models import Plan
from django.conf import settings
from django.db import transaction from django.db import transaction
from django_ledger.io import roles from django_ledger.io import roles
from django_q.tasks import async_task from django_q.tasks import async_task
from django.core.mail import send_mail from django.core.mail import send_mail
from appointment.models import StaffMember from appointment.models import StaffMember
from django.utils.translation import activate
from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from django.core.mail import EmailMultiAlternatives
from inventory.models import DealerSettings, Dealer from inventory.models import DealerSettings, Dealer
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
@ -1151,14 +1158,6 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
user.set_password(password) user.set_password(password)
user.save() user.save()
#TODO remove this later
EmailAddress.objects.create(
user=user,
email=user.email,
verified=True,
primary=True
)
group = Group.objects.create(name=f"{user.pk}-Admin") group = Group.objects.create(name=f"{user.pk}-Admin")
user.groups.add(group) user.groups.add(group)
for perm in Permission.objects.filter( for perm in Permission.objects.filter(
@ -1195,3 +1194,64 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
# instance.user.groups.add(group) # instance.user.groups.add(group)
# transaction.on_commit(run) # transaction.on_commit(run)
def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire):
"""Send bilingual email reminder using Django-Q"""
try:
user = User.objects.get(id=user_id)
plan = Plan.objects.get(id=plan_id)
# Determine user language preference
user_language = getattr(user, 'language', settings.LANGUAGE_CODE)
activate(user_language)
# Context data
context = {
'user': user,
'plan': plan,
'expiration_date': expiration_date,
'days_until_expire': days_until_expire,
'SITE_NAME': settings.SITE_NAME,
'RENEWAL_URL': "url" ,#settings.RENEWAL_URL,
'direction': 'rtl' if user_language.startswith('ar') else 'ltr'
}
# Subject with translation
subject_en = f"Your {plan.name} subscription expires in {days_until_expire} days"
subject_ar = f"اشتراكك في {plan.name} ينتهي خلال {days_until_expire} أيام"
# Render templates
text_content = render_to_string([
f'emails/expiration_reminder_{user_language}.txt',
'emails/expiration_reminder.txt'
], context)
html_content = render_to_string([
f'emails/expiration_reminder_{user_language}.html',
'emails/expiration_reminder.html'
], context)
# Create email
email = EmailMultiAlternatives(
subject=subject_ar if user_language.startswith('ar') else subject_en,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[user.email]
)
email.attach_alternative(html_content, "text/html")
email.send()
return f"Sent to {user.email} in {user_language}"
except Exception as e:
logger.error(f"Email failed: {str(e)}")
raise
def handle_email_result(task):
"""Callback for email results"""
if task.success:
logger.info(f"Email task succeeded: {task.result}")
else:
logger.error(f"Email task failed: {task.result}")

View File

@ -701,7 +701,7 @@ urlpatterns = [
), ),
path( path(
"<slug:dealer_slug>/ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/", "<slug:dealer_slug>/ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/",
views.LedgerModelDeleteView.as_view(), views.LedgerModelDeleteView,
name="ledger-delete", name="ledger-delete",
), ),
path( path(

View File

@ -1515,7 +1515,6 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMe
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["car"] = self.car context["car"] = self.car
print(self.car)
return context return context
# def get_form(self, form_class=None): # def get_form(self, form_class=None):
@ -1571,6 +1570,10 @@ class CarFinanceUpdateView(
kwargs["instance"] = self.get_object() kwargs["instance"] = self.get_object()
return kwargs return kwargs
def get_context_data(self , **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = self.object.car
return context
# def get_initial(self): # def get_initial(self):
# initial = super().get_initial() # initial = super().get_initial()
# instance = self.get_object() # instance = self.get_object()
@ -1617,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)
@ -2263,6 +2266,49 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
return context return context
# class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
# """
# CustomerDetailView handles retrieving and presenting detailed information about
# a specific customer. It ensures that the user is authenticated and has the
# necessary permissions before accessing the customer's details. This view
# provides context data including estimates and invoices related to the customer.
# :ivar model: The model associated with the view.
# :type model: CustomerModel
# :ivar template_name: The path to the template used for rendering the view.
# :type template_name: str
# :ivar context_object_name: The name of the variable in the template context
# for the object being viewed.
# :type context_object_name: str
# :ivar permission_required: The list of permissions required to access this view.
# :type permission_required: list[str]
# """
# model = models.Customer
# template_name = "customers/view_customer.html"
# context_object_name = "customer"
# permission_required = ["inventory.view_customer"]
# def get_context_data(self, **kwargs):
# dealer = get_user_type(self.request)
# entity = dealer.entity
# context = super().get_context_data(**kwargs)
# context["notes"] = models.Notes.objects.filter(
# dealer=dealer,
# content_type__model="customer", object_id=self.object.id
# )
# estimates = entity.get_estimates().filter(customer=self.object.customer_model)
# invoices = entity.get_invoices().filter(customer=self.object.customer_model)
# total = estimates.count() + invoices.count()
# context["estimates"] = estimates
# context["invoices"] = invoices
# context["total"] = total
# context["note_form"] = forms.NoteForm()
# return context
class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
""" """
CustomerDetailView handles retrieving and presenting detailed information about CustomerDetailView handles retrieving and presenting detailed information about
@ -2290,17 +2336,22 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
entity = dealer.entity entity = dealer.entity
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["customer_notes"] = models.Notes.objects.filter(
object_id=self.object.pk context["notes"] = models.Notes.objects.filter(
dealer=dealer,
content_type__model="customer", object_id=self.object.id
) )
estimates = entity.get_estimates().filter(customer=self.object.customer_model) estimates = entity.get_estimates().filter(customer=self.object.customer_model)
invoices = entity.get_invoices().filter(customer=self.object.customer_model) invoices = entity.get_invoices().filter(customer=self.object.customer_model)
context['leads']=self.object.customer_leads.all()
total = estimates.count() + invoices.count() total = estimates.count() + invoices.count()
context["estimates"] = estimates context["estimates"] = estimates
context["invoices"] = invoices context["invoices"] = invoices
context["total"] = total context["total"] = total
context["note_form"] = forms.NoteForm()
return context return context
@ -3973,7 +4024,7 @@ class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
def get_queryset(self): def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
query = self.request.GET.get("q") query = self.request.GET.get("q")
qs = self.model.objects.filter(entity=dealer.entity) qs = self.model.objects.filter(entity_model=dealer.entity)
if query: if query:
qs = apply_search_filters(qs, query) qs = apply_search_filters(qs, query)
return qs return qs
@ -4016,8 +4067,8 @@ class BankAccountUpdateView(
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
entity = dealer.entity entity = dealer.entity
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL kwargs["entity_slug"] = entity.slug
kwargs["user_model"] = entity.admin # Get user_model from the request kwargs["user_model"] = entity.admin
return kwargs return kwargs
def get_form(self, form_class=None): def get_form(self, form_class=None):
@ -4031,7 +4082,6 @@ class BankAccountUpdateView(
] ]
) )
form.fields["account_model"].queryset = account_qs form.fields["account_model"].queryset = account_qs
return form return form
def get_success_url(self): def get_success_url(self):
@ -4334,16 +4384,12 @@ def sales_list_view(request, dealer_slug):
except Exception as e: except Exception as e:
print(e) print(e)
# query = request.GET.get('q') search_query = request.GET.get('q', None)
# # if query: if search_query:
# # qs = qs.filter( qs = qs.filter(
# # Q(order_number__icontains=query) | Q(order_number__icontains=search_query)|
# # Q(customer__name__icontains=query) | Q(customer__customer_name__icontains=search_query)
# # Q(item_details__icontains=query) ).distinct()
# # ).distinct()
# for so in qs:
# if query in so.customer_customer
paginator = Paginator(qs, 30) paginator = Paginator(qs, 30)
page_number = request.GET.get("page") page_number = request.GET.get("page")
@ -4443,6 +4489,13 @@ 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,
) )
qs = EstimateModel.objects.filter(pk__in=[x.content_object.pk for x in qs])
search_query = self.request.GET.get('q', None)
if search_query:
qs = qs.filter(
Q(estimate_number__icontains=search_query)|
Q(customer__customer_name__icontains=search_query)
).distinct()
context["staff_estimates"] = qs context["staff_estimates"] = qs
return context return context
@ -4450,19 +4503,16 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
entity = dealer.entity entity = dealer.entity
status = self.request.GET.get("status") status = self.request.GET.get("status")
queryset = entity.get_estimates() queryset = entity.get_estimates()
type(queryset)
if status: if status:
queryset = queryset.filter(status=status) queryset = queryset.filter(status=status)
for f in queryset.first()._meta.get_fields(): search_query = self.request.GET.get('q', None)
print(f)
search_query = self.request.GET.get('q', '').strip()
print(search_query)
if search_query:
print("inside")
queryset = queryset.filter(
Q(estimate_number__icontains=search_query)
if search_query:
queryset = queryset.filter(
Q(estimate_number__icontains=search_query)|
Q(customer__customer_name__icontains=search_query)
).distinct() ).distinct()
return queryset return queryset
@ -4774,9 +4824,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:
@ -4824,7 +4875,7 @@ def create_sale_order(request, dealer_slug, pk):
estimate.save() estimate.save()
for item in estimate.get_itemtxs_data()[0].all(): for item in estimate.get_itemtxs_data()[0].all():
try: try:
item.item_model.additional_info["car_info"]["status"] = "sold" # item.item_model.additional_info["car_info"]["status"] = "sold"
item.item_model.save() item.item_model.save()
logger.debug( logger.debug(
f"Car status updated to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}." f"Car status updated to 'sold' for item.item_model PK: {getattr(item.item_model, 'pk', 'N/A')}."
@ -5047,6 +5098,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():
@ -5829,6 +5881,7 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
permission_required = ["inventory.view_lead"] permission_required = ["inventory.view_lead"]
def get_queryset(self): def get_queryset(self):
# print(self.request.is_dealer)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
query = self.request.GET.get("q") query = self.request.GET.get("q")
qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted") qs = models.Lead.objects.filter(dealer=dealer).exclude(status="converted")
@ -6487,7 +6540,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)
@ -6994,8 +7047,12 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
def get_queryset(self): def get_queryset(self):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
staff = getattr(self.request.user.staffmember, "staff", None)
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff) if self.request.is_dealer:
queryset = models.Opportunity.objects.filter(dealer=dealer)
elif self.request.is_staff:
staff = self.request.staff
queryset = models.Opportunity.objects.filter(dealer=dealer, lead__staff=staff)
# Search filter # Search filter
search = self.request.GET.get("q") search = self.request.GET.get("q")
@ -7168,8 +7225,8 @@ class ItemServiceCreateView(
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
vat = models.VatRate.objects.get(dealer=dealer, is_active=True) vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
form.instance.dealer = dealer form.instance.dealer = dealer
if form.instance.taxable: # if form.instance.taxable:
form.instance.price = (form.instance.price * vat.rate) + form.instance.price # form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -7217,8 +7274,8 @@ class ItemServiceUpdateView(
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
vat = models.VatRate.objects.get(dealer=dealer, is_active=True) vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
form.instance.dealer = dealer form.instance.dealer = dealer
if form.instance.taxable: # if form.instance.taxable:
form.instance.price = (form.instance.price * vat.rate) + form.instance.price # form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
@ -8880,7 +8937,7 @@ class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
permission_required = "django_ledger.view_ledgermodel" permission_required = "django_ledger.view_ledgermodel"
class LedgerModelCreateView(LedgerModelCreateViewBase): class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin):
""" """
Handles the creation of LedgerModel entities. Handles the creation of LedgerModel entities.
@ -8896,6 +8953,7 @@ class LedgerModelCreateView(LedgerModelCreateViewBase):
template_name = "ledger/ledger/ledger_form.html" template_name = "ledger/ledger/ledger_form.html"
permission_required = ["django_ledger.add_ledgermodel"] permission_required = ["django_ledger.add_ledgermodel"]
success_message = _("Ledger created successfully")
def get_form(self, form_class=None): def get_form(self, form_class=None):
return LedgerModelCreateForm( return LedgerModelCreateForm(
@ -8905,10 +8963,11 @@ class LedgerModelCreateView(LedgerModelCreateViewBase):
) )
def form_valid(self, form): def form_valid(self, form):
form.field["entity"] = self.request.dealer.entity form.fields["entity"] = self.request.dealer.entity
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
messages.success(self.request, self.success_message)
return reverse( return reverse(
"ledger_list", "ledger_list",
kwargs={ kwargs={
@ -8942,34 +9001,47 @@ class LedgerModelModelActionView(LedgerModelModelActionViewBase):
) )
class LedgerModelDeleteView(LedgerModelDeleteViewBase, SuccessMessageMixin):
"""
Handles the deletion of a Ledger model instance.
Provides functionality for rendering a confirmation template and deleting a @login_required
ledger instance from the system. Extends functionality for managing success @permission_required("django_ledger.delete_ledgermodel", raise_exception=True)
messages and redirections upon successful deletion. def LedgerModelDeleteView(request, dealer_slug,entity_slug,ledger_pk):
ledger = LedgerModel.objects.filter(pk=ledger_pk).first()
if request.method == "POST":
ledger.delete()
messages.success(request, _("Ledger deleted successfully"))
return redirect("ledger_list", dealer_slug=dealer_slug, entity_slug=entity_slug)
return render(request,"ledger/ledger/ledger_delete.html",{"ledger_model":ledger})
# class LedgerModelDeleteView(DeleteView, SuccessMessageMixin):
# """
# Handles the deletion of a Ledger model instance.
:ivar template_name: Path to the template used for rendering the delete # Provides functionality for rendering a confirmation template and deleting a
confirmation view. # ledger instance from the system. Extends functionality for managing success
:type template_name: str # messages and redirections upon successful deletion.
:ivar success_message: Success message displayed upon successful deletion
of the ledger instance.
:type success_message: str
"""
template_name = "ledger/ledger/ledger_delete.html" # :ivar template_name: Path to the template used for rendering the delete
success_message = _("Ledger deleted successfully") # confirmation view.
permission_required = ["django_ledger.delete_ledgermodel"] # :type template_name: str
# :ivar success_message: Success message displayed upon successful deletion
# of the ledger instance.
# :type success_message: str
# """
def get_success_url(self): # template_name = "ledger/ledger/ledger_delete.html"
return reverse( # pk_url_kwarg = 'ledger_pk'
"ledger_list", # context_object_name = 'ledger_model'
kwargs={
"dealer_slug": self.kwargs["dealer_slug"], # success_message = _("Ledger deleted successfully")
"entity_slug": self.kwargs["entity_slug"], # permission_required = ["django_ledger.delete_ledgermodel"]
},
) # def get_success_url(self):
# return reverse(
# "ledger_list",
# kwargs={
# "dealer_slug": self.kwargs["dealer_slug"],
# "entity_slug": self.kwargs["entity_slug"],
# },
# )
class JournalEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): class JournalEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
@ -9063,7 +9135,7 @@ class JournalEntryCreateView(
@login_required @login_required
@permission_required("django_ledger.delete_journalentrymodel", raise_exception=True) @permission_required("django_ledger.delete_journalentrymodel", raise_exception=True)
def JournalEntryDeleteView(request, pk): def JournalEntryDeleteView(request,dealer_slug, pk):
""" """
Handles the deletion of a specific journal entry. This view facilitates Handles the deletion of a specific journal entry. This view facilitates
the deletion of a journal entry identified by its primary key (pk). If the the deletion of a journal entry identified by its primary key (pk). If the
@ -9084,10 +9156,10 @@ def JournalEntryDeleteView(request, pk):
ledger = journal_entry.ledger ledger = journal_entry.ledger
if not journal_entry.can_delete(): if not journal_entry.can_delete():
messages.error(request, _("Journal Entry cannot be deleted")) messages.error(request, _("Journal Entry cannot be deleted"))
return redirect("journalentry_list", pk=ledger.pk) return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
journal_entry.delete() journal_entry.delete()
messages.success(request, "Journal Entry deleted") messages.success(request, "Journal Entry deleted")
return redirect("journalentry_list", pk=ledger.pk) return redirect("journalentry_list",dealer_slug=dealer_slug, pk=ledger.pk)
return render( return render(
request, request,
"ledger/journal_entry/journal_entry_delete.html", "ledger/journal_entry/journal_entry_delete.html",
@ -9317,39 +9389,91 @@ def payment_callback(request, dealer_slug):
payment_id = request.GET.get("id") payment_id = request.GET.get("id")
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
payment_status = request.GET.get("status") payment_status = request.GET.get("status")
order = Order.objects.filter(user=dealer.user, status=1).first() order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
if payment_status == "paid": if payment_status == "paid":
# Get or create billing info (optional step)
billing_info, created = BillingInfo.objects.get_or_create( billing_info, created = BillingInfo.objects.get_or_create(
user=dealer.user, user=dealer.user,
tax_number=dealer.vrn, defaults={
name=dealer.arabic_name, 'tax_number': dealer.vrn,
street=dealer.address, 'name': dealer.arabic_name,
zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ", 'street': dealer.address,
city=dealer.entity.city if dealer.entity.city else " ", 'zipcode': dealer.entity.zip_code or " ",
country=dealer.entity.country if dealer.entity.country else " ", 'city': dealer.entity.city or " ",
'country': dealer.entity.country or " ",
}
) )
if created:
userplan = UserPlan.objects.create(
user=request.user,
plan=order.plan,
active=True,
)
userplan.initialize()
order.complete_order() try:
history.status = "paid" # COMPLETE THE ORDER - This handles plan activation/upgrade
history.save() order.complete_order() # Critical step: activates the plan
invoice = order.get_invoices().first()
return render( # Update payment history
request, "payment_success.html", {"order": order, "invoice": invoice} history.status = "paid"
) history.save()
# Retrieve invoice
invoice = order.get_invoices().first()
return render(
request,
"payment_success.html",
{"order": order, "invoice": invoice}
)
except Exception as e:
# Handle activation errors (log, notify admin, etc.)
logger.error(f"Plan activation failed: {str(e)}")
history.status = "failed"
history.save()
return render(request, "payment_failed.html", {"message": "Plan activation error"})
elif payment_status == "failed": elif payment_status == "failed":
history.status = "failed" history.status = "failed"
history.save() history.save()
return render(request, "payment_failed.html", {"message": message})
return render(request, "payment_failed.html", {"message": message}) # Handle unexpected status
return render(request, "payment_failed.html", {"message": "Unknown payment status"})
# def payment_callback(request, dealer_slug):
# message = request.GET.get("message")
# dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
# payment_id = request.GET.get("id")
# history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
# payment_status = request.GET.get("status")
# order = Order.objects.filter(user=dealer.user, status=1).first()
# if payment_status == "paid":
# billing_info, created = BillingInfo.objects.get_or_create(
# user=dealer.user,
# tax_number=dealer.vrn,
# name=dealer.arabic_name,
# street=dealer.address,
# zipcode=dealer.entity.zip_code if dealer.entity.zip_code else " ",
# city=dealer.entity.city if dealer.entity.city else " ",
# country=dealer.entity.country if dealer.entity.country else " ",
# )
# if created:
# userplan = UserPlan.objects.create(
# user=request.user,
# plan=order.plan,
# active=True,
# )
# userplan.initialize()
# order.complete_order()
# history.status = "paid"
# history.save()
# invoice = order.get_invoices().first()
# return render(
# request, "payment_success.html", {"order": order, "invoice": invoice}
# )
# elif payment_status == "failed":
# history.status = "failed"
# history.save()
# return render(request, "payment_failed.html", {"message": message})
@login_required @login_required

View File

@ -31,10 +31,7 @@ msgstr ""
#: templates/inventory/transfer_details.html:89 #: templates/inventory/transfer_details.html:89
#: templates/sales/estimates/estimate_detail.html:234 #: templates/sales/estimates/estimate_detail.html:234
#: templates/sales/estimates/sale_order_form.html:123 #: templates/sales/estimates/sale_order_form.html:123
#: templates/sales/estimates/sale_order_preview.html:203 #: templates/sales/estimates/sale_or ils.html:503
#: templates/sales/invoices/invoice_detail.html:328
#: templates/sales/orders/order_details.html:201
#: templates/sales/orders/order_details.html:503
msgid "VIN" msgid "VIN"
msgstr "رقم الهيكل" msgstr "رقم الهيكل"

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -249,3 +249,46 @@ const getDataTableInit = () => {
}; };
/*
// Register delete modal initializer
htmxInitializer.register(function initDeleteModals() {
const deleteModal = document.getElementById("deleteModal");
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
const deleteModalMessage = document.getElementById("deleteModalText");
// Clean up old listeners
document.querySelectorAll(".delete-btn").forEach(btn => {
btn.removeEventListener("click", handleDeleteClick);
});
// Add new listeners
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?";
confirmDeleteBtn.setAttribute("href", deleteUrl);
deleteModalMessage.textContent = deleteMessage;
if (typeof htmx !== 'undefined') htmx.process(confirmDeleteBtn);
if (typeof bootstrap !== 'undefined') new bootstrap.Modal(deleteModal).show();
}
}, "delete_modals");
// Register custom selects initializer
htmxInitializer.register(function initCustomSelects() {
// Your custom select initialization code
}, "custom_selects");
// Register form submission initializer
htmxInitializer.register(function initForms() {
// Your form handling code
}, "forms");
*/

View File

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

View File

@ -80,7 +80,7 @@
</head> </head>
<body> <body>
<main class="main" id="top"> <main class="main" id="top">
<div class="px-3"> <div id="main_content" class="px-3">
<div class="row min-vh-100 flex-center p-5"> <div class="row min-vh-100 flex-center p-5">
<div class="col-12 col-xl-10 col-xxl-8"> <div class="col-12 col-xl-10 col-xxl-8">
<div class="row justify-content-center align-items-center g-5"> <div class="row justify-content-center align-items-center g-5">

View File

@ -1,9 +1,17 @@
{% extends "account/email/base_message.txt" %} {% extends "account/email/base_message.txt" %}
{% load account %} {% load account i18n %}
{% load i18n %}
{% block content %}{% autoescape off %}{% user_display user as user_display %}{% blocktranslate with site_name=current_site.name site_domain=current_site.domain %}You're receiving this email because user {{ user_display }} has given your email address to register an account on {{ site_domain }}.{% endblocktranslate %} {% block content %}{% autoescape off %}{% user_display user as user_display %}
{% blocktranslate with site_domain=current_site.domain %}تتلقى هذا البريد لأن {{ user_display }} استخدم بريدك للتسجيل في {{ site_domain }}.{% endblocktranslate %}
{% if code %}{% blocktranslate %}Your email verification code is listed below. Please enter it in your open browser window.{% endblocktranslate %} {% if code %}
{% blocktranslate %}أدخل رمز التحقق في المتصفح:{% endblocktranslate %}
{{ code }}{% else %}{% blocktranslate %}To confirm this is correct, go to {{ activate_url }}{% endblocktranslate %}{% endif %}{% endautoescape %}{% endblock content %} {{ code }}
{% blocktranslate %}ينتهي صلاحية هذا الرمز خلال {{ code_expiration }} دقيقة.{% endblocktranslate %}
{% else %}
{% blocktranslate %}أكد هذا التسجيل بالزيارة:{% endblocktranslate %}
{{ activate_url }}
{% endif %}{% endautoescape %}{% endblock content %}

View File

@ -27,6 +27,7 @@
<h3 class="mb-4">{% trans "Change Password" %}</h3> <h3 class="mb-4">{% trans "Change Password" %}</h3>
</div> </div>
<form method="post" <form method="post"
hx-boost="false"
action="{% url 'account_change_password' %}" action="{% url 'account_change_password' %}"
class="form needs-validation" class="form needs-validation"
novalidate> novalidate>

View File

@ -67,6 +67,7 @@
id="user-style-default"> id="user-style-default">
{% endif %} {% endif %}
<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 %}
@ -82,7 +83,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="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:false" 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 +94,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 %}
@ -154,8 +155,57 @@
document.getElementById('global-indicator') document.getElementById('global-indicator')
]; ];
});*/ });*/
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
document.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.xhr.status == 403){
/* Notify the user of a 404 Not Found response */
notify("error", "You do not have permission to view this page");
}
if(evt.detail.xhr.status == 404){
/* Notify the user of a 404 Not Found response */
return alert("Error: Could Not Find Resource");
}
if (evt.detail.successful != true) {
console.log(evt.detail.xhr.statusText)
/* Notify of an unexpected error, & print error to console */
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
}
});
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>

View File

@ -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>

View File

@ -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 %}

View File

@ -23,14 +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" method="post"
hx-target=".taskTable" class="add_task_form"
hx-on::after-request="{ hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
resetSubmitButton(document.querySelector('.add_task_form button[type=submit]')); hx-target="#your-content-container"
$('#taskModal').modal('hide'); hx-swap="innerHTML"
}" 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>

View File

@ -37,17 +37,9 @@
{% include "crm/leads/partials/update_action.html" %} {% include "crm/leads/partials/update_action.html" %}
<div class="row align-items-center justify-content-between g-3 mb-3"> <div class="row align-items-center justify-content-between g-3 mb-3">
<div class="col-12 col-md-auto"> <div class="col-12 col-md-auto">
<h4 class="mb-0">{{ _("Lead Details") }}</h4> <h3 class="mb-0">{{ _("Lead Details") }}</h3>
</div>
<div class="col-12 col-md-auto">
<div class="d-flex">
<div class="flex-1 d-md-none">
<button class="btn px-3 btn-phoenix-secondary text-body-tertiary me-2">
<span class="fa-solid fa-bars"></span>
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -55,12 +47,7 @@
<div class="col-md-5 col-lg-5 col-xl-4"> <div class="col-md-5 col-lg-5 col-xl-4">
<div class="sticky-leads-sidebar"> <div class="sticky-leads-sidebar">
<div class="lead-details" data-breakpoint="md"> <div class="lead-details" data-breakpoint="md">
<div class="d-flex justify-content-between align-items-center mb-2 d-md-none">
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
<button class="btn p-0">
<span class="uil uil-times fs-7"></span>
</button>
</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 class="row align-items-center g-3 text-center text-xxl-start">
@ -494,7 +481,8 @@
data-url="{% url 'update_note' request.dealer.slug note.pk %}" data-url="{% url 'update_note' request.dealer.slug note.pk %}"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
data-note-title="{{ _("Update") }}<i class='fas fa-pen-square text-primary ms-2'></i>"> data-note-title="{{ _('Update') }}">
<i class='fas fa-pen-square text-primary ms-2'></i>
{{ _("Update") }} {{ _("Update") }}
</a> </a>
<button class="btn btn-phoenix-danger btn-sm delete-btn" <button class="btn btn-phoenix-danger btn-sm delete-btn"
@ -806,24 +794,13 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9"> <div class="row align-items-center justify-content-between py-2 pe-0 fs-9 mt-3">
<div class="col-auto d-flex"> <div class="col-auto d-flex">
<p class="mb-0 d-none d-sm-block me-3 fw-semibold text-body"
data-list-info="data-list-info"></p>
<a class="nav-link px-3 d-block" <a class="nav-link px-3 d-block"
href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }} href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }}
<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a> </a>
</div>
<div class="col-auto d-flex">
<button class="page-link" data-list-pagination="prev">
<span class="fas fa-chevron-left"></span>
</button>
<ul class="mb-0 pagination">
</ul>
<button class="page-link pe-0" data-list-pagination="next">
<span class="fas fa-chevron-right"></span>
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -837,9 +814,9 @@
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<!-- schedule Modal --> <!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %} {% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
function reset_form() { function reset_form() {
document.querySelector('#id_note').value = "" document.querySelector('#id_note').value = ""
let form = document.querySelector('.add_note_form') let form = document.querySelector('.add_note_form')

View File

@ -1,16 +1,19 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static humanize %} {% load i18n static humanize %}
{% block title %} {% block title %}
{{ _("Leads") |capfirst }} {{ _("Leads") |capfirst }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if page_obj.object_list %}
<div class="row g-3 mt-4 mb-4"> <div class="row g-3 mt-4 mb-4">
<h2 class="mb-2"> <h2 class="mb-2">
{{ _("Leads") |capfirst }} {{ _("Leads") |capfirst }}
<li class="fas fa-bullhorn text-primary ms-2"></li> <li class="fas fa-bullhorn text-primary ms-2"></li>
</h2> </h2>
<!-- Action Tracking Modal --> <!-- Action Tracking Modal -->
{% include "crm/leads/partials/update_action.html" %} {% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
<div class="row g-3 justify-content-between mb-4"> <div class="row g-3 justify-content-between mb-4">
<div class="col-auto"> <div class="col-auto">
<div class="d-md-flex justify-content-between"> <div class="d-md-flex justify-content-between">
@ -26,6 +29,7 @@
<div class="d-flex">{% include 'partials/search_box.html' %}</div> <div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div> </div>
</div> </div>
<div class="row g-3"> <div class="row g-3">
<div class="col-12"> <div class="col-12">
{% if page_obj.object_list %} {% if page_obj.object_list %}
@ -201,26 +205,7 @@
</small> </small>
</div> </div>
</td> </td>
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{% if lead.opportunity.stage == "prospect" %}
<span class="badge text-bg-primary">{{ lead.opportunity.stage|upper }}</span>
{% elif lead.opportunity.stage == "proposal" %}
<span class="badge text-bg-info">{{ lead.opportunity.stage|upper }}</span>
{% elif lead.opportunity.stage == "negotiation" %}
<span class="badge text-bg-warning">{{ lead.opportunity.stage|upper }}</span>
{% elif lead.opportunity.stage == "closed_won" %}
<span class="badge text-bg-success">{{ lead.opportunity.stage|upper }}</span>
{% elif lead.opportunity.stage == "closed_lost" %}
<span class="badge text-bg-danger">{{ lead.opportunity.stage|upper }}</span>
{% endif %}
</td> {% endcomment %}
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{% if lead.opportunity %}
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}">
<span class="badge badge-phoenix badge-phoenix-success">Opportunity {{ lead.opportunity.lead}} <i class="fa-solid fa-arrow-up-right-from-square"></i></span>
</a>
{% endif %}
</td> {% endcomment %}
<td class="align-middle white-space-nowrap text-end"> <td class="align-middle white-space-nowrap text-end">
{% if user == lead.staff.user or request.is_dealer %} {% if user == lead.staff.user or request.is_dealer %}
<div class="btn-reveal-trigger position-static"> <div class="btn-reveal-trigger position-static">
@ -268,14 +253,15 @@
<div class="d-flex">{% include 'partials/pagination.html' %}</div> <div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div> </div>
{% endif %} {% endif %}
{% else %}
<tr> {% endif %}
<td colspan="6" class="text-center">{% trans "No Lead Yet" %}</td>
</tr>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
{% else %}
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %}
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>
@ -341,3 +327,5 @@
} }
</script> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -168,6 +168,11 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% if not new and not follow_up and not negotiation %}
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
aria-label="Close"></button> aria-label="Close"></button>
</div> </div>
<form id="actionTrackingForm" <form id="actionTrackingForm"
action="{% url 'update_lead_actions' lead.dealer.slug %}" action="{% url 'update_lead_actions' request.dealer.slug %}"
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML" hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
hx-swap="none" hx-swap="none"
hx-on::after-request="{ hx-on::after-request="{

View File

@ -3,7 +3,7 @@
<div class="content"> <div class="content">
<h2 class="mb-5">{{ _("Notifications") }}</h2> <h2 class="mb-5">{{ _("Notifications") }}</h2>
<div class="d-flex justify-content-end mb-3"> <div class="d-flex justify-content-end mb-3">
<a href="{% url 'mark_all_notifications_as_read' %}" class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a> <a href="{% url 'mark_all_notifications_as_read' %}" hx-select-oob="#notification-counter:outerHTML" class="btn btn-phoenix-primary"><i class="far fa-envelope fs-8 me-2"></i>{{ _("Mark all as read") }}</a>
</div> </div>
{% if notifications %} {% if notifications %}
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom"> <div class="mx-n4 mx-lg-n6 mb-5 border-bottom">

View File

@ -578,7 +578,7 @@
style="min-width:165px">Completed</th> style="min-width:165px">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 %}
@ -639,7 +639,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 opportunity.get_notes %} {% for note in opportunity.get_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>
@ -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 -->
@ -1100,3 +1101,28 @@
<!-- schedule Modal --> <!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %} {% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% endblock %} {% endblock %}
{% block customJS %}
<script>
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

@ -5,6 +5,7 @@
{{ _("Opportunities") }} {{ _("Opportunities") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if opportunities %}
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<div class="col-12"> <div class="col-12">
<h2 class="mb-3"> <h2 class="mb-3">
@ -87,6 +88,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4"> <div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
{% include 'crm/opportunities/partials/opportunity_grid.html' %} {% include 'crm/opportunities/partials/opportunity_grid.html' %}
@ -96,6 +98,10 @@
<div class="d-flex">{% include 'partials/pagination.html' %}</div> <div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div> </div>
{% endif %} {% endif %}
{% else %}
{% url 'opportunity_create' request.dealer.slug as create_opportunity_url %}
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
{% endif %}
{% block customJS %} {% block customJS %}
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {

View File

@ -6,6 +6,7 @@
{% endblock title %} {% endblock title %}
{% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %} {% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %}
{% block content %} {% block content %}
{% if customers %}
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<h2 class="mb-2"> <h2 class="mb-2">
{{ _("Customers") |capfirst }} {{ _("Customers") |capfirst }}
@ -168,4 +169,8 @@
</div> </div>
{% endif %} {% endif %}
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
{% else %}
{% url "customer_create" request.dealer.slug as create_customer_url %}
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -5,9 +5,11 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
<div class="row">
<div class="mb-9"> <!---->
<div class="row align-items-center justify-content-between g-3 mb-4"> <div class="mt-4">
<!--heading -->
<div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto"> <div class="col-auto">
<h3 class="mb-0">{% trans 'Customer details' %}</h3> <h3 class="mb-0">{% trans 'Customer details' %}</h3>
</div> </div>
@ -32,12 +34,15 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<div class="row g-5">
<div class="col-12 col-xxl-4">
<div class="row g-3 h-100">
<div class="col-12 col-md-7 col-xxl-12"> <!--cards-->
<div class="card h-100 h-xxl-auto"> <div class="row">
<div class="col m-2">
<div class="card h-100">
<div class="card-body d-flex flex-column justify-content-between pb-3"> <div class="card-body d-flex flex-column justify-content-between pb-3">
<div class="row align-items-center g-5 mb-3 text-center text-sm-start"> <div class="row align-items-center g-5 mb-3 text-center text-sm-start">
<div class="col-12 col-sm-auto mb-sm-2"> <div class="col-12 col-sm-auto mb-sm-2">
@ -62,9 +67,11 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-md-5 col-xxl-12">
<div class="card">
<div class="col m-2">
<div class="card h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center mb-3"> <div class="d-flex align-items-center mb-3">
<h3 class="me-1">{% trans 'Default Address' %}</h3> <h3 class="me-1">{% trans 'Default Address' %}</h3>
@ -82,22 +89,23 @@
<a class="text-body-secondary" href="#">{{ customer.phone_number }}</a> <a class="text-body-secondary" href="#">{{ customer.phone_number }}</a>
</div> </div>
</div> </div>
</div> </div>
<div class="col-12">
<div class="card">
<div class="col m-2">
<div class="card h-100">
<div class="card-body"> <div class="card-body">
{% if perms.inventory.change_customer %} {% if perms.inventory.change_customer %}
<div class="d-flex align-items-center justify-content-end"> <div class="d-flex align-items-center justify-content-end">
<a id="addBtn" {% if perms.inventory.change_lead %}
href="#" <button class="btn btn-phoenix-primary btn-sm"
class="btn btn-sm btn-phoenix-primary mb-3" type="button"
data-url="{% url 'add_note_to_customer' request.dealer.slug customer.slug %}" onclick=""
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal">
data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>"> <span class="fas fa-plus me-1"></span>{{ _("Add Note") }}
<span class="fas fa-plus me-1"></span> </button>
{% trans 'Add Note' %} {% endif %}
</a>
</div> </div>
{% endif %} {% endif %}
<table class="table fs-9 mb-0 table-responsive"> <table class="table fs-9 mb-0 table-responsive">
@ -105,8 +113,8 @@
<th class="align-middle pe-6 text-start" scope="col">{{ _("Note") }}</th> <th class="align-middle pe-6 text-start" scope="col">{{ _("Note") }}</th>
<th class="align-middle pe-6 text-start" scope="col">{{ _("Date") }}</th> <th class="align-middle pe-6 text-start" scope="col">{{ _("Date") }}</th>
</tr> </tr>
<tbody> <tbody id="notesTable">
{% for note in customer_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>
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created }}</td> <td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created }}</td>
@ -116,14 +124,10 @@
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="col-12 mt-3">
</div>
<div class="col-12 col-xxl-8">
<div class="mb-6"> <div class="mb-6">
<h3 class="mb-4">
{{ _("Related") }} <span class="text-body-tertiary fw-normal">({{ total }})</span>
</h3>
<div class="border-top border-bottom border-translucent" <div class="border-top border-bottom border-translucent"
id="customerOrdersTable" id="customerOrdersTable"
data-list='{"valueNames":["order","total","payment_status","fulfilment_status","delivery_type","date"],"page":6,"pagination":true}'> data-list='{"valueNames":["order","total","payment_status","fulfilment_status","delivery_type","date"],"page":6,"pagination":true}'>
@ -131,120 +135,78 @@
<table class="table table-sm fs-9 mb-0"> <table class="table table-sm fs-9 mb-0">
<thead> <thead>
<tr> <tr>
<th class="sort white-space-nowrap align-middle ps-0 pe-3" <th class="sort white-space-nowrap align-middle" scope="col" data-sort="leads">{% trans 'Leads'|upper %}</th>
scope="col" <th class="sort align-middle " scope="col" data-sort="opportunities">{% trans 'Opportunities'|upper %}</th>
data-sort="order">{% trans 'Type'|upper %}</th> <th class="sort align-middle " scope="col" data-sort="estimates">{% trans 'Estimates'|upper %}</th>
<th class="sort align-middle text-end pe-7" scope="col" data-sort="total">{% trans 'Total'|upper %}</th> <th class="sort align-middle " scope="col" data-sort="sale_orders">{% trans 'Sale orders'|upper %}</th>
<th class="sort align-middle white-space-nowrap pe-3" <th class="sort align-middle " scope="col" data-sort="invoices">{% trans 'Invoices'|upper %}</th>
scope="col" <th class="sort align-middle " scope="col" data-sort="car">{% trans 'Car'|upper %}</th>
data-sort="payment_status">{% trans 'Payment Status'|upper %}</th>
<th class="sort align-middle text-end pe-0" scope="col" data-sort="date">{% trans 'Date'|upper %}</th>
<th class="sort text-end align-middle pe-0 ps-5" scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody class="list" id="customer-order-table-body"> <tbody class="list" id="customer-order-table-body">
{% for estimate in estimates %} {% for lead in leads %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr>
<td class="order align-middle white-space-nowrap ps-0"> <td><a href="#">{{lead}} ({{ forloop.counter }})<a></td>
<p class="fw-semibold mb-0"> <td>{{lead.opportunity}} ({{ forloop.counter }})</td>
{{ _("Quotation") }}-<span class="fs-10 fw-medium">{{ estimate.estimate_number }}</span>
</p> <td>
{% for estimate in lead.customer.customer_model.estimatemodel_set.all %}
<div class="me-2">{{estimate}}</div>
<hr>
{% endfor %}
</td> </td>
<td class="total align-middle text-end fw-semibold pe-7 text-body-highlight">
{{ estimate.revenue_estimate|currency_format }} <span class="icon-saudi_riyal"></span> <td>
{% for estimate in lead.customer.customer_model.estimatemodel_set.all %}
<div>{{estimate.sale_orders.first}}</div>
<hr>
{% endfor %}
</td> </td>
<td class="payment_status align-middle white-space-nowrap text-start fw-bold text-body-tertiary"></td> <td>
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-end">{{ estimate.created }}</td> {% for invoice in lead.customer.customer_model.invoicemodel_set.all %}
<td class="align-middle white-space-nowrap text-end pe-0 ps-5">
<div class="btn-reveal-trigger position-static"> {% if invoice.is_paid %}
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10"
type="button"
data-bs-toggle="dropdown"
data-boundary="window"
aria-haspopup="true"
aria-expanded="false"
data-bs-reference="parent">
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item" href="#!">View</a><a class="dropdown-item" href="#!">Export</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#!">Remove</a>
</div>
</div>
</td>
</tr>
{% endfor %}
{% for invoice in invoices %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="order align-middle white-space-nowrap ps-0">
<p class="fw-semibold mb-0">
{{ _("Invoice") }}-<span class="fs-10 fw-medium">{{ invoice.invoice_number }}</span>
</p>
</td>
<td class="total align-middle text-end fw-semibold pe-7 text-body-highlight">
{{ invoice.amount_paid|currency_format }} <span class="icon-saudi_riyal"></span>
</td>
<td class="payment_status align-middle white-space-nowrap text-start fw-bold text-body-tertiary">
{% if invoice.is_paid %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success"> <span class="badge badge-phoenix fs-10 badge-phoenix-success">
<span class="badge-label">{{ _("Paid") }}</span> <div>{{invoice}}</div>
<span class="ms-1" data-feather="check" style="height:12.8px;width:12.8px;"></span>
</span> </span>
{% endif %} {%else%}
<span class="badge badge-phoenix fs-10 badge-phoenix-info">
<div>{{invoice}}</div>
</span>
{% endif %}
<hr>
{% endfor %}
</td> </td>
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-end">{{ invoice.created }}</td> <td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-5"> {% for estimate in lead.customer.customer_model.invoicemodel_set.all %}
<div class="btn-reveal-trigger position-static"> <div>{{estimate.itemtransactionmodel_set.first.item_model.name}}</div>
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10" <hr>
type="button" {% endfor %}
data-bs-toggle="dropdown" <td>
data-boundary="window"
aria-haspopup="true" <tr>
aria-expanded="false"
data-bs-reference="parent">
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
<a class="dropdown-item" href="#!">View</a><a class="dropdown-item" href="#!">Export</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item text-danger" href="#!">Remove</a>
</div>
</div>
</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
{% include 'partials/pagination.html' %}
</div> </div>
</div> </div>
</div> </div>
</div>
</div> <div>
<div class="modal fade" {% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
id="noteModal"
tabindex="-1"
aria-labelledby="noteModalLabel"
aria-hidden="true"> <!---->
<div class="modal-dialog modal-sm">
<div class="modal-content">
<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="noteModalLabel">{% trans 'Notes' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6"
data-bs-dismiss="modal"
aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div class="modal-body">
<!-- Content will be loaded here via AJAX -->
</div>
</div>
</div>
</div>
</div>
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal"); const noteModal = document.getElementById("noteModal");
@ -269,5 +231,6 @@
}); });
}); });
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<body style="font-family: 'Segoe UI', Tahoma, sans-serif; direction: rtl;">
<p>مرحباً {{ user.get_full_name }}،</p>
<p>
اشتراكك في <strong>{{ plan.name }}</strong> سينتهي خلال
{{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
</p>
<p>
<a href="{{ RENEWAL_URL }}">جدد اشتراكك الآن</a> لمواصلة الخدمة.
</p>
<p>مع أطيب التحيات،<br>
فريق {{ SITE_NAME }}</p>
</body>
</html>

View File

@ -0,0 +1,8 @@
مرحباً {{ user.get_full_name }}،
اشتراكك في {{ plan.name }} سينتهي خلال {{ days_until_expire }} يوم في {{ expiration_date|date:"j F Y" }}.
جدد اشتراكك الآن: {{ RENEWAL_URL }}
مع أطيب التحيات،
فريق {{ SITE_NAME }}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; direction: {{ direction }};">
<p>Hello {{ user.get_full_name }},</p>
<p>Your <strong>{{ plan.name }}</strong> subscription will expire
in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.</p>
<p><a href="{{ RENEWAL_URL }}">Renew now</a> to continue service.</p>
<p>Best regards,<br>
The {{ SITE_NAME }} Team</p>
</body>
</html>

View File

@ -0,0 +1,8 @@
Hello {{ user.get_full_name }},
Your {{ plan.name }} subscription will expire in {{ days_until_expire }} days on {{ expiration_date|date:"F j, Y" }}.
Renew now: {{ RENEWAL_URL }}
Best regards,
{{ SITE_NAME }} Team

View File

@ -0,0 +1,55 @@
{% load static %}
<style>
.empty-state-container {
background-color: #ffffff;
padding: 40px;
border-radius: 15px; /* Rounded corners */
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); /* Subtle shadow */
text-align: center;
max-width: 80rem; /* Max width for content - made wider */
width: 90%; /* Fluid width */
margin: 10px auto; /* Added margin-top and auto for horizontal centering */
max-height: auto; /* Added min-height to control the height */
display: flex; /* Use flexbox for vertical centering of content */
flex-direction: column; /* Stack children vertically */
justify-content: center; /* Center content vertically */
align-items: center; /* Center content horizontally */
}
.empty-state-image {
max-width: 50%; /* Responsive image size */
height: auto;
margin-bottom: 10px;
border-radius: 10px; /* Rounded corners for image */
}
.empty-state-title {
color: #343a40; /* Dark text for title */
font-weight: 600;
margin-bottom: 15px;
}
.empty-state-text {
color: #6c757d; /* Muted text for description */
margin-bottom: 30px;
line-height: 1.6;
}
/* No specific styles for .btn-add-new or .message-box are needed here as per previous updates */
</style>
<div class="empty-state-container">
<!-- Empty State Illustration -->
<img src="{% static 'images/logos/no-content-new.jpg' %}" alt="No-empty-state-image" class="empty-state-image">
<!-- Title -->
<h3 class="empty-state-title">No {{ value|capfirst }} Yet</h3>
<!-- Description -->
<p class="empty-state-text">
It looks like you haven't added any {{ value }} to your account.
Click the button below to get started and add your first {{ value }}!
</p>
<!-- Call to Action Button -->
<a class="btn btn-lg btn-primary" href="{{ url }}">Create New {{ value|capfirst }} </a>
</div>

View File

@ -6,6 +6,8 @@
{% trans "Groups" %} {% trans "Groups" %}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if groups %}
<section class=""> <section class="">
<div class="row mt-4"> <div class="row mt-4">
<div class="col-auto"> <div class="col-auto">
@ -57,4 +59,9 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
{% else %}
{% url "group_create" request.dealer.slug as create_group_url %}
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -20,7 +20,7 @@
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li> <li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
{% if perms.inventory.add_car %} {% if perms.inventory.add_car %}
<li class="nav-item"> <li class="nav-item">
<a id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}"> <a hx-boost="true" id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' request.dealer.slug %}">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span> <span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
</div> </div>

View File

@ -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");

View File

@ -42,9 +42,6 @@
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a> <a href="{% url 'car_detail' request.dealer.slug car.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>

View File

@ -2,7 +2,7 @@
{% load i18n static custom_filters %} {% load i18n static custom_filters %}
{% block title %} {% block title %}
{% trans 'Add New Car' %} {% endblock %} {% trans 'Add New Car' %} {% endblock %}
{% block content %} {% block content %}
<style> <style>
#video { #video {
width: 100%; width: 100%;
@ -339,336 +339,435 @@
</div> </div>
</div> </div>
<!----> <!---->
<script> {% endblock content %}
function getCookie(name) { {% block customJS %}
let cookieValue = null; <script>
if (document.cookie && document.cookie !== "") { // Global variables
const cookies = document.cookie.split(";"); let codeReader;
for (let cookie of cookies) { let currentStream = null;
cookie = cookie.trim(); const csrfToken = getCookie("csrftoken");
if (cookie.substring(0, name.length + 1) === name + "=") { const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break; // Initialize when page loads and after HTMX swaps
} document.addEventListener('DOMContentLoaded', initPage);
} document.addEventListener('htmx:afterSwap', initPage);
}
return cookieValue; function initPage() {
// Get DOM elements
const elements = {
vinInput: document.getElementById("{{ form.vin.id_for_label }}"),
decodeVinBtn: document.getElementById("decodeVinBtn"),
makeSelect: document.getElementById("{{ form.id_car_make.id_for_label }}"),
modelSelect: document.getElementById("{{ form.id_car_model.id_for_label }}"),
yearSelect: document.getElementById("{{ form.year.id_for_label }}"),
serieSelect: document.getElementById("{{ form.id_car_serie.id_for_label }}"),
trimSelect: document.getElementById("{{ form.id_car_trim.id_for_label }}"),
equipmentSelect: document.getElementById("equipment_id"),
showSpecificationButton: document.getElementById("specification-btn"),
showEquipmentButton: document.getElementById("options-btn"),
specificationsContent: document.getElementById("specificationsContent"),
optionsContent: document.getElementById("optionsContent"),
generationContainer: document.getElementById("generation-div"),
closeButton: document.querySelector(".btn-close"),
scanVinBtn: document.getElementById("scan-vin-btn"),
videoElement: document.getElementById("video"),
resultDisplay: document.getElementById("result"),
fallbackButton: document.getElementById("ocr-fallback-btn")
};
// Initialize scanner if available
if (typeof ZXing !== 'undefined' && !codeReader) {
codeReader = new ZXing.BrowserMultiFormatReader();
}
// Add event listeners
setupEventListeners(elements);
}
function setupEventListeners(elements) {
// Remove existing listeners first to prevent duplicates
removeEventListeners(elements);
// Add new listeners
if (elements.decodeVinBtn) {
elements.decodeVinBtn.addEventListener("click", decodeVin);
}
if (elements.scanVinBtn) {
elements.scanVinBtn.addEventListener("click", () => {
elements.resultDisplay.textContent = "";
startScanner(elements.videoElement);
});
}
if (elements.fallbackButton) {
elements.fallbackButton.addEventListener("click", () => {
captureAndOCR(elements.videoElement, elements.vinInput);
});
}
if (elements.serieSelect) {
elements.serieSelect.addEventListener("change", () => {
const serie_id = elements.serieSelect.value;
const model_id = elements.modelSelect.value;
if (serie_id && model_id) loadTrims(serie_id, model_id, elements);
});
}
if (elements.trimSelect) {
elements.trimSelect.addEventListener("change", () => {
const trimId = elements.trimSelect.value;
elements.showSpecificationButton.disabled = !trimId;
elements.showEquipmentButton.disabled = !trimId;
if (trimId) loadSpecifications(trimId, elements);
loadEquipment(trimId, elements);
});
}
if (elements.equipmentSelect) {
elements.equipmentSelect.addEventListener("change", () => {
const equipmentId = elements.equipmentSelect.value;
if (equipmentId) loadOptions(equipmentId, elements);
});
}
if (elements.closeButton) {
elements.closeButton.addEventListener("click", () => closeModal(elements.scanVinBtn));
}
if (elements.makeSelect) {
elements.makeSelect.addEventListener("change", (e) => {
loadModels(e.target.value, elements.modelSelect.value, elements);
});
}
if (elements.modelSelect) {
elements.modelSelect.addEventListener("change", (e) => {
loadSeries(e.target.value, elements.yearSelect.value, elements);
});
}
}
function removeEventListeners(elements) {
// Remove all event listeners to prevent duplicates
const events = [
{ element: elements.decodeVinBtn, event: "click", func: decodeVin },
{ element: elements.scanVinBtn, event: "click" },
{ element: elements.fallbackButton, event: "click" },
{ element: elements.serieSelect, event: "change" },
{ element: elements.trimSelect, event: "change" },
{ element: elements.equipmentSelect, event: "change" },
{ element: elements.closeButton, event: "click" },
{ element: elements.makeSelect, event: "change" },
{ element: elements.modelSelect, event: "change" }
];
events.forEach(item => {
if (item.element) {
item.element.removeEventListener(item.event, item.func || null);
} }
});
}
document.addEventListener("DOMContentLoaded", function () { // Cookie helper function
const csrfToken = getCookie("csrftoken"); function getCookie(name) {
let cookieValue = null;
const vinInput = document.getElementById("{{ form.vin.id_for_label }}"); if (document.cookie && document.cookie !== "") {
const decodeVinBtn = document.getElementById("decodeVinBtn"); const cookies = document.cookie.split(";");
const makeSelect = document.getElementById("{{ form.id_car_make.id_for_label }}"); for (let cookie of cookies) {
const modelSelect = document.getElementById("{{ form.id_car_model.id_for_label }}"); cookie = cookie.trim();
const yearSelect = document.getElementById("{{ form.year.id_for_label }}"); if (cookie.substring(0, name.length + 1) === name + "=") {
const serieSelect = document.getElementById("{{ form.id_car_serie.id_for_label }}"); cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
const trimSelect = document.getElementById("{{ form.id_car_trim.id_for_label }}"); break;
const equipmentSelect = document.getElementById("equipment_id")
const showSpecificationButton = document.getElementById("specification-btn");
const showEquipmentButton = document.getElementById("options-btn")
const specificationsContent = document.getElementById("specificationsContent");
const optionsContent = document.getElementById("optionsContent")
const generationContainer = document.getElementById("generation-div")
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
const closeButton = document.querySelector(".btn-close");
const scanVinBtn = document.getElementById("scan-vin-btn");
const videoElement = document.getElementById("video");
const resultDisplay = document.getElementById("result");
const fallbackButton = document.getElementById("ocr-fallback-btn");
let codeReader;
codeReader = new ZXing.BrowserMultiFormatReader();
let currentStream = null;
function closeModal() {
stopScanner();
try {
const scannerModal = document.getElementById("scannerModal");
if (scannerModal) {
document.activeElement.blur();
scannerModal.setAttribute("inert", "true");
const modalInstance = bootstrap.Modal.getInstance(scannerModal);
if (modalInstance) modalInstance.hide();
if (scanVinBtn) scanVinBtn.focus();
}
} catch (err) {
console.error("Error closing scanner modal:", err);
}
} }
}
}
return cookieValue;
}
async function decodeVin() { // VIN Decoding functions
const vinNumber = vinInput.value.trim(); async function decodeVin() {
if (vinNumber.length !== 17) { const vinInput = document.getElementById("{{ form.vin.id_for_label }}");
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}"); const vinNumber = vinInput.value.trim();
/*alert("{% trans 'Please enter a valid VIN.' %}");*/
return; if (vinNumber.length !== 17) {
} Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
showLoading(); return;
try { }
const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
headers: { showLoading();
"X-Requested-With": "XMLHttpRequest", try {
"X-CSRFToken": csrfToken, const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
}, headers: {
}); "X-Requested-With": "XMLHttpRequest",
const data = await response.json(); "X-CSRFToken": csrfToken,
if (data.success) { },
hideLoading(); });
await updateFields(data.data); const data = await response.json();
} else {
hideLoading(); if (data.success) {
Swal.fire("{% trans 'error' %}", data.error); hideLoading();
} await updateFields(data.data);
} catch (error) { } else {
console.error("Error decoding VIN:", error); hideLoading();
hideLoading(); Swal.fire("{% trans 'error' %}", data.error);
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}"); }
} } catch (error) {
console.error("Error decoding VIN:", error);
hideLoading();
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}");
}
}
async function updateFields(vinData) {
const elements = {
makeSelect: document.getElementById("{{ form.id_car_make.id_for_label }}"),
modelSelect: document.getElementById("{{ form.id_car_model.id_for_label }}"),
yearSelect: document.getElementById("{{ form.year.id_for_label }}"),
generationContainer: document.getElementById("generation-div")
};
console.log(vinData);
if (vinData.make_id && elements.makeSelect) {
elements.makeSelect.value = vinData.make_id;
document.getElementById("make-check").innerHTML = "&#10003;";
await loadModels(vinData.make_id);
}
if (vinData.model_id && elements.modelSelect) {
elements.modelSelect.value = vinData.model_id;
document.getElementById("model-check").innerHTML = "&#10003;";
await loadSeries(vinData.model_id, vinData.year);
}
if (vinData.year && elements.yearSelect) {
elements.yearSelect.value = vinData.year;
document.getElementById("year-check").innerHTML = "&#10003;";
}
}
// Scanner functions
async function startScanner(videoElement) {
if (!codeReader) return;
codeReader
.decodeFromVideoDevice(null, videoElement, async (result, err) => {
if (result) {
document.getElementById("{{ form.vin.id_for_label }}").value = result.text;
closeModal();
await decodeVin();
} }
async function updateFields(vinData) {
console.log(vinData);
if (vinData.make_id) {
makeSelect.value = vinData.make_id;
document.getElementById("make-check").innerHTML = "&#10003;";
await loadModels(vinData.make_id);
}
if (vinData.model_id) {
modelSelect.value = vinData.model_id;
document.getElementById("model-check").innerHTML = "&#10003;";
await loadSeries(vinData.model_id, vinData.year);
}
if (vinData.year) {
yearSelect.value = vinData.year;
document.getElementById("year-check").innerHTML = "&#10003;";
}
}
// Start the scanner
async function startScanner() {
codeReader
.decodeFromVideoDevice(null, videoElement, async (result, err) => {
let res = await result;
if (result) {
vinInput.value = result.text;
closeModal();
await decodeVin();
}
})
.catch(console.error);
}
function captureAndOCR() {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
Tesseract.recognize(canvas.toDataURL("image/png"), "eng")
.then(({ data: { text } }) => {
const vin = text.match(/[A-HJ-NPR-Z0-9]{17}/);
if (vin) vinInput.value = vin[0];
closeModal();
decodeVin();
})
.catch((err) => console.error("OCR Error:", err));
}
function stopScanner() {
if (currentStream) {
currentStream.getTracks().forEach((track) => track.stop());
currentStream = null;
}
codeReader.reset();
}
function resetDropdown(dropdown, placeholder) {
dropdown.innerHTML = `<option value="">${placeholder}</option>`;
}
async function loadModels(makeId) {
resetDropdown(modelSelect, '{% trans "Select" %}');
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((model) => {
const option = document.createElement("option");
option.value = model.id_car_model;
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
modelSelect.appendChild(option);
});
}
async function loadSeries(modelId, year) {
resetDropdown(serieSelect, '{% trans "Select" %}');
resetDropdown(trimSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_series&model_id=${modelId}&year=${year}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
console.log(data)
data.forEach((serie) => {
const option = document.createElement("option");
option.value = serie.id_car_serie;
option.textContent = document.documentElement.lang === "en" ? serie.name : serie.name;
generationContainer.innerHTML = serie.generation_name
serieSelect.appendChild(option);
});
}
async function loadTrims(serie_id, model_id) {
resetDropdown(trimSelect, '{% trans "Select" %}');
specificationsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_trims&serie_id=${serie_id}&model_id=${model_id}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
}
});
const data = await response.json();
data.forEach((trim) => {
const option = document.createElement("option");
option.value = trim.id_car_trim;
option.textContent = document.documentElement.lang === "en" ? trim.name : trim.name;
trimSelect.appendChild(option);
});
showSpecificationButton.disabled = !this.value;
}
async function loadEquipment(trimId){
resetDropdown(equipmentSelect, '{% trans "Select" %}');
optionsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_equipments&trim_id=${trimId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
data.forEach((equipment) => {
const option = document.createElement('option');
option.value = equipment.id_car_equipment;
option.textContent = equipment.name;
equipmentSelect.appendChild(option);
});
}
async function loadSpecifications(trimId) {
specificationsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_specifications&trim_id=${trimId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((spec) => {
const parentDiv = document.createElement("div");
parentDiv.innerHTML = `<strong>${spec.parent_name}</strong>`;
spec.specifications.forEach((s) => {
const specDiv = document.createElement("div");
specDiv.innerHTML = `• ${s.s_name}: ${s.s_value} ${s.s_unit}`;
parentDiv.appendChild(specDiv);
});
specificationsContent.appendChild(parentDiv);
});
}
async function loadOptions(equipmentId) {
optionsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_options&equipment_id=${equipmentId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((parent) => {
const parentDiv = document.createElement("div");
parentDiv.innerHTML = `<strong>${parent.parent_name}</strong>`;
parent.options.forEach((option) => {
const optDiv = document.createElement("div");
optDiv.innerHTML = `• ${option.option_name}`;
parentDiv.appendChild(optDiv);
});
optionsContent.appendChild(parentDiv);
});
}
scanVinBtn.addEventListener("click", () => {
resultDisplay.textContent = "";
startScanner();
});
fallbackButton.addEventListener("click", () => {
captureAndOCR();
})
serieSelect.addEventListener("change", () => {
const serie_id = serieSelect.value;
const model_id = modelSelect.value;
if (serie_id && model_id) loadTrims(serie_id, model_id);
})
trimSelect.addEventListener("change", () => {
const trimId = trimSelect.value
showSpecificationButton.disabled = !trimId
showEquipmentButton.disabled = !trimId
if (trimId) loadSpecifications(trimId)
loadEquipment(trimId)
})
equipmentSelect.addEventListener("change", () => {
const equipmentId = equipmentSelect.value
if (equipmentId) loadOptions(equipmentId)
})
closeButton.addEventListener("click", closeModal);
makeSelect.addEventListener("change", (e) => {
loadModels(e.target.value, modelSelect.value);
})
modelSelect.addEventListener("change", (e) => {
loadSeries(e.target.value, yearSelect.value);
})
decodeVinBtn.addEventListener("click", decodeVin);
}) })
.catch(console.error);
}
function captureAndOCR(videoElement, vinInput) {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
function showLoading() { if (typeof Tesseract !== 'undefined') {
Swal.fire({ Tesseract.recognize(canvas.toDataURL("image/png"), "eng")
title: "{% trans 'Please Wait' %}", .then(({ data: { text } }) => {
text: "{% trans 'Loading' %}...", const vin = text.match(/[A-HJ-NPR-Z0-9]{17}/);
allowOutsideClick: false, if (vin) vinInput.value = vin[0];
didOpen: () => { closeModal();
Swal.showLoading(); decodeVin();
}, })
}); .catch((err) => console.error("OCR Error:", err));
}
}
function closeModal(scanVinBtn) {
stopScanner();
try {
const scannerModal = document.getElementById("scannerModal");
if (scannerModal) {
document.activeElement.blur();
scannerModal.setAttribute("inert", "true");
const modalInstance = bootstrap.Modal.getInstance(scannerModal);
if (modalInstance) modalInstance.hide();
if (scanVinBtn) scanVinBtn.focus();
} }
} catch (err) {
console.error("Error closing scanner modal:", err);
}
}
function hideLoading() { function stopScanner() {
Swal.close(); if (currentStream) {
} currentStream.getTracks().forEach((track) => track.stop());
currentStream = null;
}
if (codeReader) codeReader.reset();
}
function notify(tag, msg) { // Data loading functions
Swal.fire({ function resetDropdown(dropdown, placeholder) {
icon: tag, if (dropdown) dropdown.innerHTML = `<option value="">${placeholder}</option>`;
titleText: msg, }
});
async function loadModels(makeId, modelSelect, elements) {
if (!modelSelect) return;
resetDropdown(modelSelect, '{% trans "Select" %}');
const response = await fetch(`${ajaxUrl}?action=get_models&make_id=${makeId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((model) => {
const option = document.createElement("option");
option.value = model.id_car_model;
option.textContent = document.documentElement.lang === "en" ? model.name : model.arabic_name;
modelSelect.appendChild(option);
});
}
async function loadSeries(modelId, year, elements) {
if (!elements?.serieSelect) return;
resetDropdown(elements.serieSelect, '{% trans "Select" %}');
resetDropdown(elements.trimSelect, '{% trans "Select" %}');
if (elements.specificationsContent) elements.specificationsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_series&model_id=${modelId}&year=${year}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
console.log(data);
data.forEach((serie) => {
const option = document.createElement("option");
option.value = serie.id_car_serie;
option.textContent = document.documentElement.lang === "en" ? serie.name : serie.name;
if (elements.generationContainer) elements.generationContainer.innerHTML = serie.generation_name;
elements.serieSelect.appendChild(option);
});
}
async function loadTrims(serie_id, model_id, elements) {
if (!elements?.trimSelect) return;
resetDropdown(elements.trimSelect, '{% trans "Select" %}');
if (elements.specificationsContent) elements.specificationsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_trims&serie_id=${serie_id}&model_id=${model_id}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
} }
</script> });
{% endblock %} const data = await response.json();
data.forEach((trim) => {
const option = document.createElement("option");
option.value = trim.id_car_trim;
option.textContent = document.documentElement.lang === "en" ? trim.name : trim.name;
elements.trimSelect.appendChild(option);
});
if (elements.showSpecificationButton) {
elements.showSpecificationButton.disabled = !elements.trimSelect.value;
}
}
async function loadEquipment(trimId, elements) {
if (!elements?.equipmentSelect) return;
resetDropdown(elements.equipmentSelect, '{% trans "Select" %}');
if (elements.optionsContent) elements.optionsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_equipments&trim_id=${trimId}`, {
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken
}
});
const data = await response.json();
data.forEach((equipment) => {
const option = document.createElement('option');
option.value = equipment.id_car_equipment;
option.textContent = equipment.name;
elements.equipmentSelect.appendChild(option);
});
}
async function loadSpecifications(trimId, elements) {
if (!elements?.specificationsContent) return;
elements.specificationsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_specifications&trim_id=${trimId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((spec) => {
const parentDiv = document.createElement("div");
parentDiv.innerHTML = `<strong>${spec.parent_name}</strong>`;
spec.specifications.forEach((s) => {
const specDiv = document.createElement("div");
specDiv.innerHTML = `• ${s.s_name}: ${s.s_value} ${s.s_unit}`;
parentDiv.appendChild(specDiv);
});
elements.specificationsContent.appendChild(parentDiv);
});
}
async function loadOptions(equipmentId, elements) {
if (!elements?.optionsContent) return;
elements.optionsContent.innerHTML = "";
const response = await fetch(`${ajaxUrl}?action=get_options&equipment_id=${equipmentId}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
data.forEach((parent) => {
const parentDiv = document.createElement("div");
parentDiv.innerHTML = `<strong>${parent.parent_name}</strong>`;
parent.options.forEach((option) => {
const optDiv = document.createElement("div");
optDiv.innerHTML = `• ${option.option_name}`;
parentDiv.appendChild(optDiv);
});
elements.optionsContent.appendChild(parentDiv);
});
}
// UI Helper functions
function showLoading() {
Swal.fire({
title: "{% trans 'Please Wait' %}",
text: "{% trans 'Loading' %}...",
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
});
}
function hideLoading() {
Swal.close();
}
function notify(tag, msg) {
Swal.fire({
icon: tag,
titleText: msg,
});
}
</script>
{% endblock customJS %}

View File

@ -24,6 +24,8 @@
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
{% if cars%}
<div class="container-fluid" id="projectSummary"> <div class="container-fluid" id="projectSummary">
<div class="row g-3 justify-content-between align-items-end mb-4"> <div class="row g-3 justify-content-between align-items-end mb-4">
<div class="col-12 col-sm-auto"> <div class="col-12 col-sm-auto">
@ -312,6 +314,13 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
{% url "car_add" request.dealer.slug as create_car_url %}
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
{% endif %}
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>

View File

@ -4,6 +4,8 @@
{% trans "Inventory Stats"|capfirst %} {% trans "Inventory Stats"|capfirst %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if inventory %}
<div class="row justify-content-between"> <div class="row justify-content-between">
<div class="col-sm-12 "> <div class="col-sm-12 ">
<div class="card border h-100 w-100 p-lg-10"> <div class="card border h-100 w-100 p-lg-10">
@ -92,4 +94,9 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% else %}
{% url "car_add" request.dealer.slug as create_car_url %}
{% include "empty-illustration-page.html" with value="car" url=create_car_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -19,7 +19,6 @@
</h3> </h3>
</div> </div>
<div class="card-body bg-light-subtle"> <div class="card-body bg-light-subtle">
<form method="post" action=""> <form method="post" action="">
{% csrf_token %} {% csrf_token %}
{{ form|crispy }} {{ form|crispy }}
@ -29,11 +28,7 @@
<button class="btn btn-lg btn-phoenix-primary md-me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button> <button class="btn btn-lg btn-phoenix-primary md-me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button>
<a href="{% url 'item_expense_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 'item_expense_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>

View File

@ -4,6 +4,9 @@
{{ _("Expenses") }} {{ _("Expenses") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if expenses %}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Expenses" %} <span class="fas fa-money-bill-wave ms-2 text-primary"></span></h3> <h3 class="">{% trans "Expenses" %} <span class="fas fa-money-bill-wave ms-2 text-primary"></span></h3>
@ -54,4 +57,9 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% else %}
{% url "item_expense_create" request.dealer.slug as create_expense_url %}
{% include "empty-illustration-page.html" with value="expense" url=create_expense_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -4,6 +4,8 @@
{{ _("Services") }} {{ _("Services") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if services %}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Services" %}<span class="fas fa-tools text-primary ms-2"></span></h3> <h3 class="">{% trans "Services" %}<span class="fas fa-tools text-primary ms-2"></span></h3>
@ -56,4 +58,10 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% else %}
{% url 'item_service_create' request.dealer.slug as create_services_url %}
{% include "empty-illustration-page.html" with value="service" url=create_services_url %}
{%endif%}
{% endblock %} {% endblock %}

View File

@ -4,6 +4,7 @@
{{ _("Bank Accounts") }} {{ _("Bank Accounts") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if bank_accounts %}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Bank Accounts" %}<span class="fas fa-bank ms-2 text-primary"></span></h3> <h3 class="">{% trans "Bank Accounts" %}<span class="fas fa-bank ms-2 text-primary"></span></h3>
@ -53,4 +54,9 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
{% else %}
{% url 'bank_account_create' request.dealer.slug as create_bank_account_url %}
{% include "empty-illustration-page.html" with value="bank account" url=create_bank_account_url%}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -5,7 +5,7 @@
{{ _("Create Bill") }} {{ _("Create Bill") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="row mt-4"> <div class="row mt-4" hx-boost="true">
<h3 class="text-center">{% trans "Create Bill" %}<span class="fas fa-money-bills ms-2 text-primary"></span></h3> <h3 class="text-center">{% trans "Create Bill" %}<span class="fas fa-money-bills ms-2 text-primary"></span></h3>
<form id="mainForm" method="post" class="needs-validation"> <form id="mainForm" method="post" class="needs-validation">
{% csrf_token %} {% csrf_token %}
@ -15,7 +15,7 @@
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"> <button class="btn btn-sm btn-phoenix-success me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }} <i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</button> </button>
<a href="{{ request.META.HTTP_REFERER }}" <a href="{% url 'bill_list' request.dealer.slug %}"
class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a> class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div> </div>
</form> </form>

View File

@ -10,6 +10,8 @@
</a> </a>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if bills %}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Bills" %}<span class="fas fa-money-bills ms-2 text-primary"></span></h3> <h3 class="">{% trans "Bills" %}<span class="fas fa-money-bills ms-2 text-primary"></span></h3>
@ -85,4 +87,10 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %}
{% else %}
{% url "bill-create" request.dealer.slug as create_bill_url %}
{% include "empty-illustration-page.html" with value="bill" url=create_bill_url %}
{% endif %}
{% endblock %}

View File

@ -10,6 +10,9 @@
</a> </a>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if accounts%}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class=""> {% trans "Accounts" %}<i class="fa-solid fa-book ms-2 text-primary"></i></h3> <h3 class=""> {% trans "Accounts" %}<i class="fa-solid fa-book ms-2 text-primary"></i></h3>
@ -196,6 +199,12 @@
</div> </div>
</div> </div>
</div> </div>
{% else %}
{% endif %}
{% url "account_create" request.dealer.slug as create_account_url %}
{% include "empty-illustration-page.html" with value="account" url=create_account_url %}
{% endblock %} {% endblock %}
{% block customerJS %} {% block customerJS %}
<script> <script>

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<form action="{% url 'journalentry_delete' journal_entry.pk %}" <form action="{% url 'journalentry_delete' request.dealer.slug journal_entry.pk %}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
<div class="card"> <div class="card">
@ -13,7 +13,7 @@
<h5 class="card-title fw-light">Are you sure you want to delete?</h5> <h5 class="card-title fw-light">Are you sure you want to delete?</h5>
</div> </div>
<div class="card-body text-center"> <div class="card-body text-center">
<a href="{% url 'journalentry_list' journal_entry.ledger.pk %}" <a href="{% url 'journalentry_list' request.dealer.slug journal_entry.ledger.pk %}"
class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a> class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a>
<button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button> <button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button>
</div> </div>

View File

@ -4,6 +4,8 @@
{{ _("Journal Entries") }} {{ _("Journal Entries") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if journal_entries %}
<div class="modal fade" <div class="modal fade"
id="confirmModal" id="confirmModal"
tabindex="-1" tabindex="-1"
@ -130,5 +132,10 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!--test-->
{% else %}
{% url 'journalentry_create' request.dealer.slug ledger.pk as create_je_url %}
{% include "empty-illustration-page.html" with value="journal entry" url=create_je_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -27,7 +27,7 @@
{% for transaction in transactions %} {% for transaction in transactions %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static"> <tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td>{{ forloop.counter }}</td> <td>{{ forloop.counter }}</td>
<td class="align-middle product white-space-nowrap py-0">{{ transaction.created|date }}</td> <td class="align-middle product white-space-nowrap py-0">{{ transaction.created|date }}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.account.name }}</td> <td class="align-middle product white-space-nowrap">{{ transaction.account.name }}</td>
<td class="align-middle product white-space-nowrap">{{ transaction.account.code }}</td> <td class="align-middle product white-space-nowrap">{{ transaction.account.code }}</td>
<td class="align-middle product white-space-nowrap text-success"> <td class="align-middle product white-space-nowrap text-success">

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<form action="{% url 'ledger-delete' entity_slug=view.kwargs.entity_slug ledger_pk=ledger_model.uuid %}" <form action="{% url 'ledger-delete' dealer_slug=request.dealer.slug entity_slug=request.dealer.entity.slug ledger_pk=ledger_model.uuid %}"
method="post"> method="post">
{% csrf_token %} {% csrf_token %}
<div class="card"> <div class="card">
@ -13,7 +13,7 @@
<h5 class="card-title fw-light">{{ ledger_model.get_delete_message }}</h5> <h5 class="card-title fw-light">{{ ledger_model.get_delete_message }}</h5>
</div> </div>
<div class="card-body text-center"> <div class="card-body text-center">
<a href="{% url 'ledger_list' %}" class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a> <a href="{% url 'ledger_list' request.dealer.slug request.dealer.entity.slug %}" class="btn btn-phoenix-primary me-2">{% trans 'Go Back' %}</a>
<button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button> <button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button>
</div> </div>
</div> </div>

View File

@ -4,6 +4,8 @@
{{ _("Ledger") }} {{ _("Ledger") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if ledgers %}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Ledger" %} <span class="fas fa-book-open ms-2 text-primary"></span></h3> <h3 class="">{% trans "Ledger" %} <span class="fas fa-book-open ms-2 text-primary"></span></h3>
@ -127,4 +129,9 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% else %}
{% url 'ledger_create' request.dealer.slug as create_ledger_url %}
{% include "empty-illustration-page.html" with value="ledger" url=create_ledger_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -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>

View File

@ -5,7 +5,7 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
<div class="row my-4"> <div class="row my-4">
<h2>{{ organization.get_local_name }}</h2> <h2 class="mb-2">{{ organization.get_local_name }}</h2>
<ul class="list-group mb-4"> <ul class="list-group mb-4">
<li class="list-group-item"> <li class="list-group-item">
<strong>{% trans "CRN" %}:</strong> {{ organization.crn }} <strong>{% trans "CRN" %}:</strong> {{ organization.crn }}

View File

@ -8,6 +8,7 @@
<a class="nav-link active">{% trans 'Organizations' %}</a> <a class="nav-link active">{% trans 'Organizations' %}</a>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% if organizations%}
<section class="pt-5 pb-9 "> <section class="pt-5 pb-9 ">
<div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0"> <div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0">
<h2 class="mb-4"> <h2 class="mb-4">
@ -182,4 +183,10 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
{% else %}
{% url 'organization_create' request.dealer.slug as create_organization_url %}
{% include "empty-illustration-page.html" with value="organization" url=create_organization_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,35 +1,16 @@
<div class="search-box me-2"> <div class="search-box me-2">
<form class="position-relative show" id="search-form"> <form class="position-relative show" id="search-form">
<input name="q" <input name="q"
hx-get=""
hx-boost="true"
hx-trigger="keyup delay:500ms"
id="search-input" id="search-input"
class="form-control form-control-sm search-input search" class="form-control form-control-sm search-input search"
type="search" type="search"
aria-label="Search" aria-label="Search"
placeholder="{{ _("Search") }}..." placeholder="{{ _('Search') }}..."
value="{{ request.GET.q }}" /> value="{{ request.GET.q }}" />
<span class="fa fa-magnifying-glass search-box-icon"></span> <span class="fa fa-magnifying-glass search-box-icon"></span>
{% if request.GET.q %}
<button type="button"
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
id="clear-search"
aria-label="Close"></button>
{% endif %}
</form> </form>
</div> </div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const searchInput = document.getElementById("search-input");
const clearButton = document.getElementById("clear-search");
if (clearButton) {
clearButton.addEventListener("click", function(event) {
event.preventDefault();
searchInput.value = ""; // Clear input field
// Remove query parameter without reloading the page
const newUrl = window.location.pathname;
history.replaceState(null, "", newUrl);
window.location.reload();
});
}
});
</script>

View File

@ -3,6 +3,9 @@
{% load i18n static %} {% load i18n static %}
{% block title %}Purchase Orders - {{ block.super }}{% endblock %} {% block title %}Purchase Orders - {{ block.super }}{% endblock %}
{% block content %} {% block content %}
{% if purchase_orders %}
<div class="row mt-4"> <div class="row mt-4">
<!-- Success Message --> <!-- Success Message -->
{% if messages %} {% if messages %}
@ -118,4 +121,9 @@
{% endif %} {% endif %}
</div> </div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
{% else %}
{% url "purchase_order_create" request.dealer.slug request.dealer.entity.slug as create_purchase_url %}
{% include "empty-illustration-page.html" with value="purchase order" url=create_purchase_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -250,12 +250,7 @@
<td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td> <td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Vat" %} ({{ data.vat }})</td>
<td class="align-middle text-start fw-semibold">
<span id="grand-total">+ {{ data.total_vat_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
</td>
</tr>
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td> <td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Discount Amount" %}</td>
<td class="align-middle text-start text-danger fw-semibold"> <td class="align-middle text-start text-danger fw-semibold">
@ -275,6 +270,12 @@
</form> </form>
</td> </td>
</tr> </tr>
<tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Vat" %} ({{ data.vat }})</td>
<td class="align-middle text-start fw-semibold">
<span id="grand-total">+ {{ data.total_vat_amount|floatformat }}<span class="icon-saudi_riyal"></span></span>
</td>
</tr>
<tr class="bg-body-secondary total-sum"> <tr class="bg-body-secondary total-sum">
<td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td> <td class="align-middle ps-4 fw-semibold text-body-highlight" colspan="7">{% trans "Additional Services" %}</td>
<td class="align-middle text-start fw-semibold"> <td class="align-middle text-start fw-semibold">
@ -334,39 +335,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 %}

View File

@ -209,158 +209,239 @@
</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> }
}
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 %} {% endblock %}

View File

@ -4,6 +4,7 @@
{{ _("Quotations") }} {{ _("Quotations") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if estimates %}
<div class="row g-3 mt-4 mb-4"> <div class="row g-3 mt-4 mb-4">
<div class="row g-3 justify-content-between mb-4"> <div class="row g-3 justify-content-between mb-4">
<div class="col-auto"> <div class="col-auto">
@ -30,31 +31,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 %}
@ -75,4 +76,10 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% else %}
{% url "estimate_create" request.dealer.slug as create_estimate_url %}
{% include "empty-illustration-page.html" with value="customer" url=create_estimate_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -3,206 +3,23 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
<h1>{% trans 'Sale Order' %}</h1> {{ _("Sale Order") }}
{% endblock %} {% endblock title %}
{% block content %} {% block content %}
<link rel="stylesheet" href="{% static 'flags/sprite.css' %}" /> <div class="row">
<div class="row"> <div class="row justify-content-center">
<div class="row mb-3"> <div class="col-md-8">
<div class="col-sm-6 col-md-8"> <div class="card">
<div class="d-sm-flex justify-content-between"> <div class="card-header">{{ _("Sale Order") }}</div>
<h3 class="mb-3"> <div class="card-body">
{% if customer.created %} <form method="post" action="{% url 'create_sale_order' request.dealer.slug estimate.pk %}">
{{ _('Edit Sale Order') }}
{% else %}
{{ _('Add Sale Order') }}
{% endif %}
<span class="fas fa-shopping-cart text-primary ms-2"></span>
</h3>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-xl-12 col-xxl-12">
<div class="px-xl-12">
<div class="row mx-0 mx-sm-3 mx-lg-0 px-lg-0">
<div class="col-sm-12 col-xxl-6 py-3">
<table class="w-100 table-stats ">
<tr>
<th></th>
<th></th>
<th></th>
</tr>
<tr>
<td class="py-2">
<div class="d-inline-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
<span class="text-success-dark" data-feather="user" style="width:16px; height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Customer Name")}}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0 mb-0 pb-3 pb-sm-0">{{ estimate.customer.customer_name }}</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
<span class="text-success-dark" data-feather="mail" style="width:16px; height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Email") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ estimate.customer.email }}</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
<span class="text-success-dark" data-feather="map-pin" style="width:16px; height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Address") }}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ estimate.customer.address_1 }}</p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
<span class="text-success-dark" data-feather="trending-down" style="width:16px; height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Total Discount")}}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ data.total_discount }} <span class="icon-saudi_riyal"></span></p>
</td>
</tr>
<tr>
<td class="py-2">
<div class="d-flex align-items-center">
<div class="d-flex bg-success-subtle rounded-circle flex-center me-3" style="width:24px; height:24px">
<span class="text-success-dark" data-feather="briefcase" style="width:16px; height:16px"></span>
</div>
<p class="fw-bold mb-0">{{ _("Total Amount")}}</p>
</div>
</td>
<td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ data.grand_total }} <span class="icon-saudi_riyal"></span></p>
</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="border-top border-bottom border-translucent mt-10" id="leadDetailsTable">
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table fs-9 mb-0">
<thead>
<tr>
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" />
</div>
</th>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="name" style="width:20%; min-width:100px">
{{ _("VIN") }}</th>
<th class="sort align-middle pe-6 text-uppercase" scope="col" data-sort="description" style="width:20%; max-width:60px">
{{ _("Make") }}</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="create_date" style="width:20%; min-width:115px">
{{ _("Model") }}</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="create_by" style="width:20%; min-width:150px">
{{ _("Year") }}</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="create_by" style="width:20%; min-width:150px">
{{ _("Unit Price")}}</th>
<th class="align-middle pe-0 text-end" scope="col" style="width:15%;"></th>
</tr>
</thead>
<tbody class="list" id="lead-details-table-body">
{% for car in data.cars %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" />
</div>
</td>
<td class="name align-middle white-space-nowrap py-2 ps-0">
<a class="d-flex align-items-center text-body-highlight" href="#!">
{% comment %} <div class="avatar avatar-m me-3 status-online">
<img class="rounded-circle" src="" alt="" />
</div> {% endcomment %}
<h6 class="mb-0 text-body-highlight fw-bold">{{ car.vin }}</h6>
</a>
</td>
<td class="description align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2 pe-6">
{{ car.make }}
</td>
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{ car.model }}</td>
<td class="create_by align-middle white-space-nowrap fw-semibold text-body-highlight">{{ car.year }}</td>
<td class="last_activity align-middle text-center py-2">
<div class="d-flex align-items-center flex-1">
<span class="fw-bold fs-9 text-body">{{ car.total }} <span class="icon-saudi_riyal"></span></span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="row mx-0">
<form method="post" class="form row g-3 needs-validation" novalidate>
{% csrf_token %} {% csrf_token %}
<div class="col-md-6"> {{ form|crispy }}
<label for="id_estimate" class="form-label">{% trans "Quotation" %}</label> <button type="submit" class="btn btn-phoenix-primary">{% trans 'Save' %}</button>
<input type="text"
class="form-control form-control-sm"
id="id_estimate"
name="estimate"
value="{{ form.estimate.value|default_if_none:'' }}"
readonly>
<div class="invalid-feedback">{% trans "Please provide an estimate." %}</div>
</div>
<div class="col-md-6">
<label for="id_payment_method" class="form-label">{% trans "Payment Method" %}</label>
<select class="form-select form-select-sm"
id="id_payment_method"
name="payment_method"
required>
{% for value, label in form.payment_method.field.choices %}
<option value="{{ value }}"
{% if form.payment_method.value == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<div class="invalid-feedback">{% trans "Please select a payment method." %}</div>
</div>
<div class="col-12">
<label for="id_comments" class="form-label">{% trans "Comments" %}</label>
<textarea class="form-control" id="id_comments" name="comments" rows="3">{{ form.comments.value|default_if_none:'' }}</textarea>
</div>
<div class="col-12">
<button class="btn btn-phoenix-primary" type="submit">{% trans 'Save' %}</button>
</div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!---->
{% endblock content %} {% endblock content %}

View File

@ -4,6 +4,8 @@
{{ _("Invoices") }} {{ _("Invoices") }}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{% if invoices %}
<div class="row mt-4"> <div class="row mt-4">
<div class="row g-3 justify-content-between mb-4"> <div class="row g-3 justify-content-between mb-4">
<div class="col-auto"> <div class="col-auto">
@ -85,4 +87,8 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% else %}
{% include "empty-illustration-page.html" with value="invoice" url="#" %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -4,6 +4,7 @@
{%block title%} {%trans 'Sale Orders'%} {%endblock%} {%block title%} {%trans 'Sale Orders'%} {%endblock%}
{% block content %} {% block content %}
{% if txs %}
<section class="mt-2"> <section class="mt-2">
<div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0"> <div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0">
@ -112,4 +113,7 @@
{% endif %} {% endif %}
</div> </div>
</section> </section>
{% else %}
{% include "empty-illustration-page.html" with value="sale order" url='#' %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -5,6 +5,9 @@
{% trans "Staffs" %} {% trans "Staffs" %}
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{%if users %}
<section class=""> <section class="">
<div class="row mt-4"> <div class="row mt-4">
<div class="col-auto"> <div class="col-auto">
@ -21,7 +24,7 @@
<i class="fa-solid fa-circle-info fs-6"></i> <i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1"> <p class="mb-0 flex-1">
{{ _("No Active Subscription,please activate your subscription.") }}<a href="{% url 'pricing_page' request.dealer.slug %}" {{ _("No Active Subscription,please activate your subscription.") }}<a href="{% url 'pricing_page' request.dealer.slug %}"
class="ms-3 text-body-primary fs-9">Manage Subscription</a> class="ms-3 text-body-primary fs-9">Manage Subscription</a>
</p> </p>
<button class="btn-close" <button class="btn-close"
type="button" type="button"
@ -86,4 +89,10 @@
</div> </div>
</div> </div>
</section> </section>
{% else %}
{% url "user_create" request.dealer.slug as create_staff_url %}
{% include "empty-illustration-page.html" with value="staff" url=create_staff_url %}
{% endif %}
{% endblock %} {% endblock %}

View File

@ -6,6 +6,8 @@
{% endblock title %} {% endblock title %}
{% block vendors %}<a class="nav-link active">{{ _("Vendors") |capfirst }}</a>{% endblock %} {% block vendors %}<a class="nav-link active">{{ _("Vendors") |capfirst }}</a>{% endblock %}
{% block content %} {% block content %}
{% if vendors %}
<div class="row mt-4"> <div class="row mt-4">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<h3 class=""> <h3 class="">
@ -161,4 +163,9 @@
{% endif %} {% endif %}
</div> </div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
{% else %}
{% url "vendor_create" request.dealer.slug as create_vendor_url %}
{% include "empty-illustration-page.html" with value="vendor" url=create_vendor_url %}
{% endif %}
{% endblock %} {% endblock %}