implement the appointment and calendar
@ -1179,10 +1179,16 @@ class ScheduleForm(forms.ModelForm):
|
||||
"purpose",
|
||||
"scheduled_type",
|
||||
"scheduled_at",
|
||||
"duration",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"notes",
|
||||
]
|
||||
|
||||
widgets = {
|
||||
"start_time": forms.TimeInput(attrs={"type": "time"}),
|
||||
"end_time": forms.TimeInput(attrs={"type": "time"}),
|
||||
}
|
||||
|
||||
|
||||
class NoteForm(forms.ModelForm):
|
||||
"""
|
||||
|
||||
@ -69,12 +69,12 @@ class Command(BaseCommand):
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
|
||||
staff_member = StaffMember.objects.create(user=user)
|
||||
services = Service.objects.all()
|
||||
for service in services:
|
||||
staff_member.services_offered.add(service)
|
||||
# staff_member = StaffMember.objects.create(user=user)
|
||||
# services = Service.objects.all()
|
||||
# for service in services:
|
||||
# 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)
|
||||
random_group = random.choice(list(groups))
|
||||
|
||||
@ -108,13 +108,13 @@ class InjectDealerMiddleware:
|
||||
request.is_dealer = True
|
||||
request.dealer = request.user.dealer
|
||||
|
||||
elif hasattr(request.user, "staffmember"):
|
||||
request.is_staff = True
|
||||
request.staff = request.user.staffmember.staff
|
||||
elif hasattr(request.user, "staff"):
|
||||
request.staff = getattr(request.user, "staff")
|
||||
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:
|
||||
request.is_accountant = True
|
||||
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.models import ContentType
|
||||
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.models import UserPlan
|
||||
from django.db.models import Q
|
||||
@ -1277,8 +1277,11 @@ class StaffTypes(models.TextChoices):
|
||||
|
||||
|
||||
class Staff(models.Model, LocalizedNameMixin):
|
||||
staff_member = models.OneToOneField(
|
||||
StaffMember, on_delete=models.CASCADE, related_name="staff"
|
||||
# staff_member = models.OneToOneField(
|
||||
# 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")
|
||||
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
|
||||
@ -1338,17 +1341,17 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
self.save()
|
||||
|
||||
def permenant_delete(self):
|
||||
# self.user.delete()
|
||||
self.staff_member.delete()
|
||||
self.user.delete()
|
||||
# self.staff_member.delete()
|
||||
self.delete()
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
return self.staff_member.user.email
|
||||
return self.user.email
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.staff_member.user
|
||||
# @property
|
||||
# def user(self):
|
||||
# return self.staff_member.user
|
||||
|
||||
@property
|
||||
def groups(self):
|
||||
@ -1388,6 +1391,12 @@ class Staff(models.Model, LocalizedNameMixin):
|
||||
models.Index(fields=["staff_type"]),
|
||||
]
|
||||
permissions = []
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['dealer', 'user'],
|
||||
name='unique_staff_email_per_dealer'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
@ -1504,7 +1513,7 @@ class Customer(models.Model):
|
||||
verbose_name=_("Gender"),
|
||||
)
|
||||
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(
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['dealer', 'email'],
|
||||
name='unique_customer_email_per_dealer'
|
||||
)
|
||||
]
|
||||
verbose_name = _("Customer")
|
||||
verbose_name_plural = _("Customers")
|
||||
indexes = [
|
||||
@ -1566,6 +1581,8 @@ class Customer(models.Model):
|
||||
|
||||
def create_customer_model(self, for_lead=False):
|
||||
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(
|
||||
commit=False,
|
||||
customer_model_kwargs={
|
||||
@ -1608,15 +1625,17 @@ class Customer(models.Model):
|
||||
return customer
|
||||
|
||||
def create_user_model(self, for_lead=False):
|
||||
user = User.objects.create_user(
|
||||
user, created = User.objects.get_or_create(
|
||||
username=self.email,
|
||||
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,
|
||||
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,
|
||||
},
|
||||
)
|
||||
self.user = user
|
||||
self.save()
|
||||
@ -2057,11 +2076,12 @@ class Schedule(models.Model):
|
||||
scheduled_by = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
purpose = models.CharField(max_length=200, choices=PURPOSE_CHOICES)
|
||||
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(
|
||||
max_length=200, choices=ScheduledType, default="Call"
|
||||
)
|
||||
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
|
||||
duration = models.DurationField(default=timedelta(minutes=5))
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
status = models.CharField(
|
||||
max_length=200, choices=ScheduleStatusChoices, default="Scheduled"
|
||||
@ -2073,11 +2093,17 @@ class Schedule(models.Model):
|
||||
return f"Scheduled {self.purpose} on {self.scheduled_at}"
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
return (self.end_time - self.start_time).seconds
|
||||
@property
|
||||
def schedule_past_date(self):
|
||||
if self.scheduled_at < now():
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def get_purpose(self):
|
||||
return self.purpose.replace("_", " ").title()
|
||||
class Meta:
|
||||
ordering = ["-scheduled_at"]
|
||||
verbose_name = _("Schedule")
|
||||
@ -2319,6 +2345,8 @@ class Tasks(models.Model):
|
||||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||||
description = models.TextField(verbose_name=_("Description"), null=True, blank=True)
|
||||
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"))
|
||||
assigned_to = models.ForeignKey(
|
||||
User,
|
||||
|
||||
@ -298,79 +298,79 @@ def update_item_model_cost(sender, instance, created, **kwargs):
|
||||
:return: None
|
||||
"""
|
||||
# if created and not instance.is_sold:
|
||||
if created:
|
||||
entity = instance.car.dealer.entity
|
||||
coa = entity.get_default_coa()
|
||||
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 = (
|
||||
# if created:
|
||||
# entity = instance.car.dealer.entity
|
||||
# coa = entity.get_default_coa()
|
||||
# inventory_account = (
|
||||
# entity.get_all_accounts()
|
||||
# .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
||||
# .filter(name=f"Inventory:{instance.car.id_car_make.name}")
|
||||
# .first()
|
||||
entity.get_all_accounts()
|
||||
.filter(role=roles.ASSET_CA_CASH, role_default=True)
|
||||
.first()
|
||||
)
|
||||
# )
|
||||
# if not inventory_account:
|
||||
# inventory_account = create_make_accounts(
|
||||
# entity,
|
||||
# coa,
|
||||
# [instance.car.id_car_make],
|
||||
# "Inventory",
|
||||
# roles.ASSET_CA_INVENTORY,
|
||||
# "debit",
|
||||
# )
|
||||
|
||||
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="",
|
||||
)
|
||||
# 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",
|
||||
# )
|
||||
|
||||
TransactionModel.objects.create(
|
||||
journal_entry=je,
|
||||
account=cash_account,
|
||||
amount=Decimal(instance.cost_price),
|
||||
tx_type="credit",
|
||||
description="",
|
||||
)
|
||||
# cash_account = (
|
||||
# # entity.get_all_accounts()
|
||||
# # .filter(name="Cash", role=roles.ASSET_CA_CASH)
|
||||
# # .first()
|
||||
# entity.get_all_accounts()
|
||||
# .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
|
||||
# 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)
|
||||
def add_service_to_staff(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
for service in Service.objects.all():
|
||||
instance.staff_member.services_offered.add(service)
|
||||
# @receiver(post_save, sender=models.Staff)
|
||||
# def add_service_to_staff(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# for service in Service.objects.all():
|
||||
# instance.services_offered.add(service)
|
||||
|
||||
|
||||
##########################################################
|
||||
@ -1026,10 +1026,11 @@ def po_fullfilled_notification(sender, instance, created, **kwargs):
|
||||
user=recipient,
|
||||
message=_(
|
||||
"""
|
||||
New Purchase Order has been added.
|
||||
PO {po_number} has been fulfilled.
|
||||
<a href="{url}" target="_blank">View</a>
|
||||
"""
|
||||
).format(
|
||||
po_number=instance.po_number,
|
||||
url=reverse(
|
||||
"purchase_order_detail",
|
||||
kwargs={"dealer_slug": dealer.slug,"entity_slug":instance.entity.slug, "pk": instance.pk},
|
||||
@ -1187,31 +1188,31 @@ def bill_model_in_approve_notification(sender, instance, created, **kwargs):
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=BillModel)
|
||||
def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||
if instance.is_approved():
|
||||
dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||
recipients = (
|
||||
models.CustomGroup.objects.filter(dealer=dealer, name="Accountant")
|
||||
.first()
|
||||
.group.user_set.exclude(email=dealer.user.email)
|
||||
.distinct()
|
||||
)
|
||||
# @receiver(post_save, sender=BillModel)
|
||||
# def bill_model_after_approve_notification(sender, instance, created, **kwargs):
|
||||
# if instance.is_approved():
|
||||
# dealer = models.Dealer.objects.get(entity=instance.ledger.entity)
|
||||
# recipients = (
|
||||
# models.CustomGroup.objects.filter(dealer=dealer, name="Accountant")
|
||||
# .first()
|
||||
# .group.user_set.exclude(email=dealer.user.email)
|
||||
# .distinct()
|
||||
# )
|
||||
|
||||
for recipient in recipients:
|
||||
models.Notification.objects.create(
|
||||
user=recipient,
|
||||
message=_(
|
||||
"""
|
||||
Bill {bill_number} has been approved.
|
||||
<a href="{url}" target="_blank">View</a>.
|
||||
please complete the bill payment.
|
||||
"""
|
||||
).format(
|
||||
bill_number=instance.bill_number,
|
||||
url=reverse(
|
||||
"bill-detail",
|
||||
kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
||||
),
|
||||
),
|
||||
)
|
||||
# for recipient in recipients:
|
||||
# models.Notification.objects.create(
|
||||
# user=recipient,
|
||||
# message=_(
|
||||
# """
|
||||
# Bill {bill_number} has been approved.
|
||||
# <a href="{url}" target="_blank">View</a>.
|
||||
# please complete the bill payment.
|
||||
# """
|
||||
# ).format(
|
||||
# bill_number=instance.bill_number,
|
||||
# url=reverse(
|
||||
# "bill-detail",
|
||||
# kwargs={"dealer_slug": dealer.slug, "entity_slug": dealer.entity.slug, "bill_pk": instance.pk},
|
||||
# ),
|
||||
# ),
|
||||
# )
|
||||
@ -1,7 +1,9 @@
|
||||
import base64
|
||||
import logging
|
||||
from plans.models import Plan
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.db import transaction
|
||||
from django_ledger.io import roles
|
||||
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.contrib.auth import get_user_model
|
||||
from allauth.account.models import EmailAddress
|
||||
from inventory.models import DealerSettings, Dealer
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
from inventory.models import DealerSettings, Dealer,Schedule,Notification
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
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)
|
||||
|
||||
StaffMember.objects.create(user=user)
|
||||
# StaffMember.objects.create(user=user)
|
||||
dealer = Dealer.objects.create(
|
||||
user=user,
|
||||
name=name,
|
||||
@ -1256,3 +1258,65 @@ def handle_email_result(task):
|
||||
logger.info(f"Email task succeeded: {task.result}")
|
||||
else:
|
||||
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}")
|
||||
@ -1281,6 +1281,9 @@ urlpatterns = [
|
||||
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'),
|
||||
|
||||
]
|
||||
|
||||
handler404 = "inventory.views.custom_page_not_found_view"
|
||||
|
||||
@ -186,7 +186,7 @@ def get_user_type(request):
|
||||
if request.is_dealer:
|
||||
return request.user.dealer
|
||||
elif request.is_staff:
|
||||
return request.user.staffmember.staff.dealer
|
||||
return request.user.staff.dealer
|
||||
return None
|
||||
|
||||
|
||||
@ -1110,16 +1110,16 @@ class CarFinanceCalculator:
|
||||
"quantity": sum(
|
||||
self._get_quantity(item) for item in self.item_transactions
|
||||
),
|
||||
"total_price": totals["total_price"],
|
||||
"total_price_discounted": totals["total_price_discounted"],
|
||||
"total_price_before_discount": totals["total_price_before_discount"],
|
||||
"total_vat": totals["total_vat_amount"] + totals["total_price"],
|
||||
"total_vat_amount": totals["total_vat_amount"],
|
||||
"total_discount": totals["total_discount"],
|
||||
"total_additionals": totals["total_additionals"],
|
||||
"grand_total": totals["grand_total"],
|
||||
"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": self.vat_rate,
|
||||
"vat": round(self.vat_rate, 2),
|
||||
}
|
||||
# class CarFinanceCalculator:
|
||||
# """
|
||||
|
||||
@ -511,8 +511,8 @@ class SalesDashboard(LoginRequiredMixin, TemplateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
dealer = get_user_type(self.request)
|
||||
staff = getattr(self.request.user, "staff", None)
|
||||
dealer = self.request.dealer
|
||||
staff = self.request.staff
|
||||
total_cars = models.Car.objects.filter(dealer=dealer).count()
|
||||
total_reservations = models.CarReservation.objects.filter(
|
||||
reserved_by=self.request.user, reserved_until__gte=timezone.now()
|
||||
@ -2478,7 +2478,7 @@ class CustomerCreateView(
|
||||
customer = form.instance.create_customer_model()
|
||||
|
||||
form.instance.user = user
|
||||
form.instance.customer_model = customer
|
||||
# form.instance.customer_model = customer
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
@ -3417,7 +3417,7 @@ class UserCreateView(
|
||||
return self.form_invalid(form)
|
||||
|
||||
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(
|
||||
self.request,
|
||||
_(
|
||||
@ -3425,16 +3425,25 @@ class UserCreateView(
|
||||
),
|
||||
)
|
||||
return redirect("user_create", dealer_slug=dealer.slug)
|
||||
|
||||
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.save()
|
||||
|
||||
staff_member = StaffMember.objects.create(user=user)
|
||||
for service in form.cleaned_data["service_offered"]:
|
||||
staff_member.services_offered.add(service)
|
||||
staff.staff_member = staff_member
|
||||
# staff_member, _ = StaffMember.objects.get_or_create(user=user)
|
||||
# for service in form.cleaned_data["service_offered"]:
|
||||
# staff_member.services_offered.add(service)
|
||||
staff.user = user
|
||||
staff.dealer = dealer
|
||||
staff.save()
|
||||
self.staff_pk = staff.pk
|
||||
@ -3495,7 +3504,7 @@ class UserUpdateView(
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
initial["email"] = self.object.staff_member.user.email
|
||||
initial["email"] = self.object.user.email
|
||||
initial["group"] = self.object.groups
|
||||
|
||||
return initial
|
||||
@ -4350,7 +4359,7 @@ def sales_list_view(request, 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 = []
|
||||
try:
|
||||
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)
|
||||
title = data.get("title")
|
||||
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", [])
|
||||
quantities = data.get("quantity", [])
|
||||
@ -5173,7 +5182,7 @@ class InvoiceListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
def get_queryset(self):
|
||||
dealer = get_object_or_404(models.Dealer, slug=self.kwargs["dealer_slug"])
|
||||
entity = dealer.entity
|
||||
staff = getattr(self.request.user.staffmember, "staff", None)
|
||||
staff = getattr(self.request.user, "staff", None)
|
||||
qs = []
|
||||
try:
|
||||
if any(
|
||||
@ -5933,12 +5942,12 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
)
|
||||
context["transfer_form"] = forms.LeadTransferForm()
|
||||
context["transfer_form"].fields["transfer_to"].queryset = (
|
||||
models.Staff.objects.select_related("staff_member", "staff_member__user")
|
||||
models.Staff.objects.select_related("user")
|
||||
.filter(
|
||||
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()
|
||||
)
|
||||
|
||||
@ -6062,10 +6071,10 @@ def lead_create(request, dealer_slug):
|
||||
)
|
||||
form.fields["staff"].queryset = (
|
||||
form.fields["staff"]
|
||||
.queryset.select_related("staff_member", "staff_member__user")
|
||||
.queryset.select_related("user")
|
||||
.filter(
|
||||
dealer=dealer,
|
||||
staff_member__user__groups__permissions__codename__contains="add_lead",
|
||||
user__groups__permissions__codename__contains="add_lead",
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
@ -6093,8 +6102,8 @@ def lead_create(request, dealer_slug):
|
||||
def lead_tracking(request, dealer_slug):
|
||||
dealer = get_object_or_404(models.Dealer, slug=dealer_slug)
|
||||
staff = (
|
||||
models.Staff.objects.select_related("staff_member", "staff_member__user")
|
||||
.filter(dealer=dealer, staff_member__user=request.user)
|
||||
models.Staff.objects.select_related("user")
|
||||
.filter(dealer=dealer, user=request.user)
|
||||
.first()
|
||||
)
|
||||
|
||||
@ -6275,10 +6284,10 @@ class LeadUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
||||
].queryset = form.instance.id_car_make.carmodel_set.all()
|
||||
form.fields["staff"].queryset = (
|
||||
form.fields["staff"]
|
||||
.queryset.select_related("staff_member", "staff_member__user")
|
||||
.queryset.select_related("user")
|
||||
.filter(
|
||||
dealer=dealer,
|
||||
staff_member__user__groups__permissions__codename__contains="add_lead",
|
||||
user__groups__permissions__codename__contains="add_lead",
|
||||
)
|
||||
.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)
|
||||
|
||||
if request.method == "POST":
|
||||
from django_q.models import Schedule as DjangoQSchedule
|
||||
|
||||
form = forms.ScheduleForm(request.POST)
|
||||
if form.is_valid():
|
||||
instance = form.save(commit=False)
|
||||
@ -6534,34 +6545,33 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
||||
elif obj.organization:
|
||||
instance.cutsomer = obj.organization.customer_model
|
||||
|
||||
service = Service.objects.get(name=instance.scheduled_type)
|
||||
# Log attempt to create AppointmentRequest
|
||||
logger.debug(
|
||||
f"User {user_username} attempting to create AppointmentRequest "
|
||||
f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
||||
f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
||||
)
|
||||
# service = Service.objects.get(name=instance.scheduled_type)
|
||||
# # Log attempt to create AppointmentRequest
|
||||
# logger.debug(
|
||||
# f"User {user_username} attempting to create AppointmentRequest "
|
||||
# f"for {content_type} ID: {obj.pk} ('{obj.slug}'). Service: '{service.name}', "
|
||||
# f"Scheduled At: '{instance.scheduled_at}', Duration: '{instance.duration}'."
|
||||
# )
|
||||
|
||||
try:
|
||||
appointment_request = AppointmentRequest.objects.create(
|
||||
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"))
|
||||
# try:
|
||||
# appointment_request = AppointmentRequest.objects.create(
|
||||
# date=instance.scheduled_at.date(),
|
||||
|
||||
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
|
||||
Appointment.objects.create(
|
||||
client=client,
|
||||
appointment_request=appointment_request,
|
||||
phone=instance.customer.phone,
|
||||
address=instance.customer.address_1,
|
||||
)
|
||||
# Appointment.objects.create(
|
||||
# client=client,
|
||||
# appointment_request=appointment_request,
|
||||
# phone=instance.customer.phone,
|
||||
# address=instance.customer.address_1,
|
||||
# )
|
||||
|
||||
instance.save()
|
||||
models.Activity.objects.create(
|
||||
@ -6571,11 +6581,19 @@ def schedule_event(request, dealer_slug, content_type, slug):
|
||||
created_by=request.user,
|
||||
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 ---
|
||||
logger.info(
|
||||
f"User {user_username} successfully scheduled {content_type} ID: {obj.pk} ('{obj.slug}'). "
|
||||
f"AppointmentRequest ID: {appointment_request.pk}, Appointment ID: {appointment_request.appointment.pk}."
|
||||
reminder_time = scheduled_at_aware - timezone.timedelta(minutes=15)
|
||||
# Only schedule if the reminder time is in the future
|
||||
# Reminder emails are scheduled to be sent 15 minutes before the scheduled time
|
||||
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"))
|
||||
|
||||
@ -6902,7 +6920,7 @@ class OpportunityUpdateView(
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
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(
|
||||
dealer=dealer, status="available", finances__marked_price__gt=0
|
||||
)
|
||||
@ -9764,12 +9782,12 @@ def management_view(request, dealer_slug):
|
||||
@login_required
|
||||
@permission_required("inventory.change_dealer", raise_exception=True)
|
||||
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 = {
|
||||
"customers": models.Customer.objects.filter(active=False),
|
||||
"organizations": models.Organization.objects.filter(active=False),
|
||||
"vendors": models.Vendor.objects.filter(active=False),
|
||||
"staff": models.Staff.objects.filter(active=False),
|
||||
"customers": models.Customer.objects.filter(active=False,dealer=dealer),
|
||||
"organizations": models.Organization.objects.filter(active=False,dealer=dealer),
|
||||
"vendors": models.Vendor.objects.filter(active=False,dealer=dealer),
|
||||
"staff": models.Staff.objects.filter(active=False,dealer=dealer),
|
||||
}
|
||||
return render(request, "admin_management/user_management.html", context)
|
||||
|
||||
@ -10910,3 +10928,15 @@ class RecallCreateView(FormView):
|
||||
|
||||
class RecallSuccessView(TemplateView):
|
||||
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
@ -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
@ -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
@ -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
|
After Width: | Height: | Size: 19 KiB |
@ -132,3 +132,45 @@ html[dir="rtl"] .form-icon-container .form-control {
|
||||
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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 570 B |
BIN
staticfiles/images/default-image/user.jpg
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
staticfiles/images/logos/no-content-new.jpg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
staticfiles/images/logos/no-content-new1.jpg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
staticfiles/images/logos/no-content-new2.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 573 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 506 KiB |
|
After Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 709 KiB |
BIN
staticfiles/images/logos/users/customer2.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
staticfiles/images/logos/users/dealer.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
staticfiles/images/logos/users/dealer_default.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
staticfiles/images/logos/users/employee.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
staticfiles/images/logos/users/new_dealer.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
staticfiles/images/logos/users/output_2.jpg
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
staticfiles/images/logos/users/salesperson.png
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
staticfiles/images/logos/users/user-logo.jpg
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png
vendored
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png
vendored
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png
vendored
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
staticfiles/images/logos/vendors/output_2.jpg
vendored
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg
vendored
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
staticfiles/images/logos/vendors/output_4.jpg
vendored
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
staticfiles/images/logos/vendors/output_4_owyivsr.jpg
vendored
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
staticfiles/images/logos/vendors/salesperson.png
vendored
Normal file
|
After Width: | Height: | Size: 601 KiB |
BIN
staticfiles/images/logos/vendors/vendor.png
vendored
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
staticfiles/images/logos/vendors/vnd.png
vendored
Normal file
|
After Width: | Height: | Size: 619 KiB |
BIN
staticfiles/images/no_content/no_car.png
Normal file
|
After Width: | Height: | Size: 574 KiB |
BIN
staticfiles/images/no_content/no_estimate.jpg
Normal file
|
After Width: | Height: | Size: 573 KiB |
BIN
staticfiles/images/no_content/no_item.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
staticfiles/images/no_content/no_plan.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
staticfiles/images/no_content/no_search_results.png
Normal file
|
After Width: | Height: | Size: 7.8 MiB |
BIN
staticfiles/images/no_content/no_user.png
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
staticfiles/images/no_content/no_vendor.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
@ -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
@ -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
|
After Width: | Height: | Size: 2.5 KiB |
BIN
staticfiles/user-logo1.png
Normal file
|
After Width: | Height: | Size: 814 B |
@ -84,7 +84,7 @@
|
||||
{% include "plans/expiration_messages.html" %}
|
||||
{% block period_navigation %}
|
||||
{% endblock period_navigation %}
|
||||
<div id="main_content" class="fade-me-in" hx-boost="true" hx-target="#main_content" hx-select="#main_content" hx-swap="outerHTML transition:true" hx-select-oob="#toast-container" hx-history-elt>
|
||||
<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>
|
||||
<div id="spinner" class="htmx-indicator spinner-bg">
|
||||
<img src="{% static 'spinner.svg' %}" width="100" height="100" alt="">
|
||||
</div>
|
||||
|
||||
@ -201,25 +201,25 @@
|
||||
<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"
|
||||
{% if not request.is_accountant %}disabled{% endif %}>
|
||||
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if "detail" not in request.path %}
|
||||
<!-- Mark as Draft -->
|
||||
{% if bill.can_draft %}
|
||||
<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')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<!-- Mark as Review -->
|
||||
{% if bill.can_review %}
|
||||
{{request.is_accountant}}
|
||||
<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')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
|
||||
</button>
|
||||
@ -253,7 +253,6 @@
|
||||
<!-- Cancel Button -->
|
||||
{% if bill.can_cancel %}
|
||||
<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')">
|
||||
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
|
||||
</button>
|
||||
|
||||
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
@ -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>
|
||||
<td>
|
||||
<p>
|
||||
<strong>{{ _("Name") }}:</strong> {{ user.staffmember.staff }}
|
||||
<strong>{{ _("Name") }}:</strong> {{ user.staff }}
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@ -445,8 +445,8 @@
|
||||
<li class="nav-item">
|
||||
<div class="theme-control-toggle fa-icon-wait">
|
||||
<input class="form-check-input ms-0 theme-control-toggle-input" type="checkbox" data-theme-control="phoenixTheme" value="dark" id="themeControlToggleSm" />
|
||||
<label class="mb-0 theme-control-toggle-label theme-control-toggle-light" for="themeControlToggleSm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{ _("Switch theme")}}" style="height:32px;width:32px;"><span class="icon" data-feather="moon"></span></label>
|
||||
<label class="mb-0 theme-control-toggle-label theme-control-toggle-dark" for="themeControlToggleSm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="{{ _("Switch theme")}}" style="height:32px;width:32px;"><span class="icon" data-feather="sun"></span></label>
|
||||
<label class="mb-0 theme-control-toggle-label theme-control-toggle-light" for="themeControlToggleSm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title='{{ _("Switch theme")}}' style="height:32px;width:32px;"><span class="icon" data-feather="moon"></span></label>
|
||||
<label class="mb-0 theme-control-toggle-label theme-control-toggle-dark" for="themeControlToggleSm" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title='{{ _("Switch theme")}}' style="height:32px;width:32px;"><span class="icon" data-feather="sun"></span></label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
@ -462,7 +462,7 @@
|
||||
{% include "notifications.html" %}
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated and request.is_dealer or request.is_staff %}
|
||||
{% if user.is_authenticated%}
|
||||
<li class="nav-item dropdown">
|
||||
<a hx-boost="false" class="nav-link lh-1 pe-0" id="navbarDropdownUser" role="button" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-haspopup="true" aria-expanded="false">
|
||||
<div class="avatar avatar-l text-center align-middle">
|
||||
@ -492,7 +492,7 @@
|
||||
{% if request.is_dealer %}
|
||||
<h6 class="mt-2 text-body-emphasis">{{ user.dealer.get_local_name }}</h6>
|
||||
{% else %}
|
||||
<h6 class="mt-2 text-body-emphasis">{{ user.staffmember.staff.get_local_name }}</h6>
|
||||
<h6 class="mt-2 text-body-emphasis">{{ user.staff.get_local_name }}</h6>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -530,7 +530,7 @@
|
||||
</li>
|
||||
{% if request.is_staff %}
|
||||
<li class="nav-item">
|
||||
<a hx-boost="false" class="nav-link px-3 d-block" href="{% url 'appointment:get_user_appointments' %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("My Calendar") }}</a>
|
||||
<a hx-boost="false" class="nav-link px-3 d-block" href="{% url 'schedule_calendar' request.dealer.slug %}"> <span class="me-2 text-body align-bottom" data-feather="calendar"></span>{{ _("My Calendar") }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<!--<li class="nav-item"><a class="nav-link px-3 d-block" href=""> Language</a></li>-->
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<script src="{% static 'vendors/zxing/index.min.js' %}"></script>
|
||||
<script src="{% static 'vendors/tesseract/tesseract.min.js' %}"></script>
|
||||
{% 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 %}
|
||||
{% endif %}
|
||||
<!---->
|
||||
|
||||
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 %}
|
||||