diff --git a/inventory/forms.py b/inventory/forms.py index e80e6e4a..131f05d8 100644 --- a/inventory/forms.py +++ b/inventory/forms.py @@ -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): """ diff --git a/inventory/management/commands/seed1.py b/inventory/management/commands/seed1.py index 478f3ef0..a7e7d9e1 100644 --- a/inventory/management/commands/seed1.py +++ b/inventory/management/commands/seed1.py @@ -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)) diff --git a/inventory/middleware.py b/inventory/middleware.py index 8a022f5c..e9b1ccc6 100644 --- a/inventory/middleware.py +++ b/inventory/middleware.py @@ -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: diff --git a/inventory/models.py b/inventory/models.py index 6dd4c092..2446d135 100644 --- a/inventory/models.py +++ b/inventory/models.py @@ -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,19 +1581,21 @@ class Customer(models.Model): def create_customer_model(self, for_lead=False): customer_dict = to_dict(self) - customer = self.dealer.entity.create_customer( - commit=False, - customer_model_kwargs={ - "customer_name": self.full_name, - "address_1": self.address, - "phone": self.phone_number, - "email": self.email, - }, - ) - try: - customer.additional_info.update({"customer_info": customer_dict}) - except Exception: - pass + 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={ + "customer_name": self.full_name, + "address_1": self.address, + "phone": self.phone_number, + "email": self.email, + }, + ) + try: + customer.additional_info.update({"customer_info": customer_dict}) + except Exception: + pass customer.active = False if for_lead else True customer.save() self.customer_model = customer @@ -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, diff --git a/inventory/signals.py b/inventory/signals.py index e34ee8f4..c63a9c55 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -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", - ) + # 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", - ) + # 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() - # .filter(name="Cash", role=roles.ASSET_CA_CASH) - # .first() - entity.get_all_accounts() - .filter(role=roles.ASSET_CA_CASH, role_default=True) - .first() - ) + # 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="", - ) + # 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="", - ) + # 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. View """ ).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. - View. - 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}, - ), - ), - ) \ No newline at end of file +# for recipient in recipients: +# models.Notification.objects.create( +# user=recipient, +# message=_( +# """ +# Bill {bill_number} has been approved. +# View. +# 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}, +# ), +# ), +# ) \ No newline at end of file diff --git a/inventory/tasks.py b/inventory/tasks.py index 9f319af9..b04b49af 100644 --- a/inventory/tasks.py +++ b/inventory/tasks.py @@ -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, @@ -1255,4 +1257,66 @@ def handle_email_result(task): if task.success: logger.info(f"Email task succeeded: {task.result}") else: - logger.error(f"Email task failed: {task.result}") \ No newline at end of file + 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 View. + """ + ).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}") \ No newline at end of file diff --git a/inventory/urls.py b/inventory/urls.py index 86363bf6..c2060772 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -1281,6 +1281,9 @@ urlpatterns = [ path('feature/recall//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('/schedules/calendar/', views.schedule_calendar, name='schedule_calendar'), + ] handler404 = "inventory.views.custom_page_not_found_view" diff --git a/inventory/utils.py b/inventory/utils.py index 8ffbfe09..86e5ccb1 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -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: # """ diff --git a/inventory/views.py b/inventory/views.py index 434a540b..42c5b5c9 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -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,12 +6581,20 @@ 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")) return redirect(f'{content_type}_detail',dealer_slug=dealer_slug, slug=slug) @@ -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) @@ -10373,9 +10391,9 @@ def upload_cars(request, dealer_slug, pk=None): f"User {user_username} retrieved ItemTransactionModel PK: {pk} for car upload." ) item = get_object_or_404(ItemTransactionModel, pk=pk) - + po_item = models.PoItemsUploaded.objects.get(dealer=dealer, item=item) - + response = redirect("upload_cars", dealer_slug=dealer_slug, pk=pk) if po_item.status == "uploaded": messages.add_message(request, messages.ERROR, "Item already uploaded.") @@ -10613,7 +10631,7 @@ def purchase_report_view(request,dealer_slug): po_quantity=0 for item in items: po_amount+=item["total"] - po_quantity+=item["q"] + po_quantity+=item["q"] total_po_amount+=po_amount total_po_cars+=po_quantity @@ -10909,4 +10927,16 @@ class RecallCreateView(FormView): ) class RecallSuccessView(TemplateView): - template_name = 'recalls/recall_success.html' \ No newline at end of file + 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) \ No newline at end of file diff --git a/pg-compose.yml b/pg-compose.yml new file mode 100644 index 00000000..ef583a1e --- /dev/null +++ b/pg-compose.yml @@ -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: \ No newline at end of file diff --git a/static/css/calendar_dark.css b/static/css/calendar_dark.css new file mode 100644 index 00000000..bb5bc90f --- /dev/null +++ b/static/css/calendar_dark.css @@ -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; + } +} \ No newline at end of file diff --git a/static/css/calendar_light.css b/static/css/calendar_light.css new file mode 100644 index 00000000..e0a47a89 --- /dev/null +++ b/static/css/calendar_light.css @@ -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; + } +} \ No newline at end of file diff --git a/static/images/logos/vendors/logo1.jpg b/static/images/logos/vendors/logo1.jpg new file mode 100644 index 00000000..c7fa991e Binary files /dev/null and b/static/images/logos/vendors/logo1.jpg differ diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index b61b11af..987242ff 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -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; +} \ No newline at end of file diff --git a/staticfiles/images/CACHE/images/default-image/sales_person/1dc58e585ee40e1488dd12804b4e30e4.webp b/staticfiles/images/CACHE/images/default-image/sales_person/1dc58e585ee40e1488dd12804b4e30e4.webp new file mode 100644 index 00000000..b685c9f1 Binary files /dev/null and b/staticfiles/images/CACHE/images/default-image/sales_person/1dc58e585ee40e1488dd12804b4e30e4.webp differ diff --git a/staticfiles/images/CACHE/images/default-image/user/cee30baf4aa2dd157df9788045f59f21.webp b/staticfiles/images/CACHE/images/default-image/user/cee30baf4aa2dd157df9788045f59f21.webp new file mode 100644 index 00000000..4c38a19e Binary files /dev/null and b/staticfiles/images/CACHE/images/default-image/user/cee30baf4aa2dd157df9788045f59f21.webp differ diff --git a/staticfiles/images/default-image/user.jpg b/staticfiles/images/default-image/user.jpg new file mode 100644 index 00000000..4b267969 Binary files /dev/null and b/staticfiles/images/default-image/user.jpg differ diff --git a/staticfiles/images/logos/no-content-new.jpg b/staticfiles/images/logos/no-content-new.jpg new file mode 100644 index 00000000..84e4b140 Binary files /dev/null and b/staticfiles/images/logos/no-content-new.jpg differ diff --git a/staticfiles/images/logos/no-content-new1.jpg b/staticfiles/images/logos/no-content-new1.jpg new file mode 100644 index 00000000..e71780d9 Binary files /dev/null and b/staticfiles/images/logos/no-content-new1.jpg differ diff --git a/staticfiles/images/logos/no-content-new2.jpg b/staticfiles/images/logos/no-content-new2.jpg new file mode 100644 index 00000000..0545ec71 Binary files /dev/null and b/staticfiles/images/logos/no-content-new2.jpg differ diff --git a/staticfiles/images/logos/users/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg b/staticfiles/images/logos/users/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg new file mode 100644 index 00000000..4b267969 Binary files /dev/null and b/staticfiles/images/logos/users/2a6e210e-4c38-4137-b0d3-119d94de74b6_1.jpg differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z.png b/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z.png new file mode 100644 index 00000000..c8bb6dfc Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z.png differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z_1.png b/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z_1.png new file mode 100644 index 00000000..e3c3347d Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z_1.png differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z_2.png b/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z_2.png new file mode 100644 index 00000000..3c86d07a Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_4m8zep4m8zep4m8z_2.png differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_4ochp94ochp94och.png b/staticfiles/images/logos/users/Gemini_Generated_Image_4ochp94ochp94och.png new file mode 100644 index 00000000..2579b1bf Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_4ochp94ochp94och.png differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_7zwaak7zwaak7zwa.png b/staticfiles/images/logos/users/Gemini_Generated_Image_7zwaak7zwaak7zwa.png new file mode 100644 index 00000000..d6fb6852 Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_7zwaak7zwaak7zwa.png differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_8uszgl8uszgl8usz.png b/staticfiles/images/logos/users/Gemini_Generated_Image_8uszgl8uszgl8usz.png new file mode 100644 index 00000000..7bd298af Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_8uszgl8uszgl8usz.png differ diff --git a/staticfiles/images/logos/users/Gemini_Generated_Image_s2tn1ss2tn1ss2tn.png b/staticfiles/images/logos/users/Gemini_Generated_Image_s2tn1ss2tn1ss2tn.png new file mode 100644 index 00000000..6a91ab8c Binary files /dev/null and b/staticfiles/images/logos/users/Gemini_Generated_Image_s2tn1ss2tn1ss2tn.png differ diff --git a/staticfiles/images/logos/users/customer2.jpg b/staticfiles/images/logos/users/customer2.jpg new file mode 100644 index 00000000..10c5ecb6 Binary files /dev/null and b/staticfiles/images/logos/users/customer2.jpg differ diff --git a/staticfiles/images/logos/users/dealer.png b/staticfiles/images/logos/users/dealer.png new file mode 100644 index 00000000..7127aa86 Binary files /dev/null and b/staticfiles/images/logos/users/dealer.png differ diff --git a/staticfiles/images/logos/users/dealer_default.png b/staticfiles/images/logos/users/dealer_default.png new file mode 100644 index 00000000..ee9c22fe Binary files /dev/null and b/staticfiles/images/logos/users/dealer_default.png differ diff --git a/staticfiles/images/logos/users/employee.png b/staticfiles/images/logos/users/employee.png new file mode 100644 index 00000000..0dc96509 Binary files /dev/null and b/staticfiles/images/logos/users/employee.png differ diff --git a/staticfiles/images/logos/users/new_dealer.png b/staticfiles/images/logos/users/new_dealer.png new file mode 100644 index 00000000..31655236 Binary files /dev/null and b/staticfiles/images/logos/users/new_dealer.png differ diff --git a/staticfiles/images/logos/users/output_2.jpg b/staticfiles/images/logos/users/output_2.jpg new file mode 100644 index 00000000..8b62098b Binary files /dev/null and b/staticfiles/images/logos/users/output_2.jpg differ diff --git a/staticfiles/images/logos/users/salesperson.png b/staticfiles/images/logos/users/salesperson.png new file mode 100644 index 00000000..26a371fd Binary files /dev/null and b/staticfiles/images/logos/users/salesperson.png differ diff --git a/staticfiles/images/logos/users/user-logo.jpg b/staticfiles/images/logos/users/user-logo.jpg new file mode 100644 index 00000000..bb7e4923 Binary files /dev/null and b/staticfiles/images/logos/users/user-logo.jpg differ diff --git a/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png b/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png new file mode 100644 index 00000000..24298e22 Binary files /dev/null and b/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1.png differ diff --git a/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png b/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png new file mode 100644 index 00000000..41ba0b0c Binary files /dev/null and b/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_1_PcQIcmG.png differ diff --git a/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png b/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png new file mode 100644 index 00000000..e375187d Binary files /dev/null and b/staticfiles/images/logos/vendors/Gemini_Generated_Image_6tpm9i6tpm9i6tpm_2.png differ diff --git a/staticfiles/images/logos/vendors/output_2.jpg b/staticfiles/images/logos/vendors/output_2.jpg new file mode 100644 index 00000000..8b62098b Binary files /dev/null and b/staticfiles/images/logos/vendors/output_2.jpg differ diff --git a/staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg b/staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg new file mode 100644 index 00000000..8b62098b Binary files /dev/null and b/staticfiles/images/logos/vendors/output_2_tiU2l8C.jpg differ diff --git a/staticfiles/images/logos/vendors/output_4.jpg b/staticfiles/images/logos/vendors/output_4.jpg new file mode 100644 index 00000000..1f193dea Binary files /dev/null and b/staticfiles/images/logos/vendors/output_4.jpg differ diff --git a/staticfiles/images/logos/vendors/output_4_owyivsr.jpg b/staticfiles/images/logos/vendors/output_4_owyivsr.jpg new file mode 100644 index 00000000..1f193dea Binary files /dev/null and b/staticfiles/images/logos/vendors/output_4_owyivsr.jpg differ diff --git a/staticfiles/images/logos/vendors/salesperson.png b/staticfiles/images/logos/vendors/salesperson.png new file mode 100644 index 00000000..26a371fd Binary files /dev/null and b/staticfiles/images/logos/vendors/salesperson.png differ diff --git a/staticfiles/images/logos/vendors/vendor.png b/staticfiles/images/logos/vendors/vendor.png new file mode 100644 index 00000000..9f98dc1c Binary files /dev/null and b/staticfiles/images/logos/vendors/vendor.png differ diff --git a/staticfiles/images/logos/vendors/vnd.png b/staticfiles/images/logos/vendors/vnd.png new file mode 100644 index 00000000..95e02e5a Binary files /dev/null and b/staticfiles/images/logos/vendors/vnd.png differ diff --git a/staticfiles/images/no_content/no_car.png b/staticfiles/images/no_content/no_car.png new file mode 100644 index 00000000..b5ea2f6f Binary files /dev/null and b/staticfiles/images/no_content/no_car.png differ diff --git a/staticfiles/images/no_content/no_estimate.jpg b/staticfiles/images/no_content/no_estimate.jpg new file mode 100644 index 00000000..79792155 Binary files /dev/null and b/staticfiles/images/no_content/no_estimate.jpg differ diff --git a/staticfiles/images/no_content/no_item.jpg b/staticfiles/images/no_content/no_item.jpg new file mode 100644 index 00000000..0545ec71 Binary files /dev/null and b/staticfiles/images/no_content/no_item.jpg differ diff --git a/staticfiles/images/no_content/no_plan.jpg b/staticfiles/images/no_content/no_plan.jpg new file mode 100644 index 00000000..9e84c1f9 Binary files /dev/null and b/staticfiles/images/no_content/no_plan.jpg differ diff --git a/staticfiles/images/no_content/no_search_results.png b/staticfiles/images/no_content/no_search_results.png new file mode 100644 index 00000000..a360db49 Binary files /dev/null and b/staticfiles/images/no_content/no_search_results.png differ diff --git a/staticfiles/images/no_content/no_user.png b/staticfiles/images/no_content/no_user.png new file mode 100644 index 00000000..f7062e5f Binary files /dev/null and b/staticfiles/images/no_content/no_user.png differ diff --git a/staticfiles/images/no_content/no_vendor.png b/staticfiles/images/no_content/no_vendor.png new file mode 100644 index 00000000..acb91474 Binary files /dev/null and b/staticfiles/images/no_content/no_vendor.png differ diff --git a/staticfiles/js/main.js b/staticfiles/js/main.js index da998095..10093df8 100644 --- a/staticfiles/js/main.js +++ b/staticfiles/js/main.js @@ -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"); +*/ \ No newline at end of file diff --git a/staticfiles/spinner.svg b/staticfiles/spinner.svg new file mode 100644 index 00000000..2cd994df --- /dev/null +++ b/staticfiles/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/staticfiles/user-logo.jpg b/staticfiles/user-logo.jpg new file mode 100644 index 00000000..bb7e4923 Binary files /dev/null and b/staticfiles/user-logo.jpg differ diff --git a/staticfiles/user-logo1.png b/staticfiles/user-logo1.png new file mode 100644 index 00000000..f5e79dfd Binary files /dev/null and b/staticfiles/user-logo1.png differ diff --git a/templates/base.html b/templates/base.html index 5c3cf542..3e1e03c5 100644 --- a/templates/base.html +++ b/templates/base.html @@ -84,7 +84,7 @@ {% include "plans/expiration_messages.html" %} {% block period_navigation %} {% endblock period_navigation %} -
+
diff --git a/templates/bill/includes/card_bill.html b/templates/bill/includes/card_bill.html index f6c27c08..5eb7b3ad 100644 --- a/templates/bill/includes/card_bill.html +++ b/templates/bill/includes/card_bill.html @@ -201,25 +201,25 @@
{% if perms.django_ledger.change_billmodel %} - - - + + + {% endif %} {% if "detail" not in request.path %} {% if bill.can_draft %} {% endif %} {% if bill.can_review %} + {{request.is_accountant}} @@ -253,7 +253,6 @@ {% if bill.can_cancel %} diff --git a/templates/emails/schedule_reminder.html b/templates/emails/schedule_reminder.html new file mode 100644 index 00000000..23768aec --- /dev/null +++ b/templates/emails/schedule_reminder.html @@ -0,0 +1,32 @@ + + + + + + + +
+

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!

