This commit is contained in:
Marwan Alwali 2024-12-31 13:56:15 +03:00
parent d29982d175
commit 8b00f9a40f
28 changed files with 574 additions and 534 deletions

BIN
db.sqlite

Binary file not shown.

View File

@ -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',)

View File

@ -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
@ -433,4 +434,18 @@ class ItemForm(forms.Form):
quantity = forms.DecimalField(label="Quantity", required=True) quantity = forms.DecimalField(label="Quantity", required=True)
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),
}

View File

@ -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',
},
),
]

View File

@ -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,
),
]

View File

@ -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')

View File

@ -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)

View File

@ -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'),

View File

@ -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,
@ -1878,4 +1880,86 @@ 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')

View File

@ -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,

View File

@ -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

Binary file not shown.

View File

@ -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>

View 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 %}

View 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>

View 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>

View 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 %}

View File

@ -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>

View File

@ -291,14 +291,8 @@
</div> </div>
<script> <script>
const financialActivitiesChartInit = () => { const profitData = [
const { getColor, getData, getItemFromStore } = window.phoenix.utils; [350000, 390000, 410700, 450000, 390000, 410700],
const $financialActivitiesChartEl = document.querySelector(
'.echart-financial-Activities'
);
const profitData = [
[9, 8678, 2122, 99898998, 767, 1],
[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 %}

View File

@ -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">

View 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>

View 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">&laquo; 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 &raquo;</a>
{% endif %}
</span>
</div>
{% else %}
<p>No notifications found.</p>
{% endif %}
{% endblock %}

View File

@ -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 %}