Compare commits

..

No commits in common. "953157ea15c7ccd3b7db1a798704e3ede11dd704" and "5a24f572f602f406d95a7f661799ed7e3a21819a" have entirely different histories.

58 changed files with 710 additions and 490 deletions

View File

@ -16,7 +16,7 @@ from django_ledger.forms.invoice import (
) )
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase
from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
from django_ledger.forms.journal_entry import ( from django_ledger.forms.journal_entry import (
JournalEntryModelCreateForm as JournalEntryModelCreateFormBase, JournalEntryModelCreateForm as JournalEntryModelCreateFormBase,
) )
@ -2102,14 +2102,3 @@ class VatRateForm(forms.ModelForm):
class Meta: class Meta:
model = VatRate model = VatRate
fields = ["rate"] fields = ["rate"]
class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField(
label="New Password",
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'New Password'})
)
new_password2 = forms.CharField(
label="Confirm New Password",
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm New Password'})
)

View File

@ -1184,7 +1184,7 @@ class Dealer(models.Model, LocalizedNameMixin):
blank=True, blank=True,
null=True, null=True,
verbose_name=_("Logo"), verbose_name=_("Logo"),
default="default-image/dealer.png", default="user-logo.png",
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -1288,7 +1288,7 @@ class Staff(models.Model, LocalizedNameMixin):
max_length=200, blank=True, null=True, verbose_name=_("Address") max_length=200, blank=True, null=True, verbose_name=_("Address")
) )
logo = models.ImageField( logo = models.ImageField(
upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="default-image/sales_person.png" upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="user-logo.jpg"
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",
@ -1511,7 +1511,7 @@ class Customer(models.Model):
) )
active = models.BooleanField(default=True, verbose_name=_("Active")) active = models.BooleanField(default=True, verbose_name=_("Active"))
image = models.ImageField( image = models.ImageField(
upload_to="customers/", blank=True, null=True, verbose_name=_("Image"),default="default-image/customer.png" upload_to="customers/", blank=True, null=True, verbose_name=_("Image")
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="image", source="image",
@ -2481,7 +2481,7 @@ class Vendor(models.Model, LocalizedNameMixin):
email = models.EmailField(max_length=255, verbose_name=_("Email Address")) email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, verbose_name=_("Address")) address = models.CharField(max_length=200, verbose_name=_("Address"))
logo = models.ImageField( logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="default-image/vendor.png" upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="user-logo.jpg"
) )
thumbnail = ImageSpecField( thumbnail = ImageSpecField(
source="logo", source="logo",

View File

@ -1,4 +1,3 @@
from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from django.urls import reverse from django.urls import reverse
from inventory.tasks import create_coa_accounts, create_make_accounts from inventory.tasks import create_coa_accounts, create_make_accounts
@ -25,7 +24,6 @@ from . import models
from django.utils.timezone import now from django.utils.timezone import now
from django.db import transaction from django.db import transaction
from django_q.tasks import async_task from django_q.tasks import async_task
from plans.models import UserPlan
from plans.signals import order_completed, activate_user_plan from plans.signals import order_completed, activate_user_plan
# logging # logging
@ -1215,3 +1213,9 @@ def bill_model_after_approve_notification(sender, instance, created, **kwargs):
), ),
), ),
) )
def handle_upgrade(sender, order, **kwargs):
logger.info(f"User {order.user} upgraded to {order.plan}")
order_completed.connect(handle_upgrade)

View File

@ -1,4 +1,4 @@
import base64
import logging import logging
from plans.models import Plan from plans.models import Plan
from django.conf import settings from django.conf import settings
@ -8,11 +8,10 @@ from django_q.tasks import async_task
from django.core.mail import send_mail from django.core.mail import send_mail
from appointment.models import StaffMember from appointment.models import StaffMember
from django.utils.translation import activate from django.utils.translation import activate
from django.core.files.base import ContentFile
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from allauth.account.models import EmailAddress from allauth.account.models import EmailAddress
from inventory.models import DealerSettings, Dealer
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from inventory.models import DealerSettings, Dealer
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission

View File

