feat(home): add user_history widget for viewers and adjust quick action shortcuts
This commit is contained in:
parent
51e6680763
commit
875efc67f4
17 changed files with 710 additions and 11 deletions
|
|
@ -142,5 +142,25 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
const item = {
|
||||||
|
type: 'contract',
|
||||||
|
id: '{{ contract.id }}',
|
||||||
|
title: 'Marché {{ contract.contract_number|escapejs }}',
|
||||||
|
subtitle: '{{ contract.description|truncatechars:60|default:""|escapejs }}',
|
||||||
|
company: '{{ contract.company.name|default:""|escapejs }}',
|
||||||
|
url: window.location.pathname,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
let h = JSON.parse(localStorage.getItem('loko_user_recent_history') || '[]');
|
||||||
|
h = h.filter(x => !(x.type === item.type && String(x.id) === String(item.id)));
|
||||||
|
h.unshift(item);
|
||||||
|
if (h.length > 20) h = h.slice(0, 20);
|
||||||
|
localStorage.setItem('loko_user_recent_history', JSON.stringify(h));
|
||||||
|
} catch(e) {}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Binary file not shown.
|
|
@ -1146,3 +1146,48 @@ msgstr "to process · in progress"
|
||||||
msgid "Aucun bon à traiter ou en cours"
|
msgid "Aucun bon à traiter ou en cours"
|
||||||
msgstr "No orders to process or in progress"
|
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"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1155,3 +1155,48 @@ msgstr "à traiter · en cours"
|
||||||
msgid "Aucun bon à traiter ou en cours"
|
msgid "Aucun bon à traiter ou en cours"
|
||||||
msgstr "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"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1152,3 +1152,48 @@ msgstr "te verwerken · in behandeling"
|
||||||
msgid "Aucun bon à traiter ou en cours"
|
msgid "Aucun bon à traiter ou en cours"
|
||||||
msgstr "Geen bonnen te verwerken of in behandeling"
|
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"
|
||||||
|
|
||||||
|
|
|
||||||
18
loko/home/migrations/0010_alter_homewidget_widget_code.py
Normal file
18
loko/home/migrations/0010_alter_homewidget_widget_code.py
Normal file
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
49
loko/home/migrations/0011_create_user_history_widget.py
Normal file
49
loko/home/migrations/0011_create_user_history_widget.py
Normal file
|
|
@ -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),
|
||||||
|
]
|
||||||
|
|
@ -24,6 +24,7 @@ WIDGET_CODE_CHOICES = [
|
||||||
('stock_actions', _('Actions stock')),
|
('stock_actions', _('Actions stock')),
|
||||||
('exceptional_transport', _('Transport exceptionnel')),
|
('exceptional_transport', _('Transport exceptionnel')),
|
||||||
('controller_shortcuts', _('Raccourcis contrôleur')),
|
('controller_shortcuts', _('Raccourcis contrôleur')),
|
||||||
|
('user_history', _('Mon historique')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Condition type choices
|
# Condition type choices
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
|
|
||||||
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
map.addControl(new maplibregl.NavigationControl(), 'top-left');
|
||||||
|
|
||||||
let currentMode = 'heatmap'; // 'heatmap' or 'points'
|
let currentMode = 'points'; // 'heatmap' or 'points'
|
||||||
let activityGeojsonData = null;
|
let activityGeojsonData = null;
|
||||||
|
|
||||||
const THEMATIC_COLORS = {
|
const THEMATIC_COLORS = {
|
||||||
|
|
@ -279,6 +279,9 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
type: 'heatmap',
|
type: 'heatmap',
|
||||||
source: 'activity-data',
|
source: 'activity-data',
|
||||||
maxzoom: 17,
|
maxzoom: 17,
|
||||||
|
layout: {
|
||||||
|
'visibility': 'none'
|
||||||
|
},
|
||||||
paint: {
|
paint: {
|
||||||
'heatmap-weight': 1,
|
'heatmap-weight': 1,
|
||||||
'heatmap-intensity': [
|
'heatmap-intensity': [
|
||||||
|
|
@ -350,12 +353,7 @@ if (mapContainer && typeof maplibregl !== 'undefined') {
|
||||||
'icon-anchor': 'center'
|
'icon-anchor': 'center'
|
||||||
},
|
},
|
||||||
paint: {
|
paint: {
|
||||||
// En mode Heatmap : les symboles s'estompent au dézoom pour laisser le champ libre à la heatmap
|
'icon-opacity': 1
|
||||||
'icon-opacity': [
|
|
||||||
'interpolate', ['linear'], ['zoom'],
|
|
||||||
13.8, 0,
|
|
||||||
15.2, 1
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,20 @@
|
||||||
<div class="row justify-content-center mt-2 mb-3">
|
<div class="row justify-content-center mt-2 mb-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="quick-actions-bar">
|
<div class="quick-actions-bar">
|
||||||
|
{% if user.config|has_any_role:"viewer" and not user.config|has_any_role:"manager,admin,controller,operator,inspector" %}
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}" class="quick-action-btn">
|
||||||
|
<span class="quick-action-icon"><i class="bi bi-search"></i></span>
|
||||||
|
<span class="quick-action-text">{% translate "Rechercher une intervention" %}</span>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'interventions:interventions_calendar' %}" class="quick-action-btn">
|
||||||
|
<span class="quick-action-icon"><i class="bi bi-calendar3"></i></span>
|
||||||
|
<span class="quick-action-text">{% translate "Calendrier" %}</span>
|
||||||
|
</a>
|
||||||
|
<a href="#widget-user-history" class="quick-action-btn" onclick="const el=document.getElementById('widget-user-history'); if(el){ el.scrollIntoView({behavior:'smooth'}); el.style.boxShadow='0 0 0 3px rgba(13, 110, 253, 0.4)'; setTimeout(()=>el.style.boxShadow='', 1800); return false; }" data-bs-toggle="modal" data-bs-target="#userHistoryModal">
|
||||||
|
<span class="quick-action-icon"><i class="bi bi-clock-history"></i></span>
|
||||||
|
<span class="quick-action-text">{% translate "Mon historique" %}</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if user.config|has_any_role:"observer,manager,controller,admin,external_manager,top_manager" %}
|
{% if user.config|has_any_role:"observer,manager,controller,admin,external_manager,top_manager" %}
|
||||||
<a href="{% url 'observations:observation_add' %}" class="quick-action-btn quick-action-obs">
|
<a href="{% url 'observations:observation_add' %}" class="quick-action-btn quick-action-obs">
|
||||||
<span class="quick-action-icon"><i class="bi bi-eye"></i></span>
|
<span class="quick-action-icon"><i class="bi bi-eye"></i></span>
|
||||||
|
|
@ -149,6 +163,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{% include "notifications/_new_discussion_modal.html" %}
|
{% include "notifications/_new_discussion_modal.html" %}
|
||||||
|
{% include "home/includes/_user_history_modal.html" %}
|
||||||
{% endblock content%}
|
{% endblock content%}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
155
loko/home/templates/home/includes/_user_history_modal.html
Normal file
155
loko/home/templates/home/includes/_user_history_modal.html
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<!-- Modal Mon Historique -->
|
||||||
|
<div class="modal fade" id="userHistoryModal" tabindex="-1" aria-labelledby="userHistoryModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px; overflow: hidden;">
|
||||||
|
<div class="modal-header border-bottom py-3 px-4 bg-white">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center bg-primary bg-opacity-10 text-primary" style="width: 40px; height: 40px;">
|
||||||
|
<i class="bi bi-clock-history fs-5"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 class="modal-title fw-bold mb-0 text-dark" id="userHistoryModalLabel">{% translate "Mon historique" %}</h5>
|
||||||
|
<small class="text-muted">{% translate "Vos consultations récentes d'interventions et de contrats" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body p-3 p-md-4" style="max-height: 60vh; overflow-y: auto;">
|
||||||
|
{% if user_recent_history %}
|
||||||
|
<div class="list-group list-group-flush gap-2" id="user-history-list">
|
||||||
|
{% for item in user_recent_history %}
|
||||||
|
<a href="{{ item.url }}" class="list-group-item list-group-item-action d-flex align-items-center justify-content-between p-3 border rounded-3 text-decoration-none transition-all user-history-item shadow-sm-hover">
|
||||||
|
<div class="d-flex align-items-center gap-3 min-w-0">
|
||||||
|
{% if item.type == 'intervention' %}
|
||||||
|
<div class="rounded-3 d-flex align-items-center justify-content-center bg-primary text-white flex-shrink-0" style="width: 42px; height: 42px;">
|
||||||
|
<i class="bi bi-tools fs-5"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-truncate">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
|
||||||
|
<span class="badge bg-light text-dark border fw-semibold">{% translate "Intervention" %} #{{ item.id }}</span>
|
||||||
|
{% if item.status %}
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">{{ item.status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="fw-semibold text-dark text-truncate" style="max-width: 520px;" title="{{ item.title }}">{{ item.title }}</div>
|
||||||
|
{% if item.subtitle %}
|
||||||
|
<small class="text-muted"><i class="bi bi-tag me-1"></i>{{ item.subtitle }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% elif item.type == 'contract' %}
|
||||||
|
<div class="rounded-3 d-flex align-items-center justify-content-center bg-success text-white flex-shrink-0" style="width: 42px; height: 42px;">
|
||||||
|
<i class="bi bi-file-earmark-text fs-5"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-truncate">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
|
||||||
|
<span class="badge bg-light text-dark border fw-semibold">{{ item.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="fw-semibold text-dark text-truncate" style="max-width: 520px;" title="{{ item.subtitle }}">{{ item.subtitle }}</div>
|
||||||
|
{% if item.company %}
|
||||||
|
<small class="text-muted"><i class="bi bi-building me-1"></i>{{ item.company }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-end text-nowrap ms-3 flex-shrink-0">
|
||||||
|
<small class="text-muted d-block"><i class="bi bi-clock me-1"></i>{{ item.relative_time }}</small>
|
||||||
|
<span class="btn btn-sm btn-outline-primary rounded-pill mt-1 py-0 px-2.5" style="font-size: 0.75rem;">
|
||||||
|
{% translate "Consulter" %} <i class="bi bi-chevron-right ms-1"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5 px-3" id="user-history-empty">
|
||||||
|
<div class="rounded-circle d-inline-flex align-items-center justify-content-center bg-light text-muted mb-3" style="width: 64px; height: 64px;">
|
||||||
|
<i class="bi bi-clock-history fs-2"></i>
|
||||||
|
</div>
|
||||||
|
<h6 class="fw-bold text-dark mb-1">{% translate "Aucun élément consulté récemment" %}</h6>
|
||||||
|
<p class="text-muted small mb-3" style="max-width: 380px; margin: 0 auto;">
|
||||||
|
{% translate "Les interventions et contrats que vous consultez s'afficheront ici automatiquement pour vous permettre d'y revenir rapidement." %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}" class="btn btn-outline-primary btn-sm rounded-pill px-3">
|
||||||
|
<i class="bi bi-search me-1"></i>{% translate "Rechercher une intervention" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-top bg-light py-2.5 px-4 d-flex justify-content-between align-items-center">
|
||||||
|
<small class="text-muted" style="font-size: 0.78rem;">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>{% translate "Historique basé sur vos consultations dans l'application" %}
|
||||||
|
</small>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm rounded-pill px-3" data-bs-dismiss="modal">
|
||||||
|
{% translate "Fermer" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const modalEl = document.getElementById('userHistoryModal');
|
||||||
|
if (!modalEl) return;
|
||||||
|
|
||||||
|
modalEl.addEventListener('show.bs.modal', function() {
|
||||||
|
try {
|
||||||
|
const localHistory = JSON.parse(localStorage.getItem('loko_user_recent_history') || '[]');
|
||||||
|
if (!localHistory.length) return;
|
||||||
|
|
||||||
|
const listEl = document.getElementById('user-history-list');
|
||||||
|
const emptyEl = document.getElementById('user-history-empty');
|
||||||
|
|
||||||
|
if (emptyEl && !listEl) {
|
||||||
|
const bodyEl = modalEl.querySelector('.modal-body');
|
||||||
|
emptyEl.style.display = 'none';
|
||||||
|
const newList = document.createElement('div');
|
||||||
|
newList.className = 'list-group list-group-flush gap-2';
|
||||||
|
newList.id = 'user-history-list';
|
||||||
|
|
||||||
|
localHistory.slice(0, 10).forEach(item => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = item.url;
|
||||||
|
a.className = 'list-group-item list-group-item-action d-flex align-items-center justify-content-between p-3 border rounded-3 text-decoration-none transition-all user-history-item shadow-sm-hover';
|
||||||
|
|
||||||
|
const isInterv = item.type === 'intervention';
|
||||||
|
const iconBg = isInterv ? 'bg-primary' : 'bg-success';
|
||||||
|
const iconClass = isInterv ? 'bi-tools' : 'bi-file-earmark-text';
|
||||||
|
const badgeText = isInterv ? `Intervention #${item.id}` : item.title;
|
||||||
|
const titleText = isInterv ? item.title : item.subtitle || item.title;
|
||||||
|
const subText = isInterv ? item.subtitle : item.company;
|
||||||
|
|
||||||
|
a.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center gap-3 min-w-0">
|
||||||
|
<div class="rounded-3 d-flex align-items-center justify-content-center ${iconBg} text-white flex-shrink-0" style="width: 42px; height: 42px;">
|
||||||
|
<i class="bi ${iconClass} fs-5"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-truncate">
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-1 flex-wrap">
|
||||||
|
<span class="badge bg-light text-dark border fw-semibold">${badgeText}</span>
|
||||||
|
${item.status ? `<span class="badge bg-primary-subtle text-primary border border-primary-subtle">${item.status}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="fw-semibold text-dark text-truncate" style="max-width: 520px;" title="${titleText}">${titleText}</div>
|
||||||
|
${subText ? `<small class="text-muted"><i class="bi bi-tag me-1"></i>${subText}</small>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end text-nowrap ms-3 flex-shrink-0">
|
||||||
|
<small class="text-muted d-block"><i class="bi bi-clock me-1"></i>Récemment</small>
|
||||||
|
<span class="btn btn-sm btn-outline-primary rounded-pill mt-1 py-0 px-2.5" style="font-size: 0.75rem;">
|
||||||
|
Consulter <i class="bi bi-chevron-right ms-1"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
newList.appendChild(a);
|
||||||
|
});
|
||||||
|
bodyEl.appendChild(newList);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error reading local history:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -8,16 +8,15 @@
|
||||||
<i class="bi bi-geo-alt-fill text-primary"></i>
|
<i class="bi bi-geo-alt-fill text-primary"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0 fw-bold text-dark">{% translate "Activité du Territoire" %}</h6>
|
<h6 class="mb-0 fw-bold text-dark">{% translate "Activité des trois derniers mois" %}</h6>
|
||||||
<small class="text-muted">{% translate "Mise en lumière & intensité des 90 derniers jours" %}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<div class="btn-group btn-group-sm" role="group" id="activityMapModeToggle">
|
<div class="btn-group btn-group-sm" role="group" id="activityMapModeToggle">
|
||||||
<button type="button" class="btn btn-primary active" id="btnModeHeatmap">
|
<button type="button" class="btn btn-outline-primary" id="btnModeHeatmap">
|
||||||
<i class="bi bi-fire me-1"></i>{% translate "Heatmap" %}
|
<i class="bi bi-fire me-1"></i>{% translate "Heatmap" %}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-primary" id="btnModePoints">
|
<button type="button" class="btn btn-primary active" id="btnModePoints">
|
||||||
<i class="bi bi-geo-alt me-1"></i>{% translate "Points" %}
|
<i class="bi bi-geo-alt me-1"></i>{% translate "Points" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
161
loko/home/templates/home/widgets/user_history.html
Normal file
161
loko/home/templates/home/widgets/user_history.html
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
<div class="card border-0 shadow-sm user-history-card h-100" id="widget-user-history" style="border-radius: 12px; overflow: hidden; transition: box-shadow 0.3s ease;">
|
||||||
|
<div class="card-header bg-white border-bottom d-flex align-items-center justify-content-between py-2.5 px-3">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center bg-primary bg-opacity-10 text-primary" style="width: 34px; height: 34px;">
|
||||||
|
<i class="bi bi-clock-history fs-5"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-0 fw-bold text-dark">{% translate "Mon historique" %}</h6>
|
||||||
|
<small class="text-muted">{% translate "Vos dernières consultations" %}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge rounded-pill bg-primary-subtle text-primary border border-primary-subtle px-2.5 py-1 fw-semibold" id="widget-user-history-count" style="font-size: 0.75rem;">
|
||||||
|
{{ user_recent_history|length|default:"0" }} {% translate "consultation(s)" %}
|
||||||
|
</span>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 rounded-pill" data-bs-toggle="modal" data-bs-target="#userHistoryModal" title="{% translate 'Ouvrir en grand' %}">
|
||||||
|
<i class="bi bi-arrows-fullscreen"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body p-0" style="max-height: 480px; overflow-y: auto; overflow-x: hidden;">
|
||||||
|
{% if user_recent_history %}
|
||||||
|
<div class="list-group list-group-flush" id="widget-user-history-list">
|
||||||
|
{% for item in user_recent_history %}
|
||||||
|
<a href="{{ item.url }}" class="list-group-item list-group-item-action py-2.5 px-3 border-bottom-subtle transition-all user-history-item shadow-sm-hover">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
{% if item.type == 'intervention' %}
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center bg-primary text-white flex-shrink-0" style="width: 38px; height: 38px;">
|
||||||
|
<i class="bi bi-tools fs-6"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 min-w-0">
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-1 mb-1">
|
||||||
|
<span class="badge bg-light text-dark border fw-semibold" style="font-size: 0.72rem;">{% translate "Intervention" %} #{{ item.id }}</span>
|
||||||
|
<small class="text-muted text-nowrap ms-1" style="font-size: 0.72rem;">
|
||||||
|
<i class="bi bi-clock me-1"></i>{{ item.relative_time }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="fw-semibold text-dark text-truncate" style="font-size: 0.85rem;" title="{{ item.title }}">{{ item.title }}</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-1 mt-0.5">
|
||||||
|
{% if item.subtitle %}
|
||||||
|
<small class="text-muted text-truncate" style="font-size: 0.75rem;"><i class="bi bi-tag me-1"></i>{{ item.subtitle }}</small>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.status %}
|
||||||
|
<span class="badge bg-primary-subtle text-primary border border-primary-subtle" style="font-size: 0.68rem; padding: 2px 6px;">{{ item.status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif item.type == 'contract' %}
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center bg-success text-white flex-shrink-0" style="width: 38px; height: 38px;">
|
||||||
|
<i class="bi bi-file-earmark-text fs-6"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 min-w-0">
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-1 mb-1">
|
||||||
|
<span class="badge bg-light text-dark border fw-semibold" style="font-size: 0.72rem;">{{ item.title }}</span>
|
||||||
|
<small class="text-muted text-nowrap ms-1" style="font-size: 0.72rem;">
|
||||||
|
<i class="bi bi-clock me-1"></i>{{ item.relative_time }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="fw-semibold text-dark text-truncate" style="font-size: 0.85rem;" title="{{ item.subtitle }}">{{ item.subtitle }}</div>
|
||||||
|
{% if item.company %}
|
||||||
|
<small class="text-muted text-truncate" style="font-size: 0.75rem;"><i class="bi bi-building me-1"></i>{{ item.company }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5 px-3 text-muted" id="widget-user-history-empty">
|
||||||
|
<i class="bi bi-clock-history fs-2 mb-2 d-block text-secondary"></i>
|
||||||
|
<h6 class="fw-bold text-dark mb-1">{% translate "Aucun élément consulté récemment" %}</h6>
|
||||||
|
<p class="small text-muted mb-3" style="max-width: 360px; margin: 0 auto;">
|
||||||
|
{% translate "Vos consultations d'interventions et de contrats apparaîtront ici pour un accès rapide." %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}" class="btn btn-outline-primary btn-sm rounded-pill px-3">
|
||||||
|
<i class="bi bi-search me-1"></i>{% translate "Rechercher une intervention" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-footer bg-light border-top py-2 px-3 text-center d-flex justify-content-between align-items-center flex-wrap gap-1">
|
||||||
|
<a href="{% url 'interventions:interventions_index' %}" class="btn btn-sm btn-link text-decoration-none fw-semibold text-primary p-0" style="font-size: 0.8rem;">
|
||||||
|
<i class="bi bi-search me-1"></i>{% translate "Toutes les interventions" %}
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-sm btn-link text-decoration-none fw-semibold text-secondary p-0" data-bs-toggle="modal" data-bs-target="#userHistoryModal" style="font-size: 0.8rem;">
|
||||||
|
<i class="bi bi-arrows-angle-expand me-1"></i>{% translate "Vue détaillée" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
try {
|
||||||
|
const localHistory = JSON.parse(localStorage.getItem('loko_user_recent_history') || '[]');
|
||||||
|
if (!localHistory.length) return;
|
||||||
|
|
||||||
|
const widgetList = document.getElementById('widget-user-history-list');
|
||||||
|
const widgetEmpty = document.getElementById('widget-user-history-empty');
|
||||||
|
const countBadge = document.getElementById('widget-user-history-count');
|
||||||
|
|
||||||
|
if (widgetEmpty && !widgetList) {
|
||||||
|
const bodyEl = document.querySelector('#widget-user-history .card-body');
|
||||||
|
if (bodyEl) {
|
||||||
|
widgetEmpty.style.display = 'none';
|
||||||
|
const newList = document.createElement('div');
|
||||||
|
newList.className = 'list-group list-group-flush';
|
||||||
|
newList.id = 'widget-user-history-list';
|
||||||
|
|
||||||
|
localHistory.slice(0, 10).forEach(item => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = item.url;
|
||||||
|
a.className = 'list-group-item list-group-item-action py-2.5 px-3 border-bottom-subtle transition-all user-history-item shadow-sm-hover';
|
||||||
|
|
||||||
|
const isInterv = item.type === 'intervention';
|
||||||
|
const iconBg = isInterv ? 'bg-primary' : 'bg-success';
|
||||||
|
const iconClass = isInterv ? 'bi-tools' : 'bi-file-earmark-text';
|
||||||
|
const badgeText = isInterv ? `Intervention #${item.id}` : item.title;
|
||||||
|
const titleText = isInterv ? item.title : item.subtitle || item.title;
|
||||||
|
const subText = isInterv ? item.subtitle : item.company;
|
||||||
|
|
||||||
|
a.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<div class="rounded-circle d-flex align-items-center justify-content-center ${iconBg} text-white flex-shrink-0" style="width: 38px; height: 38px;">
|
||||||
|
<i class="bi ${iconClass} fs-6"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 min-w-0">
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-1 mb-1">
|
||||||
|
<span class="badge bg-light text-dark border fw-semibold" style="font-size: 0.72rem;">${badgeText}</span>
|
||||||
|
<small class="text-muted text-nowrap ms-1" style="font-size: 0.72rem;">
|
||||||
|
<i class="bi bi-clock me-1"></i>Récemment
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="fw-semibold text-dark text-truncate" style="font-size: 0.85rem;" title="${titleText}">${titleText}</div>
|
||||||
|
<div class="d-flex align-items-center justify-content-between gap-1 mt-0.5">
|
||||||
|
${subText ? `<small class="text-muted text-truncate" style="font-size: 0.75rem;"><i class="bi bi-tag me-1"></i>${subText}</small>` : '<span></span>'}
|
||||||
|
${item.status ? `<span class="badge bg-primary-subtle text-primary border border-primary-subtle" style="font-size: 0.68rem; padding: 2px 6px;">${item.status}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
newList.appendChild(a);
|
||||||
|
});
|
||||||
|
bodyEl.appendChild(newList);
|
||||||
|
if (countBadge) {
|
||||||
|
countBadge.textContent = `${localHistory.length} consultation(s)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error syncing widget local history:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -10,8 +10,11 @@ from datetime import timedelta
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# logger = logging.getLogger('home.perf')
|
# logger = logging.getLogger('home.perf')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,6 +87,19 @@ def index(request):
|
||||||
if result:
|
if result:
|
||||||
resolved_widgets.append(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']))
|
resolved_widgets.sort(key=lambda r: (r['order'], r['col_order']))
|
||||||
|
|
||||||
# Group widgets by order into rows; within each row sort by 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)
|
territory_kpis = _get_territory_kpis(request.user, user_contracts)
|
||||||
recent_activity = _get_recent_activity(request.user, user_contracts)
|
recent_activity = _get_recent_activity(request.user, user_contracts)
|
||||||
|
user_recent_history = _get_user_recent_history(request.user)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'territory_kpis': territory_kpis,
|
'territory_kpis': territory_kpis,
|
||||||
'recent_activity': recent_activity,
|
'recent_activity': recent_activity,
|
||||||
|
'user_recent_history': user_recent_history,
|
||||||
'is_inspector': is_inspector,
|
'is_inspector': is_inspector,
|
||||||
'inspection_configs': inspection_configs,
|
'inspection_configs': inspection_configs,
|
||||||
'series_data': json.dumps(series_data),
|
'series_data': json.dumps(series_data),
|
||||||
|
|
@ -1230,6 +1248,118 @@ def _get_recent_activity(user, allowed_contracts):
|
||||||
return recent_items
|
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
|
@login_required
|
||||||
def activity_geojson(request):
|
def activity_geojson(request):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,22 @@
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
{% include "interventions/detail/scripts.html" %}
|
{% include "interventions/detail/scripts.html" %}
|
||||||
|
<script>
|
||||||
|
try {
|
||||||
|
const item = {
|
||||||
|
type: 'intervention',
|
||||||
|
id: '{{ intervention.id }}',
|
||||||
|
title: '{{ intervention.title|default:""|escapejs }}' || 'Intervention #{{ intervention.code|default:intervention.id }}',
|
||||||
|
subtitle: '{{ intervention.thematic.name|default:""|escapejs }}',
|
||||||
|
status: '{{ intervention.get_status_display|default:""|escapejs }}',
|
||||||
|
url: window.location.pathname,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
let h = JSON.parse(localStorage.getItem('loko_user_recent_history') || '[]');
|
||||||
|
h = h.filter(x => !(x.type === item.type && String(x.id) === String(item.id)));
|
||||||
|
h.unshift(item);
|
||||||
|
if (h.length > 20) h = h.slice(0, 20);
|
||||||
|
localStorage.setItem('loko_user_recent_history', JSON.stringify(h));
|
||||||
|
} catch(e) {}
|
||||||
|
</script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue