This commit is contained in:
gitea 2024-12-31 15:44:49 +00:00
commit 7b167e5756
31 changed files with 620 additions and 293 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)
@ -27,6 +26,10 @@ admin.site.register(models.Representative)
admin.site.register(models.CarTrim) admin.site.register(models.CarTrim)
admin.site.register(models.AdditionalServices) admin.site.register(models.AdditionalServices)
admin.site.register(models.Payment) admin.site.register(models.Payment)
admin.site.register(models.VatRate)
admin.site.register(models.Customer)
admin.site.register(models.Opportunity)
admin.site.register(models.Notification)
@admin.register(models.CarMake) @admin.register(models.CarMake)
class CarMakeAdmin(admin.ModelAdmin): class CarMakeAdmin(admin.ModelAdmin):
@ -95,3 +98,4 @@ class CarSpecificationAdmin(admin.ModelAdmin):
# list_display = ('user', 'action', 'timestamp') # list_display = ('user', 'action', 'timestamp')
# search_fields = ('user__username', 'action') # search_fields = ('user__username', 'action')
# list_filter = ('timestamp',) # list_filter = ('timestamp',)

View File

@ -24,6 +24,11 @@ from .models import (
SaleQuotationCar, SaleQuotationCar,
AdditionalServices, AdditionalServices,
Staff, Staff,
<<<<<<< HEAD
=======
Opportunity
>>>>>>> 8b00f9a40fc336f209f4ae6fb03785df6c97d265
) )
from django_ledger.models import ItemModel, InvoiceModel from django_ledger.models import ItemModel, InvoiceModel
from django.forms import ModelMultipleChoiceField, ValidationError from django.forms import ModelMultipleChoiceField, ValidationError
@ -527,6 +532,7 @@ class ItemForm(forms.Form):
unit_sales_price = forms.DecimalField(label="Unit Sales Price", required=True) unit_sales_price = forms.DecimalField(label="Unit Sales Price", required=True)
<<<<<<< HEAD
class PaymentForm(forms.Form): class PaymentForm(forms.Form):
invoice = forms.ModelChoiceField( invoice = forms.ModelChoiceField(
queryset=InvoiceModel.objects.all(), label="Invoice", required=True queryset=InvoiceModel.objects.all(), label="Invoice", required=True
@ -553,3 +559,17 @@ class EmailForm(forms.Form):
message = forms.CharField(widget=forms.Textarea) message = forms.CharField(widget=forms.Textarea)
from_email = forms.EmailField() from_email = forms.EmailField()
to_email = forms.EmailField() to_email = forms.EmailField()
=======
class OpportunityForm(forms.ModelForm):
class Meta:
model = Opportunity
fields = [
'car', 'deal_name', 'deal_value', 'deal_status',
'priority', 'source'
]
widgets = {
'deal_status': forms.Select(choices=Opportunity.DEAL_STATUS_CHOICES),
'priority': forms.Select(choices=Opportunity.PRIORITY_CHOICES),
'source': forms.Select(choices=Opportunity.DEAL_SOURCES_CHOICES),
}
>>>>>>> 8b00f9a40fc336f209f4ae6fb03785df6c97d265

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-30 15:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0015_merge_0008_vatrate_0014_useractivitylog'),
]
operations = [
migrations.AlterField(
model_name='staff',
name='staff_type',
field=models.CharField(choices=[('manager', 'Manager'), ('inventory', 'Inventory'), ('accountant', 'Accountant'), ('sales', 'Sales')], max_length=255, verbose_name='Staff Type'),
),
]

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

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.paginator import Paginator from django.core.paginator import Paginator
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel,LedgerModel from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel,LedgerModel
from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm from django_ledger.forms.bank_account import BankAccountCreateForm,BankAccountUpdateForm
@ -38,6 +39,7 @@ from django.contrib import messages
from django.db.models import Sum, F, Count from django.db.models import Sum, F, Count
from django.db import transaction from django.db import transaction
from .models import Customer
from .services import ( from .services import (
elm, elm,
decodevin, decodevin,
@ -1689,7 +1691,8 @@ class EstimateDetailView(LoginRequiredMixin, DetailView):
if estimate.get_itemtxs_data(): if estimate.get_itemtxs_data():
total = sum(x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all()) total = sum(x.ce_cost_estimate for x in estimate.get_itemtxs_data()[0].all())
vat = models.VatRate.objects.filter(is_active=True).first() vat = models.VatRate.objects.filter(is_active=True).first()
kwargs["vate_amount"] = (total * vat.vat_rate) # vat = settings.VAT_RATE
kwargs["vat_amount"] = (total * vat.vat_rate)
kwargs["total"] = (total * vat.vat_rate) + total kwargs["total"] = (total * vat.vat_rate) + total
kwargs["vat"] = vat.rate kwargs["vat"] = vat.rate
kwargs["invoice"] = InvoiceModel.objects.all().filter(ce_model=estimate).first() kwargs["invoice"] = InvoiceModel.objects.all().filter(ce_model=estimate).first()
@ -1874,7 +1877,7 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView):
total = sum(x.unit_cost * x.quantity for x in invoice.get_itemtxs_data()[0].all()) total = sum(x.unit_cost * x.quantity for x in invoice.get_itemtxs_data()[0].all())
total = int(total) total = int(total)
vat = models.VatRate.objects.filter(is_active=True).first() vat = models.VatRate.objects.filter(is_active=True).first()
kwargs["vate_amount"] = (total * vat.vat_rate) kwargs["vat_amount"] = (total * vat.vat_rate)
kwargs["total"] = (total * vat.vat_rate) + total kwargs["total"] = (total * vat.vat_rate) + total
kwargs["vat"] = vat.rate kwargs["vat"] = vat.rate
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -1995,3 +1998,85 @@ def send_email_view(request,pk):
estimate.mark_as_review() estimate.mark_as_review()
messages.success(request, "Email sent successfully!") messages.success(request, "Email sent successfully!")
return redirect("estimate_detail", pk=estimate.pk) return redirect("estimate_detail", pk=estimate.pk)
def create_lead(request, pk):
customer = get_object_or_404(models.Customer, pk=pk)
if customer.is_lead:
messages.warning(request, _('Customer is already a lead.'))
else:
customer.is_lead = True
customer.save()
messages.success(request, _('Customer successfully marked as a lead.'))
return redirect(reverse('customer_detail', kwargs={'pk': customer.pk}))
class OpportunityCreateView(CreateView):
model = models.Opportunity
form_class = forms.OpportunityForm
template_name = 'crm/opportunity_form.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['customer'] = models.Customer.objects.get(pk=self.kwargs['customer_id'])
context['cars'] = models.Car.objects.all()
return context
def form_valid(self, form):
form.instance.customer = models.Customer.objects.get(pk=self.kwargs['customer_id'])
form.instance.created_by = self.request.user.staff
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('opportunity_detail', kwargs={'pk': self.object.pk})
class OpportunityUpdateView(UpdateView):
model = models.Opportunity
form_class = forms.OpportunityForm
template_name = 'crm/opportunity_form.html'
def get_success_url(self):
return reverse_lazy('opportunity_detail', kwargs={'pk': self.object.pk})
class OpportunityDetailView(DetailView):
model = models.Opportunity
template_name = 'crm/opportunity_detail.html'
context_object_name = 'opportunity'
class OpportunityListView(ListView):
model = models.Opportunity
template_name = 'crm/opportunity_list.html'
context_object_name = 'opportunities'
class OpportunityDeleteView(DeleteView):
model = models.Opportunity
template_name = 'crm/opportunity_confirm_delete.html'
success_url = reverse_lazy('opportunity_list')
def notifications_view(request):
notifications = models.Notification.objects.filter(user=request.user, is_read=False).order_by('-created_at')
return render(request, 'notifications.html', {'notifications': notifications})
class NotificationListView(LoginRequiredMixin, ListView):
model = models.Notification
template_name = 'notifications_history.html'
context_object_name = 'notifications'
paginate_by = 10
def get_queryset(self):
return models.Notification.objects.filter(user=self.request.user).order_by('-created_at')
def mark_notification_as_read(request, pk):
notification = get_object_or_404(models.Notification, pk=pk)
notification.is_read = True
notification.save()
return redirect('notifications_history')

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 = [
[350000, 390000, 410700, 450000, 390000, 410700],
[245000, 310000, 420000, 480000, 530000, 580000],
[278450, 513220, 359890, 444567, 201345, 589000]
];
const revenueData = [
[-810000, -640000, -630000, -590000, -620000, -780000],
[-482310, -726590, -589120, -674832, -811245, -455678],
[-432567, -688921, -517389, -759234, -601876, -485112]
];
const expansesData = [
[-450000, -250000, -200000, -120000, -230000, -270000],
[-243567, -156789, -398234, -120456, -321890, -465678],
[-235678, -142345, -398765, -287456, -173890, -451234]
];
if ($financialActivitiesChartEl) { if ($financialActivitiesChartEl) {
const userOptions = getData($financialActivitiesChartEl, 'options'); const userOptions = getData($financialActivitiesChartEl, 'options');

BIN
templates/.DS_Store vendored

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>
<!-- ===============================================--> <!-- ===============================================-->

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

@ -290,5 +290,23 @@
</div> </div>
<script>
const profitData = [
[350000, 390000, 410700, 450000, 390000, 410700],
[245000, 310000, 420000, 480000, 530000, 580000],
[278450, 513220, 359890, 444567, 201345, 589000]
];
const revenueData = [
[-810000, -640000, -630000, -590000, -620000, -780000],
[-482310, -726590, -589120, -674832, -811245, -455678],
[-432567, -688921, -517389, -759234, -601876, -485112]
];
const expansesData = [
[-450000, -250000, -200000, -120000, -230000, -270000],
[-243567, -156789, -398234, -120456, -321890, -465678],
[-235678, -142345, -398765, -287456, -173890, -451234]
];
</script>
{% endblock %} {% endblock %}

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

@ -13,7 +13,7 @@
border-color: #000; border-color: #000;
} }
</style> </style>
<div class="content"> <div class="container">
<div class="pb-5"> <div class="pb-5">
<!-- Custom Card Modal --> <!-- Custom Card Modal -->
<div class="modal fade" id="customCardModal" tabindex="-1" aria-labelledby="customCardModalLabel" aria-hidden="true"> <div class="modal fade" id="customCardModal" tabindex="-1" aria-labelledby="customCardModalLabel" aria-hidden="true">
@ -85,8 +85,8 @@
<div class="card rounded shadow d-flex align-content-center"> <div class="card rounded shadow d-flex align-content-center">
<div class="card overflow-hidden m-3 " style="max-width:35rem;"> <div class="card overflow-hidden m-3 " style="max-width:25rem;">
<img class="card-img-top shadow-info" src="{% static 'images/generic/4.jpg' %}" alt="..."> <img class="card-img-top shadow-info" src="{% static 'images/generic/2022-lincoln-corsair.jpg' %}" alt="...">
<div class="card-img-overlay d-flex align-items-end"> <div class="card-img-overlay d-flex align-items-end">
<div> <div>
<h4 class="card-title">{{ car.year }} - {{ car.id_car_make.get_local_name }}</h4> <h4 class="card-title">{{ car.year }} - {{ car.id_car_make.get_local_name }}</h4>

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

View File

@ -170,11 +170,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- end of .container-->
</section>
<!-- <section> close ============================-->
<!-- ============================================-->
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}