update on the lead & opportunity

This commit is contained in:
ismail 2025-05-15 19:29:22 +03:00
parent cca37be3b3
commit 2ee9a86720
223 changed files with 15974 additions and 15114 deletions

10
.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always"
}

View File

@ -50,7 +50,8 @@ from .models import (
CarMake, CarMake,
Customer, Customer,
Organization, Organization,
DealerSettings DealerSettings,
Tasks
) )
from django_ledger import models as ledger_models from django_ledger import models as ledger_models
from django.forms import ( from django.forms import (
@ -1120,6 +1121,7 @@ class ActivityForm(forms.ModelForm):
associated with the form and the fields it comprises. associated with the form and the fields it comprises.
:type Meta: type :type Meta: type
""" """
activity_type = forms.ChoiceField(choices=[("call", "Call"), ("email", "Email"), ("meeting", "Meeting")])
class Meta: class Meta:
model = Activity model = Activity
fields = ["activity_type", "notes"] fields = ["activity_type", "notes"]
@ -1140,9 +1142,34 @@ class OpportunityForm(forms.ModelForm):
:ivar Meta.fields: List of fields from the model included in the form. :ivar Meta.fields: List of fields from the model included in the form.
:type Meta.fields: list :type Meta.fields: list
""" """
closing_date = forms.DateField(
label=_("Expected Closing Date"),
widget=forms.DateInput(attrs={"type": "date"})
)
probability = forms.IntegerField(
label=_("Probability (%)"),
widget=forms.NumberInput(attrs={
'type': 'range',
'min': '0',
'max': '100',
'step': '1',
'class': 'form-range',
'oninput': 'this.nextElementSibling.value = this.value'
}),
initial=50 # Default value
)
class Meta: class Meta:
model = Opportunity model = Opportunity
fields = ["customer", "car", "stage", "probability", "staff", "closing_date"] fields = ["lead", "car", "stage", "probability", "expected_revenue", "closing_date"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add a visible number input to display the current value
self.fields['probability'].widget.attrs['class'] = 'd-none' # Hide the default input
if self.instance and self.instance.pk:
self.fields['probability'].initial = self.instance.probability
class InvoiceModelCreateForm(InvoiceModelCreateFormBase): class InvoiceModelCreateForm(InvoiceModelCreateFormBase):
@ -1684,3 +1711,33 @@ class PaymentPlanForm(forms.Form):
self.fields['first_name'].initial = user.first_name self.fields['first_name'].initial = user.first_name
self.fields['last_name'].initial = user.last_name self.fields['last_name'].initial = user.last_name
self.fields['email'].initial = user.email self.fields['email'].initial = user.email
# class ActivityHistoryForm(forms.Form):
# activity_type = forms.ChoiceField(
# choices=[
# ('note', 'Note'),
# ('call', 'Call'),
# ('email', 'Email'),
# ('meeting', 'Meeting'),],
# widget=forms.Select(attrs={
# 'class': 'form-control',
# 'id': 'activity-type'
# }),
# label=_('Activity Type')
# )
# description = forms.CharField(
# widget=forms.Textarea(attrs={
# 'class': 'form-control',
# 'id': 'activity-description'
# }),
# label=_('Description')
# )
class StaffTaskForm(forms.ModelForm):
class Meta:
model = Tasks
fields = ['title','due_date' ,'description']
widgets = {
'due_date': forms.DateTimeInput(attrs={'type': 'date'}),
}

View File

@ -0,0 +1,44 @@
# Generated by Django 5.1.7 on 2025-05-13 15:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('inventory', '0017_alter_activity_activity_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='opportunity',
name='closed',
),
migrations.RemoveField(
model_name='opportunity',
name='status',
),
migrations.CreateModel(
name='Tasks',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.UUIDField()),
('title', models.CharField(max_length=255, verbose_name='Title')),
('description', models.TextField(blank=True, null=True, verbose_name='Description')),
('due_date', models.DateField(verbose_name='Due Date')),
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
('updated', models.DateTimeField(auto_now=True, verbose_name='Updated')),
('assigned_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_assigned', to=settings.AUTH_USER_MODEL)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='tasks_created', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Task',
'verbose_name_plural': 'Tasks',
},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.7 on 2025-05-13 16:22
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0018_remove_opportunity_closed_remove_opportunity_status_and_more'),
]
operations = [
migrations.AddField(
model_name='tasks',
name='dealer',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='inventory.dealer'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-13 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0019_tasks_dealer'),
]
operations = [
migrations.AddField(
model_name='tasks',
name='completed',
field=models.BooleanField(default=False, verbose_name='Completed'),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.1.7 on 2025-05-14 10:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0020_tasks_completed'),
]
operations = [
migrations.AlterField(
model_name='lead',
name='status',
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], db_index=True, default='new', max_length=50, verbose_name='Status'),
),
migrations.AlterField(
model_name='leadstatushistory',
name='new_status',
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='New Status'),
),
migrations.AlterField(
model_name='leadstatushistory',
name='old_status',
field=models.CharField(choices=[('new', 'New'), ('follow_up', 'Follow-up'), ('negotiation', 'Negotiation'), ('won', 'Won'), ('lost', 'Lost'), ('closed', 'Closed')], max_length=50, verbose_name='Old Status'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 5.1.7 on 2025-05-14 10:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0021_alter_lead_status_alter_leadstatushistory_new_status_and_more'),
]
operations = [
migrations.AddField(
model_name='opportunity',
name='expected_revenue',
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Expected Revenue'),
preserve_default=False,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-05-14 10:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0022_opportunity_expected_revenue'),
]
operations = [
migrations.AlterField(
model_name='opportunity',
name='stage',
field=models.CharField(choices=[('discovery', 'Discovery'), ('proposal', 'Proposal'), ('negotiation', 'Negotiation'), ('closed_won', 'Closed Won'), ('closed_lost', 'Closed Lost')], max_length=20, verbose_name='Stage'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.7 on 2025-05-14 10:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('django_ledger', '0021_alter_bankaccountmodel_account_model_and_more'),
('inventory', '0023_alter_opportunity_stage'),
]
operations = [
migrations.AlterField(
model_name='opportunity',
name='customer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='opportunities', to='django_ledger.customermodel'),
),
]

View File

@ -973,9 +973,9 @@ class Channel(models.TextChoices):
class Status(models.TextChoices): class Status(models.TextChoices):
NEW = "new", _("New") NEW = "new", _("New")
FOLLOW_UP = "follow_up", _("Needs Follow-up") FOLLOW_UP = "follow_up", _("Follow-up")
NEGOTIATION = "negotiation", _("Under Negotiation") NEGOTIATION = "negotiation", _("Negotiation")
WON = "won", _("Converted") WON = "won", _("Won")
LOST = "lost", _("Lost") LOST = "lost", _("Lost")
CLOSED = "closed", _("Closed") CLOSED = "closed", _("Closed")
@ -1020,7 +1020,7 @@ class ActionChoices(models.TextChoices):
class Stage(models.TextChoices): class Stage(models.TextChoices):
PROSPECT = "prospect", _("Prospect") DISCOVERY = "discovery", _("Discovery")
PROPOSAL = "proposal", _("Proposal") PROPOSAL = "proposal", _("Proposal")
NEGOTIATION = "negotiation", _("Negotiation") NEGOTIATION = "negotiation", _("Negotiation")
CLOSED_WON = "closed_won", _("Closed Won") CLOSED_WON = "closed_won", _("Closed Won")
@ -1515,7 +1515,7 @@ class Opportunity(models.Model):
Dealer, on_delete=models.CASCADE, related_name="opportunities" Dealer, on_delete=models.CASCADE, related_name="opportunities"
) )
customer = models.ForeignKey( customer = models.ForeignKey(
CustomerModel, on_delete=models.CASCADE, related_name="opportunities" CustomerModel, on_delete=models.CASCADE, related_name="opportunities",null=True,blank=True
) )
car = models.ForeignKey( car = models.ForeignKey(
Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car") Car, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")
@ -1523,12 +1523,6 @@ class Opportunity(models.Model):
stage = models.CharField( stage = models.CharField(
max_length=20, choices=Stage.choices, verbose_name=_("Stage") max_length=20, choices=Stage.choices, verbose_name=_("Stage")
) )
status = models.CharField(
max_length=20,
choices=Status.choices,
verbose_name=_("Status"),
default=Status.NEW,
)
staff = models.ForeignKey( staff = models.ForeignKey(
Staff, Staff,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -1538,10 +1532,12 @@ class Opportunity(models.Model):
) )
lead = models.OneToOneField("Lead",related_name="opportunity", on_delete=models.CASCADE,null=True,blank=True) lead = models.OneToOneField("Lead",related_name="opportunity", on_delete=models.CASCADE,null=True,blank=True)
probability = models.PositiveIntegerField(validators=[validate_probability]) probability = models.PositiveIntegerField(validators=[validate_probability])
expected_revenue = models.DecimalField(
max_digits=10, decimal_places=2, verbose_name=_("Expected Revenue")
)
closing_date = models.DateField(verbose_name=_("Closing Date"),null=True,blank=True) closing_date = models.DateField(verbose_name=_("Closing Date"),null=True,blank=True)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created")) created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated")) updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
closed = models.BooleanField(default=False, verbose_name=_("Closed"))
estimate = models.OneToOneField(EstimateModel, related_name="opportunity",on_delete=models.SET_NULL,null=True,blank=True) estimate = models.OneToOneField(EstimateModel, related_name="opportunity",on_delete=models.SET_NULL,null=True,blank=True)
class Meta: class Meta:
verbose_name = _("Opportunity") verbose_name = _("Opportunity")
@ -1569,6 +1565,31 @@ class Notes(models.Model):
def __str__(self): def __str__(self):
return f"Note by {self.created_by.first_name} on {self.content_object}" return f"Note by {self.created_by.first_name} on {self.content_object}"
class Tasks(models.Model):
dealer = models.ForeignKey(Dealer, on_delete=models.CASCADE, related_name="tasks")
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.UUIDField()
content_object = GenericForeignKey("content_type", "object_id")
title = models.CharField(max_length=255, verbose_name=_("Title"))
description = models.TextField(verbose_name=_("Description"),null=True,blank=True)
due_date = models.DateField(verbose_name=_("Due Date"))
completed = models.BooleanField(default=False, verbose_name=_("Completed"))
assigned_to = models.ForeignKey(
User, on_delete=models.DO_NOTHING, related_name="tasks_assigned",null=True,blank=True
)
created_by = models.ForeignKey(
User, on_delete=models.DO_NOTHING, related_name="tasks_created"
)
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
updated = models.DateTimeField(auto_now=True, verbose_name=_("Updated"))
class Meta:
verbose_name = _("Task")
verbose_name_plural = _("Tasks")
def __str__(self):
return f"Task by {self.created_by.email} on {self.content_object}"
class Email(models.Model): class Email(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.UUIDField() object_id = models.UUIDField()

View File

@ -117,8 +117,18 @@ urlpatterns = [
path('crm/leads/<int:pk>/update-note/', views.update_note, name='update_note_to_lead'), path('crm/leads/<int:pk>/update-note/', views.update_note, name='update_note_to_lead'),
path("crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"), path("crm/leads/<int:pk>/delete-note/", views.delete_note, name="delete_note_to_lead"),
path( path(
"crm/leads/<int:pk>/add-activity/", "crm/<int:pk>/update-task/",
views.add_activity_to_lead, views.update_task,
name="update_task",
),
path(
"crm/<str:content_type>/<int:pk>/add-task/",
views.add_task,
name="add_task",
),
path(
"crm/<str:content_type>/<int:pk>/add-activity/",
views.add_activity,
name="add_activity", name="add_activity",
), ),
path( path(

View File

@ -1362,6 +1362,6 @@ def handle_payment(request,order):
return transaction_url return transaction_url
# def get_user_quota(user): # def get_user_quota(user):
# return user.dealer.quota # return user.dealer.quota

View File

@ -8,6 +8,7 @@ import numpy as np
# from rich import print # from rich import print
from random import randint from random import randint
from decimal import Decimal from decimal import Decimal
from django.apps import apps
from datetime import timedelta from datetime import timedelta
from calendar import month_name from calendar import month_name
from pyzbar.pyzbar import decode from pyzbar.pyzbar import decode
@ -24,7 +25,7 @@ from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import Func from django.db.models import Func
from django.contrib import messages from django.contrib import messages
from django.http import JsonResponse, HttpResponseForbidden from django.http import Http404, JsonResponse, HttpResponseForbidden
from django.forms import HiddenInput, ValidationError from django.forms import HiddenInput, ValidationError
from django.shortcuts import HttpResponse from django.shortcuts import HttpResponse
from django.db.models import Sum, F, Count from django.db.models import Sum, F, Count
@ -4580,10 +4581,15 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
context["activities"] = models.Activity.objects.filter( context["activities"] = models.Activity.objects.filter(
content_type__model="lead", object_id=self.object.id content_type__model="lead", object_id=self.object.id
) )
context["tasks"] = models.Tasks.objects.filter(
content_type__model="lead", object_id=self.object.id
)
context["status_history"] = models.LeadStatusHistory.objects.filter( context["status_history"] = models.LeadStatusHistory.objects.filter(
lead=self.object lead=self.object
) )
context["transfer_form"] = forms.LeadTransferForm() context["transfer_form"] = forms.LeadTransferForm()
context["activity_form"] = forms.ActivityForm()
context["staff_task_form"] = forms.StaffTaskForm()
return context return context
@ -4673,10 +4679,10 @@ def lead_create(request):
def lead_tracking(request): def lead_tracking(request):
dealer = get_user_type(request) dealer = get_user_type(request)
new = models.Lead.objects.filter(dealer=dealer) new = models.Lead.objects.filter(dealer=dealer, status="new")
follow_up = models.Lead.objects.filter(dealer=dealer, next_action__in=["call", "meeting"]) follow_up = models.Lead.objects.filter(dealer=dealer, next_action__in=["call", "meeting"])
won = models.Lead.objects.filter(dealer=dealer, status="won") won = models.Lead.objects.filter(dealer=dealer, status="won")
lose = models.Lead.objects.filter(dealer=dealer, status="lose") lose = models.Lead.objects.filter(dealer=dealer, status="lost")
negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation") negotiation = models.Lead.objects.filter(dealer=dealer, status="negotiation")
context = {"new": new,"follow_up": follow_up,"won": won,"lose": lose,"negotiation": negotiation} context = {"new": new,"follow_up": follow_up,"won": won,"lose": lose,"negotiation": negotiation}
return render(request, "crm/leads/lead_tracking.html", context) return render(request, "crm/leads/lead_tracking.html", context)
@ -4687,23 +4693,23 @@ def update_lead_actions(request):
lead_id = request.POST.get('lead_id') lead_id = request.POST.get('lead_id')
current_action = request.POST.get('current_action') current_action = request.POST.get('current_action')
next_action = request.POST.get('next_action') next_action = request.POST.get('next_action')
next_action_date = request.POST.get('next_action_date') next_action_date = request.POST.get('next_action_date',None)
action_notes = request.POST.get('action_notes', '') action_notes = request.POST.get('action_notes', '')
# Validate required fields # Validate required fields
if not all([lead_id, current_action, next_action, next_action_date]): if not all([lead_id, current_action, next_action]):
return JsonResponse({'success': False, 'message': 'All fields are required'}, status=400) return JsonResponse({'success': False, 'message': 'All fields are required'}, status=400)
# Get the lead # Get the lead
lead = models.Lead.objects.get(id=lead_id) lead = models.Lead.objects.get(id=lead_id)
# Update lead fields # Update lead fields
lead.action = current_action lead.status = current_action
lead.next_action = next_action lead.next_action = next_action
lead.next_action_date = next_action_date
# Parse the datetime string # Parse the datetime string
try: try:
if next_action_date:
lead.next_action_date = next_action_date
next_action_datetime = datetime.strptime(next_action_date, '%Y-%m-%dT%H:%M') next_action_datetime = datetime.strptime(next_action_date, '%Y-%m-%dT%H:%M')
lead.next_action_date = timezone.make_aware(next_action_datetime) lead.next_action_date = timezone.make_aware(next_action_datetime)
except ValueError: except ValueError:
@ -5060,10 +5066,6 @@ def send_lead_email(request, pk, email_pk=None):
response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk]) response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk])
return response return response
lead.status = models.Status.CONTACTED
lead.save()
if request.method == "POST": if request.method == "POST":
email_pk = request.POST.get("email_pk") email_pk = request.POST.get("email_pk")
if email_pk not in [None, "None", ""]: if email_pk not in [None, "None", ""]:
@ -5138,11 +5140,16 @@ def add_activity_to_lead(request, pk):
:return: An HTTP response that either renders the form or redirects to the lead detail page :return: An HTTP response that either renders the form or redirects to the lead detail page
""" """
lead = get_object_or_404(models.Lead, pk=pk) lead = get_object_or_404(models.Lead, pk=pk)
dealer = get_user_type(request)
if request.method == "POST": if request.method == "POST":
form = forms.ActivityForm(request.POST) form = forms.ActivityForm(request.POST)
if form.is_valid(): if form.is_valid():
activity = form.save(commit=False) activity = form.save(commit=False)
print(activity)
activity.content_object = lead activity.content_object = lead
activity.dealer = dealer
activity.activity_type = form.cleaned_data["activity_type"]
activity.notes = form.cleaned_data["notes"]
activity.created_by = request.user activity.created_by = request.user
activity.save() activity.save()
return redirect("lead_detail", pk=pk) return redirect("lead_detail", pk=pk)
@ -5151,7 +5158,7 @@ def add_activity_to_lead(request, pk):
return render(request, "crm/add_activity.html", {"form": form, "lead": lead}) return render(request, "crm/add_activity.html", {"form": form, "lead": lead})
class OpportunityCreateView(CreateView, LoginRequiredMixin): class OpportunityCreateView(CreateView,SuccessMessageMixin, LoginRequiredMixin):
""" """
Handles the creation of Opportunity instances through a form while enforcing Handles the creation of Opportunity instances through a form while enforcing
specific user access control and initial data population. This view ensures specific user access control and initial data population. This view ensures
@ -5173,30 +5180,33 @@ class OpportunityCreateView(CreateView, LoginRequiredMixin):
model = models.Opportunity model = models.Opportunity
form_class = forms.OpportunityForm form_class = forms.OpportunityForm
template_name = "crm/opportunities/opportunity_form.html" template_name = "crm/opportunities/opportunity_form.html"
success_message = "Opportunity created successfully."
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
return context return context
def get_initial(self): # def get_initial(self):
initial = super().get_initial() # initial = super().get_initial()
if self.kwargs.get("pk", None): # if self.kwargs.get("pk", None):
lead = models.Lead.objects.get(pk=self.kwargs.get("pk")) # lead = models.Lead.objects.get(pk=self.kwargs.get("pk"))
initial["customer"] = lead.customer # initial["customer"] = lead.customer
return initial # return initial
def form_valid(self, form): def form_valid(self, form):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
form.instance.dealer = dealer form.instance.dealer = dealer
form.instance.customer = form.instance.lead.customer
form.instance.staff = form.instance.lead.staff
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk}) return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk})
class OpportunityUpdateView(LoginRequiredMixin, UpdateView): class OpportunityUpdateView(LoginRequiredMixin,SuccessMessageMixin, UpdateView):
""" """
Handles the update functionality for Opportunity objects. Handles the update functionality for Opportunity objects.
@ -5221,6 +5231,7 @@ class OpportunityUpdateView(LoginRequiredMixin, UpdateView):
model = models.Opportunity model = models.Opportunity
form_class = forms.OpportunityForm form_class = forms.OpportunityForm
template_name = "crm/opportunities/opportunity_form.html" template_name = "crm/opportunities/opportunity_form.html"
success_message = "Opportunity updated successfully."
def get_success_url(self): def get_success_url(self):
return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk}) return reverse_lazy("opportunity_detail", kwargs={"pk": self.object.pk})
@ -5252,7 +5263,6 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView):
url = reverse("opportunity_update_status", args=[self.object.pk]) url = reverse("opportunity_update_status", args=[self.object.pk])
form.fields["status"].widget.attrs["hx-get"] = url form.fields["status"].widget.attrs["hx-get"] = url
form.fields["stage"].widget.attrs["hx-get"] = url form.fields["stage"].widget.attrs["hx-get"] = url
form.fields["status"].initial = self.object.status
form.fields["stage"].initial = self.object.stage form.fields["stage"].initial = self.object.stage
context["status_form"] = form context["status_form"] = form
context["notes"] = models.Notes.objects.filter( context["notes"] = models.Notes.objects.filter(
@ -5272,25 +5282,6 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView):
class OpportunityListView(LoginRequiredMixin, ListView): class OpportunityListView(LoginRequiredMixin, ListView):
"""
View for displaying a paginated list of opportunities.
This class-based view inherits from `LoginRequiredMixin` and `ListView` to
provide a view rendering a list of `Opportunity` objects associated with
the current dealer. It ensures the user is authenticated before providing
access to the opportunity list and adds filtering based on the dealer
associated with the request.
:ivar model: The model used to retrieve opportunities.
:type model: models.Opportunity
:ivar template_name: The template used to render the opportunities list.
:type template_name: str
:ivar context_object_name: The context variable name for the list of
opportunities in the template.
:type context_object_name: str
:ivar paginate_by: The number of opportunities displayed per page.
:type paginate_by: int
"""
model = models.Opportunity model = models.Opportunity
template_name = "crm/opportunities/opportunity_list.html" template_name = "crm/opportunities/opportunity_list.html"
context_object_name = "opportunities" context_object_name = "opportunities"
@ -5298,9 +5289,38 @@ class OpportunityListView(LoginRequiredMixin, ListView):
def get_queryset(self): def get_queryset(self):
dealer = get_user_type(self.request) dealer = get_user_type(self.request)
return models.Opportunity.objects.filter(dealer=dealer).all() queryset = models.Opportunity.objects.filter(dealer=dealer)
# Search filter
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(customer__customer_name__icontains=search) |
Q(customer__email__icontains=search))
# Stage filter
stage = self.request.GET.get('stage')
if stage:
queryset = queryset.filter(stage=stage)
# Sorting
sort = self.request.GET.get('sort', 'newest')
if sort == 'newest':
queryset = queryset.order_by('-created')
elif sort == 'highest':
queryset = queryset.order_by('-expected_revenue')
elif sort == 'closing':
queryset = queryset.order_by('closing_date')
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['stage_choices'] = models.Stage.choices
return context
def get_template_names(self):
return self.template_name
@login_required @login_required
def delete_opportunity(request, pk): def delete_opportunity(request, pk):
""" """
@ -7706,3 +7726,78 @@ def mark_all_notifications_as_read(request):
def notifications_history(request): def notifications_history(request):
models.Notification.objects.filter(user=request.user, is_read=False).update(read=True) models.Notification.objects.filter(user=request.user, is_read=False).update(read=True)
return JsonResponse({'status': 'success'}) return JsonResponse({'status': 'success'})
# def activity_create(request,pk):
# lead = get_object_or_404(models.Lead, pk=pk)
# form = forms.ActivityHistoryForm()
# dealer = get_user_type(request)
# if request.method == "POST":
# form = forms.ActivityHistoryForm(request.POST)
# if form.is_valid():
# models.Activity.objects.create(
# dealer=dealer,
# activity_type=form.cleaned_data['activity_type'],
# notes=form.cleaned_data['description'],
# created_by=request.user,
# content_object=lead
# )
# return render(request, 'activity_history.html')
def add_activity(request,content_type,pk):
try:
model = apps.get_model(f'inventory.{content_type}')
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, pk=pk)
dealer = get_user_type(request)
if request.method == "POST":
form = forms.ActivityForm(request.POST)
if form.is_valid():
activity = form.save(commit=False)
activity.dealer = dealer
activity.content_object = obj
activity.created_by = request.user
activity.notes = form.cleaned_data['notes']
activity.activity_type = form.cleaned_data['activity_type']
activity.save()
messages.success(request, _("Activity added successfully"))
else:
messages.error(request, _("Activity form is not valid"))
return redirect(f"{content_type}_detail", pk=pk)
def add_task(request,content_type,pk):
try:
model = apps.get_model(f'inventory.{content_type}')
except LookupError:
raise Http404("Model not found")
obj = get_object_or_404(model, pk=pk)
dealer = get_user_type(request)
if request.method == "POST":
form = forms.StaffTaskForm(request.POST)
if form.is_valid():
task = form.save(commit=False)
task.dealer = dealer
task.content_object = obj
task.assigned_to = request.user
task.created_by = request.user
task.due_date = form.cleaned_data['due_date']
task.save()
messages.success(request, _("Task added successfully"))
else:
print(form.errors)
messages.error(request, _("Task form is not valid"))
return redirect(f"{content_type}_detail", pk=pk)
def update_task(request,pk):
task = get_object_or_404(models.Tasks, pk=pk)
if request.method == "POST":
task.completed = False if task.completed else True
task.save()
messages.success(request, _("Task updated successfully"))
else:
messages.error(request, _("Task form is not valid"))
response = HttpResponse()
response['HX-Refresh'] = 'true'
return response

View File

@ -0,0 +1,30 @@
{% load static i18n crispy_forms_tags %}
<!-- activity Modal -->
<div class="modal fade" id="activityModal" tabindex="-1" aria-labelledby="activityModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="noteModalLabel">{% trans 'Activity' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div class="modal-body">
<form action="{% url 'add_activity' content_type=content_type pk=pk %}" method="post" class="add_activity_form">
{% csrf_token %}
<div class="mb-2 form-group">
<select class="form-select" name="activity_type" id="activity_type">
<option value="call">{% trans 'Call' %}</option>
<option value="email">{% trans 'Email' %}</option>
<option value="meeting">{% trans 'Meeting' %}</option>
</select>
</div>
<div class="mb-3 form-group">
<textarea class="form-control" name="notes" id="notes" rows="6"></textarea>
</div>
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static humanize %}
{% block content %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% block customCSS %} {% block customCSS %}
@ -8,11 +7,91 @@
.main-tab li:last-child { .main-tab li:last-child {
margin-left: auto; margin-left: auto;
} }
.completed-task {
text-decoration: line-through;
opacity: 0.7;
}
.kanban-header {
position: relative;
background-color:rgb(237, 241, 245);
font-weight: 600;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
color: #333;
clip-path: polygon(0 0, calc(100% - 15px) 0, 100% 50%, calc(100% - 15px) 100%, 0 100%);
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.kanban-header::after {
content: "";
position: absolute;
right: -20px;
top: 0;
width: 0;
height: 0;
border-top: 28px solid transparent;
border-bottom: 28px solid transparent;
border-left: 20px solid #dee2e6;
}
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %}
<div class="row g-3"> <div class="row g-3">
<div class="col-12"> <div class="col-12">
<div class="modal fade" id="actionTrackingModal" tabindex="-1" aria-labelledby="actionTrackingModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="actionTrackingModalLabel">{{ _("Update Lead Actions") }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="actionTrackingForm" method="post">
<div class="modal-body">
{% csrf_token %}
<input type="hidden" id="leadId" name="lead_id">
<div class="mb-3">
<label for="currentAction" class="form-label">{{ _("Current Action") }}</label>
<select class="form-select" id="currentAction" name="current_action" required>
<option value="">{{ _("Select Action") }}</option>
<option value="follow_up">{{ _("Follow Up") }}</option>
<option value="negotiation">{{ _("Negotiation") }}</option>
<option value="won">{{ _("Won") }}</option>
<option value="lost">{{ _("Lost") }}</option>
<option value="closed">{{ _("Closed") }}</option>
</select>
</div>
<div class="mb-3">
<label for="nextAction" class="form-label">{{ _("Next Action") }}</label>
<select class="form-select" id="nextAction" name="next_action" required>
<option value="">{{ _("Select Next Action") }}</option>
<option value="no_action">{{ _("No Action") }}</option>
<option value="call">{{ _("Call") }}</option>
<option value="meeting">{{ _("Meeting") }}</option>
<option value="email">{{ _("Email") }}</option>
</select>
</div>
<div class="mb-3">
<label for="nextActionDate" class="form-label">{{ _("Next Action Date") }}</label>
<input type="datetime-local" class="form-control" id="nextActionDate" name="next_action_date">
</div>
<div class="mb-3">
<label for="actionNotes" class="form-label">{{ _("Notes") }}</label>
<textarea class="form-control" id="actionNotes" name="action_notes" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-primary">{{ _("Save Changes") }}</button>
</div>
</form>
</div>
</div>
</div>
<div class="row align-items-center justify-content-between g-3 mb-3"> <div class="row align-items-center justify-content-between g-3 mb-3">
<div class="col-12 col-md-auto"> <div class="col-12 col-md-auto">
<h4 class="mb-0">{{ _("Lead Details")}}</h4> <h4 class="mb-0">{{ _("Lead Details")}}</h4>
@ -99,7 +178,7 @@
<div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-clock"></span> <div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-clock"></span>
<h5 class="text-body-highlight mb-0">{{ _("Created")}}</h5> <h5 class="text-body-highlight mb-0">{{ _("Created")}}</h5>
</div> </div>
<p class="mb-0 text-body-secondary">{{ lead.created|date }}</p> <p class="mb-0 text-body-secondary">{{ lead.created|naturalday|capfirst }}</p>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-file-check-alt"></span> <div class="d-flex align-items-center mb-1"><span class="me-2 uil uil-file-check-alt"></span>
@ -132,12 +211,22 @@
</div> </div>
</div> </div>
<div class="col-md-7 col-lg-7 col-xl-8"> <div class="col-md-7 col-lg-7 col-xl-8">
<ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end" id="myTab" role="tablist" style="overflow-y: hidden;"> <div class="d-flex w-100 gap-5">
<div class="kanban-header bg-success w-50 text-white fw-bold"><i class="fa-solid fa-circle-check me-2"></i>{{lead.status|capfirst}} <br> &nbsp; <small>{% trans "Current Stage" %}</small></div>
<div class="kanban-header bg-secondary w-50 text-white fw-bold"><i class="fa-solid fa-circle-info me-2"></i>{{lead.next_action|capfirst}} <br> &nbsp; <small>{% trans "Next Action" %}</small></div>
</div>
<ul class="nav main-tab nav-underline fs-9 deal-details scrollbar flex-nowrap w-100 pb-1 mb-6 justify-content-end mt-5" id="myTab" role="tablist" style="overflow-y: hidden;">
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link active" id="activity-tab" data-bs-toggle="tab" href="#tab-activity" role="tab" aria-controls="tab-activity" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-chart-line me-2 tab-icon-color fs-8"></span>{{ _("Activity") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="notes-tab" data-bs-toggle="tab" href="#tab-notes" role="tab" aria-controls="tab-notes" aria-selected="false" tabindex="-1"> <span class="fa-solid fa-clipboard me-2 tab-icon-color fs-8"></span>{{ _("Notes") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li> <li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
<li class="nav-item text-nowrap ml-auto" role="presentation"> <li class="nav-item text-nowrap ml-auto" role="presentation">
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal">Reassign Lead</button> <button class="btn btn-info btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Create Opportunity</button>
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
<button class="btn btn-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
<i class="fa-solid fa-user-plus me-2"></i>
{% trans "Update Actions" %}
</button>
<div class="modal fade" id="exampleModal" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="exampleModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -162,8 +251,9 @@
</ul> </ul>
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
<div class="tab-pane fade active show" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab"> <div class="tab-pane fade active show" id="tab-activity" role="tabpanel" aria-labelledby="activity-tab">
<div class="mb-1"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-4" id="scrollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3> <h3 class="mb-4" id="scrollspyTask">{{ _("Activities") }} <span class="fw-light fs-7">({{ activities.count}})</span></h3>
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
</div> </div>
<div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top"> <div class="row justify-content-between align-items-md-center hover-actions-trigger btn-reveal-trigger border-translucent py-3 gx-0 border-top">
<div class="col-12 col-lg-auto"> <div class="col-12 col-lg-auto">
@ -196,9 +286,9 @@
<div class="d-flex mb-2"> <div class="d-flex mb-2">
<h6 class="lh-sm mb-0 me-2 text-body-secondary timeline-item-title">{{ activity.activity_type|capfirst }}</h6> <h6 class="lh-sm mb-0 me-2 text-body-secondary timeline-item-title">{{ activity.activity_type|capfirst }}</h6>
</div> </div>
<p class="text-body-quaternary fs-9 mb-0 text-nowrap timeline-time"><span class="fa-regular fa-clock me-1"></span>{{ activity.created }}</p> <p class="text-body-quaternary fs-9 mb-0 text-nowrap timeline-time"><span class="fa-regular fa-clock me-1"></span>{{ activity.created|naturalday|capfirst }}</p>
</div> </div>
<h6 class="fs-10 fw-normal mb-3">{{ _("by") }} <a class="fw-semibold" href="#!">{{ activity.created_by }}</a></h6> <h6 class="fs-10 fw-normal mb-3">{{ _("created by") }} <a class="fw-semibold" href="#!">{{ activity.created_by }}</a></h6>
<p class="fs-9 text-body-secondary w-sm-60 mb-5">{{ activity.notes }}</p> <p class="fs-9 text-body-secondary w-sm-60 mb-5">{{ activity.notes }}</p>
</div> </div>
</div> </div>
@ -209,15 +299,14 @@
</div> </div>
</div> </div>
<div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab"> <div class="tab-pane fade" id="tab-notes" role="tabpanel" aria-labelledby="notes-tab">
<div class="mb-1"> <div class="mb-1 d-flex align-items-center justify-content-between">
<h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3> <h3 class="mb-4" id="scrollspyNotes">{{ _("Notes") }}</h3>
</div> <a id="addBtn" href="#" class="btn btn-sm btn-phoenix-primary mb-3 btn-sm" data-url="{% url 'add_note_to_lead' lead.pk %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
<div class="d-flex align-items-center justify-content-start">
<a id="addBtn" href="#" class="btn btn-sm btn-phoenix-primary mb-3" data-url="{% url 'add_note_to_lead' lead.pk %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Add") }}<i class='fa fa-plus-circle text-success ms-2'></i>">
<span class="fas fa-plus me-1"></span> <span class="fas fa-plus me-1"></span>
{% trans 'Add Note' %} {% trans 'Add Note' %}
</a> </a>
</div> </div>
<div class="border-top border-bottom border-translucent" id="leadDetailsTable"> <div class="border-top border-bottom border-translucent" id="leadDetailsTable">
<div class="table-responsive scrollbar mx-n1 px-1"> <div class="table-responsive scrollbar mx-n1 px-1">
<table class="table fs-9 mb-0"> <table class="table fs-9 mb-0">
@ -239,7 +328,7 @@
{% else %} {% else %}
<td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.dealer.get_local_name|default:note.created_by.dealer.name }}</td> <td class="align-middle white-space-nowrap text-start white-space-nowrap">{{ note.created_by.dealer.get_local_name|default:note.created_by.dealer.name }}</td>
{% endif %} {% endif %}
<td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created }}</td> <td class="align-middle text-body-tertiary text-start white-space-nowrap">{{ note.created|naturalday|capfirst }}</td>
<td class="align-middle text-end white-space-nowrap pe-0 action py-2"> <td class="align-middle text-end white-space-nowrap pe-0 action py-2">
{% if note.created_by == request.user %} {% if note.created_by == request.user %}
<a id="updateBtn" href="#" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'update_note_to_lead' note.pk %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Update") }}<i class='fas fa-pen-square text-primary ms-2'></i>"> <a id="updateBtn" href="#" class="btn btn-sm btn-phoenix-primary me-2" data-url="{% url 'update_note_to_lead' note.pk %}" data-bs-toggle="modal" data-bs-target="#noteModal" data-note-title="{{ _("Update") }}<i class='fas fa-pen-square text-primary ms-2'></i>">
@ -261,8 +350,14 @@
</div> </div>
</div> </div>
<div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab"> <div class="tab-pane fade" id="tab-emails" role="tabpanel" aria-labelledby="emails-tab">
<div class="mb-1"> <div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3> <h3 class="mb-0" id="scrollspyEmails">{{ _("Emails") }}</h3>
<a href="{% url 'send_lead_email' lead.pk %}">
<button type="button" class="btn btn-sm btn-phoenix-primary">
<span class="fas fa-plus me-1"></span>
{% trans 'Send Email' %}
</button>
</a>
</div> </div>
<div> <div>
<div class="scrollbar"> <div class="scrollbar">
@ -302,7 +397,7 @@
<div class="fs-10 d-block">{{email.to_email}}</div> <div class="fs-10 d-block">{{email.to_email}}</div>
</td> </td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td> <td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{email.from_email}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{email.created}}</td> <td class="date align-middle white-space-nowrap text-body py-2">{{email.created|naturalday}}</td>
<td class="align-middle white-space-nowrap ps-3"><a class="text-body" href=""><span class="fa-solid fa-phone text-primary me-2"></span>Call</a></td> <td class="align-middle white-space-nowrap ps-3"><a class="text-body" href=""><span class="fa-solid fa-phone text-primary me-2"></span>Call</a></td>
<td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span></td> <td class="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span></td>
</tr> </tr>
@ -375,13 +470,73 @@
</div> </div>
</div> </div>
</div> </div>
{% comment %} {% endcomment %}
<div class="tab-pane fade" id="tab-tasks" role="tabpanel" aria-labelledby="tasks-tab">
<div class="mb-1 d-flex justify-content-between align-items-center">
<h3 class="mb-0" id="scrollspyEmails">{{ _("Tasks") }}</h3>
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#taskModal"><span class="fas fa-plus me-1"></span>{{ _("Add Task") }}</button>
</div>
<div>
<div class="border-top border-bottom border-translucent" id="allEmailsTable" data-list='{"valueNames":["subject","sent","date","source","status"],"page":7,"pagination":true}'>
<div class="table-responsive scrollbar mx-n1 px-1">
<table class="table fs-9 mb-0">
<thead>
<tr>
<th class="white-space-nowrap fs-9 align-middle ps-0" style="width:26px;">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" data-bulk-select='{"body":"all-email-table-body"}' />
</div>
</th>
<th class="sort white-space-nowrap align-middle pe-3 ps-0 text-uppercase" scope="col" data-sort="subject" style="width:31%; min-width:350px">Title</th>
<th class="sort align-middle pe-3 text-uppercase" scope="col" data-sort="sent" style="width:15%; min-width:130px">Assigned to</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Due Date</th>
<th class="sort align-middle text-start text-uppercase" scope="col" data-sort="date" style="min-width:165px">Completed</th>
</tr>
</thead>
<tbody class="list" id="all-email-table-body">
{% for task in tasks %}
<tr class="hover-actions-trigger btn-reveal-trigger position-static {% if task.completed %}completed-task{% endif %}">
<td class="fs-9 align-middle px-0 py-3">
<div class="form-check mb-0 fs-8">
<input class="form-check-input" type="checkbox" hx-post="{% url 'update_task' task.pk %}" hx-trigger="change" hx-swap="none" />
</div>
</td>
<td class="subject order align-middle white-space-nowrap py-2 ps-0"><a class="fw-semibold text-primary" href="">{{task.title}}</a>
<div class="fs-10 d-block">{{task.description}}</div>
</td>
<td class="sent align-middle white-space-nowrap text-start fw-bold text-body-tertiary py-2">{{task.assigned_to}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">{{task.created|naturalday|capfirst}}</td>
<td class="date align-middle white-space-nowrap text-body py-2">
{% if task.completed %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success"><i class="fa-solid fa-check"></i></span>
{% else %}
<span class="badge badge-phoenix fs-10 badge-phoenix-warning"><i class="fa-solid fa-xmark"></i></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="row align-items-center justify-content-between py-2 pe-0 fs-9">
<div class="col-auto d-flex">
<p class="mb-0 d-none d-sm-block me-3 fw-semibold text-body" data-list-info="data-list-info"></p><a class="fw-semibold" href="" data-list-view="*">View all<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a><a class="fw-semibold d-none" href="" data-list-view="less">View Less<span class="fas fa-angle-right ms-1" data-fa-transform="down-1"></span></a>
</div>
<div class="col-auto d-flex">
<button class="page-link" data-list-pagination="prev"><span class="fas fa-chevron-left"></span></button>
<ul class="mb-0 pagination"></ul>
<button class="page-link pe-0" data-list-pagination="next"><span class="fas fa-chevron-right"></span></button>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
{% include 'modal/delete_modal.html' %} {% include 'modal/delete_modal.html' %}
<!-- add update Modal --> <!-- add update Modal -->
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true"> <div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm"> <div class="modal-dialog modal-md">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0"> <div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="noteModalLabel">{% trans 'Notes' %}</h4> <h4 class="modal-title" id="noteModalLabel">{% trans 'Notes' %}</h4>
@ -395,6 +550,33 @@
</div> </div>
</div> </div>
</div> </div>
<!-- activity Modal -->
{% include "components/activity_modal.html" with content_type="lead" pk=lead.pk %}
<!-- task Modal -->
<div class="modal fade" id="taskModal" tabindex="-1" aria-labelledby="taskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header justify-content-between align-items-start gap-5 px-4 pt-4 pb-3 border-0">
<h4 class="modal-title" id="noteModalLabel">{% trans 'Task' %}</h4>
<button class="btn p-0 text-body-quaternary fs-6" data-bs-dismiss="modal" aria-label="Close">
<span class="fas fa-times"></span>
</button>
</div>
<div class="modal-body">
<form action="{% url 'add_task' 'lead' lead.pk %}" method="post" class="add_task_form">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block customJS %}
<script> <script>
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const noteModal = document.getElementById("noteModal"); const noteModal = document.getElementById("noteModal");
@ -419,5 +601,127 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
}); });
}); });
let Toast = Swal.mixin({
toast: true,
position: "top-end",
showConfirmButton: false,
timer: 3000,
timerProgressBar: true,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
// Display Django messages
{% if messages %}
{% for message in messages %}
Toast.fire({
icon: "{{ message.tags }}",
titleText: "{{ message|safe }}"
});
{% endfor %}
{% endif %}
function openActionModal(leadId, currentAction, nextAction, nextActionDate) {
const modal = new bootstrap.Modal(document.getElementById('actionTrackingModal'));
document.getElementById('leadId').value = leadId;
document.getElementById('currentAction').value = currentAction;
document.getElementById('nextAction').value = nextAction;
document.getElementById('nextActionDate').value = nextActionDate;
modal.show();
}
document.getElementById('actionTrackingForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
// Show loading indicator
Swal.fire({
toast: true,
icon: 'info',
text: 'Please wait...',
allowOutsideClick: false,
position: "top-end",
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
fetch("{% url 'update_lead_actions' %}", {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': '{{ csrf_token }}'
}
})
.then(response => response.json())
.then(data => {
Swal.close();
if (data.success) {
// Success notification
Swal.fire({
toast: true,
icon: 'success',
position: "top-end",
text: data.message || 'Actions updated successfully',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
}).then(() => {
location.reload(); // Refresh after user clicks OK
});
} else {
// Error notification
Swal.fire({
toast: true,
icon: 'error',
position: "top-end",
text: data.message || 'Failed to update actions',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
}
})
.catch(error => {
Swal.close();
console.error('Error:', error);
Swal.fire({
toast: true,
icon: 'error',
position: "top-end",
text: 'An unexpected error occurred',
showConfirmButton: false,
timer: 2000,
timerProgressBar: false,
didOpen: (toast) => {
toast.onmouseenter = Swal.stopTimer;
toast.onmouseleave = Swal.resumeTimer;
}
});
});
});
// Helper function for notifications
function notify(tag, msg) {
Toast.fire({
icon: tag,
titleText: msg
});
}
</script> </script>
{% endblock %} {% endblock customJS %}

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static humanize %}
{% block title %}{{ _('Leads')|capfirst }}{% endblock title %} {% block title %}{{ _('Leads')|capfirst }}{% endblock title %}
{% block content %} {% block content %}
@ -112,19 +112,7 @@
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;"> <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center"> <div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div> <div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Current Action")|capfirst }}</span> <span>{{ _("Action")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Next Action")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Next Action Date")|capfirst }}</span>
</div> </div>
</th> </th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;"> <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
@ -133,32 +121,15 @@
<span>{{ _("Assigned To")|capfirst }}</span> <span>{{ _("Assigned To")|capfirst }}</span>
</div> </div>
</th> </th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<span>{{ _("Source")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;"> <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center"> <div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div> <div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
<span>{{ _("Channel")|capfirst }}</span> <span>{{ _("Opportunity")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
<span>{{ _("Stage")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="grid"></span></div>
<span>{{ _("Is Opportunity")|capfirst }}</span>
</div> </div>
</th> </th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 15%;"> <th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 15%;">
{{ _("Create date") }} {{ _("Action") }}
</th> </th>
<th class="text-end white-space-nowrap align-middle" scope="col"></th> <th class="text-end white-space-nowrap align-middle" scope="col"></th>
</tr> </tr>
@ -230,15 +201,15 @@
{% if schedule.scheduled_type == "call" %} {% if schedule.scheduled_type == "call" %}
<a href="{% url 'appointment:get_user_appointments' %}"> <a href="{% url 'appointment:get_user_appointments' %}">
<span class="badge badge-phoenix badge-phoenix-primary text-primary {% if schedule.schedule_past_date %}badge-phoenix-danger text-danger{% endif %} fw-semibold"><span class="text-primary {% if schedule.schedule_past_date %}text-danger{% endif %}" data-feather="phone"></span> <span class="badge badge-phoenix badge-phoenix-primary text-primary {% if schedule.schedule_past_date %}badge-phoenix-danger text-danger{% endif %} fw-semibold"><span class="text-primary {% if schedule.schedule_past_date %}text-danger{% endif %}" data-feather="phone"></span>
{{ schedule.scheduled_at }}</span></a> {{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
{% elif schedule.scheduled_type == "meeting" %} {% elif schedule.scheduled_type == "meeting" %}
<a href="{% url 'appointment:get_user_appointments' %}"> <a href="{% url 'appointment:get_user_appointments' %}">
<span class="badge badge-phoenix badge-phoenix-success text-success fw-semibold"><span class="text-success" data-feather="calendar"></span> <span class="badge badge-phoenix badge-phoenix-success text-success fw-semibold"><span class="text-success" data-feather="calendar"></span>
{{ schedule.scheduled_at }}</span></a> {{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
{% elif schedule.scheduled_type == "email" %} {% elif schedule.scheduled_type == "email" %}
<a href="{% url 'appointment:get_user_appointments' %}"> <a href="{% url 'appointment:get_user_appointments' %}">
<span class="badge badge-phoenix badge-phoenix-warning text-warning fw-semibold"><span class="text-warning" data-feather="email"></span> <span class="badge badge-phoenix badge-phoenix-warning text-warning fw-semibold"><span class="text-warning" data-feather="email"></span>
{{ schedule.scheduled_at }}</span></a> {{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
{% endif %} {% endif %}
</td> </td>
<td> <td>
@ -255,12 +226,8 @@
</div> </div>
</td> </td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.get_status|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{% if lead.next_action %}{{ lead.next_action|upper }}{% endif %}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{% if lead.next_action %}{{ lead.next_action_date|upper }}{% endif %}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.staff|upper }}</td>
<td class="align-middle white-space-nowrap fw-semibold text-body-highlight">{{ lead.source|upper }}</td> {% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">{{ lead.channel|upper }}</td>
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{% if lead.opportunity.stage == "prospect" %} {% if lead.opportunity.stage == "prospect" %}
<span class="badge text-bg-primary">{{ lead.opportunity.stage|upper }}</span> <span class="badge text-bg-primary">{{ lead.opportunity.stage|upper }}</span>
{% elif lead.opportunity.stage == "proposal" %} {% elif lead.opportunity.stage == "proposal" %}
@ -272,14 +239,12 @@
{% elif lead.opportunity.stage == "closed_lost" %} {% elif lead.opportunity.stage == "closed_lost" %}
<span class="badge text-bg-danger">{{ lead.opportunity.stage|upper }}</span> <span class="badge text-bg-danger">{{ lead.opportunity.stage|upper }}</span>
{% endif %} {% endif %}
</td> </td> {% endcomment %}
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight"> <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
{% if lead.opportunity %} {% if lead.opportunity %}
<a href="{% url 'opportunity_detail' lead.opportunity.id %}"> <a href="{% url 'opportunity_detail' lead.opportunity.id %}">
<span class="badge badge-phoenix badge-phoenix-success">View Details <i class="fa-solid fa-arrow-up-right-from-square"></i></span> <span class="badge badge-phoenix badge-phoenix-success">Opportunity {{ lead.opportunity.lead}} <i class="fa-solid fa-arrow-up-right-from-square"></i></span>
</a> </a>
{% else %}
<span class="badge badge-phoenix badge-phoenix-danger">{{ _("No") }}</span>
{% endif %} {% endif %}
</td> </td>
<td class="align-middle white-space-nowrap text-end"> <td class="align-middle white-space-nowrap text-end">

View File

@ -42,6 +42,22 @@
.lead-card small { .lead-card small {
color: #6c757d; color: #6c757d;
} }
.bg-success-soft {
background-color: rgba(17, 240, 66, 0.1) !important;
opacity: .8;
}
.bg-danger-soft {
background-color: rgba(230, 50, 68, 0.1) !important;
opacity: .8;
}
.bg-info-soft {
background-color: rgba(41, 197, 245, 0.1) !important;
opacity: .8;
}
.bg-negotiation-soft {
background-color: rgba(113, 206, 206, 0.1) !important;
opacity: .8;
}
</style> </style>
{% endblock customCSS %} {% endblock customCSS %}
{% block content %} {% block content %}
@ -51,7 +67,7 @@
</div> </div>
<div class="row g-3"> <div class="row g-3">
<!-- Column Template --> <!-- New Lead -->
<div class="col-md"> <div class="col-md">
<div class="kanban-column"> <div class="kanban-column">
<div class="kanban-header">New Leads ({{new|length}})</div> <div class="kanban-header">New Leads ({{new|length}})</div>
@ -83,38 +99,6 @@
</div> </div>
</div> </div>
<!-- Under Review -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Won ({{won|length}})</div>
{% for lead in won %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Demo -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Lose ({{lose|length}})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Negotiation --> <!-- Negotiation -->
<div class="col-md"> <div class="col-md">
<div class="kanban-column"> <div class="kanban-column">
@ -130,6 +114,39 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<!-- Won -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Won ({{won|length}})</div>
{% for lead in won %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
<!-- Lose -->
<div class="col-md">
<div class="kanban-column">
<div class="kanban-header">Lose ({{lose|length}})</div>
{% for lead in lose %}
<a href="{% url 'lead_detail' lead.id %}">
<div class="lead-card">
<strong>{{lead.full_name|capfirst}}</strong><br>
<small>{{lead.email}}</small><br>
<small>{{lead.phone_number}}</small>
</div>
</a>
{% endfor %}
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static humanize %}
{% block content %} {% block content %}
<div class="row align-items-center justify-content-between g-3 mb-4"> <div class="row align-items-center justify-content-between g-3 mb-4">
@ -18,7 +18,8 @@
<a class="dropdown-item" href="{% url 'estimate_create_from_opportunity' opportunity.pk %}">{{ _("Create Quotation")}}</a> <a class="dropdown-item" href="{% url 'estimate_create_from_opportunity' opportunity.pk %}">{{ _("Create Quotation")}}</a>
{% endif %} {% endif %}
</li> </li>
<li><a class="dropdown-item text-danger" href="">Delete Lead</a></li> <li><a class="dropdown-item" href="{% url 'update_opportunity' opportunity.pk %}">Update Opportunity</a></li>
<li><a class="dropdown-item text-danger" href="">Delete Opportunity</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -169,9 +170,9 @@
<p class="fw-bold mb-0">{{ _("Estimated Revenue") }}</p> <p class="fw-bold mb-0">{{ _("Estimated Revenue") }}</p>
</div> </div>
</td> </td>
<td class="py-2 d-none d-sm-block pe-sm-2">:{{opportunity.estimate.get_revenue_estimate}}</td> <td class="py-2 d-none d-sm-block pe-sm-2">:</td>
<td class="py-2"> <td class="py-2">
<p class="ps-6 ps-sm-0 fw-semibold mb-0">{{ opportunity.car.finances.revenue }}</p> <p class="ps-6 ps-sm-0 fw-semibold mb-0"><span class="currency">{{CURRENCY}}</span>{{ opportunity.expected_revenue }}</p>
</td> </td>
</tr> </tr>
</table> </table>
@ -296,7 +297,7 @@
</div> </div>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<button class="btn btn-phoenix-primary px-6">Add Activity</button> <button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#activityModal"><span class="fas fa-plus me-1"></span>{{ _("Add Activity") }}</button>
</div> </div>
</div> </div>
{% for activity in activities %} {% for activity in activities %}
@ -307,7 +308,7 @@
<span class="fa-solid fa-phone text-warning fs-8"></span> <span class="fa-solid fa-phone text-warning fs-8"></span>
{% elif activity.activity_type == "email" %} {% elif activity.activity_type == "email" %}
<span class="fa-solid fa-envelope text-info-light fs-8"></span> <span class="fa-solid fa-envelope text-info-light fs-8"></span>
{% elif activity.activity_type == "visit" %} {% elif activity.activity_type == "meeting" %}
<span class="fa-solid fa-users text-danger fs-8"></span> <span class="fa-solid fa-users text-danger fs-8"></span>
{% elif activity.activity_type == "whatsapp" %} {% elif activity.activity_type == "whatsapp" %}
<span class="fab fa-whatsapp text-success-dark fs-7"></span> <span class="fab fa-whatsapp text-success-dark fs-7"></span>
@ -319,7 +320,7 @@
<h5 class="text-body-highlight lh-sm"></h5> <h5 class="text-body-highlight lh-sm"></h5>
<p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p> <p class="fs-9 mb-0">by<a class="ms-1" href="#!">{{activity.created_by}}</a></p>
</div> </div>
<div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created}}</span></div> <div class="fs-9"><span class="fa-regular fa-calendar-days text-primary me-2"></span><span class="fw-semibold">{{activity.created|naturalday|capfirst}}</span></div>
</div> </div>
<p class="fs-9 mb-0"></p> <p class="fs-9 mb-0"></p>
</div> </div>
@ -552,4 +553,5 @@
</div> </div>
</div> </div>
</div> </div>
{% include "components/activity_modal.html" with content_type="opportunity" pk=opportunity.pk %}
{% endblock %} {% endblock %}

View File

@ -1,95 +1,203 @@
{% extends "base.html" %} <!-- Assuming you have a base template --> {% extends 'base.html' %}
{% load i18n %} <!-- Load the internationalization template tags --> {% load i18n static widget_tweaks custom_filters %}
{% block content %} {% block content %}
<div class="container-fluid">
<div class="row g-3 mb-4 align-items-center">
<div class="col">
<h2 class="mb-0">
{% if form.instance.pk %}
{% trans "Edit Opportunity" %}
{% else %}
{% trans "Create New Opportunity" %}
{% endif %}
</h2>
</div>
<div class="col-auto">
<a href="{% url 'opportunity_list' %}" class="btn btn-phoenix-secondary">
<span class="fas fa-arrow-left me-2"></span>{% trans "Back to list" %}
</a>
</div>
</div>
<div class="row g-3"> <div class="row g-3">
<div class="col-sm-6 col-md-8"> <div class="col-lg-8">
<h2>{% if form.instance.pk %}{{ _("Edit Opportunity") }}{% else %}{{ _("Add New Opportunity") }}{% endif %}</h2> <div class="card">
</div> <div class="card-body p-4 p-sm-5">
<div class="col-sm-6 col-md-8"> <form method="post" enctype="multipart/form-data">
<form class="row g-3" method="post" class="form" novalidate>
{% csrf_token %} {% csrf_token %}
<!-- Customer --> {% if form.non_field_errors %}
<div class="col-sm-6 col-md-4"> <div class="alert alert-danger">
<div class="form-floating"> {{ form.non_field_errors }}
<select class="form-control" id="{{ form.customer.id_for_label }}" name="{{ form.customer.name }}">
{% for value, label in form.customer.field.choices %}
<option value="{{ value }}" {% if form.customer.value == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<label for="{{ form.customer.id_for_label }}">{{ _("Customer") }}</label>
</div> </div>
{{ form.customer.errors }} {% endif %}
<!-- Lead Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.lead.id_for_label }}">
{{ form.lead.label }}
<span class="text-danger">*</span>
</label>
{{ form.lead|add_class:"form-control" }}
{% if form.lead.errors %}
<div class="invalid-feedback d-block">
{{ form.lead.errors }}
</div>
{% endif %}
</div> </div>
<!-- Car --> <!-- Car Field -->
<div class="col-sm-6 col-md-4"> <div class="mb-4">
<div class="form-floating"> <label class="form-label" for="{{ form.car.id_for_label }}">
<select class="form-control" id="{{ form.car.id_for_label }}" name="{{ form.car.name }}"> {{ form.car.label }}
{% for value, label in form.car.field.choices %} <span class="text-danger">*</span>
<option value="{{ value }}" {% if form.car.value == value %}selected{% endif %}>{{ label }}</option> </label>
{% endfor %} {{ form.car|add_class:"form-control" }}
</select> {% if form.car.errors %}
<label for="{{ form.car.id_for_label }}">{{ _("Car") }}</label> <div class="invalid-feedback d-block">
</div>
{{ form.car.errors }} {{ form.car.errors }}
</div> </div>
{% endif %}
<!-- Stage -->
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<select class="form-control" id="{{ form.stage.id_for_label }}" name="{{ form.stage.name }}">
{% for value, label in form.stage.field.choices %}
<option value="{{ value }}" {% if form.stage.value == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<label for="{{ form.stage.id_for_label }}">{{ _("Stage") }}</label>
</div> </div>
<!-- Stage Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.stage.id_for_label }}">
{{ form.stage.label }}
<span class="text-danger">*</span>
</label>
{{ form.stage|add_class:"form-control" }}
{% if form.stage.errors %}
<div class="invalid-feedback d-block">
{{ form.stage.errors }} {{ form.stage.errors }}
</div> </div>
{% endif %}
<!-- Probability -->
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<input type="number" class="form-control" id="{{ form.probability.id_for_label }}" name="{{ form.probability.name }}" value="{{ form.probability.value|default:'' }}" placeholder="{{ _('Enter probability') }}">
<label for="{{ form.probability.id_for_label }}">{{ _("Probability") }}</label>
</div> </div>
<!-- Probability Field -->
<div class="mb-4">
<label class="form-label" for="{{ form.probability.id_for_label }}">
{{ form.probability.label }}
<span class="text-danger">*</span>
</label>
<div class="d-flex align-items-center gap-3">
<input type="range"
name="{{ form.probability.name }}"
id="{{ form.probability.id_for_label }}"
min="0" max="100" step="1"
value="{{ form.probability.value|default:'50' }}"
class="form-control form-range"
oninput="updateProbabilityValue(this.value)">
<span id="probability-value" class="badge badge-phoenix fs-6 badge-phoenix-primary">
{{ form.probability.value|default:'50' }}%
</span>
</div>
{% if form.probability.errors %}
<div class="invalid-feedback d-block">
{{ form.probability.errors }} {{ form.probability.errors }}
</div> </div>
{% endif %}
<!-- Staff -->
<div class="col-sm-6 col-md-4">
<div class="form-floating">
<select class="form-control" id="{{ form.staff.id_for_label }}" name="{{ form.staff.name }}">
{% for value, label in form.staff.field.choices %}
<option value="{{ value }}" {% if form.staff.value == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<label for="{{ form.staff.id_for_label }}">{{ _("Staff") }}</label>
</div>
{{ form.staff.errors }}
</div> </div>
<!-- closing date--> <!-- Expected Revenue -->
<div class="col-sm-6 col-md-4"> <div class="mb-4">
<div class="form-floating"> <label class="form-label" for="{{ form.expected_revenue.id_for_label }}">
<input type="date" class="form-control" id="{{ form.closing_date.id_for_label }}" name="{{ form.closing_date.name }}" value="{{ form.closing_date.value|date:'Y-m-d' }}"> {{ form.expected_revenue.label }}
<label for="{{ form.closing_date.id_for_label }}">{{ _("Closing Date")}}</label> </label>
<div class="input-group">
<span class="input-group-text"><span class="currency">{{CURRENCY}}</span></span>
{{ form.expected_revenue|add_class:"form-control" }}
</div>
{% if form.expected_revenue.errors %}
<div class="invalid-feedback d-block">
{{ form.expected_revenue.errors }}
</div>
{% endif %}
</div>
<!-- Closing Date -->
<div class="mb-5">
<label class="form-label" for="{{ form.closing_date.id_for_label }}">
{{ form.closing_date.label }}
</label>
<div class="input-group">
{{ form.closing_date|add_class:"form-control" }}
<span class="input-group-text"><span class="far fa-calendar"></span></span>
</div>
{% if form.closing_date.errors %}
<div class="invalid-feedback d-block">
{{ form.closing_date.errors }} {{ form.closing_date.errors }}
</div> </div>
{{ form.closing_date.errors }} {% endif %}
</div> </div>
<!-- Buttons --> <!-- Form Actions -->
<div class="row mt-4"> <div class="d-flex justify-content-end gap-3">
<div class="col-sm-12"> <button type="reset" class="btn btn-phoenix-danger px-4">
<button type="submit" class="btn btn-primary">{{ _("Save") }}</button> <span class="fas fa-redo me-2"></span>{% trans "Reset" %}
<a href="{% url 'opportunity_list' %}" class="btn btn-secondary">{{ _("Cancel") }}</a> </button>
</div> <button type="submit" class="btn btn-phoenix-primary px-6">
{% if form.instance.pk %}
<span class="fas fa-save me-2"></span>{% trans "Update" %}
{% else %}
<span class="fas fa-plus me-2"></span>{% trans "Create" %}
{% endif %}
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-body p-4">
<h4 class="mb-3">{% trans "Opportunity Guidelines" %}</h4>
<ul class="nav flex-column gap-2 nav-guide">
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-primary fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Probability indicates conversion chance" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-warning fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Update stage as deal progresses" %}</span>
</div>
</li>
<li class="nav-item">
<div class="d-flex align-items-center">
<span class="fas fa-circle text-success fs-11 me-2"></span>
<span class="text-body-highlight">{% trans "Set realistic closing dates" %}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script>
function updateProbabilityValue(value) {
const badge = document.getElementById('probability-value');
badge.textContent = value + '%';
// Update badge color based on value
if (value >= 75) {
badge.className = 'badge badge-phoenix fs-6 badge-phoenix-success';
} else if (value >= 50) {
badge.className = 'badge badge-phoenix fs-6 badge-phoenix-warning';
} else {
badge.className = 'badge badge-phoenix fs-6 badge-phoenix-danger';
}
}
// Initialize on load
document.addEventListener('DOMContentLoaded', function() {
const rangeInput = document.getElementById('{{ form.probability.id_for_label }}');
updateProbabilityValue(rangeInput.value);
});
</script>
{% endblock %} {% endblock %}

View File

@ -1,134 +1,93 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static %} {% load i18n static humanize %}
{% load custom_filters %}
{% block content %} {% block content %}
<div class="row g-3"> <div class="row g-3">
<h2 class="mb-5">{{ _("Opportunities") }}</h2> <div class="col-12">
<div class="d-xl-flex justify-content-between"> <h2 class="mb-3">{{ _("Opportunities") }}</h2>
<div class="mb-3">
<a class="btn btn-sm btn-phoenix-primary me-4" href="{% url 'opportunity_create' %}"><span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}</a>
</div>
</div>
</div>
<div>
<div class="px-4 px-lg-6 ">
<div class="deals-items-container">
<div class="deals scrollbar">
<div class="deals-col me-4">
<div class="w-100 min-vh-50">
{% for opportunity in opportunities %}
<div class="card mb-3">
<div class="card-body">
<h5 class="mb-2 ms-4">{{ opportunity.car.id_car_make.get_local_name }} - {{ opportunity.car.id_car_model.get_local_name }} - {{ opportunity.car.year }}</h5>
<a class="dropdown-indicator-icon position-absolute text-body-tertiary text-end"
href="#collapseWidthDeals-{{ opportunity.pk }}"
role="button"
data-bs-toggle="collapse"
aria-expanded="false"
aria-controls="collapseWidthDeals-{{ opportunity.pk }}">
<span class="fa-solid fa-angle-down text-end"></span>
</a>
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex gap-3">
</div>
<div class="d-flex">
<span class="me-2" data-feather="clock" style="stroke-width:2;"></span>
<p class="mb-0 fs-9 fw-semibold text-body-tertiary date">{{ opportunity.created|date }}<span class="text-body-quaternary"> . {{ opportunity.created|time}}</span></p>
</div>
</div>
<div class="deals-items-head d-flex align-items-center mb-2">
<a class="text-primary fw-bold line-clamp-1 me-3 mb-0 fs-9" href="{% url 'opportunity_detail' opportunity.pk %}">{{ _("View") }}</a>
<p class="fs-10 mb-0 mt-1 d-none"><span class="me-1 text-body-quaternary" data-feather="grid" style="stroke-width:2; height: 12px; width: 12px"></span>{{ opportunity.get_stage_display }}</p>
<p class="ms-auto fs-9 text-body-emphasis fw-semibold mb-0 deals-revenue">{{ opportunity.car.finances.total }}</p>
</div>
<div class="deals-company-agent d-flex flex-between-center">
<div class="d-flex align-items-center"><span class="uil uil-user me-2"></span>
<p class="text-body-secondary fw-bold fs-10 mb-0">{{ opportunity.customer.get_full_name }}</p>
</div>
<div class="d-flex align-items-center"><span class="uil uil-headphones me-2"></span>
<p class="text-body-secondary fw-bold fs-10 mb-0">{{ opportunity.staff.name }}</p>
</div>
</div>
<div class="collapse" id="collapseWidthDeals-{{ opportunity.pk }}">
<div class="d-flex gap-2 mb-5"><span class="badge badge-phoenix fs-10 badge-phoenix-info">{{ opportunity.get_stage_display }}</span><span class="badge badge-phoenix fs-10 badge-phoenix-danger">{{ opportunity.get_status_display }}</span></div>
<table class="mb-4 w-100 table-stats table-stats">
<tr>
<th>{{ _("Details") }}</th>
<th>:</th>
<th></th>
</tr>
<tr>
<td class="py-1">
<div class="d-flex align-items-center"><span class="me-2 text-body-tertiary" data-feather="dollar-sign"></span>
<p class="fw-semibold fs-9 mb-0 text-body-tertiary">{{ _("Expected Revenue")}}</p>
</div>
</td>
<td class="d-none d-sm-block pe-sm-2">:</td>
<td class="py-1">
<p class="ps-6 ps-sm-0 fw-semibold fs-9 mb-0 mb-0 pb-3 pb-sm-0 text-body-emphasis">{{ opportunity.car.finances.total }}</p>
</td>
</tr>
<tr>
<td class="py-1">
<div class="d-flex align-items-center"><span class="me-2 text-body-tertiary" data-feather="user" style="width:16px; height:16px"></span>
<p class="fw-semibold fs-9 mb-0 text-body-tertiary">{{ _("Contact") }}</p>
</div>
</td>
<td class=" d-none d-sm-block pe-sm-2">:</td>
<td class="py-1">
<p class="fw-semibold fs-9 mb-0 mb-0 pb-3 pb-sm-0 text-body-emphasis d-flex align-items-center gap-2"><a href=""> <span class="fa-solid fa-square-phone text-body-tertiary"></span></a><a href=""> <span class="fa-solid fa-square-envelope text-body-tertiary"></span></a><a href=""> <span class="fab fa-whatsapp-square text-body-tertiary"></span></a></p>
</td>
</tr>
<tr>
<td class="py-1">
<div class="d-flex align-items-center"><span class="me-2 text-body-tertiary" data-feather="calendar" style="width:16px; height:16px"></span>
<p class="fw-semibold fs-9 mb-0 text-body-tertiary">{{ _("Closing Date")}}</p>
</div>
</td>
<td class=" d-none d-sm-block pe-sm-2">:</td>
<td class="py-1">
<p class="fw-semibold fs-9 mb-0 mb-0 pb-3 pb-sm-0 text-body-emphasis">{{ opportunity.closing_date }}</p>
</td>
</tr>
</table>
<p class="fs-9 mb-1 fw-bold"> {{ _("Probability") }}: %</p>
<div class="progress" style="height:16px">
{% if opportunity.probability >= 25 and opportunity.probability < 49 %}
<div class="progress-bar rounded-pill bg-danger-dark" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="{{ opportunity.probability }}" aria-valuemin="0" aria-valuemax="100">
<span class="fw-bolder fs-9 text-sm-end me-1">{{ opportunity.probability }}</span>
</div>
{% elif opportunity.probability >= 50 and opportunity.probability <= 74 %}
<div class="progress-bar rounded-pill bg-warning-dark" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="{{ opportunity.probability }}" aria-valuemin="0" aria-valuemax="100">
<span class="fw-bolder fs-9 text-sm-end me-1">{{ opportunity.probability }}</span>
</div>
{% elif opportunity.probability >= 75 and opportunity.probability <= 100 %}
<div class="progress-bar rounded-pill bg-success-dark" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="{{ opportunity.probability }}" aria-valuemin="0" aria-valuemax="100">
<span class="fw-bolder fs-9 text-sm-end me-1">{{ opportunity.probability }}</span>
</div>
{% endif %}
</div>
</div>
</div> </div>
<div class="col-12">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
<!-- Filter Controls -->
<div class="d-flex flex-column flex-lg-row align-items-start align-items-lg-center gap-3 w-100" id="filter-container">
<!-- Search Input - Wider and properly aligned -->
<div class="position-relative flex-grow-1" style="min-width: 300px;">
<span class="fas fa-search position-absolute top-50 translate-middle-y ms-3 text-body-tertiary"></span>
<input
class="form-control ps-6"
type="text"
placeholder="{% trans 'Search opportunities...' %}"
name="search"
hx-get="{% url 'opportunity_list' %}"
hx-trigger="keyup changed delay:500ms"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-include="#filter-container select"
hx-swap="outerHTML"
style="width: 100%;"
>
</div> </div>
<!-- Filter Dropdowns - Aligned in a row -->
<div class="d-flex flex-column flex-sm-row gap-3 w-100" style="max-width: 500px;">
<!-- Stage Filter -->
<div class="flex-grow-1">
<select
class="form-select"
name="stage"
hx-get="{% url 'opportunity_list' %}"
hx-trigger="change"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select"
>
<option value="">{% trans "All Stages" %}</option>
{% for value, label in stage_choices %}
<option value="{{ value }}" {% if request.GET.stage == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %} {% endfor %}
</select>
</div>
<!-- Sort Filter -->
<div class="flex-grow-1">
<select
class="form-select"
name="sort"
hx-get="{% url 'opportunity_list' %}"
hx-trigger="change"
hx-target="#opportunities-grid"
hx-select="#opportunities-grid"
hx-swap="outerHTML"
hx-include="#filter-container input, #filter-container select"
>
<option value="newest" {% if request.GET.sort == 'newest' %}selected{% endif %}>
{% trans "Newest First" %}
</option>
<option value="highest" {% if request.GET.sort == 'highest' %}selected{% endif %}>
{% trans "Highest Value" %}
</option>
<option value="closing" {% if request.GET.sort == 'closing' %}selected{% endif %}>
{% trans "Earliest Close Date" %}
</option>
</select>
</div>
</div>
</div>
<div class="d-flex justify-content-end">
<a class="btn btn-phoenix-primary btn-sm" href="{% url 'opportunity_create' %}">
<span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}
</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1">
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,109 @@
{% load i18n static humanize %}
{% load custom_filters %}
{% block customCSS %}
<style>
.bg-success-soft {
background-color: rgba(25, 135, 84, 0.1) !important;
opacity: .8;
}
.bg-danger-soft {
background-color: rgba(220, 53, 69, 0.1) !important;
opacity: .8;
}
</style>
{% endblock customCSS %}
{% for opportunity in opportunities %}
<div class="col-12 col-md-6 col-lg-4 col-xl-3">
<div class="card h-100
{% if opportunity.get_stage_display == 'Closed Won' %}bg-success-soft
{% elif opportunity.get_stage_display == 'Closed Lost' %}bg-danger-soft{% endif %}">
<div class="card-body">
<h5 class="mb-4">Opportunity for {{ opportunity.customer.customer_name }}</h5>
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex gap-2">
{% if opportunity.get_stage_display == "Negotiation" %}
<span class="badge badge-phoenix fs-10 badge-phoenix-primary">{{ opportunity.get_stage_display }}</span>
{% elif opportunity.get_stage_display == "Discovery" %}
<span class="badge badge-phoenix fs-10 badge-phoenix-info">{{ opportunity.get_stage_display }}</span>
{% elif opportunity.get_stage_display == "Proposal" %}
<span class="badge badge-phoenix fs-10 badge-phoenix-warning">{{ opportunity.get_stage_display }}</span>
{% elif opportunity.get_stage_display == "Closed Won" %}
<span class="badge badge-phoenix fs-10 badge-phoenix-success">{{ opportunity.get_stage_display }}</span>
{% elif opportunity.get_stage_display == "Closed Lost" %}
<span class="badge badge-phoenix fs-10 badge-phoenix-danger">{{ opportunity.get_stage_display }}</span>
{% endif %}
<span class="badge badge-phoenix fs-10
{% if opportunity.get_stage_display == 'Won' %}badge-phoenix-success
{% elif opportunity.get_stage_display == 'Lost' %}badge-phoenix-danger{% endif %}">
{{ opportunity.get_status_display }}
</span>
</div>
<div class="d-flex align-items-center">
<i class="fa-regular fa-clock"></i>&nbsp;
<p class="mb-0 fs-9 fw-semibold text-body-tertiary">{{ opportunity.created|naturalday|capfirst }}</p>
</div>
</div>
<div class="deals-company-agent d-flex justify-content-between mb-3">
<div class="d-flex align-items-center">
<span class="uil uil-user me-2"></span>
<p class="text-body-secondary fw-bold fs-10 mb-0">{{ opportunity.staff.name }}</p>
</div>
</div>
<table class="mb-3 w-100">
<tr>
<td class="py-1">
<div class="d-flex align-items-center">
<span class="me-2 text-body-tertiary"><span class="currency">{{ CURRENCY }}</span></span>
<p class="fw-semibold fs-9 mb-0 text-body-tertiary">{{ _("Expected Revenue")}}</p>
</div>
</td>
<td class="text-end">
<p class="fw-semibold fs-9 mb-0 text-body-emphasis"><span class="currency">{{ CURRENCY }}</span>{{ opportunity.expected_revenue }}</p>
</td>
</tr>
<tr>
<td class="py-1">
<div class="d-flex align-items-center">
<i class="uil uil-calendar-alt"></i>&nbsp;
<p class="fw-semibold fs-9 mb-0 text-body-tertiary">{{ _("Closing Date")}}</p>
</div>
</td>
<td class="text-end">
<p class="fw-semibold fs-9 mb-0 text-body-emphasis">{{ opportunity.closing_date|naturalday|capfirst }}</p>
</td>
</tr>
</table>
<p class="fs-9 mb-1 fw-bold">{{ _("Probability") }}: {{ opportunity.probability }}%</p>
<div class="progress mb-3" style="height:16px">
{% if opportunity.probability >= 25 and opportunity.probability < 49 %}
<div class="progress-bar rounded-pill bg-danger-dark" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="{{ opportunity.probability }}" aria-valuemin="0" aria-valuemax="100">
<span class="fw-bolder fs-9 text-sm-end me-1">{{ opportunity.probability }}</span>
</div>
{% elif opportunity.probability >= 50 and opportunity.probability <= 74 %}
<div class="progress-bar rounded-pill bg-warning-dark" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="{{ opportunity.probability }}" aria-valuemin="0" aria-valuemax="100">
<span class="fw-bolder fs-9 text-sm-end me-1">{{ opportunity.probability }}</span>
</div>
{% elif opportunity.probability >= 75 and opportunity.probability <= 100 %}
<div class="progress-bar rounded-pill bg-success-dark" role="progressbar" style="width: {{ opportunity.probability }}%" aria-valuenow="{{ opportunity.probability }}" aria-valuemin="0" aria-valuemax="100">
<span class="fw-bolder fs-9 text-sm-end me-1">{{ opportunity.probability }}</span>
</div>
{% endif %}
</div>
<div class="d-flex gap-2">
<a class="btn btn-sm btn-phoenix-primary" href="{% url 'opportunity_detail' opportunity.pk %}">
{{ _("View Details") }} <i class="fa-solid fa-eye ms-2"></i>
</a>
<a class="btn btn-sm btn-phoenix-success" href="{% url 'update_opportunity' opportunity.pk %}">
{{ _("Update") }} <i class="fa-solid fa-pen ms-2"></i>
</a>
</div>
</div>
</div>
</div>
{% endfor %}

View File

@ -142,6 +142,13 @@
</div> </div>
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{% url 'opportunity_list' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span data-feather="users"></span></span><span class="nav-link-text">{% trans 'Opportunity'|capfirst %}</span>
</div>
</a>
</li>
{% endif %} {% endif %}
{% if perms.django_ledger.view_customermodel %} {% if perms.django_ledger.view_customermodel %}
<li class="nav-item"> <li class="nav-item">

View File

@ -47,4 +47,3 @@
</div> </div>
</div> </div>
</li> </li>