changes
This commit is contained in:
commit
d9d1a8c376
@ -1179,10 +1179,16 @@ class ScheduleForm(forms.ModelForm):
|
|||||||
"purpose",
|
"purpose",
|
||||||
"scheduled_type",
|
"scheduled_type",
|
||||||
"scheduled_at",
|
"scheduled_at",
|
||||||
"duration",
|
"start_time",
|
||||||
|
"end_time",
|
||||||
"notes",
|
"notes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
"start_time": forms.TimeInput(attrs={"type": "time"}),
|
||||||
|
"end_time": forms.TimeInput(attrs={"type": "time"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class NoteForm(forms.ModelForm):
|
class NoteForm(forms.ModelForm):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -69,12 +69,12 @@ class Command(BaseCommand):
|
|||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
staff_member = StaffMember.objects.create(user=user)
|
# staff_member = StaffMember.objects.create(user=user)
|
||||||
services = Service.objects.all()
|
# services = Service.objects.all()
|
||||||
for service in services:
|
# for service in services:
|
||||||
staff_member.services_offered.add(service)
|
# staff_member.services_offered.add(service)
|
||||||
|
|
||||||
staff = Staff.objects.create(dealer=dealer,staff_member=staff_member,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)
|
groups = CustomGroup.objects.filter(dealer=dealer)
|
||||||
random_group = random.choice(list(groups))
|
random_group = random.choice(list(groups))
|
||||||
|
|||||||
@ -108,13 +108,13 @@ class InjectDealerMiddleware:
|
|||||||
request.is_dealer = True
|
request.is_dealer = True
|
||||||
request.dealer = request.user.dealer
|
request.dealer = request.user.dealer
|
||||||
|
|
||||||
elif hasattr(request.user, "staffmember"):
|
elif hasattr(request.user, "staff"):
|
||||||
request.is_staff = True
|
request.staff = getattr(request.user, "staff")
|
||||||
request.staff = request.user.staffmember.staff
|
|
||||||
request.dealer = request.staff.dealer
|
request.dealer = request.staff.dealer
|
||||||
|
request.is_staff = True
|
||||||
|
|
||||||
|
staff_groups = request.staff.groups.values_list("name", flat=True)
|
||||||
|
|
||||||
staff = getattr(request.user.staffmember, "staff")
|
|
||||||
staff_groups = staff.groups.values_list("name", flat=True)
|
|
||||||
if "Accountant" in staff_groups:
|
if "Accountant" in staff_groups:
|
||||||
request.is_accountant = True
|
request.is_accountant = True
|
||||||
elif "Manager" in staff_groups:
|
elif "Manager" in staff_groups:
|
||||||
|
|||||||
@ -40,7 +40,7 @@ from django_ledger.models import (
|
|||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from appointment.models import StaffMember
|
# from appointment.models import StaffMember
|
||||||
from plans.quota import get_user_quota
|
from plans.quota import get_user_quota
|
||||||
from plans.models import UserPlan
|
from plans.models import UserPlan
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -1277,8 +1277,11 @@ class StaffTypes(models.TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class Staff(models.Model, LocalizedNameMixin):
|
class Staff(models.Model, LocalizedNameMixin):
|
||||||
staff_member = models.OneToOneField(
|
# staff_member = models.OneToOneField(
|
||||||
StaffMember, on_delete=models.CASCADE, related_name="staff"
|
# StaffMember, 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")
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="staff")
|
||||||
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
|
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
|
||||||
@ -1338,17 +1341,17 @@ class Staff(models.Model, LocalizedNameMixin):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def permenant_delete(self):
|
def permenant_delete(self):
|
||||||
# self.user.delete()
|
self.user.delete()
|
||||||
self.staff_member.delete()
|
# self.staff_member.delete()
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def email(self):
|
def email(self):
|
||||||
return self.staff_member.user.email
|
return self.user.email
|
||||||
|
|
||||||
@property
|
# @property
|
||||||
def user(self):
|
# def user(self):
|
||||||
return self.staff_member.user
|
# return self.staff_member.user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def groups(self):
|
def groups(self):
|
||||||
@ -1388,6 +1391,12 @@ class Staff(models.Model, LocalizedNameMixin):
|
|||||||
models.Index(fields=["staff_type"]),
|
models.Index(fields=["staff_type"]),
|
||||||
]
|
]
|
||||||
permissions = []
|
permissions = []
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['dealer', 'user'],
|
||||||
|
name='unique_staff_email_per_dealer'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
@ -1504,7 +1513,7 @@ class Customer(models.Model):
|
|||||||
verbose_name=_("Gender"),
|
verbose_name=_("Gender"),
|
||||||
)
|
)
|
||||||
dob = models.DateField(verbose_name=_("Date of Birth"), null=True, blank=True)
|
dob = models.DateField(verbose_name=_("Date of Birth"), null=True, blank=True)
|
||||||
email = models.EmailField(unique=True, verbose_name=_("Email"))
|
email = models.EmailField(verbose_name=_("Email"))
|
||||||
national_id = models.CharField(
|
national_id = models.CharField(
|
||||||
max_length=10, unique=True, verbose_name=_("National ID"), null=True, blank=True
|
max_length=10, unique=True, verbose_name=_("National ID"), null=True, blank=True
|
||||||
)
|
)
|
||||||
@ -1546,6 +1555,12 @@ class Customer(models.Model):
|
|||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=['dealer', 'email'],
|
||||||
|
name='unique_customer_email_per_dealer'
|
||||||
|
)
|
||||||
|
]
|
||||||
verbose_name = _("Customer")
|
verbose_name = _("Customer")
|
||||||
verbose_name_plural = _("Customers")
|
verbose_name_plural = _("Customers")
|
||||||
indexes = [
|
indexes = [
|
||||||
@ -1566,6 +1581,8 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
def create_customer_model(self, for_lead=False):
|
def create_customer_model(self, for_lead=False):
|
||||||
customer_dict = to_dict(self)
|
customer_dict = to_dict(self)
|
||||||
|
customer = self.dealer.entity.get_customers().filter(email=self.email).first()
|
||||||
|
if not customer:
|
||||||
customer = self.dealer.entity.create_customer(
|
customer = self.dealer.entity.create_customer(
|
||||||
commit=False,
|
commit=False,
|
||||||
customer_model_kwargs={
|
customer_model_kwargs={
|
||||||
@ -1608,15 +1625,17 @@ class Customer(models.Model):
|
|||||||
return customer
|
return customer
|
||||||
|
|
||||||
def create_user_model(self, for_lead=False):
|
def create_user_model(self, for_lead=False):
|
||||||
user = User.objects.create_user(
|
user, created = User.objects.get_or_create(
|
||||||
username=self.email,
|
username=self.email,
|
||||||
email=self.email,
|
defaults={
|
||||||
first_name=self.first_name,
|
'email': self.email,
|
||||||
last_name=self.last_name,
|
'first_name': self.first_name,
|
||||||
password=make_random_password(),
|
'last_name': self.last_name,
|
||||||
is_staff=False,
|
'password': make_random_password(),
|
||||||
is_superuser=False,
|
'is_staff': False,
|
||||||
is_active=False if for_lead else True,
|
'is_superuser': False,
|
||||||
|
'is_active': False if for_lead else True,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
self.user = user
|
self.user = user
|
||||||
self.save()
|
self.save()
|
||||||
@ -2057,11 +2076,12 @@ class Schedule(models.Model):
|
|||||||
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
|
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
|
||||||
scheduled_at = models.DateTimeField()
|
scheduled_at = models.DateTimeField()
|
||||||
|
start_time = models.TimeField(verbose_name=_("Start Time"), null=True, blank=True)
|
||||||
|
end_time = models.TimeField(verbose_name=_("End Time"), null=True, blank=True)
|
||||||
scheduled_type = models.CharField(
|
scheduled_type = models.CharField(
|
||||||
max_length=200, choices=ScheduledType, default="Call"
|
max_length=200, choices=ScheduledType, default="Call"
|
||||||
)
|
)
|
||||||
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
||||||
duration = models.DurationField(default=timedelta(minutes=5))
|
|
||||||
notes = models.TextField(blank=True, null=True)
|
notes = models.TextField(blank=True, null=True)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=200, choices=ScheduleStatusChoices, default="Scheduled"
|
max_length=200, choices=ScheduleStatusChoices, default="Scheduled"
|
||||||
@ -2073,11 +2093,17 @@ class Schedule(models.Model):
|
|||||||
return f"Scheduled {self.purpose} on {self.scheduled_at}"
|
return f"Scheduled {self.purpose} on {self.scheduled_at}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
def duration(self):
|
||||||
|
return (self.end_time - self.start_time).seconds
|
||||||
|
@property
|
||||||
def schedule_past_date(self):
|
def schedule_past_date(self):
|
||||||
if self.scheduled_at < now():
|
if self.scheduled_at < now():
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_purpose(self):
|
||||||
|
return self.purpose.replace("_", " ").title()
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-scheduled_at"]
|
ordering = ["-scheduled_at"]
|
||||||
verbose_name = _("Schedule")
|
verbose_name = _("Schedule")
|
||||||
@ -2319,6 +2345,8 @@ class Tasks(models.Model):
|
|||||||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||||||
description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
|
description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
|
||||||
due_date = models.DateField(verbose_name=_("Due Date"))
|
due_date = models.DateField(verbose_name=_("Due Date"))
|
||||||
|
start_time = models.TimeField(verbose_name=_("Start Time"), null=True, blank=True)
|
||||||
|
end_time = models.TimeField(verbose_name=_("End Time"), null=True, blank=True)
|
||||||
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
||||||
assigned_to = models.ForeignKey(
|
assigned_to = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
|
|||||||
@ -298,79 +298,79 @@ def update_item_model_cost(sender, instance, created, **kwargs):
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# if created and not instance.is_sold:
|
# if created and not instance.is_sold:
|
||||||
if created:
|
# if created:
|
||||||
entity = instance.car.dealer.entity
|
# entity = instance.car.dealer.entity
|
||||||
coa = entity.get_default_coa()
|
# coa = entity.get_default_coa()
|
||||||
inventory_account = (
|
# inventory_account = (
|
||||||
entity.get_all_accounts()
|
|
||||||
.filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not inventory_account:
|
|
||||||
inventory_account = create_make_accounts(
|
|
||||||
entity,
|
|
||||||
coa,
|
|
||||||
[instance.car.id_car_make],
|
|
||||||
"Inventory",
|
|
||||||
roles.ASSET_CA_INVENTORY,
|
|
||||||
"debit",
|
|
||||||
)
|
|
||||||
|
|
||||||
cogs = (
|
|
||||||
entity.get_all_accounts()
|
|
||||||
.filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not cogs:
|
|
||||||
cogs = create_make_accounts(
|
|
||||||
entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
|
||||||
)
|
|
||||||
revenue = (
|
|
||||||
entity.get_all_accounts()
|
|
||||||
.filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
if not revenue:
|
|
||||||
revenue = create_make_accounts(
|
|
||||||
entity,
|
|
||||||
coa,
|
|
||||||
[instance.car.id_car_make],
|
|
||||||
"Revenue",
|
|
||||||
roles.ASSET_CA_RECEIVABLES,
|
|
||||||
"credit",
|
|
||||||
)
|
|
||||||
|
|
||||||
cash_account = (
|
|
||||||
# entity.get_all_accounts()
|
# entity.get_all_accounts()
|
||||||
# .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
# .filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
||||||
# .first()
|
# .first()
|
||||||
entity.get_all_accounts()
|
# )
|
||||||
.filter(role=roles.ASSET_CA_CASH, role_default=True)
|
# if not inventory_account:
|
||||||
.first()
|
# inventory_account = create_make_accounts(
|
||||||
)
|
# entity,
|
||||||
|
# coa,
|
||||||
|
# [instance.car.id_car_make],
|
||||||
|
# "Inventory",
|
||||||
|
# roles.ASSET_CA_INVENTORY,
|
||||||
|
# "debit",
|
||||||
|
# )
|
||||||
|
|
||||||
ledger = LedgerModel.objects.create(
|
# cogs = (
|
||||||
entity=entity, name=f"Inventory Purchase - {instance.car}"
|
# entity.get_all_accounts()
|
||||||
)
|
# .filter(name=f"Cogs:{instance.car.id_car_make.name}")
|
||||||
je = JournalEntryModel.objects.create(
|
# .first()
|
||||||
ledger=ledger,
|
# )
|
||||||
description=f"Acquired {instance.car} for inventory",
|
# if not cogs:
|
||||||
)
|
# cogs = create_make_accounts(
|
||||||
TransactionModel.objects.create(
|
# entity, coa, [instance.car.id_car_make], "Cogs", roles.COGS, "debit"
|
||||||
journal_entry=je,
|
# )
|
||||||
account=inventory_account,
|
# revenue = (
|
||||||
amount=Decimal(instance.cost_price),
|
# entity.get_all_accounts()
|
||||||
tx_type="debit",
|
# .filter(name=f"Revenue:{instance.car.id_car_make.name}")
|
||||||
description="",
|
# .first()
|
||||||
)
|
# )
|
||||||
|
# if not revenue:
|
||||||
|
# revenue = create_make_accounts(
|
||||||
|
# entity,
|
||||||
|
# coa,
|
||||||
|
# [instance.car.id_car_make],
|
||||||
|
# "Revenue",
|
||||||
|
# roles.ASSET_CA_RECEIVABLES,
|
||||||
|
# "credit",
|
||||||
|
# )
|
||||||
|
|
||||||
TransactionModel.objects.create(
|
# cash_account = (
|
||||||
journal_entry=je,
|
# # entity.get_all_accounts()
|
||||||
account=cash_account,
|
# # .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
||||||
amount=Decimal(instance.cost_price),
|
# # .first()
|
||||||
tx_type="credit",
|
# entity.get_all_accounts()
|
||||||
description="",
|
# .filter(role=roles.ASSET_CA_CASH, role_default=True)
|
||||||
)
|
# .first()
|
||||||
|
# )
|
||||||
|
|
||||||
|
# ledger = LedgerModel.objects.create(
|
||||||
|
# entity=entity, name=f"Inventory Purchase - {instance.car}"
|
||||||
|
# )
|
||||||
|
# je = JournalEntryModel.objects.create(
|
||||||
|
# ledger=ledger,
|
||||||
|
# description=f"Acquired {instance.car} for inventory",
|
||||||
|
# )
|
||||||
|
# TransactionModel.objects.create(
|
||||||
|
# journal_entry=je,
|
||||||
|
# account=inventory_account,
|
||||||
|
# amount=Decimal(instance.cost_price),
|
||||||
|
# tx_type="debit",
|
||||||
|
# description="",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# TransactionModel.objects.create(
|
||||||
|
# journal_entry=je,
|
||||||
|
# account=cash_account,
|
||||||
|
# amount=Decimal(instance.cost_price),
|
||||||
|
# tx_type="credit",
|
||||||
|
# description="",
|
||||||
|
# )
|
||||||
|
|
||||||
instance.car.item_model.default_amount = instance.marked_price
|
instance.car.item_model.default_amount = instance.marked_price
|
||||||
# if not isinstance(instance.car.item_model.additional_info, dict):
|
# if not isinstance(instance.car.item_model.additional_info, dict):
|
||||||
@ -952,11 +952,11 @@ def create_po_item_upload(sender, instance, created, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.Staff)
|
# @receiver(post_save, sender=models.Staff)
|
||||||
def add_service_to_staff(sender, instance, created, **kwargs):
|
# def add_service_to_staff(sender, instance, created, **kwargs):
|
||||||
if created:
|
# if created:
|
||||||
for service in Service.objects.all():
|
# for service in Service.objects.all():
|
||||||
instance.staff_member.services_offered.add(service)
|
# instance.services_offered.add(service)
|
||||||
|
|
||||||
|
|
||||||
##########################################################
|
##########################################################
|
||||||
@ -1026,10 +1026,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
|
|||||||
user=recipient,
|
user=recipient,
|
||||||
message=_(
|
message=_(
|
||||||
"""
|
"""
|
||||||
New Purchase Order has been added.
|
PO {po_number} has been fulfilled.
|
||||||
<a href="{url}" target="_blank">View</a>
|
<a href="{url}" target="_blank">View</a>
|
||||||
"""
|
"""
|
||||||
).format(
|
).format(
|
||||||
|
po_number=instance.po_number,
|
||||||
url=reverse(
|
url=reverse(
|
||||||
"purchase_order_detail",
|
"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},
|
||||||
@ -1180,38 +1181,38 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
|||||||
).format(
|
).format(
|
||||||
bill_number=instance.bill_number,
|
bill_number=instance.bill_number,
|
||||||
url=reverse(
|
url=reverse(
|
||||||
"bill-detail",
|
"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},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=BillModel)
|
# @receiver(post_save, sender=BillModel)
|
||||||
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
# def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||||
if instance.is_approved():
|
# if instance.is_approved():
|
||||||
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
# dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||||
recipients = (
|
# recipients = (
|
||||||
models.CustomGroup.objects.filter(dealer=dealer, name="Accountant")
|
# models.CustomGroup.objects.filter(dealer=dealer, name="Accountant")
|
||||||
.first()
|
# .first()
|
||||||
.group.user_set.exclude(email=dealer.user.email)
|
# .group.user_set.exclude(email=dealer.user.email)
|
||||||
.distinct()
|
# .distinct()
|
||||||
)
|
# )
|
||||||
|
|
||||||
for recipient in recipients:
|
# for recipient in recipients:
|
||||||
models.Notification.objects.create(
|
# models.Notification.objects.create(
|
||||||
user=recipient,
|
# user=recipient,
|
||||||
message=_(
|
# message=_(
|
||||||
"""
|
# """
|
||||||
Bill {bill_number} has been approved.
|
# Bill {bill_number} has been approved.
|
||||||
<a href="{url}" target="_blank">View</a>.
|
# <a href="{url}" target="_blank">View</a>.
|
||||||
please complete the bill payment.
|
# please complete the bill payment.
|
||||||
"""
|
# """
|
||||||
).format(
|
# ).format(
|
||||||
bill_number=instance.bill_number,
|
# bill_number=instance.bill_number,
|
||||||
url=reverse(
|
# url=reverse(
|
||||||
"bill-detail",
|
# "bill-detail",
|
||||||
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},
|
||||||
),
|
# ),
|
||||||
),
|
# ),
|
||||||
)
|
# )
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
from plans.models import Plan
|
from plans.models import Plan
|
||||||
|
from django.urls import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django_ledger.io import roles
|
from django_ledger.io import roles
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
@ -11,11 +13,11 @@ from django.utils.translation import activate
|
|||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from allauth.account.models import EmailAddress
|
from allauth.account.models import EmailAddress
|
||||||
from inventory.models import DealerSettings, Dealer
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.contrib.auth.models import User, Group, Permission
|
from django.contrib.auth.models import User, Group, Permission
|
||||||
|
from inventory.models import DealerSettings, Dealer,Schedule,Notification
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
@ -1166,7 +1168,7 @@ def create_user_dealer(email, password, name, arabic_name, phone, crn, vrn, addr
|
|||||||
):
|
):
|
||||||
group.permissions.add(perm)
|
group.permissions.add(perm)
|
||||||
|
|
||||||
StaffMember.objects.create(user=user)
|
# StaffMember.objects.create(user=user)
|
||||||
dealer = Dealer.objects.create(
|
dealer = Dealer.objects.create(
|
||||||
user=user,
|
user=user,
|
||||||
name=name,
|
name=name,
|
||||||
@ -1256,3 +1258,65 @@ def handle_email_result(task):
|
|||||||
logger.info(f"Email task succeeded: {task.result}")
|
logger.info(f"Email task succeeded: {task.result}")
|
||||||
else:
|
else:
|
||||||
logger.error(f"Email task failed: {task.result}")
|
logger.error(f"Email task failed: {task.result}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def send_schedule_reminder_email(schedule_id):
|
||||||
|
"""
|
||||||
|
Sends an email reminder for a specific schedule.
|
||||||
|
This function is designed to be called by django-q.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
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}.")
|
||||||
|
return
|
||||||
|
|
||||||
|
user_email = schedule.scheduled_by.email
|
||||||
|
Notification.objects.create(
|
||||||
|
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})),)
|
||||||
|
# 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
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}")
|
||||||
|
|
||||||
|
except Schedule.DoesNotExist:
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
This function will be called by django-q after the send_schedule_reminder_email task completes.
|
||||||
|
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}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Email task for Schedule ID {task.args[0]} failed. Error: {task.result}")
|
||||||
@ -1287,6 +1287,9 @@ urlpatterns = [
|
|||||||
path('feature/recall/<int:pk>/view/', views.RecallDetailView.as_view(), name='recall_detail'),
|
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/create/', views.RecallCreateView.as_view(), name='recall_create'),
|
||||||
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
|
path('feature/recall/success/', views.RecallSuccessView.as_view(), name='recall_success'),
|
||||||
|
|
||||||
|
path('<slug:dealer_slug>/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
handler404 = "inventory.views.custom_page_not_found_view"
|
handler404 = "inventory.views.custom_page_not_found_view"
|
||||||
|
|||||||
@ -186,7 +186,7 @@ def get_user_type(request):
|
|||||||
if request.is_dealer:
|
if request.is_dealer:
|
||||||
return request.user.dealer
|
return request.user.dealer
|
||||||
elif request.is_staff:
|
elif request.is_staff:
|
||||||
return request.user.staffmember.staff.dealer
|
return request.user.staff.dealer
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -1110,16 +1110,16 @@ class CarFinanceCalculator:
|
|||||||
"quantity": sum(
|
"quantity": sum(
|
||||||
self._get_quantity(item) for item in self.item_transactions
|
self._get_quantity(item) for item in self.item_transactions
|
||||||
),
|
),
|
||||||
"total_price": totals["total_price"],
|
"total_price": round(totals["total_price"], 2),
|
||||||
"total_price_discounted": totals["total_price_discounted"],
|
"total_price_discounted": round(totals["total_price_discounted"], 2),
|
||||||
"total_price_before_discount": totals["total_price_before_discount"],
|
"total_price_before_discount": round(totals["total_price_before_discount"], 2),
|
||||||
"total_vat": totals["total_vat_amount"] + totals["total_price"],
|
"total_vat": round(totals["total_vat_amount"] + totals["total_price"], 2),
|
||||||
"total_vat_amount": totals["total_vat_amount"],
|
"total_vat_amount": round(totals["total_vat_amount"], 2),
|
||||||
"total_discount": totals["total_discount"],
|
"total_discount": round(totals["total_discount"], 2),
|
||||||
"total_additionals": totals["total_additionals"],
|
"total_additionals": round(totals["total_additionals"], 2),
|
||||||
"grand_total": totals["grand_total"],
|
"grand_total": round(totals["grand_total"], 2),
|
||||||
"additionals": self._get_additional_services(),
|
"additionals": self._get_additional_services(),
|
||||||
"vat": self.vat_rate,
|
"vat": round(self.vat_rate, 2),
|
||||||
}
|
}
|
||||||
# class CarFinanceCalculator:
|
# class CarFinanceCalculator:
|
||||||
# """
|
# """
|
||||||
|
|||||||
@ -511,8 +511,8 @@ class SalesDashboard(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
dealer = get_user_type(self.request)
|
dealer = self.request.dealer
|
||||||
staff = getattr(self.request.user, "staff", None)
|
staff = self.request.staff
|
||||||
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
||||||
total_reservations = models.CarReservation.objects.filter(
|
total_reservations = models.CarReservation.objects.filter(
|
||||||
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
||||||
@ -2478,7 +2478,7 @@ class CustomerCreateView(
|
|||||||
customer = form.instance.create_customer_model()
|
customer = form.instance.create_customer_model()
|
||||||
|
|
||||||
form.instance.user = user
|
form.instance.user = user
|
||||||
form.instance.customer_model = customer
|
# form.instance.customer_model = customer
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -3417,7 +3417,7 @@ class UserCreateView(
|
|||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
email = form.cleaned_data["email"]
|
email = form.cleaned_data["email"]
|
||||||
if User.objects.filter(email=email).exists():
|
if models.Staff.objects.filter(dealer=dealer, user__email=email).exists():
|
||||||
messages.error(
|
messages.error(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
@ -3425,16 +3425,25 @@ class UserCreateView(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
return redirect("user_create", dealer_slug=dealer.slug)
|
return redirect("user_create", dealer_slug=dealer.slug)
|
||||||
|
|
||||||
password = "Tenhal@123"
|
password = "Tenhal@123"
|
||||||
|
|
||||||
user = User.objects.create_user(first_name=staff.first_name, last_name=staff.last_name, username=email, email=email, password=password)
|
user, created = User.objects.get_or_create(
|
||||||
|
email=email,
|
||||||
|
defaults={
|
||||||
|
"first_name": staff.first_name,
|
||||||
|
"last_name": staff.last_name,
|
||||||
|
"username": email,
|
||||||
|
"password": password,
|
||||||
|
},
|
||||||
|
)
|
||||||
user.is_staff = True
|
user.is_staff = True
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
staff_member = StaffMember.objects.create(user=user)
|
# staff_member, _ = StaffMember.objects.get_or_create(user=user)
|
||||||
for service in form.cleaned_data["service_offered"]:
|
# for service in form.cleaned_data["service_offered"]:
|
||||||
staff_member.services_offered.add(service)
|
# staff_member.services_offered.add(service)
|
||||||
staff.staff_member = staff_member
|
staff.user = user
|
||||||
staff.dealer = dealer
|
staff.dealer = dealer
|
||||||
staff.save()
|
staff.save()
|
||||||
self.staff_pk = staff.pk
|
self.staff_pk = staff.pk
|
||||||
@ -3495,7 +3504,7 @@ class UserUpdateView(
|
|||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
initial["email"] = self.object.staff_member.user.email
|
initial["email"] = self.object.user.email
|
||||||
initial["group"] = self.object.groups
|
initial["group"] = self.object.groups
|
||||||
|
|
||||||
return initial
|
return initial
|
||||||
@ -4350,7 +4359,7 @@ def sales_list_view(request, dealer_slug):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
staff = getattr(request.user.staffmember, "staff", None)
|
staff = getattr(request.user, "staff", None)
|
||||||
qs = []
|
qs = []
|
||||||
try:
|
try:
|
||||||
if any([request.is_dealer, request.is_manager, request.is_accountant]):
|
if any([request.is_dealer, request.is_manager, request.is_accountant]):
|
||||||
@ -4525,7 +4534,7 @@ def create_estimate(request, dealer_slug, slug=None):
|
|||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
title = data.get("title")
|
title = data.get("title")
|
||||||
customer_id = data.get("customer")
|
customer_id = data.get("customer")
|
||||||
customer = models.Customer.objects.filter(pk=int(customer_id)).first()
|
customer = models.Customer.objects.filter(pk=int(customer_id),dealer=dealer).first()
|
||||||
|
|
||||||
items = data.get("item", [])
|
items = data.get("item", [])
|
||||||
quantities = data.get("quantity", [])
|
quantities = data.get("quantity", [])
|
||||||
@ -5173,7 +5182,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||||
entity = dealer.entity
|
entity = dealer.entity
|
||||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
staff = getattr(self.request.user, "staff", None)
|
||||||
qs = []
|
qs = []
|
||||||
try:
|
try:
|
||||||
if any(
|
if any(
|
||||||
@ -5933,12 +5942,12 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
|||||||
)
|
)
|
||||||
context["transfer_form"] = forms.LeadTransferForm()
|
context["transfer_form"] = forms.LeadTransferForm()
|
||||||
context["transfer_form"].fields["transfer_to"].queryset = (
|
context["transfer_form"].fields["transfer_to"].queryset = (
|
||||||
models.Staff.objects.select_related("staff_member", "staff_member__user")
|
models.Staff.objects.select_related("user")
|
||||||
.filter(
|
.filter(
|
||||||
dealer=dealer,
|
dealer=dealer,
|
||||||
staff_member__user__groups__permissions__codename__contains="can_reassign_lead",
|
user__groups__permissions__codename__contains="can_reassign_lead",
|
||||||
)
|
)
|
||||||
.exclude(staff_member__user=self.request.user)
|
.exclude(user=self.request.user)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -6062,10 +6071,10 @@ def lead_create(request, dealer_slug):
|
|||||||
)
|
)
|
||||||
form.fields["staff"].queryset = (
|
form.fields["staff"].queryset = (
|
||||||
form.fields["staff"]
|
form.fields["staff"]
|
||||||
.queryset.select_related("staff_member", "staff_member__user")
|
.queryset.select_related("user")
|
||||||
.filter(
|
.filter(
|
||||||
dealer=dealer,
|
dealer=dealer,
|
||||||
staff_member__user__groups__permissions__codename__contains="add_lead",
|
user__groups__permissions__codename__contains="add_lead",
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
@ -6093,8 +6102,8 @@ def lead_create(request, dealer_slug):
|
|||||||
def lead_tracking(request, dealer_slug):
|
def lead_tracking(request, dealer_slug):
|
||||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
staff = (
|
staff = (
|
||||||
models.Staff.objects.select_related("staff_member", "staff_member__user")
|
models.Staff.objects.select_related("user")
|
||||||
.filter(dealer=dealer, staff_member__user=request.user)
|
.filter(dealer=dealer, user=request.user)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -6275,10 +6284,10 @@ class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|||||||
].queryset = form.instance.id_car_make.carmodel_set.all()
|
].queryset = form.instance.id_car_make.carmodel_set.all()
|
||||||
form.fields["staff"].queryset = (
|
form.fields["staff"].queryset = (
|
||||||
form.fields["staff"]
|
form.fields["staff"]
|
||||||
.queryset.select_related("staff_member", "staff_member__user")
|
.queryset.select_related("user")
|
||||||
.filter(
|
.filter(
|
||||||
dealer=dealer,
|
dealer=dealer,
|
||||||
staff_member__user__groups__permissions__codename__contains="add_lead",
|
user__groups__permissions__codename__contains="add_lead",
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
@ -6522,6 +6531,8 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
return redirect(f"{content_type}_detail", dealer_slug=dealer_slug, slug=slug)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
from django_q.models import Schedule as DjangoQSchedule
|
||||||
|
|
||||||
form = forms.ScheduleForm(request.POST)
|
form = forms.ScheduleForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
instance = form.save(commit=False)
|
instance = form.save(commit=False)
|
||||||
@ -6534,34 +6545,33 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
elif obj.organization:
|
elif obj.organization:
|
||||||
instance.cutsomer = obj.organization.customer_model
|
instance.cutsomer = obj.organization.customer_model
|
||||||
|
|
||||||
service = Service.objects.get(name=instance.scheduled_type)
|
# service = Service.objects.get(name=instance.scheduled_type)
|
||||||
# Log attempt to create AppointmentRequest
|
# # Log attempt to create AppointmentRequest
|
||||||
logger.debug(
|
# logger.debug(
|
||||||
f"User {user_username} attempting to create AppointmentRequest "
|
# f"User {user_username} attempting to create AppointmentRequest "
|
||||||
f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
# f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
||||||
f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
# f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
||||||
)
|
# )
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
appointment_request = AppointmentRequest.objects.create(
|
# appointment_request = AppointmentRequest.objects.create(
|
||||||
date=instance.scheduled_at.date(),
|
# date=instance.scheduled_at.date(),
|
||||||
start_time=instance.scheduled_at.time(),
|
|
||||||
end_time=(instance.scheduled_at + instance.duration).time(),
|
|
||||||
service=service,
|
|
||||||
staff_member=request.user.staffmember,
|
|
||||||
)
|
|
||||||
except ValidationError as e:
|
|
||||||
messages.error(request, str(e))
|
|
||||||
return redirect(request.META.get("HTTP_REFERER"))
|
|
||||||
|
|
||||||
client = get_object_or_404(User, email=instance.customer.email)
|
# service=service,
|
||||||
|
# staff_member=request.user.staffmember,
|
||||||
|
# )
|
||||||
|
# except ValidationError as e:
|
||||||
|
# messages.error(request, str(e))
|
||||||
|
# return redirect(request.META.get("HTTP_REFERER"))
|
||||||
|
|
||||||
|
# client = get_object_or_404(User, email=instance.customer.email)
|
||||||
# Create Appointment
|
# Create Appointment
|
||||||
Appointment.objects.create(
|
# Appointment.objects.create(
|
||||||
client=client,
|
# client=client,
|
||||||
appointment_request=appointment_request,
|
# appointment_request=appointment_request,
|
||||||
phone=instance.customer.phone,
|
# phone=instance.customer.phone,
|
||||||
address=instance.customer.address_1,
|
# address=instance.customer.address_1,
|
||||||
)
|
# )
|
||||||
|
|
||||||
instance.save()
|
instance.save()
|
||||||
models.Activity.objects.create(
|
models.Activity.objects.create(
|
||||||
@ -6571,11 +6581,19 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
|||||||
created_by=request.user,
|
created_by=request.user,
|
||||||
activity_type=instance.scheduled_type,
|
activity_type=instance.scheduled_type,
|
||||||
)
|
)
|
||||||
|
scheduled_at_aware = timezone.make_aware(instance.scheduled_at, timezone.get_current_timezone()) if timezone.is_naive(instance.scheduled_at) else instance.scheduled_at
|
||||||
|
|
||||||
# --- Logging for successful AppointmentRequest and Appointment creation ---
|
reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15)
|
||||||
logger.info(
|
# Only schedule if the reminder time is in the future
|
||||||
f"User {user_username} successfully scheduled {content_type} ID: {obj.pk} ('{obj.slug}'). "
|
# Reminder emails are scheduled to be sent 15 minutes before the scheduled time
|
||||||
f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}."
|
if reminder_time > timezone.now():
|
||||||
|
DjangoQSchedule.objects.create(
|
||||||
|
name=f"send_schedule_reminder_email_to_{instance.scheduled_by.email}_for_{content_type}_with_PK_{instance.pk}",
|
||||||
|
func='inventory.tasks.send_schedule_reminder_email',
|
||||||
|
args=f'"{instance.pk}"',
|
||||||
|
schedule_type=DjangoQSchedule.ONCE,
|
||||||
|
next_run=reminder_time,
|
||||||
|
hook='inventory.tasks.log_email_status',
|
||||||
)
|
)
|
||||||
messages.success(request, _("Appointment Created Successfully"))
|
messages.success(request, _("Appointment Created Successfully"))
|
||||||
|
|
||||||
@ -6913,7 +6931,7 @@ class OpportunityUpdateView(
|
|||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
dealer = get_object_or_404(models.Dealer, slug=self.kwargs.get("dealer_slug"))
|
||||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
staff = getattr(self.request.user, "staff", None)
|
||||||
form.fields["car"].queryset = models.Car.objects.filter(
|
form.fields["car"].queryset = models.Car.objects.filter(
|
||||||
dealer=dealer, status="available", finances__marked_price__gt=0
|
dealer=dealer, status="available", finances__marked_price__gt=0
|
||||||
)
|
)
|
||||||
@ -9844,12 +9862,12 @@ def management_view(request, dealer_slug):
|
|||||||
@login_required
|
@login_required
|
||||||
@permission_required("inventory.change_dealer", raise_exception=True)
|
@permission_required("inventory.change_dealer", raise_exception=True)
|
||||||
def user_management(request, dealer_slug):
|
def user_management(request, dealer_slug):
|
||||||
get_object_or_404(models.Dealer, slug=dealer_slug)
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
context = {
|
context = {
|
||||||
"customers": models.Customer.objects.filter(active=False),
|
"customers": models.Customer.objects.filter(active=False,dealer=dealer),
|
||||||
"organizations": models.Organization.objects.filter(active=False),
|
"organizations": models.Organization.objects.filter(active=False,dealer=dealer),
|
||||||
"vendors": models.Vendor.objects.filter(active=False),
|
"vendors": models.Vendor.objects.filter(active=False,dealer=dealer),
|
||||||
"staff": models.Staff.objects.filter(active=False),
|
"staff": models.Staff.objects.filter(active=False,dealer=dealer),
|
||||||
}
|
}
|
||||||
return render(request, "admin_management/user_management.html", context)
|
return render(request, "admin_management/user_management.html", context)
|
||||||
|
|
||||||
@ -11049,3 +11067,15 @@ class RecallCreateView(FormView):
|
|||||||
|
|
||||||
class RecallSuccessView(TemplateView):
|
class RecallSuccessView(TemplateView):
|
||||||
template_name = 'recalls/recall_success.html'
|
template_name = 'recalls/recall_success.html'
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def schedule_calendar(request,dealer_slug):
|
||||||
|
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||||
|
user_schedules = models.Schedule.objects.filter(dealer=dealer,scheduled_by=request.user).order_by('scheduled_at')
|
||||||
|
upcoming_schedules = user_schedules.filter(scheduled_at__gte=timezone.now()).order_by('scheduled_at')
|
||||||
|
context = {
|
||||||
|
'schedules': user_schedules,
|
||||||
|
'upcoming_schedules':upcoming_schedules
|
||||||
|
}
|
||||||
|
return render(request, 'schedule_calendar.html', context)
|
||||||
18
pg-compose.yml
Normal file
18
pg-compose.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: dev_db
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: dev_database
|
||||||
|
POSTGRES_USER: dev_user
|
||||||
|
POSTGRES_PASSWORD: dev_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- dev_postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
dev_postgres_data:
|
||||||
105
static/css/calendar_dark.css
Normal file
105
static/css/calendar_dark.css
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #4299e1;
|
||||||
|
border-color: #4299e1;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #202c3c !important;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #38a169 !important;
|
||||||
|
border-color: #38a169 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event colors (you can adjust these in your Django template) */
|
||||||
|
/* .fc-event-completed { background-color: #38a169; border-color: #38a169; } */
|
||||||
|
/* .fc-event-canceled { background-color: #e53e3e; border-color: #e53e3e; } */
|
||||||
|
/* .fc-event-scheduled { background-color: #4299e1; border-color: #4299e1; } */
|
||||||
|
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #4a5568;
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
static/css/calendar_light.css
Normal file
99
static/css/calendar_light.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* static/css/light_theme.css */
|
||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #e2e6ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
border-color: #ffeeba !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #e0e6ed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
static/images/logos/vendors/logo1.jpg
vendored
Normal file
BIN
static/images/logos/vendors/logo1.jpg
vendored
Normal file
Binary file not shown.
105
staticfiles/css/calendar_dark.css
Normal file
105
staticfiles/css/calendar_dark.css
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #4299e1;
|
||||||
|
border-color: #4299e1;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #202c3c !important;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #38a169 !important;
|
||||||
|
border-color: #38a169 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event colors (you can adjust these in your Django template) */
|
||||||
|
/* .fc-event-completed { background-color: #38a169; border-color: #38a169; } */
|
||||||
|
/* .fc-event-canceled { background-color: #e53e3e; border-color: #e53e3e; } */
|
||||||
|
/* .fc-event-scheduled { background-color: #4299e1; border-color: #4299e1; } */
|
||||||
|
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #4a5568;
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
staticfiles/css/calendar_light.css
Normal file
99
staticfiles/css/calendar_light.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* static/css/light_theme.css */
|
||||||
|
|
||||||
|
/* Card and container styling */
|
||||||
|
.card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FullCalendar header */
|
||||||
|
.fc .fc-toolbar.fc-header-toolbar {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar buttons */
|
||||||
|
.fc .fc-button-group > .fc-button {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-group > .fc-button:hover {
|
||||||
|
background-color: #e2e6ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc .fc-button-primary:not(:disabled).fc-button-active,
|
||||||
|
.fc .fc-button-primary:not(:disabled):active {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Day cells */
|
||||||
|
.fc-daygrid-day {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e0e6ed;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-other {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
color: #ced4da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-day-today {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
border-color: #ffeeba !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fc-daygrid-day-number {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event styling */
|
||||||
|
.fc-event {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-shadow: 0 1px 1px rgba(0,0,0,0.2);
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List group styling */
|
||||||
|
.list-group-item {
|
||||||
|
border-color: #e0e6ed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .close {
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
.fc .fc-toolbar-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -132,3 +132,45 @@ html[dir="rtl"] .form-icon-container .form-control {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#spinner {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spinner-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 500ms ease-in;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spinner-bg.htmx-request {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* .fade-me-in.htmx-added {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.fade-me-in {
|
||||||
|
opacity: .9;
|
||||||
|
transition: opacity 300ms ease-out;
|
||||||
|
} */
|
||||||
|
|
||||||
|
#main_content.fade-me-in:not(.modal):not(.modal *) {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 300ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main_content.fade-me-in.htmx-added:not(.modal):not(.modal *) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
staticfiles/images/default-image/user.jpg
Normal file
BIN
staticfiles/images/default-image/user.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/no-content-new.jpg
Normal file
BIN
staticfiles/images/logos/no-content-new.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/no-content-new1.jpg
Normal file
BIN
staticfiles/images/logos/no-content-new1.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/no-content-new2.jpg
Normal file
BIN
staticfiles/images/logos/no-content-new2.jpg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
staticfiles/images/logos/users/customer2.jpg
Normal file
BIN
staticfiles/images/logos/users/customer2.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/dealer.png
Normal file
BIN
staticfiles/images/logos/users/dealer.png
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/dealer_default.png
Normal file
BIN
staticfiles/images/logos/users/dealer_default.png
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/employee.png
Normal file
BIN
staticfiles/images/logos/users/employee.png
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/new_dealer.png
Normal file
BIN
staticfiles/images/logos/users/new_dealer.png
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/output_2.jpg
Normal file
BIN
staticfiles/images/logos/users/output_2.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/salesperson.png
Normal file
BIN
staticfiles/images/logos/users/salesperson.png
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/users/user-logo.jpg
Normal file
BIN
staticfiles/images/logos/users/user-logo.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png
vendored
Normal file
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png
vendored
Normal file
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png
vendored
Normal file
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/logo1.jpg
vendored
Normal file
BIN
staticfiles/images/logos/vendors/logo1.jpg
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/output_2.jpg
vendored
Normal file
BIN
staticfiles/images/logos/vendors/output_2.jpg
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg
vendored
Normal file
BIN
staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/output_4.jpg
vendored
Normal file
BIN
staticfiles/images/logos/vendors/output_4.jpg
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/output_4_owyivsr.jpg
vendored
Normal file
BIN
staticfiles/images/logos/vendors/output_4_owyivsr.jpg
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/salesperson.png
vendored
Normal file
BIN
staticfiles/images/logos/vendors/salesperson.png
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/vendor.png
vendored
Normal file
BIN
staticfiles/images/logos/vendors/vendor.png
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/logos/vendors/vnd.png
vendored
Normal file
BIN
staticfiles/images/logos/vendors/vnd.png
vendored
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_car.png
Normal file
BIN
staticfiles/images/no_content/no_car.png
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_estimate.jpg
Normal file
BIN
staticfiles/images/no_content/no_estimate.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_item.jpg
Normal file
BIN
staticfiles/images/no_content/no_item.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_plan.jpg
Normal file
BIN
staticfiles/images/no_content/no_plan.jpg
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_search_results.png
Normal file
BIN
staticfiles/images/no_content/no_search_results.png
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_user.png
Normal file
BIN
staticfiles/images/no_content/no_user.png
Normal file
Binary file not shown.
BIN
staticfiles/images/no_content/no_vendor.png
Normal file
BIN
staticfiles/images/no_content/no_vendor.png
Normal file
Binary file not shown.
@ -249,3 +249,46 @@ const getDataTableInit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Register delete modal initializer
|
||||||
|
htmxInitializer.register(function initDeleteModals() {
|
||||||
|
const deleteModal = document.getElementById("deleteModal");
|
||||||
|
const confirmDeleteBtn = document.getElementById("deleteModalConfirm");
|
||||||
|
const deleteModalMessage = document.getElementById("deleteModalText");
|
||||||
|
|
||||||
|
// Clean up old listeners
|
||||||
|
document.querySelectorAll(".delete-btn").forEach(btn => {
|
||||||
|
btn.removeEventListener("click", handleDeleteClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new listeners
|
||||||
|
document.querySelectorAll(".delete-btn").forEach(button => {
|
||||||
|
button.addEventListener("click", handleDeleteClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleDeleteClick() {
|
||||||
|
if (!deleteModal || !confirmDeleteBtn || !deleteModalMessage) return;
|
||||||
|
|
||||||
|
const deleteUrl = this.getAttribute("data-url");
|
||||||
|
const deleteMessage = this.getAttribute("data-message") || "Are you sure?";
|
||||||
|
|
||||||
|
confirmDeleteBtn.setAttribute("href", deleteUrl);
|
||||||
|
deleteModalMessage.textContent = deleteMessage;
|
||||||
|
|
||||||
|
if (typeof htmx !== 'undefined') htmx.process(confirmDeleteBtn);
|
||||||
|
if (typeof bootstrap !== 'undefined') new bootstrap.Modal(deleteModal).show();
|
||||||
|
}
|
||||||
|
}, "delete_modals");
|
||||||
|
|
||||||
|
// Register custom selects initializer
|
||||||
|
htmxInitializer.register(function initCustomSelects() {
|
||||||
|
// Your custom select initialization code
|
||||||
|
}, "custom_selects");
|
||||||
|
|
||||||
|
// Register form submission initializer
|
||||||
|
htmxInitializer.register(function initForms() {
|
||||||
|
// Your form handling code
|
||||||
|
}, "forms");
|
||||||
|
*/
|
||||||
1
staticfiles/spinner.svg
Normal file
1
staticfiles/spinner.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='25' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='-.4'></animate></rect><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='85' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='-.2'></animate></rect><rect fill='#09577F' stroke='#09577F' stroke-width='15' width='30' height='30' x='145' y='85'><animate attributeName='opacity' calcMode='spline' dur='2' values='1;0;1;' keySplines='.5 0 .5 1;.5 0 .5 1' repeatCount='indefinite' begin='0'></animate></rect></svg>
|
||||||
BIN
staticfiles/user-logo.jpg
Normal file
BIN
staticfiles/user-logo.jpg
Normal file
Binary file not shown.
BIN
staticfiles/user-logo1.png
Normal file
BIN
staticfiles/user-logo1.png
Normal file
Binary file not shown.
@ -201,25 +201,25 @@
|
|||||||
<div class="d-flex flex-wrap gap-2 mt-2">
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
<!-- Update Button -->
|
<!-- Update Button -->
|
||||||
{% if perms.django_ledger.change_billmodel %}
|
{% 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 %}">
|
<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"
|
<button class="btn btn-phoenix-primary">
|
||||||
{% if not request.is_accountant %}disabled{% endif %}>
|
|
||||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if "detail" not in request.path %}
|
{% if "detail" not in request.path %}
|
||||||
<!-- Mark as Draft -->
|
<!-- Mark as Draft -->
|
||||||
{% if bill.can_draft %}
|
{% if bill.can_draft %}
|
||||||
<button class="btn btn-phoenix-success"
|
<button class="btn btn-phoenix-success"
|
||||||
{% if not request.is_accountant %}disabled{% endif %}
|
|
||||||
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' dealer_slug=request.dealer.slug entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
|
||||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Mark as Review -->
|
<!-- Mark as Review -->
|
||||||
{% if bill.can_review %}
|
{% if bill.can_review %}
|
||||||
|
{{request.is_accountant}}
|
||||||
<button class="btn btn-phoenix-warning"
|
<button class="btn btn-phoenix-warning"
|
||||||
{% if not request.is_accountant %}disabled{% endif %}
|
|
||||||
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')">
|
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' %}
|
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
|
||||||
</button>
|
</button>
|
||||||
@ -239,7 +239,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Mark as Paid -->
|
<!-- Mark as Paid -->
|
||||||
{% if "detail" not in request.path %}
|
|
||||||
{% if bill.can_pay %}
|
{% if bill.can_pay %}
|
||||||
<button class="btn btn-phoenix-success"
|
<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')">
|
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')">
|
||||||
@ -256,7 +255,6 @@
|
|||||||
<!-- Cancel Button -->
|
<!-- Cancel Button -->
|
||||||
{% if bill.can_cancel %}
|
{% if bill.can_cancel %}
|
||||||
<button class="btn btn-phoenix-danger"
|
<button class="btn btn-phoenix-danger"
|
||||||
{% if not request.is_accountant %}disabled{% endif %}
|
|
||||||
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')">
|
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' %}
|
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
32
templates/emails/schedule_reminder.html
Normal file
32
templates/emails/schedule_reminder.html
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!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>This is a friendly reminder for your upcoming schedule:</p>
|
||||||
|
<p>
|
||||||
|
<span class="highlight">Purpose:</span> {{ schedule_purpose }}<br>
|
||||||
|
<span class="highlight">Scheduled At:</span> {{ scheduled_at }}<br>
|
||||||
|
<span class="highlight">Type:</span> {{ schedule_type }}<br>
|
||||||
|
{% if customer_name != 'N/A' %}<span class="highlight">Customer:</span> {{ customer_name }}<br>{% endif %}
|
||||||
|
{% if notes %}<span class="highlight">Notes:</span> {{ notes }}<br>{% endif %}
|
||||||
|
</p>
|
||||||
|
<p>Please be prepared for your schedule.</p>
|
||||||
|
<p>Thank you!</p>
|
||||||
|
<div class="footer">
|
||||||
|
<p>This is an automated reminder. Please do not reply to this email.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
templates/emails/schedule_reminder.txt
Normal file
16
templates/emails/schedule_reminder.txt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
Hello {{ user_name }},
|
||||||
|
|
||||||
|
This is a friendly reminder for your upcoming schedule:
|
||||||
|
|
||||||
|
Purpose: {{ schedule_purpose }}
|
||||||
|
Scheduled At: {{ scheduled_at }}
|
||||||
|
Type: {{ schedule_type }}
|
||||||
|
{% if customer_name != 'N/A' %}Customer: {{ customer_name }}{% endif %}
|
||||||
|
{% if notes %}Notes: {{ notes }}{% endif %}
|
||||||
|
|
||||||
|
Please be prepared for your schedule.
|
||||||
|
|
||||||
|
Thank you!
|
||||||
|
|
||||||
|
---
|
||||||
|
This is an automated reminder. Please do not reply to this email.
|
||||||
@ -59,7 +59,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<p>
|
<p>
|
||||||
<strong>{{ _("Name") }}:</strong> {{ user.staffmember.staff }}
|
<strong>{{ _("Name") }}:</strong> {{ user.staff }}
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<script src="{% static 'vendors/zxing/index.min.js' %}"></script>
|
<script src="{% static 'vendors/zxing/index.min.js' %}"></script>
|
||||||
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
|
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
|
||||||
{% if not vendor_exists %}
|
{% if not vendor_exists %}
|
||||||
{% url "user_create" request.dealer.slug as create_vendor_url %}
|
{% url "vendor_create" request.dealer.slug as create_vendor_url %}
|
||||||
{% include "empty-illustration-page.html" with value="Vendor" url=create_vendor_url %}
|
{% include "empty-illustration-page.html" with value="Vendor" url=create_vendor_url %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!---->
|
<!---->
|
||||||
|
|||||||
233
templates/schedule_calendar.html
Normal file
233
templates/schedule_calendar.html
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block customCSS %}
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
|
||||||
|
<link id="theme-link" rel="stylesheet" href="{% static 'css/calendar_dark.css' %}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.schedule-list {
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock customCSS %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4>Upcoming Schedules</h4>
|
||||||
|
{% comment %} <button class="btn btn-phoenix-primary btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#scheduleModal">
|
||||||
|
<span class="fas fa-plus me-1"></span>{{ _("Add New Schedule") }}
|
||||||
|
</button> {% endcomment %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body schedule-list">
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for schedule in upcoming_schedules %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1">{{ schedule.get_purpose }}</h6>
|
||||||
|
<p class="mb-1 text-muted">{{ schedule.scheduled_at|date:"F d, Y P" }}</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-info btn-sm view-schedule-btn" data-toggle="modal" data-target="#viewScheduleModal"
|
||||||
|
data-purpose="{{ schedule.get_purpose }}"
|
||||||
|
data-scheduled-type="{{ schedule.scheduled_type }}"
|
||||||
|
data-scheduled-at="{{ schedule.scheduled_at }}"
|
||||||
|
data-start-time="{{ schedule.start_time }}"
|
||||||
|
data-end-time="{{ schedule.end_time }}"
|
||||||
|
data-completed="{{ schedule.completed }}"
|
||||||
|
data-status="{{ schedule.status }}"
|
||||||
|
data-notes="{{ schedule.notes }}"
|
||||||
|
data-customer="{% if schedule.customer %}{{ schedule.customer.customer_name }}{% else %}N/A{% endif %}"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{% empty %}
|
||||||
|
<li class="list-group-item text-center text-muted">No schedules found.</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="addScheduleModal" tabindex="-1" role="dialog" aria-labelledby="addScheduleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addScheduleModalLabel">Add New Schedule</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form action="{% url 'schedule_calendar' request.dealer.slug %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit" class="btn btn-primary">Save Schedule</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="viewScheduleModal" tabindex="-1" role="dialog" aria-labelledby="viewScheduleModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="viewScheduleTitle">Schedule Details</h5>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p><strong>Purpose:</strong> <span id="modal-schedule-purpose"></span></p>
|
||||||
|
<p><strong>Scheduled Type:</strong> <span id="modal-schedule-type"></span></p>
|
||||||
|
<p><strong>Customer:</strong> <span id="modal-schedule-customer"></span></p>
|
||||||
|
<p><strong>Scheduled At:</strong> <span id="modal-schedule-at"></span></p>
|
||||||
|
<p><strong>Time:</strong> <span id="modal-schedule-time"></span></p>
|
||||||
|
<p><strong>Status:</strong> <span id="modal-schedule-status"></span></p>
|
||||||
|
<p><strong>Notes:</strong> <span id="modal-schedule-notes"></span></p>
|
||||||
|
<p><strong>Completed:</strong> <span id="modal-schedule-completed"></span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
{% block customJS %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const themeToggle = document.getElementById('themeControlToggleSm');
|
||||||
|
const themeLink = document.getElementById('theme-link');
|
||||||
|
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
|
||||||
|
function setTheme(theme) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
themeLink.href = '{% static "css/dark_theme.css" %}';
|
||||||
|
themeToggle.checked = true;
|
||||||
|
} else {
|
||||||
|
themeLink.href = '{% static "css/light_theme.css" %}';
|
||||||
|
themeToggle.checked = false;
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the initial theme on page load
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// Listen for changes on the toggle switch
|
||||||
|
themeToggle.addEventListener('change', function() {
|
||||||
|
if (this.checked) {
|
||||||
|
setTheme('dark');
|
||||||
|
} else {
|
||||||
|
setTheme('light');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var calendarEl = document.getElementById('calendar');
|
||||||
|
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
themeSystem: 'bootstrap',
|
||||||
|
locale: '{{ LANGUAGE_CODE }}',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
},
|
||||||
|
events: [
|
||||||
|
{% for schedule in schedules %}
|
||||||
|
{
|
||||||
|
title: '{{ schedule.get_purpose|escapejs }}',
|
||||||
|
start: '{{ schedule.scheduled_at|date:"Y-m-d" }}{% if schedule.start_time %}T{{ schedule.start_time|time:"H:i:s" }}{% endif %}',
|
||||||
|
end: '{{ schedule.scheduled_at|date:"Y-m-d" }}{% if schedule.end_time %}T{{ schedule.end_time|time:"H:i:s" }}{% endif %}',
|
||||||
|
backgroundColor: '{% if schedule.status == "completed" %}#1abc9c{% elif schedule.status == "canceled" %}#e74c3c{% else %}#3498db{% endif %}',
|
||||||
|
borderColor: '{% if schedule.status == "completed" %}#1abc9c{% elif schedule.status == "canceled" %}#e74c3c{% else %}#3498db{% endif %}',
|
||||||
|
extendedProps: {
|
||||||
|
scheduledType: '{{ schedule.scheduled_type|escapejs }}',
|
||||||
|
notes: '{{ schedule.notes|escapejs }}',
|
||||||
|
completed: '{{ schedule.completed }}',
|
||||||
|
status: '{{ schedule.status|escapejs }}',
|
||||||
|
customer: '{{ schedule.customer.customer_name|default:"N/A"|escapejs }}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
eventClick: function(info) {
|
||||||
|
// Populate the view modal with schedule details
|
||||||
|
$('#viewScheduleTitle').text(info.event.title);
|
||||||
|
$('#modal-schedule-purpose').text(info.event.title);
|
||||||
|
$('#modal-schedule-type').text(info.event.extendedProps.scheduledType);
|
||||||
|
$('#modal-schedule-customer').text(info.event.extendedProps.customer);
|
||||||
|
$('#modal-schedule-at').text(info.event.startStr.split('T')[0]);
|
||||||
|
|
||||||
|
let startTime = info.event.startStr.split('T')[1];
|
||||||
|
let endTime = info.event.endStr ? info.event.endStr.split('T')[1] : '';
|
||||||
|
|
||||||
|
let timeString = '';
|
||||||
|
if (startTime) {
|
||||||
|
timeString += startTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
timeString += ' - ' + endTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#modal-schedule-time').text(timeString);
|
||||||
|
$('#modal-schedule-status').text(info.event.extendedProps.status);
|
||||||
|
$('#modal-schedule-notes').text(info.event.extendedProps.notes);
|
||||||
|
$('#modal-schedule-completed').text(info.event.extendedProps.completed === 'True' ? 'Yes' : 'No');
|
||||||
|
|
||||||
|
$('#viewScheduleModal').modal('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calendar.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle view schedule buttons in the list
|
||||||
|
$('.view-schedule-btn').on('click', function() {
|
||||||
|
const purpose = $(this).data('purpose');
|
||||||
|
const scheduledType = $(this).data('scheduled-type');
|
||||||
|
const customer = $(this).data('customer');
|
||||||
|
const scheduledAt = $(this).data('scheduled-at');
|
||||||
|
const startTime = $(this).data('start-time');
|
||||||
|
const endTime = $(this).data('end-time');
|
||||||
|
const status = $(this).data('status');
|
||||||
|
const notes = $(this).data('notes');
|
||||||
|
const completed = $(this).data('completed');
|
||||||
|
|
||||||
|
$('#viewScheduleTitle').text(purpose);
|
||||||
|
$('#modal-schedule-purpose').text(purpose);
|
||||||
|
$('#modal-schedule-type').text(scheduledType);
|
||||||
|
$('#modal-schedule-customer').text(customer);
|
||||||
|
$('#modal-schedule-at').text(scheduledAt);
|
||||||
|
|
||||||
|
let timeString = '';
|
||||||
|
if (startTime) {
|
||||||
|
timeString += startTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
if (endTime) {
|
||||||
|
if (timeString) {
|
||||||
|
timeString += ' - ';
|
||||||
|
}
|
||||||
|
timeString += endTime.substring(0, 5);
|
||||||
|
}
|
||||||
|
$('#modal-schedule-time').text(timeString);
|
||||||
|
$('#modal-schedule-status').text(status);
|
||||||
|
$('#modal-schedule-notes').text(notes);
|
||||||
|
$('#modal-schedule-completed').text(completed === true ? 'Yes' : 'No');
|
||||||
|
|
||||||
|
$('#viewScheduleModal').modal('show');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock customJS %}
|
||||||
Loading…
x
Reference in New Issue
Block a user