feat(interventions): afficher les statuts de la chronologie sous forme de cercles numérotés
- Reprend le code visuel du stepper supérieur pour les transitions de statut dans l'historique - Affiche une boule colorée avec le numéro canonique (1 à 15) et le libellé sous chaque statut - Aligne la flèche de transition au centre des cercles - Ajuste les contrastes de texte (noir/blanc) selon la luminosité du statut
This commit is contained in:
parent
fd794737a8
commit
2df4621e5b
5 changed files with 184 additions and 9 deletions
|
|
@ -58,6 +58,10 @@ STATUS_ORDERS = {
|
||||||
'canceled': 200,
|
'canceled': 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Numérotation 1-15 des statuts dans l'ordre canonique du stepper
|
||||||
|
STATUS_STEP_NUMBERS = {key: idx + 1 for idx, key in enumerate(STATUS_CHOICES.keys())}
|
||||||
|
|
||||||
|
|
||||||
# Couleurs canoniques des statuts (hex). Source de vérité partagée avec status_colors.js.
|
# Couleurs canoniques des statuts (hex). Source de vérité partagée avec status_colors.js.
|
||||||
STATUS_COLORS = {
|
STATUS_COLORS = {
|
||||||
'in_preparation': '#757575',
|
'in_preparation': '#757575',
|
||||||
|
|
@ -1328,6 +1332,32 @@ class InterventionTimeLine(models.Model):
|
||||||
def get_to_status_color(self):
|
def get_to_status_color(self):
|
||||||
return STATUS_COLOR_MAP.get(self.to_status, "secondary")
|
return STATUS_COLOR_MAP.get(self.to_status, "secondary")
|
||||||
|
|
||||||
|
def get_from_status_number(self):
|
||||||
|
return STATUS_STEP_NUMBERS.get(self.from_status, '')
|
||||||
|
|
||||||
|
def get_to_status_number(self):
|
||||||
|
return STATUS_STEP_NUMBERS.get(self.to_status, '')
|
||||||
|
|
||||||
|
def get_from_status_hex(self):
|
||||||
|
return STATUS_COLORS.get(self.from_status, '#757575')
|
||||||
|
|
||||||
|
def get_to_status_hex(self):
|
||||||
|
return STATUS_COLORS.get(self.to_status, '#757575')
|
||||||
|
|
||||||
|
def get_from_status_text_color(self):
|
||||||
|
return '#000000' if self.from_status in STATUS_COLORS_DARK_TEXT else '#ffffff'
|
||||||
|
|
||||||
|
def get_to_status_text_color(self):
|
||||||
|
return '#000000' if self.to_status in STATUS_COLORS_DARK_TEXT else '#ffffff'
|
||||||
|
|
||||||
|
from_status_number = property(get_from_status_number)
|
||||||
|
to_status_number = property(get_to_status_number)
|
||||||
|
from_status_hex = property(get_from_status_hex)
|
||||||
|
to_status_hex = property(get_to_status_hex)
|
||||||
|
from_status_text_color = property(get_from_status_text_color)
|
||||||
|
to_status_text_color = property(get_to_status_text_color)
|
||||||
|
|
||||||
|
|
||||||
def get_badge_class(self):
|
def get_badge_class(self):
|
||||||
return {
|
return {
|
||||||
'creation': 'bg-success',
|
'creation': 'bg-success',
|
||||||
|
|
|
||||||
|
|
@ -261,4 +261,66 @@
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Transition de statut : boules de couleur avec numéro et libellé en-dessous */
|
||||||
|
.history-status-transition {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-step {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 90px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-circle {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
margin-bottom: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-arrow {
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
padding: 0 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
word-break: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-change-badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-desc {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,14 +55,28 @@
|
||||||
<!-- Event Info -->
|
<!-- Event Info -->
|
||||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
{% if event.event_type == 'status_change' %}
|
{% if event.event_type == 'status_change' %}
|
||||||
<span class="badge bg-purple history-event-badge">{% translate "Statut" %}</span>
|
<span class="badge bg-purple history-event-badge history-status-change-badge">{% translate "Statut" %}</span>
|
||||||
|
<div class="history-status-transition d-inline-flex align-items-start">
|
||||||
{% if event.from_status %}
|
{% if event.from_status %}
|
||||||
<span class="badge bg-{{ event.get_from_status_color }} history-status-badge" title="{{ event.get_from_status_display }}">{{ event.get_from_status_display }}</span>
|
<div class="history-status-step" title="{{ event.get_from_status_display }}">
|
||||||
<i class="bi bi-arrow-right text-muted small"></i>
|
<div class="history-status-circle" style="background-color: {{ event.from_status_hex }}; color: {{ event.from_status_text_color }};">
|
||||||
|
{{ event.from_status_number }}
|
||||||
|
</div>
|
||||||
|
<div class="history-status-label">{{ event.get_from_status_display }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-status-arrow">
|
||||||
|
<i class="bi bi-arrow-right"></i>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="badge bg-{{ event.get_to_status_color }} history-status-badge" title="{{ event.get_to_status_display }}">{{ event.get_to_status_display }}</span>
|
<div class="history-status-step" title="{{ event.get_to_status_display }}">
|
||||||
|
<div class="history-status-circle" style="background-color: {{ event.to_status_hex }}; color: {{ event.to_status_text_color }};">
|
||||||
|
{{ event.to_status_number }}
|
||||||
|
</div>
|
||||||
|
<div class="history-status-label">{{ event.get_to_status_display }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% if event.event_description %}
|
{% if event.event_description %}
|
||||||
<span class="small text-muted ms-1">({{ event.event_description }})</span>
|
<span class="small text-muted ms-2 align-self-start history-status-desc">({{ event.event_description }})</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif event.event_type == 'assignment' %}
|
{% elif event.event_type == 'assignment' %}
|
||||||
<span class="badge bg-info text-dark history-event-badge">{% translate "Assignation" %}</span>
|
<span class="badge bg-info text-dark history-event-badge">{% translate "Assignation" %}</span>
|
||||||
|
|
|
||||||
|
|
@ -606,4 +606,65 @@
|
||||||
background-color: #6f42c1 !important;
|
background-color: #6f42c1 !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Transition de statut : boules de couleur avec numéro et libellé en-dessous */
|
||||||
|
.history-status-transition {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-step {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 90px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-circle {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||||
|
margin-bottom: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-arrow {
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
padding: 0 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
word-break: normal;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-change-badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status-desc {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ class TimelineHistoryTests(TestCase):
|
||||||
event_time=timezone.now(),
|
event_time=timezone.now(),
|
||||||
event_type='status_change',
|
event_type='status_change',
|
||||||
from_status='in_preparation',
|
from_status='in_preparation',
|
||||||
to_status='planned',
|
to_status='to_be_planned',
|
||||||
event_description='Passage en planifié'
|
event_description='Passage en planifié'
|
||||||
)
|
)
|
||||||
self.assertTrue(event_status.is_status_change)
|
self.assertTrue(event_status.is_status_change)
|
||||||
|
|
@ -90,6 +90,12 @@ class TimelineHistoryTests(TestCase):
|
||||||
self.assertFalse(event_status.is_assignment)
|
self.assertFalse(event_status.is_assignment)
|
||||||
self.assertEqual(event_status.get_badge_class(), 'bg-purple')
|
self.assertEqual(event_status.get_badge_class(), 'bg-purple')
|
||||||
self.assertEqual(event_status.get_icon(), 'bi-arrow-repeat')
|
self.assertEqual(event_status.get_icon(), 'bi-arrow-repeat')
|
||||||
|
self.assertEqual(event_status.from_status_number, 1)
|
||||||
|
self.assertEqual(event_status.to_status_number, 3)
|
||||||
|
self.assertEqual(event_status.from_status_hex, '#757575')
|
||||||
|
self.assertEqual(event_status.to_status_hex, '#ff8a80')
|
||||||
|
self.assertEqual(event_status.from_status_text_color, '#ffffff')
|
||||||
|
self.assertEqual(event_status.to_status_text_color, '#000000')
|
||||||
|
|
||||||
# Assignment event
|
# Assignment event
|
||||||
event_assign = InterventionTimeLine.objects.create(
|
event_assign = InterventionTimeLine.objects.create(
|
||||||
|
|
@ -450,6 +456,7 @@ class TimelineHistoryTests(TestCase):
|
||||||
|
|
||||||
def test_detail_view_shows_all_in_full_history(self):
|
def test_detail_view_shows_all_in_full_history(self):
|
||||||
"""Test that notes, documents, and links appear as detailed events in timeline."""
|
"""Test that notes, documents, and links appear as detailed events in timeline."""
|
||||||
|
record_timeline_event(self.intervention, self.user, event_type='status_change', from_status='in_preparation', to_status='to_be_processed')
|
||||||
record_note_event(self.intervention, self.user, action='create', note_type='comment', content='Note test')
|
record_note_event(self.intervention, self.user, action='create', note_type='comment', content='Note test')
|
||||||
record_document_event(self.intervention, self.user, action='upload', filenames=['doc_test.png'])
|
record_document_event(self.intervention, self.user, action='upload', filenames=['doc_test.png'])
|
||||||
record_relation_link_event(self.intervention, self.user, relation_label='Intervention', target_labels=['I999999'])
|
record_relation_link_event(self.intervention, self.user, relation_label='Intervention', target_labels=['I999999'])
|
||||||
|
|
@ -467,7 +474,8 @@ class TimelineHistoryTests(TestCase):
|
||||||
self.assertContains(response, 'data-event-type="document"')
|
self.assertContains(response, 'data-event-type="document"')
|
||||||
self.assertContains(response, 'data-event-type="link"')
|
self.assertContains(response, 'data-event-type="link"')
|
||||||
self.assertContains(response, 'history-event-badge')
|
self.assertContains(response, 'history-event-badge')
|
||||||
self.assertContains(response, 'history-status-badge')
|
self.assertContains(response, 'history-status-circle')
|
||||||
|
self.assertContains(response, 'history-status-transition')
|
||||||
self.assertContains(response, 'bg-purple')
|
self.assertContains(response, 'bg-purple')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue