Compare commits

...

2 Commits

Author SHA1 Message Date
2bbcae2e7a lint and formate 2025-08-27 13:04:41 +03:00
4d63b17e68 merge complete 2025-08-27 12:59:19 +03:00
199 changed files with 13812 additions and 13143 deletions

View File

@ -10,9 +10,11 @@ https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
# asgi.py
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "car_inventory.settings")
import django
django.setup()
@ -30,11 +32,17 @@ from django.core.asgi import get_asgi_application
# # "websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)),
# }
# )
application = ProtocolTypeRouter({
"http": AuthMiddlewareStack(
URLRouter([
path("sse/notifications/", NotificationSSEApp()),
re_path(r"", get_asgi_application()), # All other routes go to Django
])
),
})
application = ProtocolTypeRouter(
{
"http": AuthMiddlewareStack(
URLRouter(
[
path("sse/notifications/", NotificationSSEApp()),
re_path(
r"", get_asgi_application()
), # All other routes go to Django
]
)
),
}
)

View File

@ -56,7 +56,7 @@ from .models import (
DealerSettings,
Tasks,
Recall,
Ticket
Ticket,
)
from django_ledger import models as ledger_models
from django.forms import (
@ -146,9 +146,16 @@ class StaffForm(forms.ModelForm):
)
class Meta:
model = Staff
fields = ["first_name","last_name", "arabic_name", "phone_number", "address", "logo", "group"]
fields = [
"first_name",
"last_name",
"arabic_name",
"phone_number",
"address",
"logo",
"group",
]
# Dealer Form
@ -439,13 +446,15 @@ class CarFinanceForm(forms.ModelForm):
marked_price = cleaned_data.get("marked_price")
if cost_price > marked_price:
raise forms.ValidationError({"cost_price": "Cost price should not be greater than marked price"})
raise forms.ValidationError(
{"cost_price": "Cost price should not be greater than marked price"}
)
return cleaned_data
class Meta:
model = Car
fields = ["cost_price","marked_price"]
fields = ["cost_price", "marked_price"]
class CarLocationForm(forms.ModelForm):
@ -1168,7 +1177,7 @@ class ScheduleForm(forms.ModelForm):
scheduled_at = forms.DateTimeField(
widget=DateTimeInput(attrs={"type": "datetime-local"})
)
reminder = forms.BooleanField(help_text=_("Send a reminder?"),required=False)
reminder = forms.BooleanField(help_text=_("Send a reminder?"), required=False)
class Meta:
model = Schedule
@ -1289,6 +1298,7 @@ class OpportunityForm(forms.ModelForm):
if self.instance and self.instance.pk:
self.fields["probability"].initial = self.instance.probability
class OpportunityStageForm(forms.ModelForm):
"""
Represents a form for creating or editing Opportunity instances.
@ -1305,17 +1315,13 @@ class OpportunityStageForm(forms.ModelForm):
:type Meta.fields: list
"""
class Meta:
model = Opportunity
fields = [
"stage",
]
class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
"""
Represents a form for creating an Invoice model that inherits from a base
@ -1633,8 +1639,7 @@ class PermissionForm(forms.ModelForm):
"django_ledger.billmodeldjango_ledger.itemmodel",
"django_ledger.invoicemodel",
"django_ledger.vendormodel",
"django_ledger.journalentrymodel"
"django_ledger.purchaseordermodel",
"django_ledger.journalentrymodeldjango_ledger.purchaseordermodel",
]
permissions = cache.get(
@ -2138,91 +2143,115 @@ class VatRateForm(forms.ModelForm):
class CustomSetPasswordForm(SetPasswordForm):
new_password1 = forms.CharField(
label="New Password",
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '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'})
widget=forms.PasswordInput(
attrs={"class": "form-control", "placeholder": "Confirm New Password"}
),
)
# forms.py
class RecallFilterForm(forms.Form):
make = forms.ModelChoiceField(
queryset=CarMake.objects.all(),
required=False,
label=_("Make"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
model = forms.ModelChoiceField(
queryset=CarModel.objects.none(),
required=False,
label=_("Model"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
serie = forms.ModelChoiceField(
queryset=CarSerie.objects.none(),
required=False,
label=_("Series"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
trim = forms.ModelChoiceField(
queryset=CarTrim.objects.none(),
required=False,
label=_("Trim"),
widget=forms.Select(attrs={'class': 'form-control'})
widget=forms.Select(attrs={"class": "form-control"}),
)
year_from = forms.IntegerField(
required=False,
label=_("From Year"),
widget=forms.NumberInput(attrs={"class": "form-control"}),
)
year_to = forms.IntegerField(
required=False,
label=_("To Year"),
widget=forms.NumberInput(attrs={"class": "form-control"}),
)
year_from = forms.IntegerField(required=False, label=_("From Year"),
widget=forms.NumberInput(attrs={'class': 'form-control'}))
year_to = forms.IntegerField(required=False, label=_("To Year"),
widget=forms.NumberInput(attrs={'class': 'form-control'}))
def __init__(self, *args, **kwargs):
make_id = kwargs.pop('make_id', None)
model_id = kwargs.pop('model_id', None)
serie_id = kwargs.pop('serie_id', None)
make_id = kwargs.pop("make_id", None)
model_id = kwargs.pop("model_id", None)
serie_id = kwargs.pop("serie_id", None)
super().__init__(*args, **kwargs)
if make_id:
self.fields['model'].queryset = CarModel.objects.filter(id_car_make_id=make_id)
self.fields["model"].queryset = CarModel.objects.filter(
id_car_make_id=make_id
)
if model_id:
self.fields['serie'].queryset = CarSerie.objects.filter(id_car_model_id=model_id)
self.fields["serie"].queryset = CarSerie.objects.filter(
id_car_model_id=model_id
)
if serie_id:
self.fields['trim'].queryset = CarTrim.objects.filter(id_car_serie_id=serie_id)
self.fields["trim"].queryset = CarTrim.objects.filter(
id_car_serie_id=serie_id
)
class RecallCreateForm(forms.ModelForm):
class Meta:
model = Recall
fields = ['title', 'description', 'make', 'model', 'serie', 'trim', 'year_from', 'year_to']
fields = [
"title",
"description",
"make",
"model",
"serie",
"trim",
"year_from",
"year_to",
]
widgets = {
'make': forms.Select(attrs={'class': 'form-control'}),
'model': forms.Select(attrs={'class': 'form-control'}),
'serie': forms.Select(attrs={'class': 'form-control'}),
'trim': forms.Select(attrs={'class': 'form-control'}),
'title': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control'}),
'year_from': forms.NumberInput(attrs={'class': 'form-control'}),
'year_to': forms.NumberInput(attrs={'class': 'form-control'}),
"make": forms.Select(attrs={"class": "form-control"}),
"model": forms.Select(attrs={"class": "form-control"}),
"serie": forms.Select(attrs={"class": "form-control"}),
"trim": forms.Select(attrs={"class": "form-control"}),
"title": forms.TextInput(attrs={"class": "form-control"}),
"description": forms.Textarea(attrs={"class": "form-control"}),
"year_from": forms.NumberInput(attrs={"class": "form-control"}),
"year_to": forms.NumberInput(attrs={"class": "form-control"}),
}
class TicketForm(forms.ModelForm):
class Meta:
model = Ticket
fields = ['subject', 'description', 'priority']
fields = ["subject", "description", "priority"]
widgets = {
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
"description": forms.Textarea(attrs={"class": "form-control", "rows": 10}),
}
class TicketResolutionForm(forms.ModelForm):
class Meta:
model = Ticket
fields = ['status', 'resolution_notes']
fields = ["status", "resolution_notes"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit status choices to resolution options
self.fields['status'].choices = [
('resolved', 'Resolved'),
('closed', 'Closed')
]
self.fields["status"].choices = [("resolved", "Resolved"), ("closed", "Closed")]

View File

@ -1,9 +1,10 @@
import logging
from inventory.models import Dealer
from .utils import get_accounts_data,create_account
from .utils import get_accounts_data, create_account
logger = logging.getLogger(__name__)
def check_create_coa_accounts(task):
logger.info("Checking if all accounts are created")
instance = task.kwargs["dealer"]
@ -17,7 +18,8 @@ def check_create_coa_accounts(task):
logger.info(f"Default account does not exist: {account_data['code']}")
create_account(entity, coa, account_data)
def print_results(task):
dealer= task.kwargs["dealer"]
print("HOOK: ",dealer)
print("HOOK: ",dealer.pk)
dealer = task.kwargs["dealer"]
print("HOOK: ", dealer)
print("HOOK: ", dealer.pk)

View File

@ -8,11 +8,12 @@ from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django_ledger.models import InvoiceModel,EstimateModel
from inventory.models import ExtraInfo,Notification,CustomGroup
from django_ledger.models import InvoiceModel, EstimateModel
from inventory.models import ExtraInfo, Notification, CustomGroup
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Handles invoices due date reminders"
@ -33,27 +34,30 @@ class Command(BaseCommand):
def invocie_expiration_reminders(self):
"""Queue email reminders for expiring plans"""
reminder_days = getattr(settings, 'INVOICE_PAST_DUE_REMIND', [3, 7, 14])
reminder_days = getattr(settings, "INVOICE_PAST_DUE_REMIND", [3, 7, 14])
today = timezone.now().date()
for days in reminder_days:
target_date = today + timedelta(days=days)
expiring_plans = InvoiceModel.objects.filter(
date_due=target_date
).select_related('customer','ce_model')
).select_related("customer", "ce_model")
for inv in expiring_plans:
# dealer = inv.customer.customer_set.first().dealer
subject = f"Your invoice is due in {days} days"
message = render_to_string('emails/invoice_past_due_reminder.txt', {
'customer_name': inv.customer.customer_name,
'invoice_number': inv.invoice_number,
'amount_due': inv.amount_due,
'days_past_due': inv.due_in_days(),
'SITE_NAME': settings.SITE_NAME
})
message = render_to_string(
"emails/invoice_past_due_reminder.txt",
{
"customer_name": inv.customer.customer_name,
"invoice_number": inv.invoice_number,
"amount_due": inv.amount_due,
"days_past_due": inv.due_in_days(),
"SITE_NAME": settings.SITE_NAME,
},
)
send_email(
'noreply@yourdomain.com',
"noreply@yourdomain.com",
inv.customer.email,
subject,
message,
@ -65,21 +69,24 @@ class Command(BaseCommand):
"""Queue email reminders for expiring plans"""
today = timezone.now().date()
expiring_plans = InvoiceModel.objects.filter(
date_due__lte = today
).select_related('customer','ce_model')
date_due__lte=today
).select_related("customer", "ce_model")
# Send email
for inv in expiring_plans:
dealer = inv.customer.customer_set.first().dealer
subject = f"Your invoice is past due"
message = render_to_string('emails/invoice_past_due.txt', {
'customer_name': inv.customer.customer_name,
'invoice_number': inv.invoice_number,
'amount_due': inv.amount_due,
'days_past_due': (today - inv.date_due).days,
'SITE_NAME': settings.SITE_NAME
})
message = render_to_string(
"emails/invoice_past_due.txt",
{
"customer_name": inv.customer.customer_name,
"invoice_number": inv.invoice_number,
"amount_due": inv.amount_due,
"days_past_due": (today - inv.date_due).days,
"SITE_NAME": settings.SITE_NAME,
},
)
# send notification to accountatnt
recipients = (
@ -90,24 +97,28 @@ class Command(BaseCommand):
)
for rec in recipients:
Notification.objects.create(
user=rec,
message=_(
"""
user=rec,
message=_(
"""
Invoice {invoice_number} is past due,please your
<a href="{url}" target="_blank">View</a>.
"""
).format(
invoice_number=inv.invoice_number,
url=reverse(
"invoice_detail",
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "pk": inv.pk},
).format(
invoice_number=inv.invoice_number,
url=reverse(
"invoice_detail",
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"pk": inv.pk,
},
),
),
)
# send email to customer
send_email(
'noreply@yourdomain.com',
"noreply@yourdomain.com",
inv.customer.email,
subject,
message,
@ -131,4 +142,4 @@ class Command(BaseCommand):
# created__lt=cutoff,
# status=Order.STATUS.NEW
# ).delete()
# self.stdout.write(f"Cleaned up {count} old incomplete orders")
# self.stdout.write(f"Cleaned up {count} old incomplete orders")

View File

@ -2,9 +2,11 @@ from decimal import Decimal
import random
from django.core.management.base import BaseCommand
from inventory.models import Car
from django_ledger.models import EntityModel,InvoiceModel,ItemModel
from django_ledger.models import EntityModel, InvoiceModel, ItemModel
from inventory.utils import CarFinanceCalculator
from rich import print
class Command(BaseCommand):
help = ""
@ -14,27 +16,43 @@ class Command(BaseCommand):
admin = e.admin
# estimate = e.get_estimates().first()
# e.create_invoice(coa_model=e.get_default_coa(), customer_model=customer, terms="net_30")
i=InvoiceModel.objects.first()
i = InvoiceModel.objects.first()
calc = CarFinanceCalculator(i)
data = calc.get_finance_data()
for car_data in data['cars']:
car = i.get_itemtxs_data()[0].filter(
item_model__car__vin=car_data['vin']
).first().item_model.car
calc = CarFinanceCalculator(i)
data = calc.get_finance_data()
for car_data in data["cars"]:
car = (
i.get_itemtxs_data()[0]
.filter(item_model__car__vin=car_data["vin"])
.first()
.item_model.car
)
print("car", car)
qty = Decimal(car_data['quantity'])
qty = Decimal(car_data["quantity"])
print("qty", qty)
# amounts from calculator
net_car_price = Decimal(car_data['total']) # after discount
net_add_price = Decimal(data['total_additionals']) # per car or split however you want
vat_amount = Decimal(data['total_vat_amount']) * qty # prorate if multi-qty
net_car_price = Decimal(car_data["total"]) # after discount
net_add_price = Decimal(
data["total_additionals"]
) # per car or split however you want
vat_amount = Decimal(data["total_vat_amount"]) * qty # prorate if multi-qty
# grand_total = net_car_price + net_add_price + vat_amount
grand_total = Decimal(data['grand_total'])
cost_total = Decimal(car_data['cost_price']) * qty
grand_total = Decimal(data["grand_total"])
cost_total = Decimal(car_data["cost_price"]) * qty
print("net_car_price", net_car_price, "net_add_price", net_add_price, "vat_amount", vat_amount, "grand_total", grand_total, "cost_total", cost_total)
print(
"net_car_price",
net_car_price,
"net_add_price",
net_add_price,
"vat_amount",
vat_amount,
"grand_total",
grand_total,
"cost_total",
cost_total,
)
# acc_cars = e.get_coa_accounts().get(name="Inventory (Cars)")
# acc_sales = e.get_coa_accounts().get(name="Car Sales")
@ -76,4 +94,4 @@ class Command(BaseCommand):
# operation=InvoiceModel.ITEMIZE_APPEND)
# print(i.amount_due)
# i.save()
# i.save()

View File

@ -2,8 +2,11 @@ from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
import datetime
from inventory.models import Dealer
from plans.models import Plan, Order,PlanPricing
from plans.models import Plan, Order, PlanPricing
User = get_user_model()
class Command(BaseCommand):
help = ""
@ -25,4 +28,4 @@ class Command(BaseCommand):
)
order.complete_order()
print(user.userplan)
print(user.userplan)

View File

@ -11,6 +11,7 @@ from inventory.tasks import send_bilingual_reminder, handle_email_result
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Handles subscription plan maintenance tasks"
@ -30,17 +31,18 @@ class Command(BaseCommand):
def send_expiration_reminders(self):
"""Queue email reminders for expiring plans"""
reminder_days = getattr(settings, 'PLANS_EXPIRATION_REMIND', [3, 7, 14])
reminder_days = getattr(settings, "PLANS_EXPIRATION_REMIND", [3, 7, 14])
today = timezone.now().date()
for days in reminder_days:
target_date = today + timedelta(days=days)
expiring_plans = UserPlan.objects.filter(
active=True,
expire=target_date
).select_related('user', 'plan')
active=True, expire=target_date
).select_related("user", "plan")
self.stdout.write(f"Queuing {days}-day reminders for {expiring_plans.count()} plans")
self.stdout.write(
f"Queuing {days}-day reminders for {expiring_plans.count()} plans"
)
for user_plan in expiring_plans:
# Queue email task
@ -50,14 +52,13 @@ class Command(BaseCommand):
user_plan.plan_id,
user_plan.expire,
days,
hook=handle_email_result
hook=handle_email_result,
)
def deactivate_expired_plans(self):
"""Deactivate plans that have expired (synchronous)"""
expired_plans = UserPlan.objects.filter(
active=True,
expire__lt=timezone.now().date()
active=True, expire__lt=timezone.now().date()
)
count = expired_plans.update(active=False)
self.stdout.write(f"Deactivated {count} expired plans")
@ -66,7 +67,6 @@ class Command(BaseCommand):
"""Delete incomplete orders older than 30 days"""
cutoff = timezone.now() - timedelta(days=30)
count, _ = Order.objects.filter(
created__lt=cutoff,
status=Order.STATUS.NEW
created__lt=cutoff, status=Order.STATUS.NEW
).delete()
self.stdout.write(f"Cleaned up {count} old incomplete orders")
self.stdout.write(f"Cleaned up {count} old incomplete orders")

View File

@ -5,5 +5,10 @@ from django_q.tasks import async_task, result
class Command(BaseCommand):
def handle(self, *args, **kwargs):
from inventory.models import Dealer
instance = Dealer.objects.first()
async_task(func="inventory.tasks.test_task",dealer=instance,hook="inventory.hooks.print_results")
async_task(
func="inventory.tasks.test_task",
dealer=instance,
hook="inventory.hooks.print_results",
)

View File

@ -3,21 +3,24 @@ import json, random, string, decimal
from django.core.management.base import BaseCommand
from django.test import Client
from django.contrib.auth import get_user_model
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan
from inventory.tasks import create_user_dealer
from inventory import models # adjust import to your app
from inventory import models # adjust import to your app
from django_q.tasks import async_task
User = get_user_model()
class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views"
def add_arguments(self, parser):
parser.add_argument('--count', type=int, default=1, help='Number of dealers to seed')
parser.add_argument(
"--count", type=int, default=1, help="Number of dealers to seed"
)
def handle(self, *args, **opts):
count = opts['count']
count = opts["count"]
client = Client() # lives inside management command
for n in range(6, 9):
@ -43,7 +46,16 @@ class Command(BaseCommand):
"address": f"Street {n}, Riyadh",
}
dealer = create_user_dealer(payload['email'], payload['password'], payload['name'], payload['arabic_name'], payload['phone_number'], payload['crn'], payload['vrn'], payload['address'])
dealer = create_user_dealer(
payload["email"],
payload["password"],
payload["name"],
payload["arabic_name"],
payload["phone_number"],
payload["crn"],
payload["vrn"],
payload["address"],
)
user = dealer.user
self._assign_random_plan(user)
self._services(dealer)
@ -61,7 +73,7 @@ class Command(BaseCommand):
return payload["email"]
def _assign_random_plan(self,user):
def _assign_random_plan(self, user):
"""
Pick a random Plan and create + initialize a UserPlan for the user.
"""
@ -72,14 +84,13 @@ class Command(BaseCommand):
plan = random.choice(plans)
user_plan, created = UserPlan.objects.get_or_create(
user=user,
defaults={'plan': plan, 'active': True}
user=user, defaults={"plan": plan, "active": True}
)
if created:
user_plan.initialize()
return user_plan
def _services(self,dealer):
def _services(self, dealer):
additional_services = [
{
"name": "Vehicle registration transfer assistance",
@ -114,5 +125,5 @@ class Command(BaseCommand):
price=additional_service["price"],
description=additional_service["description"],
dealer=dealer,
uom="Unit"
uom="Unit",
)

View File

@ -4,11 +4,31 @@ import json, random, string, decimal
from django.core.management.base import BaseCommand
from django.test import Client
from django.contrib.auth import get_user_model
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo,Plan
from plans.models import Order, PlanPricing, AbstractOrder, UserPlan, BillingInfo, Plan
from inventory.services import decodevin
from inventory.tasks import create_user_dealer
from inventory.models import AdditionalServices, Car, CarColors, CarFinance, CarMake, CustomGroup, Customer, Dealer, ExteriorColors, InteriorColors, Lead, UnitOfMeasure,Vendor,Staff
from django_ledger.models import PurchaseOrderModel,ItemTransactionModel,ItemModel,EntityModel
from inventory.models import (
AdditionalServices,
Car,
CarColors,
CarFinance,
CarMake,
CustomGroup,
Customer,
Dealer,
ExteriorColors,
InteriorColors,
Lead,
UnitOfMeasure,
Vendor,
Staff,
)
from django_ledger.models import (
PurchaseOrderModel,
ItemTransactionModel,
ItemModel,
EntityModel,
)
from django_q.tasks import async_task
from faker import Faker
from appointment.models import Appointment, AppointmentRequest, Service, StaffMember
@ -16,6 +36,7 @@ from appointment.models import Appointment, AppointmentRequest, Service, StaffMe
User = get_user_model()
fake = Faker()
class Command(BaseCommand):
help = "Seed a full dealership via the real signup & downstream views"
@ -31,7 +52,6 @@ class Command(BaseCommand):
# self._create_randome_services(dealer)
# self._create_random_lead(dealer)
# dealer = Dealer.objects.get(name="Dealer #6")
# coa_model = dealer.entity.get_default_coa()
# inventory_account = dealer.entity.get_all_accounts().get(name="Inventory (Cars)")
@ -43,20 +63,32 @@ class Command(BaseCommand):
self.stdout.write(self.style.SUCCESS(f"✅ PO created for {dealers}"))
def _create_random_po(self, dealer):
for i in range(random.randint(1,70)):
for i in range(random.randint(1, 70)):
try:
e: EntityModel = dealer.entity
e.create_purchase_order(po_title=f"Test PO {random.randint(1,9999)}-{i}")
e.create_purchase_order(
po_title=f"Test PO {random.randint(1, 9999)}-{i}"
)
except Exception as e:
self.stderr.write(self.style.ERROR(f"Error : {e}"))
def _create_random_vendors(self, dealer):
for i in range(random.randint(1,50)):
for i in range(random.randint(1, 50)):
try:
name = fake.name()
n = random.randint(1,9999)
phone = f"05678{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}{random.randint(0,9)}"
Vendor.objects.create(dealer=dealer, name=f"{name}{n}", arabic_name=f"{name}{n}", email=f"{name}{n}@tenhal.sa", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {name}{n}")
n = random.randint(1, 9999)
phone = f"05678{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
Vendor.objects.create(
dealer=dealer,
name=f"{name}{n}",
arabic_name=f"{name}{n}",
email=f"{name}{n}@tenhal.sa",
phone_number=phone,
crn=f"CRN {n}",
vrn=f"VRN {n}",
address=f"Address {fake.address()}",
contact_person=f"Contact Person {name}{n}",
)
except Exception as e:
pass
@ -65,7 +97,9 @@ class Command(BaseCommand):
name = f"{fake.name()}{i}"
email = fake.email()
password = "Tenhal@123"
user = User.objects.create_user(username=email, email=email, password=password)
user = User.objects.create_user(
username=email, email=email, password=password
)
user.is_staff = True
user.save()
@ -74,17 +108,24 @@ class Command(BaseCommand):
# for service in services:
# staff_member.services_offered.add(service)
staff = Staff.objects.create(dealer=dealer,user=user,name=name,arabic_name=name,phone_number=fake.phone_number(),active=True)
staff = Staff.objects.create(
dealer=dealer,
user=user,
name=name,
arabic_name=name,
phone_number=fake.phone_number(),
active=True,
)
groups = CustomGroup.objects.filter(dealer=dealer)
random_group = random.choice(list(groups))
staff.add_group(random_group.group)
# for i in range(random.randint(1,15)):
# n = random.randint(1,9999)
# phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}"
# Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}")
# n = random.randint(1,9999)
# phone = f"05678{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}{random.randint(1,9999)}"
# Vendor.objects.create(dealer=dealer, name=f"{fake.name}", arabic_name=f"{fake.first_name_female()} {fake.last_name_female()}", email=f"vendor{n}@test.com", phone_number=phone,crn=f"CRN {n}", vrn=f"VRN {n}", address=f"Address {fake.address()}",contact_person=f"Contact Person {n}")
def _create_random_cars(self,dealer):
def _create_random_cars(self, dealer):
vendors = Vendor.objects.filter(dealer=dealer).all()
vin_list = [
@ -103,18 +144,20 @@ class Command(BaseCommand):
]
for vin in vin_list:
try:
for _ in range(random.randint(1,2)):
for _ in range(random.randint(1, 2)):
vin = f"{vin[:-4]}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}{random.randint(0, 9)}"
result = decodevin(vin)
make = CarMake.objects.get(name=result["maker"])
model = make.carmodel_set.filter(name__contains=result["model"]).first()
model = make.carmodel_set.filter(
name__contains=result["model"]
).first()
if not model or model == "":
model = random.choice(make.carmodel_set.all())
year = result["modelYear"]
serie = random.choice(model.carserie_set.all())
trim = random.choice(serie.cartrim_set.all())
vendor = random.choice(vendors)
print(make, model, serie, trim, vendor,vin)
print(make, model, serie, trim, vendor, vin)
car = Car.objects.create(
vin=vin,
id_car_make=make,
@ -128,9 +171,12 @@ class Command(BaseCommand):
mileage=0,
)
print(car)
cp=random.randint(10000, 100000)
cp = random.randint(10000, 100000)
CarFinance.objects.create(
car=car, cost_price=cp, selling_price=0,marked_price=cp+random.randint(2000, 7000)
car=car,
cost_price=cp,
selling_price=0,
marked_price=cp + random.randint(2000, 7000),
)
CarColors.objects.create(
car=car,
@ -141,8 +187,8 @@ class Command(BaseCommand):
except Exception as e:
print(e)
def _create_random_customers(self,dealer):
for i in range(random.randint(1,60)):
def _create_random_customers(self, dealer):
for i in range(random.randint(1, 60)):
try:
c = Customer(
dealer=dealer,
@ -161,7 +207,7 @@ class Command(BaseCommand):
except Exception as e:
pass
def _create_randome_services(self,dealer):
def _create_randome_services(self, dealer):
additional_services = [
{
"name": "Vehicle registration transfer assistance",
@ -196,12 +242,11 @@ class Command(BaseCommand):
price=additional_service["price"],
description=additional_service["description"],
dealer=dealer,
uom=uom
uom=uom,
)
def _create_random_lead(self,dealer):
for i in range(random.randint(1,60)):
def _create_random_lead(self, dealer):
for i in range(random.randint(1, 60)):
try:
first_name = fake.name()
last_name = fake.last_name()
@ -224,7 +269,7 @@ class Command(BaseCommand):
id_car_model=model,
source="website",
channel="website",
staff=staff
staff=staff,
)
c = Customer(
dealer=dealer,
@ -243,4 +288,4 @@ class Command(BaseCommand):
lead.customer = c
lead.save()
except Exception as e:
pass
pass

View File

@ -152,11 +152,22 @@ class DealerSlugMiddleware:
def process_view(self, request, view_func, view_args, view_kwargs):
paths = [
"/ar/signup/", "/en/signup/", "/ar/login/", "/en/login/",
"/ar/logout/", "/en/logout/", "/en/ledger/", "/ar/ledger/",
"/en/notifications/", "/ar/notifications/", "/en/appointment/",
"/ar/appointment/", "/en/feature/recall/","/ar/feature/recall/",
"/ar/help_center/", "/en/help_center/",
"/ar/signup/",
"/en/signup/",
"/ar/login/",
"/en/login/",
"/ar/logout/",
"/en/logout/",
"/en/ledger/",
"/ar/ledger/",
"/en/notifications/",
"/ar/notifications/",
"/en/appointment/",
"/ar/appointment/",
"/en/feature/recall/",
"/ar/feature/recall/",
"/ar/help_center/",
"/en/help_center/",
]
print("------------------------------------")
print(request.path in paths)

View File

@ -42,12 +42,14 @@ from django_ledger.models import (
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.serializers.json import DjangoJSONEncoder
# from appointment.models import StaffMember
from plans.quota import get_user_quota
from plans.models import UserPlan
from django.db.models import Q
from imagekit.models import ImageSpecField
from imagekit.processors import ResizeToFill
# from plans.models import AbstractPlan
# from simple_history.models import HistoricalRecords
from plans.models import Invoice
@ -229,7 +231,9 @@ class CarMake(models.Model, LocalizedNameMixin):
name = models.CharField(max_length=255, blank=True, null=True)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
arabic_name = models.CharField(max_length=255, blank=True, null=True)
logo = models.ImageField(_("logo"), upload_to="car_make", blank=True, null=True,default="user-logo.jpg")
logo = models.ImageField(
_("logo"), upload_to="car_make", blank=True, null=True, default="user-logo.jpg"
)
is_sa_import = models.BooleanField(default=False)
car_type = models.SmallIntegerField(choices=CarType.choices, blank=True, null=True)
@ -589,7 +593,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
"price_": str(self.price_),
"taxable": self.taxable,
"uom": self.uom,
"service_tax":str(self.service_tax)
"service_tax": str(self.service_tax),
}
@property
@ -604,9 +608,7 @@ class AdditionalServices(models.Model, LocalizedNameMixin):
@property
def service_tax(self):
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return (
Decimal(self.price * vat.rate)
)
return Decimal(self.price * vat.rate)
class Meta:
verbose_name = _("Additional Services")
@ -683,10 +685,13 @@ class Car(Base):
)
#
additional_services = models.ManyToManyField(
AdditionalServices, related_name="additionals", blank=True,null=True
AdditionalServices, related_name="additionals", blank=True, null=True
)
cost_price = models.DecimalField(
max_digits=14, decimal_places=2, verbose_name=_("Cost Price"),default=Decimal("0.00")
max_digits=14,
decimal_places=2,
verbose_name=_("Cost Price"),
default=Decimal("0.00"),
)
selling_price = models.DecimalField(
max_digits=14,
@ -710,7 +715,7 @@ class Car(Base):
remarks = models.TextField(blank=True, null=True, verbose_name=_("Remarks"))
mileage = models.IntegerField(blank=True, null=True, verbose_name=_("Mileage"))
receiving_date = models.DateTimeField(verbose_name=_("Receiving Date"))
sold_date=models.DateTimeField(verbose_name=_("Sold Date"),null=True,blank=True)
sold_date = models.DateTimeField(verbose_name=_("Sold Date"), null=True, blank=True)
hash = models.CharField(
max_length=64, blank=True, null=True, verbose_name=_("Hash")
)
@ -773,6 +778,7 @@ class Car(Base):
@property
def logo(self):
return getattr(self.id_car_make, "logo", "")
# @property
# def additional_services(self):
# return self.additional_services.all()
@ -787,9 +793,15 @@ class Car(Base):
)
except Exception:
return False
@property
def invoice(self):
return self.item_model.invoicemodel_set.first if self.item_model.invoicemodel_set.first() else None
return (
self.item_model.invoicemodel_set.first
if self.item_model.invoicemodel_set.first()
else None
)
def get_transfer(self):
return self.transfer_logs.filter(active=True).first()
@ -873,39 +885,54 @@ class Car(Base):
car=self, exterior=exterior, interior=interior
)
self.save()
@property
def logo(self):
return self.id_car_make.logo.url if self.id_car_make.logo else None
#
@property
def get_additional_services_amount(self):
return sum([Decimal(x.price) for x in self.additional_services.all()])
@property
def get_additional_services_amount_(self):
return sum([Decimal(x.price_) for x in self.additional_services.all()])
@property
def get_additional_services_vat(self):
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
return sum([Decimal((x.price)*(vat.rate)) for x in self.additional_services.filter(taxable=True)])
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return sum(
[
Decimal((x.price) * (vat.rate))
for x in self.additional_services.filter(taxable=True)
]
)
def get_additional_services(self):
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
return {"services": [[x,((x.price)*(vat.rate) if x.taxable else 0)] for x in self.additional_services.all()],
"total_":self.get_additional_services_amount_,
"total":self.get_additional_services_amount,
"services_vat":self.get_additional_services_vat}
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return {
"services": [
[x, ((x.price) * (vat.rate) if x.taxable else 0)]
for x in self.additional_services.all()
],
"total_": self.get_additional_services_amount_,
"total": self.get_additional_services_amount,
"services_vat": self.get_additional_services_vat,
}
@property
def final_price(self):
return Decimal(self.marked_price -self.discount)
return Decimal(self.marked_price - self.discount)
@property
def vat_amount(self):
vat = VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
vat = VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
return Decimal(self.final_price) * (vat.rate)
@property
def total_services_and_car_vat(self):
return self.vat_amount+self.get_additional_services()['services_vat']
return self.vat_amount + self.get_additional_services()["services_vat"]
@property
def final_price_plus_vat(self):
@ -913,29 +940,32 @@ class Car(Base):
@property
def final_price_plus_services_plus_vat(self):
return Decimal(self.final_price_plus_vat) + Decimal(self.get_additional_services()['total_']) #total services with vat and car_sell price with vat
return Decimal(self.final_price_plus_vat) + Decimal(
self.get_additional_services()["total_"]
) # total services with vat and car_sell price with vat
# to be used after invoice is created
@property
def invoice(self):
return self.item_model.invoicemodel_set.first() or None
@property
def estimate(self):
return getattr(self.invoice,'ce_model',None)
return getattr(self.invoice, "ce_model", None)
@property
def discount(self):
if not self.estimate:
return 0
try:
instance = ExtraInfo.objects.get(
dealer=self.dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=self.estimate.pk,
)
return Decimal(instance.data.get('discount',0))
dealer=self.dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=self.estimate.pk,
)
return Decimal(instance.data.get("discount", 0))
except ExtraInfo.DoesNotExist:
return Decimal(0)
return Decimal(0)
# def get_discount_amount(self,estimate,user):
# try:
@ -961,10 +991,6 @@ class Car(Base):
# return round(self.total_discount + self.vat_amount + self.total_additionals, 2)
class CarTransfer(models.Model):
car = models.ForeignKey(
"Car",
@ -1001,7 +1027,7 @@ class CarTransfer(models.Model):
@property
def total_price(self):
return self.quantity * self.car.total_vat # TODO : check later
return self.quantity * self.car.total_vat # TODO : check later
class Meta:
verbose_name = _("Car Transfer Log")
@ -1311,7 +1337,11 @@ class Dealer(models.Model, LocalizedNameMixin):
)
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
@ -1365,6 +1395,7 @@ class Dealer(models.Model, LocalizedNameMixin):
@property
def customers(self):
return models.Customer.objects.filter(dealer=self)
@property
def user_quota(self):
try:
@ -1415,6 +1446,7 @@ class Dealer(models.Model, LocalizedNameMixin):
def invoices(self):
return Invoice.objects.filter(order__user=self.user)
class StaffTypes(models.TextChoices):
# MANAGER = "manager", _("Manager")
INVENTORY = "inventory", _("Inventory")
@ -1429,15 +1461,17 @@ class Staff(models.Model):
# staff_member = models.OneToOneField(
# StaffMember, on_delete=models.CASCADE, related_name="staff"
# )
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="staff"
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="staff")
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
staff_type = models.CharField(
choices=StaffTypes.choices, max_length=255, verbose_name=_("Staff Type")
)
@ -1445,7 +1479,11 @@ class Staff(models.Model):
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos/staff", blank=True, null=True, verbose_name=_("Image"),default="default-image/user.jpg"
upload_to="logos/staff",
blank=True,
null=True,
verbose_name=_("Image"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
@ -1480,6 +1518,7 @@ class Staff(models.Model):
@property
def fullname(self):
return self.first_name + " " + self.last_name
def deactivate_account(self):
self.active = False
self.user.is_active = False
@ -1544,8 +1583,7 @@ class Staff(models.Model):
permissions = []
constraints = [
models.UniqueConstraint(
fields=['dealer', 'user'],
name='unique_staff_email_per_dealer'
fields=["dealer", "user"], name="unique_staff_email_per_dealer"
)
]
@ -1648,7 +1686,11 @@ class Customer(models.Model):
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="customer_profile", null=True, blank=True
User,
on_delete=models.CASCADE,
related_name="customer_profile",
null=True,
blank=True,
)
title = models.CharField(
choices=Title.choices, default=Title.NA, max_length=10, verbose_name=_("Title")
@ -1676,7 +1718,11 @@ class Customer(models.Model):
)
active = models.BooleanField(default=True, verbose_name=_("Active"))
image = models.ImageField(
upload_to="customers/", blank=True, null=True, verbose_name=_("Image"),default="default-image/user-jpg"
upload_to="customers/",
blank=True,
null=True,
verbose_name=_("Image"),
default="default-image/user-jpg",
)
thumbnail = ImageSpecField(
source="image",
@ -1708,8 +1754,7 @@ class Customer(models.Model):
class Meta:
constraints = [
models.UniqueConstraint(
fields=['dealer', 'email'],
name='unique_customer_email_per_dealer'
fields=["dealer", "email"], name="unique_customer_email_per_dealer"
)
]
verbose_name = _("Customer")
@ -1779,13 +1824,13 @@ class Customer(models.Model):
user, created = User.objects.get_or_create(
username=self.email,
defaults={
'email': self.email,
'first_name': self.first_name,
'last_name': self.last_name,
'password': make_random_password(),
'is_staff': False,
'is_superuser': False,
'is_active': False if for_lead else True,
"email": self.email,
"first_name": self.first_name,
"last_name": self.last_name,
"password": make_random_password(),
"is_staff": False,
"is_superuser": False,
"is_active": False if for_lead else True,
},
)
self.user = user
@ -1822,7 +1867,11 @@ class Organization(models.Model, LocalizedNameMixin):
CustomerModel, on_delete=models.SET_NULL, null=True
)
user = models.OneToOneField(
User, on_delete=models.CASCADE, related_name="organization_profile", null=True, blank=True
User,
on_delete=models.CASCADE,
related_name="organization_profile",
null=True,
blank=True,
)
name = models.CharField(max_length=255, verbose_name=_("Name"))
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
@ -1831,12 +1880,20 @@ class Organization(models.Model, LocalizedNameMixin):
)
vrn = models.CharField(max_length=15, verbose_name=_("VAT Registration Number"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
logo = models.ImageField(
upload_to="logos", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg"
upload_to="logos",
blank=True,
null=True,
verbose_name=_("Logo"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
@ -1965,7 +2022,11 @@ class Representative(models.Model, LocalizedNameMixin):
id_number = models.CharField(
max_length=10, unique=True, verbose_name=_("ID Number")
)
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
@ -1985,7 +2046,11 @@ class Lead(models.Model):
first_name = models.CharField(max_length=50, verbose_name=_("First Name"))
last_name = models.CharField(max_length=50, verbose_name=_("Last Name"))
email = models.EmailField(verbose_name=_("Email"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
address = models.CharField(
max_length=200, blank=True, null=True, verbose_name=_("Address")
)
@ -2175,8 +2240,9 @@ class Lead(models.Model):
.order_by("-updated")
.first()
)
def get_absolute_url(self):
return reverse("lead_detail", args=[self.dealer.slug,self.slug])
return reverse("lead_detail", args=[self.dealer.slug, self.slug])
def save(self, *args, **kwargs):
if not self.slug:
@ -2246,6 +2312,7 @@ class Schedule(models.Model):
@property
def duration(self):
return (self.end_time - self.start_time).seconds
@property
def schedule_past_date(self):
if self.scheduled_at < now():
@ -2255,6 +2322,7 @@ class Schedule(models.Model):
@property
def get_purpose(self):
return self.purpose.replace("_", " ").title()
class Meta:
ordering = ["-scheduled_at"]
verbose_name = _("Schedule")
@ -2673,11 +2741,19 @@ class Vendor(models.Model, LocalizedNameMixin):
arabic_name = models.CharField(max_length=255, verbose_name=_("Arabic Name"))
name = models.CharField(max_length=255, verbose_name=_("English Name"))
contact_person = models.CharField(max_length=100, verbose_name=_("Contact Person"))
phone_number = models.CharField(max_length=255, verbose_name=_("Phone Number"),validators=[SaudiPhoneNumberValidator()])
phone_number = models.CharField(
max_length=255,
verbose_name=_("Phone Number"),
validators=[SaudiPhoneNumberValidator()],
)
email = models.EmailField(max_length=255, verbose_name=_("Email Address"))
address = models.CharField(max_length=200, verbose_name=_("Address"))
logo = models.ImageField(
upload_to="logos/vendors", blank=True, null=True, verbose_name=_("Logo"),default="default-image/user.jpg"
upload_to="logos/vendors",
blank=True,
null=True,
verbose_name=_("Logo"),
default="default-image/user.jpg",
)
thumbnail = ImageSpecField(
source="logo",
@ -3225,7 +3301,6 @@ class CustomGroup(models.Model):
"activity",
"payment",
"vendor",
],
other_perms=[
"view_car",
@ -3236,8 +3311,7 @@ class CustomGroup(models.Model):
"view_saleorder",
"view_leads",
"view_opportunity",
'view_customer'
"view_customer",
],
)
self.set_permissions(
@ -3533,7 +3607,7 @@ class ExtraInfo(models.Model):
return f"ExtraInfo for {self.content_object} ({self.content_type})"
@classmethod
def get_sale_orders(cls, staff=None, is_dealer=False,dealer=None):
def get_sale_orders(cls, staff=None, is_dealer=False, dealer=None):
if not staff and not is_dealer:
return []
@ -3546,11 +3620,13 @@ class ExtraInfo(models.Model):
content_type=content_type,
related_content_type=related_content_type,
related_object_id__isnull=False,
).union(cls.objects.filter(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(User),
))
).union(
cls.objects.filter(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
related_content_type=ContentType.objects.get_for_model(User),
)
)
else:
qs = cls.objects.filter(
dealer=dealer,
@ -3559,7 +3635,17 @@ class ExtraInfo(models.Model):
related_object_id=staff.pk,
)
# qs = qs.select_related("customer","estimate","invoice")
data = SaleOrder.objects.filter(pk__in=[x.content_object.sale_orders.select_related("customer","estimate","invoice").first().pk for x in qs if x.content_object.sale_orders.first()])
data = SaleOrder.objects.filter(
pk__in=[
x.content_object.sale_orders.select_related(
"customer", "estimate", "invoice"
)
.first()
.pk
for x in qs
if x.content_object.sale_orders.first()
]
)
return data
@ -3572,7 +3658,7 @@ class ExtraInfo(models.Model):
# ]
@classmethod
def get_invoices(cls, staff=None, is_dealer=False,dealer=None):
def get_invoices(cls, staff=None, is_dealer=False, dealer=None):
if not staff and not is_dealer:
return []
@ -3585,11 +3671,13 @@ class ExtraInfo(models.Model):
content_type=content_type,
related_content_type=related_content_type,
related_object_id__isnull=False,
).union(cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=ContentType.objects.get_for_model(User),
))
).union(
cls.objects.filter(
dealer=dealer,
content_type=content_type,
related_content_type=ContentType.objects.get_for_model(User),
)
)
else:
qs = cls.objects.filter(
dealer=dealer,
@ -3608,32 +3696,16 @@ class Recall(models.Model):
title = models.CharField(max_length=200, verbose_name=_("Recall Title"))
description = models.TextField(verbose_name=_("Description"))
make = models.ForeignKey(
CarMake,
models.DO_NOTHING,
verbose_name=_("Make"),
null=True,
blank=True
CarMake, models.DO_NOTHING, verbose_name=_("Make"), null=True, blank=True
)
model = models.ForeignKey(
CarModel,
models.DO_NOTHING,
verbose_name=_("Model"),
null=True,
blank=True
CarModel, models.DO_NOTHING, verbose_name=_("Model"), null=True, blank=True
)
serie = models.ForeignKey(
CarSerie,
models.DO_NOTHING,
verbose_name=_("Series"),
null=True,
blank=True
CarSerie, models.DO_NOTHING, verbose_name=_("Series"), null=True, blank=True
)
trim = models.ForeignKey(
CarTrim,
models.DO_NOTHING,
verbose_name=_("Trim"),
null=True,
blank=True
CarTrim, models.DO_NOTHING, verbose_name=_("Trim"), null=True, blank=True
)
year_from = models.IntegerField(verbose_name=_("From Year"), null=True, blank=True)
year_to = models.IntegerField(verbose_name=_("To Year"), null=True, blank=True)
@ -3643,7 +3715,7 @@ class Recall(models.Model):
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_("Created By")
verbose_name=_("Created By"),
)
class Meta:
@ -3653,11 +3725,16 @@ class Recall(models.Model):
def __str__(self):
return self.title
class RecallNotification(models.Model):
recall = models.ForeignKey(Recall, on_delete=models.CASCADE, related_name='notifications')
dealer = models.ForeignKey("Dealer", on_delete=models.CASCADE, related_name='recall_notifications')
recall = models.ForeignKey(
Recall, on_delete=models.CASCADE, related_name="notifications"
)
dealer = models.ForeignKey(
"Dealer", on_delete=models.CASCADE, related_name="recall_notifications"
)
sent_at = models.DateTimeField(auto_now_add=True)
cars_affected = models.ManyToManyField(Car, related_name='recall_notifications')
cars_affected = models.ManyToManyField(Car, related_name="recall_notifications")
class Meta:
verbose_name = _("Recall Notification")
@ -3666,27 +3743,30 @@ class RecallNotification(models.Model):
def __str__(self):
return f"Notification for {self.dealer} about {self.recall}"
class Ticket(models.Model):
STATUS_CHOICES = [
('open', 'Open'),
('in_progress', 'In Progress'),
('resolved', 'Resolved'),
('closed', 'Closed'),
("open", "Open"),
("in_progress", "In Progress"),
("resolved", "Resolved"),
("closed", "Closed"),
]
PRIORITY_CHOICES = [
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('critical', 'Critical'),
("low", "Low"),
("medium", "Medium"),
("high", "High"),
("critical", "Critical"),
]
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='tickets')
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="tickets")
subject = models.CharField(max_length=200)
description = models.TextField()
resolution_notes = models.TextField(blank=True, null=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open')
priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="open")
priority = models.CharField(
max_length=20, choices=PRIORITY_CHOICES, default="medium"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@ -3697,7 +3777,7 @@ class Ticket(models.Model):
Returns None if ticket isn't resolved/closed.
Returns timedelta if resolved/closed.
"""
if self.status in ['resolved', 'closed'] and self.created_at:
if self.status in ["resolved", "closed"] and self.created_at:
return self.updated_at - self.created_at
return None
@ -3729,9 +3809,11 @@ class Ticket(models.Model):
class CarImage(models.Model):
car = models.OneToOneField('Car', on_delete=models.CASCADE, related_name='generated_image')
car = models.OneToOneField(
"Car", on_delete=models.CASCADE, related_name="generated_image"
)
image_hash = models.CharField(max_length=64, unique=True)
image = models.ImageField(upload_to='car_images/', null=True, blank=True)
image = models.ImageField(upload_to="car_images/", null=True, blank=True)
is_generating = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
@ -3752,5 +3834,5 @@ class CarImage(models.Model):
async_task(
generate_car_image_task,
self.id,
task_name=f"generate_car_image_{self.car.vin}"
)
task_name=f"generate_car_image_{self.car.vin}",
)

View File

@ -87,6 +87,7 @@ from inventory.models import Notification
import asyncio
from datetime import datetime
@database_sync_to_async
def get_user(user_id):
User = get_user_model()
@ -95,24 +96,24 @@ def get_user(user_id):
except User.DoesNotExist:
return AnonymousUser()
@database_sync_to_async
def get_notifications(user, last_id):
notifications = Notification.objects.filter(
user=user,
id__gt=last_id,
is_read=False
user=user, id__gt=last_id, is_read=False
).order_by("created")
return [
{
'id': n.id,
'message': n.message,
'created': n.created.isoformat(), # Convert datetime to string
'is_read': n.is_read
"id": n.id,
"message": n.message,
"created": n.created.isoformat(), # Convert datetime to string
"is_read": n.is_read,
}
for n in notifications
]
class NotificationSSEApp:
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
@ -143,15 +144,17 @@ class NotificationSSEApp:
for notification in notifications:
await self._send_notification(send, notification)
if notification['id'] > last_id:
last_id = notification['id']
if notification["id"] > last_id:
last_id = notification["id"]
# Send keep-alive comment every 15 seconds
await send({
"type": "http.response.body",
"body": b":keep-alive\n\n",
"more_body": True
})
await send(
{
"type": "http.response.body",
"body": b":keep-alive\n\n",
"more_body": True,
}
)
# await asyncio.sleep(3)
@ -161,16 +164,18 @@ class NotificationSSEApp:
await self._close_connection(send)
async def _send_headers(self, send):
await send({
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-type", b"text/event-stream"),
(b"cache-control", b"no-cache"),
(b"connection", b"keep-alive"),
(b"x-accel-buffering", b"no"),
]
})
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [
(b"content-type", b"text/event-stream"),
(b"cache-control", b"no-cache"),
(b"connection", b"keep-alive"),
(b"x-accel-buffering", b"no"),
],
}
)
async def _send_notification(self, send, notification):
try:
@ -179,27 +184,25 @@ class NotificationSSEApp:
f"event: notification\n"
f"data: {json.dumps(notification)}\n\n"
)
await send({
"type": "http.response.body",
"body": event_str.encode("utf-8"),
"more_body": True
})
await send(
{
"type": "http.response.body",
"body": event_str.encode("utf-8"),
"more_body": True,
}
)
except Exception as e:
print(f"Error sending notification: {e}")
async def _send_response(self, send, status, body):
await send({
"type": "http.response.start",
"status": status,
"headers": [(b"content-type", b"text/plain")]
})
await send({
"type": "http.response.body",
"body": body
})
await send(
{
"type": "http.response.start",
"status": status,
"headers": [(b"content-type", b"text/plain")],
}
)
await send({"type": "http.response.body", "body": body})
async def _close_connection(self, send):
await send({
"type": "http.response.body",
"body": b""
})
await send({"type": "http.response.body", "body": b""})

View File

@ -20,7 +20,12 @@ from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django_ledger.models import ItemTransactionModel,InvoiceModel,LedgerModel,EntityModel
from django_ledger.models import (
ItemTransactionModel,
InvoiceModel,
LedgerModel,
EntityModel,
)
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView
from django_ledger.forms.chart_of_accounts import (
@ -35,17 +40,28 @@ from django_ledger.forms.purchase_order import (
get_po_itemtxs_formset_class,
)
from django_ledger.views.purchase_order import PurchaseOrderModelModelViewQuerySetMixIn
from django_ledger.models import PurchaseOrderModel, EstimateModel, BillModel, ChartOfAccountModel
from django_ledger.models import (
PurchaseOrderModel,
EstimateModel,
BillModel,
ChartOfAccountModel,
)
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import UpdateView
from django.views.generic.base import RedirectView
from django.views.generic.list import ListView
from django.utils.translation import gettext_lazy as _
from django_ledger.forms.invoice import (BaseInvoiceModelUpdateForm, InvoiceModelCreateForEstimateForm,
get_invoice_itemtxs_formset_class,
DraftInvoiceModelUpdateForm, InReviewInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm, PaidInvoiceModelUpdateForm,
AccruedAndApprovedInvoiceModelUpdateForm, InvoiceModelCreateForm)
from django_ledger.forms.invoice import (
BaseInvoiceModelUpdateForm,
InvoiceModelCreateForEstimateForm,
get_invoice_itemtxs_formset_class,
DraftInvoiceModelUpdateForm,
InReviewInvoiceModelUpdateForm,
ApprovedInvoiceModelUpdateForm,
PaidInvoiceModelUpdateForm,
AccruedAndApprovedInvoiceModelUpdateForm,
InvoiceModelCreateForm,
)
logger = logging.getLogger(__name__)
@ -71,7 +87,11 @@ class PurchaseOrderModelUpdateView(
context = super().get_context_data(**kwargs)
context["entity_slug"] = dealer.entity.slug
context["po_ready_to_fulfill"] = [item for item in po_model.get_itemtxs_data()[0] if item.po_item_status == 'received']
context["po_ready_to_fulfill"] = [
item
for item in po_model.get_itemtxs_data()[0]
if item.po_item_status == "received"
]
if not itemtxs_formset:
itemtxs_qs = self.get_po_itemtxs_qs(po_model)
@ -776,12 +796,12 @@ class InventoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
slug_url_kwarg = 'invoice_pk'
slug_field = 'uuid'
context_object_name = 'invoice'
slug_url_kwarg = "invoice_pk"
slug_field = "uuid"
context_object_name = "invoice"
# template_name = 'inventory/sales/invoices/invoice_update.html'
form_class = BaseInvoiceModelUpdateForm
http_method_names = ['get', 'post']
http_method_names = ["get", "post"]
action_update_items = False
@ -802,115 +822,137 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
def get_form(self, form_class=None):
form_class = self.get_form_class()
if self.request.method == 'POST' and self.action_update_items:
if self.request.method == "POST" and self.action_update_items:
return form_class(
entity_slug=self.kwargs['entity_slug'],
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.dealer.user,
instance=self.object
instance=self.object,
)
return form_class(
entity_slug=self.kwargs['entity_slug'],
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.dealer.user,
**self.get_form_kwargs()
**self.get_form_kwargs(),
)
def get_context_data(self, itemtxs_formset=None, **kwargs):
context = super().get_context_data(**kwargs)
invoice_model: InvoiceModel = self.object
title = f'Invoice {invoice_model.invoice_number}'
context['page_title'] = title
context['header_title'] = title
title = f"Invoice {invoice_model.invoice_number}"
context["page_title"] = title
context["header_title"] = title
ledger_model: LedgerModel = self.object.ledger
if not invoice_model.is_configured():
messages.add_message(
request=self.request,
message=f'Invoice {invoice_model.invoice_number} must have all accounts configured.',
message=f"Invoice {invoice_model.invoice_number} must have all accounts configured.",
level=messages.ERROR,
extra_tags='is-danger'
extra_tags="is-danger",
)
if not invoice_model.is_paid():
if ledger_model.locked:
messages.add_message(self.request,
messages.ERROR,
f'Warning! This invoice is locked. Must unlock before making any changes.',
extra_tags='is-danger')
messages.add_message(
self.request,
messages.ERROR,
f"Warning! This invoice is locked. Must unlock before making any changes.",
extra_tags="is-danger",
)
if ledger_model.locked:
messages.add_message(self.request,
messages.ERROR,
f'Warning! This Invoice is Locked. Must unlock before making any changes.',
extra_tags='is-danger')
messages.add_message(
self.request,
messages.ERROR,
f"Warning! This Invoice is Locked. Must unlock before making any changes.",
extra_tags="is-danger",
)
if not ledger_model.is_posted():
messages.add_message(self.request,
messages.INFO,
f'This Invoice has not been posted. Must post to see ledger changes.',
extra_tags='is-info')
messages.add_message(
self.request,
messages.INFO,
f"This Invoice has not been posted. Must post to see ledger changes.",
extra_tags="is-info",
)
if not itemtxs_formset:
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related('item_model')
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_qs)
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
itemtxs_formset = invoice_itemtxs_formset_class(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.dealer.user,
invoice_model=invoice_model,
itemtxs_qs = invoice_model.itemtransactionmodel_set.all().select_related(
"item_model"
)
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(
queryset=itemtxs_qs
)
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(
invoice_model
)
itemtxs_formset = invoice_itemtxs_formset_class(
entity_slug=self.kwargs["entity_slug"],
user_model=self.request.dealer.user,
invoice_model=invoice_model,
queryset=itemtxs_qs,
)
else:
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(queryset=itemtxs_formset.queryset)
itemtxs_qs, itemtxs_agg = invoice_model.get_itemtxs_data(
queryset=itemtxs_formset.queryset
)
context['itemtxs_formset'] = itemtxs_formset
context['total_amount__sum'] = itemtxs_agg['total_amount__sum']
context["itemtxs_formset"] = itemtxs_formset
context["total_amount__sum"] = itemtxs_agg["total_amount__sum"]
return context
def get_success_url(self):
entity_slug = self.kwargs['entity_slug']
invoice_pk = self.kwargs['invoice_pk']
return reverse('invoice_detail',
kwargs={
'dealer_slug': self.request.dealer.slug,
'entity_slug': entity_slug,
'pk': invoice_pk
})
entity_slug = self.kwargs["entity_slug"]
invoice_pk = self.kwargs["invoice_pk"]
return reverse(
"invoice_detail",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": entity_slug,
"pk": invoice_pk,
},
)
# def get_queryset(self):
# qs = super().get_queryset()
# return qs.prefetch_related('itemtransactionmodel_set')
def get_queryset(self):
if self.queryset is None:
self.queryset = InvoiceModel.objects.for_entity(
entity_slug=self.kwargs['entity_slug'],
user_model=self.request.user
).select_related('customer', 'ledger').order_by('-created')
return super().get_queryset().prefetch_related('itemtransactionmodel_set')
self.queryset = (
InvoiceModel.objects.for_entity(
entity_slug=self.kwargs["entity_slug"], user_model=self.request.user
)
.select_related("customer", "ledger")
.order_by("-created")
)
return super().get_queryset().prefetch_related("itemtransactionmodel_set")
def form_valid(self, form):
invoice_model: InvoiceModel = form.save(commit=False)
if invoice_model.can_migrate():
invoice_model.migrate_state(
user_model=self.request.dealer.user,
entity_slug=self.kwargs['entity_slug']
entity_slug=self.kwargs["entity_slug"],
)
messages.add_message(self.request,
messages.SUCCESS,
f'Invoice {self.object.invoice_number} successfully updated.',
extra_tags='is-success')
messages.add_message(
self.request,
messages.SUCCESS,
f"Invoice {self.object.invoice_number} successfully updated.",
extra_tags="is-success",
)
return super().form_valid(form)
def get(self, request, entity_slug, invoice_pk, *args, **kwargs):
if self.action_update_items:
return HttpResponseRedirect(
redirect_to=reverse('invoice_update',
kwargs={
'dealer_slug': request.dealer.slug,
'entity_slug': entity_slug,
'pk': invoice_pk
})
redirect_to=reverse(
"invoice_update",
kwargs={
"dealer_slug": request.dealer.slug,
"entity_slug": entity_slug,
"pk": invoice_pk,
},
)
)
return super(InvoiceModelUpdateView, self).get(request, *args, **kwargs)
@ -922,18 +964,22 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
queryset = self.get_queryset()
invoice_model = self.get_object(queryset=queryset)
self.object = invoice_model
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(invoice_model)
itemtxs_formset = invoice_itemtxs_formset_class(request.POST,
user_model=self.request.dealer.user,
invoice_model=invoice_model,
entity_slug=entity_slug)
invoice_itemtxs_formset_class = get_invoice_itemtxs_formset_class(
invoice_model
)
itemtxs_formset = invoice_itemtxs_formset_class(
request.POST,
user_model=self.request.dealer.user,
invoice_model=invoice_model,
entity_slug=entity_slug,
)
if not invoice_model.can_edit_items():
messages.add_message(
request,
message=f'Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}',
message=f"Cannot update items once Invoice is {invoice_model.get_invoice_status_display()}",
level=messages.ERROR,
extra_tags='is-danger'
extra_tags="is-danger",
)
context = self.get_context_data(itemtxs_formset=itemtxs_formset)
return self.render_to_response(context=context)
@ -941,8 +987,12 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
if itemtxs_formset.has_changed():
if itemtxs_formset.is_valid():
itemtxs_list = itemtxs_formset.save(commit=False)
entity_qs = EntityModel.objects.for_user(user_model=self.request.dealer.user)
entity_model: EntityModel = get_object_or_404(entity_qs, slug__exact=entity_slug)
entity_qs = EntityModel.objects.for_user(
user_model=self.request.dealer.user
)
entity_model: EntityModel = get_object_or_404(
entity_qs, slug__exact=entity_slug
)
for itemtxs in itemtxs_list:
itemtxs.invoice_model_id = invoice_model.uuid
@ -953,53 +1003,72 @@ class InvoiceModelUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
invoice_model.get_state(commit=True)
invoice_model.clean()
invoice_model.save(
update_fields=['amount_due',
'amount_receivable',
'amount_unearned',
'amount_earned',
'updated']
update_fields=[
"amount_due",
"amount_receivable",
"amount_unearned",
"amount_earned",
"updated",
]
)
invoice_model.migrate_state(
entity_slug=entity_slug,
user_model=self.request.user,
raise_exception=False,
itemtxs_qs=itemtxs_qs
itemtxs_qs=itemtxs_qs,
)
messages.add_message(request,
message=f'Items for Invoice {invoice_model.invoice_number} saved.',
level=messages.SUCCESS,
extra_tags='is-success')
messages.add_message(
request,
message=f"Items for Invoice {invoice_model.invoice_number} saved.",
level=messages.SUCCESS,
extra_tags="is-success",
)
return HttpResponseRedirect(
redirect_to=reverse('django_ledger:invoice-update',
kwargs={
'entity_slug': entity_slug,
'invoice_pk': invoice_pk
})
redirect_to=reverse(
"django_ledger:invoice-update",
kwargs={
"entity_slug": entity_slug,
"invoice_pk": invoice_pk,
},
)
)
# if not valid, return formset with errors...
return self.render_to_response(context=self.get_context_data(itemtxs_formset=itemtxs_formset))
return self.render_to_response(
context=self.get_context_data(itemtxs_formset=itemtxs_formset)
)
return super(InvoiceModelUpdateView, self).post(request, **kwargs)
class ChartOfAccountModelModelBaseViewMixIn(LoginRequiredMixin, PermissionRequiredMixin):
class ChartOfAccountModelModelBaseViewMixIn(
LoginRequiredMixin, PermissionRequiredMixin
):
queryset = None
permission_required = []
def get_queryset(self):
if self.queryset is None:
entity_model = self.request.dealer.entity
self.queryset = entity_model.chartofaccountmodel_set.all().order_by('-updated')
self.queryset = entity_model.chartofaccountmodel_set.all().order_by(
"-updated"
)
return super().get_queryset()
def get_redirect_url(self, *args, **kwargs):
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
'entity_slug': self.request.entity.slug})
return reverse(
"coa-list",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListView):
template_name = 'chart_of_accounts/coa_list.html'
context_object_name = 'coa_list'
template_name = "chart_of_accounts/coa_list.html"
context_object_name = "coa_list"
inactive = False
def get_queryset(self):
@ -1010,84 +1079,116 @@ class ChartOfAccountModelListView(ChartOfAccountModelModelBaseViewMixIn, ListVie
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=None, **kwargs)
context['inactive'] = self.inactive
context['header_subtitle'] = self.request.entity.name
context['header_subtitle_icon'] = 'gravity-ui:hierarchy'
context['page_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List'
context['header_title'] = 'Inactive Chart of Account List' if self.inactive else 'Chart of Accounts List'
context["inactive"] = self.inactive
context["header_subtitle"] = self.request.entity.name
context["header_subtitle_icon"] = "gravity-ui:hierarchy"
context["page_title"] = (
"Inactive Chart of Account List"
if self.inactive
else "Chart of Accounts List"
)
context["header_title"] = (
"Inactive Chart of Account List"
if self.inactive
else "Chart of Accounts List"
)
return context
class ChartOfAccountModelCreateView(ChartOfAccountModelModelBaseViewMixIn, CreateView):
template_name = 'chart_of_accounts/coa_create.html'
template_name = "chart_of_accounts/coa_create.html"
extra_context = {
'header_title': _('Create Chart of Accounts'),
'page_title': _('Create Chart of Account'),
"header_title": _("Create Chart of Accounts"),
"page_title": _("Create Chart of Account"),
}
def get_initial(self):
return {
'entity': self.request.entity,
"entity": self.request.entity,
}
def get_form(self, form_class=None):
return ChartOfAccountsModelCreateForm(
entity_model=self.request.entity,
**self.get_form_kwargs()
entity_model=self.request.entity, **self.get_form_kwargs()
)
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(object_list=None, **kwargs)
context['header_subtitle'] = f'New Chart of Accounts: {self.request.entity.name}'
context['header_subtitle_icon'] = 'gravity-ui:hierarchy'
context["header_subtitle"] = (
f"New Chart of Accounts: {self.request.entity.name}"
)
context["header_subtitle_icon"] = "gravity-ui:hierarchy"
return context
def get_success_url(self):
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
'entity_slug': self.request.entity.slug})
return reverse(
"coa-list",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class ChartOfAccountModelUpdateView(ChartOfAccountModelModelBaseViewMixIn, UpdateView):
context_object_name = 'coa_model'
slug_url_kwarg = 'coa_slug'
template_name = 'chart_of_accounts/coa_update.html'
context_object_name = "coa_model"
slug_url_kwarg = "coa_slug"
template_name = "chart_of_accounts/coa_update.html"
form_class = ChartOfAccountsModelUpdateForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
chart_of_accounts_model: ChartOfAccountModel = self.object
context['page_title'] = f'Update Chart of Account {chart_of_accounts_model.name}'
context['header_title'] = f'Update Chart of Account {chart_of_accounts_model.name}'
context["page_title"] = (
f"Update Chart of Account {chart_of_accounts_model.name}"
)
context["header_title"] = (
f"Update Chart of Account {chart_of_accounts_model.name}"
)
return context
def get_success_url(self):
return reverse('coa-list', kwargs={'dealer_slug': self.request.dealer.slug,
'entity_slug': self.request.entity.slug})
return reverse(
"coa-list",
kwargs={
"dealer_slug": self.request.dealer.slug,
"entity_slug": self.request.entity.slug,
},
)
class CharOfAccountModelActionView(ChartOfAccountModelModelBaseViewMixIn,
RedirectView,
SingleObjectMixin):
http_method_names = ['get']
slug_url_kwarg = 'coa_slug'
class CharOfAccountModelActionView(
ChartOfAccountModelModelBaseViewMixIn, RedirectView, SingleObjectMixin
):
http_method_names = ["get"]
slug_url_kwarg = "coa_slug"
action_name = None
commit = True
def get(self, request, *args, **kwargs):
kwargs['user_model'] = self.request.user
kwargs["user_model"] = self.request.user
if not self.action_name:
raise ImproperlyConfigured('View attribute action_name is required.')
response = super(CharOfAccountModelActionView, self).get(request, *args, **kwargs)
raise ImproperlyConfigured("View attribute action_name is required.")
response = super(CharOfAccountModelActionView, self).get(
request, *args, **kwargs
)
coa_model: ChartOfAccountModel = self.get_object()
try:
getattr(coa_model, self.action_name)(commit=self.commit, **kwargs)
messages.add_message(request, level=messages.SUCCESS, extra_tags='is-success',
message=_('Successfully updated {} Default Chart of Account to '.format(
request.entity.name) +
'{}'.format(coa_model.name)))
messages.add_message(
request,
level=messages.SUCCESS,
extra_tags="is-success",
message=_(
"Successfully updated {} Default Chart of Account to ".format(
request.entity.name
)
+ "{}".format(coa_model.name)
),
)
except ValidationError as e:
messages.add_message(request,
message=e.message,
level=messages.ERROR,
extra_tags='is-danger')
messages.add_message(
request, message=e.message, level=messages.ERROR, extra_tags="is-danger"
)
return response

View File

@ -28,6 +28,7 @@ from plans.models import UserPlan
from plans.signals import order_completed, activate_user_plan
from inventory.tasks import send_email
from django.conf import settings
# logging
import logging
@ -177,7 +178,11 @@ def create_ledger_entity(sender, instance, created, **kwargs):
entity.create_uom(name=u[1], unit_abbr=u[0])
# Create COA accounts, background task
async_task(func="inventory.tasks.create_coa_accounts",dealer=instance,hook="inventory.hooks.check_create_coa_accounts")
async_task(
func="inventory.tasks.create_coa_accounts",
dealer=instance,
hook="inventory.hooks.check_create_coa_accounts",
)
# async_task('inventory.tasks.check_create_coa_accounts', instance, schedule_type='O', schedule_time=timedelta(seconds=20))
# create_settings(instance.pk)
@ -273,14 +278,14 @@ def create_item_model(sender, instance, created, **kwargs):
else:
instance.item_model.default_amount = instance.marked_price
# inventory = entity.create_item_inventory(
# name=instance.vin,
# uom_model=uom,
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM
# )
# inventory.additional_info = {}
# inventory.additional_info.update({"car_info": instance.to_dict()})
# inventory.save()
# inventory = entity.create_item_inventory(
# name=instance.vin,
# uom_model=uom,
# item_type=ItemModel.ITEM_TYPE_LUMP_SUM
# )
# inventory.additional_info = {}
# inventory.additional_info.update({"car_info": instance.to_dict()})
# inventory.save()
# else:
# instance.item_model.additional_info.update({"car_info": instance.to_dict()})
# instance.item_model.save()
@ -1039,7 +1044,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
po_number=instance.po_number,
url=reverse(
"purchase_order_detail",
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": instance.entity.slug,
"pk": instance.pk,
},
),
),
)
@ -1086,7 +1095,10 @@ def sale_order_created_notification(sender, instance, created, **kwargs):
estimate_number=instance.estimate.estimate_number,
url=reverse(
"estimate_detail",
kwargs={"dealer_slug": instance.dealer.slug, "pk": instance.estimate.pk},
kwargs={
"dealer_slug": instance.dealer.slug,
"pk": instance.estimate.pk,
},
),
),
)
@ -1131,7 +1143,7 @@ def estimate_in_review_notification(sender, instance, created, **kwargs):
url=reverse(
"estimate_detail",
kwargs={"dealer_slug": dealer.slug, "pk": instance.pk},
)
),
),
)
@ -1188,7 +1200,11 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
bill_number=instance.bill_number,
url=reverse(
"bill-update",
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
kwargs={
"dealer_slug": dealer.slug,
"entity_slug": dealer.entity.slug,
"bill_pk": instance.pk,
},
),
),
)
@ -1224,7 +1240,6 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
# )
@receiver(post_save, sender=models.Ticket)
def send_ticket_notification(sender, instance, created, **kwargs):
if created:
@ -1249,20 +1264,23 @@ def send_ticket_notification(sender, instance, created, **kwargs):
)
else:
models.Notification.objects.create(
user=instance.dealer.user,
message=_(
"""
user=instance.dealer.user,
message=_(
"""
Support Ticket #{ticket_number} has been updated.
<a href="{url}" target="_blank">View</a>.
"""
).format(
ticket_number=instance.pk,
url=reverse(
"ticket_detail",
kwargs={"dealer_slug": instance.dealer.slug, "ticket_id": instance.pk},
),
).format(
ticket_number=instance.pk,
url=reverse(
"ticket_detail",
kwargs={
"dealer_slug": instance.dealer.slug,
"ticket_id": instance.pk,
},
),
)
),
)
@receiver(post_save, sender=models.CarColors)
@ -1273,30 +1291,31 @@ def handle_car_image(sender, instance, created, **kwargs):
try:
# Create or get car image record
car = instance.car
car_image, created = models.CarImage.objects.get_or_create(car=car, defaults={'image_hash': car.get_hash})
car_image, created = models.CarImage.objects.get_or_create(
car=car, defaults={"image_hash": car.get_hash}
)
# Check for existing image with same hash
existing = models.CarImage.objects.filter(
image_hash=car_image.image_hash,
image__isnull=False
).exclude(car=car).first()
existing = (
models.CarImage.objects.filter(
image_hash=car_image.image_hash, image__isnull=False
)
.exclude(car=car)
.first()
)
if existing:
# Copy existing image
car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
car_image.image.save(existing.image.name, existing.image.file, save=True)
logger.info(f"Reused image for car {car.vin}")
else:
# Schedule async generation
async_task(
'inventory.tasks.generate_car_image_task',
"inventory.tasks.generate_car_image_task",
car_image.id,
task_name=f"generate_car_image_{car.vin}"
task_name=f"generate_car_image_{car.vin}",
)
logger.info(f"Scheduled image generation for car {car.vin}")
except Exception as e:
logger.error(f"Error handling car image for {car.vin}: {e}")
logger.error(f"Error handling car image for {car.vin}: {e}")

View File

@ -17,11 +17,19 @@ 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 .utils import get_accounts_data,create_account
from .utils import get_accounts_data, create_account
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
from inventory.models import DealerSettings, Dealer,Schedule,Notification,CarReservation,CarStatusChoices,CarImage
from inventory.models import (
DealerSettings,
Dealer,
Schedule,
Notification,
CarReservation,
CarStatusChoices,
CarImage,
)
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
@ -52,6 +60,7 @@ def create_settings(pk):
.first(),
)
def create_coa_accounts(**kwargs):
logger.info("creating all accounts are created")
instance = kwargs.get("dealer")
@ -62,9 +71,6 @@ def create_coa_accounts(**kwargs):
create_account(entity, coa, account_data)
# def create_coa_accounts1(pk):
# with transaction.atomic():
# instance = Dealer.objects.select_for_update().get(pk=pk)
@ -800,8 +806,6 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
# transaction.on_commit(run)
def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire):
"""Send bilingual email reminder using Django-Q"""
try:
@ -809,41 +813,49 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire
plan = Plan.objects.get(id=plan_id)
# Determine user language preference
user_language = getattr(user, 'language', settings.LANGUAGE_CODE)
user_language = getattr(user, "language", settings.LANGUAGE_CODE)
activate(user_language)
# Context data
context = {
'user': user,
'plan': plan,
'expiration_date': expiration_date,
'days_until_expire': days_until_expire,
'SITE_NAME': settings.SITE_NAME,
'RENEWAL_URL': "url" ,#settings.RENEWAL_URL,
'direction': 'rtl' if user_language.startswith('ar') else 'ltr'
"user": user,
"plan": plan,
"expiration_date": expiration_date,
"days_until_expire": days_until_expire,
"SITE_NAME": settings.SITE_NAME,
"RENEWAL_URL": "url", # settings.RENEWAL_URL,
"direction": "rtl" if user_language.startswith("ar") else "ltr",
}
# Subject with translation
subject_en = f"Your {plan.name} subscription expires in {days_until_expire} days"
subject_en = (
f"Your {plan.name} subscription expires in {days_until_expire} days"
)
subject_ar = f"اشتراكك في {plan.name} ينتهي خلال {days_until_expire} أيام"
# Render templates
text_content = render_to_string([
f'emails/expiration_reminder_{user_language}.txt',
'emails/expiration_reminder.txt'
], context)
text_content = render_to_string(
[
f"emails/expiration_reminder_{user_language}.txt",
"emails/expiration_reminder.txt",
],
context,
)
html_content = render_to_string([
f'emails/expiration_reminder_{user_language}.html',
'emails/expiration_reminder.html'
], context)
html_content = render_to_string(
[
f"emails/expiration_reminder_{user_language}.html",
"emails/expiration_reminder.html",
],
context,
)
# Create email
email = EmailMultiAlternatives(
subject=subject_ar if user_language.startswith('ar') else subject_en,
subject=subject_ar if user_language.startswith("ar") else subject_en,
body=text_content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[user.email]
to=[user.email],
)
email.attach_alternative(html_content, "text/html")
email.send()
@ -853,6 +865,7 @@ def send_bilingual_reminder(user_id, plan_id, expiration_date, days_until_expire
logger.error(f"Email failed: {str(e)}")
raise
def handle_email_result(task):
"""Callback for email results"""
if task.success:
@ -861,7 +874,6 @@ def handle_email_result(task):
logger.error(f"Email task failed: {task.result}")
def send_schedule_reminder_email(schedule_id):
"""
Sends an email reminder for a specific schedule.
@ -871,46 +883,67 @@ def send_schedule_reminder_email(schedule_id):
schedule = Schedule.objects.get(pk=schedule_id)
# Ensure the user has an email and the schedule is not completed/canceled
if not schedule.scheduled_by.email or schedule.status in ["completed", "canceled"]:
logger.error(f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}.")
if not schedule.scheduled_by.email or schedule.status in [
"completed",
"canceled",
]:
logger.error(
f"Skipping email for Schedule ID {schedule_id}: No email or schedule status is {schedule.status}."
)
return
user_email = schedule.scheduled_by.email
Notification.objects.create(
user=schedule.scheduled_by,
message=_(
"""
user=schedule.scheduled_by,
message=_(
"""
Reminder: You have an appointment scheduled for {scheduled_type} After 15 minutes <a href="{url}" target="_blank">View</a>.
"""
).format(scheduled_type=schedule.scheduled_type, url=reverse("schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug})),)
).format(
scheduled_type=schedule.scheduled_type,
url=reverse(
"schedule_calendar", kwargs={"dealer_slug": schedule.dealer.slug}
),
),
)
# Prepare context for email templates
context = {
'schedule_purpose': schedule.purpose,
'scheduled_at': schedule.scheduled_at.astimezone(timezone.get_current_timezone()).strftime('%Y-%m-%d %H:%M %Z'), # Format with timezone
'schedule_type': schedule.scheduled_type,
'customer_name': schedule.customer.customer_name if schedule.customer else 'N/A',
'notes': schedule.notes,
'user_name': schedule.scheduled_by.get_full_name() or schedule.scheduled_by.email,
"schedule_purpose": schedule.purpose,
"scheduled_at": schedule.scheduled_at.astimezone(
timezone.get_current_timezone()
).strftime("%Y-%m-%d %H:%M %Z"), # Format with timezone
"schedule_type": schedule.scheduled_type,
"customer_name": schedule.customer.customer_name
if schedule.customer
else "N/A",
"notes": schedule.notes,
"user_name": schedule.scheduled_by.get_full_name()
or schedule.scheduled_by.email,
}
# Render email content from templates
html_message = render_to_string('emails/schedule_reminder.html', context)
plain_message = render_to_string('emails/schedule_reminder.txt', context)
html_message = render_to_string("emails/schedule_reminder.html", context)
plain_message = render_to_string("emails/schedule_reminder.txt", context)
send_mail(
f'Reminder: Your Upcoming Schedule - {schedule.purpose}',
f"Reminder: Your Upcoming Schedule - {schedule.purpose}",
plain_message,
settings.DEFAULT_FROM_EMAIL,
[user_email],
html_message=html_message,
)
logger.info(f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}")
logger.info(
f"Successfully sent reminder email for Schedule ID: {schedule_id} to {user_email}"
)
except Schedule.DoesNotExist:
logger.info(f"Schedule with ID {schedule_id} does not exist. Cannot send reminder.")
logger.info(
f"Schedule with ID {schedule_id} does not exist. Cannot send reminder."
)
except Exception as e:
logger.info(f"Error sending reminder email for Schedule ID {schedule_id}: {e}")
# Optional: A hook function to log the status of the email task (add to your_app/tasks.py)
def log_email_status(task):
"""
@ -918,9 +951,14 @@ def log_email_status(task):
It logs whether the task was successful or not.
"""
if task.success:
logger.info(f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}")
logger.info(
f"Email task for Schedule ID {task.args[0]} completed successfully. Result: {task.result}"
)
else:
logger.error(f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}")
logger.error(
f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}"
)
def remove_reservation_by_id(reservation_id):
try:
@ -931,8 +969,9 @@ def remove_reservation_by_id(reservation_id):
except Exception as e:
logger.error(f"Error removing reservation with ID {reservation_id}: {e}")
def test_task(**kwargs):
print("TASK : ",kwargs.get("dealer"))
print("TASK : ", kwargs.get("dealer"))
def generate_car_image_task(car_image_id):
@ -940,22 +979,25 @@ def generate_car_image_task(car_image_id):
Simple async task to generate car image
"""
from inventory.utils import generate_car_image_simple
try:
car_image = CarImage.objects.get(id=car_image_id)
result = generate_car_image_simple(car_image)
return {
'success': result.get('success', False),
'car_image_id': car_image_id,
'error': result.get('error'),
'message': 'Image generated' if result.get('success') else 'Generation failed'
"success": result.get("success", False),
"car_image_id": car_image_id,
"error": result.get("error"),
"message": "Image generated"
if result.get("success")
else "Generation failed",
}
except CarImage.DoesNotExist:
error_msg = f"CarImage with id {car_image_id} not found"
logger.error(error_msg)
return {'success': False, 'error': error_msg}
return {"success": False, "error": error_msg}
except Exception as e:
error_msg = f"Unexpected error: {e}"
logger.error(error_msg)
return {'success': False, 'error': error_msg}
return {"success": False, "error": error_msg}

View File

@ -13,9 +13,9 @@ from django.db.models import Case, Value, When, IntegerField
register = template.Library()
@register.filter
def get_percentage(value, total):
try:
value = int(value)
total = int(total)
@ -25,6 +25,7 @@ def get_percentage(value, total):
except (ValueError, TypeError):
return 0
@register.filter(name="percentage")
def percentage(value):
if value is not None:
@ -686,11 +687,12 @@ def count_checked(permissions, group_permission_ids):
# """Count how many permissions are checked from the allowed list"""
# return sum(1 for perm in permissions if perm.id in group_permission_ids)
@register.inclusion_tag('sales/tags/invoice_item_formset.html', takes_context=True)
@register.inclusion_tag("sales/tags/invoice_item_formset.html", takes_context=True)
def invoice_item_formset_table(context, itemtxs_formset):
return {
'entity_slug': context['view'].kwargs['entity_slug'],
'invoice_model': context['invoice'],
'total_amount__sum': context['total_amount__sum'],
'itemtxs_formset': itemtxs_formset,
"entity_slug": context["view"].kwargs["entity_slug"],
"invoice_model": context["invoice"],
"total_amount__sum": context["total_amount__sum"],
"itemtxs_formset": itemtxs_formset,
}

View File

@ -40,13 +40,18 @@ urlpatterns = [
views.assign_car_makes,
name="assign_car_makes",
),
#dashboards for manager, dealer, inventory and accounatant
path("dashboards/<slug:dealer_slug>/general/", views.general_dashboard,name="general_dashboard"),
#dashboard for sales
path("dashboards/<slug:dealer_slug>/sales/", views.sales_dashboard, name="sales_dashboard"),
# dashboards for manager, dealer, inventory and accounatant
path(
"dashboards/<slug:dealer_slug>/general/",
views.general_dashboard,
name="general_dashboard",
),
# dashboard for sales
path(
"dashboards/<slug:dealer_slug>/sales/",
views.sales_dashboard,
name="sales_dashboard",
),
path(
"<slug:dealer_slug>/cars/aging-inventory/list",
views.aging_inventory_list_view,
@ -777,7 +782,11 @@ urlpatterns = [
views.EstimateDetailView.as_view(),
name="estimate_detail",
),
path('<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/', views.EstimatePrintView.as_view(), name='estimate_print'),
path(
"<slug:dealer_slug>/sales/estimates/print/<uuid:pk>/",
views.EstimatePrintView.as_view(),
name="estimate_print",
),
path(
"<slug:dealer_slug>/sales/estimates/create/",
views.create_estimate,
@ -934,7 +943,6 @@ urlpatterns = [
views.ItemServiceUpdateView.as_view(),
name="item_service_update",
),
# Expanese
path(
"<slug:dealer_slug>/items/expeneses/",
@ -1093,32 +1101,47 @@ urlpatterns = [
name="entity-ic-date",
),
# Chart of Accounts...
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/',
path(
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/",
views.ChartOfAccountModelListView.as_view(),
name='coa-list'),
path('<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/',
views.ChartOfAccountModelListView.as_view(inactive=True),
name='coa-list-inactive'),
path('<slug:dealer_slug>/<slug:entity_slug>/create/',
views.ChartOfAccountModelCreateView.as_view(),
name='coa-create'),
path('<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/',
views.ChartOfAccountModelListView.as_view(),
name='coa-detail'),
path('<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/',
views.ChartOfAccountModelUpdateView.as_view(),
name='coa-update'),
name="coa-list",
),
path(
"<slug:dealer_slug>/chart-of-accounts/<slug:entity_slug>/list/inactive/",
views.ChartOfAccountModelListView.as_view(inactive=True),
name="coa-list-inactive",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/create/",
views.ChartOfAccountModelCreateView.as_view(),
name="coa-create",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/detail/<slug:coa_slug>/",
views.ChartOfAccountModelListView.as_view(),
name="coa-detail",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/update/<slug:coa_slug>/",
views.ChartOfAccountModelUpdateView.as_view(),
name="coa-update",
),
# ACTIONS....
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/',
views.CharOfAccountModelActionView.as_view(action_name='mark_as_default'),
name='coa-action-mark-as-default'),
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/',
views.CharOfAccountModelActionView.as_view(action_name='mark_as_active'),
name='coa-action-mark-as-active'),
path('<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/',
views.CharOfAccountModelActionView.as_view(action_name='mark_as_inactive'),
name='coa-action-mark-as-inactive'),
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-default/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_default"),
name="coa-action-mark-as-default",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-active/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_active"),
name="coa-action-mark-as-active",
),
path(
"<slug:dealer_slug>/<slug:entity_slug>/action/<slug:coa_slug>/mark-as-inactive/",
views.CharOfAccountModelActionView.as_view(action_name="mark_as_inactive"),
name="coa-action-mark-as-inactive",
),
# CASH FLOW STATEMENTS...
# Entities...
path(
@ -1294,40 +1317,74 @@ urlpatterns = [
views.PurchaseOrderMarkAsVoidView.as_view(),
name="po-action-mark-as-void",
),
# reports
path(
path(
"<slug:dealer_slug>/purchase-report/",
views.purchase_report_view,
name="po-report",
),
path('purchase-report/<slug:dealer_slug>/csv/', views.purchase_report_csv_export, name='purchase-report-csv-export'),
path(
path(
"purchase-report/<slug:dealer_slug>/csv/",
views.purchase_report_csv_export,
name="purchase-report-csv-export",
),
path(
"<slug:dealer_slug>/car-sale-report/",
views.car_sale_report_view,
name="car-sale-report",
),
path('car-sale-report/<slug:dealer_slug>/csv/', views.car_sale_report_csv_export, name='car-sale-report-csv-export'),
path('feature/recall/', views.RecallListView.as_view(), name='recall_list'),
path('feature/recall/filter/', views.RecallFilterView, name='recall_filter'),
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'),
path('feature/recall/create/', views.RecallCreateView.as_view(), name='recall_create'),
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'),
path(
"car-sale-report/<slug:dealer_slug>/csv/",
views.car_sale_report_csv_export,
name="car-sale-report-csv-export",
),
path("feature/recall/", views.RecallListView.as_view(), name="recall_list"),
path("feature/recall/filter/", views.RecallFilterView, name="recall_filter"),
path(
"feature/recall/<int:pk>/view/",
views.RecallDetailView.as_view(),
name="recall_detail",
),
path(
"feature/recall/create/", views.RecallCreateView.as_view(), name="recall_create"
),
path(
"feature/recall/success/",
views.RecallSuccessView.as_view(),
name="recall_success",
),
path(
"<slug:dealer_slug>/schedules/calendar/",
views.schedule_calendar,
name="schedule_calendar",
),
# staff profile
path('<slug:dealer_slug>/staff/<slug:slug>detail/', views.StaffDetailView.as_view(), name='staff_detail'),
path(
"<slug:dealer_slug>/staff/<slug:slug>detail/",
views.StaffDetailView.as_view(),
name="staff_detail",
),
# tickets
path('help_center/view/', views.help_center, name='help_center'),
path('<slug:dealer_slug>/help_center/tickets/', views.ticket_list, name='ticket_list'),
path('help_center/tickets/<slug:dealer_slug>/create/', views.create_ticket, name='create_ticket'),
path('<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/', views.ticket_detail, name='ticket_detail'),
path('help_center/tickets/<int:ticket_id>/update/', views.ticket_update, name='ticket_update'),
path("help_center/view/", views.help_center, name="help_center"),
path(
"<slug:dealer_slug>/help_center/tickets/", views.ticket_list, name="ticket_list"
),
path(
"help_center/tickets/<slug:dealer_slug>/create/",
views.create_ticket,
name="create_ticket",
),
path(
"<slug:dealer_slug>/help_center/tickets/<int:ticket_id>/",
views.ticket_detail,
name="ticket_detail",
),
path(
"help_center/tickets/<int:ticket_id>/update/",
views.ticket_update,
name="ticket_update",
),
# path('help_center/tickets/<int:ticket_id>/ticket_mark_resolved/', views.ticket_mark_resolved, name='ticket_mark_resolved'),
]
handler404 = "inventory.views.custom_page_not_found_view"

View File

@ -73,15 +73,12 @@ def get_jwt_token():
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status()
#logging for success
# logging for success
logger.info("Successfully fetched JWT token.")
return response.text
except requests.exceptions.RequestException as e:
#logging for error
logger.error(
f"HTTP error fetching JWT token from {url}: ",
exc_info=True
)
# logging for error
logger.error(f"HTTP error fetching JWT token from {url}: ", exc_info=True)
print(f"Error obtaining JWT token: {e}")
return None
@ -169,7 +166,7 @@ def send_email(from_, to_, subject, message):
message = message
from_email = from_
recipient_list = [to_]
async_task(send_mail,subject, message, from_email, recipient_list)
async_task(send_mail, subject, message, from_email, recipient_list)
def get_user_type(request):
@ -236,10 +233,10 @@ def reserve_car(car, request):
)
car.status = models.CarStatusChoices.RESERVED
car.save()
# --- Logging for Success ---
# --- Logging for Success ---
DjangoQSchedule.objects.create(
name=f"remove_reservation_for_car_with_vin_{car.vin}",
func='inventory.tasks.remove_reservation_by_id',
func="inventory.tasks.remove_reservation_by_id",
args=reservation.pk,
schedule_type=DjangoQSchedule.ONCE,
next_run=reserved_until,
@ -257,7 +254,7 @@ def reserve_car(car, request):
f"Error reserving car {car.pk} ('{car.id_car_make} {car.id_car_model}') "
f"for user {request.user} . "
f"Error: {e}",
exc_info=True
exc_info=True,
)
messages.error(request, f"Error reserving car: {e}")
@ -1038,22 +1035,25 @@ class CarFinanceCalculator1:
self.item_transactions = self._get_item_transactions()
# self.additional_services = self._get_additional_services()
def _get_vat_rate(self):
vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
if not vat:
raise ObjectDoesNotExist("No active VAT rate found")
return vat.rate
def _get_additional_services(self):
return [x for item in self.item_transactions
for x in item.item_model.car.additional_services
]
return [
x
for item in self.item_transactions
for x in item.item_model.car.additional_services
]
def _get_item_transactions(self):
return self.model.get_itemtxs_data()[0].all()
def get_items(self):
return self._get_item_transactions()
@staticmethod
def _get_quantity(item):
return item.ce_quantity or item.quantity
@ -1068,17 +1068,17 @@ class CarFinanceCalculator1:
quantity = self._get_quantity(item)
car = item.item_model.car
unit_price = Decimal(car.marked_price)
discount = self.extra_info.data.get("discount",0)
discount = self.extra_info.data.get("discount", 0)
sell_price = unit_price - Decimal(discount)
return {
"item_number": item.item_model.item_number,
"vin": car.vin, #car_info.get("vin"),
"make": car.id_car_make ,#car_info.get("make"),
"model": car.id_car_model ,#car_info.get("model"),
"year": car.year ,# car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim ,# car_info.get("trim"),
"mileage": car.mileage ,# car_info.get("mileage"),
"vin": car.vin, # car_info.get("vin"),
"make": car.id_car_make, # car_info.get("make"),
"model": car.id_car_model, # car_info.get("model"),
"year": car.year, # car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim, # car_info.get("trim"),
"mileage": car.mileage, # car_info.get("mileage"),
"cost_price": car.cost_price,
"selling_price": car.selling_price,
"marked_price": car.marked_price,
@ -1091,21 +1091,23 @@ class CarFinanceCalculator1:
"total_discount": discount,
"final_price": sell_price + (sell_price * self.vat_rate),
"total_additionals": car.total_additional_services,
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
"additional_services": car.additional_services,# self._get_nested_value(
#item, self.ADDITIONAL_SERVICES_KEY
#),
"grand_total": sell_price
+ (sell_price * self.vat_rate)
+ car.total_additional_services,
"additional_services": car.additional_services, # self._get_nested_value(
# item, self.ADDITIONAL_SERVICES_KEY
# ),
}
def calculate_totals(self):
total_price = sum(
Decimal(item.item_model.car.marked_price)
for item in self.item_transactions
Decimal(item.item_model.car.marked_price) for item in self.item_transactions
)
total_additionals = sum(
Decimal(item.price_) for item in self._get_additional_services())
Decimal(item.price_) for item in self._get_additional_services()
)
total_discount = self.extra_info.data.get("discount",0)
total_discount = self.extra_info.data.get("discount", 0)
total_price_discounted = total_price
if total_discount:
total_price_discounted = total_price - Decimal(total_discount)
@ -1113,13 +1115,15 @@ class CarFinanceCalculator1:
total_vat_amount = total_price_discounted * self.vat_rate
return {
"total_price_discounted":total_price_discounted,
"total_price_before_discount":total_price,
"total_price_discounted": total_price_discounted,
"total_price_before_discount": total_price,
"total_price": total_price_discounted,
"total_vat_amount": total_vat_amount,
"total_discount": Decimal(total_discount),
"total_additionals": total_additionals,
"grand_total":total_price_discounted + total_vat_amount + total_additionals,
"grand_total": total_price_discounted
+ total_vat_amount
+ total_additionals,
}
def get_finance_data(self):
@ -1131,7 +1135,9 @@ class CarFinanceCalculator1:
),
"total_price": round(totals["total_price"], 2),
"total_price_discounted": round(totals["total_price_discounted"], 2),
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
"total_price_before_discount": round(
totals["total_price_before_discount"], 2
),
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
"total_vat_amount": round(totals["total_vat_amount"], 2),
"total_discount": round(totals["total_discount"], 2),
@ -1140,6 +1146,8 @@ class CarFinanceCalculator1:
"additionals": self._get_additional_services(),
"vat": round(self.vat_rate, 2),
}
class CarFinanceCalculator:
"""
Class responsible for calculating car financing details.
@ -1185,22 +1193,25 @@ class CarFinanceCalculator:
self.item_transactions = self._get_item_transactions()
# self.additional_services = self._get_additional_services()
def _get_vat_rate(self):
vat = models.VatRate.objects.filter(dealer=self.dealer,is_active=True).first()
vat = models.VatRate.objects.filter(dealer=self.dealer, is_active=True).first()
if not vat:
raise ObjectDoesNotExist("No active VAT rate found")
return vat.rate
def _get_additional_services(self):
return [x for item in self.item_transactions
for x in item.item_model.car.additional_services
]
return [
x
for item in self.item_transactions
for x in item.item_model.car.additional_services
]
def _get_item_transactions(self):
return self.model.get_itemtxs_data()[0].all()
def get_items(self):
return self._get_item_transactions()
@staticmethod
def _get_quantity(item):
return item.ce_quantity or item.quantity
@ -1215,17 +1226,17 @@ class CarFinanceCalculator:
quantity = self._get_quantity(item)
car = item.item_model.car
unit_price = Decimal(car.marked_price)
discount = self.extra_info.data.get("discount",0)
discount = self.extra_info.data.get("discount", 0)
sell_price = unit_price - Decimal(discount)
return {
"item_number": item.item_model.item_number,
"vin": car.vin, #car_info.get("vin"),
"make": car.id_car_make ,#car_info.get("make"),
"model": car.id_car_model ,#car_info.get("model"),
"year": car.year ,# car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim ,# car_info.get("trim"),
"mileage": car.mileage ,# car_info.get("mileage"),
"vin": car.vin, # car_info.get("vin"),
"make": car.id_car_make, # car_info.get("make"),
"model": car.id_car_model, # car_info.get("model"),
"year": car.year, # car_info.get("year"),
"logo": car.logo, # getattr(car.id_car_make, "logo", ""),
"trim": car.id_car_trim, # car_info.get("trim"),
"mileage": car.mileage, # car_info.get("mileage"),
"cost_price": car.cost_price,
"selling_price": car.selling_price,
"marked_price": car.marked_price,
@ -1238,21 +1249,23 @@ class CarFinanceCalculator:
"total_discount": discount,
"final_price": sell_price + (sell_price * self.vat_rate),
"total_additionals": car.total_additional_services,
"grand_total": sell_price + (sell_price * self.vat_rate) + car.total_additional_services,
"additional_services": car.additional_services,# self._get_nested_value(
#item, self.ADDITIONAL_SERVICES_KEY
#),
"grand_total": sell_price
+ (sell_price * self.vat_rate)
+ car.total_additional_services,
"additional_services": car.additional_services, # self._get_nested_value(
# item, self.ADDITIONAL_SERVICES_KEY
# ),
}
def calculate_totals(self):
total_price = sum(
Decimal(item.item_model.car.marked_price)
for item in self.item_transactions
Decimal(item.item_model.car.marked_price) for item in self.item_transactions
)
total_additionals = sum(
Decimal(item.price_) for item in self._get_additional_services())
Decimal(item.price_) for item in self._get_additional_services()
)
total_discount = self.extra_info.data.get("discount",0)
total_discount = self.extra_info.data.get("discount", 0)
total_price_discounted = total_price
if total_discount:
total_price_discounted = total_price - Decimal(total_discount)
@ -1260,13 +1273,15 @@ class CarFinanceCalculator:
total_vat_amount = total_price_discounted * self.vat_rate
return {
"total_price_discounted":total_price_discounted,
"total_price_before_discount":total_price,
"total_price_discounted": total_price_discounted,
"total_price_before_discount": total_price,
"total_price": total_price_discounted,
"total_vat_amount": total_vat_amount,
"total_discount": Decimal(total_discount),
"total_additionals": total_additionals,
"grand_total":total_price_discounted + total_vat_amount + total_additionals,
"grand_total": total_price_discounted
+ total_vat_amount
+ total_additionals,
}
def get_finance_data(self):
@ -1278,7 +1293,9 @@ class CarFinanceCalculator:
),
"total_price": round(totals["total_price"], 2),
"total_price_discounted": round(totals["total_price_discounted"], 2),
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
"total_price_before_discount": round(
totals["total_price_before_discount"], 2
),
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
"total_vat_amount": round(totals["total_vat_amount"], 2),
"total_discount": round(totals["total_discount"], 2),
@ -1288,58 +1305,60 @@ class CarFinanceCalculator:
"vat": round(self.vat_rate, 2),
}
def get_finance_data(estimate,dealer):
vat = models.VatRate.objects.filter(dealer=dealer,is_active=True).first()
item = estimate.get_itemtxs_data()[0].first()
car = item.item_model.car
if isinstance(estimate,InvoiceModel) and hasattr(estimate, "ce_model"):
estimate = estimate.ce_model
extra_info = models.ExtraInfo.objects.get(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=estimate.pk,
)
discount = extra_info.data.get("discount", 0)
discount = Decimal(discount)
def get_finance_data(estimate, dealer):
vat = models.VatRate.objects.filter(dealer=dealer, is_active=True).first()
item = estimate.get_itemtxs_data()[0].first()
car = item.item_model.car
if isinstance(estimate, InvoiceModel) and hasattr(estimate, "ce_model"):
estimate = estimate.ce_model
additional_services = car.get_additional_services()
discounted_price=(Decimal(car.marked_price) - discount)
vat_amount = discounted_price * vat.rate
total_services_vat=sum([x[1] for x in additional_services.get("services")])
total_vat=vat_amount+total_services_vat
return {
"car": car,
"discounted_price": discounted_price or 0,
"price_before_discount": car.marked_price,
"vat_amount": vat_amount,
"vat_rate": vat.rate,
"discount_amount": discount,
"additional_services": additional_services,
"final_price": discounted_price + vat_amount,
"total_services_vat":total_services_vat,
"total_vat":total_vat,
"grand_total": discounted_price + total_vat + additional_services.get("total")
}
extra_info = models.ExtraInfo.objects.get(
dealer=dealer,
content_type=ContentType.objects.get_for_model(EstimateModel),
object_id=estimate.pk,
)
discount = extra_info.data.get("discount", 0)
discount = Decimal(discount)
additional_services = car.get_additional_services()
discounted_price = Decimal(car.marked_price) - discount
vat_amount = discounted_price * vat.rate
total_services_vat = sum([x[1] for x in additional_services.get("services")])
total_vat = vat_amount + total_services_vat
return {
"car": car,
"discounted_price": discounted_price or 0,
"price_before_discount": car.marked_price,
"vat_amount": vat_amount,
"vat_rate": vat.rate,
"discount_amount": discount,
"additional_services": additional_services,
"final_price": discounted_price + vat_amount,
"total_services_vat": total_services_vat,
"total_vat": total_vat,
"grand_total": discounted_price + total_vat + additional_services.get("total"),
}
# totals = self.calculate_totals()
# return {
# "car": [self._get_car_data(item) for item in self.item_transactions],
# "quantity": sum(
# self._get_quantity(item) for item in self.item_transactions
# ),
# "total_price": round(totals["total_price"], 2),
# "total_price_discounted": round(totals["total_price_discounted"], 2),
# "total_price_before_discount": round(totals["total_price_before_discount"], 2),
# "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
# "total_vat_amount": round(totals["total_vat_amount"], 2),
# "total_discount": round(totals["total_discount"], 2),
# "total_additionals": round(totals["total_additionals"], 2),
# "grand_total": round(totals["grand_total"], 2),
# "additionals": self._get_additional_services(),
# "vat": round(self.vat_rate, 2),
# }
# totals = self.calculate_totals()
# return {
# "car": [self._get_car_data(item) for item in self.item_transactions],
# "quantity": sum(
# self._get_quantity(item) for item in self.item_transactions
# ),
# "total_price": round(totals["total_price"], 2),
# "total_price_discounted": round(totals["total_price_discounted"], 2),
# "total_price_before_discount": round(totals["total_price_before_discount"], 2),
# "total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
# "total_vat_amount": round(totals["total_vat_amount"], 2),
# "total_discount": round(totals["total_discount"], 2),
# "total_additionals": round(totals["total_additionals"], 2),
# "grand_total": round(totals["grand_total"], 2),
# "additionals": self._get_additional_services(),
# "vat": round(self.vat_rate, 2),
# }
# class CarFinanceCalculator:
# """
# Class responsible for calculating car financing details.
@ -1554,7 +1573,6 @@ def get_local_name(self):
return getattr(self, "name", None)
@transaction.atomic
def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
"""
@ -1566,6 +1584,7 @@ def set_invoice_payment(dealer, entity, invoice, amount, payment_method):
_post_sale_and_cogs(invoice, dealer)
def _post_sale_and_cogs(invoice, dealer):
"""
For every car line on the invoice:
@ -1574,15 +1593,39 @@ def _post_sale_and_cogs(invoice, dealer):
"""
entity = invoice.ledger.entity
# calc = CarFinanceCalculator(invoice)
data = get_finance_data(invoice,dealer)
data = get_finance_data(invoice, dealer)
car = data.get("car")
cash_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_CASH).first()
ar_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES).first()
vat_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE).first()
car_rev = entity.get_default_coa_accounts().filter(role_default=True, role=roles.INCOME_OPERATIONAL).first()
add_rev = entity.get_default_coa_accounts().filter(code="4020").first()
cogs_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.COGS).first()
inv_acc = entity.get_default_coa_accounts().filter(role_default=True, role=roles.ASSET_CA_INVENTORY).first()
cash_acc = (
entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.ASSET_CA_CASH)
.first()
)
ar_acc = (
entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.ASSET_CA_RECEIVABLES)
.first()
)
vat_acc = (
entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.LIABILITY_CL_TAXES_PAYABLE)
.first()
)
car_rev = (
entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.INCOME_OPERATIONAL)
.first()
)
add_rev = entity.get_default_coa_accounts().filter(code="4020").first()
cogs_acc = (
entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.COGS)
.first()
)
inv_acc = (
entity.get_default_coa_accounts()
.filter(role_default=True, role=roles.ASSET_CA_INVENTORY)
.first()
)
# for car_data in data['cars']:
# car = invoice.get_itemtxs_data()[0].filter(
@ -1590,12 +1633,12 @@ def _post_sale_and_cogs(invoice, dealer):
# ).first().item_model.car
# qty = Decimal(car_data['quantity'])
net_car_price = Decimal(data['discounted_price'])
net_additionals_price = Decimal(data['additional_services']['total'])
vat_amount = Decimal(data['vat_amount'])
grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount
cost_total = Decimal(car.cost_price)
discount_amount =Decimal(data['discount_amount'])
net_car_price = Decimal(data["discounted_price"])
net_additionals_price = Decimal(data["additional_services"]["total"])
vat_amount = Decimal(data["vat_amount"])
grand_total = net_car_price + car.get_additional_services_amount_ + vat_amount
cost_total = Decimal(car.cost_price)
discount_amount = Decimal(data["discount_amount"])
# ------------------------------------------------------------------
# 2A. Journal: Cash / A-R / VAT / Sales
@ -1606,15 +1649,15 @@ def _post_sale_and_cogs(invoice, dealer):
description=f"Sale {car.vin}",
origin=f"Invoice {invoice.invoice_number}",
locked=False,
posted=False
posted=False,
)
# Dr Cash (what the customer paid)
TransactionModel.objects.create(
journal_entry=je_sale,
account=cash_acc,
amount=grand_total,
tx_type='debit',
description='Debit to Cash on Hand'
tx_type="debit",
description="Debit to Cash on Hand",
)
# # Cr A/R (clear the receivable)
@ -1630,8 +1673,8 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_sale,
account=vat_acc,
amount=vat_amount,
tx_type='credit',
description="Credit to Tax Payable"
tx_type="credit",
description="Credit to Tax Payable",
)
# Cr Sales Car
@ -1639,8 +1682,8 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_sale,
account=car_rev,
amount=net_car_price,
tx_type='credit',
description=" Credit to Car Sales"
tx_type="credit",
description=" Credit to Car Sales",
)
if car.get_additional_services_amount > 0:
@ -1649,16 +1692,15 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_sale,
account=add_rev,
amount=car.get_additional_services_amount,
tx_type='credit',
description="Credit to After-Sales Services"
tx_type="credit",
description="Credit to After-Sales Services",
)
TransactionModel.objects.create(
journal_entry=je_sale,
account=vat_acc,
amount=car.get_additional_services_vat,
tx_type='credit',
description="Credit to Tax Payable (Additional Services)"
tx_type="credit",
description="Credit to Tax Payable (Additional Services)",
)
# ------------------------------------------------------------------
@ -1669,7 +1711,7 @@ def _post_sale_and_cogs(invoice, dealer):
description=f"COGS {car.vin}",
origin=f"Invoice {invoice.invoice_number}",
locked=False,
posted=False
posted=False,
)
# Dr COGS
@ -1677,15 +1719,12 @@ def _post_sale_and_cogs(invoice, dealer):
journal_entry=je_cogs,
account=cogs_acc,
amount=cost_total,
tx_type='debit',
tx_type="debit",
)
# Cr Inventory
TransactionModel.objects.create(
journal_entry=je_cogs,
account=inv_acc,
amount=cost_total,
tx_type='credit'
journal_entry=je_cogs, account=inv_acc, amount=cost_total, tx_type="credit"
)
# ------------------------------------------------------------------
# 2C. Update car state flags inside the same transaction
@ -1693,10 +1732,12 @@ def _post_sale_and_cogs(invoice, dealer):
entity.get_items_inventory().filter(name=car.vin).update(for_inventory=False)
# car.item_model.for_inventory = False
# car.item_model.save(update_fields=['for_inventory'])
car.discount_amount=discount_amount
car.discount_amount = discount_amount
car.selling_price = grand_total
# car.is_sold = True
car.save()
# def handle_account_process(invoice, amount, finance_data):
# """
# Processes accounting transactions based on an invoice, financial data,
@ -1787,29 +1828,29 @@ def _post_sale_and_cogs(invoice, dealer):
# car.finances.save()
# car.item_model.save()
# TransactionModel.objects.create(
# journal_entry=journal,
# account=additional_services_account, # Debit Additional Services
# amount=Decimal(car.finances.total_additionals),
# tx_type="debit",
# description="Additional Services",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=additional_services_account, # Debit Additional Services
# amount=Decimal(car.finances.total_additionals),
# tx_type="debit",
# description="Additional Services",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=inventory_account, # Credit Inventory account
# amount=Decimal(finance_data.get("grand_total")),
# tx_type="credit",
# description="Account Adjustment",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=inventory_account, # Credit Inventory account
# amount=Decimal(finance_data.get("grand_total")),
# tx_type="credit",
# description="Account Adjustment",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=vat_payable_account, # Credit VAT Payable
# amount=finance_data.get("total_vat_amount"),
# tx_type="credit",
# description="VAT Payable on Invoice",
# )
# TransactionModel.objects.create(
# journal_entry=journal,
# account=vat_payable_account, # Credit VAT Payable
# amount=finance_data.get("total_vat_amount"),
# tx_type="credit",
# description="VAT Payable on Invoice",
# )
def create_make_accounts(dealer):
@ -1857,6 +1898,7 @@ def create_make_accounts(dealer):
active=True,
)
def handle_payment(request, order):
url = "https://api.moyasar.com/v1/payments"
callback_url = request.build_absolute_uri(
@ -1943,7 +1985,6 @@ def handle_payment(request, order):
# return user.dealer.quota
def get_accounts_data():
return [
# Current Assets (must start with 1)
@ -2339,6 +2380,7 @@ def get_accounts_data():
},
]
def create_account(entity, coa, account_data):
try:
account = entity.create_account(
@ -2371,17 +2413,16 @@ def get_or_generate_car_image(car):
return car_image.image.url
# Check for existing image with same hash
existing = models.CarImage.objects.filter(
image_hash=car_image.image_hash,
image__isnull=False
).exclude(car=car).first()
existing = (
models.CarImage.objects.filter(
image_hash=car_image.image_hash, image__isnull=False
)
.exclude(car=car)
.first()
)
if existing:
car_image.image.save(
existing.image.name,
existing.image.file,
save=True
)
car_image.image.save(existing.image.name, existing.image.file, save=True)
return car_image.image.url
# If no image exists and not already generating, schedule generation
@ -2394,6 +2435,7 @@ def get_or_generate_car_image(car):
logger.error(f"Error getting/generating car image: {e}")
return None
def force_regenerate_car_image(car):
"""
Force regeneration of car image (useful for admin actions)
@ -2414,6 +2456,7 @@ def force_regenerate_car_image(car):
logger.error(f"Error forcing image regeneration: {e}")
return False
class CarImageAPIClient:
"""Simple client to handle authenticated requests to the car image API"""
@ -2436,7 +2479,7 @@ class CarImageAPIClient:
response.raise_for_status()
# Get CSRF token from cookies
self.csrf_token = self.session.cookies.get('csrftoken')
self.csrf_token = self.session.cookies.get("csrftoken")
if not self.csrf_token:
raise Exception("CSRF token not found in cookies")
@ -2444,16 +2487,17 @@ class CarImageAPIClient:
login_data = {
"username": self.USERNAME,
"password": self.PASSWORD,
"csrfmiddlewaretoken": self.csrf_token
"csrfmiddlewaretoken": self.csrf_token,
}
login_response = self.session.post(
f"{self.BASE_URL}/login",
data=login_data
f"{self.BASE_URL}/login", data=login_data
)
if login_response.status_code != 200:
raise Exception(f"Login failed with status {login_response.status_code}")
raise Exception(
f"Login failed with status {login_response.status_code}"
)
logger.info("Successfully logged in to car image API")
return True
@ -2472,39 +2516,38 @@ class CarImageAPIClient:
try:
headers = {
'X-CSRFToken': self.csrf_token,
'Referer': self.BASE_URL,
"X-CSRFToken": self.csrf_token,
"Referer": self.BASE_URL,
}
print(payload)
generate_data = {
"year": payload['year'],
"make": payload['make'],
"model": payload['model'],
"exterior_color": payload['color'],
"year": payload["year"],
"make": payload["make"],
"model": payload["model"],
"exterior_color": payload["color"],
"angle": "3/4 rear",
"reference_image": ""
"reference_image": "",
}
response = self.session.post(
f"{self.BASE_URL}/generate",
json=generate_data,
headers=headers,
timeout=160
timeout=160,
)
response.raise_for_status()
# Parse response
result = response.json()
image_url = result.get('url')
image_url = result.get("url")
if not image_url:
raise Exception("No image URL in response")
# Download the actual image
image_response = self.session.get(
f"{self.BASE_URL}{image_url}",
timeout=160
f"{self.BASE_URL}{image_url}", timeout=160
)
image_response.raise_for_status()
@ -2520,9 +2563,11 @@ class CarImageAPIClient:
logger.error(error_msg)
return None, error_msg
# Global client instance
api_client = CarImageAPIClient()
def resize_image(image_data, max_size=(800, 600)):
"""
Resize image to make it smaller while maintaining aspect ratio
@ -2539,29 +2584,31 @@ def resize_image(image_data, max_size=(800, 600)):
# Save back to bytes in original format
output_buffer = BytesIO()
if original_format and original_format.upper() in ['JPEG', 'JPG']:
img.save(output_buffer, format='JPEG', quality=95, optimize=True)
elif original_format and original_format.upper() == 'PNG':
if original_format and original_format.upper() in ["JPEG", "JPG"]:
img.save(output_buffer, format="JPEG", quality=95, optimize=True)
elif original_format and original_format.upper() == "PNG":
# Preserve transparency for PNG
if original_mode == 'RGBA':
img.save(output_buffer, format='PNG', optimize=True)
if original_mode == "RGBA":
img.save(output_buffer, format="PNG", optimize=True)
else:
img.save(output_buffer, format='PNG', optimize=True)
img.save(output_buffer, format="PNG", optimize=True)
else:
# Default to JPEG for other formats
if img.mode in ('RGBA', 'LA', 'P'):
if img.mode in ("RGBA", "LA", "P"):
# Convert to RGB if image has transparency
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'RGBA':
background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "RGBA":
background.paste(img, mask=img.split()[3])
else:
background.paste(img, (0, 0))
img = background
img.save(output_buffer, format='JPEG', quality=95, optimize=True)
img.save(output_buffer, format="JPEG", quality=95, optimize=True)
resized_data = output_buffer.getvalue()
logger.info(f"Resized image from {len(image_data)} to {len(resized_data)} bytes")
logger.info(
f"Resized image from {len(image_data)} to {len(resized_data)} bytes"
)
return resized_data, None
except Exception as e:
@ -2569,6 +2616,7 @@ def resize_image(image_data, max_size=(800, 600)):
logger.error(error_msg)
return None, error_msg
def generate_car_image_simple(car_image):
"""
Simple function to generate car image with authentication and resizing
@ -2577,10 +2625,10 @@ def generate_car_image_simple(car_image):
# Prepare payload
payload = {
'make': car.id_car_make.name if car.id_car_make else '',
'model': car.id_car_model.name if car.id_car_model else '',
'year': car.year,
'color': car.colors.exterior.name
"make": car.id_car_make.name if car.id_car_make else "",
"model": car.id_car_model.name if car.id_car_model else "",
"year": car.year,
"color": car.colors.exterior.name,
}
logger.info(f"Generating image for car {car.vin}")
@ -2589,10 +2637,10 @@ def generate_car_image_simple(car_image):
image_data, error = api_client.generate_image(payload)
if error:
return {'success': False, 'error': error}
return {"success": False, "error": error}
if not image_data:
return {'success': False, 'error': 'No image data received'}
return {"success": False, "error": "No image data received"}
try:
# Resize the image to make it smaller
@ -2606,21 +2654,21 @@ def generate_car_image_simple(car_image):
# Determine file extension based on content
try:
img = Image.open(BytesIO(resized_data))
file_extension = img.format.lower() if img.format else 'jpg'
file_extension = img.format.lower() if img.format else "jpg"
except:
file_extension = 'jpg'
file_extension = "jpg"
# Save the resized image
car_image.image.save(
f"{car_image.image_hash}.{file_extension}",
ContentFile(resized_data),
save=False
save=False,
)
logger.info(f"Successfully generated and resized image for car {car.vin}")
return {'success': True}
return {"success": True}
except Exception as e:
error_msg = f"Image processing failed: {e}"
logger.error(error_msg)
return {'success': False, 'error': error_msg}
return {"success": False, "error": error_msg}

View File

@ -2,13 +2,15 @@ from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
import re
class SaudiPhoneNumberValidator(RegexValidator):
def __init__(self, *args, **kwargs):
super().__init__(
regex=r"^(\+9665|05)[0-9]{8}$",
message=_("Enter a valid Saudi phone number (05XXXXXXXX or +9665XXXXXXXX)"),
)
def __call__(self, value):
# Remove any whitespace, dashes, or other separators
cleaned_value = re.sub(r'[\s\-\(\)\.]', '', str(value))
super().__call__(cleaned_value)
cleaned_value = re.sub(r"[\s\-\(\)\.]", "", str(value))
super().__call__(cleaned_value)

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ def run():
# arabic_name=item.get("arabic_name", ""),
# logo=item.get("Logo", ""),
# is_sa_import=item.get("is_sa_import", False),
slug=unique_slug
slug=unique_slug,
)
# Step 2: Insert CarModel
@ -60,7 +60,7 @@ def run():
id_car_make_id=item["id_car_make"],
name=item["name"],
# arabic_name=item.get("arabic_name", ""),
slug=unique_slug
slug=unique_slug,
)
# Step 3: Insert CarSerie
@ -77,7 +77,7 @@ def run():
year_begin=item.get("year_begin"),
year_end=item.get("year_end"),
generation_name=item.get("generation_name", ""),
slug=unique_slug
slug=unique_slug,
)
# Step 4: Insert CarTrim
@ -98,9 +98,10 @@ def run():
# Step 5: Insert CarEquipment
for item in tqdm(data["car_equipment"], desc="Inserting CarEquipment"):
if not CarEquipment.objects.filter(id_car_equipment=item["id_car_equipment"]).exists():
if not CarEquipment.objects.filter(
id_car_equipment=item["id_car_equipment"]
).exists():
if CarTrim.objects.filter(id_car_trim=item["id_car_trim"]).exists():
unique_slug = generate_unique_slug(CarEquipment, item["name"])
CarEquipment.objects.create(
@ -108,7 +109,7 @@ def run():
id_car_trim_id=item["id_car_trim"],
name=item["name"],
year_begin=item.get("year"),
slug=unique_slug
slug=unique_slug,
)
# Step 6: Insert CarSpecification (Parent specifications first)

View File

@ -34,7 +34,7 @@ def run():
is_boolean=True,
url="pricing",
)
# Create the plans
# Create the plans
free_plan = Plan.objects.create(
name="Free",
description="Free plan with limited features",
@ -46,7 +46,7 @@ def run():
order=1,
)
free_plan.quotas.add(free_quota)
# Create the plans
basic_plan = Plan.objects.create(
name="Basic",
@ -58,7 +58,7 @@ def run():
visible=True,
order=1,
)
basic_plan.quotas.add(basic_quota,free_quota)
basic_plan.quotas.add(basic_quota, free_quota)
pro_plan = Plan.objects.create(
name="Professional",
@ -69,7 +69,7 @@ def run():
visible=True,
# order=2
)
pro_plan.quotas.add(free_quota,basic_quota, pro_quota)
pro_plan.quotas.add(free_quota, basic_quota, pro_quota)
premium_plan = Plan.objects.create(
name="Premium",
@ -80,4 +80,4 @@ def run():
visible=True,
order=3,
)
premium_plan.quotas.add(free_quota,basic_quota, pro_quota, premium_quota)
premium_plan.quotas.add(free_quota, basic_quota, pro_quota, premium_quota)

View File

@ -1,177 +1,175 @@
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 - Access Forbidden</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
:root {
--dark-bg: #121212;
--main-color: #ff3864;
--secondary-color: #e6e6e6;
}
body, html {
height: 100%;
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--dark-bg);
color: var(--secondary-color);
overflow: hidden; /* Hide overflow for the particles */
}
.center-content {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
flex-direction: column;
z-index: 2; /* Ensure content is on top of particles */
position: relative;
}
.glitch {
font-size: 10rem;
font-weight: bold;
color: var(--main-color);
position: relative;
animation: glitch-animation 2.5s infinite;
}
@keyframes glitch-animation {
0% { text-shadow: 2px 2px var(--secondary-color); }
20% { text-shadow: -2px -2px var(--secondary-color); }
40% { text-shadow: 4px 4px var(--main-color); }
60% { text-shadow: -4px -4px var(--main-color); }
80% { text-shadow: 6px 6px var(--secondary-color); }
100% { text-shadow: -6px -6px var(--secondary-color); }
}
.main-message {
font-size: 2.5rem;
margin-top: -20px;
letter-spacing: 5px;
text-transform: uppercase;
}
.sub-message {
font-size: 1.2rem;
color: #8c8c8c;
margin-top: 10px;
}
.home-button {
margin-top: 30px;
padding: 12px 30px;
background-color: var(--main-color);
border: none;
color: #fff;
border-radius: 5px;
font-size: 1rem;
text-decoration: none;
transition: background-color 0.3s, transform 0.3s;
}
.home-button:hover {
background-color: #d12e52;
transform: scale(1.05);
}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 - Access Forbidden</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
<style>
:root {
--dark-bg: #121212;
--main-color: #ff3864;
--secondary-color: #e6e6e6;
}
body, html {
height: 100%;
margin: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--dark-bg);
color: var(--secondary-color);
overflow: hidden; /* Hide overflow for the particles */
}
.center-content {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
flex-direction: column;
z-index: 2; /* Ensure content is on top of particles */
position: relative;
}
.glitch {
font-size: 10rem;
font-weight: bold;
color: var(--main-color);
position: relative;
animation: glitch-animation 2.5s infinite;
}
@keyframes glitch-animation {
0% { text-shadow: 2px 2px var(--secondary-color); }
20% { text-shadow: -2px -2px var(--secondary-color); }
40% { text-shadow: 4px 4px var(--main-color); }
60% { text-shadow: -4px -4px var(--main-color); }
80% { text-shadow: 6px 6px var(--secondary-color); }
100% { text-shadow: -6px -6px var(--secondary-color); }
}
.main-message {
font-size: 2.5rem;
margin-top: -20px;
letter-spacing: 5px;
text-transform: uppercase;
}
.sub-message {
font-size: 1.2rem;
color: #8c8c8c;
margin-top: 10px;
}
.home-button {
margin-top: 30px;
padding: 12px 30px;
background-color: var(--main-color);
border: none;
color: #fff;
border-radius: 5px;
font-size: 1rem;
text-decoration: none;
transition: background-color 0.3s, transform 0.3s;
}
.home-button:hover {
background-color: #d12e52;
transform: scale(1.05);
}
/* Particle Background styles */
#particles-js {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
}
</style>
</head>
<body>
<div id="particles-js"></div>
<div class="center-content container-fluid">
<h1 class="glitch">403</h1>
<h2 class="main-message">{% trans "Access Forbidden" %}</h2>
<p class="sub-message">{% trans "You do not have permission to view this page."%}</p>
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia"%}</p>
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
<script>
#particles-js {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
}
</style>
</head>
<body>
<div id="particles-js"></div>
<div class="center-content container-fluid">
<h1 class="glitch">403</h1>
<h2 class="main-message">{% trans "Access Forbidden" %}</h2>
<p class="sub-message">{% trans "You do not have permission to view this page." %}</p>
<p class="sub-message fs-2">{% trans "Powered By Tenhal, Riyadh Saudi Arabia" %}</p>
<a href="{% url 'home' %}" class="home-button">{% trans "Go Home" %}</a>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/particles.js@2.0.0/particles.min.js"></script>
<script>
/* Particles.js Configuration */
particlesJS("particles-js", {
"particles": {
"number": {
"value": 80,
"density": {
"enable": true,
"value_area": 800
}
},
"color": {
"value": "#ff3864"
},
"shape": {
"type": "circle",
},
"opacity": {
"value": 0.5,
"random": false,
"anim": {
"enable": false
}
},
"size": {
"value": 3,
"random": true,
},
"line_linked": {
"enable": true,
"distance": 150,
"color": "#e6e6e6",
"opacity": 0.4,
"width": 1
},
"move": {
"enable": true,
"speed": 6,
"direction": "none",
"random": false,
"straight": false,
"out_mode": "out",
"bounce": false,
"attract": {
"enable": false,
}
}
},
"interactivity": {
"detect_on": "canvas",
"events": {
"onhover": {
"enable": true,
"mode": "grab"
},
"onclick": {
"enable": true,
"mode": "push"
},
"resize": true
},
"modes": {
"grab": {
"distance": 140,
"line_linked": {
"opacity": 1
particlesJS("particles-js", {
"particles": {
"number": {
"value": 80,
"density": {
"enable": true,
"value_area": 800
}
},
"push": {
"particles_nb": 4
"color": {
"value": "#ff3864"
},
"shape": {
"type": "circle",
},
"opacity": {
"value": 0.5,
"random": false,
"anim": {
"enable": false
}
},
"size": {
"value": 3,
"random": true,
},
"line_linked": {
"enable": true,
"distance": 150,
"color": "#e6e6e6",
"opacity": 0.4,
"width": 1
},
"move": {
"enable": true,
"speed": 6,
"direction": "none",
"random": false,
"straight": false,
"out_mode": "out",
"bounce": false,
"attract": {
"enable": false,
}
}
}
},
"retina_detect": true
});
</script>
</body>
</html>
},
"interactivity": {
"detect_on": "canvas",
"events": {
"onhover": {
"enable": true,
"mode": "grab"
},
"onclick": {
"enable": true,
"mode": "push"
},
"resize": true
},
"modes": {
"grab": {
"distance": 140,
"line_linked": {
"opacity": 1
}
},
"push": {
"particles_nb": 4
}
}
},
"retina_detect": true
});
</script>
</body>
</html>

View File

@ -55,27 +55,27 @@
rel="stylesheet"
id="user-style-default" />
<style>
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.main {
width: 100%;
max-width: 1200px;
margin: auto;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.text-center {
text-align: center;
}
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.main {
width: 100%;
max-width: 1200px;
margin: auto;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.text-center {
text-align: center;
}
</style>
</head>
<body>

View File

@ -55,27 +55,27 @@
rel="stylesheet"
id="user-style-default" />
<style>
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.main {
width: 100%;
max-width: 1200px;
margin: auto;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.text-center {
text-align: center;
}
body, html {
height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.main {
width: 100%;
max-width: 1200px;
margin: auto;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.text-center {
text-align: center;
}
</style>
</head>
<body>

View File

@ -61,19 +61,19 @@
rel="stylesheet"
id="user-style-default">
<script>
var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl');
} else {
var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true);
}
var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl');
} else {
var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true);
}
</script>
</head>
<body>
@ -109,17 +109,17 @@
</div>
</div>
<script>
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker');
}
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker');
}
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
}
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
}
</script>
<div class="support-chat-row">
<div class="row-fluid support-chat">

View File

@ -6,7 +6,6 @@
{% trans "Sign In" %}
{% endblock head_title %}
{% block content %}
<section class="main my-2">
<div class="container" style="max-width:40rem;">
<div class="class="row form-container" id="form-container"">
@ -78,9 +77,7 @@
</div>
</div>
</section>
{% include 'footer.html' %}
{% include 'footer.html' %}
{% if LOGIN_BY_CODE_ENABLED or PASSKEY_LOGIN_ENABLED %}
<hr>
{% element button_group vertical=True %}

View File

@ -282,9 +282,7 @@
</div>
</div>
</section>
{% include 'footer.html' %}
{% include 'footer.html' %}
<script src="{% static 'js/phoenix.js' %}"></script>
{% endblock content %}
{% block customJS %}
@ -293,97 +291,97 @@
<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 validatePassword(password, confirmPassword) {
return password === confirmPassword && password.length > 7 && password !== '';
}
});
}
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;
}
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email) && email !== '';
}
}
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);
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);
}
}
} catch (error) {
notify("error",error);
}
}
</script>
{% endblock customJS %}

View File

@ -6,42 +6,42 @@
{% trans 'Dealer Settings' %}
{% endblock %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-6">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% trans "Dealer Settings" %}
<i class="fas fa-solid fa-gear ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form action="" method="post" class="needs-validation" novalidate>
{% csrf_token %}
<div class="row g-1">
<div class="col-12">
<h4 class="mb-4 text-center">{% trans 'Default Invoice Accounts' %}</h4>
{{ form.invoice_cash_account|as_crispy_field }}
{{ form.invoice_prepaid_account|as_crispy_field }}
{{ form.invoice_unearned_account|as_crispy_field }}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-6">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% trans "Dealer Settings" %}
<i class="fas fa-solid fa-gear ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form action="" method="post" class="needs-validation" novalidate>
{% csrf_token %}
<div class="row g-1">
<div class="col-12">
<h4 class="mb-4 text-center">{% trans 'Default Invoice Accounts' %}</h4>
{{ form.invoice_cash_account|as_crispy_field }}
{{ form.invoice_prepaid_account|as_crispy_field }}
{{ form.invoice_unearned_account|as_crispy_field }}
</div>
<div class="col-12 mt-4">
<h4 class="mb-4 text-center">{% trans 'Default Bill Accounts' %}</h4>
{{ form.bill_cash_account|as_crispy_field }}
{{ form.bill_prepaid_account|as_crispy_field }}
{{ form.bill_unearned_account|as_crispy_field }}
</div>
</div>
<div class="col-12 mt-4">
<h4 class="mb-4 text-center">{% trans 'Default Bill Accounts' %}</h4>
{{ form.bill_cash_account|as_crispy_field }}
{{ form.bill_prepaid_account|as_crispy_field }}
{{ form.bill_unearned_account|as_crispy_field }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg" type="submit">
<i class="fa-solid fa-pen-to-square me-1"></i>
{% trans 'Update' %}
</button>
</div>
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg" type="submit">
<i class="fa-solid fa-pen-to-square me-1"></i>
{% trans 'Update' %}
</button>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -23,7 +23,7 @@
<h3 class="mb-0 fs-4 text-center text-white">{% trans 'Activate Account' %}</h3>
</div>
<div class="card-body bg-light-subtle">
<p class="text-center">{{ _("Are you sure you want to activate this account")}} "{{ obj.email }}"?</p>
<p class="text-center">{{ _("Are you sure you want to activate this account") }} "{{ obj.email }}"?</p>
<form method="post">
{% csrf_token %}
<hr class="my-2">

View File

@ -1,29 +1,33 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans 'Admin Management' %} {% endblock %}
{% block content %}
<h3 class="my-4">{% trans "Admin Management" %}<li class="fa fa-user-cog ms-2 text-primary"></li></h3>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
<div class="col">
<a href="{% url 'user_management' request.dealer.slug %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("User Management") }}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
{% trans 'Admin Management' %}
{% endblock %}
{% block content %}
<h3 class="my-4">
{% trans "Admin Management" %}
<li class="fa fa-user-cog ms-2 text-primary"></li>
</h3>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-4 mt-10">
<div class="col">
<a href="{% url 'user_management' request.dealer.slug %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("User Management") }}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
</a>
</div>
<div class="col">
<a href="{% url 'audit_log_dashboard' request.dealer.slug %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("Audit Log Dashboard") }}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
</div>
</a>
</div>
</div>
</a>
</div>
{% endblock content %}
<div class="col">
<a href="{% url 'audit_log_dashboard' request.dealer.slug %}">
<div class="card h-100">
<div class="card-header text-center">
<h5 class="card-title">{{ _("Audit Log Dashboard") }}</h5>
<span class="me-2"><i class="fas fa-user fa-2x"></i></span>
</div>
</div>
</a>
</div>
</div>
{% endblock content %}

View File

@ -32,15 +32,7 @@
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -78,15 +78,7 @@
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -65,15 +65,7 @@
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -25,15 +25,7 @@
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -11,22 +11,9 @@
method="post"
action=""
id="workingHoursForm"
data-action="{% if working_hours_instance %}
update
{% else %}
create
{% endif %}"
data-working-hours-id="
{% if working_hours_instance %}
{{ working_hours_instance.id }}
{% else %}
0
{% endif %}"
data-staff-user-id="{% if staff_user_id %}
{{ staff_user_id }}
{% else %}
0
{% endif %}">
data-action="{% if working_hours_instance %} update {% else %} create {% endif %}"
data-working-hours-id=" {% if working_hours_instance %} {{ working_hours_instance.id }} {% else %} 0 {% endif %}"
data-staff-user-id="{% if staff_user_id %} {{ staff_user_id }} {% else %} 0 {% endif %}">
{% csrf_token %}
{% if working_hours_form.staff_member %}
<div class="form-group mb-3">
@ -94,15 +81,7 @@
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -23,15 +23,7 @@
<div class="messages" style="margin: 20px 0">
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -68,7 +68,9 @@
{% endif %}
{% for sf in all_staff_members %}
<option value="{{ sf.id }}"
{% if staff_member and staff_member.id == sf.id %}selected{% endif %}>{{ sf.get_staff_member_name }}</option>
{% if staff_member and staff_member.id == sf.id %}selected{% endif %}>
{{ sf.get_staff_member_name }}
</option>
{% endfor %}
</select>
</div>
@ -88,15 +90,7 @@
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -24,15 +24,7 @@
</ul>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -29,8 +29,7 @@
</form>
{% if messages %}
{% for message in messages %}
<div class="vcode-alert vcode-alert-
{% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
<div class="vcode-alert vcode-alert- {% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
{% endfor %}
{% endif %}
</div>

View File

@ -69,15 +69,7 @@
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -40,15 +40,7 @@
<p class="message">{{ page_message }}</p>
{% if messages %}
{% for message in messages %}
<div class="alert alert-dismissible
{% if message.tags %}
alert-
{% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %}
danger
{% else %}
{{ message.tags }}
{% endif %}
{% endif %}"
<div class="alert alert-dismissible {% if message.tags %} alert- {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} danger {% else %} {{ message.tags }} {% endif %} {% endif %}"
role="alert">{{ message }}</div>
{% endfor %}
{% endif %}

View File

@ -3,11 +3,7 @@
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}"
dir="{% if LANGUAGE_CODE == 'ar' %}
rtl
{% else %}
ltr
{% endif %}"
dir="{% if LANGUAGE_CODE == 'ar' %} rtl {% else %} ltr {% endif %}"
data-bs-theme=""
data-navigation-type="default"
data-navbar-horizontal-shape="default">

View File

@ -2,11 +2,7 @@
<!DOCTYPE html>
{% get_current_language as LANGUAGE_CODE %}
<html lang="{{ LANGUAGE_CODE }}"
{% if LANGUAGE_CODE == 'ar' %}
dir="rtl"
{% else %}
dir="ltr"
{% endif %}
{% if LANGUAGE_CODE == 'ar' %} dir="rtl" {% else %} dir="ltr" {% endif %}
data-bs-theme=""
data-navigation-type="default"
data-navbar-horizontal-shape="default">
@ -54,8 +50,14 @@
<link href="{% static 'css/custom.css' %}" rel="stylesheet">
{% comment %} <link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css"> {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
<link href="{% static 'css/theme-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}"
type="text/css"
rel="stylesheet"
id="user-style-rtl">
{% else %}
<link href="{% static 'css/theme.min.css' %}"
type="text/css"
@ -66,11 +68,8 @@
rel="stylesheet"
id="user-style-default">
{% endif %}
<script src="{% static 'js/main.js' %}"></script>
<script src="{% static 'js/jquery.min.js' %}"></script>
{% comment %} <script src="{% static 'js/echarts.js' %}"></script> {% endcomment %}
{% comment %} {% block customCSS %}{% endblock %} {% endcomment %}
</head>
@ -85,19 +84,25 @@
{% block period_navigation %}
{% endblock period_navigation %}
<div id="spinner" class="htmx-indicator spinner-bg">
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
</div>
<div id="main_content" class="fade-me-in" hx-boost="false" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
</div>
<div id="main_content"
class="fade-me-in"
hx-boost="false"
hx-target="#main_content"
hx-select="#main_content"
hx-swap="outerHTML transition:true"
hx-select-oob="#toast-container"
hx-history-elt>
{% block customCSS %}{% endblock %}
{% block content %}{% endblock content %}
{% block content %}
{% endblock content %}
{% block customJS %}{% endblock %}
{% comment %} <script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
<script src="{% static 'vendors/popper/popper.min.js' %}"></script>
<script src="{% static 'vendors/bootstrap/bootstrap.min.js' %}"></script> {% endcomment %}
<script src="{% static 'js/phoenix.js' %}"></script>
</div>
{% block body %}
{% endblock body %}
@ -126,7 +131,7 @@
{% comment %} <script src="{% static 'vendors/echarts/echarts.min.js' %}"></script> {% endcomment %}
{% comment %} <script src="{% static 'js/crm-analytics.js' %}"></script> {% endcomment %}
{% comment %} <script src="{% static 'js/travel-agency-dashboard.js' %}"></script>
<script src="{% static 'js/crm-dashboard.js' %}"></script>
<script src="{% static 'js/crm-dashboard.js' %}"></script>
<script src="{% static 'js/projectmanagement-dashboard.js' %}"></script> {% endcomment %}
{% comment %} <script src="{% static 'vendors/mapbox-gl/mapbox-gl.js' %}"></script> {% endcomment %}
{% comment %} <script src="{% static 'vendors/turf.min.js' %}"></script> {% endcomment %}
@ -156,57 +161,57 @@
document.getElementById('global-indicator')
];
});*/
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
document.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.xhr.status == 403){
/* Notify the user of a 404 Not Found response */
notify("error", "You do not have permission to view this page");
}
if(evt.detail.xhr.status == 404){
/* Notify the user of a 404 Not Found response */
return alert("Error: Could Not Find Resource");
}
if (evt.detail.successful != true) {
console.log(evt.detail.xhr.statusText)
/* Notify of an unexpected error, & print error to console */
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
}
});
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) {
backdrop.remove();
});
}
});
// Close modal after successful form submission
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
document.querySelectorAll('.modal').forEach(function(m) {
var modal = bootstrap.Modal.getInstance(m);
if (modal) {
modal.hide();
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
});
}
});
document.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.xhr.status == 403){
/* Notify the user of a 404 Not Found response */
notify("error", "You do not have permission to view this page");
}
if(evt.detail.xhr.status == 404){
/* Notify the user of a 404 Not Found response */
return alert("Error: Could Not Find Resource");
}
if (evt.detail.successful != true) {
console.log(evt.detail.xhr.statusText)
/* Notify of an unexpected error, & print error to console */
notify("error", `Unexpected Error ,${evt.detail.xhr.statusText}`);
}
});
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) {
backdrop.remove();
});
}
});
// Close modal after successful form submission
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
document.querySelectorAll('.modal').forEach(function(m) {
var modal = bootstrap.Modal.getInstance(m);
if (modal) {
modal.hide();
}
});
}
});
</script>
{% comment %} {% block customJS %}{% endblock %} {% endcomment %}
</body>

View File

@ -6,48 +6,48 @@
{% block title %}
{{ _("Create Bill") |capfirst }}
{% endblock title %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5 ">
<div class="col-md-6">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% trans 'Create Bill' %}
<i class="fas fa-money-bills ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form action="{{ form_action_url }}" method="post" id="djl-bill-model-create-form-id" class="needs-validation" novalidate>
{% csrf_token %}
{% if po_model %}
<div class="text-center mb-4">
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
<div class="d-flex flex-column gap-2">
{% for itemtxs in po_itemtxs_qs %}
<span class="badge bg-secondary">{{ itemtxs }}</span>
{% endfor %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5 ">
<div class="col-md-6">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% trans 'Create Bill' %}
<i class="fas fa-money-bills ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form action="{{ form_action_url }}"
method="post"
id="djl-bill-model-create-form-id"
class="needs-validation"
novalidate>
{% csrf_token %}
{% if po_model %}
<div class="text-center mb-4">
<h3 class="h5">{% trans 'Bill for' %} {{ po_model.po_number }}</h3>
<p class="text-muted mb-3">{% trans 'Bill for' %} {{ po_model.po_title }}</p>
<div class="d-flex flex-column gap-2">
{% for itemtxs in po_itemtxs_qs %}<span class="badge bg-secondary">{{ itemtxs }}</span>{% endfor %}
</div>
</div>
{% endif %}
<div class="mb-4">{{ form|crispy }}</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{% url 'bill_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
{% endif %}
<div class="mb-4">
{{ form|crispy }}
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{% url 'bill_list' request.dealer.slug%}" class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -4,180 +4,160 @@
{% load django_ledger %}
{% load custom_filters %}
{% block title %}Bill Details{% endblock %}
{% block content%}
{% block content %}
<div class="row mt-4">
<div class="col-12 mb-3">
<div class="card shadow-sm">
<div class="card-body">
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
</div>
{% if bill.is_configured %}
<div class="row text-center g-3 mb-3">
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">
{% trans 'Cash Account' %}:
{% if bill.cash_account %}
<a href="{% url 'account_detail' request.dealer.slug bill.cash_account.coa_model.pk bill.cash_account.uuid %}"
class="text-decoration-none ms-1">{{ bill.cash_account.code }}</a>
{% else %}
{{ bill.cash_account.code }}
{% endif %}
</h6>
<h4 class="mb-0" id="djl-bill-detail-amount-paid">
{% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}
</h4>
</div>
{% if bill.accrue %}
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">
{% trans 'Prepaid Account' %}:
{% if bill.prepaid_account %}
<a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.coa_model.pk bill.prepaid_account.uuid %}"
class="text-decoration-none ms-1">
{{ bill.prepaid_account.code }}
</a>
{% else %}
{{ bill.prepaid_account.code }}
{% endif %}
</h6>
<h4 class="text-success mb-0" id="djl-bill-detail-amount-prepaid">
{% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }}
</h4>
</div>
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">
{% trans 'Accounts Payable' %}:
{% if bill.unearned_account %}
<a href="{% url 'account_detail' request.dealer.slug bill.unearned_account.coa_model.pk bill.unearned_account.uuid %}"
class="text-decoration-none ms-1">
{{ bill.unearned_account.code }}
</a>
{% else %}
{{ bill.unearned_account.code }}
{% endif %}
</h6>
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-unearned">
{% currency_symbol %}{{ bill.get_amount_unearned | currency_format }}
</h4>
</div>
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'Accrued' %} {{ bill.get_progress | percentage }}</h6>
<h4 class="mb-0">{% currency_symbol %}{{ bill.get_amount_earned | currency_format }}</h4>
</div>
{% else %}
<div class="col-12 col-md-3 offset-md-6">
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'You Still Owe' %}</h6>
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-owed">
{% currency_symbol %}{{ bill.get_amount_open | currency_format }}
</h4>
</div>
{% endif %}
</div>
{% endif %}
<div class="card shadow-sm">
<div class="card-body">
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
</div>
</div>
<div class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-receipt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Items' %}</h5>
</div>
{% if bill.is_configured %}
<div class="row text-center g-3 mb-3">
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">
{% trans 'Cash Account' %}:
{% if bill.cash_account %}
<a href="{% url 'account_detail' request.dealer.slug bill.cash_account.coa_model.pk bill.cash_account.uuid %}"
class="text-decoration-none ms-1">{{ bill.cash_account.code }}</a>
{% else %}
{{ bill.cash_account.code }}
{% endif %}
</h6>
<h4 class="mb-0" id="djl-bill-detail-amount-paid">
{% currency_symbol %}{{ bill.get_amount_cash | absolute | currency_format }}
</h4>
</div>
<div class="card-body px-0 pt-0 pb-2">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Item' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Entity Unit' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Unit Cost' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Quantity' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Total' %}</th>
<th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th>
</tr>
</thead>
<tbody class="list fs-9" id="project-list-table-body">
{% for bill_item in itemtxs_qs %}
<tr>
<td class="align-middle white-space-nowrap">
<div class="d-flex px-2 py-1">
<div class="d-flex flex-column justify-content-center">
<h6 class="mb-0 text-sm">{{ bill_item.item_model }}</h6>
</div>
</div>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">
{% if bill_item.entity_unit %}{{ bill_item.entity_unit }}{% endif %}
</span>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.unit_cost | currency_format }}</span>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.quantity }}</span>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.total_amount | currency_format }}</span>
</td>
<td class="align-items-start white-space-nowrap pe-2">
{% if bill_item.po_model_id %}
{% if perms.django_ledger.view_purchaseordermodel %}
<a class="btn btn-sm btn-phoenix-primary"
href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}">
{% trans 'View PO' %}
</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td class="text-end">
<strong>{% trans 'Total' %}</strong>
</td>
<td class="text-end">
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
</td>
<td></td>
</tr>
</tfoot>
</table>
{% if bill.accrue %}
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">
{% trans 'Prepaid Account' %}:
{% if bill.prepaid_account %}
<a href="{% url 'account_detail' request.dealer.slug bill.prepaid_account.coa_model.pk bill.prepaid_account.uuid %}"
class="text-decoration-none ms-1">{{ bill.prepaid_account.code }}</a>
{% else %}
{{ bill.prepaid_account.code }}
{% endif %}
</h6>
<h4 class="text-success mb-0" id="djl-bill-detail-amount-prepaid">
{% currency_symbol %}{{ bill.get_amount_prepaid | currency_format }}
</h4>
</div>
</div>
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">
{% trans 'Accounts Payable' %}:
{% if bill.unearned_account %}
<a href="{% url 'account_detail' request.dealer.slug bill.unearned_account.coa_model.pk bill.unearned_account.uuid %}"
class="text-decoration-none ms-1">{{ bill.unearned_account.code }}</a>
{% else %}
{{ bill.unearned_account.code }}
{% endif %}
</h6>
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-unearned">
{% currency_symbol %}{{ bill.get_amount_unearned | currency_format }}
</h4>
</div>
<div class="col-12 col-md-3">
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'Accrued' %} {{ bill.get_progress | percentage }}</h6>
<h4 class="mb-0">{% currency_symbol %}{{ bill.get_amount_earned | currency_format }}</h4>
</div>
{% else %}
<div class="col-12 col-md-3 offset-md-6">
<h6 class="text-uppercase text-xs text-muted mb-2">{% trans 'You Still Owe' %}</h6>
<h4 class="text-danger mb-0" id="djl-bill-detail-amount-owed">
{% currency_symbol %}{{ bill.get_amount_open | currency_format }}
</h4>
</div>
{% endif %}
</div>
</div>
<div class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-exchange-alt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Transactions' %}</h5>
</div>
</div>
<div class="card-body px-0 pt-0 pb-2 table-responsive">{% transactions_table bill %}</div>
{% endif %}
</div>
</div>
<div>
{% include "bill/includes/mark_as.html" %}
{% endblock %}
<div class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-receipt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Items' %}</h5>
</div>
</div>
<div class="card-body px-0 pt-0 pb-2">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Item' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Entity Unit' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Unit Cost' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Quantity' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Total' %}</th>
<th class="sort white-space-nowrap align-middle " scope="col">{% trans 'PO' %}</th>
</tr>
</thead>
<tbody class="list fs-9" id="project-list-table-body">
{% for bill_item in itemtxs_qs %}
<tr>
<td class="align-middle white-space-nowrap">
<div class="d-flex px-2 py-1">
<div class="d-flex flex-column justify-content-center">
<h6 class="mb-0 text-sm">{{ bill_item.item_model }}</h6>
</div>
</div>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">
{% if bill_item.entity_unit %}{{ bill_item.entity_unit }}{% endif %}
</span>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.unit_cost | currency_format }}</span>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.quantity }}</span>
</td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.total_amount | currency_format }}</span>
</td>
<td class="align-items-start white-space-nowrap pe-2">
{% if bill_item.po_model_id %}
{% if perms.django_ledger.view_purchaseordermodel %}
<a class="btn btn-sm btn-phoenix-primary"
href="{% url 'purchase_order_detail' request.dealer.slug request.dealer.entity.slug bill_item.po_model_id %}">
{% trans 'View PO' %}
</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="3"></td>
<td class="text-end">
<strong>{% trans 'Total' %}</strong>
</td>
<td class="text-end">
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card mb-3 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-exchange-alt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Transactions' %}</h5>
</div>
</div>
<div class="card-body px-0 pt-0 pb-2 table-responsive">{% transactions_table bill %}</div>
</div>
</div>
<div>
{% include "bill/includes/mark_as.html" %}
{% endblock %}

View File

@ -14,7 +14,8 @@
<div class="card mb-2">
<div class="card-body">
{% include 'bill/includes/card_bill.html' with dealer_slug=request.dealer.slug bill=bill_model style='bill-detail' entity_slug=view.kwargs.entity_slug %}
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}" method="post">
<form action="{% url 'bill-update' dealer_slug=request.dealer.slug entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
method="post">
{% csrf_token %}
<div class="mb-3">{{ form|crispy }}</div>
<button type="submit" class="btn btn-phoenix-primary mb-2 me-2">

View File

@ -4,7 +4,6 @@
{% if not create_bill %}
{% if style == 'dashboard' %}
<!-- Dashboard Style Card -->
<div class="">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3 text-primary">
@ -51,15 +50,16 @@
</div>
<!-- Modal Action -->
{% modal_action bill 'get' entity_slug %}
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-phoenix-primary me-md-2">{% trans 'View' %}</a>
{% if perms.django_ledger.change_billmodel %}
<a hx-boost="true" href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
<a hx-boost="true"
href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-phoenix-warning me-md-2">{% trans 'Update' %}</a>
{% if bill.can_pay %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')" class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
class="btn btn-sm btn-phoenix-info">{% trans 'Mark as Paid' %}</button>
{% endif %}
{% if bill.can_cancel %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
@ -202,10 +202,11 @@
<div class="d-flex flex-wrap gap-2 mt-2">
<!-- Update Button -->
{% if perms.django_ledger.change_billmodel %}
{% if "update" not in request.path %}
<a hx-boost="true" href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
<button class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
{% if "update" not in request.path %}
<a hx-boost="true"
href="{% url 'bill-update' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill.uuid %}">
<button class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</button>
</a>
{% endif %}
@ -219,7 +220,6 @@
{% endif %}
<!-- Mark as Review -->
{% if bill.can_review %}
<button class="btn btn-phoenix-warning"
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
@ -227,41 +227,40 @@
{% endif %}
<!-- Mark as Approved -->
{% endif %}
{% if bill.can_approve and request.is_accountant %}
<button class="btn btn-phoenix-warning" disabled>
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
</button>
{% else %}
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
<button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
</button>
{% endif %}
{% endif %}
<!-- Mark as Paid -->
{% if bill.can_pay %}
{% if bill.can_approve and request.is_accountant %}
<button class="btn btn-phoenix-warning" disabled>
<i class="fas fa-hourglass-start me-2"></i><span class="text-warning">{% trans 'Waiting for Manager Approval' %}</span>
</button>
{% else %}
{% if bill.can_approve and perms.django_ledger.can_approve_billmodel %}
<button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
</button>
{% endif %}
<!-- Void Button -->
{% if bill.can_void %}
<button class="btn btn-phoenix-danger"
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
</button>
{% endif %}
<!-- Cancel Button -->
{% if bill.can_cancel %}
<button class="btn btn-phoenix-danger"
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
</button>
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
{% endif %}
{% endif %}
<!-- Mark as Paid -->
{% if bill.can_pay %}
<button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
</button>
{% endif %}
<!-- Void Button -->
{% if bill.can_void %}
<button class="btn btn-phoenix-danger"
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
</button>
{% endif %}
<!-- Cancel Button -->
{% if bill.can_cancel %}
<button class="btn btn-phoenix-danger"
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
</button>
{% modal_action_v2 bill bill.get_mark_as_canceled_url bill.get_mark_as_canceled_message bill.get_mark_as_canceled_html_id %}
{% endif %}
{% endif %}
</div>
</div>

View File

@ -2,137 +2,142 @@
{% load static %}
{% load django_ledger %}
{% load widget_tweaks %}
{% if bill.get_itemtxs_data.1.total_amount__sum > 0 %}
<form id="bill-update-form" action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% else %}
<form id="bill-update-form" hx-trigger="load delay:300ms" hx-swap="outerHTML" hx-target="#bill-update-form" hx-select="#bill-update-form" hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% endif %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="text-primary mb-0 d-flex align-items-center">
<i class="fas fa-receipt me-2"></i>
{% trans 'Bill Items' %}
</h2>
<hr class="my-3">
<form id="bill-update-form"
action="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% else %}
<form id="bill-update-form"
hx-trigger="load delay:300ms"
hx-swap="outerHTML"
hx-target="#bill-update-form"
hx-select="#bill-update-form"
hx-post="{% url 'bill-update-items' dealer_slug=dealer_slug entity_slug=entity_slug bill_pk=bill_pk %}"
method="post">
{% endif %}
<div class="container-fluid py-4">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<h2 class="text-primary mb-0 d-flex align-items-center">
<i class="fas fa-receipt me-2"></i>
{% trans 'Bill Items' %}
</h2>
<hr class="my-3">
</div>
</div>
</div>
<!-- Form Content -->
<div class="row">
<div class="col-12">
{% csrf_token %}
{{ item_formset.non_form_errors }}
{{ item_formset.management_form }}
<!-- Card Container -->
<div class="card shadow-sm">
<div class="card-body p-0">
<!-- Responsive Table -->
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="text-uppercase text-xxs font-weight-bolder">{% trans 'Item' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Qty' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Amount' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Quantity' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit Cost' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-end">{% trans 'Total' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Delete' %}</th>
</tr>
</thead>
<tbody>
{% for f in item_formset %}
<tr class="align-middle">
<!-- Item Column -->
<td>
<div class="d-flex flex-column ms-2">
{% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %}
{{ f.item_model|add_class:"form-control" }}
{% if f.errors %}<span class="text-danger text-xs">{{ f.errors }}</span>{% endif %}
</div>
</td>
<!-- PO Quantity -->
<td class="text-center">
<span class="text-muted text-xs">
{% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %}
</span>
</td>
<!-- PO Amount -->
<td class="text-center">
{% if f.instance.po_total_amount %}
<div class="d-flex flex-column">
<span class="text-xs font-weight-bold">{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</span>
<a class="btn btn-sm btn-phoenix-info mt-1"
href="{% url 'purchase_order_detail' dealer_slug entity_slug f.instance.po_model_id %}">
{% trans 'View PO' %}
</a>
</div>
{% endif %}
</td>
<!-- Quantity -->
<td class="text-center">
<div class="input-group input-group-sm w-100">{{ f.quantity|add_class:"form-control" }}</div>
</td>
<!-- Unit Cost -->
<td class="text-center">
<div class="input-group input-group-sm w-100">{{ f.unit_cost|add_class:"form-control" }}</div>
</td>
<!-- Entity Unit -->
<td class="text-center">{{ f.entity_unit|add_class:"form-control" }}</td>
<!-- Total Amount -->
<td class="text-end">
<span class="text-xs font-weight-bold">
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
</span>
</td>
<!-- Delete Checkbox -->
<td class="text-center">
{% if item_formset.can_delete %}<div class="form-check d-flex justify-content-center">{{ f.DELETE }}</div>{% endif %}
</td>
<!-- Form Content -->
<div class="row">
<div class="col-12">
{% csrf_token %}
{{ item_formset.non_form_errors }}
{{ item_formset.management_form }}
<!-- Card Container -->
<div class="card shadow-sm">
<div class="card-body p-0">
<!-- Responsive Table -->
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="text-uppercase text-xxs font-weight-bolder">{% trans 'Item' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Qty' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'PO Amount' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Quantity' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit Cost' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Unit' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-end">{% trans 'Total' %}</th>
<th class="text-uppercase text-xxs font-weight-bolder text-center">{% trans 'Delete' %}</th>
</tr>
{% endfor %}
</tbody>
<!-- Footer Total -->
<tfoot class="total-row">
<tr>
<td colspan="5"></td>
<td class="text-end">
<strong>{% trans 'Total' %}</strong>
</td>
<td class="text-end">
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
</td>
<td></td>
</tr>
</tfoot>
</table>
</thead>
<tbody>
{% for f in item_formset %}
<tr class="align-middle">
<!-- Item Column -->
<td>
<div class="d-flex flex-column ms-2">
{% for hidden_field in f.hidden_fields %}{{ hidden_field }}{% endfor %}
{{ f.item_model|add_class:"form-control" }}
{% if f.errors %}<span class="text-danger text-xs">{{ f.errors }}</span>{% endif %}
</div>
</td>
<!-- PO Quantity -->
<td class="text-center">
<span class="text-muted text-xs">
{% if f.instance.po_quantity %}{{ f.instance.po_quantity }}{% endif %}
</span>
</td>
<!-- PO Amount -->
<td class="text-center">
{% if f.instance.po_total_amount %}
<div class="d-flex flex-column">
<span class="text-xs font-weight-bold">{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}</span>
<a class="btn btn-sm btn-phoenix-info mt-1"
href="{% url 'purchase_order_detail' dealer_slug entity_slug f.instance.po_model_id %}">
{% trans 'View PO' %}
</a>
</div>
{% endif %}
</td>
<!-- Quantity -->
<td class="text-center">
<div class="input-group input-group-sm w-100">{{ f.quantity|add_class:"form-control" }}</div>
</td>
<!-- Unit Cost -->
<td class="text-center">
<div class="input-group input-group-sm w-100">{{ f.unit_cost|add_class:"form-control" }}</div>
</td>
<!-- Entity Unit -->
<td class="text-center">{{ f.entity_unit|add_class:"form-control" }}</td>
<!-- Total Amount -->
<td class="text-end">
<span class="text-xs font-weight-bold">
<span>{% currency_symbol %}</span>{{ f.instance.total_amount | currency_format }}
</span>
</td>
<!-- Delete Checkbox -->
<td class="text-center">
{% if item_formset.can_delete %}<div class="form-check d-flex justify-content-center">{{ f.DELETE }}</div>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<!-- Footer Total -->
<tfoot class="total-row">
<tr>
<td colspan="5"></td>
<td class="text-end">
<strong>{% trans 'Total' %}</strong>
</td>
<td class="text-end">
<strong>{% currency_symbol %}{{ total_amount__sum | currency_format }}</strong>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-start gap-2">
{% if not item_formset.has_po %}
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
class="btn btn-phoenix-primary">
<i class="fas fa-plus me-1"></i>
{% trans 'New Item' %}
</a>
{% endif %}
<button type="submit" class="btn btn-phoenix-primary">
<i class="fas fa-save me-1"></i>
{% trans 'Save Changes' %}
</button>
<!-- Action Buttons -->
<div class="row mt-4">
<div class="col-12">
<div class="d-flex justify-content-start gap-2">
{% if not item_formset.has_po %}
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
class="btn btn-phoenix-primary">
<i class="fas fa-plus me-1"></i>
{% trans 'New Item' %}
</a>
{% endif %}
<button type="submit" class="btn btn-phoenix-primary">
<i class="fas fa-save me-1"></i>
{% trans 'Save Changes' %}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</form>

View File

@ -2,58 +2,52 @@
{% load i18n static %}
{% load django_ledger %}
{% load widget_tweaks %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% trans 'Create Chart of Accounts' %}
<i class="fa-solid fa-chart-pie ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post" id="{{ form.get_form_id }}" class="needs-validation" novalidate>
{% csrf_token %}
{# Bootstrap form rendering #}
<div class="mb-3">
{{ form.name.label_tag }}
{{ form.name|add_class:"form-control" }}
{% if form.name.help_text %}
<small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label_tag }}
{{ form.description|add_class:"form-control" }}
{% if form.description.help_text %}
<small class="form-text text-muted">{{ form.description.help_text }}</small>
{% endif %}
{% for error in form.description.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">
<i class="fa-solid fa-plus me-1"></i>
{% trans 'Create' %}
</button>
<a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans 'Cancel' %}
</a>
</div>
</form>
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% trans 'Create Chart of Accounts' %}
<i class="fa-solid fa-chart-pie ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post"
id="{{ form.get_form_id }}"
class="needs-validation"
novalidate>
{% csrf_token %}
{# Bootstrap form rendering #}
<div class="mb-3">
{{ form.name.label_tag }}
{{ form.name|add_class:"form-control" }}
{% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label_tag }}
{{ form.description|add_class:"form-control" }}
{% if form.description.help_text %}
<small class="form-text text-muted">{{ form.description.help_text }}</small>
{% endif %}
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button type="submit" class="btn btn-phoenix-primary btn-lg me-md-2">
<i class="fa-solid fa-plus me-1"></i>
{% trans 'Create' %}
</button>
<a href="{% url 'coa-list' request.dealer.slug request.entity.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans 'Cancel' %}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -2,7 +2,6 @@
{% load i18n %}
{% load static %}
{% load icon from django_ledger %}
{% block content %}
<div class="card mb-4">
<div class="card-body">
@ -10,17 +9,19 @@
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="display-4 mb-0">{% trans "Chart of Accounts" %}</h1>
<a href="{% url 'coa-create' request.dealer.slug request.entity.slug %}" class="btn btn-phoenix-primary">
<a href="{% url 'coa-create' request.dealer.slug request.entity.slug %}"
class="btn btn-phoenix-primary">
<i class="fa-solid fa-plus"></i> {% trans "Add New" %}
</a>
</div>
{% if not inactive %}
<a class="btn btn-phoenix-warning mb-4" href="{% url 'coa-list-inactive' request.dealer.slug request.entity.slug %}">
<a class="btn btn-phoenix-warning mb-4"
href="{% url 'coa-list-inactive' request.dealer.slug request.entity.slug %}">
{% trans 'Show Inactive' %}
</a>
{% else %}
<a class="btn btn-phoenix-warning mb-4" href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
<a class="btn btn-phoenix-warning mb-4"
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
{% trans 'Show Active' %}
</a>
{% endif %}
@ -28,11 +29,9 @@
</div>
<div class="row row-cols-1 row-cols-md-2 g-4">
{% for coa_model in coa_list %}
<div class="col">
{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}
</div>
<div class="col">{% include 'chart_of_accounts/includes/coa_card.html' with coa_model=coa_model %}</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -2,23 +2,20 @@
{% load i18n %}
{% load static %}
{% load widget_tweaks %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-6 col-md-8">
<div class="card shadow-sm">
<div class="card-body">
<form action="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}" id="{{ form.form_id }}" method="post">
<form action="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}"
id="{{ form.form_id }}"
method="post">
{% csrf_token %}
<div class="mb-3">
{{ form.name.label_tag }}
{{ form.name|add_class:"form-control" }}
{% if form.name.help_text %}
<small class="form-text text-muted">{{ form.name.help_text }}</small>
{% endif %}
{% for error in form.name.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% if form.name.help_text %}<small class="form-text text-muted">{{ form.name.help_text }}</small>{% endif %}
{% for error in form.name.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label_tag }}
@ -26,19 +23,13 @@
{% if form.description.help_text %}
<small class="form-text text-muted">{{ form.description.help_text }}</small>
{% endif %}
{% for error in form.description.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
{% for error in form.description.errors %}<div class="invalid-feedback d-block">{{ error }}</div>{% endfor %}
</div>
<div class="d-flex justify-content-center gap-2 mt-4">
<button class="btn btn-phoenix-primary" type="submit">
{% trans 'Update'%}
</button>
<button class="btn btn-phoenix-primary" type="submit">{% trans 'Update' %}</button>
<a class="btn btn-phoenix-secondary"
href="{% url 'coa-list' request.dealer.slug request.entity.slug %}">
{% trans 'Back'%}
{% trans 'Back' %}
</a>
</div>
</form>
@ -46,4 +37,4 @@
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -1,7 +1,6 @@
{% load django_ledger %}
{% load i18n %}
{% now "Y" as current_year %}
<div class="card shadow-sm border-0 mb-4">
<div class="card-header {% if coa_model.is_default %}bg-gray-100{% elif not coa_model.is_active %}bg-danger text-white{% endif %} py-3 d-flex align-items-center">
<div class="me-3">
@ -19,17 +18,16 @@
{% endif %}
</div>
<div class="ms-auto d-flex flex-column align-items-end">
{% if coa_model.is_active %}
{% if coa_model.is_active %}
<span class="badge bg-success"><i class="fas fa-check-circle"></i> {% trans 'Active' %}</span>
{% else %}
{% else %}
<span class="badge bg-danger"><i class="fas fa-times-circle"></i> {% trans 'Inactive' %}</span>
{% endif %}
{% if coa_model.is_default %}
<span class="badge bg-primary-subtle text-primary mt-1">{% trans 'Entity Default' %}</span>
{% endif %}
{% endif %}
{% if coa_model.is_default %}
<span class="badge bg-primary-subtle text-primary mt-1">{% trans 'Entity Default' %}</span>
{% endif %}
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-sm-6">
@ -46,27 +44,22 @@
<span class="text-muted ms-2">{{ coa_model.slug }}</span>
</div>
</div>
<div class="col-sm-6">
<div class="mb-2">
<span class="fw-bold"><i class="fas fa-list-alt me-1"></i> {% trans 'Total Accounts' %}:</span>
<span class="ms-2">{{ coa_model.accountmodel_total__count }}</span>
</div>
<div class="mb-2">
<span class="fw-bold text-info"><i class="fas fa-check-circle me-1"></i> {% trans 'Active Accounts' %}:</span>
<span class="ms-2">{{ coa_model.accountmodel_active__count }}</span>
</div>
<div class="mb-2">
<span class="fw-bold text-danger"><i class="fas fa-lock me-1"></i> {% trans 'Locked Accounts' %}:</span>
<span class="ms-2">{{ coa_model.accountmodel_locked__count }}</span>
</div>
</div>
</div>
<hr class="my-3">
<div class="row g-2">
<div class="col-sm-6">
<small class="text-muted d-block">
@ -84,36 +77,37 @@
</div>
</div>
</div>
<div class="card-footer bg-transparent border-top-0 pt-0 pt-sm-3">
<div class="d-flex flex-wrap gap-2">
<a href="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-update' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-edit me-1"></i> {% trans 'Update' %}
</a>
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'account_list' request.dealer.slug coa_model.pk %}"
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-book me-1"></i> {% trans 'Accounts' %}
</a>
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}" class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'account_create' request.dealer.slug coa_model.pk %}"
class="btn btn-sm btn-phoenix-info fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-plus-circle me-1"></i> {% trans 'Add Account' %}
</a>
{% if coa_model.can_mark_as_default %}
<a href="{% url 'coa-action-mark-as-default' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-danger fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-action-mark-as-default' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-danger fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-star me-1"></i> {% trans 'Mark as Default' %}
</a>
{% endif %}
{% if coa_model.can_deactivate %}
<a href="{% url 'coa-action-mark-as-inactive' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-action-mark-as-inactive' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-warning fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-toggle-off me-1"></i> {% trans 'Mark as Inactive' %}
</a>
{% elif coa_model.can_activate %}
<a href="{% url 'coa-action-mark-as-active' request.dealer.slug request.entity.slug coa_model.slug %}" class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<a href="{% url 'coa-action-mark-as-active' request.dealer.slug request.entity.slug coa_model.slug %}"
class="btn btn-sm btn-phoenix-success fw-bold flex-grow-1 flex-sm-grow-0">
<i class="fas fa-toggle-on me-1"></i> {% trans 'Mark as Active' %}
</a>
{% endif %}
</div>
</div>
</div>
</div>

View File

@ -1,16 +1,22 @@
{% load i18n crispy_forms_tags %}
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal fade"
id="emailModal"
tabindex="-1"
aria-labelledby="emailModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div id="emailModalBody" class="modal-body">
<h1>hi</h1>
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="emailModalLabel">{% trans 'Send Email' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6"
data-bs-dismiss="modal"
aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div id="emailModalBody" class="modal-body">
<h1>hi</h1>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -15,13 +15,11 @@
</button>
</div>
<div class="modal-body">
<form id="noteForm" 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="{
resetSubmitButton(document.querySelector('.add_note_form button[type=submit]'));
$('#noteModal').modal('hide');
}"
hx-on::after-request="{ resetSubmitButton(document.querySelector('.add_note_form button[type=submit]')); $('#noteModal').modal('hide'); }"
hx-swap="outerHTML"
method="post"
class="add_note_form">
@ -34,7 +32,7 @@
</div>
</div>
<script>
function updateNote(e) {
function updateNote(e) {
let url = e.getAttribute('data-url');
let note = e.getAttribute('data-note');

View File

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

View File

@ -1,10 +1,10 @@
{% load static i18n crispy_forms_tags %}
<!-- task Modal -->
<style>
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
</style>
<div class="modal fade"
id="taskModal"
@ -22,13 +22,14 @@
</button>
</div>
<div class="modal-body">
<form id="taskForm" action="{% url 'add_task' request.dealer.slug content_type slug %}"
method="post"
class="add_task_form"
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-target="#your-content-container"
hx-swap="innerHTML"
hx-boost="true">
<form id="taskForm"
action="{% url 'add_task' request.dealer.slug content_type slug %}"
method="post"
class="add_task_form"
hx-post="{% url 'add_task' request.dealer.slug content_type slug %}"
hx-target="#your-content-container"
hx-swap="innerHTML"
hx-boost="true">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>

View File

@ -3,31 +3,31 @@
{% load crispy_forms_tags %}
{% block customCSS %}
<style>
.main-tab li:last-child {
margin-left: auto;
}
.kanban-header {
position: relative;
background-color:rgb(237, 241, 245);
font-weight: 600;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: #333;
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.main-tab li:last-child {
margin-left: auto;
}
.kanban-header {
position: relative;
background-color:rgb(237, 241, 245);
font-weight: 600;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: #333;
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.kanban-header::after {
content: "";
position: absolute;
right: -20px;
top: 0;
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6;
}
.kanban-header::after {
content: "";
position: absolute;
right: -20px;
top: 0;
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6;
}
</style>
{% endblock customCSS %}
@ -39,7 +39,6 @@
<div class="col-12 col-md-auto">
<h3 class="mb-0">{{ _("Lead Details") }}</h3>
</div>
</div>
</div>
</div>
@ -47,7 +46,6 @@
<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">
@ -80,11 +78,11 @@
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Car Requested") }}</h5>
{% if lead.id_car_make.logo %}
<img src="{{ lead.id_car_make.logo.url }}"
alt="Car Make Logo"
class="img-fluid rounded mb-2"
style="width: 60px;
height: 60px">
<img src="{{ lead.id_car_make.logo.url }}"
alt="Car Make Logo"
class="img-fluid rounded mb-2"
style="width: 60px;
height: 60px">
{% endif %}
<p class="mb-0">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</p>
</div>
@ -93,16 +91,17 @@
</div>
<div class="card mb-2">
<div class="card-body">
<div id="assignedTo" class="row align-items-center g-3 text-center text-xxl-start">
<div id="assignedTo"
class="row align-items-center g-3 text-center text-xxl-start">
<div class="col-6 col-sm-auto d-flex flex-column align-items-center text-center">
<h5 class="fw-bolder mb-2 text-body-highlight">{{ _("Assigned To") }}</h5>
<div class="d-flex align-items-center">
<div class="avatar avatar-tiny me-2">
{% if lead.staff.logo %}
<img class="avatar-img rounded-circle"
src="{{ lead.staff.thumbnail.url }}"
onerror="this.src='/static/img/brand/brand-logo.png'"
alt="Logo">
src="{{ lead.staff.thumbnail.url }}"
onerror="this.src='/static/img/brand/brand-logo.png'"
alt="Logo">
{% endif %}
</div>
<small>
@ -293,11 +292,9 @@
<div class="modal-content">
<form class="modal-content"
action="{% url 'lead_transfer' request.dealer.slug lead.slug %}"
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{
resetSubmitButton(document.querySelector('#exampleModal button[type=submit]'));
$('#exampleModal').modal('hide');}"
hx-select-oob="#assignedTo:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{ resetSubmitButton(document.querySelector('#exampleModal button[type=submit]')); $('#exampleModal').modal('hide');}"
method="post">
{% csrf_token %}
<div class="modal-header">
@ -488,7 +485,7 @@
data-url="{% url 'update_note' request.dealer.slug note.pk %}"
data-bs-toggle="modal"
data-bs-target="#noteModal"
data-note-title="{{ _('Update') }}">
data-note-title="{{ _("Update") }}">
<i class='fas fa-pen-square text-primary ms-2'></i>
{{ _("Update") }}
</a>
@ -528,8 +525,7 @@
hx-get="{% url 'send_lead_email' request.dealer.slug lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML"
>
hx-swap="innerHTML">
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button>
{% endif %}
@ -807,7 +803,6 @@
href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("View in Calendar") }}
</a>
</div>
</div>
</div>
</div>
@ -821,62 +816,62 @@
{% include "components/note_modal.html" with content_type="lead" slug=lead.slug %}
<!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="lead" slug=lead.slug %}
{% endblock content %}
{% block customJS %}
<script>
function reset_form() {
document.querySelector('#id_note').value = ""
let form = document.querySelector('.add_note_form')
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
}
{% endblock content %}
{% block customJS %}
<script>
function reset_form() {
document.querySelector('#id_note').value = ""
let form = document.querySelector('.add_note_form')
form.action = "{% url 'add_note' request.dealer.slug 'lead' lead.slug %}"
}
{% if messages %}
{% for message in messages %}
Toast.fire({
icon: "{{ message.tags }}",
titleText: "{{ message|safe }}"
});
{% endfor %}
{% endif %}
{% if messages %}
{% for message in messages %}
Toast.fire({
icon: "{{ message.tags }}",
titleText: "{{ message|safe }}"
});
{% endfor %}
{% endif %}
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false');
document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction;
document.getElementById('nextActionDate').value = nextActionDate;
modal.show();
}
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('actionTrackingForm').setAttribute('hx-boost', 'false');
document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction;
document.getElementById('nextActionDate').value = nextActionDate;
modal.show();
}
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
// Close modal after successful form submission
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
if (modal) {
modal.hide();
}
}
});
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
if (modal) {
modal.hide();
}
}
});
// Cleanup modal backdrop if needed
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) {
backdrop.remove();
});
}
});
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) {
backdrop.remove();
});
}
});
</script>
{% endblock customJS %}
</script>
{% endblock customJS %}

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% load i18n static crispy_forms_filters %}
{% block title %}
{% if object %}
{% trans 'Update Lead' %}
@ -8,7 +7,6 @@
{% trans 'Add New Lead' %}
{% endif %}
{% endblock %}
{% block customcss %}
<style>
.htmx-indicator{
@ -30,41 +28,40 @@
}
</style>
{% endblock customcss %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5">
<div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %}
{% trans "Update Lead" %}
<i class="fa-solid fa-edit ms-2"></i>
{% else %}
{% trans "Create New Lead" %}
<i class="fa-solid fa-bullhorn ms-2"></i>
{% endif %}
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{% trans "Save" %}
</button>
<a href="{% url 'lead_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5">
<div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %}
{% trans "Update Lead" %}
<i class="fa-solid fa-edit ms-2"></i>
{% else %}
{% trans "Create New Lead" %}
<i class="fa-solid fa-bullhorn ms-2"></i>
{% endif %}
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form class="form" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{% trans "Save" %}
</button>
<a href="{% url 'lead_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -1,270 +1,263 @@
{% extends 'base.html' %}
{% load i18n static humanize %}
{% block title %}
{{ _("Leads") |capfirst }}
{% endblock title %}
{% block content %}
{% if page_obj.object_list or request.GET.q%}
<div class="row g-3 mt-4 mb-4">
<h2 class="mb-2">
{{ _("Leads") |capfirst }}
<li class="fas fa-bullhorn text-primary ms-2"></li>
</h2>
<!-- Action Tracking Modal -->
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
{% if perms.inventory.add_lead %}
<div>
<a href="{% url 'lead_create' request.dealer.slug %}"
class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a>
</div>
{% endif %}
{% if page_obj.object_list or request.GET.q %}
<div class="row g-3 mt-4 mb-4">
<h2 class="mb-2">
{{ _("Leads") |capfirst }}
<li class="fas fa-bullhorn text-primary ms-2"></li>
</h2>
<!-- Action Tracking Modal -->
{% comment %} {% include "crm/leads/partials/update_action.html" %} {% endcomment %}
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
{% if perms.inventory.add_lead %}
<div>
<a href="{% url 'lead_create' request.dealer.slug %}"
class="btn btn-sm btn-phoenix-primary"><span class="fas fa-plus me-2"></span>{{ _("Add Lead") }}</a>
</div>
{% endif %}
</div>
</div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
{% if page_obj.object_list or request.GET.q%}
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 20%">{{ _("Lead Name") |capfirst }}</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<i class="text-success-dark fas fa-car"></i>
<div class="row g-3">
<div class="col-12">
{% if page_obj.object_list or request.GET.q %}
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 20%">{{ _("Lead Name") |capfirst }}</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<i class="text-success-dark fas fa-car"></i>
</div>
<span>{{ _("Car") |capfirst }}</span>
</div>
<span>{{ _("Car") |capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<span class="text-success-dark" data-feather="mail"></span>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<span class="text-success-dark" data-feather="mail"></span>
</div>
<span>{{ _("email") |capfirst }}</span>
</div>
<span>{{ _("email") |capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span class="text-primary-dark" data-feather="phone"></span>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span class="text-primary-dark" data-feather="phone"></span>
</div>
<div class="" dir="ltr">{{ _("Phone Number") }}</div>
</div>
<div class="" dir="ltr">{{ _("Phone Number") }}</div>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 10%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2">
<span class="text-warning-dark" data-feather="zap"></span>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 10%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2">
<span class="text-warning-dark" data-feather="zap"></span>
</div>
<span>{{ _("Next Action") |capfirst }}</span>
</div>
<span>{{ _("Next Action") |capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span class="far fa-calendar-alt"></span>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span class="far fa-calendar-alt"></span>
</div>
<span>{{ _("Scheduled at") }}</span>
</div>
<span>{{ _("Scheduled at") }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 10%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-success-subtle rounded me-2">
<span class="text-success-dark" data-feather="user-check"></span>
</th>
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 10%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-success-subtle rounded me-2">
<span class="text-success-dark" data-feather="user-check"></span>
</div>
<span>{{ _("Assigned To") |capfirst }}</span>
</div>
<span>{{ _("Assigned To") |capfirst }}</span>
</div>
</th>
{% comment %} <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
</th>
{% comment %} <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
<span>{{ _("Opportunity")|capfirst }}</span>
</div>
</th> {% endcomment %}
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">{{ _("Action") }}</th>
<th class="text-end white-space-nowrap align-middle" scope="col"></th>
</tr>
{% for lead in leads %}
<!-- Delete Modal -->
<div class="modal fade"
id="deleteModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="mb-0 me-2 text-danger">
{{ _("Delete") }}<i class="fas fa-exclamation-circle text-danger ms-2"></i>
</h4>
<button class="btn p-0 text-body-quaternary fs-6"
data-bs-dismiss="modal"
aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div class="modal-body p-4">
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
</div>
<div class="modal-footer flex justify-content-center border-top-0">
<a type="button"
class="btn btn-sm btn-phoenix-danger w-100"
href="{% url 'lead_delete' request.dealer.slug lead.slug %}">
{% trans "Yes" %}
</a>
</th> {% endcomment %}
<th class="align-middle white-space-nowrap text-uppercase"
scope="col"
style="width: 15%">{{ _("Action") }}</th>
<th class="text-end white-space-nowrap align-middle" scope="col"></th>
</tr>
{% for lead in leads %}
<!-- Delete Modal -->
<div class="modal fade"
id="deleteModal"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="mb-0 me-2 text-danger">
{{ _("Delete") }}<i class="fas fa-exclamation-circle text-danger ms-2"></i>
</h4>
<button class="btn p-0 text-body-quaternary fs-6"
data-bs-dismiss="modal"
aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div class="modal-body p-4">
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
</div>
<div class="modal-footer flex justify-content-center border-top-0">
<a type="button"
class="btn btn-sm btn-phoenix-danger w-100"
href="{% url 'lead_delete' request.dealer.slug lead.slug %}">
{% trans "Yes" %}
</a>
</div>
</div>
</div>
</div>
</div>
<tbody>
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-1">
<div class="d-flex align-items-center">
<div>
<a class="fs-8 fw-bold"
href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
{% if lead.status == "new" %}
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{ _("New") }}</span><span class="fa fa-bell ms-1"></span></span>
{% elif lead.status == "pending" %}
<span class="badge badge-phoenix badge-phoenix-warning"><span class="badge-label">{{ _("Pending") }}</span><span class="fa fa-clock-o ms-1"></span></span>
{% elif lead.status == "in_progress" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("In Progress") }}</span><span class="fa fa-wrench ms-1"></span></span>
{% elif lead.status == "qualified" %}
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{ _("Qualified") }}</span><span class="fa fa-check ms-1"></span></span>
{% elif lead.status == "contacted" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("Contacted") }}</span><span class="fa fa-times ms-1"></span></span>
{% elif lead.status == "canceled" %}
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{ _("Canceled") }}</span><span class="fa fa-times ms-1"></span></span>
{% endif %}
</div>
</div>
</div>
</td>
<td class="align-middle white-space-nowrap fw-semibold">
<a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a>
</td>
<td class="align-middle white-space-nowrap fw-semibold">
<a class="text-body-highlight" href="">{{ lead.email }}</a>
</td>
<td class="align-middle white-space-nowrap fw-semibold">
<a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a>
</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{{ lead.next_action|upper }}
</td>
<td class="align-middle white-space-nowrap fw-semibold">{{ lead.next_action_date|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
<div class="d-flex align-items-center">
<div class="avatar avatar-tiny me-2">
{% if lead.staff.logo %}
<img class="avatar-img rounded-circle"
src="{{ lead.staff.thumbnail.url }}"
onerror="this.src='/static/img/brand/brand-logo.png'"
alt="Logo">
{% endif %}
</div>
<small>
{% if lead.staff == request.staff %}
{{ _("Me") }}
{% elif LANGUAGE_CODE == "en" %}
{{ lead.staff.fullname|capfirst }}
{% else %}
{{ lead.staff.arabic_name }}
{% endif %}
</small>
</div>
</td>
<td class="align-middle white-space-nowrap text-end">
{% if user == lead.staff.user or request.is_dealer %}
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10"
type="button"
data-bs-toggle="dropdown"
data-boundary="window"
aria-haspopup="true"
aria-expanded="false"
data-bs-reference="parent">
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
{% if perms.inventory.change_lead %}
<a href="{% url 'lead_update' request.dealer.slug lead.slug %}"
class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
{% endif %}
{% if perms.inventory.change_lead %}
{% endif %}
{% if not lead.opportunity %}
{% if perms.inventory.add_opportunity %}
<a href="{% url 'lead_opportunity_create' request.dealer.slug lead.slug %}"
class="dropdown-item text-success-dark">{% trans "Convert to Opportunity" %}</a>
<tbody>
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="name align-middle white-space-nowrap ps-1">
<div class="d-flex align-items-center">
<div>
<a class="fs-8 fw-bold"
href="{% url 'lead_detail' request.dealer.slug lead.slug %}">{{ lead.full_name|capfirst }}</a>
<div class="d-flex align-items-center">
<p class="mb-0 text-body-highlight fw-semibold fs-9 me-2"></p>
{% if lead.status == "new" %}
<span class="badge badge-phoenix badge-phoenix-primary"><span class="badge-label">{{ _("New") }}</span><span class="fa fa-bell ms-1"></span></span>
{% elif lead.status == "pending" %}
<span class="badge badge-phoenix badge-phoenix-warning"><span class="badge-label">{{ _("Pending") }}</span><span class="fa fa-clock-o ms-1"></span></span>
{% elif lead.status == "in_progress" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("In Progress") }}</span><span class="fa fa-wrench ms-1"></span></span>
{% elif lead.status == "qualified" %}
<span class="badge badge-phoenix badge-phoenix-success"><span class="badge-label">{{ _("Qualified") }}</span><span class="fa fa-check ms-1"></span></span>
{% elif lead.status == "contacted" %}
<span class="badge badge-phoenix badge-phoenix-info"><span class="badge-label">{{ _("Contacted") }}</span><span class="fa fa-times ms-1"></span></span>
{% elif lead.status == "canceled" %}
<span class="badge badge-phoenix badge-phoenix-danger"><span class="badge-label">{{ _("Canceled") }}</span><span class="fa fa-times ms-1"></span></span>
{% endif %}
{% endif %}
{% if perms.inventory.delete_lead %}
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</div>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center">{% trans "No Leads found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</td>
<td class="align-middle white-space-nowrap fw-semibold">
<a class="text-body-highlight" href="">{{ lead.id_car_make.get_local_name }} - {{ lead.id_car_model.get_local_name }} {{ lead.year }}</a>
</td>
<td class="align-middle white-space-nowrap fw-semibold">
<a class="text-body-highlight" href="">{{ lead.email }}</a>
</td>
<td class="align-middle white-space-nowrap fw-semibold">
<a class="text-body-highlight" href="tel:{{ lead.phone_number }}">{{ lead.phone_number }}</a>
</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{{ lead.next_action|upper }}
</td>
<td class="align-middle white-space-nowrap fw-semibold">{{ lead.next_action_date|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
<div class="d-flex align-items-center">
<div class="avatar avatar-tiny me-2">
{% if lead.staff.logo %}
<img class="avatar-img rounded-circle"
src="{{ lead.staff.thumbnail.url }}"
onerror="this.src='/static/img/brand/brand-logo.png'"
alt="Logo">
{% endif %}
</div>
<small>
{% if lead.staff == request.staff %}
{{ _("Me") }}
{% elif LANGUAGE_CODE == "en" %}
{{ lead.staff.fullname|capfirst }}
{% else %}
{{ lead.staff.arabic_name }}
{% endif %}
</small>
</div>
</td>
<td class="align-middle white-space-nowrap text-end">
{% if user == lead.staff.user or request.is_dealer %}
<div class="btn-reveal-trigger position-static">
<button class="btn btn-sm dropdown-toggle dropdown-caret-none transition-none btn-reveal fs-10"
type="button"
data-bs-toggle="dropdown"
data-boundary="window"
aria-haspopup="true"
aria-expanded="false"
data-bs-reference="parent">
<span class="fas fa-ellipsis-h fs-10"></span>
</button>
<div class="dropdown-menu dropdown-menu-end py-2">
{% if perms.inventory.change_lead %}
<a href="{% url 'lead_update' request.dealer.slug lead.slug %}"
class="dropdown-item text-success-dark">{% trans "Edit" %}</a>
{% endif %}
{% if perms.inventory.change_lead %}{% endif %}
{% if not lead.opportunity %}
{% if perms.inventory.add_opportunity %}
<a href="{% url 'lead_opportunity_create' request.dealer.slug lead.slug %}"
class="dropdown-item text-success-dark">{% trans "Convert to Opportunity" %}</a>
{% endif %}
{% endif %}
{% if perms.inventory.delete_lead %}
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
{% trans "Delete" %}
</button>
{% endif %}
</div>
</div>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center">{% trans "No Leads found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %}
{% endblock %}
{% else %}
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %}
{% endblock %}

View File

@ -5,179 +5,176 @@
{% endblock title %}
{% block customCSS %}
<style>
.kanban-column {
border-radius: 8px;
padding: 1rem;
min-height: 500px;
}
.kanban-header {
position: relative;
font-weight: 600;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: #333;
--pointed-edge: {% if LANGUAGE_CODE == 'en' %} right {% else %} left {% endif %};
clip-path: {% if LANGUAGE_CODE == 'en' %}
polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%)
{% else %}
polygon(15px 0, 100% 0, 100% 100%, 15px 100%, 0 50%)
{% endif %};
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.kanban-column {
border-radius: 8px;
padding: 1rem;
min-height: 500px;
}
.kanban-header {
position: relative;
font-weight: 600;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: #333;
--pointed-edge: {% if LANGUAGE_CODE == 'en' %} right {% else %} left {% endif %};
clip-path: {% if LANGUAGE_CODE == 'en' %}
polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%)
{% else %}
polygon(15px 0, 100% 0, 100% 100%, 15px 100%, 0 50%)
{% endif %};
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.kanban-header::after {
content: "";
position: absolute;
right: -20px;
top: 0;
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6;
}
.lead-card {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1rem;
}
.lead-card small {
color: #6c757d;
}
.bg-success-soft {
background-color: rgba(17, 240, 66, 0.1) !important;
opacity: .8;
}
.bg-danger-soft {
background-color: rgba(230, 50, 68, 0.1) !important;
opacity: .8;
}
.bg-info-soft {
background-color: rgba(41, 197, 245, 0.1) !important;
opacity: .8;
}
.bg-negotiation-soft {
background-color: rgba(113, 206, 206, 0.1) !important;
opacity: .8;
}
.kanban-header::after {
content: "";
position: absolute;
right: -20px;
top: 0;
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6;
}
.lead-card {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1rem;
}
.lead-card small {
color: #6c757d;
}
.bg-success-soft {
background-color: rgba(17, 240, 66, 0.1) !important;
opacity: .8;
}
.bg-danger-soft {
background-color: rgba(230, 50, 68, 0.1) !important;
opacity: .8;
}
.bg-info-soft {
background-color: rgba(41, 197, 245, 0.1) !important;
opacity: .8;
}
.bg-negotiation-soft {
background-color: rgba(113, 206, 206, 0.1) !important;
opacity: .8;
}
</style>
{% endblock customCSS %}
{% block content %}
{% if leads %}
<div class="container-fluid my-4">
<div class="row justify-content-center">
<div class="col">
<div class="d-flex justify-content-between mb-3">
<h3>
{{ _("Lead Tracking") }}
<li class="fas fa-bullhorn text-primary ms-2"></li>
</h3>
</div>
<div class="row g-3">
<!-- New Lead -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span>
{% if leads %}
<div class="container-fluid my-4">
<div class="row justify-content-center">
<div class="col">
<div class="d-flex justify-content-between mb-3">
<h3>
{{ _("Lead Tracking") }}
<li class="fas fa-bullhorn text-primary ms-2"></li>
</h3>
</div>
<div class="row g-3">
<!-- New Lead -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("New Leads") }} ({{ new|length }})</span>
</div>
{% for lead in new %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
{% for lead in new %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Follow Ups -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
<!-- Follow Ups -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Follow Ups") }} ({{ follow_up|length }})</span>
</div>
{% for lead in follow_up %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
{% for lead in follow_up %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Negotiation -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
<!-- Negotiation -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header opacity-75">
<span class="text-body">{{ _("Negotiation Ups") }} ({{ follow_up|length }})</span>
</div>
{% for lead in negotiation %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
{% for lead in negotiation %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Won -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-success-light opacity-75">
<span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
<!-- Won -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-success-light opacity-75">
<span class="text-body">{{ _("Won") }} ({{ won|length }}) ({{ follow_up|length }})</span>
</div>
{% for lead in won %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Lose -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{ lose|length }})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
{% for lead in won %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Lose -->
<div class="col-md">
<div class="kanban-column bg-body">
<div class="kanban-header bg-danger-light opacity-75">{{ _("Lost") }} ({{ lose|length }})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}">
<div class="lead-card">
<strong>{{ lead.full_name|capfirst }}</strong>
<br>
<small>{{ lead.email }}</small>
<br>
<small>{{ lead.phone_number }}</small>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% else %}
{% url 'lead_create' request.dealer.slug as create_lead_url %}
{% include "empty-illustration-page.html" with value="lead" url=create_lead_url %}
{% endif %}
{% endblock %}

View File

@ -2,22 +2,22 @@
{% load static i18n humanize %}
{% block customCSS %}
<style>
.card {
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: none;
}
.card-header {
background-color: #f0f2f5;
font-weight: 600;
}
.table thead {
background-color: #f9fafb;
}
.empty-state {
text-align: center;
padding: 50px;
color: #aaa;
}
.card {
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: none;
}
.card-header {
background-color: #f0f2f5;
font-weight: 600;
}
.table thead {
background-color: #f9fafb;
}
.empty-state {
text-align: center;
padding: 50px;
color: #aaa;
}
</style>
{% endblock customCSS %}
{% block content %}
@ -27,7 +27,7 @@
<h5 class="mb-0">مرحبًا</h5>
<div>
<button class="btn btn-phoenix-secondary dropdown-toggle"
data-bs-toggle="dropdown">الصفحة الرئيسية لـ </button>
data-bs-toggle="dropdown">الصفحة الرئيسية لـ</button>
</div>
</div>
<!-- Main Row -->

View File

@ -13,14 +13,11 @@
aria-label="Close"></button>
</div>
<form id="actionTrackingForm"
action="{% url 'update_lead_actions' request.dealer.slug %}"
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{
resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]'));
$('#actionTrackingModal').modal('hide');
}"
method="post">
action="{% url 'update_lead_actions' request.dealer.slug %}"
hx-select-oob="#currentStage:outerHTML,#leadStatus:outerHTML,#toast-container:outerHTML"
hx-swap="none"
hx-on::after-request="{ resetSubmitButton(document.querySelector('#actionTrackingForm button[type=submit]')); $('#actionTrackingModal').modal('hide'); }"
method="post">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="leadId" name="lead_id">

View File

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

View File

@ -40,7 +40,8 @@
href="{% url 'update_opportunity' request.dealer.slug opportunity.slug %}">Update Opportunity</a>
</li>
<li>
<a class="dropdown-item" type="button"
<a class="dropdown-item"
type="button"
data-bs-toggle="modal"
data-bs-target="#updateStageModal">Update Stage</a>
</li>
@ -683,16 +684,16 @@
<form action="{% url 'add_note_to_opportunity' request.dealer.slug opportunity.slug %}" method="post">
{% csrf_token %}
<textarea class="form-control mb-3" id="notes" rows="4" name="notes" required> </textarea>
<button type="submit" class="btn btn-phoenix-primary mb-3">Add Note</button>
</form>
{% endif %}
<div class="row gy-4 note-list">
<div class="col-12 col-xl-auto flex-1">
{% for note in opportunity.get_notes %}
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
<p class="mb-1 text-body-highlight">{{ note.note }}</p>
<div class="d-flex">
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created|naturaltime|capfirst}}</span></div>
<button type="submit" class="btn btn-phoenix-primary mb-3">Add Note</button>
</form>
{% endif %}
<div class="row gy-4 note-list">
<div class="col-12 col-xl-auto flex-1">
{% for note in opportunity.get_notes %}
<div class="border-2 border-dashed mb-4 pb-4 border-bottom border-translucent">
<p class="mb-1 text-body-highlight">{{ note.note }}</p>
<div class="d-flex">
<div class="fs-9 text-body-tertiary text-opacity-85"><span class="fa-solid fa-clock me-2"></span><span class="fw-semibold me-1">{{note.created|naturaltime|capfirst}}</span></div>
<p class="fs-9 mb-0 text-body-tertiary text-opacity-85">by<a class="ms-1 fw-semibold" href="#!">{{note.created_by}}</a></p>
</div>
</div>
@ -828,15 +829,14 @@
</button>
</a> {% endcomment %}
{% if opportunity.lead %}
<button class="btn btn-phoenix-primary btn-sm"
type="button"
data-bs-toggle="modal"
data-bs-target="#emailModal"
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML"
>
<button class="btn btn-phoenix-primary btn-sm"
type="button"
data-bs-toggle="modal"
data-bs-target="#emailModal"
hx-get="{% url 'send_lead_email' request.dealer.slug opportunity.lead.slug %}"
hx-target="#emailModalBody"
hx-select=".email-form"
hx-swap="innerHTML">
<span class="fas fa-plus me-1"></span>{{ _("Send Email") }}
</button>
{% endif %}
@ -858,7 +858,6 @@
</li>
</ul>
</div>
<div class="tab-content" id="profileTabContent">
<div class="tab-pane fade show active"
id="tab-mail"
@ -871,7 +870,6 @@
<table class="table fs-9 mb-0">
<thead>
<tr>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase"
scope="col"
data-sort="subject"
@ -896,14 +894,12 @@
<tbody class="list" id="all-email-table-body">
{% for email in opportunity.lead.get_emails %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td class="subject order align-middle white-space-nowrap py-2 ps-0">
<a class="fw-semibold text-primary" href="#!">{{ email.subject }}</a>
<div class="fs-10 d-block">{{ email.to_email }}</div>
</td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{ email.from_email }}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{ email.created }}</td>
<td class="status align-middle fw-semibold text-end py-2">
<span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span>
</td>
@ -1023,10 +1019,10 @@
<div class="tab-pane fade"
id="tab-activity"
hx-get="{% url 'opportunity_detail' request.dealer.slug opportunity.slug %}"
hx-trigger="htmx:afterRequest from:"
hx-select="#tab-activity"
hx-target="this"
hx-swap="outerHTML"
hx-trigger="htmx:afterRequest from:"
hx-select="#tab-activity"
hx-target="this"
hx-swap="outerHTML"
role="tabpanel"
aria-labelledby="activity-tab">
<h2 class="mb-4">Activity</h2>
@ -1097,37 +1093,36 @@
</div>
</div>
</div>
<div class="modal fade" id="updateStageModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post"
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
hx-swap="none"
hx-on::after-request="location.reload()">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
<button class="btn btn-close p-1"
type="button"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
{{ stage_form|crispy }}
</div>
<div class="modal-footer">
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
<button class="btn btn-phoenix-secondary"
type="button"
data-bs-dismiss="modal">{{ _("Cancel") }}</button>
</div>
</form>
</div>
<div class="modal fade"
id="updateStageModal"
tabindex="-1"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="post"
action="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
hx-post="{% url 'update_opportunity_stage' request.dealer.slug opportunity.slug %}"
hx-swap="none"
hx-on::after-request="location.reload()">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title" id="updateStageModalLabel">{{ _("Update Opportunity Stage") }}</h5>
<button class="btn btn-close p-1"
type="button"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">{{ stage_form|crispy }}</div>
<div class="modal-footer">
<button class="btn btn-phoenix-primary" type="submit">{{ _("Save") }}</button>
<button class="btn btn-phoenix-secondary"
type="button"
data-bs-dismiss="modal">{{ _("Cancel") }}</button>
</div>
</form>
</div>
</div>
</div>
{% include 'modal/delete_modal.html' %}
<!-- email Modal -->
{% include "components/email_modal.html" %}
@ -1138,28 +1133,26 @@
<!-- schedule Modal -->
{% include "components/schedule_modal.html" with content_type="opportunity" slug=opportunity.slug %}
{% endblock %}
{% block customJS %}
<script>
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
if (modal) {
modal.hide();
}
}
});
<script>
document.body.addEventListener('htmx:afterSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var modal = bootstrap.Modal.getInstance(document.getElementById('exampleModal'));
if (modal) {
modal.hide();
}
}
});
// Cleanup modal backdrop if needed
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) {
backdrop.remove();
});
}
});
document.body.addEventListener('htmx:beforeSwap', function(evt) {
if (evt.detail.target.id === 'main_content') {
var backdrops = document.querySelectorAll('.modal-backdrop');
backdrops.forEach(function(backdrop) {
backdrop.remove();
});
}
});
</script>
{% endblock customJS %}
</script>
{% endblock customJS %}

View File

@ -9,201 +9,162 @@
{% endif %}
{% endblock %}
{% block content %}
<div class="container-fluid mt-4 mb-3">
<div class="row g-3 mb-4 align-items-center">
<div class="col">
<h2 class="mb-0">
{% if form.instance.pk %}
{% trans "Edit Opportunity" %}
{% else %}
{% trans "Create New Opportunity" %}
{% endif %}
</h2>
</div>
<div class="col-auto">
<a href="{% url 'opportunity_list' request.dealer.slug %}" class="btn btn-phoenix-secondary">
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
</a>
</div>
</div>
<div class="row g-3">
<div class="col-lg-8">
<div class="card">
<div class="card-body p-4 p-sm-5">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<!-- Lead Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.lead.id_for_label }}">
{{ form.lead.label }}
<span class="text-danger">*</span>
</label>
{{ form.lead|add_class:"form-control" }}
{% if form.lead.errors %}
<div class="invalid-feedback d-block">
{{ form.lead.errors }}
</div>
{% endif %}
</div>
<!-- Car Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.car.id_for_label }}">
{{ form.car.label }}
<span class="text-danger">*</span>
</label>
{{ form.car|add_class:"form-control" }}
{% if form.car.errors %}
<div class="invalid-feedback d-block">
{{ form.car.errors }}
</div>
{% endif %}
</div>
<!-- Stage Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.stage.id_for_label }}">
{{ form.stage.label }}
<span class="text-danger">*</span>
</label>
{{ form.stage|add_class:"form-control" }}
{% if form.stage.errors %}
<div class="invalid-feedback d-block">
{{ form.stage.errors }}
</div>
{% endif %}
</div>
<!-- Amount Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.amount.id_for_label }}">
{{ form.amount.label }}
<span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
{{ form.amount|add_class:"form-control" }}
</div>
{% if form.amount.errors %}
<div class="invalid-feedback d-block">
{{ form.amount.errors }}
</div>
{% endif %}
</div>
<!-- Probability Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.probability.id_for_label }}">
{{ form.probability.label }}
<span class="text-danger">*</span>
</label>
<div class="d-flex align-items-center gap-3">
<input type="range"
name="{{ form.probability.name }}"
id="{{ form.probability.id_for_label }}"
min="0" max="100" step="1"
value="{{ form.probability.value|default:'50' }}"
class="form-control form-range"
oninput="updateProbabilityValue(this.value)">
<span id="probability-value" class="badge badge-phoenix fs-6 badge-phoenix-primary">
{{ form.probability.value|default:'50' }}%
</span>
</div>
{% if form.probability.errors %}
<div class="invalid-feedback d-block">
{{ form.probability.errors }}
</div>
{% endif %}
</div>
<!-- Expected Revenue -->
<div class="mb-4">
<label class="form-label" for="{{ form.expected_revenue.id_for_label }}">
{{ form.expected_revenue.label }}
</label>
<div class="input-group">
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
{{ form.expected_revenue|add_class:"form-control" }}
</div>
{% if form.expected_revenue.errors %}
<div class="invalid-feedback d-block">
{{ form.expected_revenue.errors }}
</div>
{% endif %}
</div>
<!-- Closing Date -->
<div class="mb-5">
<label class="form-label" for="{{ form.closing_date.id_for_label }}">
{{ form.closing_date.label }}
</label>
<div class="input-group">
{{ form.expected_close_date|add_class:"form-control" }}
<span class="input-group-text"><span class="far fa-calendar"></span></span>
</div>
{% if form.expected_close_date.errors %}
<div class="invalid-feedback d-block">
{{ form.expected_close_date.errors }}
</div>
{% endif %}
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-end gap-3">
<button type="reset" class="btn btn-phoenix-danger px-4">
<span class="fas fa-redo me-1"></span>{% trans "Reset" %}
</button>
<button type="submit" class="btn btn-phoenix-primary px-6">
{% if form.instance.pk %}
<span class="fas fa-save me-1"></span>{% trans "Update" %}
{% else %}
<span class="fas fa-plus me-1"></span>{% trans "Create" %}
{% endif %}
</button>
</div>
</form>
</div>
<div class="container-fluid mt-4 mb-3">
<div class="row g-3 mb-4 align-items-center">
<div class="col">
<h2 class="mb-0">
{% if form.instance.pk %}
{% trans "Edit Opportunity" %}
{% else %}
{% trans "Create New Opportunity" %}
{% endif %}
</h2>
</div>
<div class="col-auto">
<a href="{% url 'opportunity_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary">
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body p-4">
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
<ul class="nav flex-column gap-2 nav-guide">
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-primary fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
<div class="row g-3">
<div class="col-lg-8">
<div class="card">
<div class="card-body p-4 p-sm-5">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if form.non_field_errors %}<div class="alert alert-danger">{{ form.non_field_errors }}</div>{% endif %}
<!-- Lead Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.lead.id_for_label }}">
{{ form.lead.label }}
<span class="text-danger">*</span>
</label>
{{ form.lead|add_class:"form-control" }}
{% if form.lead.errors %}<div class="invalid-feedback d-block">{{ form.lead.errors }}</div>{% endif %}
</div>
<!-- Car Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.car.id_for_label }}">
{{ form.car.label }}
<span class="text-danger">*</span>
</label>
{{ form.car|add_class:"form-control" }}
{% if form.car.errors %}<div class="invalid-feedback d-block">{{ form.car.errors }}</div>{% endif %}
</div>
<!-- Stage Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.stage.id_for_label }}">
{{ form.stage.label }}
<span class="text-danger">*</span>
</label>
{{ form.stage|add_class:"form-control" }}
{% if form.stage.errors %}<div class="invalid-feedback d-block">{{ form.stage.errors }}</div>{% endif %}
</div>
<!-- Amount Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.amount.id_for_label }}">
{{ form.amount.label }}
<span class="text-danger">*</span>
</label>
<div class="input-group">
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
{{ form.amount|add_class:"form-control" }}
</div>
{% if form.amount.errors %}<div class="invalid-feedback d-block">{{ form.amount.errors }}</div>{% endif %}
</div>
<!-- Probability Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.probability.id_for_label }}">
{{ form.probability.label }}
<span class="text-danger">*</span>
</label>
<div class="d-flex align-items-center gap-3">
<input type="range"
name="{{ form.probability.name }}"
id="{{ form.probability.id_for_label }}"
min="0"
max="100"
step="1"
value="{{ form.probability.value|default:'50' }}"
class="form-control form-range"
oninput="updateProbabilityValue(this.value)">
<span id="probability-value"
class="badge badge-phoenix fs-6 badge-phoenix-primary">
{{ form.probability.value|default:'50' }}%
</span>
</div>
{% if form.probability.errors %}<div class="invalid-feedback d-block">{{ form.probability.errors }}</div>{% endif %}
</div>
<!-- Expected Revenue -->
<div class="mb-4">
<label class="form-label" for="{{ form.expected_revenue.id_for_label }}">{{ form.expected_revenue.label }}</label>
<div class="input-group">
<span class="input-group-text"><span class="icon-saudi_riyal"></span></span>
{{ form.expected_revenue|add_class:"form-control" }}
</div>
{% if form.expected_revenue.errors %}
<div class="invalid-feedback d-block">{{ form.expected_revenue.errors }}</div>
{% endif %}
</div>
<!-- Closing Date -->
<div class="mb-5">
<label class="form-label" for="{{ form.closing_date.id_for_label }}">{{ form.closing_date.label }}</label>
<div class="input-group">
{{ form.expected_close_date|add_class:"form-control" }}
<span class="input-group-text"><span class="far fa-calendar"></span></span>
</div>
{% if form.expected_close_date.errors %}
<div class="invalid-feedback d-block">{{ form.expected_close_date.errors }}</div>
{% endif %}
</div>
<!-- Form Actions -->
<div class="d-flex justify-content-end gap-3">
<button type="reset" class="btn btn-phoenix-danger px-4">
<span class="fas fa-redo me-1"></span>{% trans "Reset" %}
</button>
<button type="submit" class="btn btn-phoenix-primary px-6">
{% if form.instance.pk %}
<span class="fas fa-save me-1"></span>{% trans "Update" %}
{% else %}
<span class="fas fa-plus me-1"></span>{% trans "Create" %}
{% endif %}
</button>
</div>
</form>
</div>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-warning fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body p-4">
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
<ul class="nav flex-column gap-2 nav-guide">
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-primary fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-warning fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-success fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
</div>
</li>
</ul>
</div>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-success fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
<script>
function updateProbabilityValue(value) {
const amount = document.getElementById('id_amount');
const expectedRevenue = document.getElementById('id_expected_revenue');

View File

@ -75,7 +75,8 @@
height: 12px;
width: 12px"></span>{{ opportunity.get_stage_display }}
</p>
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p># TODO : check later
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.total }}</p>
# TODO : check later
</div>
<div class="deals-company-agent d-flex flex-between-center">
<div class="d-flex align-items-center">

View File

@ -5,136 +5,137 @@
{{ _("Opportunities") }}
{% endblock title %}
{% block content %}
{% if opportunities or request.GET.q%}
<div class="row g-3 mt-4">
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
<h2 class="mb-3">
{{ _("Opportunities") }}
<li class="fas fas fa-rocket text-primary ms-2"></li>
</h2>
{% if opportunities or request.GET.q %}
<div class="row g-3 mt-4">
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
<h2 class="mb-3">
{{ _("Opportunities") }}
<li class="fas fas fa-rocket text-primary ms-2"></li>
</h2>
</div>
</div>
<div class="col-auto">
{% if perms.inventory.add_opportunity %}
<div class="d-flex justify-content-between">
<a class="btn btn-phoenix-primary btn-sm"
href="{% url 'opportunity_create' request.dealer.slug %}">
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
</a>
</div>
{% endif %}
</div>
</div>
<div class="col-auto">
{% if perms.inventory.add_opportunity %}
<div class="d-flex justify-content-between">
<a class="btn btn-phoenix-primary btn-sm"
href="{% url 'opportunity_create' request.dealer.slug %}">
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
</a>
</div>
{% endif %}
</div>
</div>
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<!-- Filter Controls -->
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
id="filter-container">
<!-- Search Input - Wider and properly aligned -->
<div class="search-box position-relative flex-grow-1 me-2" style="min-width: 200px">
<form class="position-relative show" id="search-form"
hx-get=""
hx-boost="false"
hx-trigger="keyup changed delay:500ms, search">
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<!-- Filter Controls -->
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100"
id="filter-container">
<!-- Search Input - Wider and properly aligned -->
<div class="search-box position-relative flex-grow-1 me-2"
style="min-width: 200px">
<form class="position-relative show"
id="search-form"
hx-get=""
hx-boost="false"
hx-trigger="keyup changed delay:500ms, search">
<input name="q"
id="search-input"
class="form-control form-control-sm search-input search"
type="search"
aria-label="Search"
placeholder="{{ _("Search") }}..."
value="{{ request.GET.q}}" />
id="search-input"
class="form-control form-control-sm search-input search"
type="search"
aria-label="Search"
placeholder="{{ _("Search") }}..."
value="{{ request.GET.q }}" />
<span class="fa fa-magnifying-glass search-box-icon"></span>
{% if request.GET.q %}
<button type="button"
class="btn-close position-absolute end-0 top-50 translate-middle cursor-pointer shadow-none"
id="clear-search"
aria-label="Clear Search"></button>
{% endif %}
</form>
</div>
<!-- Filter Dropdowns - Aligned in a row -->
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
style="max-width: 400px">
<!-- Stage Filter -->
<!-- Stage Filter -->
<div class="flex-grow-1">
<select class="form-select"
name="stage"
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
hx-trigger="change"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select">
<option value="">{% trans "All Stages" %}</option>
{% for value, label in stage_choices %}
<option value="{{ value }}"
{% if request.GET.stage == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<!-- Sort Filter -->
<div class="flex-grow-1">
<select class="form-select"
name="sort"
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
hx-trigger="change"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select">
<option value="newest"
{% if request.GET.sort == 'newest' %}selected{% endif %}>{% trans "Newest First" %}</option>
<option value="highest"
{% if request.GET.sort == 'highest' %}selected{% endif %}>{% trans "Highest Value" %}</option>
<option value="closing"
{% if request.GET.sort == 'closing' %}selected{% endif %}>{% trans "Earliest Close Date" %}</option>
</select>
<!-- Filter Dropdowns - Aligned in a row -->
<div class="d-flex flex-column flex-sm-row gap-3 w-100"
style="max-width: 400px">
<!-- Stage Filter -->
<!-- Stage Filter -->
<div class="flex-grow-1">
<select class="form-select"
name="stage"
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
hx-trigger="change"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select">
<option value="">{% trans "All Stages" %}</option>
{% for value, label in stage_choices %}
<option value="{{ value }}"
{% if request.GET.stage == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
<!-- Sort Filter -->
<div class="flex-grow-1">
<select class="form-select"
name="sort"
hx-get="{% url 'opportunity_list' request.dealer.slug %}"
hx-trigger="change"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select">
<option value="newest"
{% if request.GET.sort == 'newest' %}selected{% endif %}>
{% trans "Newest First" %}
</option>
<option value="highest"
{% if request.GET.sort == 'highest' %}selected{% endif %}>
{% trans "Highest Value" %}
</option>
<option value="closing"
{% if request.GET.sort == 'closing' %}selected{% endif %}>
{% trans "Earliest Close Date" %}
</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1 mb-4">
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
{% else %}
{% url 'opportunity_create' request.dealer.slug as create_opportunity_url %}
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
{% endif %}
{% else %}
{% url 'opportunity_create' request.dealer.slug as create_opportunity_url %}
{% include "empty-illustration-page.html" with value="opportunity" url=create_opportunity_url %}
{% endif %}
{% block customJS %}
<script>
document.addEventListener("DOMContentLoaded", function() {
const searchInput = document.getElementById("search-input");
const clearButton = document.getElementById("clear-search");
const searchForm = document.getElementById("search-form");
<script>
document.addEventListener("DOMContentLoaded", function() {
const searchInput = document.getElementById("search-input");
const clearButton = document.getElementById("clear-search");
const searchForm = document.getElementById("search-form");
if (clearButton) {
clearButton.addEventListener("click", function() {
searchInput.value = "";
if (clearButton) {
clearButton.addEventListener("click", function() {
searchInput.value = "";
// This clears the search and triggers the htmx search
// by submitting the form with an empty query.
searchForm.submit();
searchForm.submit();
});
}
});
}
});
</script>
</script>
{% endblock %}
{% endblock %}

View File

@ -2,24 +2,19 @@
{% load custom_filters %}
{% block customCSS %}
<style>
.bg-success-soft {
background-color: rgba(25, 135, 84, 0.1) !important;
opacity: .8;
}
.bg-danger-soft {
background-color: rgba(220, 53, 69, 0.1) !important;
opacity: .8;
}
.bg-success-soft {
background-color: rgba(25, 135, 84, 0.1) !important;
opacity: .8;
}
.bg-danger-soft {
background-color: rgba(220, 53, 69, 0.1) !important;
opacity: .8;
}
</style>
{% endblock customCSS %}
{% for opportunity in opportunities %}
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
<div class="card h-100
{% if opportunity.get_stage_display == 'Closed Won' %}
bg-success-soft
{% elif opportunity.get_stage_display == 'Closed Lost' %}
bg-danger-soft
{% endif %}">
<div class="card h-100 {% if opportunity.get_stage_display == 'Closed Won' %} bg-success-soft {% elif opportunity.get_stage_display == 'Closed Lost' %} bg-danger-soft {% endif %}">
<div class="card-body">
<div class="avatar avatar-xl me-3 mb-3">
{% if opportunity.car.id_car_make.logo %}
@ -53,12 +48,7 @@
<span class="badge badge-phoenix fs-10 badge-phoenix-secondary">
{% endif %}
{{ opportunity.stage }}</span>
<span class="badge badge-phoenix fs-10
{% if opportunity.get_stage_display == 'Won' %}
badge-phoenix-success
{% elif opportunity.get_stage_display == 'Lost' %}
badge-phoenix-danger
{% endif %}">
<span class="badge badge-phoenix fs-10 {% if opportunity.get_stage_display == 'Won' %} badge-phoenix-success {% elif opportunity.get_stage_display == 'Lost' %} badge-phoenix-danger {% endif %}">
{{ opportunity.get_status_display }}
</span>
</div>

View File

@ -3,7 +3,6 @@
{% block title %}
{% trans "Car Bulk Upload"|capfirst %}
{% endblock %}
{% block customCSS %}
<style>
.color-card {
@ -74,13 +73,13 @@
</style>
{% endblock customCSS %}
{% block content %}
<div class="container mt-4">
<h2>
Upload Cars CSV <i class="fa-solid fa-file-csv text-primary"></i>
</h2>
<div class="d-flex justify-content-end">
<a href="{% static 'sample/cars_sample.csv' %}" class="btn btn-phoenix-primary mt-4">
<a href="{% static 'sample/cars_sample.csv' %}"
class="btn btn-phoenix-primary mt-4">
<i class="fa-solid fa-file-csv me-2"></i>Download Sample CSV
</a>
</div>
@ -182,7 +181,8 @@
<div class="form-text">{{ _("CSV should include columns: vin") }}</div>
</div>
<button type="submit" class="btn btn-phoenix-primary mb-2">Upload</button>
<a href="{% url 'car_list' request.dealer.slug %}" class="btn btn-phoenix-secondary mb-2">Cancel</a>
<a href="{% url 'car_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary mb-2">Cancel</a>
</form>
</div>
{% endblock %}

View File

@ -1,7 +1,6 @@
{% extends "base.html" %}
{% load i18n static %}
{% load crispy_forms_filters %}
{% block title %}
{% if object %}
{% trans 'Update Customer' %}
@ -9,52 +8,53 @@
{% trans 'Add New Customer' %}
{% endif %}
{% endblock %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5 ">
<div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %}
{% trans "Update Customer" %}
<i class="fa-solid fa-user-edit ms-2"></i>
{% else %}
{% trans "Add New Customer" %}
<i class="fa-solid fa-user-plus ms-2"></i>
{% endif %}
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post" class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
{% endfor %}
</ul>
<main class="d-flex align-items-center justify-content-center min-vh-100 py-5 ">
<div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %}
{% trans "Update Customer" %}
<i class="fa-solid fa-user-edit ms-2"></i>
{% else %}
{% trans "Add New Customer" %}
<i class="fa-solid fa-user-plus ms-2"></i>
{% endif %}
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post" class="form" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li>
<strong>{{ field|capfirst }}:</strong>
{% for error in errors %}{{ error }}{% endfor %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{% trans "Save" %}
</button>
<a href="{% url 'customer_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{% trans "Save" %}
</button>
<a href="{% url 'customer_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -6,176 +6,175 @@
{% endblock title %}
{% block vendors %}<a class="nav-link active">{{ _("Customers") |capfirst }}</a>{% endblock %}
{% block content %}
{% if customers or request.GET.q %}
<div class="row g-3 mt-4">
<h2 class="mb-2">
{{ _("Customers") |capfirst }}
<li class="fas fa-people-group text-primary ms-2"></li>
</h2>
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
{% if perms.inventory.add_customer %}
<div>
<a href="{% url 'customer_create' request.dealer.slug %}"
class="btn btn-sm btn-phoenix-primary me-4"><span class="fas fa-plus me-2"></span>{{ _("Add Customer") }}</a>
</div>
{% endif %}
{% if customers or request.GET.q %}
<div class="row g-3 mt-4">
<h2 class="mb-2">
{{ _("Customers") |capfirst }}
<li class="fas fa-people-group text-primary ms-2"></li>
</h2>
<div class="row g-3 justify-content-between mb-4">
<div class="col-auto">
<div class="d-md-flex justify-content-between">
{% if perms.inventory.add_customer %}
<div>
<a href="{% url 'customer_create' request.dealer.slug %}"
class="btn btn-sm btn-phoenix-primary me-4"><span class="fas fa-plus me-2"></span>{{ _("Add Customer") }}</a>
</div>
{% endif %}
</div>
</div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
<div class="col-auto">
<div class="d-flex">{% include 'partials/search_box.html' %}</div>
</div>
</div>
{% if page_obj.object_list or request.GET.q%}
<div class="table-responsive scrollbar transition">
<table class="table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th></th>
<th class="sort white-space-nowrap align-middle text-uppercase ps-0"
scope="col"
data-sort="name"
style="width:25%">{{ _("Name") |capfirst }}</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="email"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<span class="text-success-dark" data-feather="mail"></span>
</div>
<span>{{ _("email") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="phone"
style="width:15%;
min-width: 180px">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span class="text-primary-dark" data-feather="phone"></span>
</div>
<span>{{ _("Phone Number") }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="contact"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2">
<span class="text-info-dark" data-feather="user"></span>
</div>
<span>{{ _("National ID") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="company"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
<span class="text-warning-dark" data-feather="home"></span>
</div>
<span>{{ _("Address") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="company"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
<span class="text-warning-dark" data-feather="grid"></span>
</div>
<span>{{ _("Active") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase"
scope="col"
data-sort="date"
style="width:15%">
{{ _("Create date") }} <span class="text-warning-dark" data-feather="clock"></span>
</th>
<th class="sort text-end align-middle pe-0 ps-4" scope="col"></th>
</tr>
</thead>
<tbody class="list" id="lead-tables-body">
{% for customer in customers %}
<!-- Delete Modal -->
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td></td>
{% if perms.inventory.view_customer %}
<td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center">
<div>
<a class="fs-8 fw-bold"
href="{% url 'customer_detail' request.dealer.slug customer.slug %}">{{ customer.full_name }}</a>
<div class="d-flex align-items-center"></div>
</div>
{% if page_obj.object_list or request.GET.q %}
<div class="table-responsive scrollbar transition">
<table class="table align-items-center table-flush table-hover">
<thead>
<tr class="bg-body-highlight">
<th></th>
<th class="sort white-space-nowrap align-middle text-uppercase ps-0"
scope="col"
data-sort="name"
style="width:25%">{{ _("Name") |capfirst }}</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="email"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-success-subtle rounded me-2">
<span class="text-success-dark" data-feather="mail"></span>
</div>
</td>
{% endif %}
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">
<a class="text-body-highlight" href="">{{ customer.email }}</a>
</td>
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">
<a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone_number }}</a>
</td>
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.national_id }}
</td>
<td class="company align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.address }}
</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
{% if customer.active %}
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{ customer.active }}</span>
{% else %}
<span class="badge badge-phoenix badge-phoenix-danger"><i class="fas fa-times"></i> {{ customer.active }}</span>
{% endif %}
</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
{{ customer.created|date }}
</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
{% if perms.inventory.change_customer %}
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
class="btn btn-sm btn-phoenix-primary me-2"
data-url="{% url 'customer_update' request.dealer.slug customer.slug %}">
<i class="fas fa-pen"></i>
</a>
{% endif %}
{% if perms.inventory.delete_customer %}
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
data-message="{{ _("Are you sure you want to delete this customer") }}"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</td>
<span>{{ _("email") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="phone"
style="width:15%;
min-width: 180px">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-primary-subtle rounded me-2">
<span class="text-primary-dark" data-feather="phone"></span>
</div>
<span>{{ _("Phone Number") }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="contact"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-info-subtle rounded me-2">
<span class="text-info-dark" data-feather="user"></span>
</div>
<span>{{ _("National ID") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="company"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
<span class="text-warning-dark" data-feather="home"></span>
</div>
<span>{{ _("Address") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase border-end border-translucent"
scope="col"
data-sort="company"
style="width:15%">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center px-1 py-1 bg-warning-subtle rounded me-2">
<span class="text-warning-dark" data-feather="grid"></span>
</div>
<span>{{ _("Active") |capfirst }}</span>
</div>
</th>
<th class="sort align-middle ps-4 pe-5 text-uppercase"
scope="col"
data-sort="date"
style="width:15%">
{{ _("Create date") }} <span class="text-warning-dark" data-feather="clock"></span>
</th>
<th class="sort text-end align-middle pe-0 ps-4" scope="col"></th>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center">{% trans "No Customers found." %}</td>
</tr>
{% endfor %}
</tbody>
</thead>
<tbody class="list" id="lead-tables-body">
{% for customer in customers %}
<!-- Delete Modal -->
<tr class="hover-actions-trigger btn-reveal-trigger position-static">
<td></td>
{% if perms.inventory.view_customer %}
<td class="name align-middle white-space-nowrap ps-0">
<div class="d-flex align-items-center">
<div>
<a class="fs-8 fw-bold"
href="{% url 'customer_detail' request.dealer.slug customer.slug %}">{{ customer.full_name }}</a>
<div class="d-flex align-items-center"></div>
</div>
</div>
</td>
{% endif %}
<td class="email align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">
<a class="text-body-highlight" href="">{{ customer.email }}</a>
</td>
<td class="phone align-middle white-space-nowrap fw-semibold ps-4 border-end border-translucent">
<a class="text-body-highlight" href="tel:{{ customer.phone }}">{{ customer.phone_number }}</a>
</td>
<td class="contact align-middle white-space-nowrap ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.national_id }}
</td>
<td class="company align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 border-end border-translucent fw-semibold text-body-highlight">
{{ customer.address }}
</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
{% if customer.active %}
<span class="badge badge-phoenix badge-phoenix-success"><i class="fas fa-check"></i> {{ customer.active }}</span>
{% else %}
<span class="badge badge-phoenix badge-phoenix-danger"><i class="fas fa-times"></i> {{ customer.active }}</span>
{% endif %}
</td>
<td class="date align-middle white-space-nowrap text-body-tertiary text-opacity-85 ps-4 text-body-tertiary">
{{ customer.created|date }}
</td>
<td class="align-middle white-space-nowrap text-end pe-0 ps-4">
{% if perms.inventory.change_customer %}
<a href="{% url 'customer_update' request.dealer.slug customer.slug %}"
class="btn btn-sm btn-phoenix-primary me-2"
data-url="{% url 'customer_update' request.dealer.slug customer.slug %}">
<i class="fas fa-pen"></i>
</a>
{% endif %}
{% if perms.inventory.delete_customer %}
<button class="btn btn-phoenix-danger btn-sm delete-btn"
data-url="{% url 'customer_delete' request.dealer.slug customer.slug %}"
data-message="{{ _("Are you sure you want to delete this customer") }}"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center">{% trans "No Customers found." %}</td>
</tr>
{% endfor %}
</tbody>
{% endif %}
</table>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
</table>
{% if page_obj.paginator.num_pages > 1 %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">{% include 'partials/pagination.html' %}</div>
</div>
{% include 'modal/delete_modal.html' %}
{% else %}
{% url "customer_create" request.dealer.slug as create_customer_url %}
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
{% endif %}
{% include 'modal/delete_modal.html' %}
{% else %}
{% url "customer_create" request.dealer.slug as create_customer_url %}
{% include "empty-illustration-page.html" with value="customer" url=create_customer_url %}
{% endif %}
{% endblock %}

View File

@ -6,11 +6,12 @@
{% block content %}
{% include 'modal/delete_modal.html' %}
{% include 'components/note_modal.html' with content_type="customer" slug=customer.slug %}
<div class="mt-4">
<div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto">
<h3 class="mb-0">{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i></h3>
<h3 class="mb-0">
{% trans 'Customer details' %}<i class="fas fa-user ms-2 text-primary"></i>
</h3>
</div>
<div class="col-auto d-flex gap-2">
{% if perms.inventory.change_customer %}
@ -30,7 +31,6 @@
{% endif %}
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12 col-lg-4">
<div class="card h-100 shadow-sm">
@ -39,7 +39,9 @@
<div class="col-12 col-sm-auto mb-sm-2">
<div class="avatar avatar-5xl">
{% if customer.image %}
<img class="rounded-circle border border-2 border-primary" src="{{ customer.image.url }}" alt="{{ customer.full_name }}"/>
<img class="rounded-circle border border-2 border-primary"
src="{{ customer.image.url }}"
alt="{{ customer.full_name }}" />
{% else %}
<div class="avatar-text rounded-circle bg-secondary text-white border border-2 border-primary">
<span class="fs-4">{{ customer.full_name|first|default:"?" }}</span>
@ -65,7 +67,6 @@
</div>
</div>
</div>
<div class="col-12 col-lg-8">
<div class="card h-100 shadow-sm">
<div class="card-body p-4">
@ -79,18 +80,19 @@
</li>
<li class="mb-2">
<strong class="text-body-secondary d-block">{% trans 'Email' %}:</strong>
<a href="mailto:{{ customer.email|default:"" }}" class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
<a href="mailto:{{ customer.email|default:"" }}"
class="text-decoration-none">{{ customer.email|default:_("N/A") }}</a>
</li>
<li class="mb-0">
<strong class="text-body-secondary d-block">{% trans 'Phone Number' %}:</strong>
<a href="tel:{{ customer.phone_number|default:"" }}" class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
<a href="tel:{{ customer.phone_number|default:"" }}"
class="text-decoration-none">{{ customer.phone_number|default:_("N/A") }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card shadow-sm">
@ -116,16 +118,16 @@
</thead>
<tbody>
{% for note in notes %}
<tr class="align-middle">
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td>
<td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
</tr>
<tr class="align-middle">
<td class="text-body-secondary">{{ note.note|default_if_none:""|linebreaksbr }}</td>
<td class="text-body-secondary text-nowrap">{{ note.created|date:"d M Y" }}</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-body-secondary">
<i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
</td>
</tr>
<tr>
<td colspan="4" class="text-center text-body-secondary">
<i class="fas fa-info-circle me-2"></i>{% trans 'No notes found for this customer.' %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
@ -134,7 +136,6 @@
</div>
</div>
</div>
<div class="row g-4 mb-3">
<div class="col-12">
<div class="card shadow-sm">
@ -142,48 +143,82 @@
<h5 class="card-title mb-3">{% trans 'Sales History' %}</h5>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item me-6" role="presentation">
<button class="nav-link active" id="leads-tab" data-bs-toggle="tab" data-bs-target="#leads-tab-pane" type="button" role="tab" aria-controls="leads-tab-pane" aria-selected="true">{% trans 'Leads' %}</button>
<button class="nav-link active"
id="leads-tab"
data-bs-toggle="tab"
data-bs-target="#leads-tab-pane"
type="button"
role="tab"
aria-controls="leads-tab-pane"
aria-selected="true">{% trans 'Leads' %}</button>
</li>
<li class="nav-item me-6" role="presentation">
<button class="nav-link" id="opportunities-tab" data-bs-toggle="tab" data-bs-target="#opportunities-tab-pane" type="button" role="tab" aria-controls="opportunities-tab-pane" aria-selected="false">{% trans 'Opportunities' %}</button>
<button class="nav-link"
id="opportunities-tab"
data-bs-toggle="tab"
data-bs-target="#opportunities-tab-pane"
type="button"
role="tab"
aria-controls="opportunities-tab-pane"
aria-selected="false">{% trans 'Opportunities' %}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="estimates-tab" data-bs-toggle="tab" data-bs-target="#estimates-tab-pane" type="button" role="tab" aria-controls="estimates-tab-pane" aria-selected="false">{% trans 'Estimates' %}</button>
<button class="nav-link"
id="estimates-tab"
data-bs-toggle="tab"
data-bs-target="#estimates-tab-pane"
type="button"
role="tab"
aria-controls="estimates-tab-pane"
aria-selected="false">{% trans 'Estimates' %}</button>
</li>
</ul>
<div class="tab-content pt-3" id="myTabContent">
<div class="tab-pane fade show active" id="leads-tab-pane" role="tabpanel" aria-labelledby="leads-tab" tabindex="0">
<div class="tab-pane fade show active"
id="leads-tab-pane"
role="tabpanel"
aria-labelledby="leads-tab"
tabindex="0">
{% for lead in leads %}
<div class="d-flex align-items-center mb-2">
<i class="fas fa-handshake me-2 text-primary"></i>
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}" class="fw-bold">{{ lead }}</a>
<a href="{% url 'lead_detail' request.dealer.slug lead.slug %}"
class="fw-bold">{{ lead }}</a>
</div>
{% empty %}
<p class="text-body-secondary">{% trans 'No leads found for this customer.' %}</p>
{% endfor %}
</div>
<div class="tab-pane fade" id="opportunities-tab-pane" role="tabpanel" aria-labelledby="opportunities-tab" tabindex="0">
<div class="tab-pane fade"
id="opportunities-tab-pane"
role="tabpanel"
aria-labelledby="opportunities-tab"
tabindex="0">
{% for lead in leads %}
{% if lead.opportunity %}
<div class="d-flex align-items-center mb-2">
<i class="fas fa-chart-line me-2 text-success"></i>
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}" class="fw-bold">{{ lead.opportunity }}</a>
<a href="{% url 'opportunity_detail' request.dealer.slug lead.opportunity.slug %}"
class="fw-bold">{{ lead.opportunity }}</a>
</div>
{% endif %}
{% empty %}
<p class="text-body-secondary">{% trans 'No opportunities found for this customer.' %}</p>
{% endfor %}
</div>
<div class="tab-pane fade" id="estimates-tab-pane" role="tabpanel" aria-labelledby="estimates-tab" tabindex="0">
<div class="tab-pane fade"
id="estimates-tab-pane"
role="tabpanel"
aria-labelledby="estimates-tab"
tabindex="0">
{% for estimate in estimates %}
<div class="card mb-3 shadow-sm">
<div class="card-body p-3">
<div class="d-flex align-items-center justify-content-between mb-3">
<h6 class="mb-0">
<i class="fas fa-file-invoice me-2 text-info"></i>
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}" class="text-decoration-none">{{ estimate }}</a>
<a href="{% url 'estimate_detail' request.dealer.slug estimate.pk %}"
class="text-decoration-none">{{ estimate }}</a>
</h6>
<span class="badge bg-success">{{ estimate.created|date:"d M Y" }}</span>
</div>
@ -197,16 +232,22 @@
{% for invoice in estimate.invoicemodel_set.all %}
<li class="mb-2">
<i class="fas fa-receipt me-2 {% if invoice.is_paid %}text-success{% else %}text-warning{% endif %}"></i>
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}" class="text-decoration-none">{{ invoice }}</a>
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">{% if invoice.is_paid %}{% trans "Paid" %}{% else %}{% trans "Unpaid" %}{% endif %}</span>
<a href="{% url 'invoice_detail' request.dealer.slug request.entity.slug invoice.pk %}"
class="text-decoration-none">{{ invoice }}</a>
<span class="badge rounded-pill {% if invoice.is_paid %}bg-success{% else %}bg-warning{% endif %} ms-2">
{% if invoice.is_paid %}
{% trans "Paid" %}
{% else %}
{% trans "Unpaid" %}
{% endif %}
</span>
</li>
{% endfor %}
{% for item in estimate.itemtransactionmodel_set.all %}
<li>
<i class="fas fa-car me-2 text-primary"></i>
<a href="{% url 'car_detail' request.dealer.slug item.item_model.car.slug %}">{{ item.item_model.car.vin}}&nbsp;&vert;&nbsp;{{item.item_model.car.id_car_make.name}}&nbsp;&vert;&nbsp;{{item.item_model.car.id_car_model.name}}</a>
<a href="{% url 'car_detail' request.dealer.slug item.item_model.car.slug %}">{{ item.item_model.car.vin }}&nbsp;&vert;&nbsp;{{ item.item_model.car.id_car_make.name }}&nbsp;&vert;&nbsp;{{ item.item_model.car.id_car_model.name }}</a>
</li>
{% endfor %}
</ul>
</div>
@ -221,37 +262,32 @@
</div>
</div>
</div>
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
{% include "components/note_modal.html" with content_type="customer" slug=customer.slug %}
<!---->
<script>
document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal");
const modalTitle = document.getElementById("noteModalLabel");
document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal");
const modalTitle = document.getElementById("noteModalLabel");
const modalBody = noteModal.querySelector(".modal-body");
const modalBody = noteModal.querySelector(".modal-body");
noteModal.addEventListener("", function (event) {
const button = event.relatedTarget;
const url = button.getAttribute("data-url");
const title = button.getAttribute("data-note-title");
noteModal.addEventListener("", function (event) {
const button = event.relatedTarget;
const url = button.getAttribute("data-url");
const title = button.getAttribute("data-note-title");
fetch(url)
.then((response) => response.text())
.then((html) => {
modalBody.innerHTML = html;
modalTitle.innerHTML = title;
})
.catch((error) => {
modalBody.innerHTML = '<p class="text-danger">{% trans 'Error loading form. Please try again later' %}.</p>';
console.error("Error loading form:", error);
});
});
});
fetch(url)
.then((response) => response.text())
.then((html) => {
modalBody.innerHTML = html;
modalTitle.innerHTML = title;
})
.catch((error) => {
modalBody.innerHTML = '<p class="text-danger">{% trans 'Error loading form. Please try again later' %}.</p>';
console.error("Error loading form:", error);
});
});
});
</script>
{% endblock %}
{% endblock %}

View File

@ -1,124 +1,126 @@
{% extends 'base.html' %}
{% load i18n %}
{% load tenhal_tag %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">
{% trans "Aging Inventory" %}
<i class="fas fa-box-open text-danger ms-2"></i>
</h2>
<h4 class="text-muted mb-3 ">{% trans "Aging Inventory Total" %} :: <span class=" text-danger">{{total_aging_inventory_value|default:0.00}}<span class="icon-saudi_riyal"></span></span></h4>
<p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p>
</div>
<form method="GET" class="d-flex flex-wrap align-items-center mb-4 g-3">
<div class="col-sm-6 col-md-2 me-2">
<label for="make-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label>
<select class="form-select" name="make" id="make-filter">
<option value="">{% trans "All" %}</option>
{% for make in all_makes %}
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="model-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label>
<select class="form-select" name="model" id="model-filter">
<option value="">{% trans "All" %}</option>
{% for model in all_models %}
<option value="{{ model }}" {% if model == selected_model %}selected{% endif %}>{{ model }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="series-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label>
<select class="form-select" name="series" id="series-filter">
<option value="">{% trans "All" %}</option>
{% for series in all_series %}
<option value="{{ series }}" {% if series == selected_series %}selected{% endif %}>{{ series }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="year-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label>
<select class="form-select" name="year" id="year-filter">
<option value="">{% trans "All" %}</option>
{% for year in all_years %}
<option value="{{ year }}" {% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="stock-type-filter" class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label>
<select class="form-select" name="stock_type" id="stock-type-filter">
<option value="">{% trans "All" %}</option>
{% for stock_type in all_stock_types %}
<option value="{{ stock_type }}" {% if stock_type == selected_stock_type %}selected{% endif %}>{{ stock_type|title }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button>
</div>
</form>
{% if is_paginated %}
<div class="d-flex justify-content-between mb-4">
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span>
</div>
{% endif %}
{% if cars %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for car in cars %}
<div class="col">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column">
<h5 class="card-title text-danger fw-bold">
<img src="{{car.logo}}" width="40" height="40" class="">&nbsp;&nbsp;{{ car.id_car_make.name }}&nbsp;&nbsp;{{ car.id_car_model.name }}&nbsp;&nbsp;{{ car.id_car_serie.name }}&nbsp;&nbsp;{{ car.year}}
</h5>
<p class="card-text text-muted mb-2">
<strong>{% trans "VIN:" %}</strong> {{ car.vin }}
</p>
<p class="card-text mb-2">
<strong>{% trans "Age:" %}</strong>
<span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span>
</p>
<p class="card-text mb-2">
<strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }}
</p>
<div class="mt-auto pt-3 border-top">
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-outline-primary btn-sm w-100">
{% trans "View Details" %}
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-success d-flex align-items-center" role="alert">
<i class="fas fa-check-circle me-2"></i>
<div>
{% trans "Excellent! There are no cars in the aging inventory at the moment." %}
</div>
</div>
{% endif %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">
{% if is_paginated %}
{% include 'partials/pagination.html' %}
{% endif %}
</div>
</div>
</div>
{% endblock content %}
{% extends 'base.html' %}
{% load i18n %}
{% load tenhal_tag %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">
{% trans "Aging Inventory" %}
<i class="fas fa-box-open text-danger ms-2"></i>
</h2>
<h4 class="text-muted mb-3 ">
{% trans "Aging Inventory Total" %} :: <span class=" text-danger">{{ total_aging_inventory_value|default:0.00 }}<span class="icon-saudi_riyal"></span></span>
</h4>
<p class="text-muted mb-0">{% trans "Cars in inventory for more than 60 days." %}</p>
</div>
<form method="GET" class="d-flex flex-wrap align-items-center mb-4 g-3">
<div class="col-sm-6 col-md-2 me-2">
<label for="make-filter"
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Make:" %}</label>
<select class="form-select" name="make" id="make-filter">
<option value="">{% trans "All" %}</option>
{% for make in all_makes %}
<option value="{{ make }}" {% if make == selected_make %}selected{% endif %}>{{ make }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="model-filter"
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Model:" %}</label>
<select class="form-select" name="model" id="model-filter">
<option value="">{% trans "All" %}</option>
{% for model in all_models %}
<option value="{{ model }}"
{% if model == selected_model %}selected{% endif %}>{{ model }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="series-filter"
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Series:" %}</label>
<select class="form-select" name="series" id="series-filter">
<option value="">{% trans "All" %}</option>
{% for series in all_series %}
<option value="{{ series }}"
{% if series == selected_series %}selected{% endif %}>{{ series }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="year-filter"
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Year:" %}</label>
<select class="form-select" name="year" id="year-filter">
<option value="">{% trans "All" %}</option>
{% for year in all_years %}
<option value="{{ year }}"
{% if year|stringformat:"s" == selected_year %}selected{% endif %}>{{ year }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6 col-md-2 me-2">
<label for="stock-type-filter"
class="form-label mb-0 small text-uppercase fw-bold">{% trans "Stock Type:" %}</label>
<select class="form-select" name="stock_type" id="stock-type-filter">
<option value="">{% trans "All" %}</option>
{% for stock_type in all_stock_types %}
<option value="{{ stock_type }}"
{% if stock_type == selected_stock_type %}selected{% endif %}>
{{ stock_type|title }}
</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary mt-4">{% trans "Filter" %}</button>
</div>
</form>
{% if is_paginated %}
<div class="d-flex justify-content-between mb-4">
<span class="text-muted">{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}</span>
<span class="text-muted">{% trans "Total Aging Cars:" %} {{ page_obj.paginator.count }}</span>
</div>
{% endif %}
{% if cars %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
{% for car in cars %}
<div class="col">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column">
<h5 class="card-title text-danger fw-bold">
<img src="{{ car.logo }}" width="40" height="40" class="">
&nbsp;&nbsp;{{ car.id_car_make.name }}&nbsp;&nbsp;{{ car.id_car_model.name }}&nbsp;&nbsp;{{ car.id_car_serie.name }}&nbsp;&nbsp;{{ car.year }}
</h5>
<p class="card-text text-muted mb-2">
<strong>{% trans "VIN:" %}</strong> {{ car.vin }}
</p>
<p class="card-text mb-2">
<strong>{% trans "Age:" %}</strong>
<span class="badge bg-danger">{{ car.age_in_days }} {% trans "days" %}</span>
</p>
<p class="card-text mb-2">
<strong>{% trans "Acquisition Date:" %}</strong> {{ car.receiving_date|date:"F j, Y" }}
</p>
<div class="mt-auto pt-3 border-top">
<a href="{% url 'car_detail' request.dealer.slug car.slug %}"
class="btn btn-outline-primary btn-sm w-100">{% trans "View Details" %}</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-success d-flex align-items-center" role="alert">
<i class="fas fa-check-circle me-2"></i>
<div>{% trans "Excellent! There are no cars in the aging inventory at the moment." %}</div>
</div>
{% endif %}
<div class="d-flex justify-content-end mt-3">
<div class="d-flex">
{% if is_paginated %}
{% include 'partials/pagination.html' %}
{% endif %}
</div>
</div>
</div>
{% endblock content %}

View File

@ -1,109 +1,112 @@
{% load i18n %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Monthly Performance Trends ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Revenue & Profit" %}</h5>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="revenueProfitChart"></canvas>
</div>
</div>
</div>
<div class="col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="CarsSoldByMonthChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="salesByBrandChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0 text-dark">{% trans "Models Sold" %}</h5>
<form method="GET" class="d-flex align-items-center">
<div class="form-group d-flex align-items-center me-2">
<label for="carMakeSelectSales" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
<select id="carMakeSelectSales" class="form-select" name="make_sold">
<option value="">{% trans "All Makes" %}</option>
{% for make_sold in all_makes_sold %}
<option value="{{ make_sold }}" {% if make_sold == selected_make_sales %}selected{% endif %}>{{ make_sold }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
<input type="hidden" name="start_date" value="{{ start_date|date:'Y-m-d' }}">
<input type="hidden" name="end_date" value="{{ end_date|date:'Y-m-d' }}">
</form>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="salesChartByModel"></canvas>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_inventory %}
<h3 class="fw-bold mb-3">{% trans "Inventory Trends" %}</h3>
<div class="row g-4 mb-5">
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="inventoryByMakeChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0 text-dark">{% trans "Models in Inventory" %}</h5>
<form method="GET" class="d-flex align-items-center">
<div class="form-group d-flex align-items-center me-2">
<label for="carMakeSelectInventory" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
<select id="carMakeSelectInventory" class="form-select" name="make_inventory">
<option value="">{% trans "All Makes" %}</option>
{% for make_inv in all_makes_inventory %}
<option value="{{ make_inv }}" {% if make_inv == selected_make_inventory %}selected{% endif %}>{{ make_inv }}</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
</form>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="inventoryByModelChart"></canvas>
</div>
</div>
</div>
</div>
{% endif %}
{% load i18n %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Monthly Performance Trends ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Revenue & Profit" %}</h5>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="revenueProfitChart"></canvas>
</div>
</div>
</div>
<div class="col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Monthly Cars Sold" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="CarsSoldByMonthChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Sales by Make" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="salesByBrandChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0 text-dark">{% trans "Models Sold" %}</h5>
<form method="GET" class="d-flex align-items-center">
<div class="form-group d-flex align-items-center me-2">
<label for="carMakeSelectSales" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
<select id="carMakeSelectSales" class="form-select" name="make_sold">
<option value="">{% trans "All Makes" %}</option>
{% for make_sold in all_makes_sold %}
<option value="{{ make_sold }}"
{% if make_sold == selected_make_sales %}selected{% endif %}>
{{ make_sold }}
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
<input type="hidden" name="start_date" value="{{ start_date|date:'Y-m-d' }}">
<input type="hidden" name="end_date" value="{{ end_date|date:'Y-m-d' }}">
</form>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="salesChartByModel"></canvas>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_inventory %}
<h3 class="fw-bold mb-3">{% trans "Inventory Trends" %}</h3>
<div class="row g-4 mb-5">
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Inventory by Make" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="inventoryByMakeChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6 col-12">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0 text-dark">{% trans "Models in Inventory" %}</h5>
<form method="GET" class="d-flex align-items-center">
<div class="form-group d-flex align-items-center me-2">
<label for="carMakeSelectInventory" class="form-label mb-0 me-2">{% trans "Select Make:" %}</label>
<select id="carMakeSelectInventory" class="form-select" name="make_inventory">
<option value="">{% trans "All Makes" %}</option>
{% for make_inv in all_makes_inventory %}
<option value="{{ make_inv }}"
{% if make_inv == selected_make_inventory %}selected{% endif %}>
{{ make_inv }}
</option>
{% endfor %}
</select>
</div>
<button type="submit" class="btn btn-primary">{% trans "Go" %}</button>
</form>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="inventoryByModelChart"></canvas>
</div>
</div>
</div>
</div>
{% endif %}

View File

@ -1,266 +1,312 @@
{% load i18n %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Sales KPIs ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_sold }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
</div>
<h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_sold }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
</div>
<h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_sold }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Revenue" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Cost" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_inventory %}
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-danger fw-bold small mb-1"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a></p>
<h4 class="fw-bolder text-danger mb-3"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a></h4>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Financial Health KPIs ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p>
<h4 class="fw-bolder text-info mb-3">{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue Generated" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ total_revenue_generated|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT Collected" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_vat_collected|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Expenses" %}</p>
<h4 class="fw-bolder text-danger mb-3">{{ total_expenses|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Gross Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">{{ gross_profit|floatformat:2 }}<span class="icon-saudi_riyal"></span></h4>
</div>
</div>
</div>
</div>
{% endif %}
{% load i18n %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Sales KPIs ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_sold }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_revenue_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Net Profit from Cars" %}</p>
<h4 class="fw-bolder text-success mb-3">
{{ net_profit_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Discount on Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_discount_on_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cost of Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_cost_of_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Cars" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
<h4 class="fw-bold my-4">{% trans "Sales of New Cars" %}</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_sold }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Revenue" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_revenue_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Net Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">
{{ net_profit_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_new_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Cost" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_cost_of_new_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
<h4 class="fw-bold my-4">{% trans "Sales of Used Cars" %}</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Sold" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_sold }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Revenue" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_revenue_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Net Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">
{{ net_profit_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars VAT" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_used_cars|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Cost" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_cost_of_used_cars_sold|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_inventory %}
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_inventory_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ new_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars Inventory Value" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ used_car_value|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-danger fw-bold small mb-1">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
</p>
<h4 class="fw-bolder text-danger mb-3">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
</h4>
</div>
</div>
</div>
</div>
{% endif %}
{% if request.is_dealer or request.is_manager or request.is_accountant %}
<h3 class="fw-bold mb-3">
{% blocktrans with start_date=start_date|date:"F j, Y" end_date=end_date|date:"F j, Y" %}
Financial Health KPIs ({{ start_date }} - {{ end_date }})
{% endblocktrans %}
</h3>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue from Services" %}</p>
<h4 class="fw-bolder text-info mb-3">
{{ total_revenue_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT from Services" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected_from_services|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Revenue Generated" %}</p>
<h4 class="fw-bolder text-success mb-3">
{{ total_revenue_generated|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total VAT Collected" %}</p>
<h4 class="fw-bolder text-primary mb-3">
{{ total_vat_collected|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Expenses" %}</p>
<h4 class="fw-bolder text-danger mb-3">
{{ total_expenses|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Gross Profit" %}</p>
<h4 class="fw-bolder text-success mb-3">
{{ gross_profit|floatformat:2 }}<span class="icon-saudi_riyal"></span>
</h4>
</div>
</div>
</div>
</div>
{% endif %}

View File

@ -1,429 +1,427 @@
{% extends 'base.html' %}
{% load i18n %}
{% load tenhal_tag %}
{% block title %}
{% trans "Dealership Dashboard"|capfirst %}
{% endblock title %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">
{% if request.is_dealer %}
{% trans "Business Health Dashboard" %}
{% elif request.is_manger %}
{% trans "Manager Dashboard" %}
{% elif request.is_inventory %}
{% trans "Inventory Dashboard" %}
{% else %}
{% trans "Accountant Dashboard" %}
{% endif %}
<i class="fas fa-chart-area text-primary ms-2"></i>
</h2>
<form method="GET" class="date-filter-form">
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date" class="form-control" id="start-date" name="start_date"
value="{{ start_date|date:'Y-m-d' }}" required>
</div>
<div class="col-12 col-md-4">
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
<input type="date" class="form-control" id="end-date" name="end_date"
value="{{ end_date|date:'Y-m-d' }}" required>
</div>
<div class="col-12 col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
</div>
</div>
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
</form>
</div>
<div class="row g-4 mb-5">
{% include 'dashboards/financial_data_cards.html' %}
</div>
<div class="row g-4 mb-5">
{% include 'dashboards/chart.html' %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
// Define a color palette that aligns with the Phoenix template
const primaryColor = '#7249b6';
const secondaryColor = '#8193a6';
const successColor = '#00d074';
const dangerColor = '#e63757';
const chartColors = [
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
'#495057'
];
// Pass translated strings from Django to JavaScript
const translatedStrings = {
monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}",
monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}",
monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}",
salesByMakeLabel: "{% trans 'Car Count by Make' %}",
salesByModelPrefix: "{% trans 'Cars Sold for' %}",
inventoryByMakeLabel: "{% trans 'Car Count by Make' %}",
inventoryByModelLabel: "{% trans 'Cars in Inventory' %}",
jan: "{% trans 'Jan' %}",
feb: "{% trans 'Feb' %}",
mar: "{% trans 'Mar' %}",
apr: "{% trans 'Apr' %}",
may: "{% trans 'May' %}",
jun: "{% trans 'Jun' %}",
jul: "{% trans 'Jul' %}",
aug: "{% trans 'Aug' %}",
sep: "{% trans 'Sep' %}",
oct: "{% trans 'Oct' %}",
nov: "{% trans 'Nov' %}",
dec: "{% trans 'Dec' %}",
cars: "{% trans 'cars' %}"
};
// Monthly Cars Sold (Bar Chart)
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
new Chart(ctx1, {
type: 'bar',
data: {
labels: [
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
],
datasets: [{
label: translatedStrings.monthlyCarsSoldLabel,
data: {{ monthly_cars_sold_json|safe }},
backgroundColor: primaryColor,
borderColor: primaryColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: {
color: secondaryColor,
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
}
},
x: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
});
// Monthly Revenue & Profit (Line Chart)
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
new Chart(ctx2, {
type: 'line',
data: {
labels: [
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
],
datasets: [
{
label: translatedStrings.monthlyRevenueLabel,
data: {{ monthly_revenue_json|safe }},
borderColor: primaryColor,
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: primaryColor,
pointRadius: 5,
pointHoverRadius: 8
},
{
label: translatedStrings.monthlyNetProfitLabel,
data: {{ monthly_net_profit_json|safe }},
borderColor: successColor,
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: successColor,
pointRadius: 5,
pointHoverRadius: 8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: { color: '#495057', boxWidth: 20 }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: 'white',
bodyColor: 'white',
padding: 10,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y);
}
return label;
}
}
}
},
scales: {
x: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
},
y: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
}
}
}
});
// Sales by Make (Pie Chart)
function getChartColors(count) {
const colors = [];
for (let i = 0; i < count; i++) {
colors.push(chartColors[i % chartColors.length]);
}
return colors;
}
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
new Chart(ctx3, {
type: 'pie',
data: {
labels: {{ sales_by_make_labels_json|safe }},
datasets: [{
label: translatedStrings.salesByMakeLabel,
data: {{ sales_by_make_counts_json|safe }},
backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length),
hoverOffset: 15,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#343a40', font: { size: 14 } }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
}
}
}
}
}
});
// -----------------------------------------------------------
// 4. Sales by Model (Bar Chart)
// -----------------------------------------------------------
const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}');
const canvasElementSales = document.getElementById('salesChartByModel');
let chartInstanceSales = null;
if (salesDataByModel.length > 0) {
const labels = salesDataByModel.map(item => item.id_car_model__name);
const counts = salesDataByModel.map(item => item.count);
const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]);
chartInstanceSales = new Chart(canvasElementSales, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`,
data: counts,
backgroundColor: backgroundColor,
borderColor: backgroundColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
label += Math.round(context.parsed.y);
return label;
}
}
}
}
}
});
}
// -----------------------------------------------------------
// 5. Inventory by Make (Pie Chart)
// -----------------------------------------------------------
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
new Chart(ctxInventoryMake, {
type: 'pie',
data: {
labels: {{ inventory_by_make_labels_json|safe }},
datasets: [{
label: translatedStrings.inventoryByMakeLabel,
data: {{ inventory_by_make_counts_json|safe }},
backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length),
hoverOffset: 15,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#343a40', font: { size: 14 } }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
}
}
}
}
}
});
// -----------------------------------------------------------
// 6. Inventory by Model (Bar Chart)
// -----------------------------------------------------------
const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}');
const canvasInventoryModel = document.getElementById('inventoryByModelChart');
const messageInventoryModel = document.getElementById('inventoryByModelMessage');
if (inventoryDataByModel.length > 0) {
canvasInventoryModel.style.display = 'block';
if (messageInventoryModel) {
messageInventoryModel.style.display = 'none';
}
const labels = inventoryDataByModel.map(item => item.id_car_model__name);
const counts = inventoryDataByModel.map(item => item.count);
const backgroundColor = getChartColors(labels.length);
new Chart(canvasInventoryModel, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: translatedStrings.inventoryByModelLabel,
data: counts,
backgroundColor: backgroundColor,
borderColor: backgroundColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
label += Math.round(context.parsed.y);
return label;
}
}
}
}
}
});
} else {
canvasInventoryModel.style.display = 'none';
if (messageInventoryModel) {
messageInventoryModel.style.display = 'flex';
}
}
</script>
{% endblock %}
{% extends 'base.html' %}
{% load i18n %}
{% load tenhal_tag %}
{% block title %}
{% trans "Dealership Dashboard"|capfirst %}
{% endblock title %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">
{% if request.is_dealer %}
{% trans "Business Health Dashboard" %}
{% elif request.is_manger %}
{% trans "Manager Dashboard" %}
{% elif request.is_inventory %}
{% trans "Inventory Dashboard" %}
{% else %}
{% trans "Accountant Dashboard" %}
{% endif %}
<i class="fas fa-chart-area text-primary ms-2"></i>
</h2>
<form method="GET" class="date-filter-form">
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date"
class="form-control"
id="start-date"
name="start_date"
value="{{ start_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4">
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
<input type="date"
class="form-control"
id="end-date"
name="end_date"
value="{{ end_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
</div>
</div>
<input type="hidden" name="make_sold" value="{{ selected_make_sales }}">
</form>
</div>
<div class="row g-4 mb-5">{% include 'dashboards/financial_data_cards.html' %}</div>
<div class="row g-4 mb-5">{% include 'dashboards/chart.html' %}</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
// Define a color palette that aligns with the Phoenix template
const primaryColor = '#7249b6';
const secondaryColor = '#8193a6';
const successColor = '#00d074';
const dangerColor = '#e63757';
const chartColors = [
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
'#495057'
];
// Pass translated strings from Django to JavaScript
const translatedStrings = {
monthlyCarsSoldLabel: "{% trans 'Total Cars Sold' %}",
monthlyRevenueLabel: "{% trans 'Monthly Revenue' %}",
monthlyNetProfitLabel: "{% trans 'Monthly Net Profit' %}",
salesByMakeLabel: "{% trans 'Car Count by Make' %}",
salesByModelPrefix: "{% trans 'Cars Sold for' %}",
inventoryByMakeLabel: "{% trans 'Car Count by Make' %}",
inventoryByModelLabel: "{% trans 'Cars in Inventory' %}",
jan: "{% trans 'Jan' %}",
feb: "{% trans 'Feb' %}",
mar: "{% trans 'Mar' %}",
apr: "{% trans 'Apr' %}",
may: "{% trans 'May' %}",
jun: "{% trans 'Jun' %}",
jul: "{% trans 'Jul' %}",
aug: "{% trans 'Aug' %}",
sep: "{% trans 'Sep' %}",
oct: "{% trans 'Oct' %}",
nov: "{% trans 'Nov' %}",
dec: "{% trans 'Dec' %}",
cars: "{% trans 'cars' %}"
};
// Monthly Cars Sold (Bar Chart)
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
new Chart(ctx1, {
type: 'bar',
data: {
labels: [
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
],
datasets: [{
label: translatedStrings.monthlyCarsSoldLabel,
data: {{ monthly_cars_sold_json|safe }},
backgroundColor: primaryColor,
borderColor: primaryColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: {
color: secondaryColor,
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
}
},
x: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
});
// Monthly Revenue & Profit (Line Chart)
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
new Chart(ctx2, {
type: 'line',
data: {
labels: [
translatedStrings.jan, translatedStrings.feb, translatedStrings.mar, translatedStrings.apr,
translatedStrings.may, translatedStrings.jun, translatedStrings.jul, translatedStrings.aug,
translatedStrings.sep, translatedStrings.oct, translatedStrings.nov, translatedStrings.dec
],
datasets: [
{
label: translatedStrings.monthlyRevenueLabel,
data: {{ monthly_revenue_json|safe }},
borderColor: primaryColor,
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: primaryColor,
pointRadius: 5,
pointHoverRadius: 8
},
{
label: translatedStrings.monthlyNetProfitLabel,
data: {{ monthly_net_profit_json|safe }},
borderColor: successColor,
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: successColor,
pointRadius: 5,
pointHoverRadius: 8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: { color: '#495057', boxWidth: 20 }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: 'white',
bodyColor: 'white',
padding: 10,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'SAR' }).format(context.parsed.y);
}
return label;
}
}
}
},
scales: {
x: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
},
y: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
}
}
}
});
// Sales by Make (Pie Chart)
function getChartColors(count) {
const colors = [];
for (let i = 0; i < count; i++) {
colors.push(chartColors[i % chartColors.length]);
}
return colors;
}
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
new Chart(ctx3, {
type: 'pie',
data: {
labels: {{ sales_by_make_labels_json|safe }},
datasets: [{
label: translatedStrings.salesByMakeLabel,
data: {{ sales_by_make_counts_json|safe }},
backgroundColor: getChartColors({{ sales_by_make_counts_json|safe }}.length),
hoverOffset: 15,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#343a40', font: { size: 14 } }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
}
}
}
}
}
});
// -----------------------------------------------------------
// 4. Sales by Model (Bar Chart)
// -----------------------------------------------------------
const salesDataByModel = JSON.parse('{{ sales_data_by_model_json|safe }}');
const canvasElementSales = document.getElementById('salesChartByModel');
let chartInstanceSales = null;
if (salesDataByModel.length > 0) {
const labels = salesDataByModel.map(item => item.id_car_model__name);
const counts = salesDataByModel.map(item => item.count);
const backgroundColor = labels.map((_, index) => getChartColors(labels.length)[index]);
chartInstanceSales = new Chart(canvasElementSales, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: `${translatedStrings.salesByModelPrefix} {{ selected_make_sales }}`,
data: counts,
backgroundColor: backgroundColor,
borderColor: backgroundColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
label += Math.round(context.parsed.y);
return label;
}
}
}
}
}
});
}
// -----------------------------------------------------------
// 5. Inventory by Make (Pie Chart)
// -----------------------------------------------------------
const ctxInventoryMake = document.getElementById('inventoryByMakeChart').getContext('2d');
new Chart(ctxInventoryMake, {
type: 'pie',
data: {
labels: {{ inventory_by_make_labels_json|safe }},
datasets: [{
label: translatedStrings.inventoryByMakeLabel,
data: {{ inventory_by_make_counts_json|safe }},
backgroundColor: getChartColors({{ inventory_by_make_counts_json|safe }}.length),
hoverOffset: 15,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#343a40', font: { size: 14 } }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} ${translatedStrings.cars} (${percentage}%)`;
}
}
}
}
}
});
// -----------------------------------------------------------
// 6. Inventory by Model (Bar Chart)
// -----------------------------------------------------------
const inventoryDataByModel = JSON.parse('{{ inventory_data_by_model_json|safe }}');
const canvasInventoryModel = document.getElementById('inventoryByModelChart');
const messageInventoryModel = document.getElementById('inventoryByModelMessage');
if (inventoryDataByModel.length > 0) {
canvasInventoryModel.style.display = 'block';
if (messageInventoryModel) {
messageInventoryModel.style.display = 'none';
}
const labels = inventoryDataByModel.map(item => item.id_car_model__name);
const counts = inventoryDataByModel.map(item => item.count);
const backgroundColor = getChartColors(labels.length);
new Chart(canvasInventoryModel, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: translatedStrings.inventoryByModelLabel,
data: counts,
backgroundColor: backgroundColor,
borderColor: backgroundColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
}
}
},
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
label += Math.round(context.parsed.y);
return label;
}
}
}
}
}
});
} else {
canvasInventoryModel.style.display = 'none';
if (messageInventoryModel) {
messageInventoryModel.style.display = 'flex';
}
}
</script>
{% endblock %}

View File

@ -1,367 +1,371 @@
{% extends 'base.html' %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-0">Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i></h2>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Last 30 Days
</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><a class="dropdown-item" href="#">Today</a></li>
<li><a class="dropdown-item" href="#">Last 7 Days</a></li>
<li><a class="dropdown-item" href="#">Last 90 Days</a></li>
</ul>
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex justify-content-between align-items-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-0">
Manager Dashboard<i class="fas fa-chart-area text-primary ms-2"></i>
</h2>
<div class="dropdown">
<button class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">Last 30 Days</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li>
<a class="dropdown-item" href="#">Today</a>
</li>
<li>
<a class="dropdown-item" href="#">Last 7 Days</a>
</li>
<li>
<a class="dropdown-item" href="#">Last 90 Days</a>
</li>
</ul>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
<div class="row g-4 mb-5">
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Revenue</p>
<h4 class="fw-bolder text-primary mb-3">$1.25M</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+8% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+8% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
</div>
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+3% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+5 units from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Avg. Time on Lot</p>
<h4 class="fw-bolder text-warning mb-3">10 days</h4>
</div>
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+2 days from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Inventory Value</p>
<h4 class="fw-bolder text-primary mb-3">$5.8M</h4>
</div>
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
-2% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Aging Inventory</p>
<h4 class="fw-bolder text-danger mb-3">12 units</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
-4 cars from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+8% from last month
</span>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+8% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Net Profit</p>
<h4 class="fw-bolder text-success mb-3">$1.25M</h4>
<div class="row g-4 mb-5">
<div class="col-lg-8">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="revenueProfitChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Monthly Cars Sold</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="CarsSoldByMonthChart"></canvas>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+8% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Expense</p>
<h4 class="fw-bolder text-danger mb-3">$1.25M</h4>
<div class="row g-4">
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Sales by Make</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="salesByBrandChart"></canvas>
</div>
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+3% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Total Cars Sold</p>
<h4 class="fw-bolder text-success mb-3">{{ sold_cars }}</h4>
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+5 units from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Avg. Time on Lot</p>
<h4 class="fw-bolder text-warning mb-3">10 days</h4>
<div class="card-body" style="height: 400px;">
<canvas id="salespersonChart"></canvas>
</div>
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+2 days from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Inventory Value</p>
<h4 class="fw-bolder text-primary mb-3">$5.8M</h4>
</div>
<span class="badge bg-danger-subtle text-danger fw-bold p-2 rounded-pill d-inline-flex align-self-start">
-2% from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Aging Inventory</p>
<h4 class="fw-bolder text-danger mb-3">12 units</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
-4 cars from last month
</span>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body d-flex flex-column justify-content-between p-4">
<div>
<p class="text-uppercase text-muted fw-bold small mb-1">Gross Profit</p>
<h4 class="fw-bolder text-info mb-3">$1.25M</h4>
</div>
<span class="badge bg-success-subtle text-success fw-bold p-2 rounded-pill d-inline-flex align-self-start">
+8% from last month
</span>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-lg-8">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Monthly Revenue & Profit</h5>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="revenueProfitChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Monthly Cars Sold</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="CarsSoldByMonthChart"></canvas>
</div>
</div>
</div>
</div>
<div class="row g-4">
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Sales by Make</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="salesByBrandChart"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">Top Salesperson Performance</h5>
</div>
<div class="card-body" style="height: 400px;">
<canvas id="salespersonChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
<script>
// Define a color palette that aligns with the Phoenix template
const primaryColor = '#7249b6'; // A vibrant purple
const secondaryColor = '#8193a6'; // A muted gray/blue
const successColor = '#00d074'; // A bright green
const warningColor = '#ffc107'; // A strong yellow
const dangerColor = '#e63757'; // A deep red
const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107'];
const primaryColor = '#7249b6'; // A vibrant purple
const secondaryColor = '#8193a6'; // A muted gray/blue
const successColor = '#00d074'; // A bright green
const warningColor = '#ffc107'; // A strong yellow
const dangerColor = '#e63757'; // A deep red
const chartColors = ['#00d27a', '#7249b6', '#32b9ff', '#e63757', '#ffc107'];
// Monthly Cars Sold (Bar Chart)
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
new Chart(ctx1, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [{
label: 'Total Cars Sold',
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
backgroundColor: primaryColor,
borderColor: primaryColor,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
const ctx1 = document.getElementById('CarsSoldByMonthChart').getContext('2d');
new Chart(ctx1, {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [{
label: 'Total Cars Sold',
data: [2, 3, 10, 4, 30, 12, 8, 9, 20, 12, 15, 35],
backgroundColor: primaryColor,
borderColor: primaryColor,
borderWidth: 1
}]
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor }
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
x: {
grid: { display: false },
ticks: { color: secondaryColor }
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor }
},
x: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
}
});
});
// Monthly Revenue & Profit (Line Chart)
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
new Chart(ctx2, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [
{
label: 'Monthly Revenue',
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
borderColor: primaryColor,
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: primaryColor,
pointRadius: 5,
pointHoverRadius: 8
},
{
label: 'Monthly Net Profit',
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
borderColor: successColor,
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: successColor,
pointRadius: 5,
pointHoverRadius: 8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: { color: '#495057', boxWidth: 20 }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: 'white',
bodyColor: 'white',
padding: 10,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
const ctx2 = document.getElementById('revenueProfitChart').getContext('2d');
new Chart(ctx2, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [
{
label: 'Monthly Revenue',
data: [120000, 150000, 130000, 180000, 200000, 175000, 190000, 220000, 210000, 250000, 240000, 280000],
borderColor: primaryColor,
backgroundColor: 'rgba(114, 73, 182, 0.1)', // Using primaryColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: primaryColor,
pointRadius: 5,
pointHoverRadius: 8
},
{
label: 'Monthly Net Profit',
data: [25000, 35000, 28000, 40000, 45000, 38000, 42000, 50000, 48000, 55000, 52000, 60000],
borderColor: successColor,
backgroundColor: 'rgba(0, 208, 116, 0.1)', // Using successColor with transparency
tension: 0.4,
fill: true,
pointBackgroundColor: successColor,
pointRadius: 5,
pointHoverRadius: 8
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
labels: { color: '#495057', boxWidth: 20 }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: 'white',
bodyColor: 'white',
padding: 10,
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
}
return label;
}
if (context.parsed.y !== null) {
label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y);
}
return label;
}
}
}
},
scales: {
x: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
},
y: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
scales: {
x: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
},
y: {
grid: { color: 'rgba(0, 0, 0, 0.05)' },
ticks: { color: secondaryColor },
border: { color: secondaryColor }
}
}
}
}
});
});
// Sales by Make (Pie Chart)
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
new Chart(ctx3, {
type: 'pie',
data: {
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
datasets: [{
label: 'Car Count by Make',
data: [45, 30, 25, 15, 10],
backgroundColor: chartColors,
hoverOffset: 15,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#343a40', font: { size: 14 } }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} cars (${percentage}%)`;
const ctx3 = document.getElementById('salesByBrandChart').getContext('2d');
new Chart(ctx3, {
type: 'pie',
data: {
labels: ['Toyota', 'Ford', 'Honda', 'BMW', 'Other'],
datasets: [{
label: 'Car Count by Make',
data: [45, 30, 25, 15, 10],
backgroundColor: chartColors,
hoverOffset: 15,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: { color: '#343a40', font: { size: 14 } }
},
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(2);
return `${label}: ${value} cars (${percentage}%)`;
}
}
}
}
}
}
});
});
// Salesperson Performance (Bar Chart)
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
new Chart(ctx_salesperson, {
type: 'bar',
data: {
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
datasets: [{
label: 'Cars Sold',
data: [15, 22, 18, 25],
backgroundColor: chartColors,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
const ctx_salesperson = document.getElementById('salespersonChart').getContext('2d');
new Chart(ctx_salesperson, {
type: 'bar',
data: {
labels: ['John Doe', 'Jane Smith', 'Peter Jones', 'Mary Brown'],
datasets: [{
label: 'Cars Sold',
data: [15, 22, 18, 25],
backgroundColor: chartColors,
borderWidth: 1
}]
},
scales: {
x: {
title: { display: true, text: 'Salesperson Name', color: secondaryColor },
ticks: { color: secondaryColor }
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: true, text: 'Top Salesperson Performance', font: { size: 16 } }
},
y: {
beginAtZero: true,
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor },
ticks: { color: secondaryColor }
scales: {
x: {
title: { display: true, text: 'Salesperson Name', color: secondaryColor },
ticks: { color: secondaryColor }
},
y: {
beginAtZero: true,
title: { display: true, text: 'Number of Cars Sold', color: secondaryColor },
ticks: { color: secondaryColor }
}
}
}
}
});
</script>
{% endblock %}
});
</script>
{% endblock %}

View File

@ -1,269 +1,275 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">{% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i></h2>
<form method="GET" class="date-filter-form">
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date" class="form-control" id="start-date" name="start_date"
value="{{ start_date|date:'Y-m-d' }}" required>
</div>
<div class="col-12 col-md-4">
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
<input type="date" class="form-control" id="end-date" name="end_date"
value="{{ end_date|date:'Y-m-d' }}" required>
</div>
<div class="col-12 col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
</div>
</div>
</form>
</div>
<div class="row g-4 mb-5">
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-danger fw-bold small mb-1"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a></p>
<h4 class="fw-bolder text-danger mb-3"><a class="text-danger" href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a></h4>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="leadSourcesChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center" style="height: 400px;">
<canvas id="leadFunnelChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS%}
<script>
// Define your color palette at the top
const primaryColor = '#7249b6'; // A vibrant purple
const secondaryColor = '#8193a6'; // A muted gray/blue
const successColor = '#00d074'; // A bright green
const dangerColor = '#e63757'; // A deep red
const infoColor = '#17a2b8'; // Correcting the missing variable
const warningColor = '#ffc107'; // Add other colors if needed
const chartColors = [
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
'#495057'
];
// Pass translated strings from Django to JavaScript
const translatedStrings = {
numberOfLeads: "{% trans 'Number of Leads' %}",
leads: "{% trans 'Leads' %}",
numberOfOpportunities: "{% trans 'Number of Opportunities' %}"
};
// Get the canvas and message elements
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
const leadSourcesMessage = document.getElementById('leadSourcesMessage');
// Parse the JSON data from Django
const leadSourcesLabels = JSON.parse('{{ lead_sources_labels_json|safe }}');
const leadSourcesCounts = JSON.parse('{{ lead_sources_counts_json|safe }}');
// Check if there is any data to display
if (leadSourcesCounts.length > 0) {
// Show the chart and hide the message
ctx_leadSources.canvas.style.display = 'block';
if (leadSourcesMessage) {
leadSourcesMessage.style.display = 'none';
}
new Chart(ctx_leadSources, {
type: 'bar',
data: {
labels: leadSourcesLabels,
datasets: [{
label: translatedStrings.numberOfLeads,
data: leadSourcesCounts,
backgroundColor: infoColor,
borderColor: infoColor,
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: false },
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
return `${translatedStrings.leads}: ${context.parsed.x}`;
}
}
}
},
scales: {
x: {
beginAtZero: true,
title: { display: true, text: translatedStrings.numberOfLeads, color: secondaryColor },
ticks: {
color: secondaryColor,
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
},
grid: { color: 'rgba(0, 0, 0, 0.05)' }
},
y: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
});
} else {
// Hide the chart and show the message
ctx_leadSources.canvas.style.display = 'none';
if (leadSourcesMessage) {
leadSourcesMessage.style.display = 'flex';
}
}
// Lead Conversion Funnel (Horizontal Bar Chart)
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
const leadFunnelMessage = document.getElementById('leadFunnelMessage');
// Parse the dynamic data from Django
const opportunityStagesLabels = JSON.parse('{{ opportunity_stage_labels_json|safe }}');
const opportunityStagesCounts = JSON.parse('{{ opportunity_stage_counts_json|safe }}');
if (opportunityStagesCounts.length > 0) {
// Show the chart and hide the message
ctx_funnel.canvas.style.display = 'block';
if (leadFunnelMessage) {
leadFunnelMessage.style.display = 'none';
}
// Get a subset of colors based on the number of data points
const backgroundColors = chartColors.slice(0, opportunityStagesCounts.length);
new Chart(ctx_funnel, {
type: 'bar',
data: {
labels: opportunityStagesLabels,
datasets: [{
label: translatedStrings.numberOfOpportunities,
data: opportunityStagesCounts,
// Use the new backgroundColors array
backgroundColor: backgroundColors,
// Set borders to match the fill color
borderColor: backgroundColors,
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: false },
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const totalOpportunities = opportunityStagesCounts[0] || 0;
const currentOpportunities = context.parsed.x;
const percentage = totalOpportunities > 0 ? ((currentOpportunities / totalOpportunities) * 100).toFixed(1) : 0;
return `${translatedStrings.leads}: ${currentOpportunities} (${percentage}%)`;
}
}
}
},
scales: {
x: {
beginAtZero: true,
display: false
},
y: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
});
} else {
// Hide the chart and show the message
ctx_funnel.canvas.style.display = 'none';
if (leadFunnelMessage) {
leadFunnelMessage.style.display = 'flex';
}
}
</script>
{% endblock %}
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<div class="main-content flex-grow-1 container-fluid mt-4 mb-3">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 pb-3 border-bottom">
<h2 class="h3 fw-bold mb-3 mb-md-0">
{% trans "Sales Dashboard" %} <i class="fas fa-chart-area text-primary ms-2"></i>
</h2>
<form method="GET" class="date-filter-form">
<div class="row g-3">
<div class="col-12 col-md-4">
<label for="start-date" class="form-label">{% trans "Start Date" %}</label>
<input type="date"
class="form-control"
id="start-date"
name="start_date"
value="{{ start_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4">
<label for="end-date" class="form-label">{% trans "End Date" %}</label>
<input type="date"
class="form-control"
id="end-date"
name="end_date"
value="{{ end_date|date:'Y-m-d' }}"
required>
</div>
<div class="col-12 col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{% trans "Apply Filter" %}</button>
</div>
</div>
</form>
</div>
<div class="row g-4 mb-5">
<h3 class="fw-bold mb-3">{% trans "Inventory KPIs" %}</h3>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Total Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "New Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_new_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-muted fw-bold small mb-1">{% trans "Used Cars in Inventory" %}</p>
<h4 class="fw-bolder text-primary mb-3">{{ total_used_cars_in_inventory }}</h4>
</div>
</div>
</div>
<div class="col-sm-6 col-md-4 col-lg-3">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4">
<p class="text-uppercase text-danger fw-bold small mb-1">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{% trans "Aging Inventory (> 60 days)" %}</a>
</p>
<h4 class="fw-bolder text-danger mb-3">
<a class="text-danger"
href="{% url 'aging_inventory_list' request.dealer.slug %}">{{ aging_inventory_count }}</a>
</h4>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-5">
<div class="col-md-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Top Lead Sources" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="leadSourcesChart"></canvas>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm border-0">
<div class="card-header bg-white border-bottom-0">
<h5 class="fw-bold mb-0 text-dark">{% trans "Lead Conversion Funnel" %}</h5>
</div>
<div class="card-body d-flex align-items-center justify-content-center"
style="height: 400px">
<canvas id="leadFunnelChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock content %}
{% block customJS %}
<script>
// Define your color palette at the top
const primaryColor = '#7249b6'; // A vibrant purple
const secondaryColor = '#8193a6'; // A muted gray/blue
const successColor = '#00d074'; // A bright green
const dangerColor = '#e63757'; // A deep red
const infoColor = '#17a2b8'; // Correcting the missing variable
const warningColor = '#ffc107'; // Add other colors if needed
const chartColors = [
'#7249b6', '#00d074', '#e63757', '#17a2b8', '#ffc107',
'#8193a6', '#28a745', '#6c757d', '#fd7e14', '#dc3545',
'#20c997', '#6f42c1', '#e83e8c', '#6610f2', '#007bff',
'#495057'
];
// Pass translated strings from Django to JavaScript
const translatedStrings = {
numberOfLeads: "{% trans 'Number of Leads' %}",
leads: "{% trans 'Leads' %}",
numberOfOpportunities: "{% trans 'Number of Opportunities' %}"
};
// Get the canvas and message elements
const ctx_leadSources = document.getElementById('leadSourcesChart').getContext('2d');
const leadSourcesMessage = document.getElementById('leadSourcesMessage');
// Parse the JSON data from Django
const leadSourcesLabels = JSON.parse('{{ lead_sources_labels_json|safe }}');
const leadSourcesCounts = JSON.parse('{{ lead_sources_counts_json|safe }}');
// Check if there is any data to display
if (leadSourcesCounts.length > 0) {
// Show the chart and hide the message
ctx_leadSources.canvas.style.display = 'block';
if (leadSourcesMessage) {
leadSourcesMessage.style.display = 'none';
}
new Chart(ctx_leadSources, {
type: 'bar',
data: {
labels: leadSourcesLabels,
datasets: [{
label: translatedStrings.numberOfLeads,
data: leadSourcesCounts,
backgroundColor: infoColor,
borderColor: infoColor,
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: false },
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
return `${translatedStrings.leads}: ${context.parsed.x}`;
}
}
}
},
scales: {
x: {
beginAtZero: true,
title: { display: true, text: translatedStrings.numberOfLeads, color: secondaryColor },
ticks: {
color: secondaryColor,
callback: function(value) {
if (Number.isInteger(value)) {
return value;
}
}
},
grid: { color: 'rgba(0, 0, 0, 0.05)' }
},
y: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
});
} else {
// Hide the chart and show the message
ctx_leadSources.canvas.style.display = 'none';
if (leadSourcesMessage) {
leadSourcesMessage.style.display = 'flex';
}
}
// Lead Conversion Funnel (Horizontal Bar Chart)
const ctx_funnel = document.getElementById('leadFunnelChart').getContext('2d');
const leadFunnelMessage = document.getElementById('leadFunnelMessage');
// Parse the dynamic data from Django
const opportunityStagesLabels = JSON.parse('{{ opportunity_stage_labels_json|safe }}');
const opportunityStagesCounts = JSON.parse('{{ opportunity_stage_counts_json|safe }}');
if (opportunityStagesCounts.length > 0) {
// Show the chart and hide the message
ctx_funnel.canvas.style.display = 'block';
if (leadFunnelMessage) {
leadFunnelMessage.style.display = 'none';
}
// Get a subset of colors based on the number of data points
const backgroundColors = chartColors.slice(0, opportunityStagesCounts.length);
new Chart(ctx_funnel, {
type: 'bar',
data: {
labels: opportunityStagesLabels,
datasets: [{
label: translatedStrings.numberOfOpportunities,
data: opportunityStagesCounts,
// Use the new backgroundColors array
backgroundColor: backgroundColors,
// Set borders to match the fill color
borderColor: backgroundColors,
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
title: { display: false },
tooltip: {
backgroundColor: 'rgba(33, 37, 41, 0.9)',
titleColor: '#fff',
bodyColor: '#fff',
callbacks: {
label: function(context) {
const totalOpportunities = opportunityStagesCounts[0] || 0;
const currentOpportunities = context.parsed.x;
const percentage = totalOpportunities > 0 ? ((currentOpportunities / totalOpportunities) * 100).toFixed(1) : 0;
return `${translatedStrings.leads}: ${currentOpportunities} (${percentage}%)`;
}
}
}
},
scales: {
x: {
beginAtZero: true,
display: false
},
y: {
grid: { display: false },
ticks: { color: secondaryColor }
}
}
}
});
} else {
// Hide the chart and show the message
ctx_funnel.canvas.style.display = 'none';
if (leadFunnelMessage) {
leadFunnelMessage.style.display = 'flex';
}
}
</script>
{% endblock %}

View File

@ -1,93 +1,94 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block title %}
{% trans 'Activity' %}{% endblock %}
{% block content %}
<div class="row">
<div class="ol-auto pt-5 pb-9">
<div class="row-sm">
<div class="row d-flex-center">
<div class="col-8">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade active show"
id="tab-activity"
role="tabpanel"
aria-labelledby="activity-tab">
<h3 class="mb-4">{{ _("Activity") }}</h3>
<div class="border-bottom py-4">
{% for log in logs %}
<div class="d-flex">
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle"
style="width: 25px;
height: 25px">
<span class="fa-solid text-primary-dark fs-9 fa-clipboard text-primary-dark"></span>
</div>
<div class="flex-1">
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
<div class="flex-1 me-2">
<h5 class="text-body-highlight lh-sm">{{ log.user }}</h5>
</div>
<div class="fs-9">
<span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{ log.timestamp }}</span>
</div>
{% trans 'Activity' %}
{% endblock %}
{% block content %}
<div class="row">
<div class="ol-auto pt-5 pb-9">
<div class="row-sm">
<div class="row d-flex-center">
<div class="col-8">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade active show"
id="tab-activity"
role="tabpanel"
aria-labelledby="activity-tab">
<h3 class="mb-4">{{ _("Activity") }}</h3>
<div class="border-bottom py-4">
{% for log in logs %}
<div class="d-flex">
<div class="d-flex bg-primary-subtle rounded-circle flex-center me-3 bg-primary-subtle"
style="width: 25px;
height: 25px">
<span class="fa-solid text-primary-dark fs-9 fa-clipboard text-primary-dark"></span>
</div>
<div class="flex-1">
<div class="d-flex justify-content-between flex-column flex-xl-row mb-2 mb-sm-0">
<div class="flex-1 me-2">
<h5 class="text-body-highlight lh-sm">{{ log.user }}</h5>
</div>
<div class="fs-9">
<span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{ log.timestamp }}</span>
</div>
<p class="fs-9 mb-0">{{ log.action }}</p>
</div>
<p class="fs-9 mb-0">{{ log.action }}</p>
</div>
</div>
<div class="border-bottom border-translucent py-4">{% endfor %}</div>
</div>
</div>
<div class="border-bottom border-translucent py-4">{% endfor %}</div>
</div>
</div>
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination mb-0">
{% if page_obj.has_previous %}
<li class="page-item py-0">
<a class="page-link"
href="?page={{ page_obj.previous_page_number }}"
aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link"
href="?page={{ page_obj.next_page_number }}"
aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination mb-0">
{% if page_obj.has_previous %}
<li class="page-item py-0">
<a class="page-link"
href="?page={{ page_obj.previous_page_number }}"
aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true"><span class="fas fa-chevron-left"></span></span>
</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link"
href="?page={{ page_obj.next_page_number }}"
aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true"><span class="fas fa-chevron-right"></span></span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
{% endblock %}
</div>
{% endblock %}

View File

@ -1,9 +1,10 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}
{% trans 'Car Makes' %}{% endblock %}
{% block content %}
<style>
{% trans 'Car Makes' %}
{% endblock %}
{% block content %}
<style>
/* Your existing CSS styles here */
.car-makes-grid {
display: grid;
@ -55,37 +56,36 @@
background: #f8f9fa;
border-radius: 8px;
}
</style>
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
<form method="post" class="mb-3"
action="{% url 'assign_car_makes' request.dealer.slug %}">
{% csrf_token %}
<div class="car-makes-grid">
{% for car_make in form.fields.car_makes.queryset %}
<label class="car-make-option">
<input type="checkbox"
name="car_makes"
value="{{ car_make.pk }}"
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %}
checked
{% endif %}>
<div class="car-make-image-container">
{% if car_make.logo and car_make.logo.url %}
<img src="{{ car_make.logo.url }}"
alt="{{ car_make.name }}"
class="car-make-image">
{% else %}
<div class="logo-placeholder">{{ car_make.name }}</div>
{% endif %}
</div>
<div class="car-make-name">{{ car_make.get_local_name }}</div>
</label>
{% endfor %}
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-primary btn-lg" type="submit">
<i class="fa fa-save me-2"></i>{{ _("Save") }}
</button>
</div>
</form>
{% endblock %}
</style>
<h2 class="text-center text-primary">{{ _("Select Car Makes You Sell") }}</h2>
<form method="post"
class="mb-3"
action="{% url 'assign_car_makes' request.dealer.slug %}">
{% csrf_token %}
<div class="car-makes-grid">
{% for car_make in form.fields.car_makes.queryset %}
<label class="car-make-option">
<input type="checkbox"
name="car_makes"
value="{{ car_make.pk }}"
{% if car_make.pk in form.initial.car_makes or car_make.pk|stringformat:"s" in form.car_makes.value %} checked {% endif %}>
<div class="car-make-image-container">
{% if car_make.logo and car_make.logo.url %}
<img src="{{ car_make.logo.url }}"
alt="{{ car_make.name }}"
class="car-make-image">
{% else %}
<div class="logo-placeholder">{{ car_make.name }}</div>
{% endif %}
</div>
<div class="car-make-name">{{ car_make.get_local_name }}</div>
</label>
{% endfor %}
</div>
<div class="d-grid gap-2">
<button class="btn btn-outline-primary btn-lg" type="submit">
<i class="fa fa-save me-2"></i>{{ _("Save") }}
</button>
</div>
</form>
{% endblock %}

View File

@ -3,64 +3,92 @@
{% block title %}
{% trans 'Profile' %}
{% endblock %}
{% block content %}
<div class="container-fluid mb-3">
<div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto">
<h2 class="mb-0">{% trans 'Profile' %}</h2>
</div>
<div class="col-auto">
<div class="dropdown">
<button class="btn btn-phoenix-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a></li>
<li><a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a></li>
<li><a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a></li>
</ul>
<div class="container-fluid mb-3">
<div class="row align-items-center justify-content-between g-3 mb-4">
<div class="col-auto">
<h2 class="mb-0">{% trans 'Profile' %}</h2>
</div>
<div class="col-auto">
<div class="dropdown">
<button class="btn btn-phoenix-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="fas fa-cog me-2"></span>{{ _("Manage Profile") }}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="{% url 'dealer_update' dealer.slug %}"><span class="fas fa-edit me-2"></span>{{ _("Edit Profile") }}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'billing_info' %}"><span class="fas fa-credit-card me-2"></span>{{ _("Billing Information") }}</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'order_list' %}"><span class="fas fa-clipboard-list me-2"></span>{{ _("Plans History") }}</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item text-danger"
href="{% url 'account_change_password' %}"><span class="fas fa-key me-2"></span>{{ _("Change Password") }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start">
<div class="col-12 col-sm-auto mb-3 mb-sm-0">
<input class="d-none" id="avatarFile" type="file" />
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm" for="avatarFile">
{% if dealer.logo %}
<img src="{{ dealer.logo.url }}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px" />
{% else %}
<img src="{% static 'images/logos/logo.png' %}" alt="{{ dealer.get_local_name }}" class="rounded-circle" style="max-width: 150px" />
{% endif %}
</label>
</div>
<div class="flex-1 col-12 col-sm ms-2">
<h3>{{ dealer.get_local_name }}</h3>
<p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p>
<span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
</div>
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="d-flex flex-column flex-sm-row align-items-sm-center g-3 g-sm-5 text-center text-sm-start">
<div class="col-12 col-sm-auto mb-3 mb-sm-0">
<input class="d-none" id="avatarFile" type="file" />
<label class="cursor-pointer avatar avatar-5xl border rounded-circle shadow-sm"
for="avatarFile">
{% if dealer.logo %}
<img src="{{ dealer.logo.url }}"
alt="{{ dealer.get_local_name }}"
class="rounded-circle"
style="max-width: 150px" />
{% else %}
<img src="{% static 'images/logos/logo.png' %}"
alt="{{ dealer.get_local_name }}"
class="rounded-circle"
style="max-width: 150px" />
{% endif %}
</label>
</div>
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div>
<div class="flex-1 col-12 col-sm ms-2">
<h3>{{ dealer.get_local_name }}</h3>
<p class="text-body-secondary mb-1">{% trans 'Joined' %} {{ dealer.joined_at|timesince }} {% trans 'ago' %}</p>
<span class="badge bg-primary-subtle text-primary">{% trans 'Last login' %}: {{ dealer.user.last_login|date:"D M d, Y H:i" }}</span>
</div>
<div class="col-12 col-sm-auto d-flex align-items-center justify-content-around flex-wrap mt-3 mt-sm-0">
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total users'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ dealer.staff_count }} / {{ allowed_users }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-success"
role="progressbar"
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
<div class="text-center mx-3 mb-2 mb-sm-0">
<h6 class="mb-2 text-body-secondary">{% trans 'Total cars'|capfirst %}</h6>
<h4 class="fs-7 text-body-highlight mb-2">{{ cars_count }} / {{ allowed_cars }}</h4>
<div class="progress" style="height: 5px; width: 100px;">
<div class="progress-bar bg-info"
role="progressbar"
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
@ -68,64 +96,89 @@
</div>
</div>
</div>
</div>
<div class="row g-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="subscription-tab" data-bs-toggle="tab" data-bs-target="#subscription-pane" type="button" role="tab" aria-controls="subscription-pane" aria-selected="true"><span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-pane" type="button" role="tab" aria-controls="contact-pane" aria-selected="false"><span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="makes-tab" data-bs-toggle="tab" data-bs-target="#makes-pane" type="button" role="tab" aria-controls="makes-pane" aria-selected="false"><span class="fas fa-car me-2"></span>{{ _("Car Brands") }}</button>
</li>
</ul>
<div class="tab-content pt-4" id="profileTabsContent">
<div class="tab-pane fade show active" id="subscription-pane" role="tabpanel" aria-labelledby="subscription-tab">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">{{ dealer.user.userplan.plan|capfirst }}</h3>
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
<span class="badge bg-success-subtle text-success">{{ _("Active") }}</span>
{% elif dealer.user.userplan and dealer.user.userplan.is_expired %}
<span class="badge bg-danger-subtle text-danger">{{ _("Expired") }}</span>
{% else %}
<span class="badge bg-warning-subtle text-warning">{{ _("No Active Plan") }}</span>
{% endif %}
</div>
<p class="fs-9 text-body-secondary">
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
{% trans 'Active until' %}: {{ dealer.user.userplan.expire }} &nbsp; <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left }}</small>
{% else %}
{% trans 'Please subscribe or renew your plan to continue using our services.' %}
{% endif %}
</p>
<div class="d-flex align-items-end mb-3">
<h4 class="fw-bolder me-1">
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span>
</h4>
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5>
</div>
<ul class="list-unstyled mb-4">
{% for line in dealer.user.userplan.plan.description|splitlines %}
<li class="d-flex align-items-center mb-1">
<span class="uil uil-check-circle text-success me-2"></span>
<span class="text-body-secondary">{{ line }}</span>
</li>
{% endfor %}
</ul>
{% comment %} <div class="d-flex justify-content-end gap-2">
<div class="row g-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<ul class="nav nav-tabs nav-justified" id="profileTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active"
id="subscription-tab"
data-bs-toggle="tab"
data-bs-target="#subscription-pane"
type="button"
role="tab"
aria-controls="subscription-pane"
aria-selected="true">
<span class="fas fa-star me-2"></span>{{ _("Plan & Subscription") }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link"
id="contact-tab"
data-bs-toggle="tab"
data-bs-target="#contact-pane"
type="button"
role="tab"
aria-controls="contact-pane"
aria-selected="false">
<span class="fas fa-info-circle me-2"></span>{{ _("Company Details") }}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link"
id="makes-tab"
data-bs-toggle="tab"
data-bs-target="#makes-pane"
type="button"
role="tab"
aria-controls="makes-pane"
aria-selected="false">
<span class="fas fa-car me-2"></span>{{ _("Car Brands") }}
</button>
</li>
</ul>
<div class="tab-content pt-4" id="profileTabsContent">
<div class="tab-pane fade show active"
id="subscription-pane"
role="tabpanel"
aria-labelledby="subscription-tab">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-center justify-content-between mb-3">
<h3 class="mb-0">{{ dealer.user.userplan.plan|capfirst }}</h3>
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
<span class="badge bg-success-subtle text-success">{{ _("Active") }}</span>
{% elif dealer.user.userplan and dealer.user.userplan.is_expired %}
<span class="badge bg-danger-subtle text-danger">{{ _("Expired") }}</span>
{% else %}
<span class="badge bg-warning-subtle text-warning">{{ _("No Active Plan") }}</span>
{% endif %}
</div>
<p class="fs-9 text-body-secondary">
{% if dealer.user.userplan and not dealer.user.userplan.is_expired %}
{% trans 'Active until' %}: {{ dealer.user.userplan.expire }} &nbsp; <small>{% trans 'Days left' %}: {{ dealer.user.userplan.days_left }}</small>
{% else %}
{% trans 'Please subscribe or renew your plan to continue using our services.' %}
{% endif %}
</p>
<div class="d-flex align-items-end mb-3">
<h4 class="fw-bolder me-1">
{{ dealer.user.userplan.plan.planpricing_set.first.price }} <span class="icon-saudi_riyal"></span>
</h4>
<h5 class="fs-9 fw-normal text-body-tertiary ms-1">{{ _("Per month") }}</h5>
</div>
<ul class="list-unstyled mb-4">
{% for line in dealer.user.userplan.plan.description|splitlines %}
<li class="d-flex align-items-center mb-1">
<span class="uil uil-check-circle text-success me-2"></span>
<span class="text-body-secondary">{{ line }}</span>
</li>
{% endfor %}
</ul>
{% comment %} <div class="d-flex justify-content-end gap-2">
{% if dealer.user.userplan.is_expired %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
{% endif %}
@ -135,114 +188,141 @@
{% if not dealer.user.userplan %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-success"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
{% endif %}
</div> {% endcomment %}
<div class="d-flex justify-content-end gap-2">
</div> {% endcomment %}
<div class="d-flex justify-content-end gap-2">
{% if not dealer.user.userplan %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
<a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-primary"><span class="fas fa-cart-plus me-2"></span>{{ _("Subscribe Now") }}</a>
{% elif dealer.user.userplan.is_expired %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
<a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-warning"><span class="fas fa-redo-alt me-2"></span>{{ _("Renew") }}</a>
{% elif dealer.user.userplan.plan.name != "Enterprise" %}
<a href="{% url 'pricing_page' request.dealer.slug %}" class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
<a href="{% url 'pricing_page' request.dealer.slug %}"
class="btn btn-outline-primary"><span class="fas fa-rocket me-2"></span>{{ _("Upgrade Plan") }}</a>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex flex-column justify-content-center">
<div class="d-flex justify-content-between mb-4">
<h5 class="mb-0 text-body-highlight">{{ _("Manage Users & Cars") }}</h5>
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body d-flex flex-column justify-content-center">
<div class="d-flex justify-content-between mb-4">
<h5 class="mb-0 text-body-highlight">{{ _("Manage Users & Cars") }}</h5>
</div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total users") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-success"
role="progressbar"
style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%"
aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_users }}</span>
</div>
</div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total cars") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info"
role="progressbar"
style="width: {{ cars_count|get_percentage:allowed_cars }}%"
aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}"
aria-valuemin="0"
aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ cars_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_cars }}</span>
</div>
</div>
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>
</div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total users") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ dealer.staff_count|get_percentage:allowed_users }}%;" aria-valuenow="{{ dealer.staff_count|get_percentage:allowed_users }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ dealer.staff_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_users }}</span>
</div>
</div>
<div class="mb-4">
<h6 class="text-body-secondary">{{ _("Total cars") }}</h6>
<div class="progress" style="height: 10px;">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ cars_count|get_percentage:allowed_cars }}%;" aria-valuenow="{{ cars_count|get_percentage:allowed_cars }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="d-flex justify-content-between text-body-secondary fs-9 mt-2">
<span>{{ _("Used") }}: {{ cars_count }}</span>
<span>{{ _("Limit") }}: {{ allowed_cars }}</span>
</div>
</div>
<small class="text-body-secondary mt-auto">{{ _("Contact support to increase your limits") }}</small>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h5 class="mb-3">{% trans 'Contact Information' %}</h5>
<div class="d-flex align-items-center mb-3">
<span class="fas fa-location-dot me-3 text-primary"></span>
<div>
<h6 class="mb-0">{% trans 'Address' %}</h6>
<p class="mb-0 text-body-secondary">{{ dealer.address }}</p>
<div class="tab-pane fade"
id="contact-pane"
role="tabpanel"
aria-labelledby="contact-tab">
<div class="row g-3">
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h5 class="mb-3">{% trans 'Contact Information' %}</h5>
<div class="d-flex align-items-center mb-3">
<span class="fas fa-location-dot me-3 text-primary"></span>
<div>
<h6 class="mb-0">{% trans 'Address' %}</h6>
<p class="mb-0 text-body-secondary">{{ dealer.address }}</p>
</div>
</div>
</div>
<div class="d-flex align-items-center mb-3">
<span class="fas fa-envelope me-3 text-info"></span>
<div>
<h6 class="mb-0">{% trans 'Email' %}</h6>
<p class="mb-0 text-body-secondary">{{ dealer.user.email }}</p>
<div class="d-flex align-items-center mb-3">
<span class="fas fa-envelope me-3 text-info"></span>
<div>
<h6 class="mb-0">{% trans 'Email' %}</h6>
<p class="mb-0 text-body-secondary">{{ dealer.user.email }}</p>
</div>
</div>
</div>
<div class="d-flex align-items-center">
<span class="fas fa-phone me-3 text-success"></span>
<div>
<h6 class="mb-0">{% trans 'Phone' %}</h6>
<p class="mb-0 text-body-secondary" dir="ltr">{{ dealer.phone_number }}</p>
<div class="d-flex align-items-center">
<span class="fas fa-phone me-3 text-success"></span>
<div>
<h6 class="mb-0">{% trans 'Phone' %}</h6>
<p class="mb-0 text-body-secondary" dir="ltr">{{ dealer.phone_number }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h5 class="mb-3">{{ _("VAT Information") }}</h5>
<form action="{% url 'dealer_vat_rate_update' request.dealer.slug %}" method="post">
{% csrf_token %}
{{ vatform|crispy }}
<button class="btn btn-phoenix-primary mt-3" type="submit"><i class="fa-solid fa-pen-to-square me-1"></i>{% trans 'Update VAT' %}</button>
</form>
<div class="col-12 col-lg-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h5 class="mb-3">{{ _("VAT Information") }}</h5>
<form action="{% url 'dealer_vat_rate_update' request.dealer.slug %}"
method="post">
{% csrf_token %}
{{ vatform|crispy }}
<button class="btn btn-phoenix-primary mt-3" type="submit">
<i class="fa-solid fa-pen-to-square me-1"></i>{% trans 'Update VAT' %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="makes-pane" role="tabpanel" aria-labelledby="makes-tab">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h5 class="mb-4">{{ _("Makes you are selling") }}</h5>
<div class="d-flex flex-wrap gap-3 mb-4">
{% for make in car_makes %}
<div class="text-center p-2 border rounded-3">
{% if make.logo %}
<img src="{{ make.logo.url }}" alt="{{ make.get_local_name }}" class="rounded" style="height: 48px; width: auto; background-color:white;" />
{% endif %}
<p class="fs-8 text-body-secondary mt-1 mb-0">{{ make.get_local_name }}</p>
</div>
{% empty %}
<p class="text-body-secondary">{{ _("No car makes selected.") }}</p>
{% endfor %}
<div class="tab-pane fade"
id="makes-pane"
role="tabpanel"
aria-labelledby="makes-tab">
<div class="card h-100 shadow-sm">
<div class="card-body">
<h5 class="mb-4">{{ _("Makes you are selling") }}</h5>
<div class="d-flex flex-wrap gap-3 mb-4">
{% for make in car_makes %}
<div class="text-center p-2 border rounded-3">
{% if make.logo %}
<img src="{{ make.logo.url }}"
alt="{{ make.get_local_name }}"
class="rounded"
style="height: 48px;
width: auto;
background-color:white" />
{% endif %}
<p class="fs-8 text-body-secondary mt-1 mb-0">{{ make.get_local_name }}</p>
</div>
{% empty %}
<p class="text-body-secondary">{{ _("No car makes selected.") }}</p>
{% endfor %}
</div>
<a class="btn btn-phoenix-warning"
href="{% url 'assign_car_makes' request.dealer.slug %}"><span class="fas fa-plus me-2"></span>{{ _("Select Makes") }}</a>
</div>
<a class="btn btn-phoenix-warning" href="{% url 'assign_car_makes' request.dealer.slug %}"><span class="fas fa-plus me-2"></span>{{ _("Select Makes") }}</a>
</div>
</div>
</div>
@ -251,5 +331,4 @@
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -5,34 +5,38 @@
{{ _("Update Dealer Information") }}
{% endblock title %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{{ _("Update Dealer Information") }}
<i class="fas fa-car ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form hx-boost="false" method="post" enctype="multipart/form-data" class="needs-validation" novalidate>
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{% url 'dealer_detail' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-md-8">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{{ _("Update Dealer Information") }}
<i class="fas fa-car ms-2"></i>
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form hx-boost="false"
method="post"
enctype="multipart/form-data"
class="needs-validation"
novalidate>
{% csrf_token %}
{{ form|crispy }}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center mt-3">
<button class="btn btn-phoenix-primary btn-lg me-md-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>
<a href="{% url 'dealer_detail' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -170,10 +170,7 @@
valign="top"
style="font-family: Open Sans, Helvetica, Arial, sans-serif;
padding-bottom: 30px">
<p style="color: #ffffff;
font-size: 14px;
line-height: 24px;
margin: 0">{% trans 'Thank you for choosing us.' %}</p>
<p style="color: #ffffff; font-size: 14px; line-height: 24px; margin: 0">{% trans 'Thank you for choosing us.' %}</p>
</td>
</tr>
</table>

View File

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

View File

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

View File

@ -1,34 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
h2 { color: #333333; }
p { color: #555555; line-height: 1.6; }
.footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; }
.highlight { font-weight: bold; color: #007bff; }
</style>
</head>
<body>
<div class="container">
<h2>Hello {{ user_name }},</h2>
<p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p>
<p>
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}<br>
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}<br>
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}<br>
{% if customer_name != 'N/A' %}<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}<br>{% endif %}
{% if notes %}<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}<br>{% endif %}
</p>
<p>{% trans "Please be prepared for your schedule" %}.</p>
<p>{% trans "Thank you" %}!</p>
<p class="fs-4">{% trans "The team at Tenhal" %}.</p>
<div class="footer">
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
.container { max-width: 600px; margin: 20px auto; background-color: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
h2 { color: #333333; }
p { color: #555555; line-height: 1.6; }
.footer { text-align: center; margin-top: 20px; font-size: 0.8em; color: #888888; }
.highlight { font-weight: bold; color: #007bff; }
</style>
</head>
<body>
<div class="container">
<h2>Hello {{ user_name }},</h2>
<p>{% trans "This is a friendly reminder for your upcoming schedule" %}:</p>
<p>
<span class="highlight">{% trans "Purpose" %}:</span> {{ schedule_purpose }}
<br>
<span class="highlight">{% trans "Scheduled At" %}:</span> {{ scheduled_at }}
<br>
<span class="highlight">{% trans "Type" %}:</span> {{ schedule_type }}
<br>
{% if customer_name != 'N/A' %}
<span class="highlight">{% trans "Customer" %}:</span> {{ customer_name }}
<br>
{% endif %}
{% if notes %}
<span class="highlight">{% trans "Notes" %}:</span> {{ notes }}
<br>
{% endif %}
</p>
<p>{% trans "Please be prepared for your schedule" %}.</p>
<p>{% trans "Thank you" %}!</p>
<p class="fs-4">{% trans "The team at Tenhal" %}.</p>
<div class="footer">
<p>{% trans "This is an automated reminder. Please do not reply to this email." %}</p>
</div>
</div>
</div>
</body>
</html>
</body>
</html>

View File

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

View File

@ -61,19 +61,19 @@
rel="stylesheet"
id="user-style-default">
<script>
var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl');
} else {
var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true);
}
var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl');
} else {
var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true);
}
</script>
</head>
<body>
@ -115,17 +115,17 @@
</div>
</div>
<script>
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker');
}
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker');
}
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
}
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
}
</script>
<div class="support-chat-row">
<div class="row-fluid support-chat">

View File

@ -61,19 +61,19 @@
rel="stylesheet"
id="user-style-default">
<script>
var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl');
} else {
var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true);
}
var phoenixIsRTL = window.config.config.phoenixIsRTL;
if (phoenixIsRTL) {
var linkDefault = document.getElementById('style-default');
var userLinkDefault = document.getElementById('user-style-default');
linkDefault.setAttribute('disabled', true);
userLinkDefault.setAttribute('disabled', true);
document.querySelector('html').setAttribute('dir', 'rtl');
} else {
var linkRTL = document.getElementById('style-rtl');
var userLinkRTL = document.getElementById('user-style-rtl');
linkRTL.setAttribute('disabled', true);
userLinkRTL.setAttribute('disabled', true);
}
</script>
</head>
<body>
@ -111,17 +111,17 @@
</div>
</div>
<script>
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker');
}
var navbarTopStyle = window.config.config.phoenixNavbarTopStyle;
var navbarTop = document.querySelector('.navbar-top');
if (navbarTopStyle === 'darker') {
navbarTop.setAttribute('data-navbar-appearance', 'darker');
}
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
}
var navbarVerticalStyle = window.config.config.phoenixNavbarVerticalStyle;
var navbarVertical = document.querySelector('.navbar-vertical');
if (navbarVertical && navbarVerticalStyle === 'darker') {
navbarVertical.setAttribute('data-navbar-appearance', 'darker');
}
</script>
<div class="support-chat-row">
<div class="row-fluid support-chat">

View File

@ -15,7 +15,6 @@
</div>
</div>
</footer> {% endcomment %}
{% comment %} <footer class="footer position-absolute fs-9 bg-info-subtle">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center text-warning">
@ -32,7 +31,6 @@
</div>
</div>
</footer> {% endcomment %}
{% comment %} <footer class="footer position-absolute fs-9 bg-white text-secondary">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center">
@ -51,73 +49,64 @@
</div>
</div>
</footer> {% endcomment %}
<style>
<style>
.improved-footer {
.improved-footer {
/* Kept `position-absolute` and adjusted padding */
position: absolute;
bottom: 0;
width: 90%;
padding: 1.5rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-color);
}
.improved-footer .text-body {
color: var(--text-color) !important;
font-weight: 400;
}
.improved-footer .fw-bold {
font-weight: 600 !important;
color: var(--link-color);
}
.improved-footer a {
color: var(--link-color) !important;
text-decoration: none;
transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.improved-footer a:hover {
color: #d1d5db !important; /* A slightly softer white on hover */
transform: translateY(-2px);
}
.improved-footer .fas.fa-registered {
font-size: 0.8rem;
color: var(--text-color);
opacity: 0.6;
}
</style>
position: absolute;
bottom: 0;
width: 90%;
padding: 1.5rem;
<footer class="improved-footer">
<div class="container">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center">
<span class="text-body"> © 2025 All rights reserved</span>
<span class="fw-bold">Haikal</span>&nbsp;|&nbsp;<span class="fw-bold">هيكل</span>
</div>
<div class="col-12 col-sm-auto text-center">
<span class="text-body">Powered by </span>
<span>
<a class="mx-1 text-secondary" href="https://tenhal.sa">
<span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span>
</a>
</span>
<span class="fas fa-registered fw-light"></span>
</div>
border-top: 1px solid rgba(255, 255, 255, 0.05);
color: var(--text-color);
}
.improved-footer .text-body {
color: var(--text-color) !important;
font-weight: 400;
}
.improved-footer .fw-bold {
font-weight: 600 !important;
color: var(--link-color);
}
.improved-footer a {
color: var(--link-color) !important;
text-decoration: none;
transition: color 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.improved-footer a:hover {
color: #d1d5db !important; /* A slightly softer white on hover */
transform: translateY(-2px);
}
.improved-footer .fas.fa-registered {
font-size: 0.8rem;
color: var(--text-color);
opacity: 0.6;
}
</style>
<footer class="improved-footer">
<div class="container">
<div class="row g-0 justify-content-between align-items-center h-100">
<div class="col-12 col-sm-auto text-center">
<span class="text-body">© 2025 All rights reserved</span>
<span class="fw-bold">Haikal</span>&nbsp;|&nbsp;<span class="fw-bold">هيكل</span>
</div>
<div class="col-12 col-sm-auto text-center">
<span class="text-body">Powered by</span>
<span>
<a class="mx-1 text-secondary" href="https://tenhal.sa">
<span>TENHAL</span>&nbsp;|&nbsp;<span>تنحل</span>
</a>
</span>
<span class="fas fa-registered fw-light"></span>
</div>
</div>
</footer>
</div>
</footer>

View File

@ -10,49 +10,51 @@
{% endif %}
{% endblock %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %}
{% trans "Update Group" %}
<i class="fa-solid fa-user-group ms-2"></i>
{% else %}
{% trans "Create Group" %}
<i class="fa-solid fa-user-plus ms-2"></i>
{% endif %}
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li><strong>{{ field|capfirst }}:</strong> {% for error in errors %}{{ error }}{% endfor %}</li>
{% endfor %}
</ul>
<main class="d-flex align-items-center justify-content-center min-vh-80 py-5">
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-header bg-gradient py-4 border-0 rounded-top-4">
<h3 class="mb-0 fs-4 fw-bold text-center">
{% if object %}
{% trans "Update Group" %}
<i class="fa-solid fa-user-group ms-2"></i>
{% else %}
{% trans "Create Group" %}
<i class="fa-solid fa-user-plus ms-2"></i>
{% endif %}
</h3>
</div>
<div class="card-body p-4 p-md-5">
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
{% if form.errors %}
<div class="alert alert-danger mt-4" role="alert">
<h4 class="alert-heading small">{% trans "Please correct the following errors:" %}</h4>
<ul class="mb-0">
{% for field, errors in form.errors.items %}
<li>
<strong>{{ field|capfirst }}:</strong>
{% for error in errors %}{{ error }}{% endfor %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
</button>
<a href="{% url 'group_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
</a>
</div>
{% endif %}
<hr class="my-4">
<div class="d-grid gap-2 d-md-flex justify-content-md-center">
<button class="btn btn-phoenix-primary btn-lg md-me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{% trans "Save" %}
</button>
<a href="{% url 'group_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg">
<i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}
</a>
</div>
</form>
</form>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

View File

@ -6,70 +6,73 @@
{% trans "Groups" %}
{% endblock title %}
{% block content %}
<main class="py-5">
<div class="container">
{% if groups or request.GET.q %}
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp">
<div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4">
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold"> <i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}</h5>
<div class="d-flex gap-2">
<a href="{% url 'group_create' request.dealer.slug %}"
class="btn btn-phoenix-primary">
<i class="fa-solid fa-user-group fs-9 me-1"></i>
<span class="fas fa-plus me-2"></span>{% trans "Add Group" %}
</a>
<a href="{% url 'user_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary">
<span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %}
</a>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive scrollbar mx-n1 px-1 mt-3">
<table class="table align-items-center table-hover mb-0">
<thead>
<tr class="bg-light">
<th scope="col" class="text-secondary text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold text-end pe-4">{% trans 'actions'|capfirst %}</th>
</tr>
</thead>
<tbody>
{% for group in groups %}
<tr>
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
</td>
<td class="align-middle white-space-nowrap text-end pe-4">
<a class="btn btn-phoenix-secondary btn-sm"
href="{% url 'group_detail' request.dealer.slug group.id %}">
<i class="fa-solid fa-eye me-1"></i>
{% trans 'view'|capfirst %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer bg-light border-top">
<div class="d-flex justify-content-end">
{% include 'partials/pagination.html' %}
<main class="py-5">
<div class="container">
{% if groups or request.GET.q %}
<div class="card border-0 rounded-4 animate__animated animate__fadeInUp">
<div class="card-header border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center p-4">
<h5 class="card-title mb-2 mb-md-0 me-md-4 fw-bold">
<i class="fa-solid fa-user-group fs-3 me-1 text-primary "></i>{% trans "Groups" %}
</h5>
<div class="d-flex gap-2">
<a href="{% url 'group_create' request.dealer.slug %}"
class="btn btn-phoenix-primary">
<i class="fa-solid fa-user-group fs-9 me-1"></i>
<span class="fas fa-plus me-2"></span>{% trans "Add Group" %}
</a>
<a href="{% url 'user_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary">
<span class="fas fas fa-arrow-left me-2"></span>{% trans "Back to Staffs" %}
</a>
</div>
</div>
{% endif %}
</div>
{% else %}
{% url "group_create" request.dealer.slug as create_group_url %}
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
{% endif %}
</div>
</main>
{% endblock %}
<div class="card-body p-0">
<div class="table-responsive scrollbar mx-n1 px-1 mt-3">
<table class="table align-items-center table-hover mb-0">
<thead>
<tr class="bg-light">
<th scope="col" class="text-secondary text-uppercase fw-bold ps-4">{% trans 'name'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total Users'|capfirst %}</th>
<th scope="col" class="text-secondary text-uppercase fw-bold">{% trans 'total permission'|capfirst %}</th>
<th scope="col"
class="text-secondary text-uppercase fw-bold text-end pe-4">
{% trans 'actions'|capfirst %}
</th>
</tr>
</thead>
<tbody>
{% for group in groups %}
<tr>
<td class="align-middle white-space-nowrap ps-4">{{ group.name }}</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-users me-1"></i> {{ group.users.count }}
</td>
<td class="align-middle white-space-nowrap">
<i class="fa-solid fa-unlock me-1"></i> {{ group.permissions.count }}
</td>
<td class="align-middle white-space-nowrap text-end pe-4">
<a class="btn btn-phoenix-secondary btn-sm"
href="{% url 'group_detail' request.dealer.slug group.id %}">
<i class="fa-solid fa-eye me-1"></i>
{% trans 'view'|capfirst %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if page_obj.paginator.num_pages > 1 %}
<div class="card-footer bg-light border-top">
<div class="d-flex justify-content-end">{% include 'partials/pagination.html' %}</div>
</div>
{% endif %}
</div>
{% else %}
{% url "group_create" request.dealer.slug as create_group_url %}
{% include "empty-illustration-page.html" with value="group" url=create_group_url %}
{% endif %}
</div>
</main>
{% endblock %}

View File

@ -21,144 +21,138 @@
</div>
</div>
</div>
<div>
<form method="post" novalidate>
{% csrf_token %}
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text" class="form-control" id="permissionSearch"
placeholder="{% trans 'Search permissions...' %}">
</div>
</div>
<div class="col-md-6">
<div class="alert alert-info py-2 mb-0">
<i class="fas fa-info-circle me-2"></i>
{% trans "Checked items are currently assigned permissions" %}
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4" id="permissionsGrid">
{% for app_label, models in grouped_permissions.items %}
<div class="col"> {# This div opens for each app_label #}
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
{{ app_label|capfirst }}
</h5>
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
{{ models|length }} {% trans "categories" %}
</span>
</div>
<div>
<form method="post" novalidate>
{% csrf_token %}
<div class="row mb-4">
<div class="col-md-6">
<div class="input-group">
<span class="input-group-text"><i class="fas fa-search"></i></span>
<input type="text"
class="form-control"
id="permissionSearch"
placeholder="{% trans 'Search permissions...' %}">
</div>
<div class="card-body">
<div class="accordion" id="accordion-{{ app_label|slugify }}">
{% for model, perms in models.items %}
<div class="accordion-item border-0 mb-2">
<h6 class="accordion-header" id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<button class="accordion-button collapsed bg-white shadow-none py-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
aria-expanded="false"
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}">
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i>
{{ model|capfirst }}
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2">
{# This is where you might need the custom filter 'count_checked' #}
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }}
</span>
</button>
</h6>
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}"
class="accordion-collapse collapse"
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}"
data-bs-parent="#accordion-{{ app_label|slugify }}">
<div class="accordion-body pt-0 px-0">
<div class="list-group list-group-flush">
{% for perm in perms %}
<label class="list-group-item d-flex gap-2 {% if perm.id in group_permission_ids %}bg-light-primary{% endif %}">
<input class="form-check-input flex-shrink-0 mt-0"
type="checkbox"
name="permissions"
value="{{ perm.id }}"
id="perm_{{ perm.id }}"
{% if perm.id in group_permission_ids %}checked{% endif %}>
<span>
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
<small class="d-block text-muted">{{ perm.codename }}</small>
{% if model == 'Custom' %}
<span class="badge bg-info mt-1">
<i class="fas fa-star me-1"></i>{% trans "Custom" %}
</span>
{% elif perm.id in group_permission_ids %}
<span class="badge bg-success mt-1">
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
</span>
{% endif %}
</span>
</label>
{% endfor %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="alert alert-info py-2 mb-0">
<i class="fas fa-info-circle me-2"></i>
{% trans "Checked items are currently assigned permissions" %}
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4"
id="permissionsGrid">
{% for app_label, models in grouped_permissions.items %}
<div class="col">
{# This div opens for each app_label #}
<div class="card h-100 border-{% if app_label in group_permission_apps %}primary{% else %}light{% endif %}">
<div class="card-header bg-{% if app_label in group_permission_apps %}primary text-white{% else %}light{% endif %}">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-{% if app_label in group_permission_apps %}check-circle{% else %}cube{% endif %} me-2"></i>
{{ app_label|capfirst }}
</h5>
<span class="badge bg-{% if app_label in group_permission_apps %}light text-primary{% else %}secondary{% endif %}">
{{ models|length }} {% trans "categories" %}
</span>
</div>
</div>
{% endfor %}
<div class="card-body">
<div class="accordion" id="accordion-{{ app_label|slugify }}">
{% for model, perms in models.items %}
<div class="accordion-item border-0 mb-2">
<h6 class="accordion-header"
id="heading-{{ app_label|slugify }}-{{ model|slugify }}">
<button class="accordion-button collapsed bg-white shadow-none py-2"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse-{{ app_label|slugify }}-{{ model|slugify }}"
aria-expanded="false"
aria-controls="collapse-{{ app_label|slugify }}-{{ model|slugify }}">
<i class="fas fa-{% if model == 'Custom' %}star{% else %}table{% endif %} me-2"></i>
{{ model|capfirst }}
<span class="badge bg-{% if model in group_permission_models %}primary{% else %}secondary{% endif %} rounded-pill ms-2">
{# This is where you might need the custom filter 'count_checked' #}
{{ perms|length }} / {{ perms|count_checked:group_permission_ids }}
</span>
</button>
</h6>
<div id="collapse-{{ app_label|slugify }}-{{ model|slugify }}"
class="accordion-collapse collapse"
aria-labelledby="heading-{{ app_label|slugify }}-{{ model|slugify }}"
data-bs-parent="#accordion-{{ app_label|slugify }}">
<div class="accordion-body pt-0 px-0">
<div class="list-group list-group-flush">
{% for perm in perms %}
<label class="list-group-item d-flex gap-2 {% if perm.id in group_permission_ids %}bg-light-primary{% endif %}">
<input class="form-check-input flex-shrink-0 mt-0"
type="checkbox"
name="permissions"
value="{{ perm.id }}"
id="perm_{{ perm.id }}"
{% if perm.id in group_permission_ids %}checked{% endif %}>
<span>
<span class="d-block fw-bold">{{ perm.name|capfirst }}</span>
<small class="d-block text-muted">{{ perm.codename }}</small>
{% if model == 'Custom' %}
<span class="badge bg-info mt-1">
<i class="fas fa-star me-1"></i>{% trans "Custom" %}
</span>
{% elif perm.id in group_permission_ids %}
<span class="badge bg-success mt-1">
<i class="fas fa-check me-1"></i>{% trans "Assigned" %}
</span>
{% endif %}
</span>
</label>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row mt-4 mb-4">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary rounded-pill me-2">{{ group_permission_ids|length }} {% trans "selected" %}</span>
<span class="text-muted">{% trans "Permissions will be updated immediately" %}</span>
</div>
<div>
<button type="submit" class="btn btn-lg btn-primary me-2">
<i class="fas fa-save me-1"></i>{% trans "Save Changes" %}
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row mt-4 mb-4">
<div class="col">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="badge bg-primary rounded-pill me-2">
{{ group_permission_ids|length }} {% trans "selected" %}
</span>
<span class="text-muted">
{% trans "Permissions will be updated immediately" %}
</span>
</div>
<div>
<button type="submit" class="btn btn-lg btn-primary me-2">
<i class="fas fa-save me-1"></i>{% trans "Save Changes" %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
<style>
.bg-light-primary {
background-color: rgba(13, 110, 253, 0.1);
}
.list-group-item:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.accordion-button:not(.collapsed) {
box-shadow: none;
background-color: transparent;
}
.accordion-button:focus {
box-shadow: none;
border-color: rgba(0,0,0,.125);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
</form>
</div>
<style>
.bg-light-primary {
background-color: rgba(13, 110, 253, 0.1);
}
.list-group-item:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.accordion-button:not(.collapsed) {
box-shadow: none;
background-color: transparent;
}
.accordion-button:focus {
box-shadow: none;
border-color: rgba(0,0,0,.125);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize all accordions
document.querySelectorAll('.accordion-button').forEach(button => {
button.addEventListener('click', function() {

View File

@ -71,20 +71,12 @@
<div class="d-flex gap-3">
<span id="clearChatBtn"
class="translate-middle-y cursor-pointer"
title="{% if LANGUAGE_CODE == 'ar' %}
مسح المحادثة
{% else %}
Clear Chat
{% endif %}">
title="{% if LANGUAGE_CODE == 'ar' %} مسح المحادثة {% else %} Clear Chat {% endif %}">
<i class="fas fa-trash-alt text-danger"></i>
</span>
<span id="exportChatBtn"
class="translate-middle-y cursor-pointer"
title="{% if LANGUAGE_CODE == 'ar' %}
تصدير المحادثة
{% else %}
Export Chat
{% endif %}">
title="{% if LANGUAGE_CODE == 'ar' %} تصدير المحادثة {% else %} Export Chat {% endif %}">
<i class="fas fa-download text-success"></i>
</span>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,7 @@
{% block content %}
{% if request.user.is_authenticated %}
<div id="dashboard-content"
hx-get="{% if request.is_sales and not request.is_manager %}
{% url 'sales_dashboard' request.dealer.slug %}
{% else %}
{% url 'general_dashboard' request.dealer.slug %}
{% endif %}"
hx-get="{% if request.is_sales and not request.is_manager %} {% url 'sales_dashboard' request.dealer.slug %} {% else %} {% url 'general_dashboard' request.dealer.slug %} {% endif %}"
hx-trigger="load"
hx-target="#dashboard-content"
hx-swap="innerHTML">
@ -17,6 +13,4 @@
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -1,67 +1,69 @@
{% extends "base.html" %}
{% load i18n %}
{%block title%} {%trans 'Add Colors'%} {% endblock%}
{% block content %}
<div class="row mt-4 mb-3">
<h3 class="text-center">{% trans "Add Colors" %}</h3>
<p class="text-center">
{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}
</p>
<form method="post">
{% csrf_token %}
<!-- Exterior Colors -->
<div class="row g-4">
<p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p>
{% for color in form.fields.exterior.queryset %}
<div class="col-lg-4 col-xl-2">
<div class="card rounded shadow-sm color-card">
<label class="color-option">
<input class="color-radio"
type="radio"
name="exterior"
value="{{ color.id }}" {% if color.id == form.instance.exterior.id %}checked{% endif %}>
<div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})">
<div class="">
<small>{{ color.get_local_name }}</small>
{% block title %}
{% trans 'Add Colors' %} {% endblock %}
{% block content %}
<div class="row mt-4 mb-3">
<h3 class="text-center">{% trans "Add Colors" %}</h3>
<p class="text-center">
{% trans "Select exterior and interior colors for" %} {{ car.id_car_make.get_local_name }} {{ car.id_car_model.get_local_name }}
</p>
<form method="post">
{% csrf_token %}
<!-- Exterior Colors -->
<div class="row g-4">
<p class="fs-5 mb-0">{% trans 'Exterior Colors' %}</p>
{% for color in form.fields.exterior.queryset %}
<div class="col-lg-4 col-xl-2">
<div class="card rounded shadow-sm color-card">
<label class="color-option">
<input class="color-radio"
type="radio"
name="exterior"
value="{{ color.id }}"
{% if color.id == form.instance.exterior.id %}checked{% endif %}>
<div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})">
<div class="">
<small>{{ color.get_local_name }}</small>
</div>
</div>
</div>
</label>
</label>
</div>
</div>
</div>
{% endfor %}
<!-- Interior Colors -->
<p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p>
{% for color in form.fields.interior.queryset %}
<div class="col-lg-4 col-xl-2">
<div class="card rounded shadow-sm color-card">
<label class="color-option">
<input class="color-radio"
type="radio"
name="interior"
value="{{ color.id }}" {% if color.id == form.instance.interior.id %}checked{% endif %}>
<div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})">
<div class="">
<small>{{ color.get_local_name }}</small>
{% endfor %}
<!-- Interior Colors -->
<p class="fs-5 mt-3 mb-0">{% trans 'Interior Colors' %}</p>
{% for color in form.fields.interior.queryset %}
<div class="col-lg-4 col-xl-2">
<div class="card rounded shadow-sm color-card">
<label class="color-option">
<input class="color-radio"
type="radio"
name="interior"
value="{{ color.id }}"
{% if color.id == form.instance.interior.id %}checked{% endif %}>
<div class="card-body color-display"
style="background-color: rgb({{ color.rgb }})">
<div class="">
<small>{{ color.get_local_name }}</small>
</div>
</div>
</div>
</label>
</label>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="d-flex justify-content-center mt-4">
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</button>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}" class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>
<style>
{% endfor %}
</div>
<div class="d-flex justify-content-center mt-4">
<button class="btn btn-lg btn-phoenix-primary me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</button>
<a href="{% url 'car_detail' request.dealer.slug car.slug %}"
class="btn btn-lg btn-phoenix-secondary"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>
<style>
.color-card {
cursor: pointer;
transition: all 0.3s ease;
@ -93,7 +95,7 @@
}
.color-radio:focus + .color-display {
border: 3px solid rgb(44, 229, 44);
border: 3px solid rgb(44, 229, 44);
box-shadow: 0 0 10px rgba(44, 123, 229, 0.5);
}

View File

@ -1,41 +1,34 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}Delete Car{% endblock %}
{% block content %}
<main class="d-flex align-items-center justify-content-center min-vh-50 py-5">
<div class="col-md-6 ">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-body p-4 p-md-5 text-center bg-gradient">
<div class="mb-4">
<i class="fa-solid fa-triangle-exclamation text-danger" style="font-size: 2rem;"></i>
<main class="d-flex align-items-center justify-content-center min-vh-50 py-5">
<div class="col-md-6 ">
<div class="card shadow-lg border-0 rounded-4 overflow-hidden animate__animated animate__fadeInUp">
<div class="card-body p-4 p-md-5 text-center bg-gradient">
<div class="mb-4">
<i class="fa-solid fa-triangle-exclamation text-danger"
style="font-size: 2rem"></i>
</div>
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
<p class="fs-7 mb-4">{% trans "Are you absolutely sure you want to delete the car" %}</p>
<p class="fs-6 mb-4">
"<strong class="">{{ car }}</strong>"?
</p>
<p class="fs-7 mb-4">{% trans "This action is permanent and cannot be undone." %}</p>
<form method="post"
class="d-grid gap-3 d-sm-flex justify-content-sm-center">
{% csrf_token %}
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
</button>
<a href="{% url 'car_list' request.dealer.slug %}"
class="btn btn-phoenix-secondary btn-lg px-5">
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
</a>
</form>
</div>
<h1 class="card-title fw-bold mb-3 fs-4">{% trans 'Confirm Deletion' %}</h1>
<p class="fs-7 mb-4">
{% trans "Are you absolutely sure you want to delete the car" %}
</p>
<p class="fs-6 mb-4">
"<strong class="">{{ car }}</strong>"?
</p>
<p class="fs-7 mb-4">
{% trans "This action is permanent and cannot be undone." %}
</p>
<form method="post" class="d-grid gap-3 d-sm-flex justify-content-sm-center">
{% csrf_token %}
<button type="submit" class="btn btn-phoenix-danger btn-lg px-5">
<i class="fa-solid fa-trash-can me-2"></i>{% trans 'Confirm Delete' %}
</button>
<a href="{% url 'car_list' request.dealer.slug %}" class="btn btn-phoenix-secondary btn-lg px-5">
<i class="fa-solid fa-ban me-2"></i>{% trans 'Cancel' %}
</a>
</form>
</div>
</div>
</div>
</main>
{% endblock %}
</main>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More