+ +
+ + \ No newline at end of file diff --git a/templates/emails/schedule_reminder.txt b/templates/emails/schedule_reminder.txt new file mode 100644 index 00000000..d27d9e3d --- /dev/null +++ b/templates/emails/schedule_reminder.txt @@ -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. \ No newline at end of file diff --git a/templates/groups/group_detail.html b/templates/groups/group_detail.html index 288ef326..4e4474bb 100644 --- a/templates/groups/group_detail.html +++ b/templates/groups/group_detail.html @@ -59,7 +59,7 @@

- {{ _("Name") }}: {{ user.staffmember.staff }} + {{ _("Name") }}: {{ user.staff }}

diff --git a/templates/header.html b/templates/header.html index 6732d11b..11f83261 100644 --- a/templates/header.html +++ b/templates/header.html @@ -445,8 +445,8 @@
{% endif %} diff --git a/templates/inventory/car_detail.html b/templates/inventory/car_detail.html index 12fbdab3..f6499423 100644 --- a/templates/inventory/car_detail.html +++ b/templates/inventory/car_detail.html @@ -164,7 +164,7 @@ {% trans "Custom Card" %} {% if perms.inventory.add_customcard %} - {% endcomment %} +
+
+
    + {% for schedule in upcoming_schedules %} +
  • +
    +
    {{ schedule.get_purpose }}
    +

    {{ schedule.scheduled_at|date:"F d, Y P" }}

    +
    + +
  • + {% empty %} +
  • No schedules found.
  • + {% endfor %} +
+
+
+ + + + + + + + +{% endblock content %} +{% block customJS %} + + +{% endblock customJS %} \ No newline at end of file