From 37c1016b57bec840d80ddbdca54929124e25d27b Mon Sep 17 00:00:00 2001 From: kdeterme Date: Mon, 28 Sep 2026 14:45:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(interventions):=20distinguer=20les=20inter?= =?UTF-8?q?ventions=20'=C3=A0=20traiter'=20d=C3=A9j=C3=A0=20assign=C3=A9es?= =?UTF-8?q?=20en=20rose=20(#d81b60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout de la couleur 'to_be_processed_assigned' (#d81b60) dans STATUS_COLORS (Python & JS). - Ajout de la propriété is_assigned sur le modèle Intervention. - Prise en compte de la couleur dans les vues calendrier (membres, timeline, backlog, équipes) et sur la carte MapLibre du modal calendrier. - Enrichissement des popups carte et des badges du backlog avec l'équipe/membre assigné. - Ajout de tests unitaires validant la couleur et la sérialisation is_assigned. --- loko/common/static/common/base_style.css | 5 + loko/interventions/models.py | 11 +- .../calendar/interventions_calendar.css | 9 ++ .../calendar/interventions_calendar.js | 74 ++++++++++-- .../static/interventions/status_colors.js | 35 +++++- .../interventions/interventions_calendar.html | 77 +++++++++---- loko/interventions/tests/test_calendar.py | 105 ++++++++++++++++++ loko/interventions/views/calendar.py | 9 +- loko/interventions/views/general.py | 11 +- 9 files changed, 295 insertions(+), 41 deletions(-) diff --git a/loko/common/static/common/base_style.css b/loko/common/static/common/base_style.css index aa296a7..5e005f1 100644 --- a/loko/common/static/common/base_style.css +++ b/loko/common/static/common/base_style.css @@ -312,6 +312,11 @@ label, color: #ffffff !important; } +.badge.status-to_be_processed_assigned { + background-color: #d81b60 !important; + color: #ffffff !important; +} + .badge.status-assigned { background-color: #63b3ed !important; color: #000000 !important; diff --git a/loko/interventions/models.py b/loko/interventions/models.py index e5589b3..fc36bfb 100644 --- a/loko/interventions/models.py +++ b/loko/interventions/models.py @@ -67,8 +67,9 @@ STATUS_COLORS = { 'in_preparation': '#757575', 'to_be_approved': '#ffb74d', 'to_be_planned': '#ff8a80', - 'to_be_processed': '#f4511e', - 'assigned': '#63b3ed', + 'to_be_processed': '#f4511e', + 'to_be_processed_assigned': '#d81b60', + 'assigned': '#63b3ed', 'in_progress': '#3182ce', 'on_pause': '#6b226e', 'finished': '#26a69a', @@ -928,8 +929,14 @@ class Intervention(models.Model): return os.path.basename(self.intervention_sheet.name) return None + @property + def is_assigned(self): + return bool(self.assigned_team_id or self.assigned_member_id) + @property def status_color_hex(self): + if self.status == 'to_be_processed' and self.is_assigned: + return STATUS_COLORS.get('to_be_processed_assigned', '#d81b60') return STATUS_COLORS.get(self.status, '#757575') @property diff --git a/loko/interventions/static/interventions/calendar/interventions_calendar.css b/loko/interventions/static/interventions/calendar/interventions_calendar.css index 293dbcc..e273468 100644 --- a/loko/interventions/static/interventions/calendar/interventions_calendar.css +++ b/loko/interventions/static/interventions/calendar/interventions_calendar.css @@ -69,6 +69,15 @@ color: #ffffff !important; } +.vis-item.status-to_be_processed.is-assigned, +.vis-item.status-to_be_processed_assigned { background-color: #d81b60; border-color: #b51750; color: #fff; } + +.badge.status-to_be_processed_assigned, +.badge[class*="status-to_be_processed_assigned"] { + background-color: #d81b60 !important; + color: #ffffff !important; +} + .canvas-timeline { border: 1px solid var(--bs-border-color, #e2e8f0); diff --git a/loko/interventions/static/interventions/calendar/interventions_calendar.js b/loko/interventions/static/interventions/calendar/interventions_calendar.js index a70ea35..a05dcd2 100644 --- a/loko/interventions/static/interventions/calendar/interventions_calendar.js +++ b/loko/interventions/static/interventions/calendar/interventions_calendar.js @@ -2,6 +2,32 @@ // --- Status colors mapping (réfère à la constante de status_colors.js) const STATUS_COLORS = INTERVENTION_STATUS_COLORS; +/** + * Retourne la couleur d'une intervention en tenant compte du statut + * et de l'assignation (pour to_be_processed assigné -> #d81b60). + */ +function getInterventionColor(itvOrCode, isAssigned = false) { + let statusCode = ''; + let assigned = isAssigned; + if (typeof itvOrCode === 'object' && itvOrCode !== null) { + statusCode = itvOrCode.status_code || itvOrCode.status || itvOrCode.statusCode || ''; + if (itvOrCode.is_assigned !== undefined) { + assigned = Boolean(itvOrCode.is_assigned); + } else if (itvOrCode.member_name || itvOrCode.memberName || itvOrCode.team_name || itvOrCode.teamName) { + assigned = true; + } + } else { + statusCode = String(itvOrCode || ''); + } + if (typeof getInterventionStatusColor === 'function') { + return getInterventionStatusColor(statusCode, assigned); + } + if (statusCode === 'to_be_processed' && assigned) { + return STATUS_COLORS.to_be_processed_assigned || '#d81b60'; + } + return STATUS_COLORS[statusCode] || DEFAULT_STATUS_COLOR; +} + const DEFAULT_STATUS_COLOR = '#888888'; const EDITABLE_STATUSES = ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned']; const DAY_MS = 24 * 60 * 60 * 1000; @@ -562,7 +588,8 @@ document.addEventListener('DOMContentLoaded', function () { let itvsHTML = '
'; if (dayData.interventions && dayData.interventions.length > 0) { dayData.interventions.forEach(itv => { - const cardColor = STATUS_COLORS[itv.status] || DEFAULT_STATUS_COLOR; + const isAssigned = Boolean(itv.is_assigned || (team && team.id !== null) || itv.member_name); + const cardColor = getInterventionColor(itv.status, isAssigned); const textCol = idealTextColor(cardColor); let isEditable = canMoveTimelineItems && EDITABLE_STATUSES.includes(itv.status); @@ -961,6 +988,14 @@ document.addEventListener('DOMContentLoaded', function () { }; }).filter(Boolean); + const isAssigned = Boolean( + source.extendedProps?.is_assigned !== undefined + ? source.extendedProps.is_assigned + : (source.is_assigned !== undefined + ? source.is_assigned + : (memberName || source.extendedProps?.team_name || source.team_name)) + ); + return { id: source.id ?? `event-${index}`, code, @@ -970,6 +1005,8 @@ document.addEventListener('DOMContentLoaded', function () { statusHistory, milestones, memberName, + teamName: source.extendedProps?.team_name || source.team_name || '', + isAssigned, prepState, maintainTypeLabel: source.extendedProps?.maintain_type_label || source.maintain_type_label || '', startDate, @@ -1671,7 +1708,9 @@ document.addEventListener('DOMContentLoaded', function () { const sphereRadius = 8; for (let i = 0; i < historyPoints.length; i++) { const sh = historyPoints[i]; - const sphereColor = STATUS_COLORS[sh.statusCode] || DEFAULT_STATUS_COLOR; + const sphereColor = (sh.statusCode === 'to_be_processed' && (sh.isCurrent ? item.isAssigned : false)) + ? (STATUS_COLORS.to_be_processed_assigned || '#d81b60') + : (STATUS_COLORS[sh.statusCode] || DEFAULT_STATUS_COLOR); // Halo sur le statut actuel if (sh.isCurrent) { @@ -1707,6 +1746,7 @@ document.addEventListener('DOMContentLoaded', function () { date: sh.date, userName: sh.userName, isCurrent: sh.isCurrent, + itemIsAssigned: item.isAssigned, itemCode: item.code, itemTitle: item.title, x: sh.x - 10, @@ -1933,7 +1973,8 @@ document.addEventListener('DOMContentLoaded', function () { const hit = hitsList[0]; const dateStr = formatItemDate(hit.date); if (hit.kind === 'status') { - const color = STATUS_COLORS[hit.statusCode] || DEFAULT_STATUS_COLOR; + const isAssigned = Boolean(hit.itemIsAssigned || anchorHit?.itemIsAssigned); + const color = getInterventionColor(hit.statusCode, hit.isCurrent ? isAssigned : false); const currentBadge = hit.isCurrent ? ` ${gettext('Actuel')}` : ''; const userStr = hit.userName ? `
${escapeHtml(hit.userName)}
` : ''; content = ` @@ -1966,7 +2007,8 @@ document.addEventListener('DOMContentLoaded', function () { const dateStr = formatItemDate(hit.date); const isBorder = idx < hitsList.length - 1 ? 'border-bottom border-light border-opacity-10 pb-1 mb-1' : ''; if (hit.kind === 'status') { - const color = STATUS_COLORS[hit.statusCode] || DEFAULT_STATUS_COLOR; + const isAssigned = Boolean(hit.itemIsAssigned || anchorHit?.itemIsAssigned); + const color = getInterventionColor(hit.statusCode, hit.isCurrent ? isAssigned : false); const currentBadge = hit.isCurrent ? ` ${gettext('Actuel')}` : ''; const userStr = hit.userName ? `(${escapeHtml(hit.userName)})` : ''; return ` @@ -2265,7 +2307,9 @@ document.addEventListener('DOMContentLoaded', function () { const sphereRadius = 8; for (let i = 0; i < historyPoints.length; i++) { const sh = historyPoints[i]; - const sphereColor = STATUS_COLORS[sh.statusCode] || DEFAULT_STATUS_COLOR; + const sphereColor = (sh.statusCode === 'to_be_processed' && sh.isCurrent) + ? (STATUS_COLORS.to_be_processed_assigned || '#d81b60') + : (STATUS_COLORS[sh.statusCode] || DEFAULT_STATUS_COLOR); if (sh.isCurrent) { ctx.save(); @@ -3094,7 +3138,7 @@ document.addEventListener('DOMContentLoaded', function () { const barLeft = hasLayout ? layout.barLeft : Math.max(startX, MEMBERS_LABEL_WIDTH - 60); const barRight = hasLayout ? layout.barRight : Math.min(endX, MEMBERS_LABEL_WIDTH + visibleWidth + 60); const barWidth = Math.max(6, hasLayout ? layout.barWidth : (barRight - barLeft)); - const color = STATUS_COLORS[item.statusCode] || DEFAULT_STATUS_COLOR; + const color = getInterventionColor(item.statusCode, true); const shouldDim = isDraggingThisItem && dragState?.hasMoved; ctx.save(); @@ -3212,7 +3256,7 @@ document.addEventListener('DOMContentLoaded', function () { const barLeft = hasLayout ? layout.barLeft : Math.max(startX, MEMBERS_LABEL_WIDTH - 60); const barRight = hasLayout ? layout.barRight : Math.min(endX, MEMBERS_LABEL_WIDTH + visibleWidth + 60); const barWidth = Math.max(6, hasLayout ? layout.barWidth : (barRight - barLeft)); - const color = STATUS_COLORS[item.statusCode] || DEFAULT_STATUS_COLOR; + const color = getInterventionColor(item.statusCode, true); const shouldDim = isDraggingThisItem && dragState?.hasMoved; ctx.save(); @@ -3310,7 +3354,7 @@ document.addEventListener('DOMContentLoaded', function () { const laneBaseY = rowTop + MEMBERS_ROW_VERTICAL_PADDING; const laneStep = TIMELINE_BAR_HEIGHT + MEMBERS_LANE_VERTICAL_SPACING; const barY = laneBaseY + laneIndex * laneStep; - const color = STATUS_COLORS[drag.item.statusCode] || DEFAULT_STATUS_COLOR; + const color = getInterventionColor(drag.item.statusCode, true); ctx.save(); ctx.globalAlpha = 0.85; @@ -4568,7 +4612,8 @@ document.addEventListener('DOMContentLoaded', function () { item.draggable = true; item.dataset.interventionId = intervention.id; - const color = STATUS_COLORS[intervention.status_code] || DEFAULT_STATUS_COLOR; + const isAssigned = Boolean(intervention.is_assigned || intervention.member_name || intervention.team_name); + const color = getInterventionColor(intervention.status_code, isAssigned); const textColor = idealTextColor(color); const titleRow = document.createElement('div'); titleRow.className = 'd-flex justify-content-between align-items-start gap-2'; @@ -4619,6 +4664,12 @@ document.addEventListener('DOMContentLoaded', function () { assigneeBadge.style.fontSize = '0.7rem'; assigneeBadge.textContent = intervention.member_name; badgesColumn.appendChild(assigneeBadge); + } else if (intervention.team_name) { + const assigneeBadge = document.createElement('span'); + assigneeBadge.className = 'badge rounded-pill text-bg-light text-dark border border-secondary fw-bold'; + assigneeBadge.style.fontSize = '0.7rem'; + assigneeBadge.textContent = intervention.team_name; + badgesColumn.appendChild(assigneeBadge); } titleRow.appendChild(titleSpan); @@ -4756,7 +4807,8 @@ document.addEventListener('DOMContentLoaded', function () { const status = record.status || record.extendedProps?.status || ''; const statusCode = record.status_code || record.extendedProps?.status_code || ''; - const color = STATUS_COLORS[statusCode] || DEFAULT_STATUS_COLOR; + const isAssigned = Boolean(record.is_assigned || record.extendedProps?.is_assigned || record.member_name || record.extendedProps?.member_name || record.team_name || record.extendedProps?.team_name); + const color = getInterventionColor(statusCode, isAssigned); const textColor = idealTextColor(color); modalInterventionStatus.textContent = status; @@ -5265,7 +5317,7 @@ function openMemberHistoryModal(memberId) { ${escapeHtml(itv.code)} ${escapeHtml(itv.title)} ${escapeHtml(itv.team_name)} - ${escapeHtml(itv.status_display)} + ${escapeHtml(itv.status_display)} `; }).join(''); diff --git a/loko/interventions/static/interventions/status_colors.js b/loko/interventions/static/interventions/status_colors.js index ee420b0..b558faf 100644 --- a/loko/interventions/static/interventions/status_colors.js +++ b/loko/interventions/static/interventions/status_colors.js @@ -14,6 +14,7 @@ const INTERVENTION_STATUS_COLORS = { to_be_approved: '#ffb74d', to_be_planned: '#ff8a80', to_be_processed: '#f4511e', + to_be_processed_assigned: '#d81b60', assigned: '#63b3ed', in_progress: '#3182ce', on_pause: '#6b226e', @@ -31,14 +32,42 @@ const INTERVENTION_STATUS_COLORS = { const INTERVENTION_DEFAULT_STATUS_COLOR = '#6c757d'; /** - * Expression MapLibre « match » prête à l'emploi dans les couches circle/fill. - * Générée automatiquement depuis INTERVENTION_STATUS_COLORS. + * Expression MapLibre prête à l'emploi dans les couches circle/fill. + * Générée automatiquement depuis INTERVENTION_STATUS_COLORS, + * avec prise en compte du statut 'à traiter' assigné (#d81b60). */ const INTERVENTION_STATUS_COLOR_EXPR = (() => { const expr = ['match', ['get', 'status']]; for (const [status, color] of Object.entries(INTERVENTION_STATUS_COLORS)) { + if (status === 'to_be_processed_assigned') continue; expr.push(status, color); } expr.push(INTERVENTION_DEFAULT_STATUS_COLOR); // valeur par défaut - return expr; + + return [ + 'case', + [ + 'all', + ['==', ['get', 'status'], 'to_be_processed'], + [ + 'any', + ['boolean', ['get', 'is_assigned'], false], + ['!=', ['coalesce', ['get', 'team'], ''], ''], + ['!=', ['coalesce', ['get', 'member'], ''], ''] + ] + ], + INTERVENTION_STATUS_COLORS.to_be_processed_assigned, + expr + ]; })(); + +/** + * Retourne la couleur hex du statut en tenant compte de l'assignation. + * Pour 'to_be_processed' assigné, retourne '#d81b60' (rose framboise). + */ +function getInterventionStatusColor(status, isAssigned = false) { + if (status === 'to_be_processed' && isAssigned) { + return INTERVENTION_STATUS_COLORS.to_be_processed_assigned; + } + return INTERVENTION_STATUS_COLORS[status] || INTERVENTION_DEFAULT_STATUS_COLOR; +} diff --git a/loko/interventions/templates/interventions/interventions_calendar.html b/loko/interventions/templates/interventions/interventions_calendar.html index 290a584..1457739 100644 --- a/loko/interventions/templates/interventions/interventions_calendar.html +++ b/loko/interventions/templates/interventions/interventions_calendar.html @@ -948,25 +948,41 @@ // ---- Carte des interventions (modal) ---- (function () { const MAPTILER_STYLE = window.MAP_STYLE_LIGHT; - const STATUS_COLOR_EXPR = [ - 'match', ['get', 'status'], - 'in_preparation', '#6c757d', - 'to_be_approved', '#ffc107', - 'to_be_planned', '#ffc107', - 'to_be_processed', '#dc3545', - 'assigned', '#ffc107', - 'in_progress', '#007bff', - 'on_pause', '#ffc107', - 'finished', '#17a2b8', - 'processed', '#17a2b8', - 'to_be_corrected', '#dc3545', - 'corrected', '#17a2b8', - 'validated', '#28a745', - 'invoiced', '#343a40', - 'closed', '#343a40', - 'canceled', '#6c757d', - '#888888' - ]; + const STATUS_COLOR_EXPR = (typeof INTERVENTION_STATUS_COLOR_EXPR !== 'undefined') + ? INTERVENTION_STATUS_COLOR_EXPR + : [ + 'case', + [ + 'all', + ['==', ['get', 'status'], 'to_be_processed'], + [ + 'any', + ['boolean', ['get', 'is_assigned'], false], + ['!=', ['coalesce', ['get', 'team'], ''], ''], + ['!=', ['coalesce', ['get', 'member'], ''], ''] + ] + ], + '#d81b60', + [ + 'match', ['get', 'status'], + 'in_preparation', '#6c757d', + 'to_be_approved', '#ffc107', + 'to_be_planned', '#ffc107', + 'to_be_processed', '#dc3545', + 'assigned', '#ffc107', + 'in_progress', '#007bff', + 'on_pause', '#ffc107', + 'finished', '#17a2b8', + 'processed', '#17a2b8', + 'to_be_corrected', '#dc3545', + 'corrected', '#17a2b8', + 'validated', '#28a745', + 'invoiced', '#343a40', + 'closed', '#343a40', + 'canceled', '#6c757d', + '#888888' + ] + ]; const DRAW_STYLES = [{id:'gl-draw-polygon-fill',type:'fill',filter:['all',['==','$type','Polygon']],paint:{'fill-color':['case',['==',['get','active'],'true'],'orange','#0066cc'],'fill-opacity':.15}},{id:'gl-draw-lines',type:'line',filter:['any',['==','$type','LineString'],['==','$type','Polygon']],layout:{'line-cap':'round','line-join':'round'},paint:{'line-color':['case',['==',['get','active'],'true'],'orange','#0066cc'],'line-width':2}},{id:'gl-draw-point-outer',type:'circle',filter:['all',['==','$type','Point'],['==','meta','feature']],paint:{'circle-radius':['case',['==',['get','active'],'true'],7,5],'circle-color':'white'}},{id:'gl-draw-point-inner',type:'circle',filter:['all',['==','$type','Point'],['==','meta','feature']],paint:{'circle-radius':['case',['==',['get','active'],'true'],5,3],'circle-color':['case',['==',['get','active'],'true'],'orange','#0066cc']}},{id:'gl-draw-vertex-outer',type:'circle',filter:['all',['==','$type','Point'],['==','meta','vertex'],['!=','mode','simple_select']],paint:{'circle-radius':['case',['==',['get','active'],'true'],7,5],'circle-color':'white'}},{id:'gl-draw-vertex-inner',type:'circle',filter:['all',['==','$type','Point'],['==','meta','vertex'],['!=','mode','simple_select']],paint:{'circle-radius':['case',['==',['get','active'],'true'],5,3],'circle-color':'orange'}},{id:'gl-draw-midpoint',type:'circle',filter:['all',['==','meta','midpoint']],paint:{'circle-radius':3,'circle-color':'orange'}}]; const TEAM_PALETTE = ['#0066CC','#1A9850','#F46D43','#9E0142','#3288BD','#66C2A5','#FF8800','#D53E4F','#5E4FA2','#66BD63']; @@ -1090,9 +1106,30 @@ if (!e.features.length) return; const props = e.features[0].properties; if (calPopup) calPopup.remove(); + function _esc(s) { + if (!s) return ''; + return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); + } + const isAssigned = Boolean(props.is_assigned || props.team || props.member); + const statusColor = (props.status === 'to_be_processed' && isAssigned) + ? '#d81b60' + : (typeof getInterventionStatusColor === 'function' + ? getInterventionStatusColor(props.status) + : (props.status === 'to_be_processed' ? '#dc3545' : '#6c757d')); + const statusLabel = props.status_display || props.status || ''; + const assignee = props.member || props.team || ''; + const assigneeHtml = assignee ? `
${_esc(assignee)}
` : ''; + const badgeHtml = statusLabel ? `${_esc(statusLabel)}` : ''; calPopup = new maplibregl.Popup({ maxWidth: '280px' }) .setLngLat(e.lngLat) - .setHTML(`${props.code || ''}
${props.title || ''}`) + .setHTML(` +
+ ${_esc(props.code || '')} + ${badgeHtml} +
+ ${_esc(props.title || '')} + ${assigneeHtml} + `) .addTo(calMap); }); calMap.on('mouseenter', 'cal-ivt-circles', () => { calMap.getCanvas().style.cursor = 'pointer'; }); diff --git a/loko/interventions/tests/test_calendar.py b/loko/interventions/tests/test_calendar.py index 6a413f1..5803b05 100644 --- a/loko/interventions/tests/test_calendar.py +++ b/loko/interventions/tests/test_calendar.py @@ -1140,6 +1140,111 @@ class ExternalManagerScheduleUpdateTests(TestCase): self.assertIn("assigner", res.json().get('error', '').lower()) +class AssignedToBeProcessedColorTests(TestCase): + def setUp(self): + User = get_user_model() + self.user = User.objects.create_user(username='calendar-color-user', password='pwd') + self.role_admin = Role.objects.create(name='admin') + self.user_config = UserConfig.objects.create(user=self.user, is_intern=True) + self.user_config.roles.add(self.role_admin) + + self.company = Company.objects.create(name='Test Company') + self.team = CompanyTeam.objects.create(name='Team Alpha', company=self.company) + self.member = CompanyMember.objects.create(user=self.user, name='Alice Tech') + self.member.teams.add(self.team) + + self.client.login(username='calendar-color-user', password='pwd') + + def test_model_status_color_hex(self): + # Unassigned to_be_processed + itv_unassigned = Intervention.objects.create( + title='Unassigned', + status='to_be_processed', + ) + self.assertFalse(itv_unassigned.is_assigned) + self.assertEqual(itv_unassigned.status_color_hex, '#f4511e') + + # Assigned to member + itv_member = Intervention.objects.create( + title='Assigned to member', + status='to_be_processed', + assigned_member=self.member, + ) + self.assertTrue(itv_member.is_assigned) + self.assertEqual(itv_member.status_color_hex, '#d81b60') + + # Assigned to team only + itv_team = Intervention.objects.create( + title='Assigned to team', + status='to_be_processed', + assigned_team=self.team, + ) + self.assertTrue(itv_team.is_assigned) + self.assertEqual(itv_team.status_color_hex, '#d81b60') + + # Other status assigned + itv_progress = Intervention.objects.create( + title='In progress', + status='in_progress', + assigned_member=self.member, + ) + self.assertEqual(itv_progress.status_color_hex, '#3182ce') + + def test_calendar_events_extended_props_is_assigned(self): + now = timezone.now() + itv_unassigned = Intervention.objects.create( + title='Unassigned ITV', + status='to_be_processed', + planned_begin_time=now + timedelta(hours=1), + planned_end_time=now + timedelta(hours=2), + ) + itv_assigned = Intervention.objects.create( + title='Assigned ITV', + status='to_be_processed', + assigned_member=self.member, + planned_begin_time=now + timedelta(hours=3), + planned_end_time=now + timedelta(hours=4), + ) + + url = reverse('interventions:interventions_events') + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + events = res.json() + ev_unassigned = next((e for e in events if e['id'] == itv_unassigned.id), None) + ev_assigned = next((e for e in events if e['id'] == itv_assigned.id), None) + + self.assertIsNotNone(ev_unassigned) + self.assertFalse(ev_unassigned['extendedProps']['is_assigned']) + + self.assertIsNotNone(ev_assigned) + self.assertTrue(ev_assigned['extendedProps']['is_assigned']) + + def test_interventions_geojson_is_assigned(self): + itv_unassigned = Intervention.objects.create( + title='Geo Unassigned', + status='to_be_processed', + ) + itv_assigned = Intervention.objects.create( + title='Geo Assigned', + status='to_be_processed', + assigned_team=self.team, + ) + + url = reverse('interventions:interventions_geojson') + res = self.client.get(url) + self.assertEqual(res.status_code, 200) + features = res.json()['features'] + f_unassigned = next((f for f in features if f['id'] == itv_unassigned.id), None) + f_assigned = next((f for f in features if f['id'] == itv_assigned.id), None) + + self.assertIsNotNone(f_unassigned) + self.assertFalse(f_unassigned['properties']['is_assigned']) + + self.assertIsNotNone(f_assigned) + self.assertTrue(f_assigned['properties']['is_assigned']) + self.assertEqual(f_assigned['properties']['team'], 'Team Alpha') + + diff --git a/loko/interventions/views/calendar.py b/loko/interventions/views/calendar.py index 95f89e3..0223ef0 100644 --- a/loko/interventions/views/calendar.py +++ b/loko/interventions/views/calendar.py @@ -395,7 +395,7 @@ def interventions_events(request): interventions = ( interventions - .select_related('assigned_member') + .select_related('assigned_member', 'assigned_team') .prefetch_related('assigned_member__teams', 'assigned_member__teams__company') .order_by('effective_begin') # croissant: plus proche d'aujourd'hui au milieu/vers le bas ) @@ -608,7 +608,8 @@ def interventions_events(request): 'actual_end': itv.end_time.isoformat() if itv.end_time else None, 'link': reverse('interventions:intervention_detail', args=[itv.id]), 'member_name': itv.assigned_member.name if itv.assigned_member else None, - 'team_name': ", ".join(t.name for t in itv.assigned_member.teams.all()) if itv.assigned_member else None, + 'team_name': ", ".join(t.name for t in itv.assigned_member.teams.all()) if itv.assigned_member else (itv.assigned_team.name if itv.assigned_team else None), + 'is_assigned': bool(itv.assigned_team_id or itv.assigned_member_id), } }) @@ -708,6 +709,7 @@ def _interventions_events_by_members( 'team_id': team.id if team else (member.teams.all()[0].id if member and member.teams.all() else None), 'team_name': team.name if team else (member.teams.all()[0].name if member and member.teams.all() else None), 'assigned_to_team_only': bool(team and not member), + 'is_assigned': bool(team or member), 'duration_ms': duration_ms, 'link': reverse('interventions:intervention_detail', args=[itv.id]), } @@ -1056,6 +1058,7 @@ def _interventions_events_by_teams_daily( 'status': assign.status or itv.status, 'status_display': itv.get_status_display() if not assign.status else dict(STATUS_CHOICES).get(assign.status, assign.status), 'status_color': itv.get_status_color() if not assign.status else assign.status, + 'is_assigned': True, 'link': reverse('interventions:intervention_detail', args=[itv.id]), 'member_name': assign.member.name if assign.member else None, }) @@ -1092,6 +1095,7 @@ def _interventions_events_by_teams_daily( 'status': itv.status, 'status_display': itv.get_status_display(), 'status_color': itv.get_status_color(), + 'is_assigned': bool(itv.assigned_team_id or itv.assigned_member_id), 'link': reverse('interventions:intervention_detail', args=[itv.id]), 'member_name': None, }) @@ -1186,6 +1190,7 @@ def _interventions_events_by_teams_daily( 'team_id': team.id if team else (member.teams.all()[0].id if member and member.teams.all() else None), 'team_name': team.name if team else (member.teams.all()[0].name if member and member.teams.all() else None), 'assigned_to_team_only': bool(team and not member), + 'is_assigned': bool(team or member), 'duration_ms': duration_ms, 'link': reverse('interventions:intervention_detail', args=[itv.id]), } diff --git a/loko/interventions/views/general.py b/loko/interventions/views/general.py index b10f5f8..a6fb862 100644 --- a/loko/interventions/views/general.py +++ b/loko/interventions/views/general.py @@ -743,7 +743,7 @@ def interventions_geojson(request): # qs = qs.order_by('custom_priority', F('effective_begin').desc(nulls_last=True), '-id')\ qs = qs.order_by('-code')\ - .select_related('assigned_provider', 'contract', 'contract__company', 'assigned_team')\ + .select_related('assigned_provider', 'contract', 'contract__company', 'assigned_team', 'assigned_member')\ .values( 'id', 'code', 'title', 'location_code', 'status', 'maintain_type', 'origin_type_label', 'prep_state', @@ -753,7 +753,10 @@ def interventions_geojson(request): 'contract__company__name', 'contract__is_blocked', 'is_blocked_due_to_contract', + 'assigned_team_id', 'assigned_team__name', + 'assigned_member_id', + 'assigned_member__name', source_category_field, )[:MAX_FEATURES] @@ -761,6 +764,7 @@ def interventions_geojson(request): features = [] for r in qs: geometry = json.loads(r['geom_geojson']) if r['geom_geojson'] else None + is_assigned = bool(r.get('assigned_team_id') or r.get('assigned_member_id')) features.append({ "type": "Feature", "id": r['id'], @@ -772,8 +776,7 @@ def interventions_geojson(request): "location_code": r['location_code'], "begin_time": r['effective_begin'], "status": r['status'], - # If you need the display label, either map it here or annotate a Case() for it - # "status_display": r['status'], + "status_display": str(STATUS_CHOICES.get(r['status'], r['status'])), "maintain_type": r['maintain_type'], "prep_state": r['prep_state'], "assigned_provider": r['assigned_provider__name'], @@ -783,6 +786,8 @@ def interventions_geojson(request): "origin_type": r['origin_type_label'], "source_category": r[source_category_field] if r[source_category_field] else '', "team": r['assigned_team__name'] or '', + "member": r['assigned_member__name'] or '', + "is_assigned": is_assigned, }, })