@ -517,11 +517,6 @@ urlpatterns = [
views.UserDeleteview, views.UserDeleteview,
name="user_delete", name="user_delete",
), ),
path(
"<slug:dealer_slug>/user/<int:user_pk>/password_reset/",
views.staff_password_reset_view,
name="staff_password_reset",
),
path( path(
"<slug:dealer_slug>/group/create/", "<slug:dealer_slug>/group/create/",
views.GroupCreateView.as_view(), views.GroupCreateView.as_view(),

View File

@ -1,6 +1,4 @@
# Standard # Standard
from django.core.files.base import ContentFile
import base64
import os import os
import re import re
import io import io
@ -17,7 +15,7 @@ from random import randint
from decimal import Decimal from decimal import Decimal
from io import TextIOWrapper from io import TextIOWrapper
from django.apps import apps from django.apps import apps
from datetime import datetime, timedelta from datetime import datetime
from calendar import month_name from calendar import month_name
from pyzbar.pyzbar import decode from pyzbar.pyzbar import decode
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
@ -303,20 +301,18 @@ def dealer_signup(request):
:rtype: Union[django.http.HttpResponse, django.http.JsonResponse] :rtype: Union[django.http.HttpResponse, django.http.JsonResponse]
""" """
if request.method == "POST": if request.method == "POST":
try: data = json.loads(request.body)
data = json.loads(request.body)
email = data.get("email") email = data.get("email")
password = data.get("password") password = data.get("password")
password_confirm = data.get("confirm_password") password_confirm = data.get("confirm_password")
name = data.get("name") name = data.get("name")
arabic_name = data.get("arabic_name") arabic_name = data.get("arabic_name")
phone = data.get("phone_number") phone = data.get("phone_number")
crn = data.get("crn") crn = data.get("crn")
vrn = data.get("vrn") vrn = data.get("vrn")
address = data.get("address") address = data.get("address")
except json.JSONDecodeError:
pass
if User.objects.filter(email=email).exists(): if User.objects.filter(email=email).exists():
return JsonResponse({"error": _("Email already exists")}, status=400) return JsonResponse({"error": _("Email already exists")}, status=400)
if not re.match( if not re.match(
@ -422,11 +418,11 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
entity = dealer.entity entity = dealer.entity
total_cars = models.Car.objects.filter(dealer=dealer).count()
qs = models.Car.objects.filter(dealer=dealer) total_reservations = models.CarReservation.objects.filter(
total_cars = qs.count() reserved_until__gte=timezone.now()
).count()
stats = models.CarFinance.objects.filter(car__dealer=dealer).aggregate( stats = models.CarFinance.objects.aggregate(
total_cost_price=Sum("cost_price"), total_cost_price=Sum("cost_price"),
total_selling_price=Sum("selling_price"), total_selling_price=Sum("selling_price"),
) )
@ -443,48 +439,72 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
canceled_leads = models.Lead.objects.filter( canceled_leads = models.Lead.objects.filter(
dealer=dealer, status=models.Status.UNQUALIFIED dealer=dealer, status=models.Status.UNQUALIFIED
).count() ).count()
available_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.AVAILABLE
).count()
sold_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.SOLD
).count()
reserved_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.RESERVED
).count()
hold_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.HOLD
).count()
damaged_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.DAMAGED
).count()
transfer_cars = models.Car.objects.filter(
dealer=dealer, status=models.CarStatusChoices.TRANSFER
).count()
try:
reserved_percentage = reserved_cars / total_cars * 100
except ZeroDivisionError as e:
print(f"error: {e}")
try:
sold_percentage = sold_cars / total_cars * 100
except ZeroDivisionError as e:
print(f"error: {e}")
car_status_qs = qs.values("status").annotate(count=Count("status")) qs = (
car_status = {status: count for status, count in car_status_qs} models.Car.objects.values("id_car_make__name")
.annotate(count=Count("id"))
car_by_make_qs = qs.values("id_car_make__name").annotate(count=Count("id")) .order_by("id_car_make__name")
car_by_make = list(car_by_make_qs) )
car_by_make = list(qs)
total_activity = models.UserActivityLog.objects.filter(user=dealer.user).count()
staff = models.Staff.objects.filter(dealer=dealer).count()
total_leads = models.Lead.objects.filter(dealer=dealer).count()
invoices = entity.get_invoices().count()
customers = entity.get_customers().count()
purchase_orders = entity.get_purchase_orders().count()
estimates = entity.get_estimates().count()
context["dealer"] = dealer context["dealer"] = dealer
context["total_activity"] = models.UserActivityLog.objects.filter( context["total_activity"] = total_activity
user=dealer.user
).count()
context["total_cars"] = total_cars context["total_cars"] = total_cars
context["total_reservations"] = models.CarReservation.objects.filter( context["total_reservations"] = total_reservations
reserved_until__gte=timezone.now()
).count()
context["total_cost_price"] = total_cost_price context["total_cost_price"] = total_cost_price
context["total_selling_price"] = total_selling_price context["total_selling_price"] = total_selling_price
context["total_profit"] = total_profit context["total_profit"] = total_profit
context["new_leads"] = new_leads context["new_leads"] = new_leads
context["pending_leads"] = pending_leads context["pending_leads"] = pending_leads
context["canceled_leads"] = canceled_leads context["canceled_leads"] = canceled_leads
context["reserved_percentage"] = reserved_percentage
context["sold_percentage"] = sold_percentage
context["available_cars"] = available_cars
context["sold_cars"] = sold_cars
context["reserved_cars"] = reserved_cars
context["hold_cars"] = hold_cars
context["damaged_cars"] = damaged_cars
context["transfer_cars"] = transfer_cars
context["car"] = json.dumps(car_by_make) context["car"] = json.dumps(car_by_make)
context["available_cars"] = car_status.get( context["customers"] = customers
models.CarStatusChoices.AVAILABLE, 0 context["staff"] = staff
) context["total_leads"] = total_leads
context["sold_cars"] = car_status.get(models.CarStatusChoices.SOLD, 0) context["invoices"] = invoices
context["reserved_cars"] = car_status.get( context["estimates"] = estimates
models.CarStatusChoices.RESERVED, 0 context["purchase_orders"] = purchase_orders
)
context["hold_cars"] = car_status.get(models.CarStatusChoices.HOLD, 0)
context["damaged_cars"] = car_status.get(
models.CarStatusChoices.DAMAGED, 0
)
context["transfer_cars"] = car_status.get(
models.CarStatusChoices.TRANSFER, 0
)
context["customers"] = entity.get_customers().count()
context["staff"] = models.Staff.objects.filter(dealer=dealer).count()
context["total_leads"] = models.Lead.objects.filter(dealer=dealer).count()
context["invoices"] = entity.get_invoices().count()
context["estimates"] = entity.get_estimates().count()
context["purchase_orders"] = entity.get_purchase_orders().count()
return context return context
@ -635,7 +655,7 @@ class CarCreateView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["vendor_exists"] = dealer.vendors.filter(active=True).exists() context["vendor_exists"] = dealer.vendors.exists()
return context return context
@ -1605,7 +1625,7 @@ class CarUpdateView(
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
form.fields["vendor"].queryset = dealer.vendors.filter(active=True) form.fields["vendor"].queryset = dealer.vendors.all()
return form return form
@ -3396,7 +3416,6 @@ class UserCreateView(
success_url = reverse_lazy("user_list") success_url = reverse_lazy("user_list")
success_message = _("User created successfully") success_message = _("User created successfully")
permission_required = ["inventory.add_staff"] permission_required = ["inventory.add_staff"]
staff_pk = None
def get_form(self, form_class=None): def get_form(self, form_class=None):
form = super().get_form(form_class) form = super().get_form(form_class)
@ -3406,6 +3425,13 @@ class UserCreateView(
def form_valid(self, form): def form_valid(self, form):
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"]) dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
# quota_dict = get_user_quota(dealer.user)
# allowed_users = quota_dict.get("Users")
# if allowed_users is None:
# messages.error(self.request, _("The user quota for staff members is not defined. Please contact support"))
# return self.form_invalid(form)
if dealer.is_staff_exceed_quota_limit: if dealer.is_staff_exceed_quota_limit:
messages.error( messages.error(
self.request, self.request,
@ -3437,14 +3463,12 @@ class UserCreateView(
staff.staff_member = staff_member staff.staff_member = staff_member
staff.dealer = dealer staff.dealer = dealer
staff.save() staff.save()
self.staff_pk = staff.pk
for customgroup in form.cleaned_data["group"]: for customgroup in form.cleaned_data["group"]:
staff.add_group(customgroup.group) staff.add_group(customgroup.group)
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk]) return reverse_lazy("user_list", args=[self.request.dealer.slug])
# return reverse_lazy("user_list", args=[self.request.dealer.slug])
class UserUpdateView( class UserUpdateView(
@ -6124,7 +6148,6 @@ def lead_tracking(request, dealer_slug):
@permission_required("inventory.change_lead", raise_exception=True) @permission_required("inventory.change_lead", raise_exception=True)
def update_lead_actions(request, dealer_slug): def update_lead_actions(request, dealer_slug):
# get the user info for logging # get the user info for logging
user_username = ( user_username = (
request.user.username if request.user.is_authenticated else "anonymous" request.user.username if request.user.is_authenticated else "anonymous"
) )
@ -9368,7 +9391,9 @@ def payment_callback(request, dealer_slug):
history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first() history = models.PaymentHistory.objects.filter(transaction_id=payment_id).first()
payment_status = request.GET.get("status") payment_status = request.GET.get("status")
order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW order = Order.objects.filter(user=dealer.user, status=1).first() # Status 1 = NEW
if payment_status == "paid": if payment_status == "paid":
# Get or create billing info (optional step)
billing_info, created = BillingInfo.objects.get_or_create( billing_info, created = BillingInfo.objects.get_or_create(
user=dealer.user, user=dealer.user,
defaults={ defaults={
@ -9382,20 +9407,14 @@ def payment_callback(request, dealer_slug):
) )
try: try:
order.complete_order() # COMPLETE THE ORDER - This handles plan activation/upgrade
user = order.user order.complete_order() # Critical step: activates the plan
pricing = order.get_plan_pricing().pricing
logger.info(f"Processing order completion for {user} - upgrading to {order.plan}")
user.userplan.plan = order.plan
user.userplan.expire = datetime.now() + timedelta(days=pricing.period)
user.userplan.save()
user.save()
logger.info(f"User {user} upgraded to {order.plan} plan successfully")
# Update payment history
history.status = "paid" history.status = "paid"
history.save() history.save()
# Retrieve invoice
invoice = order.get_invoices().first() invoice = order.get_invoices().first()
return render( return render(
request, request,
@ -9404,6 +9423,7 @@ def payment_callback(request, dealer_slug):
) )
except Exception as e: except Exception as e:
# Handle activation errors (log, notify admin, etc.)
logger.error(f"Plan activation failed: {str(e)}") logger.error(f"Plan activation failed: {str(e)}")
history.status = "failed" history.status = "failed"
history.save() history.save()
@ -9414,6 +9434,7 @@ def payment_callback(request, dealer_slug):
history.save() history.save()
return render(request, "payment_failed.html", {"message": message}) return render(request, "payment_failed.html", {"message": message})
# Handle unexpected status
return render(request, "payment_failed.html", {"message": "Unknown payment status"}) return render(request, "payment_failed.html", {"message": "Unknown payment status"})
# def payment_callback(request, dealer_slug): # def payment_callback(request, dealer_slug):
# message = request.GET.get("message") # message = request.GET.get("message")
@ -9664,7 +9685,6 @@ def update_schedule(request, dealer_slug, pk):
@permission_required("inventory.add_notes", raise_exception=True) @permission_required("inventory.add_notes", raise_exception=True)
def add_note(request, dealer_slug, content_type, slug): def add_note(request, dealer_slug, content_type, slug):
# Get user information for logging # Get user information for logging
print("hi")
user_username = ( user_username = (
request.user.username if request.user.is_authenticated else "anonymous" request.user.username if request.user.is_authenticated else "anonymous"
) )
@ -9723,7 +9743,6 @@ def add_note(request, dealer_slug, content_type, slug):
@permission_required("inventory.change_notes", raise_exception=True) @permission_required("inventory.change_notes", raise_exception=True)
def update_note(request, dealer_slug, pk): def update_note(request, dealer_slug, pk):
note = get_object_or_404(models.Notes, pk=pk) note = get_object_or_404(models.Notes, pk=pk)
print(note)
lead = get_object_or_404(models.Lead, pk=note.content_object.id) lead = get_object_or_404(models.Lead, pk=note.content_object.id)
dealer = get_object_or_404(models.Dealer, slug=dealer_slug) dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
if request.method == "POST": if request.method == "POST":
@ -10533,7 +10552,7 @@ def upload_cars(request, dealer_slug, pk=None):
return response return response
form = forms.CSVUploadForm() form = forms.CSVUploadForm()
form.fields["vendor"].queryset = dealer.vendors.filter(active=True).all() form.fields["vendor"].queryset = dealer.vendors.all()
return render( return render(
request, request,
@ -10731,21 +10750,3 @@ def car_sale_report_csv_export(request,dealer_slug):
]) ])
return response return response
@login_required
# @permission_required('inventory.view_staff')
def staff_password_reset_view(request, dealer_slug, user_pk):
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
staff = models.Staff.objects.filter(dealer=dealer, pk=user_pk).first()
if request.method == 'POST':
form = forms.CustomSetPasswordForm(staff.user, request.POST)
if form.is_valid():
form.save()
messages.success(request, _('Your password has been set. You may go ahead and log in now.'))
return redirect('user_detail',dealer_slug=dealer_slug,slug=staff.slug)
else:
messages.error(request, _('Invalid password. Please try again.'))
form = forms.CustomSetPasswordForm(staff.user)
return render(request, 'users/user_password_reset.html', {'form': form})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 939 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 619 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,389 @@
{% extends "welcome_base.html" %}
{% load crispy_forms_filters %}
{% load i18n static %}
{% block content %}
<section class="main my-2">
<div class="container" style="max-width:60rem;">
<div class="row form-container" id="form-container">
<div class="col-12 ">
<a class="d-flex flex-center text-decoration-none mb-4"
href="{% url 'home' %}">
<div class="d-flex align-items-center fw-bolder fs-3 d-inline-block">
<img class="d-dark-none"
src="{% static 'images/logos/logo-d.png' %}"
alt="{% trans 'home' %}"
width="58" />
<img class="d-light-none"
src="{% static 'images/logos/logo.png' %}"
alt="{% trans 'home' %}"
width="58" />
</div>
</a>
<div class="text-center">
<h3 class="text-body-highlight">{% trans 'Sign Up' %}</h3>
<p class="text-body-tertiary fs-9">{% trans 'Create your account today' %}</p>
</div>
<div data-signals="{ form1:{email:'',password:'',confirm_password:''}, form2:{name:'',arabic_name:'',phone_number:''}, form3:{crn:'',vrn:'',address:''}, form1_valid:true, form2_valid:true, form3_valid:true, email_valid:true, password_valid:true, phone_number_valid:true }"
class="card theme-wizard"
data-theme-wizard="data-theme-wizard">
<div class="card-header pt-3 pb-2 ">
<ul class="nav justify-content-between nav-wizard nav-wizard-success"
role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active fw-semibold"
href="#bootstrap-wizard-validation-tab1"
data-bs-toggle="tab"
data-wizard-step="1"
aria-selected="true"
role="tab">
<div class="text-center d-inline-block">
<span class="nav-item-circle-parent"><span class="nav-item-circle"><span class="fa fa-lock"></span></span></span><span class="d-none d-md-block mt-1 fs-9">{% trans 'Access' %}</span>
</div>
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link fw-semibold"
href="#bootstrap-wizard-validation-tab2"
data-bs-toggle="tab"
data-wizard-step="2"
aria-selected="false"
tabindex="-1"
role="tab">
<div class="text-center d-inline-block">
<span class="nav-item-circle-parent"><span class="nav-item-circle"><span class="fa fa-user"></span></span></span><span class="d-none d-md-block mt-1 fs-9">{% trans 'Account' %}</span>
</div>
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link fw-semibold"
href="#bootstrap-wizard-validation-tab3"
data-bs-toggle="tab"
data-wizard-step="3"
aria-selected="false"
tabindex="-1"
role="tab">
<div class="text-center d-inline-block">
<span class="nav-item-circle-parent"><span class="nav-item-circle">
<svg class="fa fa-file-lines">
</svg>
</span></span><span class="d-none d-md-block mt-1 fs-9">{% trans 'Extra' %}</span>
</div>
</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link fw-semibold"
href="#bootstrap-wizard-validation-tab4"
data-bs-toggle="tab"
data-wizard-step="4"
aria-selected="false"
tabindex="-1"
role="tab">
<div class="text-center d-inline-block">
<span class="nav-item-circle-parent"><span class="nav-item-circle"><span class="fa fa-check"></span></span></span><span class="d-none d-md-block mt-1 fs-9">{% trans 'Done' %}</span>
</div>
</a>
</li>
</ul>
</div>
<div class="card-body pt-4 pb-0">
<div class="tab-content" data-signals-current_form="1">
<div class="tab-pane active"
role="tabpanel"
aria-labelledby="bootstrap-wizard-validation-tab1"
id="bootstrap-wizard-validation-tab1">
<form class="needs-validation"
id="wizardValidationForm1"
novalidate="novalidate"
data-wizard-form="1"
data-ref-f1>
<div class="mb-3">
<label for="email"
data-class="{'text-danger':!$email_valid}"
class="form-label">
{% trans "Email" %}
<span data-show="!$email_valid" class="text-danger">*</span>
</label>
<input data-on-input="$email_valid = validateEmail($form1.email)"
data-on-blur="$email_valid = validateEmail($form1.email)"
data-bind-form1.email
data-class="{'is-invalid': !$email_valid , 'is-valid': ($email_valid && $form1.email)}"
type="email"
class="form-control"
id="email"
name="email"
required>
<div class="invalid-feedback" data-show="!$email_valid">{% trans "Please enter a valid email address" %}</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">{% trans "Password" %}</label>
<input data-bind-form1.password
type="password"
data-on-input="$password_valid = validatePassword($form1.password,$form1.confirm_password)"
data-on-blur="$password_valid = validatePassword($form1.password,$form1.confirm_password)"
class="form-control"
data-class="{'is-invalid':($form1.password.length && $form1.password.length < 8),'is-valid':$form1.password.length > 8 }"
id="password"
name="password"
required>
<div class="invalid-feedback" data-show="!$password_valid">
{% trans "Password does not match. or length is less than 8 characters." %}
</div>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">{% trans "Confirm Password" %}</label>
<span class="text-danger" data-show="!$password_valid">*</span>
<input data-bind-form1.confirm_password
data-on-input="$password_valid = validatePassword($form1.password,$form1.confirm_password)"
data-on-blur="$password_valid = validatePassword($form1.password,$form1.confirm_password)"
type="password"
class="form-control"
data-class="{'is-invalid':!$password_valid,'is-valid':($password_valid&& $form1.confirm_password)}"
id="confirm_password"
name="confirm_password"
required>
<div class="invalid-feedback" data-show="!$password_valid">
{% trans "Password does not match. or length is less than 8 characters." %}
</div>
</div>
</form>
</div>
<div class="tab-pane"
role="tabpanel"
aria-labelledby="bootstrap-wizard-validation-tab2"
id="bootstrap-wizard-validation-tab2">
<form class="needs-validation"
id="wizardValidationForm2"
novalidate="novalidate"
data-wizard-form="2"
data-ref-f2>
<div class="mb-3">
<label for="name" class="form-label">{% trans "Name" %}</label>
<input data-bind-form2.name
type="text"
class="form-control"
id="name"
name="name"
required>
</div>
<div class="mb-3">
<label for="arabic_name" class="form-label">{% trans "Arabic Name" %}</label>
<input data-bind-form2.arabic_name
type="text"
class="form-control"
id="arabic_name"
name="arabic_name"
required>
</div>
<div class="mb-3">
<label for="phone_number" class="form-label">{% trans "Phone Number" %}</label>
<span data-show="!$phone_number_valid" class="text-danger">*</span>
<input data-bind-form2.phone_number
type="tel"
data-class="{'is-invalid':!$phone_number_valid}"
class="form-control"
id="phone_number"
name="phone_number"
required
data-on-input="$phone_number_valid = validate_sa_phone_number($form2.phone_number)">
<div class="invalid-feedback" data-show="!$phone_number_valid">{% trans "Please enter a valid phone number" %}</div>
</div>
</form>
</div>
<div class="tab-pane"
role="tabpanel"
aria-labelledby="bootstrap-wizard-validation-tab3"
id="bootstrap-wizard-validation-tab3">
<form class="needs-validation"
id="wizardValidationForm3"
novalidate="novalidate"
data-wizard-form="3"
data-ref-f3>
<div class="mb-3">
<label for="crn" class="form-label">{% trans "CRN" %}</label>
<input data-bind-form3.crn
type="text"
class="form-control"
id="crn"
name="crn"
required>
</div>
<div class="mb-3">
<label for="vrn" class="form-label">{% trans "VRN" %}</label>
<input data-bind-form3.vrn
type="text"
class="form-control"
id="vrn"
name="vrn"
required>
</div>
<div class="mb-3">
<label for="address" class="form-label">{% trans "Address" %}</label>
<textarea data-bind-form3.address
class="form-control"
id="address"
name="address"
required></textarea>
</div>
</form>
</div>
<div class="tab-pane"
role="tabpanel"
aria-labelledby="bootstrap-wizard-validation-tab4"
id="bootstrap-wizard-validation-tab4">
<div class="row flex-center pb-8 pt-4 gx-3 gy-4">
<div class="col-12 col-sm-auto">
<div class="text-center text-sm-start">
<img class="d-dark-none"
src="{% static 'images/spot-illustrations/38.webp' %}"
alt=""
width="220">
<img class="d-light-none"
src="{% static 'images/spot-illustrations/dark_38.webp' %}"
alt=""
width="220">
</div>
</div>
<div class="col-12 col-sm-auto">
<div class="text-center text-sm-start">
<h5 class="mb-3">{% trans 'You are all set!' %}</h5>
<p class="text-body-emphasis fs-9">
{% trans 'Now you can access your account' %}
<br>
{% trans 'anytime' %} {% trans 'anywhere' %}
</p>
<button data-on-click="sendFormData()"
class="btn btn-primary px-6"
id='submit_btn'>{% trans 'Submit' %}</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div data-computed-form1_valid="validatePassword($form1.password,$form1.confirm_password) && validateEmail($form1.email)"
class="card-footer border-top-0"
data-wizard-footer="data-wizard-footer">
<div class="d-flex pager wizard list-inline mb-0">
<button class="d-none btn btn-link ps-0"
type="button"
data-wizard-prev-btn="data-wizard-prev-btn">{% trans 'Previous' %}</button>
<div class="flex-1 text-end">
<button data-attr-disabled="!$form1_valid"
data-attr-disabled="!$phone_number_valid"
class="btn btn-phoenix-primary px-6 px-sm-6 next"
type="button"
id="next_btn"
data-wizard-next-btn="data-wizard-next-btn">{% trans 'Next' %}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="pt-lg-0 pt-xl-8">
{% include 'footer.html' %}
</section>
<script src="{% static 'js/phoenix.js' %}"></script>
{% endblock content %}
{% block customJS %}
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/sweetalert2.all.min.js' %}"></script>
<script type="module"
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script>
<script>
function validatePassword(password, confirmPassword) {
return password === confirmPassword && password.length > 7 && password !== '';
}
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email) && email !== '';
}
function validateform2(name,arabic_name,phone_number) {
if (name === '' || arabic_name === '' || phone_number === '' || phone_number.length < 10 || !phone_number.startsWith('056')) {
return false;
}
return true
}
function validate_sa_phone_number(phone_number) {
const phone_numberRegex = /^056[0-9]{7}$/;
return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
}
function getAllFormData() {
const forms = document.querySelectorAll('.needs-validation');
const formData = {};
forms.forEach(form => {
const fields = form.querySelectorAll('input,textarea,select');
fields.forEach(field => {
formData[field.name] = field.value;
});
});
return formData;
}
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
});
}
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 sendFormData() {
const formData = getAllFormData();
const url = "{% url 'account_signup' %}";
const csrftoken = getCookie('csrftoken');
try {
showLoading();
const response = await fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': '{{csrf_token}}',
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
hideLoading();
const data = await response.json();
if (response.ok) {
notify("success","Account created successfully");
setTimeout(() => {
window.location.href = "{% url 'account_login' %}";
}, 1000);
} else {
notify("error",data.error);
}
} catch (error) {
notify("error",error);
}
}
</script>
{% endblock customJS %}

View File

@ -282,9 +282,9 @@
</div> </div>
</div> </div>
</section> </section>
<section class="pt-lg-0 pt-xl-8">
{% include 'footer.html' %} {% include 'footer.html' %}
</section>
<script src="{% static 'js/phoenix.js' %}"></script> <script src="{% static 'js/phoenix.js' %}"></script>
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
@ -307,7 +307,7 @@
return true return true
} }
function validate_sa_phone_number(phone_number) { function validate_sa_phone_number(phone_number) {
const phone_numberRegex = /^056[0-9]{7}$/; const phone_numberRegex = /^05[0-9]{8}$/;
return phone_numberRegex.test(phone_number) && phone_numberRegex !== ''; return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
} }
function getAllFormData() { function getAllFormData() {

View File

@ -70,7 +70,6 @@
<script src="{% static 'js/main.js' %}"></script> <script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/jquery.min.js' %}"></script>
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %} {% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
{% comment %} {% block customCSS %}{% endblock %} {% endcomment %} {% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
</head> </head>

View File

@ -15,7 +15,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="noteForm" action="{% url 'add_note' request.dealer.slug content_type slug %}" <form action="{% url 'add_note' request.dealer.slug content_type slug %}"
hx-select="#notesTable" hx-select="#notesTable"
hx-target="#notesTable" hx-target="#notesTable"
hx-on::after-request="{ hx-on::after-request="{
@ -34,20 +34,11 @@
</div> </div>
</div> </div>
<script> <script>
function updateNote(e) { function updateNote(e) {
let url = e.getAttribute('data-url'); let url = e.getAttribute('data-url')
let note = e.getAttribute('data-note'); let note = e.getAttribute('data-note')
document.querySelector('#id_note').value = note
document.querySelector('#id_note').value = note; let form = document.querySelector('.add_note_form')
form.action = url
let form = document.querySelector('#noteForm');
form.action = url;
htmx.process(form);
} }
$('#noteModal').on('hidden.bs.modal', function () {
let form = document.querySelector('#noteForm');
form.action = "{% url 'add_note' request.dealer.slug content_type slug %}";
document.querySelector('#id_note').value = "";
htmx.process(form);
});
</script> </script>

View File

@ -824,7 +824,7 @@
let form = document.querySelector('.add_note_form') let form = document.querySelector('.add_note_form')
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}" form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
} }
/*let Toast = Swal.mixin({ let Toast = Swal.mixin({
toast: true, toast: true,
position: "top-end", position: "top-end",
showConfirmButton: false, showConfirmButton: false,
@ -834,7 +834,7 @@
toast.onmouseenter = Swal.stopTimer; toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer; toast.onmouseleave = Swal.resumeTimer;
} }
});*/ });
// Display Django messages // Display Django messages
{% if messages %} {% if messages %}

