update
This commit is contained in:
parent
d29982d175
commit
8b00f9a40f
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)
|
||||||
@ -28,6 +27,9 @@ 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.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):
|
||||||
@ -96,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',)
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,8 @@ from .models import (
|
|||||||
Payment,
|
Payment,
|
||||||
SaleQuotationCar,
|
SaleQuotationCar,
|
||||||
AdditionalServices,
|
AdditionalServices,
|
||||||
Staff
|
Staff,
|
||||||
|
Opportunity
|
||||||
|
|
||||||
)
|
)
|
||||||
from django_ledger.models import ItemModel
|
from django_ledger.models import ItemModel
|
||||||
@ -434,3 +435,17 @@ class ItemForm(forms.Form):
|
|||||||
unit = forms.DecimalField(label="Unit", required=True)
|
unit = forms.DecimalField(label="Unit", required=True)
|
||||||
unit_cost = forms.DecimalField(label="Unit Cost", required=True)
|
unit_cost = forms.DecimalField(label="Unit Cost", required=True)
|
||||||
unit_sales_price = forms.DecimalField(label="Unit Sales Price", required=True)
|
unit_sales_price = forms.DecimalField(label="Unit Sales Price", required=True)
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
@ -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")
|
||||||
@ -700,6 +701,63 @@ class Customer(models.Model):
|
|||||||
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')
|
||||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||||
|
|||||||
@ -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,6 +41,16 @@ 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'),
|
||||||
|
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
|
#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'),
|
||||||
@ -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.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
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
|
from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel
|
||||||
from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm
|
from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm
|
||||||
@ -36,6 +37,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,
|
||||||
@ -1879,3 +1881,85 @@ class UserActivityLogListView(ListView):
|
|||||||
|
|
||||||
def record_payment(request):
|
def record_payment(request):
|
||||||
invoice = get_object_or_404(InvoiceModel, pk=request.POST.get('invoice'))
|
invoice = get_object_or_404(InvoiceModel, pk=request.POST.get('invoice'))
|
||||||
|
|
||||||
|
|
||||||
|
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 = [
|
|
||||||
[9, 8678, 2122, 99898998, 767, 1],
|
|
||||||
[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>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ===============================================-->
|
<!-- ===============================================-->
|
||||||
@ -193,7 +72,7 @@ const url = "{% url 'estimate_create' %}"
|
|||||||
<script src="{% static 'vendors/dayjs/dayjs.min.js' %}"></script>
|
<script src="{% static 'vendors/dayjs/dayjs.min.js' %}"></script>
|
||||||
<script src="{% static 'js/phoenix.js' %}"></script>
|
<script src="{% static 'js/phoenix.js' %}"></script>
|
||||||
<script src="{% static 'vendors/echarts/echarts.min.js' %}"></script>
|
<script src="{% static 'vendors/echarts/echarts.min.js' %}"></script>
|
||||||
|
<script src="{% static 'js/travel-agency-dashboard.js' %}"></script>
|
||||||
<script src="{% static 'js/main.js' %}"></script>
|
<script src="{% static 'js/main.js' %}"></script>
|
||||||
<script src="{% static 'vendors/mapbox-gl/mapbox-gl.js' %}"></script>
|
<script src="{% static 'vendors/mapbox-gl/mapbox-gl.js' %}"></script>
|
||||||
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></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>
|
||||||
|
|||||||
@ -291,14 +291,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const financialActivitiesChartInit = () => {
|
|
||||||
const { getColor, getData, getItemFromStore } = window.phoenix.utils;
|
|
||||||
const $financialActivitiesChartEl = document.querySelector(
|
|
||||||
'.echart-financial-Activities'
|
|
||||||
);
|
|
||||||
|
|
||||||
const profitData = [
|
const profitData = [
|
||||||
[9, 8678, 2122, 99898998, 767, 1],
|
[350000, 390000, 410700, 450000, 390000, 410700],
|
||||||
[245000, 310000, 420000, 480000, 530000, 580000],
|
[245000, 310000, 420000, 480000, 530000, 580000],
|
||||||
[278450, 513220, 359890, 444567, 201345, 589000]
|
[278450, 513220, 359890, 444567, 201345, 589000]
|
||||||
];
|
];
|
||||||
@ -313,249 +307,6 @@ const financialActivitiesChartInit = () => {
|
|||||||
[-235678, -142345, -398765, -287456, -173890, -451234]
|
[-235678, -142345, -398765, -287456, -173890, -451234]
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($financialActivitiesChartEl) {
|
|
||||||
const userOptions = getData($financialActivitiesChartEl, 'options');
|
|
||||||
const chart = window.echarts.init($financialActivitiesChartEl);
|
|
||||||
const profitLagend = document.querySelector(`#${userOptions.optionOne}`);
|
|
||||||
const revenueLagend = document.querySelector(`#${userOptions.optionTwo}`);
|
|
||||||
const expansesLagend = document.querySelector(
|
|
||||||
`#${userOptions.optionThree}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const getDefaultOptions = () => ({
|
|
||||||
color: [getColor('primary'), getColor('tertiary-bg')],
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
|
||||||
padding: [7, 10],
|
|
||||||
backgroundColor: getColor('body-highlight-bg'),
|
|
||||||
borderColor: getColor('border-color'),
|
|
||||||
textStyle: { color: getColor('light-text-emphasis') },
|
|
||||||
borderWidth: 1,
|
|
||||||
transitionDuration: 0,
|
|
||||||
axisPointer: {
|
|
||||||
type: 'none'
|
|
||||||
},
|
|
||||||
position: (...params) => handleTooltipPosition(params),
|
|
||||||
formatter: params => tooltipFormatter(params),
|
|
||||||
extraCssText: 'z-index: 1000'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
data: ['Profit', 'Revenue', 'Expanses'],
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
axisLabel: {
|
|
||||||
show: true,
|
|
||||||
margin: 12,
|
|
||||||
color: getColor('secondary-text-emphasis'),
|
|
||||||
formatter: value =>
|
|
||||||
`${Math.abs(Math.round((value / 1000) * 10) / 10)}k`,
|
|
||||||
fontFamily: 'Nunito Sans',
|
|
||||||
fontWeight: 700
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: getColor('border-color-translucent')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
axisTick: {
|
|
||||||
show: false
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
'NOV-DEC',
|
|
||||||
'SEP-OCT',
|
|
||||||
'JUL-AUG',
|
|
||||||
'MAY-JUN',
|
|
||||||
'MAR-APR',
|
|
||||||
'JAN-FEB'
|
|
||||||
],
|
|
||||||
axisLabel: {
|
|
||||||
color: getColor('secondary-text-emphasis'),
|
|
||||||
margin: 8,
|
|
||||||
fontFamily: 'Nunito Sans',
|
|
||||||
fontWeight: 700
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: getColor('border-color-translucent')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: 'Profit',
|
|
||||||
stack: 'Total',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: 8,
|
|
||||||
roundCap: true,
|
|
||||||
emphasis: {
|
|
||||||
focus: 'series'
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: [0, 4, 4, 0],
|
|
||||||
color:
|
|
||||||
getItemFromStore('phoenixTheme') === 'dark'
|
|
||||||
? getColor('primary')
|
|
||||||
: getColor('primary-light')
|
|
||||||
},
|
|
||||||
data: profitData[0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Revenue',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: 8,
|
|
||||||
barGap: '100%',
|
|
||||||
stack: 'Total',
|
|
||||||
emphasis: {
|
|
||||||
focus: 'series'
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: [4, 0, 0, 4],
|
|
||||||
color:
|
|
||||||
getItemFromStore('phoenixTheme') === 'dark'
|
|
||||||
? getColor('success')
|
|
||||||
: getColor('success-light')
|
|
||||||
},
|
|
||||||
data: revenueData[0]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Expanses',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: 8,
|
|
||||||
emphasis: {
|
|
||||||
focus: 'series'
|
|
||||||
},
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: [4, 0, 0, 4],
|
|
||||||
color:
|
|
||||||
getItemFromStore('phoenixTheme') === 'dark'
|
|
||||||
? getColor('info')
|
|
||||||
: getColor('info-light')
|
|
||||||
},
|
|
||||||
data: expansesData[0]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
grid: {
|
|
||||||
right: 20,
|
|
||||||
left: 3,
|
|
||||||
bottom: 0,
|
|
||||||
top: 16,
|
|
||||||
containLabel: true
|
|
||||||
},
|
|
||||||
animation: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const responsiveOptions = {
|
|
||||||
xs: {
|
|
||||||
yAxis: {
|
|
||||||
axisLabel: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
yAxis: {
|
|
||||||
axisLabel: {
|
|
||||||
margin: 32,
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xl: {
|
|
||||||
yAxis: {
|
|
||||||
axisLabel: {
|
|
||||||
show: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xxl: {
|
|
||||||
yAxis: {
|
|
||||||
axisLabel: {
|
|
||||||
show: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
echartSetOption(chart, userOptions, getDefaultOptions, responsiveOptions);
|
|
||||||
|
|
||||||
profitLagend.addEventListener('click', () => {
|
|
||||||
profitLagend.classList.toggle('opacity-50');
|
|
||||||
chart.dispatchAction({
|
|
||||||
type: 'legendToggleSelect',
|
|
||||||
name: 'Profit'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
revenueLagend.addEventListener('click', () => {
|
|
||||||
revenueLagend.classList.toggle('opacity-50');
|
|
||||||
chart.dispatchAction({
|
|
||||||
type: 'legendToggleSelect',
|
|
||||||
name: 'Revenue'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expansesLagend.addEventListener('click', () => {
|
|
||||||
expansesLagend.classList.toggle('opacity-50');
|
|
||||||
chart.dispatchAction({
|
|
||||||
type: 'legendToggleSelect',
|
|
||||||
name: 'Expanses'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const cetegorySelect = document.querySelector('[data-activities-options]');
|
|
||||||
if (cetegorySelect) {
|
|
||||||
cetegorySelect.addEventListener('change', e => {
|
|
||||||
const { value } = e.currentTarget;
|
|
||||||
const data1 = profitData[value];
|
|
||||||
const data2 = revenueData[value];
|
|
||||||
const data3 = expansesData[value];
|
|
||||||
chart.setOption({
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
data: data1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: data2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: data3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { docReady } = window.phoenix.utils;
|
|
||||||
|
|
||||||
docReady(bookingValueChartInit);
|
|
||||||
docReady(commissionChartInit);
|
|
||||||
docReady(cancelBookingChartInit);
|
|
||||||
docReady(countryWiseVisitorsChartInit);
|
|
||||||
docReady(financialActivitiesChartInit);
|
|
||||||
docReady(holidaysNextMonthChartInit);
|
|
||||||
docReady(bookingsChartInit);
|
|
||||||
docReady(grossProfitInit);
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</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">
|
||||||
|
|||||||
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 %}
|
||||||
Loading…
x
Reference in New Issue
Block a user