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 import django_filters
from django.db.models import Q
from .models import Event, Status, Prospect from .models import Event, Status, Prospect
class ProspectFilter(django_filters.FilterSet): 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( events = django_filters.ModelMultipleChoiceFilter(
queryset=Event.objects.all(), queryset=Event.objects.all(),
field_name='events', 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: class Meta:
model = Prospect model = Prospect
fields = ['city', 'events'] fields = ['name', 'city', 'events', 'zip_code']

@ -12,8 +12,11 @@ class SmallTextArea(forms.Textarea):
}) })
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class CSVImportForm(forms.Form): class ProspectForm(forms.ModelForm):
csv_file = forms.FileField() class Meta:
model = Prospect
fields = ['entity_name', 'first_name', 'last_name', 'email',
'phone', 'address', 'zip_code', 'city']
class BulkEmailForm(forms.Form): class BulkEmailForm(forms.Form):
prospects = forms.ModelMultipleChoiceField( prospects = forms.ModelMultipleChoiceField(
@ -38,3 +41,6 @@ class EventForm(forms.ModelForm):
widgets = { widgets = {
'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}), '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"), ("view_prospects", "Can view prospects"),
] ]
def full_name(self):
return f'{self.first_name} {self.last_name}'
def __str__(self): def __str__(self):
return ' - '.join(filter(None, [self.entity_name, self.first_name, self.last_name, f"({self.email})"])) 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> </script>
<!-- End Matomo Code --> <!-- End Matomo Code -->
{% block extra_js %}{% endblock %}
</head> </head>
<body class="wrapper"> <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" %} {% extends "crm/base.html" %}
{% load static %}
{% block content %} {% block content %}
<div class="container bubble"> <div class="container bubble">
<h2>Prospects</h2> <h2>Prospects</h2>
<div class="card mb-4"> <div class="">
<div class="card-body"> <div class="">
<form method="get" class="row g-3">
{{ filter.form }} <form method="get" class="filter-form">
<div class="col-12"> {% 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> <button type="submit" class="btn btn-primary">Filter</button>
<a href="{% url 'crm:prospect-list' %}" class="btn btn-secondary">Clear</a> <a href="{% url 'crm:prospect-list' %}" class="btn btn-secondary">Clear</a>
</div> </div>
@ -16,10 +24,12 @@
</div> </div>
</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:csv-import' %}" class="btn btn-success">Import CSV</a>
<a href="{% url 'crm:send-bulk-email' %}" class="btn btn-primary">Send Email</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"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -30,27 +40,33 @@
<th>Prénom</th> <th>Prénom</th>
<th>Nom</th> <th>Nom</th>
<th>Email</th> <th>Email</th>
<th>Téléphone</th> <th>Ville</th>
<th>Statut</th> <th>Statut</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for prospect in prospects %} {% for prospect in filter.qs %}
<tr> <tr>
<td><input type="checkbox" name="selected_prospects" value="{{ prospect.id }}"></td> <td><input type="checkbox" name="selected_prospects" value="{{ prospect.id }}"></td>
<td>{{ prospect.entity_name }}</td> <td>{{ prospect.entity_name }}</td>
<td>{{ prospect.first_name }}</td> <td>{{ prospect.first_name }}</td>
<td>{{ prospect.last_name }}</td> <td>{{ prospect.last_name }}</td>
<td><a href="mailto:{{ prospect.email }}">{{ prospect.email }}</a></td> <td><a href="mailto:{{ prospect.email }}">{{ prospect.email }}</a></td>
<td>{{ prospect.phone }}</td> <td>{{ prospect.city }} ({{ prospect.zip_code }})</td>
<td> <td>
{% for status in prospect.prospectstatus_set.all %} {% for status in prospect.prospectstatus_set.all %}
<span class="badge bg-primary">{{ status.status.name }}</span> <span class="badge bg-primary">{{ status.status.name }}</span>
{% endfor %} {% endfor %}
</td> </td>
<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> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -59,3 +75,7 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script src="{% static 'crm/js/prospects.js' %}"></script>
{% endblock %}

@ -4,12 +4,14 @@ from . import views
app_name = 'crm' app_name = 'crm'
urlpatterns = [ 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/', 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>/edit/', views.EditEventView.as_view(), name='edit_event'),
path('events/<int:pk>/start/', views.StartEventView.as_view(), name='start_event'), path('events/<int:pk>/start/', views.StartEventView.as_view(), name='start_event'),
path('prospects/', views.ProspectListView.as_view(), name='prospect-list'), 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('prospects/import/', views.CSVImportView.as_view(), name='csv-import'),
path('email/send/', views.SendBulkEmailView.as_view(), name='send-bulk-email'), 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 .models import Event, Prospect, EmailTracker, EmailCampaign, EventType
from .filters import ProspectFilter from .filters import ProspectFilter
from .forms import CSVImportForm, BulkEmailForm, EventForm from .forms import ProspectForm, CSVImportForm, BulkEmailForm, EventForm
from .mixins import CRMAccessMixin from .mixins import CRMAccessMixin
@ -26,38 +26,67 @@ from io import TextIOWrapper
from datetime import datetime from datetime import datetime
@permission_required('crm.view_crm', raise_exception=True) @permission_required('crm.view_crm', raise_exception=True)
def add_prospect(request): def prospect_form(request, pk=None):
if request.method == 'POST': # Get the prospect instance if pk is provided (edit mode)
entity_name = request.POST.get('entity_name') prospect = get_object_or_404(Prospect, pk=pk) if pk else None
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') 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): class EventCreateView(CRMAccessMixin, CreateView):
model = Event model = Event
@ -65,6 +94,13 @@ class EventCreateView(CRMAccessMixin, CreateView):
template_name = 'crm/event_form.html' template_name = 'crm/event_form.html'
success_url = reverse_lazy('crm:planned_events') 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): def form_valid(self, form):
form.instance.created_by = self.request.user form.instance.created_by = self.request.user
form.instance.modified_by = self.request.user form.instance.modified_by = self.request.user
@ -131,6 +167,9 @@ class ProspectListView(CRMAccessMixin, ListView):
context_object_name = 'prospects' context_object_name = 'prospects'
filterset_class = ProspectFilter filterset_class = ProspectFilter
def get_queryset(self):
return super().get_queryset().prefetch_related('prospectstatus_set__status')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['filter'] = self.filterset_class( context['filter'] = self.filterset_class(
@ -139,7 +178,6 @@ class ProspectListView(CRMAccessMixin, ListView):
) )
return context return context
class CSVImportView(CRMAccessMixin, FormView): class CSVImportView(CRMAccessMixin, FormView):
template_name = 'crm/csv_import.html' template_name = 'crm/csv_import.html'
form_class = CSVImportForm form_class = CSVImportForm

@ -714,3 +714,44 @@ h-margin {
.right-content { .right-content {
margin-left: auto; 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