fix the plans upgrade and more
This commit is contained in:
parent
8d4d2bff28
commit
f117feee6d
@ -16,7 +16,7 @@ from django_ledger.forms.invoice import (
|
||||
)
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django_ledger.forms.bill import BillModelCreateForm as BillModelCreateFormBase
|
||||
|
||||
from django.contrib.auth.forms import PasswordChangeForm, SetPasswordForm
|
||||
from django_ledger.forms.journal_entry import (
|
||||
JournalEntryModelCreateForm as JournalEntryModelCreateFormBase,
|
||||
)
|
||||
@ -2102,3 +2102,14 @@ class VatRateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = VatRate
|
||||
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'})
|
||||
)
|
||||
@ -1,3 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from django.urls import reverse
|
||||
from inventory.tasks import create_coa_accounts, create_make_accounts
|
||||
@ -24,6 +25,7 @@ from . import models
|
||||
from django.utils.timezone import now
|
||||
from django.db import transaction
|
||||
from django_q.tasks import async_task
|
||||
from plans.models import UserPlan
|
||||
from plans.signals import order_completed, activate_user_plan
|
||||
|
||||
# logging
|
||||
@ -1212,10 +1214,4 @@ def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def handle_upgrade(sender, order, **kwargs):
|
||||
logger.info(f"User {order.user} upgraded to {order.plan}")
|
||||
|
||||
order_completed.connect(handle_upgrade)
|
||||
)
|
||||
@ -1,4 +1,4 @@
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from plans.models import Plan
|
||||
from django.conf import settings
|
||||
@ -8,10 +8,11 @@ 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.core.files.base import ContentFile
|
||||
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.core.mail import EmailMultiAlternatives
|
||||
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
|
||||
|
||||
@ -517,6 +517,11 @@ urlpatterns = [
|
||||
views.UserDeleteview,
|
||||
name="user_delete",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/user/<int:user_pk>/password_reset/",
|
||||
views.staff_password_reset_view,
|
||||
name="staff_password_reset",
|
||||
),
|
||||
path(
|
||||
"<slug:dealer_slug>/group/create/",
|
||||
views.GroupCreateView.as_view(),
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
# Standard
|
||||
from django.core.files.base import ContentFile
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import io
|
||||
@ -15,7 +17,7 @@ from random import randint
|
||||
from decimal import Decimal
|
||||
from io import TextIOWrapper
|
||||
from django.apps import apps
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from calendar import month_name
|
||||
from pyzbar.pyzbar import decode
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
@ -301,18 +303,20 @@ def dealer_signup(request):
|
||||
:rtype: Union[django.http.HttpResponse, django.http.JsonResponse]
|
||||
"""
|
||||
if request.method == "POST":
|
||||
data = json.loads(request.body)
|
||||
|
||||
email = data.get("email")
|
||||
password = data.get("password")
|
||||
password_confirm = data.get("confirm_password")
|
||||
name = data.get("name")
|
||||
arabic_name = data.get("arabic_name")
|
||||
phone = data.get("phone_number")
|
||||
crn = data.get("crn")
|
||||
vrn = data.get("vrn")
|
||||
address = data.get("address")
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
email = data.get("email")
|
||||
password = data.get("password")
|
||||
password_confirm = data.get("confirm_password")
|
||||
name = data.get("name")
|
||||
arabic_name = data.get("arabic_name")
|
||||
phone = data.get("phone_number")
|
||||
crn = data.get("crn")
|
||||
vrn = data.get("vrn")
|
||||
address = data.get("address")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if User.objects.filter(email=email).exists():
|
||||
return JsonResponse({"error": _("Email already exists")}, status=400)
|
||||
if not re.match(
|
||||
@ -418,11 +422,11 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
dealer = get_user_type(self.request)
|
||||
entity = dealer.entity
|
||||
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
||||
total_reservations = models.CarReservation.objects.filter(
|
||||
reserved_until__gte=timezone.now()
|
||||
).count()
|
||||
stats = models.CarFinance.objects.aggregate(
|
||||
|
||||
qs = models.Car.objects.filter(dealer=dealer)
|
||||
total_cars = qs.count()
|
||||
|
||||
stats = models.CarFinance.objects.filter(car__dealer=dealer).aggregate(
|
||||
total_cost_price=Sum("cost_price"),
|
||||
total_selling_price=Sum("selling_price"),
|
||||
)
|
||||
@ -439,72 +443,48 @@ class ManagerDashboard(LoginRequiredMixin, TemplateView):
|
||||
canceled_leads = models.Lead.objects.filter(
|
||||
dealer=dealer, status=models.Status.UNQUALIFIED
|
||||
).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}")
|
||||
|
||||
qs = (
|
||||
models.Car.objects.values("id_car_make__name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("id_car_make__name")
|
||||
)
|
||||
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()
|
||||
car_status_qs = qs.values("status").annotate(count=Count("status"))
|
||||
car_status = {status: count for status, count in car_status_qs}
|
||||
|
||||
car_by_make_qs = qs.values("id_car_make__name").annotate(count=Count("id"))
|
||||
car_by_make = list(car_by_make_qs)
|
||||
|
||||
context["dealer"] = dealer
|
||||
context["total_activity"] = total_activity
|
||||
context["total_activity"] = models.UserActivityLog.objects.filter(
|
||||
user=dealer.user
|
||||
).count()
|
||||
context["total_cars"] = total_cars
|
||||
context["total_reservations"] = total_reservations
|
||||
context["total_reservations"] = models.CarReservation.objects.filter(
|
||||
reserved_until__gte=timezone.now()
|
||||
).count()
|
||||
context["total_cost_price"] = total_cost_price
|
||||
context["total_selling_price"] = total_selling_price
|
||||
context["total_profit"] = total_profit
|
||||
context["new_leads"] = new_leads
|
||||
context["pending_leads"] = pending_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["customers"] = customers
|
||||
context["staff"] = staff
|
||||
context["total_leads"] = total_leads
|
||||
context["invoices"] = invoices
|
||||
context["estimates"] = estimates
|
||||
context["purchase_orders"] = purchase_orders
|
||||
context["available_cars"] = car_status.get(
|
||||
models.CarStatusChoices.AVAILABLE, 0
|
||||
)
|
||||
context["sold_cars"] = car_status.get(models.CarStatusChoices.SOLD, 0)
|
||||
context["reserved_cars"] = car_status.get(
|
||||
models.CarStatusChoices.RESERVED, 0
|
||||
)
|
||||
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
|
||||
|
||||
@ -3416,6 +3396,7 @@ class UserCreateView(
|
||||
success_url = reverse_lazy("user_list")
|
||||
success_message = _("User created successfully")
|
||||
permission_required = ["inventory.add_staff"]
|
||||
staff_pk = None
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
@ -3425,13 +3406,6 @@ class UserCreateView(
|
||||
|
||||
def form_valid(self, form):
|
||||
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:
|
||||
messages.error(
|
||||
self.request,
|
||||
@ -3463,12 +3437,14 @@ class UserCreateView(
|
||||
staff.staff_member = staff_member
|
||||
staff.dealer = dealer
|
||||
staff.save()
|
||||
self.staff_pk = staff.pk
|
||||
for customgroup in form.cleaned_data["group"]:
|
||||
staff.add_group(customgroup.group)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("user_list", args=[self.request.dealer.slug])
|
||||
return reverse_lazy("staff_password_reset", args=[self.request.dealer.slug, self.staff_pk])
|
||||
# return reverse_lazy("user_list", args=[self.request.dealer.slug])
|
||||
|
||||
|
||||
class UserUpdateView(
|
||||
@ -6148,6 +6124,7 @@ def lead_tracking(request, dealer_slug):
|
||||
@permission_required("inventory.change_lead", raise_exception=True)
|
||||
def update_lead_actions(request, dealer_slug):
|
||||
# get the user info for logging
|
||||
|
||||
user_username = (
|
||||
request.user.username if request.user.is_authenticated else "anonymous"
|
||||
)
|
||||
@ -9391,9 +9368,7 @@ def payment_callback(request, dealer_slug):
|
||||
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() # 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,
|
||||
defaults={
|
||||
@ -9407,14 +9382,20 @@ def payment_callback(request, dealer_slug):
|
||||
)
|
||||
|
||||
try:
|
||||
# COMPLETE THE ORDER - This handles plan activation/upgrade
|
||||
order.complete_order() # Critical step: activates the plan
|
||||
order.complete_order()
|
||||
user = order.user
|
||||
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.save()
|
||||
|
||||
# Retrieve invoice
|
||||
invoice = order.get_invoices().first()
|
||||
return render(
|
||||
request,
|
||||
@ -9423,7 +9404,6 @@ def payment_callback(request, dealer_slug):
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Handle activation errors (log, notify admin, etc.)
|
||||
logger.error(f"Plan activation failed: {str(e)}")
|
||||
history.status = "failed"
|
||||
history.save()
|
||||
@ -9434,7 +9414,6 @@ def payment_callback(request, dealer_slug):
|
||||
history.save()
|
||||
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")
|
||||
@ -9685,6 +9664,7 @@ def update_schedule(request, dealer_slug, pk):
|
||||
@permission_required("inventory.add_notes", raise_exception=True)
|
||||
def add_note(request, dealer_slug, content_type, slug):
|
||||
# Get user information for logging
|
||||
print("hi")
|
||||
user_username = (
|
||||
request.user.username if request.user.is_authenticated else "anonymous"
|
||||
)
|
||||
@ -9743,6 +9723,7 @@ def add_note(request, dealer_slug, content_type, slug):
|
||||
@permission_required("inventory.change_notes", raise_exception=True)
|
||||
def update_note(request, dealer_slug, pk):
|
||||
note = get_object_or_404(models.Notes, pk=pk)
|
||||
print(note)
|
||||
lead = get_object_or_404(models.Lead, pk=note.content_object.id)
|
||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
if request.method == "POST":
|
||||
@ -10750,3 +10731,21 @@ def car_sale_report_csv_export(request,dealer_slug):
|
||||
])
|
||||
|
||||
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})
|
||||
BIN
static/images/logos/users/user-logo.jpg
Normal file
BIN
static/images/logos/users/user-logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@ -1,389 +0,0 @@
|
||||
{% 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 %}
|
||||
@ -282,9 +282,9 @@
|
||||
</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 %}
|
||||
@ -307,7 +307,7 @@
|
||||
return true
|
||||
}
|
||||
function validate_sa_phone_number(phone_number) {
|
||||
const phone_numberRegex = /^05[0-9]{8}$/;
|
||||
const phone_numberRegex = /^056[0-9]{7}$/;
|
||||
return phone_numberRegex.test(phone_number) && phone_numberRegex !== '';
|
||||
}
|
||||
function getAllFormData() {
|
||||
|
||||
@ -70,6 +70,7 @@
|
||||
|
||||
<script src="{% static 'js/main.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
|
||||
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
|
||||
{% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
|
||||
</head>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form action="{% url 'add_note' request.dealer.slug content_type slug %}"
|
||||
<form id="noteForm" action="{% url 'add_note' request.dealer.slug content_type slug %}"
|
||||
hx-select="#notesTable"
|
||||
hx-target="#notesTable"
|
||||
hx-on::after-request="{
|
||||
@ -34,11 +34,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function updateNote(e) {
|
||||
let url = e.getAttribute('data-url')
|
||||
let note = e.getAttribute('data-note')
|
||||
document.querySelector('#id_note').value = note
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = url
|
||||
function updateNote(e) {
|
||||
let url = e.getAttribute('data-url');
|
||||
let note = e.getAttribute('data-note');
|
||||
|
||||
document.querySelector('#id_note').value = note;
|
||||
|
||||
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>
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
<div class="col-12 col-md-auto">
|
||||
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,7 +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="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center g-3 text-center text-xxl-start">
|
||||
@ -800,9 +800,9 @@
|
||||
<div class="col-auto d-flex">
|
||||
<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") }}
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -824,7 +824,7 @@
|
||||
let form = document.querySelector('.add_note_form')
|
||||
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
|
||||
}
|
||||
let Toast = Swal.mixin({
|
||||
/*let Toast = Swal.mixin({
|
||||
toast: true,
|
||||
position: "top-end",
|
||||
showConfirmButton: false,
|
||||
@ -834,7 +834,7 @@
|
||||
toast.onmouseenter = Swal.stopTimer;
|
||||
toast.onmouseleave = Swal.resumeTimer;
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
// Display Django messages
|
||||
{% if messages %}
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
background-position:left bottom;
|
||||
background-size:auto"></div>
|
||||
<div class="card-body d-flex flex-column justify-content-between position-relative">
|
||||
<div class="d-flex justify-content-between" hx-boost="false">
|
||||
<div class="d-flex justify-content-between">
|
||||
<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">
|
||||
<h3 class="mb-0 me-2">{{ dealer.user.userplan.plan|capfirst }}</h3>
|
||||
|
||||
@ -310,162 +310,219 @@
|
||||
{% endblock content %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.getElementById('wizardForm');
|
||||
// wizard-form.js
|
||||
document.addEventListener('DOMContentLoaded', initWizardForm);
|
||||
document.addEventListener('htmx:afterSwap', initWizardForm);
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
function initWizardForm() {
|
||||
const form = document.getElementById('wizardForm');
|
||||
if (!form) return;
|
||||
|
||||
// Show loading alert
|
||||
// Remove old event listeners to prevent duplicates
|
||||
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({
|
||||
title: 'Processing...',
|
||||
html: 'Please wait while we submit your form',
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => {
|
||||
Swal.showLoading();
|
||||
}
|
||||
title: 'Processing...',
|
||||
html: 'Please wait while we submit your form',
|
||||
allowOutsideClick: false,
|
||||
didOpen: () => Swal.showLoading()
|
||||
});
|
||||
}
|
||||
|
||||
// Submit the form after a slight delay to ensure Swal is shown
|
||||
setTimeout(() => {
|
||||
form.submit();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const radios = document.querySelectorAll('.btn-check');
|
||||
radios.forEach(radio => {
|
||||
radio.addEventListener('change', function () {
|
||||
document.querySelectorAll('.pricing-card .card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
if (this.checked) {
|
||||
const selectedCard = document.querySelector(`label[for="${this.id}"] .card`);
|
||||
selectedCard.classList.add('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger change on page load if checked
|
||||
if (radio.checked) {
|
||||
const selectedCard = document.querySelector(`label[for="${radio.id}"] .card`);
|
||||
selectedCard.classList.add('selected');
|
||||
// Submit the form after a slight delay
|
||||
setTimeout(() => {
|
||||
const form = e.target;
|
||||
if (form.reportValidity()) {
|
||||
form.submit();
|
||||
} else if (typeof Swal !== 'undefined') {
|
||||
Swal.close();
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
let currentStep = 0;
|
||||
const steps = document.querySelectorAll(".step");
|
||||
const nextBtn = document.getElementById("nextBtn");
|
||||
const prevBtn = document.getElementById("prevBtn");
|
||||
const submitBtn = document.getElementById("submitBtn");
|
||||
function initPlanSelection() {
|
||||
const radios = document.querySelectorAll('.btn-check');
|
||||
radios.forEach(radio => {
|
||||
// Remove old listeners
|
||||
radio.removeEventListener('change', handlePlanChange);
|
||||
|
||||
function showStep(index) {
|
||||
// Add new listeners
|
||||
radio.addEventListener('change', handlePlanChange);
|
||||
|
||||
// Initialize selected state
|
||||
if (radio.checked) {
|
||||
updatePlanSelection(radio.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handlePlanChange(e) {
|
||||
if (this.checked) {
|
||||
updatePlanSelection(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlanSelection(radioId) {
|
||||
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) => {
|
||||
step.classList.toggle("d-none", i !== index);
|
||||
step.classList.toggle("d-none", i !== index);
|
||||
});
|
||||
|
||||
prevBtn.disabled = index === 0;
|
||||
nextBtn.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) {
|
||||
populateSummary();
|
||||
populateSummary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateSummary() {
|
||||
const selectedPlan = document.querySelector('input[name="selected_plan"]:checked');
|
||||
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
|
||||
document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
|
||||
|
||||
/*const currencyElement = document.createElement("span");
|
||||
currencyElement.classList.add("currency");
|
||||
currencyElement.textContent = "<span class="icon-saudi_riyal"></span>";
|
||||
document.getElementById("summary_price").appendChild(currencyElement);*/
|
||||
|
||||
document.getElementById("summary_name").textContent = document.getElementById("first_name").value + " " + document.getElementById("last_name").value;
|
||||
document.getElementById("summary_email").textContent = document.getElementById("email").value;
|
||||
document.getElementById("summary_company").textContent = document.getElementById("company").value;
|
||||
document.getElementById("summary_phone").textContent = document.getElementById("phone").value;
|
||||
|
||||
document.getElementById("summary_card_name").textContent = document.getElementById("card_name").value;
|
||||
document.getElementById("summary_card_number").textContent = maskCard(document.getElementById("card_number").value);
|
||||
document.getElementById("summary_card_expiry").textContent = document.getElementById("card_expiry").value;
|
||||
}
|
||||
|
||||
function maskCard(cardNumber) {
|
||||
const last4 = cardNumber.slice(-4);
|
||||
return "**** **** **** " + last4;
|
||||
}
|
||||
|
||||
nextBtn.addEventListener("click", () => {
|
||||
function handleNext() {
|
||||
if (currentStep < steps.length - 1) {
|
||||
currentStep++;
|
||||
showStep(currentStep);
|
||||
currentStep++;
|
||||
showStep(currentStep);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
prevBtn.addEventListener("click", () => {
|
||||
function handlePrev() {
|
||||
if (currentStep > 0) {
|
||||
currentStep--;
|
||||
showStep(currentStep);
|
||||
currentStep--;
|
||||
showStep(currentStep);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Highlight selected plan
|
||||
document.querySelectorAll(".btn-check").forEach(input => {
|
||||
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() {
|
||||
function populateSummary() {
|
||||
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);
|
||||
document.getElementById("summary_plan").textContent = selectedPlan.dataset.name;
|
||||
document.getElementById("summary_price").textContent = selectedPlan.dataset.price;
|
||||
}
|
||||
}
|
||||
|
||||
const firstName = document.getElementById("first_name")?.value || '';
|
||||
const lastName = document.getElementById("last_name")?.value || '';
|
||||
document.getElementById("summary_name").textContent = `${firstName} ${lastName}`.trim();
|
||||
|
||||
planInputs.forEach(input => input.addEventListener("change", updateSummary));
|
||||
updateSummary(); // Initial call
|
||||
document.getElementById("summary_email").textContent = document.getElementById("email")?.value || '';
|
||||
document.getElementById("summary_company").textContent = document.getElementById("company")?.value || '';
|
||||
document.getElementById("summary_phone").textContent = document.getElementById("phone")?.value || '';
|
||||
document.getElementById("summary_card_name").textContent = document.getElementById("card_name")?.value || '';
|
||||
|
||||
const cardNumber = document.getElementById("card_number")?.value || '';
|
||||
document.getElementById("summary_card_number").textContent = maskCard(cardNumber);
|
||||
|
||||
document.getElementById("summary_card_expiry").textContent = document.getElementById("card_expiry")?.value || '';
|
||||
}
|
||||
|
||||
function maskCard(cardNumber) {
|
||||
const last4 = cardNumber.slice(-4);
|
||||
return "**** **** **** " + last4;
|
||||
}
|
||||
|
||||
// Initialize
|
||||
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() {
|
||||
const planInputs = document.querySelectorAll("input[name='selected_plan']");
|
||||
|
||||
planInputs.forEach(input => {
|
||||
input.removeEventListener("change", updatePricingSummary);
|
||||
input.addEventListener("change", updatePricingSummary);
|
||||
});
|
||||
|
||||
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>
|
||||
{% endblock customJS %}
|
||||
|
||||
@ -36,6 +36,9 @@
|
||||
<p>
|
||||
<strong>{{ _("Arabic Name") }}:</strong> {{ user_.arabic_name }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ _("Email") }}:</strong> {{ user_.email }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<p>
|
||||
@ -94,6 +97,11 @@
|
||||
{{ _("Back to List") }}
|
||||
<i class="fa-regular fa-circle-left"></i>
|
||||
</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>
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
{% endblock title %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
{%if users %}
|
||||
<section class="">
|
||||
<div class="row mt-4">
|
||||
@ -23,7 +22,7 @@
|
||||
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 hx-boost="false" href="{% url 'pricing_page' request.dealer.slug %}"
|
||||
{{ _("No Active Subscription,please activate your subscription.") }}<a href="{% url 'pricing_page' request.dealer.slug %}"
|
||||
class="ms-3 text-body-primary fs-9">Manage Subscription</a>
|
||||
</p>
|
||||
<button class="btn-close"
|
||||
@ -91,11 +90,11 @@
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
|
||||
|
||||
{% if request.user.userplan %}
|
||||
{% url "user_create" request.dealer.slug as create_staff_url %}
|
||||
{% include "empty-illustration-page.html" with value="staff" url=create_staff_url %}
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-outline-warning d-flex align-items-center"
|
||||
role="alert">
|
||||
@ -113,6 +112,6 @@
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
22
templates/users/user_password_reset.html
Normal file
22
templates/users/user_password_reset.html
Normal file
@ -0,0 +1,22 @@
|
||||
{% 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 %}
|
||||
Loading…
x
Reference in New Issue
Block a user