feat: add intervention on pause widget and associated backend logic

This commit is contained in:
Karl Determe 2026-08-14 22:26:41 +02:00
parent 441af33ddd
commit b1e7f726a2
12 changed files with 211 additions and 1 deletions

View file

@ -69,6 +69,10 @@ msgid "Compteur interventions terminées"
msgstr "Incidents to be processed counter" msgstr "Incidents to be processed counter"
#: streetup/home/models.py:20 #: streetup/home/models.py:20
msgid "Compteur interventions en pause"
msgstr "Paused interventions counter"
#: streetup/home/models.py:21
#: streetup/home/templates/home/widgets/stock_actions.html:9 #: streetup/home/templates/home/widgets/stock_actions.html:9
msgid "Actions stock" msgid "Actions stock"
msgstr "Stock actions" msgstr "Stock actions"
@ -543,6 +547,18 @@ msgstr "No incidents to process"
msgid "Voir toutes les interventions terminées" msgid "Voir toutes les interventions terminées"
msgstr "See all my observations" msgstr "See all my observations"
#: streetup/home/templates/home/widgets/intervention_on_pause.html:8
msgid "Interventions en pause"
msgstr "Paused interventions"
#: streetup/home/templates/home/widgets/intervention_on_pause.html:40
msgid "Aucune intervention en pause"
msgstr "No paused interventions"
#: streetup/home/templates/home/widgets/intervention_on_pause.html:48
msgid "Voir toutes les interventions en pause"
msgstr "View all paused interventions"
#: streetup/home/templates/home/widgets/intervention_stats.html:13 #: streetup/home/templates/home/widgets/intervention_stats.html:13
msgid "Interventions" msgid "Interventions"
msgstr "Interventions" msgstr "Interventions"

View file

@ -69,6 +69,10 @@ msgid "Compteur interventions terminées"
msgstr "Compteur interventions terminées" msgstr "Compteur interventions terminées"
#: streetup/home/models.py:20 #: streetup/home/models.py:20
msgid "Compteur interventions en pause"
msgstr "Compteur interventions en pause"
#: streetup/home/models.py:21
#: streetup/home/templates/home/widgets/stock_actions.html:9 #: streetup/home/templates/home/widgets/stock_actions.html:9
msgid "Actions stock" msgid "Actions stock"
msgstr "Actions stock" msgstr "Actions stock"
@ -550,6 +554,18 @@ msgstr "Aucune intervention terminée"
msgid "Voir toutes les interventions terminées" msgid "Voir toutes les interventions terminées"
msgstr "Voir toutes les interventions terminées" msgstr "Voir toutes les interventions terminées"
#: streetup/home/templates/home/widgets/intervention_on_pause.html:8
msgid "Interventions en pause"
msgstr "Interventions en pause"
#: streetup/home/templates/home/widgets/intervention_on_pause.html:40
msgid "Aucune intervention en pause"
msgstr "Aucune intervention en pause"
#: streetup/home/templates/home/widgets/intervention_on_pause.html:48
msgid "Voir toutes les interventions en pause"
msgstr "Voir toutes les interventions en pause"
#: streetup/home/templates/home/widgets/intervention_stats.html:13 #: streetup/home/templates/home/widgets/intervention_stats.html:13
msgid "Interventions" msgid "Interventions"
msgstr "Interventions" msgstr "Interventions"

View file