View File

@ -121,7 +121,7 @@
background-position:left bottom; background-position:left bottom;
background-size:auto"></div> background-size:auto"></div>
<div class="card-body d-flex flex-column justify-content-between position-relative"> <div class="card-body d-flex flex-column justify-content-between position-relative">
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between" hx-boost="false">
<div class="mb-5 mb-md-0 mb-lg-5"> <div class="mb-5 mb-md-0 mb-lg-5">
<div class="d-sm-flex d-md-block d-lg-flex align-items-center mb-3"> <div class="d-sm-flex d-md-block d-lg-flex align-items-center mb-3">
<h3 class="mb-0 me-2">{{ dealer.user.userplan.plan|capfirst }}</h3> <h3 class="mb-0 me-2">{{ dealer.user.userplan.plan|capfirst }}</h3>

View File

@ -39,15 +39,7 @@
<div class="empty-state-container"> <div class="empty-state-container">
<!-- Empty State Illustration --> <!-- Empty State Illustration -->
<img src="{% static 'images/logos/no-content-new.jpg' %}" alt="No-empty-state-image" class="empty-state-image">
{% if image %}
{% static image as final_image_path %}
{% else %}
{% static 'images/no_content/no_item.jpg' as final_image_path %}
{% endif %}
<img src="{{ final_image_path }}" alt="No-empty-state-image" class="empty-state-image">
<!-- Title --> <!-- Title -->
<h3 class="empty-state-title"> <h3 class="empty-state-title">

View File

@ -19,11 +19,22 @@
<script src="{% static 'vendors/zxing/index.min.js' %}"></script> <script src="{% static 'vendors/zxing/index.min.js' %}"></script>
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script> <script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
{% if not vendor_exists %} {% if not vendor_exists %}
{% url "vendor_create" request.dealer.slug as create_vendor_url %} <div class="alert alert-outline-warning d-flex align-items-center"
{% include "message-illustration.html" with value1="Please Add A Vendor, Before Adding A Car." value2="Create Vendor" message_image="images/empty/vendor2.png" url=create_vendor_url %} role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">
{{ _("Please Add A Vendor, Before Adding A Car .") }} <a href="{% url 'vendor_create' request.dealer.slug %}"
class="ms-3 text-body-primary fs-9">{{ _("Add Vendor") }}</a>
</p>
<button class="btn-close"
type="button"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endif %} {% endif %}
<!----> <!---->
<div class="row justify-content-center mt-5 mb-3 {% if not vendor_exists %}d-none{% endif %}" hx-boost="false"> <div class="row justify-content-center mt-5 mb-3
{% if not vendor_exists %}disabled{% endif %}" hx-boost="false">
<div class="col-lg-8 col-md-10"> <div class="col-lg-8 col-md-10">
<div class="card shadow-sm border-0 rounded-3"> <div class="card shadow-sm border-0 rounded-3">
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3"> <div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">

View File

@ -1,89 +0,0 @@
{% load static %}
{% load i18n %}
<style>
{% comment %} .empty-state-container {
background-color: #ffffff;
padding: 50px;
border-radius: 5px; /* Rounded corners */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); /* Subtle shadow */
text-align: center;
max-width: 70rem; /* Max width for content - made wider */
width: 90%; /* Fluid width */
margin: 0px auto; /* Added margin-top and auto for horizontal centering */
max-height: 40vh; /* 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: 90%; /* Responsive image size */
height: 90%;
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 */ {% endcomment %}
.empty-state-container {
background-color: #ffffff;
padding: 50px;
border-radius: 5px; /* Rounded corners */
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); /* Subtle shadow */
text-align: center;
max-width: 70rem; /* Max width for content - made wider */
width: 90%; /* Fluid width */
margin: 0px auto; /* Added margin-top and auto for horizontal centering */
max-height: 80vh; /* 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%;
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 alert mb-2" role="alert">
<img src="{% static message_image %}" alt="message-image" class="empty-state-image mb-2">
<!-- Title -->
<h3 class="empty-state-title mb-2">
{% blocktrans %}{{ value1}}{% endblocktrans %}
</h3>
<div class="d-flex justify-content-between mt-4">
<a class="btn btn-primary mx-2" href="{{ url }}">
<i class="fa fa-plus me-2"></i>
{% blocktrans %}{{value2}}{% endblocktrans %}
</a>
</div>
</div>

View File

@ -310,219 +310,162 @@
{% endblock content %} {% endblock content %}
{% block customJS %} {% block customJS %}
<script> <script>
// wizard-form.js document.addEventListener("DOMContentLoaded", function () {
document.addEventListener('DOMContentLoaded', initWizardForm); const form = document.getElementById('wizardForm');
document.addEventListener('htmx:afterSwap', initWizardForm);
function initWizardForm() { form.addEventListener('submit', function(e) {
const form = document.getElementById('wizardForm'); e.preventDefault(); // Prevent default form submission
if (!form) return;
// Remove old event listeners to prevent duplicates // Show loading alert
form.removeEventListener('submit', handleFormSubmit);
// Add new submit handler
form.addEventListener('submit', handleFormSubmit);
// Initialize radio button selections
initPlanSelection();
// Initialize wizard steps
initWizardSteps();
// Initialize card input formatting
initCardInputs();
// Initialize summary updates
initSummaryUpdates();
}
function handleFormSubmit(e) {
e.preventDefault();
if (typeof Swal !== 'undefined') {
Swal.fire({ Swal.fire({
title: 'Processing...', title: 'Processing...',
html: 'Please wait while we submit your form', html: 'Please wait while we submit your form',
allowOutsideClick: false, allowOutsideClick: false,
didOpen: () => Swal.showLoading() didOpen: () => {
Swal.showLoading();
}
}); });
}
// Submit the form after a slight delay // Submit the form after a slight delay to ensure Swal is shown
setTimeout(() => { setTimeout(() => {
const form = e.target; form.submit();
if (form.reportValidity()) { }, 100);
form.submit(); });
} else if (typeof Swal !== 'undefined') {
Swal.close();
}
}, 100);
}
function initPlanSelection() { const radios = document.querySelectorAll('.btn-check');
const radios = document.querySelectorAll('.btn-check'); radios.forEach(radio => {
radios.forEach(radio => { radio.addEventListener('change', function () {
// Remove old listeners document.querySelectorAll('.pricing-card .card').forEach(card => {
radio.removeEventListener('change', handlePlanChange); card.classList.remove('selected');
});
if (this.checked) {
const selectedCard = document.querySelector(`label[for="${this.id}"] .card`);
selectedCard.classList.add('selected');
}
});
// Add new listeners // Trigger change on page load if checked
radio.addEventListener('change', handlePlanChange);
// Initialize selected state
if (radio.checked) { if (radio.checked) {
updatePlanSelection(radio.id); const selectedCard = document.querySelector(`label[for="${radio.id}"] .card`);
selectedCard.classList.add('selected');
} }
}); });
}
function handlePlanChange(e) { //////////////////////////////////////////////////////////////////////////////////////
if (this.checked) { //////////////////////////////////////////////////////////////////////////////////////
updatePlanSelection(this.id); let currentStep = 0;
} const steps = document.querySelectorAll(".step");
} const nextBtn = document.getElementById("nextBtn");
const prevBtn = document.getElementById("prevBtn");
const submitBtn = document.getElementById("submitBtn");
function updatePlanSelection(radioId) { function showStep(index) {
document.querySelectorAll('.pricing-card .card').forEach(card => {
card.classList.remove('selected');
});
const selectedCard = document.querySelector(`label[for="${radioId}"] .card`);
if (selectedCard) {
selectedCard.classList.add('selected');
}
}
function initWizardSteps() {
let currentStep = 0;
const steps = document.querySelectorAll(".step");
const nextBtn = document.getElementById("nextBtn");
const prevBtn = document.getElementById("prevBtn");
const submitBtn = document.getElementById("submitBtn");
if (!steps.length || !nextBtn || !prevBtn || !submitBtn) return;
// Remove old listeners
nextBtn.removeEventListener("click", handleNext);
prevBtn.removeEventListener("click", handlePrev);
// Add new listeners
nextBtn.addEventListener("click", handleNext);
prevBtn.addEventListener("click", handlePrev);
function showStep(index) {
steps.forEach((step, i) => { steps.forEach((step, i) => {
step.classList.toggle("d-none", i !== index); step.classList.toggle("d-none", i !== index);
}); });
prevBtn.disabled = index === 0; prevBtn.disabled = index === 0;
nextBtn.classList.toggle("d-none", index === steps.length - 1); nextBtn.classList.toggle("d-none", index === steps.length - 1);
submitBtn.classList.toggle("d-none", index !== steps.length - 1); submitBtn.classList.toggle("d-none", index !== steps.length - 1);
// If last step, populate summary
if (index === steps.length - 1) { if (index === steps.length - 1) {
populateSummary(); populateSummary();
} }
} }
function handleNext() { function populateSummary() {
if (currentStep < steps.length - 1) {
currentStep++;
showStep(currentStep);
}
}
function handlePrev() {
if (currentStep > 0) {
currentStep--;
showStep(currentStep);
}
}
function populateSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked'); const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
if (selectedPlan) { document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name; document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
}
const firstName = document.getElementById("first_name")?.value || ''; /*const currencyElement = document.createElement("span");
const lastName = document.getElementById("last_name")?.value || ''; currencyElement.classList.add("currency");
document.getElementById("summary_name").textContent = `${firstName} ${lastName}`.trim(); currencyElement.textContent = "<span class="icon-saudi_riyal"></span>";
document.getElementById("summary_price").appendChild(currencyElement);*/
document.getElementById("summary_email").textContent = document.getElementById("email")?.value || ''; document.getElementById("summary_name").textContent = document.getElementById("first_name").value + " " + document.getElementById("last_name").value;
document.getElementById("summary_company").textContent = document.getElementById("company")?.value || ''; document.getElementById("summary_email").textContent = document.getElementById("email").value;
document.getElementById("summary_phone").textContent = document.getElementById("phone")?.value || ''; document.getElementById("summary_company").textContent = document.getElementById("company").value;
document.getElementById("summary_card_name").textContent = document.getElementById("card_name")?.value || ''; document.getElementById("summary_phone").textContent = document.getElementById("phone").value;
const cardNumber = document.getElementById("card_number")?.value || ''; document.getElementById("summary_card_name").textContent = document.getElementById("card_name").value;
document.getElementById("summary_card_number").textContent = maskCard(cardNumber); document.getElementById("summary_card_number").textContent = maskCard(document.getElementById("card_number").value);
document.getElementById("summary_card_expiry").textContent = document.getElementById("card_expiry").value;
}
document.getElementById("summary_card_expiry").textContent = document.getElementById("card_expiry")?.value || ''; function maskCard(cardNumber) {
}
function maskCard(cardNumber) {
const last4 = cardNumber.slice(-4); const last4 = cardNumber.slice(-4);
return "**** **** **** " + last4; return "**** **** **** " + last4;
} }
// Initialize nextBtn.addEventListener("click", () => {
showStep(currentStep); if (currentStep < steps.length - 1) {
} currentStep++;
showStep(currentStep);
function initCardInputs() {
const cardNumberInput = document.getElementById("card_number");
const expiryInput = document.getElementById("card_expiry");
if (cardNumberInput) {
cardNumberInput.removeEventListener("input", formatCardNumber);
cardNumberInput.addEventListener("input", formatCardNumber);
}
if (expiryInput) {
expiryInput.removeEventListener("input", formatExpiryDate);
expiryInput.addEventListener("input", formatExpiryDate);
}
function formatCardNumber(e) {
let val = this.value.replace(/\D/g, "").substring(0, 16);
this.value = val.replace(/(.{4})/g, "$1 ").trim();
}
function formatExpiryDate(e) {
let val = this.value.replace(/\D/g, "").substring(0, 4);
if (val.length >= 3) {
val = val.substring(0, 2) + "/" + val.substring(2);
} }
this.value = val; });
}
}
function initSummaryUpdates() { prevBtn.addEventListener("click", () => {
const planInputs = document.querySelectorAll("input[name='selected_plan']"); if (currentStep > 0) {
currentStep--;
showStep(currentStep);
}
});
planInputs.forEach(input => { // Highlight selected plan
input.removeEventListener("change", updatePricingSummary); document.querySelectorAll(".btn-check").forEach(input => {
input.addEventListener("change", updatePricingSummary); input.addEventListener("change", () => {
document.querySelectorAll(".pricing-card .card").forEach(card => card.classList.remove("selected"));
document.querySelector(`label[for="${input.id}"] .card`).classList.add("selected");
});
if (input.checked) {
document.querySelector(`label[for="${input.id}"] .card`).classList.add("selected");
}
});
showStep(currentStep);
//////////////////////////////////////////////////////////////////////////////////////
const cardNumberInput = document.getElementById("card_number");
cardNumberInput.addEventListener("input", function (e) {
let val = cardNumberInput.value.replace(/\D/g, "").substring(0, 16); // Only digits
let formatted = val.replace(/(.{4})/g, "$1 ").trim();
cardNumberInput.value = formatted;
});
// Format expiry date as MM/YY
const expiryInput = document.getElementById("card_expiry");
expiryInput.addEventListener("input", function (e) {
let val = expiryInput.value.replace(/\D/g, "").substring(0, 4); // Only digits
if (val.length >= 3) {
val = val.substring(0, 2) + "/" + val.substring(2);
}
expiryInput.value = val;
});
//////////////////////////////////////////////////////////////////////////////////////
const planInputs = document.querySelectorAll("input[name='selected_plan']");
const summaryPlanName = document.getElementById("summary_plan");
const summaryPrice = document.getElementById("summary_price"); //summary_price
const summaryTax = document.getElementById("summary-tax");
const summaryTotal = document.getElementById("summary-total");
function updateSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
if (selectedPlan) {
const price = parseFloat(selectedPlan.dataset.price);
const tax = price * 0.15;
const total = price + tax;
document.getElementById("summary_price").textContent = price.toFixed(2);
document.getElementById("summary-tax").textContent = tax.toFixed(2);
document.getElementById("summary-total").textContent = total.toFixed(2);
}
}
planInputs.forEach(input => input.addEventListener("change", updateSummary));
updateSummary(); // Initial call
}); });
updatePricingSummary(); // Initial call
}
function updatePricingSummary() {
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
if (!selectedPlan) return;
const price = parseFloat(selectedPlan.dataset.price) || 0;
const tax = price * 0.15;
const total = price + tax;
const summaryPrice = document.getElementById("summary_price");
const summaryTax = document.getElementById("summary-tax");
const summaryTotal = document.getElementById("summary-total");
if (summaryPrice) summaryPrice.textContent = price.toFixed(2);
if (summaryTax) summaryTax.textContent = tax.toFixed(2);
if (summaryTotal) summaryTotal.textContent = total.toFixed(2);
}
</script> </script>
{% endblock customJS %} {% endblock customJS %}

