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.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 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 debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
# path('__debug__/', include(debug_toolbar.urls)),

View File

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

@ -208,7 +208,7 @@ class Command(BaseCommand):
last_name = fake.last_name()
email = fake.email()
staff = random.choice(Staff.objects.filter(dealer=dealer))
make = random.choice(CarMake.objects.all())
model = random.choice(make.carmodel_set.all())
lead = Lead.objects.create(

View File

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

View File

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

View File

@ -24,6 +24,7 @@ from . import models
from django.utils.timezone import now
from django.db import transaction
from django_q.tasks import async_task
from plans.signals import order_completed, activate_user_plan
# 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
if not isinstance(instance.car.item_model.additional_info, dict):
instance.car.item_model.additional_info = {}
# if not isinstance(instance.car.item_model.additional_info, dict):
# 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(
# {
@ -1153,3 +1154,10 @@ def bill_model_after_approve_notification(sender, instance, created, **kwargs):
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
from plans.models import Plan
from django.conf import settings
from django.db import transaction
from django_ledger.io import roles
from django_q.tasks import async_task
from django.core.mail import send_mail
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 django.core.mail import EmailMultiAlternatives
from inventory.models import DealerSettings, Dealer
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
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.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")
user.groups.add(group)
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)
# 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(
"<slug:dealer_slug>/ledgers/<slug:entity_slug>/delete/<uuid:ledger_pk>/",
views.LedgerModelDeleteView.as_view(),
views.LedgerModelDeleteView,
name="ledger-delete",
),
path(

View File

@ -1515,7 +1515,6 @@ class CarFinanceCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMe
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["car"] = self.car
print(self.car)
return context
# def get_form(self, form_class=None):
@ -1571,6 +1570,10 @@ class CarFinanceUpdateView(
kwargs["instance"] = self.get_object()
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):
# initial = super().get_initial()
# instance = self.get_object()
@ -1617,7 +1620,7 @@ class CarUpdateView(
permission_required = ["inventory.change_car"]
def get_success_url(self):
return reverse("car_detail", kwargs={"slug": self.object.slug})
return reverse("car_detail", kwargs={"dealer_slug": self.request.dealer.slug,"slug": self.object.slug})
def get_form(self, form_class=None):
form = super().get_form(form_class)
@ -2263,6 +2266,49 @@ class CustomerListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
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):
"""
CustomerDetailView handles retrieving and presenting detailed information about
@ -2290,17 +2336,22 @@ class CustomerDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
dealer = get_user_type(self.request)
entity = dealer.entity
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)
invoices = entity.get_invoices().filter(customer=self.object.customer_model)
context['leads']=self.object.customer_leads.all()
total = estimates.count() + invoices.count()
context["estimates"] = estimates
context["invoices"] = invoices
context["total"] = total
context["note_form"] = forms.NoteForm()
return context
@ -3973,7 +4024,7 @@ class BankAccountDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
def get_queryset(self):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
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:
qs = apply_search_filters(qs, query)
return qs
@ -4016,8 +4067,8 @@ class BankAccountUpdateView(
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
entity = dealer.entity
kwargs = super().get_form_kwargs()
kwargs["entity_slug"] = entity.slug # Get entity_slug from URL
kwargs["user_model"] = entity.admin # Get user_model from the request
kwargs["entity_slug"] = entity.slug
kwargs["user_model"] = entity.admin
return kwargs
def get_form(self, form_class=None):
@ -4031,7 +4082,6 @@ class BankAccountUpdateView(
]
)
form.fields["account_model"].queryset = account_qs
return form
def get_success_url(self):
@ -4334,16 +4384,12 @@ def sales_list_view(request, dealer_slug):
except Exception as e:
print(e)
# query = request.GET.get('q')
# # if query:
# # qs = qs.filter(
# # Q(order_number__icontains=query) |
# # Q(customer__name__icontains=query) |
# # Q(item_details__icontains=query)
# # ).distinct()
# for so in qs:
# if query in so.customer_customer
search_query = request.GET.get('q', None)
if search_query:
qs = qs.filter(
Q(order_number__icontains=search_query)|
Q(customer__customer_name__icontains=search_query)
).distinct()
paginator = Paginator(qs, 30)
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_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
return context
@ -4450,19 +4503,16 @@ class EstimateListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
dealer = get_user_type(self.request)
entity = dealer.entity
status = self.request.GET.get("status")
queryset = entity.get_estimates()
type(queryset)
if status:
queryset = queryset.filter(status=status)
for f in queryset.first()._meta.get_fields():
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)
search_query = self.request.GET.get('q', None)
if search_query:
queryset = queryset.filter(
Q(estimate_number__icontains=search_query)|
Q(customer__customer_name__icontains=search_query)
).distinct()
return queryset
@ -4774,9 +4824,10 @@ class EstimateDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView
kwargs["data"] = finance_data
kwargs["invoice"] = invoice_obj
try:
cf = estimate.get_itemtxs_data()[0].first().item_model.car.finances
selected_items = cf.additional_services.filter(dealer=dealer)
car_finances = estimate.get_itemtxs_data()[0].first().item_model.car.finances
selected_items = car_finances.additional_services.filter(dealer=dealer)
form = forms.AdditionalFinancesForm()
form.fields["additional_finances"].queryset = form.fields["additional_finances"].queryset.filter(dealer=dealer)
form.initial["additional_finances"] = selected_items
kwargs["additionals_form"] = form
except Exception as e:
@ -4824,7 +4875,7 @@ def create_sale_order(request, dealer_slug, pk):
estimate.save()
for item in estimate.get_itemtxs_data()[0].all():
try:
item.item_model.additional_info["car_info"]["status"] = "sold"
# item.item_model.additional_info["car_info"]["status"] = "sold"
item.item_model.save()
logger.debug(
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)
estimate = get_object_or_404(EstimateModel, pk=pk)
mark = request.GET.get("mark")
print(mark)
if mark:
if mark == "review":
if not estimate.can_review():
@ -5829,6 +5881,7 @@ class LeadListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
permission_required = ["inventory.view_lead"]
def get_queryset(self):
# print(self.request.is_dealer)
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
query = self.request.GET.get("q")
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:
messages.error(request, _("You do not have permission to schedule."))
return redirect(request.META.get("HTTP_REFERER"))
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
if request.method == "POST":
form = forms.ScheduleForm(request.POST)
@ -6994,8 +7047,12 @@ class OpportunityListView(LoginRequiredMixin, PermissionRequiredMixin, ListView)
def get_queryset(self):
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 = self.request.GET.get("q")
@ -7168,8 +7225,8 @@ class ItemServiceCreateView(
dealer = get_user_type(self.request)
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
form.instance.dealer = dealer
if form.instance.taxable:
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
# if form.instance.taxable:
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form)
def get_success_url(self):
@ -7217,8 +7274,8 @@ class ItemServiceUpdateView(
dealer = get_user_type(self.request)
vat = models.VatRate.objects.get(dealer=dealer, is_active=True)
form.instance.dealer = dealer
if form.instance.taxable:
form.instance.price = (form.instance.price * vat.rate) + form.instance.price
# if form.instance.taxable:
# form.instance.price = (form.instance.price * vat.rate) + form.instance.price
return super().form_valid(form)
def get_success_url(self):
@ -8880,7 +8937,7 @@ class LedgerModelDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
permission_required = "django_ledger.view_ledgermodel"
class LedgerModelCreateView(LedgerModelCreateViewBase):
class LedgerModelCreateView(LedgerModelCreateViewBase,SuccessMessageMixin):
"""
Handles the creation of LedgerModel entities.
@ -8896,6 +8953,7 @@ class LedgerModelCreateView(LedgerModelCreateViewBase):
template_name = "ledger/ledger/ledger_form.html"
permission_required = ["django_ledger.add_ledgermodel"]
success_message = _("Ledger created successfully")
def get_form(self, form_class=None):
return LedgerModelCreateForm(
@ -8905,10 +8963,11 @@ class LedgerModelCreateView(LedgerModelCreateViewBase):
)
def form_valid(self, form):
form.field["entity"] = self.request.dealer.entity
form.fields["entity"] = self.request.dealer.entity
return super().form_valid(form)
def get_success_url(self):
messages.success(self.request, self.success_message)
return reverse(
"ledger_list",
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
ledger instance from the system. Extends functionality for managing success
messages and redirections upon successful deletion.
@login_required
@permission_required("django_ledger.delete_ledgermodel", raise_exception=True)
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
confirmation view.
:type template_name: str
:ivar success_message: Success message displayed upon successful deletion
of the ledger instance.
:type success_message: str
"""
# Provides functionality for rendering a confirmation template and deleting a
# ledger instance from the system. Extends functionality for managing success
# messages and redirections upon successful deletion.
template_name = "ledger/ledger/ledger_delete.html"
success_message = _("Ledger deleted successfully")
permission_required = ["django_ledger.delete_ledgermodel"]
# :ivar template_name: Path to the template used for rendering the delete
# confirmation view.
# :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):
return reverse(
"ledger_list",
kwargs={
"dealer_slug": self.kwargs["dealer_slug"],
"entity_slug": self.kwargs["entity_slug"],
},
)
# template_name = "ledger/ledger/ledger_delete.html"
# pk_url_kwarg = 'ledger_pk'
# context_object_name = 'ledger_model'
# success_message = _("Ledger deleted successfully")
# 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):
@ -9063,7 +9135,7 @@ class JournalEntryCreateView(
@login_required
@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
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
if not journal_entry.can_delete():
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()
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(
request,
"ledger/journal_entry/journal_entry_delete.html",
@ -9317,39 +9389,91 @@ def payment_callback(request, 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()
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
if payment_status == "paid":
# Get or create billing info (optional step)
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 " ",
defaults={
'tax_number': dealer.vrn,
'name': dealer.arabic_name,
'street': dealer.address,
'zipcode': dealer.entity.zip_code or " ",
'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()
history.status = "paid"
history.save()
invoice = order.get_invoices().first()
return render(
request, "payment_success.html", {"order": order, "invoice": invoice}
)
try:
# COMPLETE THE ORDER - This handles plan activation/upgrade
order.complete_order() # Critical step: activates the plan
# Update payment history
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":
history.status = "failed"
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

View File

@ -31,10 +31,7 @@ msgstr ""
#: templates/inventory/transfer_details.html:89
#: templates/sales/estimates/estimate_detail.html:234
#: templates/sales/estimates/sale_order_form.html:123
#: templates/sales/estimates/sale_order_preview.html:203
#: templates/sales/invoices/invoice_detail.html:328
#: templates/sales/orders/order_details.html:201
#: templates/sales/orders/order_details.html:503
#: templates/sales/estimates/sale_or ils.html:503
msgid "VIN"
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>
<body>
<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="col-12 col-xl-10 col-xxl-8">
<div class="row justify-content-center align-items-center g-5">

View File

@ -1,9 +1,17 @@
{% extends "account/email/base_message.txt" %}
{% load account %}
{% load i18n %}
{% load account 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>
</div>
<form method="post"
hx-boost="false"
action="{% url 'account_change_password' %}"
class="form needs-validation"
novalidate>

View File

@ -66,7 +66,8 @@
rel="stylesheet"
id="user-style-default">
{% endif %}
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
@ -82,7 +83,7 @@
{% include "plans/expiration_messages.html" %}
{% block 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">
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
</div>
@ -93,8 +94,8 @@
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script>
<script src="{% static 'js/phoenix.js' %}"></script> {% endcomment %}
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
<script src="{% static 'js/phoenix.js' %}"></script>
</div>
{% block body %}
@ -154,8 +155,57 @@
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>
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
</body>

View File

@ -40,6 +40,5 @@
document.querySelector('#id_note').value = note
let form = document.querySelector('.add_note_form')
form.action = url
}
</script>

View File

@ -16,6 +16,13 @@
</div>
<div class="modal-body">
<form action="{% url 'schedule_event' request.dealer.slug content_type slug %}"
hx-select=".taskTable"
hx-target=".taskTable"
hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_schedule_form button[type=submit]'));
$('#scheduleModal').modal('hide');
}"
hx-swap="outerHTML"
method="post"
class="add_schedule_form">
{% csrf_token %}

View File

@ -23,14 +23,12 @@
</div>
<div class="modal-body">
<form action="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-select=".taskTable"
hx-target=".taskTable"
hx-on::after-request="{
resetSubmitButton(document.querySelector('.add_task_form button[type=submit]'));
$('#taskModal').modal('hide');
}"
method="post"
class="add_task_form">
method="post"
class="add_task_form"
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-target="#your-content-container"
hx-swap="innerHTML"
hx-boost="false">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>

View File

@ -37,17 +37,9 @@
{% include "crm/leads/partials/update_action.html" %}
<div class="row align-items-center justify-content-between g-3 mb-3">
<div class="col-12 col-md-auto">
<h4 class="mb-0">{{ _("Lead Details") }}</h4>
</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>
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
</div>
</div>
</div>
</div>
@ -55,12 +47,7 @@
<div class="col-md-5 col-lg-5 col-xl-4">
<div class="sticky-leads-sidebar">
<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-body">
<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-bs-toggle="modal"
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") }}
</a>
<button class="btn btn-phoenix-danger btn-sm delete-btn"
@ -806,24 +794,13 @@
</tbody>
</table>
</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">
<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"
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>
</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>
</a>
</div>
</div>
</div>
</div>
@ -837,9 +814,9 @@
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
{% endblock content %}
{% block customJS %}
<script>
{% endblock content %}
{% block customJS %}
<script>
function reset_form() {
document.querySelector('#id_note').value = ""
let form = document.querySelector('.add_note_form')

View File

@ -1,16 +1,19 @@
{% extends 'base.html' %}
{% load i18n static humanize %}
{% block title %}
{{ _("Leads") |capfirst }}
{% endblock title %}
{% block content %}
{% if page_obj.object_list %}
<div class="row g-3 mt-4 mb-4">
<h2 class="mb-2">
{{ _("Leads") |capfirst }}
<li class="fas fa-bullhorn text-primary ms-2"></li>
</h2>
<!-- 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="col-auto">
<div class="d-md-flex justify-content-between">
@ -26,6 +29,7 @@
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
{% if page_obj.object_list %}
@ -201,26 +205,7 @@
</small>
</div>
</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">
{% if user == lead.staff.user or request.is_dealer %}
<div class="btn-reveal-trigger position-static">
@ -268,14 +253,15 @@
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
{% else %}
<tr>
<td colspan="6" class="text-center">{% trans "No Lead Yet" %}</td>
</tr>
{% endif %}
{% endif %}
</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 %}
{% block customJS %}
<script>
@ -341,3 +327,5 @@
}
</script>
{% endblock customJS %}

View File

@ -168,6 +168,11 @@
{% endfor %}
</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>

View File

@ -13,7 +13,7 @@
aria-label="Close"></button>
</div>
<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-swap="none"
hx-on::after-request="{

View File

@ -3,7 +3,7 @@
<div class="content">
<h2 class="mb-5">{{ _("Notifications") }}</h2>
<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>
{% if notifications %}
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom">

View File

@ -578,7 +578,7 @@
style="min-width:165px">Completed</th>
</tr>
</thead>
<tbody class="list" id="all-tasks-table-body">
<tbody class="list taskTable" id="all-tasks-table-body">
{% for task in schedules %}
{% include "partials/task.html" %}
{% endfor %}
@ -639,7 +639,7 @@
<th class="align-middle pe-0 text-end" scope="col" style="width:10%;"></th>
</tr>
</thead>
<tbody>
<tbody id="notesTable">
{% for note in opportunity.get_notes %}
<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>
@ -1091,6 +1091,7 @@
</div>
</div>
{% include 'modal/delete_modal.html' %}
<!-- email Modal -->
{% include "components/email_modal.html" %}
<!-- task Modal -->
@ -1100,3 +1101,28 @@
<!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% 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") }}
{% endblock title %}
{% block content %}
{% if opportunities %}
<div class="row g-3 mt-4">
<div class="col-12">
<h2 class="mb-3">
@ -87,6 +88,7 @@
{% endif %}
</div>
</div>
</div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
@ -96,6 +98,10 @@
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% 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 %}
<script>
document.addEventListener("DOMContentLoaded", function() {

View File

@ -6,6 +6,7 @@
{% endblock title %}
{% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %}
{% block content %}
{% if customers %}
<div class="row g-3 mt-4">
<h2 class="mb-2">
{{ _("Customers") |capfirst }}
@ -168,4 +169,8 @@
</div>
{% endif %}
{% 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 %}

View File

@ -5,9 +5,11 @@
{% endblock title %}
{% block content %}
{% 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">
<h3 class="mb-0">{% trans 'Customer details' %}</h3>
</div>
@ -32,12 +34,15 @@
{% endif %}
</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">
<div class="card h-100 h-xxl-auto">
</div>
<!--cards-->
<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="row align-items-center g-5 mb-3 text-center text-sm-start">
<div class="col-12 col-sm-auto mb-sm-2">
@ -62,9 +67,11 @@
</div>
</div>
</div>
</div>
<div class="col-12 col-md-5 col-xxl-12">
<div class="card">
</div>
<div class="col m-2">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<h3 class="me-1">{% trans 'Default Address' %}</h3>
@ -82,22 +89,23 @@
<a class="text-body-secondary" href="#">{{ customer.phone_number }}</a>
</div>
</div>
</div>
<div class="col-12">
<div class="card">
</div>
<div class="col m-2">
<div class="card h-100">
<div class="card-body">
{% if perms.inventory.change_customer %}
<div class="d-flex align-items-center justify-content-end">
<a id="addBtn"
href="#"
class="btn btn-sm btn-phoenix-primary mb-3"
data-url="{% url 'add_note_to_customer' request.dealer.slug customer.slug %}"
data-bs-toggle="modal"
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>
{% trans 'Add Note' %}
</a>
{% if perms.inventory.change_lead %}
<button class="btn btn-phoenix-primary btn-sm"
type="button"
onclick=""
data-bs-toggle="modal"
data-bs-target="#noteModal">
<span class="fas fa-plus me-1"></span>{{ _("Add Note") }}
</button>
{% endif %}
</div>
{% endif %}
<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">{{ _("Date") }}</th>
</tr>
<tbody>
{% for note in customer_notes %}
<tbody id="notesTable">
{% for note in notes %}
<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-body-tertiary text-start white-space-nowrap">{{ note.created }}</td>
@ -116,14 +124,10 @@
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-8">
</div>
<div class="col-12 mt-3">
<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"
id="customerOrdersTable"
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">
<thead>
<tr>
<th class="sort white-space-nowrap align-middle ps-0 pe-3"
scope="col"
data-sort="order">{% trans 'Type'|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 white-space-nowrap pe-3"
scope="col"
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>
<th class="sort white-space-nowrap align-middle" scope="col" data-sort="leads">{% trans 'Leads'|upper %}</th>
<th class="sort align-middle " scope="col" data-sort="opportunities">{% trans 'Opportunities'|upper %}</th>
<th class="sort align-middle " scope="col" data-sort="estimates">{% trans 'Estimates'|upper %}</th>
<th class="sort align-middle " scope="col" data-sort="sale_orders">{% trans 'Sale orders'|upper %}</th>
<th class="sort align-middle " scope="col" data-sort="invoices">{% trans 'Invoices'|upper %}</th>
<th class="sort align-middle " scope="col" data-sort="car">{% trans 'Car'|upper %}</th>
</tr>
</thead>
<tbody class="list" id="customer-order-table-body">
{% for estimate in estimates %}
<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">
{{ _("Quotation") }}-<span class="fs-10 fw-medium">{{ estimate.estimate_number }}</span>
</p>
{% for lead in leads %}
<tr>
<td><a href="#">{{lead}} ({{ forloop.counter }})<a></td>
<td>{{lead.opportunity}} ({{ forloop.counter }})</td>
<td>
{% for estimate in lead.customer.customer_model.estimatemodel_set.all %}
<div class="me-2">{{estimate}}</div>
<hr>
{% endfor %}
</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 class="payment_status align-middle white-space-nowrap text-start fw-bold text-body-tertiary"></td>
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-end">{{ estimate.created }}</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-5">
<div class="btn-reveal-trigger position-static">
<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 %}
<td>
{% for invoice in lead.customer.customer_model.invoicemodel_set.all %}
{% if invoice.is_paid %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success">
<span class="badge-label">{{ _("Paid") }}</span>
<span class="ms-1" data-feather="check" style="height:12.8px;width:12.8px;"></span>
<div>{{invoice}}</div>
</span>
{% endif %}
{%else%}
<span class="badge badge-phoenix fs-10 badge-phoenix-info">
<div>{{invoice}}</div>
</span>
{% endif %}
<hr>
{% endfor %}
</td>
<td class="date align-middle white-space-nowrap text-body-tertiary fs-9 ps-4 text-end">{{ invoice.created }}</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-5">
<div class="btn-reveal-trigger position-static">
<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>
<td>
{% for estimate in lead.customer.customer_model.invoicemodel_set.all %}
<div>{{estimate.itemtransactionmodel_set.first.item_model.name}}</div>
<hr>
{% endfor %}
<td>
<tr>
{% endfor %}
</tbody>
</table>
</div>
{% include 'partials/pagination.html' %}
</div>
</div>
</div>
</div>
</div>
<div class="modal fade"
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>
<div>
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
<!---->
<script>
document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal");
@ -269,5 +231,6 @@
});
});
});
</script>
{% 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" %}
{% endblock title %}
{% block content %}
{% if groups %}
<section class="">
<div class="row mt-4">
<div class="col-auto">
@ -57,4 +59,9 @@
{% endif %}
</div>
</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 %}

View File

@ -20,7 +20,7 @@
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
{% if perms.inventory.add_car %}
<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">
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
</div>

View File

@ -611,12 +611,14 @@
</div>
</div>
{% endif %}
{% endblock %}
{% block customJS %}
<script>
document.addEventListener("DOMContentLoaded", function () {
const csrftoken = getCookie("csrftoken");
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
const modalBody = customCardModal.querySelector(".modal-body");
const showSpecificationButton = document.getElementById("specification-btn");

View File

@ -27,7 +27,7 @@
{% endif %}
</div>
<div class="card-body bg-light-subtle">
<form method="post" class="needs-validation" novalidate>
<div class="row g-1">
<div class="col-lg-4 col-xl-12">
@ -35,16 +35,13 @@
{{ form|crispy }}
</div>
</div>
<hr class="my-2">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<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 '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>
</form>
</div>
</div>
</div>

View File

@ -2,7 +2,7 @@
{% load i18n static custom_filters %}
{% block title %}
{% trans 'Add New Car' %} {% endblock %}
{% block content %}
{% block content %}
<style>
#video {
width: 100%;
@ -339,336 +339,435 @@
</div>
</div>
<!---->
<script>
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
{% endblock content %}
{% block customJS %}
<script>
// Global variables
let codeReader;
let currentStream = null;
const csrfToken = getCookie("csrftoken");
const ajaxUrl = "{% url 'ajax_handler' request.dealer.slug %}";
// Initialize when page loads and after HTMX swaps
document.addEventListener('DOMContentLoaded', initPage);
document.addEventListener('htmx:afterSwap', initPage);
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 () {
const csrfToken = getCookie("csrftoken");
const vinInput = document.getElementById("{{ form.vin.id_for_label }}");
const decodeVinBtn = document.getElementById("decodeVinBtn");
const makeSelect = document.getElementById("{{ form.id_car_make.id_for_label }}");
const modelSelect = document.getElementById("{{ form.id_car_model.id_for_label }}");
const yearSelect = document.getElementById("{{ form.year.id_for_label }}");
const serieSelect = document.getElementById("{{ form.id_car_serie.id_for_label }}");
const trimSelect = document.getElementById("{{ form.id_car_trim.id_for_label }}");
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);
}
// Cookie helper function
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
async function decodeVin() {
const vinNumber = vinInput.value.trim();
if (vinNumber.length !== 17) {
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
/*alert("{% trans 'Please enter a valid VIN.' %}");*/
return;
}
showLoading();
try {
const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
if (data.success) {
hideLoading();
await updateFields(data.data);
} else {
hideLoading();
Swal.fire("{% trans 'error' %}", data.error);
}
} catch (error) {
console.error("Error decoding VIN:", error);
hideLoading();
Swal.fire("error", "{% trans 'An error occurred while decoding the VIN.' %}");
}
// VIN Decoding functions
async function decodeVin() {
const vinInput = document.getElementById("{{ form.vin.id_for_label }}");
const vinNumber = vinInput.value.trim();
if (vinNumber.length !== 17) {
Swal.fire("error", "{% trans 'Please enter a valid VIN.' %}");
return;
}
showLoading();
try {
const response = await fetch(`${ajaxUrl}?action=decode_vin&vin_no=${vinNumber}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
"X-CSRFToken": csrfToken,
},
});
const data = await response.json();
if (data.success) {
hideLoading();
await updateFields(data.data);
} else {
hideLoading();
Swal.fire("{% trans 'error' %}", data.error);
}
} 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() {
Swal.fire({
title: "{% trans 'Please Wait' %}",
text: "{% trans 'Loading' %}...",
allowOutsideClick: false,
didOpen: () => {
Swal.showLoading();
},
});
if (typeof Tesseract !== 'undefined') {
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 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() {
Swal.close();
}
function stopScanner() {
if (currentStream) {
currentStream.getTracks().forEach((track) => track.stop());
currentStream = null;
}
if (codeReader) codeReader.reset();
}
function notify(tag, msg) {
Swal.fire({
icon: tag,
titleText: msg,
});
// Data loading functions
function resetDropdown(dropdown, placeholder) {
if (dropdown) dropdown.innerHTML = `<option value="">${placeholder}</option>`;
}
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>
{% endblock customCSS %}
{% block content %}
{% if cars%}
<div class="container-fluid" id="projectSummary">
<div class="row g-3 justify-content-between align-items-end mb-4">
<div class="col-12 col-sm-auto">
@ -312,6 +314,13 @@
</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 %}
{% block customJS %}
<script>

View File

@ -4,6 +4,8 @@
{% trans "Inventory Stats"|capfirst %}
{% endblock %}
{% block content %}
{% if inventory %}
<div class="row justify-content-between">
<div class="col-sm-12 ">
<div class="card border h-100 w-100 p-lg-10">
@ -92,4 +94,9 @@
{% endif %}
</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 %}

View File

@ -6,7 +6,7 @@
{{ _("Add New Expense") }}
{% endblock title %}
{% block content %}
<!---->
<div class="row justify-content-center mt-5 mb-3">
@ -19,7 +19,6 @@
</h3>
</div>
<div class="card-body bg-light-subtle">
<form method="post" action="">
{% csrf_token %}
{{ 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>
<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>
</form>
</div>
</div>
</div>

View File

@ -4,6 +4,9 @@
{{ _("Expenses") }}
{% endblock title %}
{% block content %}
{% if expenses %}
<div class="row mt-4">
<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>
@ -54,4 +57,9 @@
{% endif %}
{% endif %}
</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 %}

View File

@ -4,6 +4,8 @@
{{ _("Services") }}
{% endblock title %}
{% block content %}
{% if services %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class="">{% trans "Services" %}<span class="fas fa-tools text-primary ms-2"></span></h3>
@ -56,4 +58,10 @@
{% endif %}
{% endif %}
</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 %}

View File

@ -4,6 +4,7 @@
{{ _("Bank Accounts") }}
{% endblock title %}
{% block content %}
{% if bank_accounts %}
<div class="row mt-4">
<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>
@ -53,4 +54,9 @@
{% endif %}
{% endif %}
</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 %}

View File

@ -5,7 +5,7 @@
{{ _("Create Bill") }}
{% endblock title %}
{% 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>
<form id="mainForm" method="post" class="needs-validation">
{% csrf_token %}
@ -15,7 +15,7 @@
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</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>
</div>
</form>

View File

@ -10,6 +10,8 @@
</a>
{% endblock %}
{% block content %}
{% if bills %}
<div class="row mt-4">
<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>
@ -85,4 +87,10 @@
</div>
{% endif %}
</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>
{% endblock %}
{% block content %}
{% if accounts%}
<div class="row mt-4">
<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>
@ -196,6 +199,12 @@
</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 %}
{% block customerJS %}
<script>

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="row justify-content-center">
<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">
{% csrf_token %}
<div class="card">
@ -13,7 +13,7 @@
<h5 class="card-title fw-light">Are you sure you want to delete?</h5>
</div>
<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>
<button type="submit" class="btn btn-phoenix-danger">{% trans 'Delete' %}</button>
</div>

View File

@ -4,6 +4,8 @@
{{ _("Journal Entries") }}
{% endblock title %}
{% block content %}
{% if journal_entries %}
<div class="modal fade"
id="confirmModal"
tabindex="-1"
@ -130,5 +132,10 @@
</div>
{% endif %}
</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 %}

View File

@ -27,7 +27,7 @@
{% for transaction in transactions %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<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.code }}</td>
<td class="align-middle product white-space-nowrap text-success">

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="row justify-content-center">
<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">
{% csrf_token %}
<div class="card">
@ -13,7 +13,7 @@
<h5 class="card-title fw-light">{{ ledger_model.get_delete_message }}</h5>
</div>
<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>
</div>
</div>

View File

@ -4,6 +4,8 @@
{{ _("Ledger") }}
{% endblock title %}
{% block content %}
{% if ledgers %}
<div class="row mt-4">
<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>
@ -127,4 +129,9 @@
</div>
{% endif %}
</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 %}

View File

@ -22,8 +22,10 @@
<div class="modal-body p-4">
<p id="deleteModalText"></p>
</div>
<div class="modal-footer flex justify-content-center border-top-0">
<div class="modal-footer flex justify-content-center border-top-0" >
<a id="deleteModalConfirm"
hx-select-oob="#notesTable:outerHTML,#toast-container:outerHTML"
hx-swap="none"
type="button"
class="btn btn-sm btn-phoenix-danger w-100"
href="">{{ _("Delete") }}</a>
@ -31,24 +33,47 @@
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const deleteModal = document.getElementById("deleteModal");
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
const deleteModalMessage = document.getElementById("deleteModalText");
document.querySelectorAll(".delete-btn").forEach(button => {
button.addEventListener("click", function () {
let deleteUrl = this.getAttribute("data-url");
let deleteMessage = this.getAttribute("data-message");
<script>
// Initialize when page loads and after HTMX swaps
document.addEventListener('DOMContentLoaded', initDeleteModals);
document.addEventListener('htmx:afterSwap', initDeleteModals);
confirmDeleteBtn.setAttribute("href", deleteUrl);
confirmDeleteBtn.setAttribute("hx-boost", "true");
confirmDeleteBtn.setAttribute("hx-select-oob", "#notesTable:outerHTML,#toast-container:outerHTML");
confirmDeleteBtn.setAttribute("hx-swap", "none");
confirmDeleteBtn.setAttribute("hx-on::after-request", "$('#deleteModal').modal('hide');");
deleteModalMessage.innerHTML = deleteMessage;
});
});
function initDeleteModals() {
const deleteModal = document.getElementById("deleteModal");
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
const deleteModalMessage = document.getElementById("deleteModalText");
// Clean up previous listeners if any
document.querySelectorAll(".delete-btn").forEach(btn => {
btn.removeEventListener("click", handleDeleteClick);
});
// Add new listeners to all delete buttons
document.querySelectorAll(".delete-btn").forEach(button => {
button.addEventListener("click", handleDeleteClick);
});
function handleDeleteClick() {
if (!deleteModal || !confirmDeleteBtn || !deleteModalMessage) return;
const deleteUrl = this.getAttribute("data-url");
const deleteMessage = this.getAttribute("data-message") || "Are you sure you want to delete this item?";
// Update modal content
confirmDeleteBtn.setAttribute("href", deleteUrl);
deleteModalMessage.textContent = deleteMessage; // Use textContent instead of innerHTML for security
// Process with HTMX if available
if (typeof htmx !== 'undefined') {
htmx.process(confirmDeleteBtn);
}
// Show the modal
/*if (typeof bootstrap !== 'undefined') {
const modal = new bootstrap.Modal(deleteModal);
modal.show();
}*/
}
}
</script>

View File

@ -5,7 +5,7 @@
{% endblock title %}
{% block content %}
<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">
<li class="list-group-item">
<strong>{% trans "CRN" %}:</strong> {{ organization.crn }}

View File

@ -8,6 +8,7 @@
<a class="nav-link active">{% trans 'Organizations' %}</a>
{% endblock %}
{% block content %}
{% if organizations%}
<section class="pt-5 pb-9 ">
<div class="row overflow-x-auto whitespace-nowrap -mx-2 sm:mx-0">
<h2 class="mb-4">
@ -182,4 +183,10 @@
{% endif %}
</div>
</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 %}

View File

@ -1,35 +1,16 @@
<div class="search-box me-2">
<form class="position-relative show" id="search-form">
<input name="q"
hx-get=""
hx-boost="true"
hx-trigger="keyup delay:500ms"
id="search-input"
class="form-control form-control-sm search-input search"
type="search"
aria-label="Search"
placeholder="{{ _("Search") }}..."
placeholder="{{ _('Search') }}..."
value="{{ request.GET.q }}" />
<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>
</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 %}
{% block title %}Purchase Orders - {{ block.super }}{% endblock %}
{% block content %}
{% if purchase_orders %}
<div class="row mt-4">
<!-- Success Message -->
{% if messages %}
@ -118,4 +121,9 @@
{% endif %}
</div>
{% 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 %}

View File

@ -250,12 +250,7 @@
<td class="align-middle text-body-tertiary fw-semibold">{{ item.total }}</td>
</tr>
{% 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">
<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">
@ -275,6 +270,12 @@
</form>
</td>
</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">
<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">
@ -334,39 +335,90 @@
</div>
{% endblock %}
{% block customJS %}
<script>
function calculateTotals() {
const table = document.getElementById('estimate-table');
const rows = table.getElementsByTagName('tbody')[0].rows;
let grandTotal = 0;
<script>
// Initialize when page loads and after HTMX swaps
document.addEventListener('DOMContentLoaded', initEstimateFunctions);
document.addEventListener('htmx:afterSwap', initEstimateFunctions);
for (let row of rows) {
// Ensure the row has the expected number of cells
if (row.cells.length >= 5) {
const quantity = parseFloat(row.cells[2].textContent); // Quantity column
const unitPrice = parseFloat(row.cells[3].textContent); // Unit Price column
function initEstimateFunctions() {
// Initialize calculateTotals if estimate table exists
const estimateTable = document.getElementById('estimate-table');
if (estimateTable) {
calculateTotals();
if (!isNaN(quantity) && !isNaN(unitPrice)) {
const total = quantity * unitPrice;
row.cells[4].textContent = total.toFixed(2); // Populate Total column
grandTotal += total; // Add to grand total
}
// Optional: If you need to recalculate when table content changes
estimateTable.addEventListener('change', calculateTotals);
}
// Initialize form action setter if form exists
const confirmForm = document.getElementById('confirmForm');
if (confirmForm) {
// Remove old event listeners if any
document.querySelectorAll('[data-set-form-action]').forEach(button => {
button.removeEventListener('click', setFormActionHandler);
button.addEventListener('click', setFormActionHandler);
});
}
}
function calculateTotals() {
const table = document.getElementById('estimate-table');
if (!table) return;
try {
const tbody = table.getElementsByTagName('tbody')[0];
if (!tbody) return;
const rows = tbody.rows;
let grandTotal = 0;
for (let row of rows) {
// Skip rows that don't have enough cells
if (row.cells.length < 5) continue;
// Get quantity and unit price
const quantityText = row.cells[2]?.textContent?.trim() || '0';
const unitPriceText = row.cells[3]?.textContent?.trim() || '0';
// Parse values, handling any formatting
const quantity = parseFloat(quantityText.replace(/[^0-9.-]/g, ''));
const unitPrice = parseFloat(unitPriceText.replace(/[^0-9.-]/g, ''));
if (!isNaN(quantity) && !isNaN(unitPrice)) {
const total = quantity * unitPrice;
if (row.cells[4]) {
row.cells[4].textContent = total.toFixed(2);
}
grandTotal += total;
}
}
}
// Display the grand total
document.getElementById('grand-total').textContent = grandTotal.toFixed(2);
// Update grand total display
const grandTotalElement = document.getElementById('grand-total');
if (grandTotalElement) {
grandTotalElement.textContent = grandTotal.toFixed(2);
}
} catch (error) {
console.error('Error calculating totals:', error);
}
}
// Run the function on page load
//window.onload = calculateTotals;
function setFormAction(action) {
// Get the form element
const form = document.getElementById('confirmForm');
// Set the form action with the query parameter
form.action = "{% url 'estimate_mark_as' request.dealer.slug estimate.pk %}?mark=" + action;
function setFormActionHandler(event) {
const action = event.currentTarget.getAttribute('data-set-form-action');
if (action) {
setFormAction(action);
}
</script>
}
function setFormAction(action) {
const form = document.getElementById('confirmForm');
if (!form) return;
const baseUrl = "{% url 'estimate_mark_as' request.dealer.slug estimate.pk %}";
form.action = `${baseUrl}?mark=${encodeURIComponent(action)}`;
// Optional: Submit form immediately after setting action
// form.submit();
}
</script>
{% endblock %}

View File

@ -154,14 +154,14 @@
<div class="card shadow-sm border-0 rounded-3">
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
<h3 class="mb-0 fs-4 text-center">
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
{% trans "Create Quotation" %}<i class="fa-regular fa-file-lines text-primary me-2"></i>
</h3>
</div>
<div class="card-body bg-light-subtle">
<form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
{% csrf_token %}
<div class="row g-3 col-12">
{{ form|crispy }}
@ -202,165 +202,246 @@
<a href="{% url 'estimate_list' request.dealer.slug%}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
function notify(tag,msg){Toast.fire({icon: tag,titleText: msg});}
// Global variables
let Toast;
let customSelectsInitialized = false;
let formInitialized = false;
const customSelects = document.querySelectorAll('.custom-select');
// Initialize when page loads and after HTMX swaps
document.addEventListener('DOMContentLoaded', initPage);
document.addEventListener('htmx:afterSwap', initPage);
customSelects.forEach(select => {
const trigger = select.querySelector('.select-trigger');
const optionsContainer = select.querySelector('.options-container');
const options = select.querySelectorAll('.option');
const nativeSelect = select.querySelector('.native-select');
const selectedValue = select.querySelector('.selected-value');
// Toggle dropdown
trigger.addEventListener('click', (e) => {
e.stopPropagation();
select.classList.toggle('open');
trigger.classList.toggle('active');
// Close other open selects
document.querySelectorAll('.custom-select').forEach(otherSelect => {
if (otherSelect !== select) {
otherSelect.classList.remove('open');
otherSelect.querySelector('.select-trigger').classList.remove('active');
}
});
});
// Handle option selection
options.forEach(option => {
option.addEventListener('click', () => {
const value = option.getAttribute('data-value');
const image = option.getAttribute('data-image');
const text = option.querySelector('span').textContent;
// Update selected display
selectedValue.innerHTML = `
<img src="${image}" alt="${text}">
<span>${text}</span>
`;
// Update native select value
nativeSelect.value = value;
// Mark as selected
options.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
// Close dropdown
select.classList.remove('open');
trigger.classList.remove('active');
// Trigger change event
const event = new Event('change');
nativeSelect.dispatchEvent(event);
});
});
// Close when clicking outside
document.addEventListener('click', () => {
select.classList.remove('open');
trigger.classList.remove('active');
});
// Initialize with native select value
if (nativeSelect.value) {
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
if (selectedOption) {
selectedOption.click();
}
}
});
// Form submission
/*const form = document.getElementById('demo-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(form);
alert(`Selected value: ${formData.get('car')}`);
});*/
document.getElementById('mainForm').addEventListener('submit', async function(e) {
e.preventDefault();
const titleInput = document.querySelector('[name="title"]');
if (titleInput.value.length < 5) {
notify("error", "Customer Estimate Title must be at least 5 characters long.");
return; // Stop form submission
}
// Collect all form data
const formData = {
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]').value,
title: document.querySelector('[name=title]').value,
customer: document.querySelector('[name=customer]').value,
item: [],
quantity: [1],
opportunity_id: "{{opportunity_id}}"
};
// Collect multi-value fields (e.g., item[], quantity[])
document.querySelectorAll('[name="item"]').forEach(input => {
formData.item.push(input.value);
});
console.log(formData);
try {
// Send data to the server using fetch
const response = await fetch("{% url 'estimate_create' request.dealer.slug %}", {
method: 'POST',
headers: {
'X-CSRFToken': formData.csrfmiddlewaretoken,
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
// Parse the JSON response
const data = await response.json();
// Handle the response
if (data.status === "error") {
notify("error", data.message); // Display an error message
} else if (data.status === "success") {
notify("success","Estimate created successfully");
setTimeout(() => {
window.location.assign(data.url); // Redirect to the provided URL
}, 1000);
} else {
notify("error","Unexpected response from the server");
}
} catch (error) {
notify("error", error);
}
});
function initPage() {
initToast();
initCustomSelects();
initFormSubmission();
}
function initToast() {
if (typeof Swal !== 'undefined') {
Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
</script>
{% endblock %}
}
}
function notify(tag, msg) {
if (Toast) {
Toast.fire({icon: tag, titleText: msg});
} else if (typeof Swal !== 'undefined') {
Swal.fire({icon: tag, titleText: msg});
} else {
console.log(`${tag}: ${msg}`);
}
}
function initCustomSelects() {
// Clean up previous listeners if any
if (customSelectsInitialized) {
document.removeEventListener('click', handleDocumentClickForSelects);
}
const customSelects = document.querySelectorAll('.custom-select');
if (!customSelects.length) return;
customSelects.forEach(select => {
const trigger = select.querySelector('.select-trigger');
const options = select.querySelectorAll('.option');
const nativeSelect = select.querySelector('.native-select');
const selectedValue = select.querySelector('.selected-value');
// Clone elements to remove old event listeners
if (trigger) trigger.replaceWith(trigger.cloneNode(true));
options.forEach(option => option.replaceWith(option.cloneNode(true)));
// Get fresh references
const newTrigger = select.querySelector('.select-trigger');
const newOptions = select.querySelectorAll('.option');
const newNativeSelect = select.querySelector('.native-select');
const newSelectedValue = select.querySelector('.selected-value');
// Add new listeners
if (newTrigger) {
newTrigger.addEventListener('click', (e) => handleSelectTriggerClick(e, select));
}
newOptions.forEach(option => {
option.addEventListener('click', () => handleOptionClick(option, select));
});
// Initialize with current value
if (newNativeSelect?.value && newSelectedValue) {
initializeSelectedOption(select);
}
});
// Add document click handler
document.addEventListener('click', handleDocumentClickForSelects);
customSelectsInitialized = true;
}
function handleSelectTriggerClick(e, select) {
e.stopPropagation();
const trigger = select.querySelector('.select-trigger');
// Toggle current select
select.classList.toggle('open');
trigger.classList.toggle('active');
// Close other open selects
document.querySelectorAll('.custom-select').forEach(otherSelect => {
if (otherSelect !== select) {
otherSelect.classList.remove('open');
const otherTrigger = otherSelect.querySelector('.select-trigger');
if (otherTrigger) otherTrigger.classList.remove('active');
}
});
}
function handleOptionClick(option, select) {
const value = option.getAttribute('data-value');
const image = option.getAttribute('data-image');
const text = option.querySelector('span')?.textContent || '';
const nativeSelect = select.querySelector('.native-select');
const selectedValue = select.querySelector('.selected-value');
const options = select.querySelectorAll('.option');
// Update display
if (selectedValue) {
selectedValue.innerHTML = `
<img src="${image}" alt="${text}">
<span>${text}</span>
`;
}
// Update native select
if (nativeSelect) {
nativeSelect.value = value;
// Trigger change event
const event = new Event('change');
nativeSelect.dispatchEvent(event);
}
// Update selected state
options.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
// Close dropdown
select.classList.remove('open');
const trigger = select.querySelector('.select-trigger');
if (trigger) trigger.classList.remove('active');
}
function handleDocumentClickForSelects() {
document.querySelectorAll('.custom-select').forEach(select => {
select.classList.remove('open');
const trigger = select.querySelector('.select-trigger');
if (trigger) trigger.classList.remove('active');
});
}
function initializeSelectedOption(select) {
const nativeSelect = select.querySelector('.native-select');
const selectedOption = select.querySelector(`.option[data-value="${nativeSelect.value}"]`);
if (selectedOption) {
handleOptionClick(selectedOption, select);
}
}
function initFormSubmission() {
const form = document.getElementById('mainForm');
if (!form) return;
// Remove old listener if exists
if (formInitialized) {
form.removeEventListener('submit', handleFormSubmit);
}
form.addEventListener('submit', handleFormSubmit);
formInitialized = true;
}
async function handleFormSubmit(e) {
e.preventDefault();
const form = e.target;
// Validate title
const titleInput = form.querySelector('[name="title"]');
if (titleInput && titleInput.value.length < 5) {
notify("error", "Customer Estimate Title must be at least 5 characters long.");
return;
}
// Prepare form data
const formData = {
csrfmiddlewaretoken: document.querySelector('[name=csrfmiddlewaretoken]')?.value,
title: form.querySelector('[name=title]')?.value,
customer: form.querySelector('[name=customer]')?.value,
item: [],
quantity: [1],
opportunity_id: "{{opportunity_id}}"
};
// Collect items
form.querySelectorAll('[name="item"]').forEach(input => {
if (input.value) formData.item.push(input.value);
});
try {
// Submit form
const response = await fetch("{% url 'estimate_create' request.dealer.slug %}", {
method: 'POST',
headers: {
'X-CSRFToken': formData.csrfmiddlewaretoken,
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
// Handle response
const data = await response.json();
handleFormResponse(data);
} catch (error) {
console.error("Form submission error:", error);
notify("error", "An error occurred while submitting the form");
}
}
function handleFormResponse(data) {
if (!data) {
notify("error", "No response from server");
return;
}
if (data.status === "error") {
notify("error", data.message || "An error occurred");
} else if (data.status === "success") {
notify("success", data.message || "Estimate created successfully");
if (data.url) {
setTimeout(() => window.location.assign(data.url), 1000);
}
} else {
notify("error", "Unexpected response from the server");
}
}
</script>
{% endblock %}

View File

@ -4,6 +4,7 @@
{{ _("Quotations") }}
{% endblock title %}
{% block content %}
{% if estimates %}
<div class="row g-3 mt-4 mb-4">
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
@ -30,31 +31,31 @@
</tr>
</thead>
<tbody class="list">
{% for extra in staff_estimates %}
{% for estimate in staff_estimates %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="align-middle product white-space-nowrap py-0 px-1">{{ extra.content_object.estimate_number }}</td>
<td class="align-middle product white-space-nowrap">{{ extra.content_object.customer.customer_name }}</td>
<td class="align-middle product white-space-nowrap py-0 px-1">{{ estimate.estimate_number }}</td>
<td class="align-middle product white-space-nowrap">{{ estimate.customer.customer_name }}</td>
<td class="align-middle product white-space-nowrap">
{% if extra.content_object.status == 'draft' %}
{% if estimate.status == 'draft' %}
<span class="badge badge-phoenix badge-phoenix-warning">{% trans "Draft" %}</span>
{% elif extra.content_object.status == 'in_review' %}
{% elif estimate.status == 'in_review' %}
<span class="badge badge-phoenix badge-phoenix-info">{% trans "In Review" %}</span>
{% elif extra.content_object.status == 'approved' %}
{% elif estimate.status == 'approved' %}
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Approved" %}</span>
{% elif extra.content_object.status == 'declined' %}
{% elif estimate.status == 'declined' %}
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Declined" %}</span>
{% elif extra.content_object.status == 'canceled' %}
{% elif estimate.status == 'canceled' %}
<span class="badge badge-phoenix badge-phoenix-danger">{% trans "Canceled" %}</span>
{% elif extra.content_object.status == 'completed' %}
{% elif estimate.status == 'completed' %}
<span class="badge badge-phoenix badge-phoenix-success">{% trans "Completed" %}</span>
{% elif extra.content_object.status == 'void' %}
{% elif estimate.status == 'void' %}
<span class="badge badge-phoenix badge-phoenix-secondary">{% trans "Void" %}</span>
{% endif %}
</td>
<td class="align-middle product white-space-nowrap">{{ extra.content_object.get_status_action_date }}</td>
<td class="align-middle product white-space-nowrap">{{ extra.content_object.created }}</td>
<td class="align-middle product white-space-nowrap">{{ estimate.get_status_action_date }}</td>
<td class="align-middle product white-space-nowrap">{{ estimate.created }}</td>
<td class="align-middle product white-space-nowrap">
<a href="{% url 'estimate_detail' request.dealer.slug extra.content_object.pk %}"
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
class="btn btn-sm btn-phoenix-success">
<i class="fa-regular fa-eye me-1"></i>
{% trans "view"|capfirst %}
@ -75,4 +76,10 @@
</div>
{% endif %}
</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 %}

View File

@ -3,206 +3,23 @@
{% load static %}
{% load i18n %}
{% block title %}
<h1>{% trans 'Sale Order' %}</h1>
{% endblock %}
{{ _("Sale Order") }}
{% endblock title %}
{% block content %}
<link rel="stylesheet" href="{% static 'flags/sprite.css' %}" />
<div class="row">
<div class="row mb-3">
<div class="col-sm-6 col-md-8">
<div class="d-sm-flex justify-content-between">
<h3 class="mb-3">
{% if customer.created %}
{{ _('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>
<div class="row">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ _("Sale Order") }}</div>
<div class="card-body">
<form method="post" action="{% url 'create_sale_order' request.dealer.slug estimate.pk %}">
{% csrf_token %}
<div class="col-md-6">
<label for="id_estimate" class="form-label">{% trans "Quotation" %}</label>
<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|crispy }}
<button type="submit" class="btn btn-phoenix-primary">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!---->
{% endblock content %}
{% endblock content %}

View File

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

View File

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

View File

@ -5,6 +5,9 @@
{% trans "Staffs" %}
{% endblock title %}
{% block content %}
{%if users %}
<section class="">
<div class="row mt-4">
<div class="col-auto">
@ -21,7 +24,7 @@
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">
{{ _("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>
<button class="btn-close"
type="button"
@ -86,4 +89,10 @@
</div>
</div>
</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 %}

View File

@ -6,6 +6,8 @@
{% endblock title %}
{% block vendors %}<a class="nav-link active">{{ _("Vendors") |capfirst }}</a>{% endblock %}
{% block content %}
{% if vendors %}
<div class="row mt-4">
<div class="d-flex justify-content-between mb-2">
<h3 class="">
@ -161,4 +163,9 @@
{% endif %}
</div>
{% 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 %}