ismail changes

This commit is contained in:
Faheedkhan 2025-07-29 13:32:08 +03:00
commit 953157ea15
16 changed files with 354 additions and 635 deletions

View File

@ -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'})
)

View File

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

View File

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

View File

@ -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(),

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@
{% endblock title %}
{% block content %}
{%if users %}
<section class="">
<div class="row mt-4">
@ -86,7 +85,7 @@
{% if request.user.userplan %}
{% url "user_create" request.dealer.slug as create_staff_url image="images/no_content/no_user.png" %}
{% include "empty-illustration-page.html" with value="staff" url=create_staff_url %}
{% else %}
{% url "pricing_page" request.dealer.slug as pricing_page_url %}
{% 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 %}
@ -94,6 +93,6 @@
{% endif %}
{% endblock %}

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