View File

@ -122,23 +122,35 @@
} }
</style> </style>
<div class="row justify-content-center mt-5 mb-3"> <div class="row justify-content-center mt-5 mb-3">
<div class="row"> {% if not items %}
<div class="col"> <div class="alert alert-outline-warning d-flex align-items-center"
{% if not items %} role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>
{% url "car_add" request.dealer.slug as create_car_url %} <p class="mb-0 flex-1">
{% include "message-illustration.html" with value1="Please add at least one car before creating a quotation." value2="Add car" message_image="images/empty/no_car.png" url=create_car_url %} {{ _("Please add at least one car before creating a quotation.") }}<a class="ms-3 text-body-primary fs-9"
href="{% url 'car_add' request.dealer.slug %}">{{ _("Add Car") }}</a>
</p>
<button class="btn-close"
type="button"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endif %} {% endif %}
</div>
<div class="col">
{% if not customer_count %} {% if not customer_count %}
{% url "customer_create" request.dealer.slug as create_customer_url %} <div class="alert alert-outline-warning d-flex align-items-center"
{% include "message-illustration.html" with value1="Please add at least one customer before creating a quotation." value2="Add Customer" message_image="images/empty/no_estimate.png" url=create_customer_url %} role="alert">
<i class="fa-solid fa-circle-info fs-6"></i>&nbsp;&nbsp;
<p class="mb-0 flex-1">
{{ _("Please add at least one customer before creating a quotation.") }}<a class="ms-3 text-body-primary fs-9"
href="{% url 'customer_create' request.dealer.slug %}">{{ _("Add Customer") }}</a>
</p>
<button class="btn-close"
type="button"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endif %} {% endif %}
</div> <div class="col-lg-8 col-md-10">
<div>
<div class="col-lg-8 col-md-10 needs-validation {% if not items or not customer_count %}d-none{% endif %}">
<div class="card shadow-sm border-0 rounded-3"> <div class="card shadow-sm border-0 rounded-3">
<div class="card-header bg-gray-200 py-3 border-0 rounded-top-3"> <div class="card-header bg-gray-200 py-3 border-0 rounded-top-3">
<h3 class="mb-0 fs-4 text-center"> <h3 class="mb-0 fs-4 text-center">
@ -148,7 +160,7 @@
<div class="card-body bg-light-subtle"> <div class="card-body bg-light-subtle">
<form id="mainForm" method="post" class="needs-validation {% if not items and not customer_count %}d-none{% endif %}"> <form id="mainForm" method="post" class="needs-validation {% if not items or not customer_count %}disabled{% endif %}">
{% csrf_token %} {% csrf_token %}
<div class="row g-3 col-12"> <div class="row g-3 col-12">

