update
This commit is contained in:
commit
7b167e5756
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,7 +5,6 @@ from . import models
|
|||||||
admin.site.register(models.Dealer)
|
admin.site.register(models.Dealer)
|
||||||
admin.site.register(models.Staff)
|
admin.site.register(models.Staff)
|
||||||
admin.site.register(models.Vendor)
|
admin.site.register(models.Vendor)
|
||||||
admin.site.register(models.Customer)
|
|
||||||
admin.site.register(models.SaleQuotation)
|
admin.site.register(models.SaleQuotation)
|
||||||
admin.site.register(models.SaleQuotationCar)
|
admin.site.register(models.SaleQuotationCar)
|
||||||
admin.site.register(models.SalesOrder)
|
admin.site.register(models.SalesOrder)
|
||||||
@ -27,6 +26,10 @@ admin.site.register(models.Representative)
|
|||||||
admin.site.register(models.CarTrim)
|
admin.site.register(models.CarTrim)
|
||||||
admin.site.register(models.AdditionalServices)
|
admin.site.register(models.AdditionalServices)
|
||||||
admin.site.register(models.Payment)
|
admin.site.register(models.Payment)
|
||||||
|
admin.site.register(models.VatRate)
|
||||||
|
admin.site.register(models.Customer)
|
||||||
|
admin.site.register(models.Opportunity)
|
||||||
|
admin.site.register(models.Notification)
|
||||||
|
|
||||||
@admin.register(models.CarMake)
|
@admin.register(models.CarMake)
|
||||||
class CarMakeAdmin(admin.ModelAdmin):
|
class CarMakeAdmin(admin.ModelAdmin):
|
||||||
@ -95,3 +98,4 @@ class CarSpecificationAdmin(admin.ModelAdmin):
|
|||||||
# list_display = ('user', 'action', 'timestamp')
|
# list_display = ('user', 'action', 'timestamp')
|
||||||
# search_fields = ('user__username', 'action')
|
# search_fields = ('user__username', 'action')
|
||||||
# list_filter = ('timestamp',)
|
# list_filter = ('timestamp',)
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,11 @@ from .models import (
|
|||||||
SaleQuotationCar,
|
SaleQuotationCar,
|
||||||
AdditionalServices,
|
AdditionalServices,
|
||||||
Staff,
|
Staff,
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
Opportunity
|
||||||
|
|
||||||
|
>>>>>>> 8b00f9a40fc336f209f4ae6fb03785df6c97d265
|
||||||
)
|
)
|
||||||
from django_ledger.models import ItemModel, InvoiceModel
|
from django_ledger.models import ItemModel, InvoiceModel
|
||||||
from django.forms import ModelMultipleChoiceField, ValidationError
|
from django.forms import ModelMultipleChoiceField, ValidationError
|
||||||
@ -527,6 +532,7 @@ class ItemForm(forms.Form):
|
|||||||
unit_sales_price = forms.DecimalField(label="Unit Sales Price", required=True)
|
unit_sales_price = forms.DecimalField(label="Unit Sales Price", required=True)
|
||||||
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
class PaymentForm(forms.Form):
|
class PaymentForm(forms.Form):
|
||||||
invoice = forms.ModelChoiceField(
|
invoice = forms.ModelChoiceField(
|
||||||
queryset=InvoiceModel.objects.all(), label="Invoice", required=True
|
queryset=InvoiceModel.objects.all(), label="Invoice", required=True
|
||||||
@ -552,4 +558,18 @@ class EmailForm(forms.Form):
|
|||||||
subject = forms.CharField(max_length=255)
|
subject = forms.CharField(max_length=255)
|
||||||
message = forms.CharField(widget=forms.Textarea)
|
message = forms.CharField(widget=forms.Textarea)
|
||||||
from_email = forms.EmailField()
|
from_email = forms.EmailField()
|
||||||
to_email = forms.EmailField()
|
to_email = forms.EmailField()
|
||||||
|
=======
|
||||||
|
class OpportunityForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Opportunity
|
||||||
|
fields = [
|
||||||
|
'car', 'deal_name', 'deal_value', 'deal_status',
|
||||||
|
'priority', 'source'
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'deal_status': forms.Select(choices=Opportunity.DEAL_STATUS_CHOICES),
|
||||||
|
'priority': forms.Select(choices=Opportunity.PRIORITY_CHOICES),
|
||||||
|
'source': forms.Select(choices=Opportunity.DEAL_SOURCES_CHOICES),
|
||||||
|
}
|
||||||
|
>>>>>>> 8b00f9a40fc336f209f4ae6fb03785df6c97d265
|
||||||
|
|||||||
18
inventory/migrations/0016_alter_staff_staff_type.py
Normal file
18
inventory/migrations/0016_alter_staff_staff_type.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-30 15:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('inventory', '0015_merge_0008_vatrate_0014_useractivitylog'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='staff',
|
||||||
|
name='staff_type',
|
||||||
|
field=models.CharField(choices=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-30 22:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('inventory', '0016_alter_staff_staff_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customer',
|
||||||
|
name='is_lead',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Is Lead'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Notification',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('message', models.CharField(max_length=255, verbose_name='Message')),
|
||||||
|
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('staff', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='inventory.staff')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Notification',
|
||||||
|
'verbose_name_plural': 'Notifications',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Opportunity',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('deal_name', models.CharField(max_length=255, verbose_name='Deal Name')),
|
||||||
|
('deal_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Deal Value')),
|
||||||
|
('deal_status', models.CharField(choices=[('new', 'New'), ('in_progress', 'In Progress'), ('pending', 'Pending'), ('canceled', 'Canceled'), ('completed', 'Completed')], default='new', max_length=50, verbose_name='Deal Status')),
|
||||||
|
('priority', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent')], default='low', max_length=50, verbose_name='Priority')),
|
||||||
|
('source', models.CharField(choices=[('referrals', 'Referrals'), ('walk_in', 'Walk In'), ('toll_free', 'Toll Free'), ('whatsapp', 'Whatsapp'), ('showroom', 'Showroom'), ('website', 'Website')], default='showroom', max_length=255, verbose_name='Source')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
|
||||||
|
('car', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.car', verbose_name='Car')),
|
||||||
|
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deals_created', to='inventory.staff')),
|
||||||
|
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='inventory.customer')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Opportunity',
|
||||||
|
'verbose_name_plural': 'Opportunities',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.1.4 on 2024-12-31 03:24
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('inventory', '0017_customer_is_lead_notification_opportunity'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='notification',
|
||||||
|
name='staff',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='notification',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -686,6 +686,7 @@ class Customer(models.Model):
|
|||||||
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
max_length=200, blank=True, null=True, verbose_name=_("Address")
|
||||||
)
|
)
|
||||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||||
|
is_lead = models.BooleanField(default=True, verbose_name=_("Is Lead"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Customer")
|
verbose_name = _("Customer")
|
||||||
@ -698,7 +699,64 @@ class Customer(models.Model):
|
|||||||
@property
|
@property
|
||||||
def get_full_name(self):
|
def get_full_name(self):
|
||||||
return f"{self.first_name} {self.middle_name} {self.last_name}"
|
return f"{self.first_name} {self.middle_name} {self.last_name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Opportunity(models.Model):
|
||||||
|
DEAL_STATUS_CHOICES = [
|
||||||
|
('new', _('New')),
|
||||||
|
('in_progress', _('In Progress')),
|
||||||
|
('pending', _('Pending')),
|
||||||
|
('canceled', _('Canceled')),
|
||||||
|
('completed', _('Completed')),
|
||||||
|
]
|
||||||
|
PRIORITY_CHOICES = [
|
||||||
|
('low', _('Low')),
|
||||||
|
('medium', _('Medium')),
|
||||||
|
('high', _('High')),
|
||||||
|
('urgent', _('Urgent')),
|
||||||
|
]
|
||||||
|
DEAL_SOURCES_CHOICES = [
|
||||||
|
('referrals', _('Referrals')),
|
||||||
|
('walk_in', _('Walk In')),
|
||||||
|
('toll_free', _('Toll Free')),
|
||||||
|
('whatsapp', _('Whatsapp')),
|
||||||
|
('showroom', _('Showroom')),
|
||||||
|
('website', _('Website')),
|
||||||
|
]
|
||||||
|
|
||||||
|
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="opportunities")
|
||||||
|
car = models.ForeignKey(Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car"))
|
||||||
|
deal_name = models.CharField(max_length=255, verbose_name=_("Deal Name"))
|
||||||
|
deal_value = models.DecimalField(max_digits=10, decimal_places=2, verbose_name=_("Deal Value"))
|
||||||
|
deal_status = models.CharField(max_length=50, choices=DEAL_STATUS_CHOICES, default='new', verbose_name=_("Deal Status"))
|
||||||
|
priority = models.CharField(max_length=50, choices=PRIORITY_CHOICES, default='low', verbose_name=_("Priority"))
|
||||||
|
source = models.CharField(max_length=255, choices=DEAL_SOURCES_CHOICES, default='showroom', verbose_name=_("Source"))
|
||||||
|
created_by = models.ForeignKey(Staff, on_delete=models.SET_NULL, null=True, related_name="deals_created")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||||
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Opportunity")
|
||||||
|
verbose_name_plural = _("Opportunities")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.deal_name} - {self.customer.get_full_name}"
|
||||||
|
|
||||||
|
|
||||||
|
class Notification(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notifications")
|
||||||
|
message = models.CharField(max_length=255, verbose_name=_("Message"))
|
||||||
|
is_read = models.BooleanField(default=False, verbose_name=_("Is Read"))
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Notification")
|
||||||
|
verbose_name_plural = _("Notifications")
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
class Organization(models.Model, LocalizedNameMixin):
|
class Organization(models.Model, LocalizedNameMixin):
|
||||||
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations')
|
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations')
|
||||||
|
|||||||
@ -334,3 +334,11 @@ def create_customer(sender, instance, created, **kwargs):
|
|||||||
# quotation.status = 'pending'
|
# quotation.status = 'pending'
|
||||||
# quotation.save()
|
# quotation.save()
|
||||||
|
|
||||||
|
@receiver(post_save, sender=models.Opportunity)
|
||||||
|
def notify_staff_on_deal_status_change(sender, instance, **kwargs):
|
||||||
|
if instance.pk:
|
||||||
|
previous = models.Opportunity.objects.get(pk=instance.pk)
|
||||||
|
if previous.deal_status != instance.deal_status:
|
||||||
|
message = f"Deal '{instance.deal_name}' status changed from {previous.deal_status} to {instance.deal_status}."
|
||||||
|
models.Notification.objects.create(staff=instance.created_by, message=message)
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ urlpatterns = [
|
|||||||
path('login/code/', allauth_views.RequestLoginCodeView.as_view(template_name='account/request_login_code.html')),
|
path('login/code/', allauth_views.RequestLoginCodeView.as_view(template_name='account/request_login_code.html')),
|
||||||
#Dashboards
|
#Dashboards
|
||||||
path('dashboards/accounting/', views.AccountingDashboard.as_view(), name='accounting'),
|
path('dashboards/accounting/', views.AccountingDashboard.as_view(), name='accounting'),
|
||||||
|
path('dashboards/crm/', views.notifications_view, name='staff_dashboard'),
|
||||||
|
|
||||||
# Dealer URLs
|
# Dealer URLs
|
||||||
path('dealers/<int:pk>/', views.DealerDetailView.as_view(), name='dealer_detail'),
|
path('dealers/<int:pk>/', views.DealerDetailView.as_view(), name='dealer_detail'),
|
||||||
@ -40,7 +41,17 @@ urlpatterns = [
|
|||||||
path('customers/create/', views.CustomerCreateView.as_view(), name='customer_create'),
|
path('customers/create/', views.CustomerCreateView.as_view(), name='customer_create'),
|
||||||
path('customers/<int:pk>/update/', views.CustomerUpdateView.as_view(), name='customer_update'),
|
path('customers/<int:pk>/update/', views.CustomerUpdateView.as_view(), name='customer_update'),
|
||||||
path('customers/<int:pk>/delete/', views.delete_customer, name='customer_delete'),
|
path('customers/<int:pk>/delete/', views.delete_customer, name='customer_delete'),
|
||||||
# Vendor URLs
|
path('customers/<int:pk>/create_lead/', views.create_lead, name='create_lead'),
|
||||||
|
path('customers/<int:customer_id>/opportunities/create/', views.OpportunityCreateView.as_view(), name='create_opportunity'),
|
||||||
|
# CRM URLs
|
||||||
|
path('opportunities/<int:pk>/', views.OpportunityDetailView.as_view(), name='opportunity_detail'),
|
||||||
|
path('opportunities/<int:pk>/edit/', views.OpportunityUpdateView.as_view(), name='update_opportunity'),
|
||||||
|
path('opportunities/', views.OpportunityListView.as_view(), name='opportunity_list'),
|
||||||
|
path('opportunities/<int:pk>/delete/', views.OpportunityDeleteView.as_view(), name='delete_opportunity'),
|
||||||
|
path('notifications/', views.NotificationListView.as_view(), name='notifications_history'),
|
||||||
|
path('notifications/<int:pk>/mark_as_read/', views.mark_notification_as_read, name='mark_notification_as_read'),
|
||||||
|
|
||||||
|
#Vendor URLs
|
||||||
path('vendors', views.VendorListView.as_view(), name='vendor_list'),
|
path('vendors', views.VendorListView.as_view(), name='vendor_list'),
|
||||||
path('vendors/<int:pk>/', views.VendorDetailView.as_view(), name='vendor_detail'),
|
path('vendors/<int:pk>/', views.VendorDetailView.as_view(), name='vendor_detail'),
|
||||||
path('vendors/create/', views.VendorCreateView.as_view(), name='vendor_create'),
|
path('vendors/create/', views.VendorCreateView.as_view(), name='vendor_create'),
|
||||||
@ -48,12 +59,8 @@ urlpatterns = [
|
|||||||
path('vendors/<int:pk>/delete/', views.VendorDetailView.as_view(), name='vendor_delete'),
|
path('vendors/<int:pk>/delete/', views.VendorDetailView.as_view(), name='vendor_delete'),
|
||||||
|
|
||||||
# Car URLs
|
# Car URLs
|
||||||
path('cars/inventory/',
|
path('cars/inventory/', views.CarInventory.as_view(), name='car_inventory_all'),
|
||||||
views.CarInventory.as_view(),
|
path('cars/inventory/<int:make_id>/<int:model_id>/<int:trim_id>/', views.CarInventory.as_view(), name='car_inventory'),
|
||||||
name='car_inventory_all'),
|
|
||||||
path('cars/inventory/<int:make_id>/<int:model_id>/<int:trim_id>/',
|
|
||||||
views.CarInventory.as_view(),
|
|
||||||
name='car_inventory'),
|
|
||||||
path('cars/inventory/stats', views.inventory_stats_view, name='inventory_stats'),
|
path('cars/inventory/stats', views.inventory_stats_view, name='inventory_stats'),
|
||||||
path('cars/<int:pk>/', views.CarDetailView.as_view(), name='car_detail'),
|
path('cars/<int:pk>/', views.CarDetailView.as_view(), name='car_detail'),
|
||||||
path('cars/<int:pk>/update/', views.CarUpdateView.as_view(), name='car_update'),
|
path('cars/<int:pk>/update/', views.CarUpdateView.as_view(), name='car_update'),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel,LedgerModel
|
from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel,LedgerModel
|
||||||
from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm
|
from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm
|
||||||
@ -38,6 +39,7 @@ from django.contrib import messages
|
|||||||
from django.db.models import Sum, F, Count
|
from django.db.models import Sum, F, Count
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
|
from .models import Customer
|
||||||
from .services import (
|
from .services import (
|
||||||
elm,
|
elm,
|
||||||
decodevin,
|
decodevin,
|
||||||
@ -1689,7 +1691,8 @@ class EstimateDetailView(LoginRequiredMixin, DetailView):
|
|||||||
if estimate.get_itemtxs_data():
|
if estimate.get_itemtxs_data():
|
||||||
total = sum(x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all())
|
total = sum(x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all())
|
||||||
vat = models.VatRate.objects.filter(is_active=True).first()
|
vat = models.VatRate.objects.filter(is_active=True).first()
|
||||||
kwargs["vate_amount"] = (total * vat.vat_rate)
|
# vat = settings.VAT_RATE
|
||||||
|
kwargs["vat_amount"] = (total * vat.vat_rate)
|
||||||
kwargs["total"] = (total * vat.vat_rate) + total
|
kwargs["total"] = (total * vat.vat_rate) + total
|
||||||
kwargs["vat"] = vat.rate
|
kwargs["vat"] = vat.rate
|
||||||
kwargs["invoice"] = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
kwargs["invoice"] = InvoiceModel.objects.all().filter(ce_model=estimate).first()
|
||||||
@ -1874,7 +1877,7 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView):
|
|||||||
total = sum(x.unit_cost * x.quantity for x in invoice.get_itemtxs_data()[0].all())
|
total = sum(x.unit_cost * x.quantity for x in invoice.get_itemtxs_data()[0].all())
|
||||||
total = int(total)
|
total = int(total)
|
||||||
vat = models.VatRate.objects.filter(is_active=True).first()
|
vat = models.VatRate.objects.filter(is_active=True).first()
|
||||||
kwargs["vate_amount"] = (total * vat.vat_rate)
|
kwargs["vat_amount"] = (total * vat.vat_rate)
|
||||||
kwargs["total"] = (total * vat.vat_rate) + total
|
kwargs["total"] = (total * vat.vat_rate) + total
|
||||||
kwargs["vat"] = vat.rate
|
kwargs["vat"] = vat.rate
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
@ -1994,4 +1997,86 @@ def send_email_view(request,pk):
|
|||||||
send_email("manager@tenhal.sa", 'user@tenhal.sa',f"Estimate-{estimate.estimate_number}", msg)
|
send_email("manager@tenhal.sa", 'user@tenhal.sa',f"Estimate-{estimate.estimate_number}", msg)
|
||||||
estimate.mark_as_review()
|
estimate.mark_as_review()
|
||||||
messages.success(request, "Email sent successfully!")
|
messages.success(request, "Email sent successfully!")
|
||||||
return redirect("estimate_detail", pk=estimate.pk)
|
return redirect("estimate_detail", pk=estimate.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def create_lead(request, pk):
|
||||||
|
customer = get_object_or_404(models.Customer, pk=pk)
|
||||||
|
if customer.is_lead:
|
||||||
|
messages.warning(request, _('Customer is already a lead.'))
|
||||||
|
else:
|
||||||
|
customer.is_lead = True
|
||||||
|
customer.save()
|
||||||
|
messages.success(request, _('Customer successfully marked as a lead.'))
|
||||||
|
return redirect(reverse('customer_detail', kwargs={'pk': customer.pk}))
|
||||||
|
|
||||||
|
|
||||||
|
class OpportunityCreateView(CreateView):
|
||||||
|
model = models.Opportunity
|
||||||
|
form_class = forms.OpportunityForm
|
||||||
|
template_name = 'crm/opportunity_form.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['customer'] = models.Customer.objects.get(pk=self.kwargs['customer_id'])
|
||||||
|
context['cars'] = models.Car.objects.all()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
form.instance.customer = models.Customer.objects.get(pk=self.kwargs['customer_id'])
|
||||||
|
form.instance.created_by = self.request.user.staff
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('opportunity_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class OpportunityUpdateView(UpdateView):
|
||||||
|
model = models.Opportunity
|
||||||
|
form_class = forms.OpportunityForm
|
||||||
|
template_name = 'crm/opportunity_form.html'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse_lazy('opportunity_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
|
class OpportunityDetailView(DetailView):
|
||||||
|
model = models.Opportunity
|
||||||
|
template_name = 'crm/opportunity_detail.html'
|
||||||
|
context_object_name = 'opportunity'
|
||||||
|
|
||||||
|
|
||||||
|
class OpportunityListView(ListView):
|
||||||
|
model = models.Opportunity
|
||||||
|
template_name = 'crm/opportunity_list.html'
|
||||||
|
context_object_name = 'opportunities'
|
||||||
|
|
||||||
|
|
||||||
|
class OpportunityDeleteView(DeleteView):
|
||||||
|
model = models.Opportunity
|
||||||
|
template_name = 'crm/opportunity_confirm_delete.html'
|
||||||
|
success_url = reverse_lazy('opportunity_list')
|
||||||
|
|
||||||
|
|
||||||
|
def notifications_view(request):
|
||||||
|
notifications = models.Notification.objects.filter(user=request.user, is_read=False).order_by('-created_at')
|
||||||
|
return render(request, 'notifications.html', {'notifications': notifications})
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationListView(LoginRequiredMixin, ListView):
|
||||||
|
model = models.Notification
|
||||||
|
template_name = 'notifications_history.html'
|
||||||
|
context_object_name = 'notifications'
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return models.Notification.objects.filter(user=self.request.user).order_by('-created_at')
|
||||||
|
|
||||||
|
|
||||||
|
def mark_notification_as_read(request, pk):
|
||||||
|
notification = get_object_or_404(models.Notification, pk=pk)
|
||||||
|
notification.is_read = True
|
||||||
|
notification.save()
|
||||||
|
return redirect('notifications_history')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,17 +16,7 @@ function getCookie(name) {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Toast = Swal.mixin({
|
|
||||||
toast: true,
|
|
||||||
position: "top-end",
|
|
||||||
showConfirmButton: false,
|
|
||||||
timer: 3000,
|
|
||||||
timerProgressBar: true,
|
|
||||||
didOpen: (toast) => {
|
|
||||||
toast.onmouseenter = Swal.stopTimer;
|
|
||||||
toast.onmouseleave = Swal.resumeTimer;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
function notify(tag,msg){
|
function notify(tag,msg){
|
||||||
Toast.fire({
|
Toast.fire({
|
||||||
icon: tag,
|
icon: tag,
|
||||||
|
|||||||
@ -653,21 +653,6 @@
|
|||||||
'.echart-financial-Activities'
|
'.echart-financial-Activities'
|
||||||
);
|
);
|
||||||
|
|
||||||
const profitData = [
|
|
||||||
[350000, 390000, 410700, 450000, 390000, 410700],
|
|
||||||
[245000, 310000, 420000, 480000, 530000, 580000],
|
|
||||||
[278450, 513220, 359890, 444567, 201345, 589000]
|
|
||||||
];
|
|
||||||
const revenueData = [
|
|
||||||
[-810000, -640000, -630000, -590000, -620000, -780000],
|
|
||||||
[-482310, -726590, -589120, -674832, -811245, -455678],
|
|
||||||
[-432567, -688921, -517389, -759234, -601876, -485112]
|
|
||||||
];
|
|
||||||
const expansesData = [
|
|
||||||
[-450000, -250000, -200000, -120000, -230000, -270000],
|
|
||||||
[-243567, -156789, -398234, -120456, -321890, -465678],
|
|
||||||
[-235678, -142345, -398765, -287456, -173890, -451234]
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($financialActivitiesChartEl) {
|
if ($financialActivitiesChartEl) {
|
||||||
const userOptions = getData($financialActivitiesChartEl, 'options');
|
const userOptions = getData($financialActivitiesChartEl, 'options');
|
||||||
|
|||||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
@ -55,128 +55,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% block extra_js %}{% endblock extra_js %}
|
{% block extra_js %}{% endblock extra_js %}
|
||||||
<script>
|
|
||||||
|
|
||||||
// Function to calculate Total Cost and Total Revenue
|
|
||||||
function calculateTotals(container) {
|
|
||||||
const quantity = parseFloat(container.querySelector('.quantity').value) || 0;
|
|
||||||
const unitCost = parseFloat(container.querySelector('.unitCost').value) || 0;
|
|
||||||
const unitSalesPrice = parseFloat(container.querySelector('.unitSalesPrice').value) || 0;
|
|
||||||
|
|
||||||
const totalCost = quantity * unitCost;
|
|
||||||
const totalRevenue = quantity * unitSalesPrice;
|
|
||||||
|
|
||||||
container.querySelector('.totalCost').value = totalCost.toFixed(2);
|
|
||||||
container.querySelector('.totalRevenue').value = totalRevenue.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners to inputs for dynamic calculation
|
|
||||||
function addInputListeners(container) {
|
|
||||||
container.querySelectorAll('.quantity, .unitCost, .unitSalesPrice').forEach(input => {
|
|
||||||
input.addEventListener('input', () => calculateTotals(container));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new form fields
|
|
||||||
document.getElementById('addMoreBtn').addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const formContainer = document.getElementById('formContainer');
|
|
||||||
const newForm = document.createElement('div');
|
|
||||||
newForm.className = 'form-container row g-3 mb-3 mt-5';
|
|
||||||
newForm.innerHTML = `
|
|
||||||
<div class="mb-2 col-sm-2">
|
|
||||||
<select class="form-control item" name="item[]" required>
|
|
||||||
{% for item in items %}
|
|
||||||
<option value="{{ item.pk }}">{{ item.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 col-sm-2">
|
|
||||||
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 col-sm-2">
|
|
||||||
<input class="form-control unitCost" type="number" placeholder="Unit Cost" name="unitCost[]" step="0.01" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 col-sm-2">
|
|
||||||
<input class="form-control unitSalesPrice" type="number" placeholder="Unit Sales Price" name="unitSalesPrice[]" step="0.01" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 col-sm-2">
|
|
||||||
<input class="form-control totalCost" type="number" placeholder="Total Cost" name="totalCost[]" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 col-sm-1">
|
|
||||||
<input class="form-control totalRevenue" type="number" placeholder="Total Revenue" name="totalRevenue[]" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2 col-sm-1">
|
|
||||||
<button class="btn btn-danger removeBtn">Remove</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
formContainer.appendChild(newForm);
|
|
||||||
addInputListeners(newForm); // Add listeners to the new form
|
|
||||||
|
|
||||||
// Add remove button functionality
|
|
||||||
newForm.querySelector('.removeBtn').addEventListener('click', function() {
|
|
||||||
newForm.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add listeners to the initial form
|
|
||||||
document.querySelectorAll('.form-container').forEach(container => {
|
|
||||||
addInputListeners(container);
|
|
||||||
|
|
||||||
// Add remove button functionality to the initial form
|
|
||||||
container.querySelector('.removeBtn').addEventListener('click', function() {
|
|
||||||
container.remove();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const url = "{% url 'estimate_create' %}"
|
|
||||||
document.getElementById('mainForm').addEventListener('submit', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// Collect all form data
|
|
||||||
const formData = new FormData(this);
|
|
||||||
const csrfToken = getCookie('csrftoken');
|
|
||||||
const data = {};
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
// Handle multi-value fields (e.g., item[], quantity[])
|
|
||||||
if (data[key]) {
|
|
||||||
if (!Array.isArray(data[key])) {
|
|
||||||
data[key] = [data[key]]; // Convert to array
|
|
||||||
}
|
|
||||||
data[key].push(value);
|
|
||||||
} else {
|
|
||||||
data[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Send data to the server using fetch
|
|
||||||
fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log('Success:', data);
|
|
||||||
if(data.status == "error"){
|
|
||||||
notify("error",data.message);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
notify("success","Estimate created successfully");
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = data.url;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
notify("error",error);
|
|
||||||
alert('An error occurred while submitting the form.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ===============================================-->
|
<!-- ===============================================-->
|
||||||
|
|||||||
11
templates/crm/opportunity_confirm_delete.html
Normal file
11
templates/crm/opportunity_confirm_delete.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Delete Opportunity</h1>
|
||||||
|
<p>Are you sure you want to delete "{{ object.deal_name }}"?</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Yes, delete</button>
|
||||||
|
<a href="{% url 'opportunity_list' %}">Cancel</a>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
11
templates/crm/opportunity_detail.html
Normal file
11
templates/crm/opportunity_detail.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<h1>{{ opportunity.deal_name }}</h1>
|
||||||
|
<p>Customer: {{ opportunity.customer.get_full_name }}</p>
|
||||||
|
<p>Car: {{ opportunity.car }}</p>
|
||||||
|
<p>Deal Value: {{ opportunity.deal_value }}</p>
|
||||||
|
<p>Deal Status: {{ opportunity.get_deal_status_display }}</p>
|
||||||
|
<p>Priority: {{ opportunity.get_priority_display }}</p>
|
||||||
|
<p>Source: {{ opportunity.get_source_display }}</p>
|
||||||
|
<p>Created By: {{ opportunity.created_by.name }}</p>
|
||||||
|
<p>Created At: {{ opportunity.created_at }}</p>
|
||||||
|
<p>Updated At: {{ opportunity.updated_at }}</p>
|
||||||
|
<a href="{% url 'update_opportunity' opportunity.pk %}">Edit</a>
|
||||||
6
templates/crm/opportunity_form.html
Normal file
6
templates/crm/opportunity_form.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<h1>{% if form.instance.pk %}Edit{% else %}Create{% endif %} Opportunity</h1>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
50
templates/crm/opportunity_list.html
Normal file
50
templates/crm/opportunity_list.html
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{% extends 'base.html' %} <!-- Assuming you have a base template -->
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Opportunities</h1>
|
||||||
|
<a href="{% url 'create_opportunity' customer_id=1 %}">Create New Opportunity</a>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Deal Name</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Car</th>
|
||||||
|
<th>Deal Value</th>
|
||||||
|
<th>Deal Status</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Created By</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for opportunity in opportunities %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ opportunity.deal_name }}</td>
|
||||||
|
<td>{{ opportunity.customer.get_full_name }}</td>
|
||||||
|
<td>{{ opportunity.car }}</td>
|
||||||
|
<td>{{ opportunity.deal_value }}</td>
|
||||||
|
<td>{{ opportunity.get_deal_status_display }}</td>
|
||||||
|
<td>{{ opportunity.get_priority_display }}</td>
|
||||||
|
<td>{{ opportunity.get_source_display }}</td>
|
||||||
|
<td>{{ opportunity.created_by.name }}</td>
|
||||||
|
<td>{{ opportunity.created_at }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'opportunity_detail' pk=opportunity.pk %}">View</a>
|
||||||
|
<a href="{% url 'update_opportunity' pk=opportunity.pk %}">Edit</a>
|
||||||
|
<form action="{% url 'delete_opportunity' pk=opportunity.pk %}" method="post" style="display:inline;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="10">No opportunities found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endblock %}
|
||||||
@ -51,7 +51,12 @@
|
|||||||
data-bs-target="#deleteModal"><span class="fa-solid fa-trash-can me-2"></span>{{ _("Delete") }}</a>
|
data-bs-target="#deleteModal"><span class="fa-solid fa-trash-can me-2"></span>{{ _("Delete") }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<a href="{% url 'customer_update' customer.id %}" class="btn btn-phoenix-secondary"><span class="fa-solid fa-pen-to-square me-2"></span>{{_("Update")}}</a>
|
<a href="{% url 'customer_update' customer.pk %}" class="btn btn-phoenix-warning"><span class="fa-solid fa-pen-to-square me-2"></span>{{_("Update")}}</a>
|
||||||
|
{% if not customer.is_lead %}
|
||||||
|
<a href="{% url 'create_lead' customer.pk %}" class="btn btn-phoenix-success"><span class="fa-solid far fa-plus-square me-2"></span>{{ _("Mark as Lead")}}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'create_opportunity' customer.pk %}" class="btn btn-phoenix-primary"><span class="fa-solid far fa-plus-square me-2"></span>{{_("Opportunity")}}</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -64,7 +69,7 @@
|
|||||||
<div class="card-body d-flex flex-column justify-content-between pb-3">
|
<div class="card-body d-flex flex-column justify-content-between pb-3">
|
||||||
<div class="row align-items-center g-5 mb-3 text-center text-sm-start">
|
<div class="row align-items-center g-5 mb-3 text-center text-sm-start">
|
||||||
<div class="col-12 col-sm-auto mb-sm-2">
|
<div class="col-12 col-sm-auto mb-sm-2">
|
||||||
<div class="avatar avatar-5xl"><img class="rounded-circle" src=".{% static 'images/team/15.webp' %}" alt="" /></div>
|
<div class="avatar avatar-5xl"><img class="rounded-circle" src="{% static "images/team/15.webp" %}" alt="" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-sm-auto flex-1">
|
<div class="col-12 col-sm-auto flex-1">
|
||||||
<h3>{{ customer.first_name }} {{ customer.middle_name }} {{ customer.last_name }}</h3>
|
<h3>{{ customer.first_name }} {{ customer.middle_name }} {{ customer.last_name }}</h3>
|
||||||
|
|||||||
@ -290,5 +290,23 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
const profitData = [
|
||||||
|
[350000, 390000, 410700, 450000, 390000, 410700],
|
||||||
|
[245000, 310000, 420000, 480000, 530000, 580000],
|
||||||
|
[278450, 513220, 359890, 444567, 201345, 589000]
|
||||||
|
];
|
||||||
|
const revenueData = [
|
||||||
|
[-810000, -640000, -630000, -590000, -620000, -780000],
|
||||||
|
[-482310, -726590, -589120, -674832, -811245, -455678],
|
||||||
|
[-432567, -688921, -517389, -759234, -601876, -485112]
|
||||||
|
];
|
||||||
|
const expansesData = [
|
||||||
|
[-450000, -250000, -200000, -120000, -230000, -270000],
|
||||||
|
[-243567, -156789, -398234, -120456, -321890, -465678],
|
||||||
|
[-235678, -142345, -398765, -287456, -173890, -451234]
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -178,127 +178,7 @@
|
|||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link" href="#" style="min-width: 2.25rem" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="outside"><span class="d-block" style="height:20px;width:20px;"><span data-feather="bell" style="height:20px;width:20px;"></span></span></a>
|
<a class="nav-link" href="#" style="min-width: 2.25rem" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-auto-close="outside"><span class="d-block" style="height:20px;width:20px;"><span data-feather="bell" style="height:20px;width:20px;"></span></span></a>
|
||||||
|
|
||||||
<div class="dropdown-menu dropdown-menu-end notification-dropdown-menu py-0 shadow border navbar-dropdown-caret" id="navbarDropdownNotfication" aria-labelledby="navbarDropdownNotfication">
|
{% include 'notifications.html' %}
|
||||||
<div class="card position-relative border-0">
|
|
||||||
<div class="card-header p-2">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<h5 class="text-body-emphasis mb-0">Notifications</h5>
|
|
||||||
<button class="btn btn-link p-0 fs-9 fw-normal" type="button">Mark all as read</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-0">
|
|
||||||
<div class="scrollbar-overlay" style="height: 27rem;">
|
|
||||||
<div class="px-2 px-sm-3 py-3 notification-card position-relative read border-bottom">
|
|
||||||
<div class="d-flex align-items-center justify-content-between position-relative">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="avatar avatar-m status-online me-3"><img class="rounded-circle" src="{% static 'images/team/40x40/30.webp' %}" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 me-sm-3">
|
|
||||||
<h4 class="fs-9 text-body-emphasis">Jessie Samson</h4>
|
|
||||||
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'>💬</span>Mentioned you in a comment.<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">10m</span></p>
|
|
||||||
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 fas fa-clock"></span><span class="fw-bold">10:41 AM </span>August 7,2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown notification-dropdown">
|
|
||||||
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
|
||||||
<div class="dropdown-menu py-2"><a class="dropdown-item" href="#!">Mark as unread</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-2 px-sm-3 py-3 notification-card position-relative unread border-bottom">
|
|
||||||
<div class="d-flex align-items-center justify-content-between position-relative">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="avatar avatar-m status-online me-3">
|
|
||||||
<div class="avatar-name rounded-circle"><span>J</span></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 me-sm-3">
|
|
||||||
<h4 class="fs-9 text-body-emphasis">Jane Foster</h4>
|
|
||||||
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'>📅</span>Created an event.<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">20m</span></p>
|
|
||||||
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 fas fa-clock"></span><span class="fw-bold">10:20 AM </span>August 7,2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown notification-dropdown">
|
|
||||||
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
|
||||||
<div class="dropdown-menu py-2"><a class="dropdown-item" href="#!">Mark as unread</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-2 px-sm-3 py-3 notification-card position-relative unread border-bottom">
|
|
||||||
<div class="d-flex align-items-center justify-content-between position-relative">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="avatar avatar-m status-online me-3"><img class="rounded-circle avatar-placeholder" src="{% static 'images/team/40x40/avatar.webp' %}" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 me-sm-3">
|
|
||||||
<h4 class="fs-9 text-body-emphasis">Jessie Samson</h4>
|
|
||||||
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'>👍</span>Liked your comment.<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">1h</span></p>
|
|
||||||
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 fas fa-clock"></span><span class="fw-bold">9:30 AM </span>August 7,2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown notification-dropdown">
|
|
||||||
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
|
||||||
<div class="dropdown-menu py-2"><a class="dropdown-item" href="#!">Mark as unread</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-2 px-sm-3 py-3 notification-card position-relative unread border-bottom">
|
|
||||||
<div class="d-flex align-items-center justify-content-between position-relative">
|
|
||||||
<div class="d-flex">
|
|
||||||
|
|
||||||
<div class="avatar avatar-m status-online me-3"><img class="rounded-circle" src="{% static 'images/team/40x40/57.webp' %}" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 me-sm-3">
|
|
||||||
<h4 class="fs-9 text-body-emphasis">{{ user.dealer.get_local_name }}</h4>
|
|
||||||
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'>💬</span>Mentioned you in a comment.<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10"></span></p>
|
|
||||||
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 fas fa-clock"></span><span class="fw-bold">9:11 AM </span>August 7,2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown notification-dropdown">
|
|
||||||
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
|
||||||
<div class="dropdown-menu py-2"><a class="dropdown-item" href="#!">Mark as unread</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-2 px-sm-3 py-3 notification-card position-relative unread border-bottom">
|
|
||||||
<div class="d-flex align-items-center justify-content-between position-relative">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="avatar avatar-m status-online me-3"><img class="rounded-circle" src="{% static 'images/team/40x40/59.webp' %}" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 me-sm-3">
|
|
||||||
<h4 class="fs-9 text-body-emphasis">Herman Carter</h4>
|
|
||||||
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'>👤</span>Tagged you in a comment.<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10"></span></p>
|
|
||||||
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 fas fa-clock"></span><span class="fw-bold">10:58 PM </span>August 7,2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown notification-dropdown">
|
|
||||||
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
|
||||||
<div class="dropdown-menu py-2"><a class="dropdown-item" href="#!">Mark as unread</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-2 px-sm-3 py-3 notification-card position-relative read ">
|
|
||||||
<div class="d-flex align-items-center justify-content-between position-relative">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="avatar avatar-m status-online me-3"><img class="rounded-circle" src="{% static 'images/team/40x40/58.webp' %}" alt="" />
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 me-sm-3">
|
|
||||||
<h4 class="fs-9 text-body-emphasis">Benjamin Button</h4>
|
|
||||||
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'>👍</span>Liked your comment.<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10"></span></p>
|
|
||||||
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 fas fa-clock"></span><span class="fw-bold">10:18 AM </span>August 7,2021</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown notification-dropdown">
|
|
||||||
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
|
||||||
<div class="dropdown-menu py-2"><a class="dropdown-item" href="#!">Mark as unread</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer p-0 border-top border-translucent border-0">
|
|
||||||
<div class="my-2 text-center fw-bold fs-10 text-body-tertiary text-opactity-85"><a class="fw-bolder" href="../pages/notifications.html">Notification history</a></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" aria-haspopup="true">
|
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside" aria-haspopup="true">
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
border-color: #000;
|
border-color: #000;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="content">
|
<div class="container">
|
||||||
<div class="pb-5">
|
<div class="pb-5">
|
||||||
<!-- Custom Card Modal -->
|
<!-- Custom Card Modal -->
|
||||||
<div class="modal fade" id="customCardModal" tabindex="-1" aria-labelledby="customCardModalLabel" aria-hidden="true">
|
<div class="modal fade" id="customCardModal" tabindex="-1" aria-labelledby="customCardModalLabel" aria-hidden="true">
|
||||||
@ -85,8 +85,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="card rounded shadow d-flex align-content-center">
|
<div class="card rounded shadow d-flex align-content-center">
|
||||||
<div class="card overflow-hidden m-3 " style="max-width:35rem;">
|
<div class="card overflow-hidden m-3 " style="max-width:25rem;">
|
||||||
<img class="card-img-top shadow-info" src="{% static 'images/generic/4.jpg' %}" alt="...">
|
<img class="card-img-top shadow-info" src="{% static 'images/generic/2022-lincoln-corsair.jpg' %}" alt="...">
|
||||||
<div class="card-img-overlay d-flex align-items-end">
|
<div class="card-img-overlay d-flex align-items-end">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="card-title">{{ car.year }} - {{ car.id_car_make.get_local_name }}</h4>
|
<h4 class="card-title">{{ car.year }} - {{ car.id_car_make.get_local_name }}</h4>
|
||||||
|
|||||||
41
templates/notifications.html
Normal file
41
templates/notifications.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
<div class="dropdown-menu dropdown-menu-end notification-dropdown-menu py-0 shadow border navbar-dropdown-caret" id="navbarDropdownNotfication" aria-labelledby="navbarDropdownNotfication">
|
||||||
|
<div class="card position-relative border-0">
|
||||||
|
<div class="card-header p-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<h5 class="text-body-emphasis mb-0">Notifications</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="scrollbar-overlay" style="height: 27rem;">
|
||||||
|
|
||||||
|
{% for notification in notifications %}
|
||||||
|
{% if not notification.is_read %}
|
||||||
|
<div class="px-2 px-sm-3 py-3 notification-card position-relative border-bottom">
|
||||||
|
<div class="d-flex align-items-center justify-content-between position-relative">
|
||||||
|
<div class="d-flex">
|
||||||
|
|
||||||
|
<div class="flex-1 me-sm-3">
|
||||||
|
<h4 class="fs-9 text-body-emphasis">{{ _("System") }}</h4>
|
||||||
|
<p class="fs-9 text-body-highlight mb-2 mb-sm-3 fw-normal"><span class='me-1 fs-10'><span class=""></span></span>{{ notification.message }}<span class="ms-2 text-body-quaternary text-opacity-75 fw-bold fs-10">{{ notification.created_at|timesince }}</span></p>
|
||||||
|
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 far fa-clock"></span>{{ notification.created_at }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown notification-dropdown">
|
||||||
|
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
||||||
|
<div class="dropdown-menu py-2"><a class="dropdown-item" href="{% url 'mark_notification_as_read' notification.pk %}">Mark as Read</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<p>No new notifications.</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer p-0 border-top border-translucent border-0">
|
||||||
|
<div class="my-2 text-center fw-bold fs-10 text-body-tertiary text-opactity-85"><a class="fw-bolder" href="{% url 'notifications_history' %}">Notification history</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
51
templates/notifications_history.html
Normal file
51
templates/notifications_history.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h2 class="mb-5">{{ _("Notifications") }}</h2>
|
||||||
|
{% if notifications %}
|
||||||
|
<div class="mx-n4 mx-lg-n6 mb-5 border-bottom">
|
||||||
|
{% for notification in notifications %}
|
||||||
|
<div class="d-flex align-items-center justify-content-between py-3 px-lg-6 px-4 notification-card border-top">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="me-3 flex-1 mt-2">
|
||||||
|
<h4 class="fs-9 text-body-emphasis">{{ _("System")}}:</h4>
|
||||||
|
{% if not notification.is_read %}
|
||||||
|
<p class="fs-9 text-body-highlight"><span class="far fa-envelope text-success-dark fs-8 me-1"></span><span class="me-1">{{ notification.message }}</span> <span class="ms-2 text-body-tertiary text-opacity-85 fw-bold fs-10 text-end">{{ notification.created_at|timesince }}</span></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="fs-9 text-body-highlight"><span class="far fa-envelope-open text-danger-dark fs-8 me-1"></span><span>{{ notification.message }}</span> <span class="ms-2 text-body-tertiary text-opacity-85 fw-bold fs-10 text-end">{{ notification.created_at|timesince }}</span></p>
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-body-secondary fs-9 mb-0"><span class="me-1 far fa-clock"></span>{{ notification.created_at }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn fs-10 btn-sm dropdown-toggle dropdown-caret-none transition-none notification-dropdown-toggle" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10 text-body"></span></button>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end py-2"><a class="dropdown-item" href="{% url 'mark_notification_as_read' notification.pk %}">{{ _("Mark as Read")}}</a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pagination">
|
||||||
|
<span class="step-links">
|
||||||
|
{% if notifications.has_previous %}
|
||||||
|
<a href="?page=1">« first</a>
|
||||||
|
<a href="?page={{ notifications.previous_page_number }}">previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="current">
|
||||||
|
Page {{ notifications.number }} of {{ notifications.paginator.num_pages }}.
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% if notifications.has_next %}
|
||||||
|
<a href="?page={{ notifications.next_page_number }}">next</a>
|
||||||
|
<a href="?page={{ notifications.paginator.num_pages }}">last »</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p>No notifications found.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
@ -55,4 +55,129 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
// Function to calculate Total Cost and Total Revenue
|
||||||
|
function calculateTotals(container) {
|
||||||
|
const quantity = parseFloat(container.querySelector('.quantity').value) || 0;
|
||||||
|
const unitCost = parseFloat(container.querySelector('.unitCost').value) || 0;
|
||||||
|
const unitSalesPrice = parseFloat(container.querySelector('.unitSalesPrice').value) || 0;
|
||||||
|
|
||||||
|
const totalCost = quantity * unitCost;
|
||||||
|
const totalRevenue = quantity * unitSalesPrice;
|
||||||
|
|
||||||
|
container.querySelector('.totalCost').value = totalCost.toFixed(2);
|
||||||
|
container.querySelector('.totalRevenue').value = totalRevenue.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners to inputs for dynamic calculation
|
||||||
|
function addInputListeners(container) {
|
||||||
|
container.querySelectorAll('.quantity, .unitCost, .unitSalesPrice').forEach(input => {
|
||||||
|
input.addEventListener('input', () => calculateTotals(container));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new form fields
|
||||||
|
document.getElementById('addMoreBtn').addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const formContainer = document.getElementById('formContainer');
|
||||||
|
const newForm = document.createElement('div');
|
||||||
|
newForm.className = 'form-container row g-3 mb-3 mt-5';
|
||||||
|
newForm.innerHTML = `
|
||||||
|
<div class="mb-2 col-sm-2">
|
||||||
|
<select class="form-control item" name="item[]" required>
|
||||||
|
{% for item in items %}
|
||||||
|
<option value="{{ item.pk }}">{{ item.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col-sm-2">
|
||||||
|
<input class="form-control quantity" type="number" placeholder="Quantity" name="quantity[]" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col-sm-2">
|
||||||
|
<input class="form-control unitCost" type="number" placeholder="Unit Cost" name="unitCost[]" step="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col-sm-2">
|
||||||
|
<input class="form-control unitSalesPrice" type="number" placeholder="Unit Sales Price" name="unitSalesPrice[]" step="0.01" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col-sm-2">
|
||||||
|
<input class="form-control totalCost" type="number" placeholder="Total Cost" name="totalCost[]" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col-sm-1">
|
||||||
|
<input class="form-control totalRevenue" type="number" placeholder="Total Revenue" name="totalRevenue[]" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2 col-sm-1">
|
||||||
|
<button class="btn btn-danger removeBtn">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
formContainer.appendChild(newForm);
|
||||||
|
addInputListeners(newForm); // Add listeners to the new form
|
||||||
|
|
||||||
|
// Add remove button functionality
|
||||||
|
newForm.querySelector('.removeBtn').addEventListener('click', function() {
|
||||||
|
newForm.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add listeners to the initial form
|
||||||
|
document.querySelectorAll('.form-container').forEach(container => {
|
||||||
|
addInputListeners(container);
|
||||||
|
|
||||||
|
// Add remove button functionality to the initial form
|
||||||
|
container.querySelector('.removeBtn').addEventListener('click', function() {
|
||||||
|
container.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const url = "{% url 'estimate_create' %}"
|
||||||
|
document.getElementById('mainForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Collect all form data
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const csrfToken = getCookie('csrftoken');
|
||||||
|
const data = {};
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
// Handle multi-value fields (e.g., item[], quantity[])
|
||||||
|
if (data[key]) {
|
||||||
|
if (!Array.isArray(data[key])) {
|
||||||
|
data[key] = [data[key]]; // Convert to array
|
||||||
|
}
|
||||||
|
data[key].push(value);
|
||||||
|
} else {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Send data to the server using fetch
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': csrfToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
console.log('Success:', data);
|
||||||
|
if(data.status == "error"){
|
||||||
|
notify("error",data.message);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
notify("success","Estimate created successfully");
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = data.url;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
notify("error",error);
|
||||||
|
alert('An error occurred while submitting the form.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -168,13 +168,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- end of .container-->
|
</div>
|
||||||
|
|
||||||
</section>
|
|
||||||
<!-- <section> close ============================-->
|
|
||||||
<!-- ============================================-->
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
@ -206,5 +201,5 @@
|
|||||||
// Run the function on page load
|
// Run the function on page load
|
||||||
window.onload = calculateTotals;
|
window.onload = calculateTotals;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user