Improvements on CRM

sync
Laurent 10 months ago
parent 53c10644c1
commit ab1713b29e
  1. 13
      crm/filters.py
  2. 10
      crm/forms.py
  3. 3
      crm/models.py
  4. 6
      crm/static/crm/js/prospects.js
  5. 10
      crm/static/js/prospect_list.js
  6. 2
      crm/templates/crm/base.html
  7. 17
      crm/templates/crm/prospect_form.html
  8. 42
      crm/templates/crm/prospect_list.html
  9. 6
      crm/urls.py
  10. 104
      crm/views.py
  11. 41
      tournaments/static/tournaments/css/style.css

@ -1,14 +1,23 @@
import django_filters
from django.db.models import Q
from .models import Event, Status, Prospect
class ProspectFilter(django_filters.FilterSet):
zip_code = django_filters.CharFilter(lookup_expr='icontains')
zip_code = django_filters.CharFilter(lookup_expr='istartswith', label='Code postal')
events = django_filters.ModelMultipleChoiceFilter(
queryset=Event.objects.all(),
field_name='events',
)
city = django_filters.CharFilter(lookup_expr='icontains', label='Ville')
name = django_filters.CharFilter(method='filter_name', label='Nom')
def filter_name(self, queryset, name, value):
return queryset.filter(
Q(first_name__icontains=value) | Q(last_name__icontains=value) | Q(entity_name__icontains=value)
)
class Meta:
model = Prospect
fields = ['city', 'events']
fields = ['name', 'city', 'events', 'zip_code']

@ -12,8 +12,11 @@ class SmallTextArea(forms.Textarea):
})
super().__init__(*args, **kwargs)
class CSVImportForm(forms.Form):
csv_file = forms.FileField()
class ProspectForm(forms.ModelForm):
class Meta:
model = Prospect
fields = ['entity_name', 'first_name', 'last_name', 'email',
'phone', 'address', 'zip_code', 'city']
class BulkEmailForm(forms.Form):
prospects = forms.ModelMultipleChoiceField(
@ -38,3 +41,6 @@ class EventForm(forms.ModelForm):
widgets = {
'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
}
class CSVImportForm(forms.Form):
csv_file = forms.FileField()

@ -41,6 +41,9 @@ class Prospect(models.Model):
("view_prospects", "Can view prospects"),
]
def full_name(self):
return f'{self.first_name} {self.last_name}'
def __str__(self):
return ' - '.join(filter(None, [self.entity_name, self.first_name, self.last_name, f"({self.email})"]))

@ -0,0 +1,6 @@
document.getElementById("select-all").addEventListener("change", function () {
const checkboxes = document.getElementsByName("selected_prospects");
for (let checkbox of checkboxes) {
checkbox.checked = this.checked;
}
});

@ -1,10 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
const selectAll = document.getElementById("select-all");
const prospectCheckboxes = document.getElementsByName("selected_prospects");
selectAll.addEventListener("change", function () {
prospectCheckboxes.forEach((checkbox) => {
checkbox.checked = selectAll.checked;
});
});
});

@ -37,6 +37,8 @@
</script>
<!-- End Matomo Code -->
{% block extra_js %}{% endblock %}
</head>
<body class="wrapper">

@ -0,0 +1,17 @@
{% extends "crm/base.html" %}
{% block head_title %}{{ first_title }}{% endblock %}
{% block first_title %}{{ first_title }}{% endblock %}
{% block second_title %}{{ second_title }}{% endblock %}
{% block content %}
<div class="container padding-bottom bubble"><form method="post">
{% csrf_token %}
{{ form.as_p }}
<button class="small-button" type="submit">
{% if is_edit %}Update{% else %}Add{% endif %} Prospect
</button>
</form>
{% endblock %}

@ -1,14 +1,22 @@
{% extends "crm/base.html" %}
{% load static %}
{% block content %}
<div class="container bubble">
<h2>Prospects</h2>
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
{{ filter.form }}
<div class="col-12">
<div class="">
<div class="">
<form method="get" class="filter-form">
{% for field in filter.form %}
<div class="filter-group">
<label class="filter-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
</div>
{% endfor %}
<div class="filter-buttons">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="{% url 'crm:prospect-list' %}" class="btn btn-secondary">Clear</a>
</div>
@ -16,10 +24,12 @@
</div>
</div>
<div class="mb-3">
<!-- <div class="mb-3">
<a href="{% url 'crm:csv-import' %}" class="btn btn-success">Import CSV</a>
<a href="{% url 'crm:send-bulk-email' %}" class="btn btn-primary">Send Email</a>
</div>
</div> -->
<span>{{ filter.qs|length }} résultats</span>
<div class="table-responsive">
<table class="table table-striped">
@ -30,27 +40,33 @@
<th>Prénom</th>
<th>Nom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Ville</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for prospect in prospects %}
{% for prospect in filter.qs %}
<tr>
<td><input type="checkbox" name="selected_prospects" value="{{ prospect.id }}"></td>
<td>{{ prospect.entity_name }}</td>
<td>{{ prospect.first_name }}</td>
<td>{{ prospect.last_name }}</td>
<td><a href="mailto:{{ prospect.email }}">{{ prospect.email }}</a></td>
<td>{{ prospect.phone }}</td>
<td>{{ prospect.city }} ({{ prospect.zip_code }})</td>
<td>
{% for status in prospect.prospectstatus_set.all %}
<span class="badge bg-primary">{{ status.status.name }}</span>
{% endfor %}
</td>
<td>
<button class="btn btn-sm btn-secondary">Edit</button>
<a href="{% url 'crm:edit-prospect' prospect.id %}">
<button class="btn btn-sm btn-secondary">Edit</button>
</a>
<a href="{% url 'crm:add-event-for-prospect' prospect.id %}">
<button class="btn btn-sm btn-secondary">+ Event</button>
</a>
</td>
</tr>
{% endfor %}
@ -59,3 +75,7 @@
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'crm/js/prospects.js' %}"></script>
{% endblock %}

@ -4,12 +4,14 @@ from . import views
app_name = 'crm'
urlpatterns = [
path('', views.EventListView.as_view(), name='planned_events'),path('', views.EventListView.as_view(), name='planned-events'),
path('', views.EventListView.as_view(), name='planned_events'),path('', views.EventListView.as_view(), name='events'),
path('events/add/', views.EventCreateView.as_view(), name='add-event'),
path('events/add/<int:prospect_id>/', views.EventCreateView.as_view(), name='add-event-for-prospect'),
path('events/<int:pk>/edit/', views.EditEventView.as_view(), name='edit_event'),
path('events/<int:pk>/start/', views.StartEventView.as_view(), name='start_event'),
path('prospects/', views.ProspectListView.as_view(), name='prospect-list'),
path('add-prospect/', views.add_prospect, name='add-prospect'),
path('prospect/add/', views.prospect_form, name='add-prospect'),
path('prospect/<int:pk>/edit/', views.prospect_form, name='edit-prospect'),
path('prospects/import/', views.CSVImportView.as_view(), name='csv-import'),
path('email/send/', views.SendBulkEmailView.as_view(), name='send-bulk-email'),
]

@ -17,7 +17,7 @@ from django.db import IntegrityError
from .models import Event, Prospect, EmailTracker, EmailCampaign, EventType
from .filters import ProspectFilter
from .forms import CSVImportForm, BulkEmailForm, EventForm
from .forms import ProspectForm, CSVImportForm, BulkEmailForm, EventForm
from .mixins import CRMAccessMixin
@ -26,38 +26,67 @@ from io import TextIOWrapper
from datetime import datetime
@permission_required('crm.view_crm', raise_exception=True)
def add_prospect(request):
if request.method == 'POST':
entity_name = request.POST.get('entity_name')
first_name = request.POST.get('first_name')
last_name = request.POST.get('last_name')
email = request.POST.get('email')
phone = request.POST.get('phone')
address = request.POST.get('address')
zip_code = request.POST.get('zip_code')
city = request.POST.get('city')
# region = request.POST.get('region')
try:
prospect = Prospect.objects.create(
entity_name=entity_name,
first_name=first_name,
last_name=last_name,
email=email,
phone=phone,
address=address,
zip_code=zip_code,
city=city,
# region=region,
created_by=request.user,
modified_by=request.user
)
messages.success(request, f'Prospect {name} has been added successfully!')
return redirect('crm:events') # or wherever you want to redirect after success
except Exception as e:
messages.error(request, f'Error adding prospect: {str(e)}')
def prospect_form(request, pk=None):
# Get the prospect instance if pk is provided (edit mode)
prospect = get_object_or_404(Prospect, pk=pk) if pk else None
return render(request, 'crm/add_prospect.html')
if request.method == 'POST':
form = ProspectForm(request.POST, instance=prospect)
if form.is_valid():
prospect = form.save(commit=False)
if not pk: # New prospect
prospect.created_by = request.user
prospect.modified_by = request.user
prospect.save()
action = 'updated' if pk else 'added'
messages.success(request,
f'Prospect {prospect.entity_name} has been {action} successfully!')
return redirect('crm:events')
else:
form = ProspectForm(instance=prospect)
context = {
'form': form,
'is_edit': prospect is not None,
'first_title': prospect.entity_name if prospect else 'Add Prospect',
'second_title': prospect.full_name() if prospect else None
}
return render(request, 'crm/prospect_form.html', context)
# @permission_required('crm.view_crm', raise_exception=True)
# def add_prospect(request):
# if request.method == 'POST':
# entity_name = request.POST.get('entity_name')
# first_name = request.POST.get('first_name')
# last_name = request.POST.get('last_name')
# email = request.POST.get('email')
# phone = request.POST.get('phone')
# address = request.POST.get('address')
# zip_code = request.POST.get('zip_code')
# city = request.POST.get('city')
# # region = request.POST.get('region')
# try:
# prospect = Prospect.objects.create(
# entity_name=entity_name,
# first_name=first_name,
# last_name=last_name,
# email=email,
# phone=phone,
# address=address,
# zip_code=zip_code,
# city=city,
# # region=region,
# created_by=request.user,
# modified_by=request.user
# )
# messages.success(request, f'Prospect {name} has been added successfully!')
# return redirect('crm:events') # or wherever you want to redirect after success
# except Exception as e:
# messages.error(request, f'Error adding prospect: {str(e)}')
# return render(request, 'crm/add_prospect.html')
class EventCreateView(CRMAccessMixin, CreateView):
model = Event
@ -65,6 +94,13 @@ class EventCreateView(CRMAccessMixin, CreateView):
template_name = 'crm/event_form.html'
success_url = reverse_lazy('crm:planned_events')
def get_initial(self):
initial = super().get_initial()
prospect_id = self.kwargs.get('prospect_id')
if prospect_id:
initial['prospects'] = [prospect_id]
return initial
def form_valid(self, form):
form.instance.created_by = self.request.user
form.instance.modified_by = self.request.user
@ -131,6 +167,9 @@ class ProspectListView(CRMAccessMixin, ListView):
context_object_name = 'prospects'
filterset_class = ProspectFilter
def get_queryset(self):
return super().get_queryset().prefetch_related('prospectstatus_set__status')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter'] = self.filterset_class(
@ -139,7 +178,6 @@ class ProspectListView(CRMAccessMixin, ListView):
)
return context
class CSVImportView(CRMAccessMixin, FormView):
template_name = 'crm/csv_import.html'
form_class = CSVImportForm

@ -714,3 +714,44 @@ h-margin {
.right-content {
margin-left: auto;
}
/* CRM form */
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 20px;
}
.filter-group {
display: flex;
align-items: center;
}
.filter-label {
margin-right: 5px;
font-size: 0.9em;
}
.filter-buttons {
display: flex;
gap: 5px;
}
.btn {
padding: 5px 10px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}

Loading…
Cancel
Save