View File

@ -79,7 +79,7 @@
{% else %} {% else %}
{% url "estimate_create" request.dealer.slug as create_estimate_url %} {% url "estimate_create" request.dealer.slug as create_estimate_url %}
{% include "empty-illustration-page.html" with value="estimate" url=create_estimate_url image='images/no_content/no_estimate.jpg' %} {% include "empty-illustration-page.html" with value="estimate" url=create_estimate_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -36,9 +36,6 @@
<p> <p>
<strong>{{ _("Arabic Name") }}:</strong> {{ user_.arabic_name }} <strong>{{ _("Arabic Name") }}:</strong> {{ user_.arabic_name }}
</p> </p>
<p>
<strong>{{ _("Email") }}:</strong> {{ user_.email }}
</p>
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<p> <p>
@ -97,11 +94,6 @@
{{ _("Back to List") }} {{ _("Back to List") }}
<i class="fa-regular fa-circle-left"></i> <i class="fa-regular fa-circle-left"></i>
</a> </a>
<a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'staff_password_reset' request.dealer.slug user_.pk %}">
{{ _("Reset Password") }}
<i class="fa-solid fa-key"></i>
</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@
{% endblock title %} {% endblock title %}
{% block content %} {% block content %}
{%if users %} {%if users %}
<section class=""> <section class="">
<div class="row mt-4"> <div class="row mt-4">
@ -18,10 +19,18 @@
<a href="{% url 'group_list' request.dealer.slug %}" <a href="{% url 'group_list' request.dealer.slug %}"
class="btn btn-sm btn-phoenix-success"><i class="fa-solid fa-user-group me-1"></i> {% trans "Manage Groups & Permissions" %}</a> class="btn btn-sm btn-phoenix-success"><i class="fa-solid fa-user-group me-1"></i> {% trans "Manage Groups & Permissions" %}</a>
{% else %} {% else %}
<div class="alert alert-outline-warning d-flex align-items-center"
{% url "pricing_page" request.dealer.slug as pricing_page_url %} role="alert">
{% include "message-illustration.html" with value1="No Active Plan, please create your subscription plan." value2="Manage Subscription" message_image="images/messages/haikal_plan_message.jpg" url=pricing_page_url %} <i class="fa-solid fa-circle-info fs-6"></i>
<p class="mb-0 flex-1">
{{ _("No Active Subscription,please activate your subscription.") }}<a hx-boost="false" href="{% url 'pricing_page' request.dealer.slug %}"
class="ms-3 text-body-primary fs-9">Manage Subscription</a>
</p>
<button class="btn-close"
type="button"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -82,13 +91,24 @@
</section> </section>
{% else %} {% else %}
{% if request.user.userplan %} {% if request.user.userplan %}
{% url "user_create" request.dealer.slug as create_staff_url image="images/no_content/no_user.png" %} {% url "user_create" request.dealer.slug as create_staff_url %}
{% include "empty-illustration-page.html" with value="staff" url=create_staff_url %} {% include "empty-illustration-page.html" with value="staff" url=create_staff_url %}
{% else %} {% else %}
{% url "pricing_page" request.dealer.slug as pricing_page_url %} <div class="alert alert-outline-warning d-flex align-items-center"
{% include "message-illustration.html" with value1="No Active Plan, please create your subscription plan." value2="Buy Plan" message_image="images/no_content/no_plan.jpg" url=pricing_page_url %} role="alert">
<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>
</p>
<button class="btn-close"
type="button"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -1,22 +0,0 @@
{% extends 'base.html' %}
{% block content %}
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card mt-5">
<div class="card-body">
<h2 class="card-title">Set New Password</h2>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">
Change Password
</button>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -166,6 +166,6 @@
{% else %} {% else %}
{% url "vendor_create" request.dealer.slug as create_vendor_url %} {% url "vendor_create" request.dealer.slug as create_vendor_url %}
{% include "empty-illustration-page.html" with value="vendor" url=create_vendor_url image="images/no_content/no_vendor.png" %} {% include "empty-illustration-page.html" with value="vendor" url=create_vendor_url %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -9,11 +9,6 @@
<div class="card shadow rounded"> <div class="card shadow rounded">
<p class="card-header mb-0 fs-5">{% trans "Vendor Details" %}</p> <p class="card-header mb-0 fs-5">{% trans "Vendor Details" %}</p>
<div class="card-body"> <div class="card-body">
<div class="col-12 col-sm-auto mb-sm-2">
<div class="avatar avatar-5xl">
{% if vendor.logo%}<img class="rounded-circle" src="{{ vendor.logo.url }}" alt="" />{% endif %}
</div>
</div>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="list-group-item"> <li class="list-group-item">
<strong>{% trans "Name" %}:</strong> {{ vendor.get_local_name }} <strong>{% trans "Name" %}:</strong> {{ vendor.get_local_name }}
@ -55,4 +50,3 @@
</div> </div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
{% endblock %} {% endblock %}