update on the lead & opportunity
This commit is contained in:
parent
cca37be3b3
commit
2ee9a86720
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@ -50,7 +50,8 @@ from .models import (
|
||||
CarMake,
|
||||
Customer,
|
||||
Organization,
|
||||
DealerSettings
|
||||
DealerSettings,
|
||||
Tasks
|
||||
)
|
||||
from django_ledger import models as ledger_models
|
||||
from django.forms import (
|
||||
@ -1120,6 +1121,7 @@ class ActivityForm(forms.ModelForm):
|
||||
associated with the form and the fields it comprises.
|
||||
:type Meta: type
|
||||
"""
|
||||
activity_type = forms.ChoiceField(choices=[("call", "Call"), ("email", "Email"), ("meeting", "Meeting")])
|
||||
class Meta:
|
||||
model = Activity
|
||||
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.
|
||||
: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:
|
||||
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):
|
||||
@ -1684,3 +1711,33 @@ class PaymentPlanForm(forms.Form):
|
||||
self.fields['first_name'].initial = user.first_name
|
||||
self.fields['last_name'].initial = user.last_name
|
||||
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'}),
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
20
inventory/migrations/0019_tasks_dealer.py
Normal file
20
inventory/migrations/0019_tasks_dealer.py
Normal 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,
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0020_tasks_completed.py
Normal file
18
inventory/migrations/0020_tasks_completed.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
19
inventory/migrations/0022_opportunity_expected_revenue.py
Normal file
19
inventory/migrations/0022_opportunity_expected_revenue.py
Normal 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,
|
||||
),
|
||||
]
|
||||
18
inventory/migrations/0023_alter_opportunity_stage.py
Normal file
18
inventory/migrations/0023_alter_opportunity_stage.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
20
inventory/migrations/0024_alter_opportunity_customer.py
Normal file
20
inventory/migrations/0024_alter_opportunity_customer.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@ -973,9 +973,9 @@ class Channel(models.TextChoices):
|
||||
|
||||
class Status(models.TextChoices):
|
||||
NEW = "new", _("New")
|
||||
FOLLOW_UP = "follow_up", _("Needs Follow-up")
|
||||
NEGOTIATION = "negotiation", _("Under Negotiation")
|
||||
WON = "won", _("Converted")
|
||||
FOLLOW_UP = "follow_up", _("Follow-up")
|
||||
NEGOTIATION = "negotiation", _("Negotiation")
|
||||
WON = "won", _("Won")
|
||||
LOST = "lost", _("Lost")
|
||||
CLOSED = "closed", _("Closed")
|
||||
|
||||
@ -1020,7 +1020,7 @@ class ActionChoices(models.TextChoices):
|
||||
|
||||
|
||||
class Stage(models.TextChoices):
|
||||
PROSPECT = "prospect", _("Prospect")
|
||||
DISCOVERY = "discovery", _("Discovery")
|
||||
PROPOSAL = "proposal", _("Proposal")
|
||||
NEGOTIATION = "negotiation", _("Negotiation")
|
||||
CLOSED_WON = "closed_won", _("Closed Won")
|
||||
@ -1515,7 +1515,7 @@ class Opportunity(models.Model):
|
||||
Dealer, on_delete=models.CASCADE, related_name="opportunities"
|
||||
)
|
||||
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, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_("Car")
|
||||
@ -1523,12 +1523,6 @@ class Opportunity(models.Model):
|
||||
stage = models.CharField(
|
||||
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,
|
||||
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)
|
||||
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)
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
||||
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)
|
||||
class Meta:
|
||||
verbose_name = _("Opportunity")
|
||||
@ -1569,6 +1565,31 @@ class Notes(models.Model):
|
||||
def __str__(self):
|
||||
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):
|
||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
object_id = models.UUIDField()
|
||||
|
||||
@ -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>/delete-note/", views.delete_note, name="delete_note_to_lead"),
|
||||
path(
|
||||
"crm/leads/<int:pk>/add-activity/",
|
||||
views.add_activity_to_lead,
|
||||
"crm/<int:pk>/update-task/",
|
||||
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",
|
||||
),
|
||||
path(
|
||||
|
||||
@ -1362,6 +1362,6 @@ def handle_payment(request,order):
|
||||
return transaction_url
|
||||
|
||||
|
||||
|
||||
# def get_user_quota(user):
|
||||
# return user.dealer.quota
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import numpy as np
|
||||
# from rich import print
|
||||
from random import randint
|
||||
from decimal import Decimal
|
||||
from django.apps import apps
|
||||
from datetime import timedelta
|
||||
from calendar import month_name
|
||||
from pyzbar.pyzbar import decode
|
||||
@ -24,7 +25,7 @@ from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import Func
|
||||
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.shortcuts import HttpResponse
|
||||
from django.db.models import Sum, F, Count
|
||||
@ -4580,10 +4581,15 @@ class LeadDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
context["activities"] = models.Activity.objects.filter(
|
||||
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(
|
||||
lead=self.object
|
||||
)
|
||||
context["transfer_form"] = forms.LeadTransferForm()
|
||||
context["activity_form"] = forms.ActivityForm()
|
||||
context["staff_task_form"] = forms.StaffTaskForm()
|
||||
return context
|
||||
|
||||
|
||||
@ -4673,10 +4679,10 @@ def lead_create(request):
|
||||
|
||||
def lead_tracking(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"])
|
||||
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")
|
||||
context = {"new": new,"follow_up": follow_up,"won": won,"lose": lose,"negotiation": negotiation}
|
||||
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')
|
||||
current_action = request.POST.get('current_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', '')
|
||||
|
||||
# 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)
|
||||
|
||||
# Get the lead
|
||||
lead = models.Lead.objects.get(id=lead_id)
|
||||
|
||||
# Update lead fields
|
||||
lead.action = current_action
|
||||
lead.status = current_action
|
||||
lead.next_action = next_action
|
||||
lead.next_action_date = next_action_date
|
||||
|
||||
# Parse the datetime string
|
||||
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')
|
||||
lead.next_action_date = timezone.make_aware(next_action_datetime)
|
||||
except ValueError:
|
||||
@ -5060,10 +5066,6 @@ def send_lead_email(request, pk, email_pk=None):
|
||||
response["HX-Redirect"] = reverse("lead_detail", args=[lead.pk])
|
||||
return response
|
||||
|
||||
lead.status = models.Status.CONTACTED
|
||||
lead.save()
|
||||
|
||||
|
||||
if request.method == "POST":
|
||||
email_pk = request.POST.get("email_pk")
|
||||
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
|
||||
"""
|
||||
lead = get_object_or_404(models.Lead, 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)
|
||||
print(activity)
|
||||
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.save()
|
||||
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})
|
||||
|
||||
|
||||
class OpportunityCreateView(CreateView, LoginRequiredMixin):
|
||||
class OpportunityCreateView(CreateView,SuccessMessageMixin, LoginRequiredMixin):
|
||||
"""
|
||||
Handles the creation of Opportunity instances through a form while enforcing
|
||||
specific user access control and initial data population. This view ensures
|
||||
@ -5173,30 +5180,33 @@ class OpportunityCreateView(CreateView, LoginRequiredMixin):
|
||||
model = models.Opportunity
|
||||
form_class = forms.OpportunityForm
|
||||
template_name = "crm/opportunities/opportunity_form.html"
|
||||
success_message = "Opportunity created successfully."
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
dealer = get_user_type(self.request)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
if self.kwargs.get("pk", None):
|
||||
lead = models.Lead.objects.get(pk=self.kwargs.get("pk"))
|
||||
# def get_initial(self):
|
||||
# initial = super().get_initial()
|
||||
# if self.kwargs.get("pk", None):
|
||||
# lead = models.Lead.objects.get(pk=self.kwargs.get("pk"))
|
||||
|
||||
initial["customer"] = lead.customer
|
||||
return initial
|
||||
# initial["customer"] = lead.customer
|
||||
# return initial
|
||||
|
||||
def form_valid(self, form):
|
||||
dealer = get_user_type(self.request)
|
||||
form.instance.dealer = dealer
|
||||
form.instance.customer = form.instance.lead.customer
|
||||
form.instance.staff = form.instance.lead.staff
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
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.
|
||||
|
||||
@ -5221,6 +5231,7 @@ class OpportunityUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = models.Opportunity
|
||||
form_class = forms.OpportunityForm
|
||||
template_name = "crm/opportunities/opportunity_form.html"
|
||||
success_message = "Opportunity updated successfully."
|
||||
|
||||
def get_success_url(self):
|
||||
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])
|
||||
form.fields["status"].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
|
||||
context["status_form"] = form
|
||||
context["notes"] = models.Notes.objects.filter(
|
||||
@ -5272,25 +5282,6 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView):
|
||||
|
||||
|
||||
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
|
||||
template_name = "crm/opportunities/opportunity_list.html"
|
||||
context_object_name = "opportunities"
|
||||
@ -5298,9 +5289,38 @@ class OpportunityListView(LoginRequiredMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
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
|
||||
def delete_opportunity(request, pk):
|
||||
"""
|
||||
@ -7706,3 +7726,78 @@ def mark_all_notifications_as_read(request):
|
||||
def notifications_history(request):
|
||||
models.Notification.objects.filter(user=request.user, is_read=False).update(read=True)
|
||||
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
|
||||
30
templates/components/activity_modal.html
Normal file
30
templates/components/activity_modal.html
Normal 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>
|
||||
@ -1,6 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
{% block content %}
|
||||
{% load i18n static humanize %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
{% block customCSS %}
|
||||
@ -8,11 +7,91 @@
|
||||
.main-tab li:last-child {
|
||||
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>
|
||||
{% endblock customCSS %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row g-3">
|
||||
<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="col-12 col-md-auto">
|
||||
<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>
|
||||
<h5 class="text-body-highlight mb-0">{{ _("Created")}}</h5>
|
||||
</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 class="mb-3">
|
||||
<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 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> <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> <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" 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="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">
|
||||
<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-dialog">
|
||||
<div class="modal-content">
|
||||
@ -162,8 +251,9 @@
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<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>
|
||||
<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 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">
|
||||
@ -196,9 +286,9 @@
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,15 +299,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<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>">
|
||||
<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>">
|
||||
<span class="fas fa-plus me-1"></span>
|
||||
{% trans 'Add Note' %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="border-top border-bottom border-translucent" id="leadDetailsTable">
|
||||
<div class="table-responsive scrollbar mx-n1 px-1">
|
||||
<table class="table fs-9 mb-0">
|
||||
@ -239,7 +328,7 @@
|
||||
{% 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>
|
||||
{% 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">
|
||||
{% 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>">
|
||||
@ -261,8 +350,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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 class="scrollbar">
|
||||
@ -302,7 +397,7 @@
|
||||
<div class="fs-10 d-block">{{email.to_email}}</div>
|
||||
</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="status align-middle fw-semibold text-end py-2"><span class="badge badge-phoenix fs-10 badge-phoenix-success">sent</span></td>
|
||||
</tr>
|
||||
@ -375,13 +470,73 @@
|
||||
</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>
|
||||
{% include 'modal/delete_modal.html' %}
|
||||
<!-- add update Modal -->
|
||||
<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-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>
|
||||
@ -395,6 +550,33 @@
|
||||
</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>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
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>
|
||||
{% endblock %}
|
||||
{% endblock customJS %}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
{% load i18n static humanize %}
|
||||
{% block title %}{{ _('Leads')|capfirst }}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
@ -112,19 +112,7 @@
|
||||
<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>{{ _("Current 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>
|
||||
<span>{{ _("Action")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
|
||||
@ -133,32 +121,15 @@
|
||||
<span>{{ _("Assigned To")|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>{{ _("Source")|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>{{ _("Channel")|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>
|
||||
<span>{{ _("Opportunity")|capfirst }}</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 15%;">
|
||||
{{ _("Create date") }}
|
||||
{{ _("Action") }}
|
||||
</th>
|
||||
<th class="text-end white-space-nowrap align-middle" scope="col"></th>
|
||||
</tr>
|
||||
@ -230,15 +201,15 @@
|
||||
{% if schedule.scheduled_type == "call" %}
|
||||
<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>
|
||||
{{ schedule.scheduled_at }}</span></a>
|
||||
{{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
|
||||
{% elif schedule.scheduled_type == "meeting" %}
|
||||
<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>
|
||||
{{ schedule.scheduled_at }}</span></a>
|
||||
{{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
|
||||
{% elif schedule.scheduled_type == "email" %}
|
||||
<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>
|
||||
{{ schedule.scheduled_at }}</span></a>
|
||||
{{ schedule.scheduled_at|naturalday|capfirst }}</span></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@ -255,12 +226,8 @@
|
||||
</div>
|
||||
</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 fw-semibold text-body-highlight">{{ lead.source|upper }}</td>
|
||||
<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">
|
||||
{% comment %} <td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{% if lead.opportunity.stage == "prospect" %}
|
||||
<span class="badge text-bg-primary">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% elif lead.opportunity.stage == "proposal" %}
|
||||
@ -272,14 +239,12 @@
|
||||
{% elif lead.opportunity.stage == "closed_lost" %}
|
||||
<span class="badge text-bg-danger">{{ lead.opportunity.stage|upper }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td> {% endcomment %}
|
||||
<td class="align-middle white-space-nowrap text-body-tertiary text-opacity-85 fw-semibold text-body-highlight">
|
||||
{% if lead.opportunity %}
|
||||
<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>
|
||||
{% else %}
|
||||
<span class="badge badge-phoenix badge-phoenix-danger">{{ _("No") }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="align-middle white-space-nowrap text-end">
|
||||
|
||||
@ -42,6 +42,22 @@
|
||||
.lead-card small {
|
||||
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>
|
||||
{% endblock customCSS %}
|
||||
{% block content %}
|
||||
@ -51,7 +67,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Column Template -->
|
||||
<!-- New Lead -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column">
|
||||
<div class="kanban-header">New Leads ({{new|length}})</div>
|
||||
@ -83,38 +99,6 @@
|
||||
</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 -->
|
||||
<div class="col-md">
|
||||
<div class="kanban-column">
|
||||
@ -130,6 +114,39 @@
|
||||
{% endfor %}
|
||||
</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>
|
||||
{% endblock %}
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
{% load i18n static humanize %}
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -169,9 +170,9 @@
|
||||
<p class="fw-bold mb-0">{{ _("Estimated Revenue") }}</p>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
@ -296,7 +297,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{% for activity in activities %}
|
||||
@ -307,7 +308,7 @@
|
||||
<span class="fa-solid fa-phone text-warning fs-8"></span>
|
||||
{% elif activity.activity_type == "email" %}
|
||||
<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>
|
||||
{% elif activity.activity_type == "whatsapp" %}
|
||||
<span class="fab fa-whatsapp text-success-dark fs-7"></span>
|
||||
@ -319,7 +320,7 @@
|
||||
<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>
|
||||
</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>
|
||||
<p class="fs-9 mb-0"></p>
|
||||
</div>
|
||||
@ -552,4 +553,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include "components/activity_modal.html" with content_type="opportunity" pk=opportunity.pk %}
|
||||
{% endblock %}
|
||||
@ -1,95 +1,203 @@
|
||||
{% extends "base.html" %} <!-- Assuming you have a base template -->
|
||||
{% load i18n %} <!-- Load the internationalization template tags -->
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static widget_tweaks custom_filters %}
|
||||
|
||||
{% 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="col-sm-6 col-md-8">
|
||||
<h2>{% if form.instance.pk %}{{ _("Edit Opportunity") }}{% else %}{{ _("Add New Opportunity") }}{% endif %}</h2>
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-8">
|
||||
<form class="row g-3" method="post" class="form" novalidate>
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 p-sm-5">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Customer -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<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>
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ form.non_field_errors }}
|
||||
</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>
|
||||
|
||||
<!-- Car -->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-control" id="{{ form.car.id_for_label }}" name="{{ form.car.name }}">
|
||||
{% for value, label in form.car.field.choices %}
|
||||
<option value="{{ value }}" {% if form.car.value == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="{{ form.car.id_for_label }}">{{ _("Car") }}</label>
|
||||
</div>
|
||||
<!-- Car Field -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.car.id_for_label }}">
|
||||
{{ form.car.label }}
|
||||
<span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.car|add_class:"form-control" }}
|
||||
{% if form.car.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{{ form.car.errors }}
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
{% endif %}
|
||||
</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 }}
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
{% endif %}
|
||||
</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 }}
|
||||
</div>
|
||||
|
||||
<!-- 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 }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- closing date-->
|
||||
<div class="col-sm-6 col-md-4">
|
||||
<div class="form-floating">
|
||||
<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' }}">
|
||||
<label for="{{ form.closing_date.id_for_label }}">{{ _("Closing Date")}}</label>
|
||||
<!-- Expected Revenue -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="{{ form.expected_revenue.id_for_label }}">
|
||||
{{ form.expected_revenue.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 }}
|
||||
</div>
|
||||
{{ form.closing_date.errors }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-primary">{{ _("Save") }}</button>
|
||||
<a href="{% url 'opportunity_list' %}" class="btn btn-secondary">{{ _("Cancel") }}</a>
|
||||
</div>
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-end gap-3">
|
||||
<button type="reset" class="btn btn-phoenix-danger px-4">
|
||||
<span class="fas fa-redo me-2"></span>{% trans "Reset" %}
|
||||
</button>
|
||||
<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>
|
||||
</form>
|
||||
</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 %}
|
||||
@ -1,134 +1,93 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static %}
|
||||
{% load i18n static humanize %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="row g-3">
|
||||
<h2 class="mb-5">{{ _("Opportunities") }}</h2>
|
||||
<div class="d-xl-flex justify-content-between">
|
||||
<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 class="col-12">
|
||||
<h2 class="mb-3">{{ _("Opportunities") }}</h2>
|
||||
</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>
|
||||
|
||||
<!-- 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 %}
|
||||
</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 id="opportunities-grid" class="row g-4 px-2 px-lg-4 mt-1">
|
||||
{% include 'crm/opportunities/partials/opportunity_grid.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
109
templates/crm/opportunities/partials/opportunity_grid.html
Normal file
109
templates/crm/opportunities/partials/opportunity_grid.html
Normal 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>
|
||||
<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>
|
||||
<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 %}
|
||||
@ -142,6 +142,13 @@
|
||||
</div>
|
||||
</a>
|
||||
</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 %}
|
||||
{% if perms.django_ledger.view_customermodel %}
|
||||
<li class="nav-item">
|
||||
|
||||
@ -47,4 +47,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user