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.Staff)
admin.site.register(models.Vendor)
admin.site.register(models.Customer)
admin.site.register(models.SaleQuotation)
admin.site.register(models.SaleQuotationCar)
admin.site.register(models.SalesOrder)
@ -28,6 +27,9 @@ admin.site.register(models.CarTrim)
admin.site.register(models.AdditionalServices)
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)
class CarMakeAdmin(admin.ModelAdmin):
@ -96,3 +98,4 @@ class CarSpecificationAdmin(admin.ModelAdmin):
# list_display = ('user', 'action', 'timestamp')
# search_fields = ('user__username', 'action')
# list_filter = ('timestamp',)

View File

@ -23,7 +23,8 @@ from .models import (
Payment,
SaleQuotationCar,
AdditionalServices,
Staff
Staff,
Opportunity
)
from django_ledger.models import ItemModel
@ -434,3 +435,17 @@ class ItemForm(forms.Form):
unit = forms.DecimalField(label="Unit", required=True)
unit_cost = forms.DecimalField(label="Unit Cost", 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")
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
is_lead = models.BooleanField(default=True, verbose_name=_("Is Lead"))
class Meta:
verbose_name = _("Customer")
@ -700,6 +701,63 @@ class Customer(models.Model):
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):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name='organizations')
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.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')),
#Dashboards
path('dashboards/accounting/', views.AccountingDashboard.as_view(), name='accounting'),
path('dashboards/crm/', views.notifications_view, name='staff_dashboard'),
# Dealer URLs
path('dealers/<int:pk>/', views.DealerDetailView.as_view(), name='dealer_detail'),
@ -40,6 +41,16 @@ urlpatterns = [
path('customers/create/', views.CustomerCreateView.as_view(), name='customer_create'),
path('customers/<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>/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/<int:pk>/', views.VendorDetailView.as_view(), name='vendor_detail'),
@ -48,12 +59,8 @@ urlpatterns = [
path('vendors/<int:pk>/delete/', views.VendorDetailView.as_view(), name='vendor_delete'),
# Car URLs
path('cars/inventory/',
views.CarInventory.as_view(),
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/', views.CarInventory.as_view(), 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/<int:pk>/', views.CarDetailView.as_view(), name='car_detail'),
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.paginator import Paginator
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django_ledger.models import EntityModel, InvoiceModel,BankAccountModel,AccountModel,JournalEntryModel,TransactionModel,EstimateModel,CustomerModel
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 import transaction
from .models import Customer
from .services import (
elm,
decodevin,
@ -1879,3 +1881,85 @@ class UserActivityLogListView(ListView):
def record_payment(request):
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){
Toast.fire({
icon: tag,

View File

@ -653,21 +653,6 @@
'.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) {
const userOptions = getData($financialActivitiesChartEl, 'options');

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@ -55,128 +55,7 @@
</main>
{% 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 'js/phoenix.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 'vendors/mapbox-gl/mapbox-gl.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>
</div>
<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>
@ -64,7 +69,7 @@
<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="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 class="col-12 col-sm-auto flex-1">
<h3>{{ customer.first_name }} {{ customer.middle_name }} {{ customer.last_name }}</h3>

View File

@ -291,14 +291,8 @@
</div>
<script>
const financialActivitiesChartInit = () => {
const { getColor, getData, getItemFromStore } = window.phoenix.utils;
const $financialActivitiesChartEl = document.querySelector(
'.echart-financial-Activities'
);
const profitData = [
[9, 8678, 2122, 99898998, 767, 1],
[350000, 390000, 410700, 450000, 390000, 410700],
[245000, 310000, 420000, 480000, 530000, 580000],
[278450, 513220, 359890, 444567, 201345, 589000]
];
@ -313,249 +307,6 @@ const financialActivitiesChartInit = () => {
[-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>
{% endblock %}

View File

@ -178,127 +178,7 @@
<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>
<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>
<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>
{% include 'notifications.html' %}
</li>
<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">

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