diff --git a/loko/contracts/templates/contracts/contracts_detail.html b/loko/contracts/templates/contracts/contracts_detail.html index 2fce608..eb8e363 100644 --- a/loko/contracts/templates/contracts/contracts_detail.html +++ b/loko/contracts/templates/contracts/contracts_detail.html @@ -142,5 +142,25 @@ +{% endblock %} +{% block script %} + {% endblock %} \ No newline at end of file diff --git a/loko/home/locale/en/LC_MESSAGES/django.mo b/loko/home/locale/en/LC_MESSAGES/django.mo index 474eb1a..ee5d5f7 100644 Binary files a/loko/home/locale/en/LC_MESSAGES/django.mo and b/loko/home/locale/en/LC_MESSAGES/django.mo differ diff --git a/loko/home/locale/en/LC_MESSAGES/django.po b/loko/home/locale/en/LC_MESSAGES/django.po index 29d031e..b7cf2eb 100644 --- a/loko/home/locale/en/LC_MESSAGES/django.po +++ b/loko/home/locale/en/LC_MESSAGES/django.po @@ -1146,3 +1146,48 @@ msgstr "to process · in progress" msgid "Aucun bon à traiter ou en cours" msgstr "No orders to process or in progress" +msgid "Activité des trois derniers mois" +msgstr "Activity of the last three months" + +msgid "Rechercher une intervention" +msgstr "Search for an intervention" + +msgid "Consulter les contrats" +msgstr "View contracts" + +msgid "Calendrier" +msgstr "Calendar" + +msgid "Mon historique" +msgstr "My history" + +msgid "Vos consultations récentes d'interventions et de contrats" +msgstr "Your recent consultations of interventions and contracts" + +msgid "Aucun élément consulté récemment" +msgstr "No recently viewed items" + +msgid "Les interventions et contrats que vous consultez s'afficheront ici automatiquement pour vous permettre d'y revenir rapidement." +msgstr "Interventions and contracts you consult will automatically appear here for quick access." + +msgid "Historique basé sur vos consultations dans l'application" +msgstr "History based on your consultations in the application" + +msgid "Vos dernières consultations" +msgstr "Your latest consultations" + +msgid "consultation(s)" +msgstr "consultation(s)" + +msgid "Ouvrir en grand" +msgstr "Open fullscreen" + +msgid "Vos consultations d'interventions et de contrats apparaîtront ici pour un accès rapide." +msgstr "Your consultations of interventions and contracts will appear here for quick access." + +msgid "Toutes les interventions" +msgstr "All interventions" + +msgid "Vue détaillée" +msgstr "Detailed view" + diff --git a/loko/home/locale/fr/LC_MESSAGES/django.mo b/loko/home/locale/fr/LC_MESSAGES/django.mo index 59ccbda..713cfda 100644 Binary files a/loko/home/locale/fr/LC_MESSAGES/django.mo and b/loko/home/locale/fr/LC_MESSAGES/django.mo differ diff --git a/loko/home/locale/fr/LC_MESSAGES/django.po b/loko/home/locale/fr/LC_MESSAGES/django.po index f40a182..f7ccc1c 100644 --- a/loko/home/locale/fr/LC_MESSAGES/django.po +++ b/loko/home/locale/fr/LC_MESSAGES/django.po @@ -1155,3 +1155,48 @@ msgstr "à traiter · en cours" msgid "Aucun bon à traiter ou en cours" msgstr "Aucun bon à traiter ou en cours" +msgid "Activité des trois derniers mois" +msgstr "Activité des trois derniers mois" + +msgid "Rechercher une intervention" +msgstr "Rechercher une intervention" + +msgid "Consulter les contrats" +msgstr "Consulter les contrats" + +msgid "Calendrier" +msgstr "Calendrier" + +msgid "Mon historique" +msgstr "Mon historique" + +msgid "Vos consultations récentes d'interventions et de contrats" +msgstr "Vos consultations récentes d'interventions et de contrats" + +msgid "Aucun élément consulté récemment" +msgstr "Aucun élément consulté récemment" + +msgid "Les interventions et contrats que vous consultez s'afficheront ici automatiquement pour vous permettre d'y revenir rapidement." +msgstr "Les interventions et contrats que vous consultez s'afficheront ici automatiquement pour vous permettre d'y revenir rapidement." + +msgid "Historique basé sur vos consultations dans l'application" +msgstr "Historique basé sur vos consultations dans l'application" + +msgid "Vos dernières consultations" +msgstr "Vos dernières consultations" + +msgid "consultation(s)" +msgstr "consultation(s)" + +msgid "Ouvrir en grand" +msgstr "Ouvrir en grand" + +msgid "Vos consultations d'interventions et de contrats apparaîtront ici pour un accès rapide." +msgstr "Vos consultations d'interventions et de contrats apparaîtront ici pour un accès rapide." + +msgid "Toutes les interventions" +msgstr "Toutes les interventions" + +msgid "Vue détaillée" +msgstr "Vue détaillée" + diff --git a/loko/home/locale/nl/LC_MESSAGES/django.mo b/loko/home/locale/nl/LC_MESSAGES/django.mo index 1fe79dd..d46f0e0 100644 Binary files a/loko/home/locale/nl/LC_MESSAGES/django.mo and b/loko/home/locale/nl/LC_MESSAGES/django.mo differ diff --git a/loko/home/locale/nl/LC_MESSAGES/django.po b/loko/home/locale/nl/LC_MESSAGES/django.po index 7f23eb3..2c383bc 100644 --- a/loko/home/locale/nl/LC_MESSAGES/django.po +++ b/loko/home/locale/nl/LC_MESSAGES/django.po @@ -1152,3 +1152,48 @@ msgstr "te verwerken · in behandeling" msgid "Aucun bon à traiter ou en cours" msgstr "Geen bonnen te verwerken of in behandeling" +msgid "Activité des trois derniers mois" +msgstr "Activiteit van de afgelopen drie maanden" + +msgid "Rechercher une intervention" +msgstr "Een interventie zoeken" + +msgid "Consulter les contrats" +msgstr "Contracten raadplegen" + +msgid "Calendrier" +msgstr "Kalender" + +msgid "Mon historique" +msgstr "Mijn geschiedenis" + +msgid "Vos consultations récentes d'interventions et de contrats" +msgstr "Uw recente raadplegingen van interventies en contracten" + +msgid "Aucun élément consulté récemment" +msgstr "Geen recent geraadpleegde items" + +msgid "Les interventions et contrats que vous consultez s'afficheront ici automatiquement pour vous permettre d'y revenir rapidement." +msgstr "De interventies en contracten die u raadpleegt, worden hier automatisch weergegeven zodat u er snel naar kunt terugkeren." + +msgid "Historique basé sur vos consultations dans l'application" +msgstr "Geschiedenis gebaseerd op uw raadplegingen in de applicatie" + +msgid "Vos dernières consultations" +msgstr "Uw laatste raadplegingen" + +msgid "consultation(s)" +msgstr "raadpleging(en)" + +msgid "Ouvrir en grand" +msgstr "Vergroten" + +msgid "Vos consultations d'interventions et de contrats apparaîtront ici pour un accès rapide." +msgstr "Uw raadplegingen van interventies en contracten verschijnen hier voor snelle toegang." + +msgid "Toutes les interventions" +msgstr "Alle interventies" + +msgid "Vue détaillée" +msgstr "Gedetailleerde weergave" + diff --git a/loko/home/migrations/0010_alter_homewidget_widget_code.py b/loko/home/migrations/0010_alter_homewidget_widget_code.py new file mode 100644 index 0000000..869f0b4 --- /dev/null +++ b/loko/home/migrations/0010_alter_homewidget_widget_code.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.7 on 2026-10-07 13:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0009_alter_homewidget_widget_code'), + ] + + operations = [ + migrations.AlterField( + model_name='homewidget', + name='widget_code', + field=models.CharField(choices=[('territory_kpis', 'Métriques du patrimoine'), ('recent_activity', "Radar d'activité"), ('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'), ('external_interventions', 'Interventions externes'), ('stock_actions', 'Actions stock'), ('exceptional_transport', 'Transport exceptionnel'), ('controller_shortcuts', 'Raccourcis contrôleur'), ('user_history', 'Mon historique')], max_length=50, unique=True, verbose_name='Code du widget'), + ), + ] diff --git a/loko/home/migrations/0011_create_user_history_widget.py b/loko/home/migrations/0011_create_user_history_widget.py new file mode 100644 index 0000000..f1ef068 --- /dev/null +++ b/loko/home/migrations/0011_create_user_history_widget.py @@ -0,0 +1,49 @@ +from django.db import migrations + + +def create_user_history_widget(apps, schema_editor): + HomeWidget = apps.get_model('home', 'HomeWidget') + HomeWidgetConditionGroup = apps.get_model('home', 'HomeWidgetConditionGroup') + HomeWidgetCondition = apps.get_model('home', 'HomeWidgetCondition') + Role = apps.get_model('common', 'Role') + + widget, created = HomeWidget.objects.get_or_create( + widget_code='user_history', + defaults={'is_active': True} + ) + + if not widget.condition_groups.exists(): + viewer_role, _ = Role.objects.get_or_create(name='viewer') + if viewer_role: + group = HomeWidgetConditionGroup.objects.create( + widget=widget, + label="Lecteurs (Viewers)", + order=20, + col_width=12, + priority=0, + is_active=True + ) + HomeWidgetCondition.objects.create( + group=group, + condition_type='role', + role=viewer_role, + is_active=True, + is_negated=False + ) + + +def remove_user_history_widget(apps, schema_editor): + HomeWidget = apps.get_model('home', 'HomeWidget') + HomeWidget.objects.filter(widget_code='user_history').delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('home', '0010_alter_homewidget_widget_code'), + ('common', '0001_initial'), + ] + + operations = [ + migrations.RunPython(create_user_history_widget, remove_user_history_widget), + ] diff --git a/loko/home/models.py b/loko/home/models.py index cf1e926..57dadb7 100644 --- a/loko/home/models.py +++ b/loko/home/models.py @@ -24,6 +24,7 @@ WIDGET_CODE_CHOICES = [ ('stock_actions', _('Actions stock')), ('exceptional_transport', _('Transport exceptionnel')), ('controller_shortcuts', _('Raccourcis contrôleur')), + ('user_history', _('Mon historique')), ] # Condition type choices diff --git a/loko/home/static/home/home_index.js b/loko/home/static/home/home_index.js index 6436641..a28f368 100644 --- a/loko/home/static/home/home_index.js +++ b/loko/home/static/home/home_index.js @@ -48,7 +48,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') { map.addControl(new maplibregl.NavigationControl(), 'top-left'); - let currentMode = 'heatmap'; // 'heatmap' or 'points' + let currentMode = 'points'; // 'heatmap' or 'points' let activityGeojsonData = null; const THEMATIC_COLORS = { @@ -279,6 +279,9 @@ if (mapContainer && typeof maplibregl !== 'undefined') { type: 'heatmap', source: 'activity-data', maxzoom: 17, + layout: { + 'visibility': 'none' + }, paint: { 'heatmap-weight': 1, 'heatmap-intensity': [ @@ -350,12 +353,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') { 'icon-anchor': 'center' }, paint: { - // En mode Heatmap : les symboles s'estompent au dézoom pour laisser le champ libre à la heatmap - 'icon-opacity': [ - 'interpolate', ['linear'], ['zoom'], - 13.8, 0, - 15.2, 1 - ] + 'icon-opacity': 1 } }); diff --git a/loko/home/templates/home/home_index.html b/loko/home/templates/home/home_index.html index 24f38aa..02e5f27 100644 --- a/loko/home/templates/home/home_index.html +++ b/loko/home/templates/home/home_index.html @@ -49,6 +49,20 @@
+ {% if user.config|has_any_role:"viewer" and not user.config|has_any_role:"manager,admin,controller,operator,inspector" %} + + + {% translate "Rechercher une intervention" %} + + + + {% translate "Calendrier" %} + + + + {% translate "Mon historique" %} + + {% endif %} {% if user.config|has_any_role:"observer,manager,controller,admin,external_manager,top_manager" %} @@ -149,6 +163,7 @@ {% include "notifications/_new_discussion_modal.html" %} + {% include "home/includes/_user_history_modal.html" %} {% endblock content%} diff --git a/loko/home/templates/home/includes/_user_history_modal.html b/loko/home/templates/home/includes/_user_history_modal.html new file mode 100644 index 0000000..5dba300 --- /dev/null +++ b/loko/home/templates/home/includes/_user_history_modal.html @@ -0,0 +1,155 @@ +{% load i18n %} +{% load static %} + + + + + diff --git a/loko/home/templates/home/widgets/map_today.html b/loko/home/templates/home/widgets/map_today.html index c9c6ea9..8977975 100644 --- a/loko/home/templates/home/widgets/map_today.html +++ b/loko/home/templates/home/widgets/map_today.html @@ -8,16 +8,15 @@
-
{% translate "Activité du Territoire" %}
- {% translate "Mise en lumière & intensité des 90 derniers jours" %} +
{% translate "Activité des trois derniers mois" %}
- -
diff --git a/loko/home/templates/home/widgets/user_history.html b/loko/home/templates/home/widgets/user_history.html new file mode 100644 index 0000000..9251646 --- /dev/null +++ b/loko/home/templates/home/widgets/user_history.html @@ -0,0 +1,161 @@ +{% load i18n %} +{% load static %} + +
+
+
+
+ +
+
+
{% translate "Mon historique" %}
+ {% translate "Vos dernières consultations" %} +
+
+
+ + {{ user_recent_history|length|default:"0" }} {% translate "consultation(s)" %} + + +
+
+ + + + +
+ + diff --git a/loko/home/views.py b/loko/home/views.py index 5d826c6..219df1f 100644 --- a/loko/home/views.py +++ b/loko/home/views.py @@ -10,8 +10,11 @@ from datetime import timedelta from collections import defaultdict import json import logging +import re import time +logger = logging.getLogger(__name__) + # logger = logging.getLogger('home.perf') @@ -84,6 +87,19 @@ def index(request): if result: resolved_widgets.append(result) + # Fallback pour le rôle viewer : s'assurer que le widget d'historique s'affiche toujours + if not any(r['widget'].widget_code == 'user_history' for r in resolved_widgets): + user_roles_set = set(user_config.roles.values_list('name', flat=True)) + if 'viewer' in user_roles_set and not (user_roles_set & {'admin', 'manager', 'controller', 'operator', 'inspector'}): + user_history_widget = HomeWidget.objects.filter(widget_code='user_history').first() or HomeWidget(widget_code='user_history') + resolved_widgets.append({ + 'widget': user_history_widget, + 'order': 20, + 'template_name': 'home/widgets/user_history.html', + 'col_width': 12, + 'col_order': 0, + }) + resolved_widgets.sort(key=lambda r: (r['order'], r['col_order'])) # Group widgets by order into rows; within each row sort by col_order. @@ -786,10 +802,12 @@ def index(request): territory_kpis = _get_territory_kpis(request.user, user_contracts) recent_activity = _get_recent_activity(request.user, user_contracts) + user_recent_history = _get_user_recent_history(request.user) context = { 'territory_kpis': territory_kpis, 'recent_activity': recent_activity, + 'user_recent_history': user_recent_history, 'is_inspector': is_inspector, 'inspection_configs': inspection_configs, 'series_data': json.dumps(series_data), @@ -1230,6 +1248,118 @@ def _get_recent_activity(user, allowed_contracts): return recent_items +def _get_user_recent_history(user, limit=10): + """ + Rassemble l'historique des dernières consultations (interventions et contrats) + de l'utilisateur, vérifié selon ses droits d'accès. + """ + try: + from analytics.models import PageView + views = ( + PageView.objects.filter( + user=user, + method='GET', + path__regex=r"^/(interventions|contracts)/[0-9]+/?$" + ) + .order_by('-timestamp')[:40] + ) + + seen_paths = set() + items = [] + for v in views: + clean_path = v.path.rstrip('/') + '/' + if clean_path in seen_paths: + continue + seen_paths.add(clean_path) + + m = re.match(r"^/(interventions|contracts)/([0-9]+)/$", clean_path) + if not m: + continue + items.append({ + 'type': m.group(1), + 'id': int(m.group(2)), + 'timestamp': v.timestamp, + 'path': clean_path, + }) + if len(items) >= limit: + break + + interv_ids = [it['id'] for it in items if it['type'] == 'interventions'] + contract_ids = [it['id'] for it in items if it['type'] == 'contracts'] + + interv_map = {} + if interv_ids: + from interventions.permissions import filter_viewable_interventions_for_user + interv_qs = filter_viewable_interventions_for_user(user).filter( + id__in=interv_ids + ).select_related('thematic') + interv_map = {i.id: i for i in interv_qs} + + contract_map = {} + if contract_ids: + from contracts.permissions import get_allowed_contracts_for_user + allowed_contracts = get_allowed_contracts_for_user(user) + contract_qs = Contract.objects.filter( + id__in=contract_ids + ).filter(id__in=allowed_contracts).select_related('company') + contract_map = {c.id: c for c in contract_qs} + + now = timezone.now() + result = [] + for it in items: + if it['type'] == 'interventions' and it['id'] in interv_map: + interv = interv_map[it['id']] + diff = now - it['timestamp'] + if diff.days == 0: + hours = int(diff.total_seconds() // 3600) + relative_time = "Il y a 1h" if hours <= 1 else f"Il y a {hours}h" if hours > 0 else "À l'instant" + elif diff.days == 1: + relative_time = "Hier" + elif diff.days < 7: + relative_time = f"Il y a {diff.days}j" + else: + relative_time = it['timestamp'].strftime("%d/%m/%Y") + + result.append({ + 'type': 'intervention', + 'id': interv.id, + 'title': interv.title or f"Intervention #{interv.code or interv.id}", + 'subtitle': interv.thematic.name if interv.thematic else '', + 'status': interv.get_status_display(), + 'status_code': interv.status, + 'url': reverse('interventions:intervention_detail', args=[interv.id]), + 'timestamp': it['timestamp'], + 'relative_time': relative_time, + }) + elif it['type'] == 'contracts' and it['id'] in contract_map: + contract = contract_map[it['id']] + diff = now - it['timestamp'] + if diff.days == 0: + hours = int(diff.total_seconds() // 3600) + relative_time = "Il y a 1h" if hours <= 1 else f"Il y a {hours}h" if hours > 0 else "À l'instant" + elif diff.days == 1: + relative_time = "Hier" + elif diff.days < 7: + relative_time = f"Il y a {diff.days}j" + else: + relative_time = it['timestamp'].strftime("%d/%m/%Y") + + result.append({ + 'type': 'contract', + 'id': contract.id, + 'title': f"Marché {contract.contract_number}", + 'subtitle': (contract.description[:60] + '...') if len(contract.description) > 60 else contract.description, + 'company': contract.company.name if contract.company else '', + 'url': reverse('contracts:contracts_detail', args=[contract.id]), + 'timestamp': it['timestamp'], + 'relative_time': relative_time, + }) + return result + except Exception as e: + logger.warning(f"Error fetching user recent history: {e}") + return [] + + @login_required def activity_geojson(request): """ diff --git a/loko/interventions/templates/interventions/intervention_detail.html b/loko/interventions/templates/interventions/intervention_detail.html index eee6b9e..127a693 100644 --- a/loko/interventions/templates/interventions/intervention_detail.html +++ b/loko/interventions/templates/interventions/intervention_detail.html @@ -53,4 +53,22 @@ {% block script %} {% include "interventions/detail/scripts.html" %} + {% endblock script %}