feat(interventions): add combined filter for manager, controller and inspector with shortnames and teams

This commit is contained in:
kdeterme 2026-09-30 18:49:19 +02:00
parent b86d805cb5
commit 3d9e7c3202
11 changed files with 647 additions and 69 deletions

View file

@ -8407,5 +8407,29 @@ msgstr "Cannot modify material from a canceled preparation order."
msgid "Bon annulé"
msgstr "Canceled order"
msgid "Rôle et personne assignée"
msgstr "Role and assigned person"
msgid "Rôle"
msgstr "Role"
msgid "Personne"
msgstr "Person"
msgid "Gestionnaire"
msgstr "Manager"
msgid "Contrôleur"
msgstr "Controller"
msgid "Inspecteur"
msgstr "Inspector"
msgid "-- Sélectionner un rôle… --"
msgstr "-- Select a role… --"
msgid "-- Choisir une personne… --"
msgstr "-- Choose a person… --"

View file

@ -8514,5 +8514,29 @@ msgstr "Impossible de modifier le matériel d'un bon de préparation annulé."
msgid "Bon annulé"
msgstr "Bon annulé"
msgid "Rôle et personne assignée"
msgstr "Rôle et personne assignée"
msgid "Rôle"
msgstr "Rôle"
msgid "Personne"
msgstr "Personne"
msgid "Gestionnaire"
msgstr "Gestionnaire"
msgid "Contrôleur"
msgstr "Contrôleur"
msgid "Inspecteur"
msgstr "Inspecteur"
msgid "-- Sélectionner un rôle… --"
msgstr "-- Sélectionner un rôle… --"
msgid "-- Choisir une personne… --"
msgstr "-- Choisir une personne… --"

View file

@ -8448,4 +8448,28 @@ msgstr "Kan materiaal van een geannuleerde voorbereidingsbon niet wijzigen."
msgid "Bon annulé"
msgstr "Geannuleerde bon"
msgid "Rôle et personne assignée"
msgstr "Rol en toegewezen persoon"
msgid "Rôle"
msgstr "Rol"
msgid "Personne"
msgstr "Persoon"
msgid "Gestionnaire"
msgstr "Beheerder"
msgid "Contrôleur"
msgstr "Controleur"
msgid "Inspecteur"
msgstr "Inspecteur"
msgid "-- Sélectionner un rôle… --"
msgstr "-- Selecteer een rol… --"
msgid "-- Choisir une personne… --"
msgstr "-- Kies een persoon… --"

View file