@ -68,7 +68,11 @@ msgstr "Tegengeverifieerde interventies moeten worden goedgekeurd"
msgid "Compteur interventions terminées" msgid "Compteur interventions terminées"
msgstr "Aantal te behandelen incidenten" msgstr "Aantal te behandelen incidenten"
#: home/models.py:20 home/templates/home/widgets/stock_actions.html:9 #: home/models.py:20
msgid "Compteur interventions en pause"
msgstr "Teller gepauzeerde interventies"
#: home/models.py:21 home/templates/home/widgets/stock_actions.html:9
msgid "Actions stock" msgid "Actions stock"
msgstr "Voorraadacties" msgstr "Voorraadacties"
@ -544,6 +548,18 @@ msgstr "Geen incidenten te behandelen"
msgid "Voir toutes les interventions terminées" msgid "Voir toutes les interventions terminées"
msgstr "Bekijk al mijn observaties" msgstr "Bekijk al mijn observaties"
#: home/templates/home/widgets/intervention_on_pause.html:8
msgid "Interventions en pause"
msgstr "Gepauzeerde interventies"
#: home/templates/home/widgets/intervention_on_pause.html:40
msgid "Aucune intervention en pause"
msgstr "Geen gepauzeerde interventies"
#: home/templates/home/widgets/intervention_on_pause.html:48
msgid "Voir toutes les interventions en pause"
msgstr "Bekijk alle gepauzeerde interventies"
#: home/templates/home/widgets/intervention_stats.html:13 #: home/templates/home/widgets/intervention_stats.html:13
msgid "Interventions" msgid "Interventions"
msgstr "Interventies" msgstr "Interventies"

View file

@ -0,0 +1,18 @@
# Generated by Django 6.0.7 on 2026-08-14 20:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('home', '0002_delete_teambuildingvote'),
]
operations = [
migrations.AlterField(
model_name='homewidget',
name='widget_code',
field=models.CharField(choices=[('intervention_stats', 'Statistiques interventions'), ('contracts_summary', 'Résumé contrats'), ('map_today', 'Carte du jour'), ('operator_shortcuts', 'Raccourcis opérateur'), ('assets_links', 'Liens assets'), ('stock_summary', 'Résumé stock'), ('observer_shortcuts', 'Raccourcis observateur'), ('incident_counter', 'Compteur incidents à traiter'), ('intervention_to_verify', 'Compteur interventions à vérifier'), ('intervention_to_plan', 'Compteur interventions à planifier'), ('intervention_to_approve', 'Compteur interventions à approuver'), ('intervention_checked_to_approve', 'Compteur interventions vérifiées à approuver'), ('intervention_finished', 'Compteur interventions terminées'), ('intervention_on_pause', 'Compteur interventions en pause'), ('stock_actions', 'Actions stock')], max_length=50, unique=True, verbose_name='Code du widget'),
),
]

View file

@ -17,6 +17,7 @@ WIDGET_CODE_CHOICES = [
('intervention_to_approve', _('Compteur interventions à approuver')), ('intervention_to_approve', _('Compteur interventions à approuver')),
('intervention_checked_to_approve', _('Compteur interventions vérifiées à approuver')), ('intervention_checked_to_approve', _('Compteur interventions vérifiées à approuver')),
('intervention_finished', _('Compteur interventions terminées')), ('intervention_finished', _('Compteur interventions terminées')),
('intervention_on_pause', _('Compteur interventions en pause')),
('stock_actions', _('Actions stock')), ('stock_actions', _('Actions stock')),
] ]

View file

@ -0,0 +1,51 @@
{% load i18n %}
{# ========== Widget compteur d'interventions en pause ========== #}
<div class="card shadow-sm h-100" style="border-color: #90a4ae !important;">
<div class="card-header d-flex align-items-center justify-content-between" style="background-color: #90a4ae !important;">
<a href="{% url 'interventions:interventions_index' %}?{% if thematic_filter_param %}{{ thematic_filter_param }}&{% endif %}status=on_pause"
class="text-decoration-none fw-bold text-uppercase" style="color: #000000 !important; letter-spacing:.05em;">
<i class="bi bi-pause-circle-fill me-2"></i>{% translate "Interventions en pause" %}
</a>
</div>
{# --- Compteur --- #}
<div class="text-center py-2 border-bottom">
<a href="{% url 'interventions:interventions_index' %}?{% if thematic_filter_param %}{{ thematic_filter_param }}&{% endif %}status=on_pause{% if is_manager_filter_applied %}&intervention_manager={{ request.user.id }}{% endif %}"
class="text-decoration-none">
<span class="display-5 fw-bold" style="color: #90a4ae !important;">{{ intervention_on_pause_count }}</span>
{% if is_manager_filter_applied %}
<span class="text-muted fs-6 fw-normal ms-1">{% translate "(gestionnaire)" %}</span>
{% endif %}
</a>
</div>
{# --- Liste des 5 premières interventions --- #}
<ul class="list-group list-group-flush">
{% for itv in intervention_on_pause_list %}
<li class="list-group-item px-2 py-1 small">
<a href="{% url 'interventions:intervention_detail' itv.id %}" class="text-decoration-none d-block">
<div class="d-flex justify-content-between align-items-start">
<span class="fw-semibold text-truncate me-2" style="max-width:70%; color: #90a4ae !important;">{{ itv.code }}</span>
<span class="text-muted text-nowrap" style="font-size:.75rem;">{{ itv.creation_time|date:"d/m H:i" }}</span>
</div>
<span class="text-muted" style="font-size:.75rem;">
{% if itv.title %}{{ itv.title|truncatechars:50 }}{% endif %}
{% if itv.thematic %} &middot; {{ itv.thematic.code }}{% endif %}
</span>
</a>
</li>
{% empty %}
<li class="list-group-item text-center text-muted py-2 small">
<i class="bi bi-check-circle text-success me-1"></i>{% translate "Aucune intervention en pause" %}
</li>
{% endfor %}
</ul>
<div class="card-footer text-center text-muted small">
<a href="{% url 'interventions:interventions_index' %}?{% if thematic_filter_param %}{{ thematic_filter_param }}&{% endif %}status=on_pause"
class="text-muted text-decoration-none">
<i class="bi bi-arrow-right-circle me-1"></i>{% translate "Voir toutes les interventions en pause" %} ({{ total_on_pause_count }})
</a>
</div>
</div>

View file

@ -231,4 +231,24 @@ class HomeIndexViewTests(TestCase):
self.assertContains(response, 'id="home-unread-notifications-alert"') self.assertContains(response, 'id="home-unread-notifications-alert"')
self.assertContains(response, 'Vous avez <strong class="home-unread-notifications-count">1</strong> notification non lue.') self.assertContains(response, 'Vous avez <strong class="home-unread-notifications-count">1</strong> notification non lue.')
def test_home_index_intervention_on_pause_widget(self):
from django.urls import reverse
from django.contrib.auth import get_user_model
from common.models import UserConfig
from home.models import HomeWidget
User = get_user_model()
user = User.objects.create_user('pause_widget_user', password='pass')
UserConfig.objects.create(user=user)
self.client.force_login(user)
# Create the widget
HomeWidget.objects.create(widget_code='intervention_on_pause')
response = self.client.get(reverse('home:index'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Interventions en pause')
self.assertContains(response, 'status=on_pause')

View file

@ -631,6 +631,17 @@ def index(request):
.order_by('-code')[:5] .order_by('-code')[:5]
) )
intervention_on_pause_qs = interventions.filter(status='on_pause').exclude(
Q(contract__uses_intervention_manager=True) &
~Q(intervention_manager=request.user)
)
intervention_on_pause_count = intervention_on_pause_qs.count()
intervention_on_pause_list = list(
intervention_on_pause_qs
.select_related('thematic')
.order_by('-code')[:5]
)
if home_contract_ids is not None: if home_contract_ids is not None:
is_manager_filter_applied = user_contracts.filter( is_manager_filter_applied = user_contracts.filter(
id__in=home_contract_ids, uses_intervention_manager=True id__in=home_contract_ids, uses_intervention_manager=True
@ -641,6 +652,7 @@ def index(request):
).exists() ).exists()
total_finished_count = interventions.filter(status='finished').count() total_finished_count = interventions.filter(status='finished').count()
total_on_pause_count = interventions.filter(status='on_pause').count()
is_inspector = 'inspector' in user_roles is_inspector = 'inspector' in user_roles
inspection_configs = [] inspection_configs = []
@ -710,8 +722,11 @@ def index(request):
'intervention_to_plan_list': intervention_to_plan_list, 'intervention_to_plan_list': intervention_to_plan_list,
'intervention_finished_count': intervention_finished_count, 'intervention_finished_count': intervention_finished_count,
'intervention_finished_list': intervention_finished_list, 'intervention_finished_list': intervention_finished_list,
'intervention_on_pause_count': intervention_on_pause_count,
'intervention_on_pause_list': intervention_on_pause_list,
'is_manager_filter_applied': is_manager_filter_applied, 'is_manager_filter_applied': is_manager_filter_applied,
'total_finished_count': total_finished_count, 'total_finished_count': total_finished_count,
'total_on_pause_count': total_on_pause_count,
'intervention_to_approve_count': intervention_to_approve_count, 'intervention_to_approve_count': intervention_to_approve_count,
'intervention_to_approve_list': intervention_to_approve_list, 'intervention_to_approve_list': intervention_to_approve_list,
'intervention_checked_to_approve_count': intervention_checked_to_approve_count, 'intervention_checked_to_approve_count': intervention_checked_to_approve_count,

View file

@ -628,6 +628,63 @@ class InterventionManagerTests(TestCase):
response2 = self.client.get(reverse('home:index')) response2 = self.client.get(reverse('home:index'))
self.assertFalse(response2.context['is_manager_filter_applied']) self.assertFalse(response2.context['is_manager_filter_applied'])
def test_homepage_on_pause_interventions_filtering(self):
# Create an intervention on_pause, with contract using manager, but manager is None (not this user)
itv_other_mgr = Intervention.objects.create(
title='Other Manager on pause',
status='on_pause',
contract=self.contract,
thematic=self.thematic,
intervention_manager=None,
symptom=self.symptom,
)
# Create an intervention on_pause, with contract using manager, managed by self.user
itv_self_mgr = Intervention.objects.create(
title='Self Manager on pause',
status='on_pause',
contract=self.contract,
thematic=self.thematic,
intervention_manager=self.user,
symptom=self.symptom,
)
# Create an intervention on_pause, but contract does not use manager
contract_no_mgr = Contract.objects.create(
company=self.company,
contract_number='C-NOMGR-PAUSE',
start_date=self.contract.start_date,
end_date=self.contract.end_date,
uses_intervention_manager=False
)
# Grant view permission for the no-manager contract
from common.models import UserContractAccess
UserContractAccess.objects.create(
user_config=self.user_config,
contract=contract_no_mgr,
can_view_interventions=True,
)
itv_no_mgr = Intervention.objects.create(
title='No Manager on pause',
status='on_pause',
contract=contract_no_mgr,
thematic=self.thematic,
symptom=self.symptom,
)
# Access the homepage
response = self.client.get(reverse('home:index'))
self.assertEqual(response.status_code, 200)
# On pause list/counter should only contain self-managed and no-manager interventions
on_pause_list = response.context['intervention_on_pause_list']
self.assertIn(itv_self_mgr, on_pause_list)
self.assertIn(itv_no_mgr, on_pause_list)
self.assertNotIn(itv_other_mgr, on_pause_list)
self.assertEqual(response.context['intervention_on_pause_count'], 2)
self.assertEqual(response.context['total_on_pause_count'], 3)
self.assertTrue(response.context['is_manager_filter_applied'])
def test_geojson_filtering_by_manager(self): def test_geojson_filtering_by_manager(self):
# Create an intervention finished managed by self.user # Create an intervention finished managed by self.user
itv_self_mgr = Intervention.objects.create( itv_self_mgr = Intervention.objects.create(