@ -330,6 +330,7 @@ document.getElementById('reset-filters').addEventListener('click', async functio
togglePauseReasonFilter();
filterCategoriesByThematics();
filterProvidersAndContracts();
resetRolePersonFilter();
// Explicitly clear date fields
document.getElementById('begin_date_min').value = '';
@ -2185,8 +2186,8 @@ function filterProvidersAndContracts() {
}
});
}
if (typeof updateInterventionManagers === 'function') {
updateInterventionManagers();
if (typeof updateRolePersonManagers === 'function') {
updateRolePersonManagers();
}
}
@ -2278,7 +2279,9 @@ function countActiveFilters() {
'contract': false,
'assigned_team': false,
'assigned_member': false,
'assigned_controller': false,
'intervention_manager': false,
'inspector': false,
'pause_reason': false,
'begin_date_min': false,
'begin_date_max': false,
@ -2330,7 +2333,8 @@ function getCurrentFilters() {
// Get all filter parameters
const filterKeys = [
'thematic', 'status', 'priority', 'maintain_type', 'category',
'type', 'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager',
'type', 'provider', 'contract', 'assigned_team', 'assigned_member',
'assigned_controller', 'intervention_manager', 'inspector',
'pause_reason', 'locations', 'overdue', 'to_be_planned_soon'
];
@ -2407,7 +2411,8 @@ function countActiveFilters() {
// Define filter parameters used in the global filter modal
const filterParams = [
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
'provider', 'contract', 'assigned_team', 'assigned_member',
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
'overdue', 'to_be_planned_soon'
];
@ -2451,7 +2456,8 @@ function savePreferencesFromUrl() {
// Collect all filter parameters
const filterParams = [
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
'provider', 'contract', 'assigned_team', 'assigned_member',
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
'overdue', 'to_be_planned_soon'
];
@ -2504,7 +2510,8 @@ async function savePreferencesFromForm(urlParams) {
// Collect all filter parameters from URLSearchParams
const filterParams = [
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
'provider', 'contract', 'assigned_team', 'assigned_member', 'intervention_manager', 'pause_reason',
'provider', 'contract', 'assigned_team', 'assigned_member',
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
'overdue', 'to_be_planned_soon'
];
@ -2558,6 +2565,7 @@ async function savePreferencesFromForm(urlParams) {
document.addEventListener('DOMContentLoaded', function() {
updateFilterBadge();
initDateFilterPresets();
initRolePersonFilter();
// Save preferences when table sort changes (user action only)
const $table = $('#interventions-table');
@ -2637,62 +2645,153 @@ if (filterModal) {
});
}
function updateInterventionManagers() {
const checkedContracts = document.querySelectorAll('.filter-options[data-field="contract"] input[type="checkbox"]:checked');
const container = document.getElementById('intervention-manager-options-container');
const row = document.getElementById('intervention-manager-filter-row');
if (!container || !row) return;
// ==================== Role & Person Filter (Gestionnaire / Contrôleur / Inspecteur) ====================
// Get currently selected managers from URL or from checkboxes if they already exist
const urlParams = new URLSearchParams(window.location.search);
const selectedManagers = new Set(urlParams.getAll('intervention_manager'));
// Also check if any are currently checked in the UI before re-drawing
const existingChecked = container.querySelectorAll('input[type="checkbox"]:checked');
existingChecked.forEach(cb => selectedManagers.add(cb.value));
// Gather unique managers
const managersMap = new Map();
let anyUsesManager = false;
checkedContracts.forEach(cb => {
const optionDiv = cb.closest('.filter-option');
if (optionDiv) {
const usesManager = optionDiv.getAttribute('data-uses-manager') === 'true';
if (usesManager) {
anyUsesManager = true;
const managersStr = optionDiv.getAttribute('data-managers');
if (managersStr) {
function getRolePersonsData() {
const scriptEl = document.getElementById('role-persons-data');
if (!scriptEl) return { manager: [], controller: [], inspector: [] };
try {
const managers = JSON.parse(managersStr);
managers.forEach(mgr => {
managersMap.set(String(mgr.id), mgr.name);
});
return JSON.parse(scriptEl.textContent) || { manager: [], controller: [], inspector: [] };
} catch (e) {
console.error("Error parsing managers JSON", e);
console.error("Error parsing role-persons data", e);
return { manager: [], controller: [], inspector: [] };
}
}
function syncRolePersonHiddenInputs() {
const roleSelect = document.getElementById('filter-person-role');
const personSelect = document.getElementById('filter-person-select');
const hiddenMgr = document.getElementById('hidden-filter-intervention-manager');
const hiddenCtrl = document.getElementById('hidden-filter-assigned-controller');
const hiddenInsp = document.getElementById('hidden-filter-inspector');
if (!roleSelect || !personSelect) return;
const role = roleSelect.value;
const personId = personSelect.value;
if (hiddenMgr) hiddenMgr.value = (role === 'manager' && personId) ? personId : '';
if (hiddenCtrl) hiddenCtrl.value = (role === 'controller' && personId) ? personId : '';
if (hiddenInsp) hiddenInsp.value = (role === 'inspector' && personId) ? personId : '';
}
function populatePersonSelect(role, preselectedId) {
const personSelect = document.getElementById('filter-person-select');
if (!personSelect) return;
const chooseLabel = typeof gettext === 'function' ? gettext("-- Choisir une personne… --") : "-- Choisir une personne… --";
personSelect.innerHTML = `<option value="">${chooseLabel}</option>`;
if (!role) {
personSelect.disabled = true;
syncRolePersonHiddenInputs();
return;
}
const data = getRolePersonsData();
let persons = data[role] || [];
// If role is manager, filter by checked contracts if any are checked
if (role === 'manager') {
const checkedContractBoxes = document.querySelectorAll('.filter-options[data-field="contract"] input[type="checkbox"]:checked');
if (checkedContractBoxes.length > 0) {
const checkedContractIds = Array.from(checkedContractBoxes).map(cb => parseInt(cb.value, 10)).filter(Boolean);
if (checkedContractIds.length > 0) {
const filtered = persons.filter(p => {
const pContracts = (p.contracts || []).map(cid => parseInt(cid, 10));
return pContracts.some(cid => checkedContractIds.includes(cid));
});
if (filtered.length > 0) {
persons = filtered;
}
}
}
}
persons.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
if (preselectedId && String(p.id) === String(preselectedId)) {
opt.selected = true;
}
personSelect.appendChild(opt);
});
if (anyUsesManager && managersMap.size > 0) {
// Build the HTML for options
let html = '';
managersMap.forEach((name, id) => {
const isChecked = selectedManagers.has(id) ? 'checked' : '';
html += `
<div class="filter-option">
<input type="checkbox" name="intervention_manager" id="intervention_manager_${id}" value="${id}" ${isChecked}>
<label for="intervention_manager_${id}">${name}</label>
</div>
`;
personSelect.disabled = false;
if (preselectedId) {
personSelect.value = String(preselectedId);
}
syncRolePersonHiddenInputs();
}
function updateRolePersonManagers() {
const roleSelect = document.getElementById('filter-person-role');
const personSelect = document.getElementById('filter-person-select');
if (!roleSelect || !personSelect) return;
if (roleSelect.value === 'manager') {
const currentPersonId = personSelect.value;
populatePersonSelect('manager', currentPersonId);
}
}
function resetRolePersonFilter() {
const roleSelect = document.getElementById('filter-person-role');
const personSelect = document.getElementById('filter-person-select');
if (roleSelect) roleSelect.value = '';
if (personSelect) {
const chooseLabel = typeof gettext === 'function' ? gettext("-- Choisir une personne… --") : "-- Choisir une personne… --";
personSelect.innerHTML = `<option value="">${chooseLabel}</option>`;
personSelect.disabled = true;
}
syncRolePersonHiddenInputs();
}
function initRolePersonFilter() {
const roleSelect = document.getElementById('filter-person-role');
const personSelect = document.getElementById('filter-person-select');
const clearBtn = document.getElementById('clear-role-person-filter');
if (!roleSelect || !personSelect) return;
roleSelect.addEventListener('change', function() {
populatePersonSelect(this.value);
});
container.innerHTML = html;
row.classList.remove('d-none');
personSelect.addEventListener('change', function() {
syncRolePersonHiddenInputs();
});
if (clearBtn) {
clearBtn.addEventListener('click', function() {
resetRolePersonFilter();
});
}
// Determine initial role and person from server context or URL parameters
const urlParams = new URLSearchParams(window.location.search);
let initRole = typeof SELECTED_ROLE !== 'undefined' ? SELECTED_ROLE : '';
let initPersonId = typeof SELECTED_PERSON_ID !== 'undefined' ? SELECTED_PERSON_ID : '';
if (!initRole) {
if (urlParams.get('intervention_manager')) {
initRole = 'manager';
initPersonId = urlParams.get('intervention_manager');
} else if (urlParams.get('assigned_controller')) {
initRole = 'controller';
initPersonId = urlParams.get('assigned_controller');
} else if (urlParams.get('inspector')) {
initRole = 'inspector';
initPersonId = urlParams.get('inspector');
}
}
if (initRole) {
roleSelect.value = initRole;
populatePersonSelect(initRole, initPersonId);
} else {
container.innerHTML = '';
row.classList.add('d-none');
personSelect.disabled = true;
}
}

View file

@ -770,21 +770,6 @@
</div>
</div>
</div>
<!-- Gestionnaires (dynamic) -->
<div class="row mt-3 d-none" id="intervention-manager-filter-row">
<div class="col-md-12 filter-section">
<div class="filter-section-header">
<label>{% translate "Gestionnaire d'intervention" %}</label>
<div class="filter-section-buttons">
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" data-field="intervention_manager">{% translate "Tous" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" data-field="intervention_manager">{% translate "Aucun" %}</button>
</div>
</div>
<div class="filter-options" data-field="intervention_manager" id="intervention-manager-options-container">
<!-- Dynamic options will be inserted here by JS -->
</div>
</div>
</div>
{% endif %}
<!-- Équipes / Membres assignés -->
@ -825,6 +810,42 @@
</div>
</div>
<!-- Rôle et Responsable / Intervenant assigné (Gestionnaire / Contrôleur / Inspecteur) -->
<div class="row mt-3 pt-3 border-top">
<div class="col-md-12 filter-section">
<div class="filter-section-header">
<label class="d-flex align-items-center">
<i class="bi bi-person-badge me-2 text-primary"></i>{% translate "Rôle et personne assignée" %}
</label>
<div class="filter-section-buttons">
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-role-person-filter">
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer" %}
</button>
</div>
</div>
<div class="row g-2 align-items-center mt-1">
<div class="col-md-4">
<label for="filter-person-role" class="form-label small text-muted mb-1">{% translate "Rôle" %}</label>
<select class="form-select form-select-sm" id="filter-person-role">
<option value="">{% translate "-- Sélectionner un rôle… --" %}</option>
<option value="manager" {% if selected_role == "manager" %}selected{% endif %}>{% translate "Gestionnaire" %}</option>
<option value="controller" {% if selected_role == "controller" %}selected{% endif %}>{% translate "Contrôleur" %}</option>
<option value="inspector" {% if selected_role == "inspector" %}selected{% endif %}>{% translate "Inspecteur" %}</option>
</select>
</div>
<div class="col-md-8">
<label for="filter-person-select" class="form-label small text-muted mb-1">{% translate "Personne" %}</label>
<select class="form-select form-select-sm" id="filter-person-select" {% if not selected_role %}disabled{% endif %}>
<option value="">{% translate "-- Choisir une personne… --" %}</option>
</select>
</div>
</div>
<input type="hidden" name="intervention_manager" id="hidden-filter-intervention-manager" value="{% if selected_role == 'manager' %}{{ selected_person_id }}{% endif %}">
<input type="hidden" name="assigned_controller" id="hidden-filter-assigned-controller" value="{% if selected_role == 'controller' %}{{ selected_person_id }}{% endif %}">
<input type="hidden" name="inspector" id="hidden-filter-inspector" value="{% if selected_role == 'inspector' %}{{ selected_person_id }}{% endif %}">
</div>
</div>
<!-- Localisations (full width) -->
<div class="filter-section mt-3 pt-3 border-top">
<div class="filter-section-header">
@ -1466,8 +1487,12 @@
const SAVED_SORT = {{ saved_sort|default:"{}"|safe }};
const SAVED_COLUMNS = {{ saved_columns|default:"null"|safe }};
const subcatNoneLabel = "{% translate '(aucune sous-catégorie)' %}";
const SELECTED_ROLE = '{{ selected_role|default:""|escapejs }}';
const SELECTED_PERSON_ID = '{{ selected_person_id|default:""|escapejs }}';
</script>
{{ role_persons_data|json_script:"role-persons-data" }}
<script src="{% url 'javascript-catalog' %}"></script>
<script src="{% static 'common/base_script.js' %}"></script>

View file

@ -2004,6 +2004,198 @@ class InterventionEndDateFilteringTests(TestCase):
self.assertIn(itv_reopened, apply_intervention_filters_from_querydict(qs, params_august, user=self.user))
class InterventionRolePersonFilteringTests(TestCase):
def setUp(self):
self.User = get_user_model()
self.admin_user = self.User.objects.create_user(
username='admin_role_test',
password='pwd',
first_name='Jean',
last_name='Dupont'
)
self.role_admin, _ = Role.objects.get_or_create(name='admin')
self.config = UserConfig.objects.create(user=self.admin_user, is_intern=True)
self.config.roles.add(self.role_admin)
self.thematic = Thematic.objects.create(
code='thematic_role_test',
name_fr='Thématique Test',
name_nl='Thema Test',
allow_planning_without_contract=True
)
UserThematics.objects.create(
user_config=self.config,
thematic=self.thematic,
can_view_interventions=True,
can_edit_interventions=True,
can_inspect=True,
)
self.company = Company.objects.create(name='Entreprise Test')
self.control_team = CompanyTeam.objects.create(
name='Équipe Contrôle Qualité',
company=self.company,
operation_types=[CompanyTeam.OPERATION_CONTROLS]
)
self.control_team.thematics.add(self.thematic)
self.controller_user = self.User.objects.create_user(
username='ctrl_user_test',
password='pwd',
first_name='Marc',
last_name='Lambert'
)
self.controller_member = CompanyMember.objects.create(
name='Marc Lambert',
user=self.controller_user
)
self.controller_member.teams.add(self.control_team)
self.inspector_user = self.User.objects.create_user(
username='insp_user_test',
password='pwd',
first_name='Claire',
last_name='Bernard'
)
self.insp_config = UserConfig.objects.create(user=self.inspector_user, is_intern=True)
UserThematics.objects.create(
user_config=self.insp_config,
thematic=self.thematic,
can_view_interventions=True,
can_inspect=True,
)
self.manager_user = self.User.objects.create_user(
username='mgr_user_test',
password='pwd',
first_name='Paul',
last_name='Martin'
)
# Interventions
self.itv_manager = Intervention.objects.create(
title='Intervention avec gestionnaire',
thematic=self.thematic,
created_by=self.admin_user,
intervention_manager=self.manager_user
)
self.itv_controller = Intervention.objects.create(
title='Intervention avec contrôleur',
thematic=self.thematic,
created_by=self.admin_user,
assigned_controller=self.controller_member
)
self.itv_inspection_checked = Intervention.objects.create(
title='Intervention avec inspection liée',
thematic=self.thematic,
created_by=self.admin_user,
)
self.itv_type_inspection = Intervention.objects.create(
title='Intervention type inspection',
thematic=self.thematic,
created_by=self.admin_user,
type='inspection',
assigned_member=CompanyMember.objects.create(
name='Claire Bernard',
user=self.inspector_user
)
)
from inspections.models import Inspection
self.inspection = Inspection.objects.create(
checked_intervention=self.itv_inspection_checked,
inspector=self.inspector_user,
asset_content_type=ContentType.objects.get_for_model(Intervention),
asset_object_id=self.itv_inspection_checked.id
)
def test_format_shortname_helpers(self):
from interventions.views.helpers import _format_user_shortname_with_team, _format_member_shortname_with_team
# User with team (via CompanyMember)
formatted_ctrl_user = _format_user_shortname_with_team(self.controller_user)
self.assertIn("Marc L.", formatted_ctrl_user)
self.assertIn("Équipe Contrôle Qualité", formatted_ctrl_user)
# Member with team
formatted_member = _format_member_shortname_with_team(self.controller_member)
self.assertEqual(formatted_member, "Marc L. (Équipe Contrôle Qualité)")
# User without team
formatted_mgr = _format_user_shortname_with_team(self.manager_user)
self.assertEqual(formatted_mgr, "Paul M.")
def test_interventions_index_role_person_context(self):
self.client.login(username='admin_role_test', password='pwd')
url = reverse('interventions:interventions_index')
# 1. No filter (follows default redirect)
resp = self.client.get(url, follow=True)
self.assertEqual(resp.status_code, 200)
self.assertIn('role_persons_data', resp.context)
self.assertEqual(resp.context['selected_role'], '')
self.assertEqual(resp.context['selected_person_id'], '')
# 2. Filter by manager
resp_mgr = self.client.get(f"{url}?intervention_manager={self.manager_user.id}")
self.assertEqual(resp_mgr.status_code, 200)
self.assertEqual(resp_mgr.context['selected_role'], 'manager')
self.assertEqual(resp_mgr.context['selected_person_id'], str(self.manager_user.id))
# 3. Filter by controller
resp_ctrl = self.client.get(f"{url}?assigned_controller={self.controller_member.id}")
self.assertEqual(resp_ctrl.status_code, 200)
self.assertEqual(resp_ctrl.context['selected_role'], 'controller')
self.assertEqual(resp_ctrl.context['selected_person_id'], str(self.controller_member.id))
# 4. Filter by inspector
resp_insp = self.client.get(f"{url}?inspector={self.inspector_user.id}")
self.assertEqual(resp_insp.status_code, 200)
self.assertEqual(resp_insp.context['selected_role'], 'inspector')
self.assertEqual(resp_insp.context['selected_person_id'], str(self.inspector_user.id))
def test_filter_by_assigned_controller_and_inspector(self):
from django.http import QueryDict
from interventions.views.helpers import apply_intervention_filters_from_querydict
qs = Intervention.objects.all()
# Filter by controller via apply_intervention_filters_from_querydict
params_ctrl = QueryDict(f'assigned_controller={self.controller_member.id}')
filtered_ctrl = list(apply_intervention_filters_from_querydict(qs, params_ctrl, user=self.admin_user))
self.assertIn(self.itv_controller, filtered_ctrl)
self.assertNotIn(self.itv_manager, filtered_ctrl)
# Filter by inspector via apply_intervention_filters_from_querydict
params_insp = QueryDict(f'inspector={self.inspector_user.id}')
filtered_insp = list(apply_intervention_filters_from_querydict(qs, params_insp, user=self.admin_user))
self.assertIn(self.itv_inspection_checked, filtered_insp)
self.assertIn(self.itv_type_inspection, filtered_insp)
self.assertNotIn(self.itv_controller, filtered_insp)
self.assertNotIn(self.itv_manager, filtered_insp)
# Test via interventions_geojson
self.client.login(username='admin_role_test', password='pwd')
geojson_url = reverse('interventions:interventions_geojson')
# GeoJSON: controller
resp_ctrl = self.client.get(f"{geojson_url}?assigned_controller={self.controller_member.id}")
self.assertEqual(resp_ctrl.status_code, 200)
features_ctrl = resp_ctrl.json().get('features', [])
itv_ids_ctrl = [f['properties']['id'] for f in features_ctrl]
self.assertIn(self.itv_controller.id, itv_ids_ctrl)
self.assertNotIn(self.itv_manager.id, itv_ids_ctrl)
# GeoJSON: inspector
resp_insp = self.client.get(f"{geojson_url}?inspector={self.inspector_user.id}")
self.assertEqual(resp_insp.status_code, 200)
features_insp = resp_insp.json().get('features', [])
itv_ids_insp = [f['properties']['id'] for f in features_insp]
self.assertIn(self.itv_inspection_checked.id, itv_ids_insp)
self.assertIn(self.itv_type_inspection.id, itv_ids_insp)
self.assertNotIn(self.itv_controller.id, itv_ids_insp)

View file

@ -269,7 +269,7 @@ def index(request):
for mgr in managers_qs:
managers_data.append({
'id': mgr.id,
'name': _get_user_short_name(mgr)
'name': _format_user_shortname_with_team(mgr)
})
managers_json = json.dumps(managers_data)
@ -356,6 +356,103 @@ def index(request):
list(m_thematics),
))
# Build role-persons data (Gestionnaire / Contrôleur / Inspecteur)
# 1. Managers
user_access_map = defaultdict(set)
for uca in UserContractAccess.objects.filter(contract__in=contracts_qs, can_approve=True):
user_access_map[uca.user_config.user_id].add(uca.contract_id)
managed_itv_map = defaultdict(set)
for itv_data in Intervention.objects.filter(contract__in=contracts_qs, intervention_manager__isnull=False).values('intervention_manager_id', 'contract_id').distinct():
managed_itv_map[itv_data['intervention_manager_id']].add(itv_data['contract_id'])
manager_users_qs = (
User.objects.filter(
Q(config__usercontractaccess__contract__in=contracts_qs, config__usercontractaccess__can_approve=True) |
Q(managed_interventions__contract__in=contracts_qs) |
Q(config__roles__name='manager')
)
.prefetch_related('companymember_set__teams', 'config__company')
.distinct()
.order_by('first_name', 'last_name', 'username')
)
manager_items = []
for mgr in manager_users_qs:
c_ids = list(user_access_map[mgr.id] | managed_itv_map[mgr.id])
manager_items.append({
'id': mgr.id,
'name': _format_user_shortname_with_team(mgr),
'contracts': c_ids,
})
# 2. Controllers
controller_members_qs = (
CompanyMember.objects.filter(
Q(teams__operation_types__contains=[CompanyTeam.OPERATION_CONTROLS]) |
Q(controlled_interventions__isnull=False)
)
)
if allowed_company_ids is not None:
controller_members_qs = controller_members_qs.filter(teams__company_id__in=allowed_company_ids)
if allowed_thematic_ids:
controller_members_qs = controller_members_qs.filter(teams__thematics__id__in=allowed_thematic_ids)
controller_members_qs = (
controller_members_qs
.select_related('user')
.prefetch_related('teams__company')
.distinct()
.order_by('name')
)
controller_items = [
{
'id': m.id,
'name': _format_member_shortname_with_team(m),
}
for m in controller_members_qs
]
# 3. Inspectors
from inspections.models import Inspection
inspector_users_qs = (
User.objects.filter(
Q(config__userthematics__can_inspect=True) |
Q(inspections__isnull=False) |
Q(companymember__teams__operation_types__contains=[CompanyTeam.OPERATION_INSPECTIONS]) |
Q(companymember__assigned_inspections__isnull=False)
)
.prefetch_related('companymember_set__teams', 'config__company')
.distinct()
.order_by('first_name', 'last_name', 'username')
)
inspector_items = [
{
'id': u.id,
'name': _format_user_shortname_with_team(u),
}
for u in inspector_users_qs
]
role_persons_data = {
'manager': manager_items,
'controller': controller_items,
'inspector': inspector_items,
}
selected_managers = _expand_csv_params(request.GET.getlist('intervention_manager'))
selected_controllers = _expand_csv_params(request.GET.getlist('assigned_controller'))
selected_inspectors = _expand_csv_params(request.GET.getlist('inspector'))
selected_role = ''
selected_person_id = ''
if selected_managers:
selected_role = 'manager'
selected_person_id = str(selected_managers[0])
elif selected_controllers:
selected_role = 'controller'
selected_person_id = str(selected_controllers[0])
elif selected_inspectors:
selected_role = 'inspector'
selected_person_id = str(selected_inspectors[0])
# Status classes (Bootstrap contextual classes)
status_classes = {
'in_preparation': 'secondary',
@ -415,7 +512,12 @@ def index(request):
'selected_teams': request.GET.getlist('assigned_team'),
'selected_members': request.GET.getlist('assigned_member'),
'selected_control_teams': request.GET.getlist('assigned_control_team'),
'selected_controllers': request.GET.getlist('assigned_controller'),
'selected_controllers': selected_controllers,
'selected_managers': selected_managers,
'selected_inspectors': selected_inspectors,
'selected_role': selected_role,
'selected_person_id': selected_person_id,
'role_persons_data': role_persons_data,
'selected_pause_reasons': pause_reasons,
'selected_preparation_states': preparation_states,
'begin_date_min': begin_date_min or '',
@ -471,6 +573,7 @@ def interventions_geojson(request):
assigned_controllers = _expand_csv_params(request.GET.getlist('assigned_controller'))
pause_reasons = _expand_csv_params(request.GET.getlist('pause_reason'))
intervention_managers = _expand_csv_params(request.GET.getlist('intervention_manager'))
inspectors = _expand_csv_params(request.GET.getlist('inspector'))
preparation_states = _expand_csv_params(request.GET.getlist('preparation_state'))
begin_date_min = request.GET.get('begin_date_min') or None
begin_date_max = request.GET.get('begin_date_max') or None
@ -555,6 +658,21 @@ def interventions_geojson(request):
qs = qs.filter(Q(assigned_control_team_id__in=assigned_control_teams) | Q(assigned_controller__teams__in=assigned_control_teams)).distinct()
if intervention_managers:
qs = qs.filter(intervention_manager_id__in=intervention_managers)
if inspectors:
from inspections.models import Inspection
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(Intervention)
insp_target_qs = Inspection.objects.filter(
Q(inspector_id__in=inspectors) |
Q(assigned_member_id__in=inspectors) |
Q(assigned_member__user_id__in=inspectors)
)
qs = qs.filter(
Q(quality_inspections__in=insp_target_qs) |
Q(generated_inspections__in=insp_target_qs) |
Q(id__in=insp_target_qs.filter(asset_content_type=ct).values('asset_object_id')) |
(Q(type='inspection') & (Q(assigned_member_id__in=inspectors) | Q(assigned_member__user_id__in=inspectors)))
).distinct()
if followed_only == '1' and request.user.is_authenticated:
qs = qs.filter(subscriptions__user=request.user)

View file

@ -173,6 +173,8 @@ __all__ = [
'_flatten_asset_collection',
'_force_delete_operations_for_assets',
'_format_location_label',
'_format_member_shortname_with_team',
'_format_user_shortname_with_team',
'_get_allowed_asset_category_ids',
'_get_asset_classes',
'_get_asset_model_classes',
@ -1765,7 +1767,10 @@ def apply_intervention_filters_from_querydict(qs, params, user=None):
contracts = _expand_csv_params(params.getlist('contract'))
assigned_teams = _expand_csv_params(params.getlist('assigned_team'))
assigned_members = _expand_csv_params(params.getlist('assigned_member'))
assigned_control_teams = _expand_csv_params(params.getlist('assigned_control_team'))
assigned_controllers = _expand_csv_params(params.getlist('assigned_controller'))
intervention_managers = _expand_csv_params(params.getlist('intervention_manager'))
inspectors = _expand_csv_params(params.getlist('inspector'))
pause_reasons = _expand_csv_params(params.getlist('pause_reason'))
preparation_states = _expand_csv_params(params.getlist('preparation_state'))
begin_date_min = params.get('begin_date_min') or None
@ -1812,8 +1817,27 @@ def apply_intervention_filters_from_querydict(qs, params, user=None):
qs = qs.filter(assigned_member_id__in=assigned_members)
if assigned_teams:
qs = qs.filter(Q(assigned_team_id__in=assigned_teams) | Q(assigned_member__teams__in=assigned_teams)).distinct()
if assigned_controllers:
qs = qs.filter(assigned_controller_id__in=assigned_controllers)
if assigned_control_teams:
qs = qs.filter(Q(assigned_control_team_id__in=assigned_control_teams) | Q(assigned_controller__teams__in=assigned_control_teams)).distinct()
if intervention_managers:
qs = qs.filter(intervention_manager_id__in=intervention_managers)
if inspectors:
from inspections.models import Inspection
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(Intervention)
insp_target_qs = Inspection.objects.filter(
Q(inspector_id__in=inspectors) |
Q(assigned_member_id__in=inspectors) |
Q(assigned_member__user_id__in=inspectors)
)
qs = qs.filter(
Q(quality_inspections__in=insp_target_qs) |
Q(generated_inspections__in=insp_target_qs) |
Q(id__in=insp_target_qs.filter(asset_content_type=ct).values('asset_object_id')) |
(Q(type='inspection') & (Q(assigned_member_id__in=inspectors) | Q(assigned_member__user_id__in=inspectors)))
).distinct()
if followed_only == '1' and user and user.is_authenticated:
qs = qs.filter(subscriptions__user=user)
@ -2387,6 +2411,54 @@ def _get_user_short_name(user):
return display
def _format_user_shortname_with_team(user):
"""
Format a User's short name with team/company in parentheses.
E.g., "Prénom N. (Nom Équipe)" or "Prénom N."
"""
if not user:
return ""
short_name = _get_user_short_name(user)
teams = [
t.name
for cm in user.companymember_set.all()
for t in cm.teams.all()
if t.name
]
if teams:
team_str = ", ".join(dict.fromkeys(teams))
elif hasattr(user, 'config') and user.config and user.config.company:
team_str = user.config.company.name
else:
team_str = ""
return f"{short_name} ({team_str})" if team_str else short_name
def _format_member_shortname_with_team(member):
"""
Format a CompanyMember's short name with team in parentheses.
E.g., "Prénom N. (Nom Équipe)" or "Prénom N."
"""
if not member:
return ""
if member.user and (member.user.first_name or member.user.last_name):
short_name = _get_user_short_name(member.user)
elif member.name:
parts = member.name.strip().split()
if len(parts) >= 2 and parts[-1].isalpha():
short_name = f"{parts[0]} {parts[-1][0].upper()}."
else:
short_name = member.name
elif member.user:
short_name = getattr(member.user, 'username', '')
else:
short_name = ""
teams = [t.name for t in member.teams.all() if t.name]
team_str = ", ".join(dict.fromkeys(teams)) if teams else ""
return f"{short_name} ({team_str})" if team_str else short_name
def _get_available_occupation_members(user, intervention):