diff --git a/loko/interventions/locale/en/LC_MESSAGES/django.mo b/loko/interventions/locale/en/LC_MESSAGES/django.mo index 0b01a19..f52a5cf 100644 Binary files a/loko/interventions/locale/en/LC_MESSAGES/django.mo and b/loko/interventions/locale/en/LC_MESSAGES/django.mo differ diff --git a/loko/interventions/locale/en/LC_MESSAGES/django.po b/loko/interventions/locale/en/LC_MESSAGES/django.po index 60739d0..5f26f5a 100644 --- a/loko/interventions/locale/en/LC_MESSAGES/django.po +++ b/loko/interventions/locale/en/LC_MESSAGES/django.po @@ -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… --" + diff --git a/loko/interventions/locale/fr/LC_MESSAGES/django.mo b/loko/interventions/locale/fr/LC_MESSAGES/django.mo index fa58b7e..c399862 100644 Binary files a/loko/interventions/locale/fr/LC_MESSAGES/django.mo and b/loko/interventions/locale/fr/LC_MESSAGES/django.mo differ diff --git a/loko/interventions/locale/fr/LC_MESSAGES/django.po b/loko/interventions/locale/fr/LC_MESSAGES/django.po index de19591..afd6c54 100644 --- a/loko/interventions/locale/fr/LC_MESSAGES/django.po +++ b/loko/interventions/locale/fr/LC_MESSAGES/django.po @@ -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… --" + diff --git a/loko/interventions/locale/nl/LC_MESSAGES/django.mo b/loko/interventions/locale/nl/LC_MESSAGES/django.mo index e7bf0ab..30f0851 100644 Binary files a/loko/interventions/locale/nl/LC_MESSAGES/django.mo and b/loko/interventions/locale/nl/LC_MESSAGES/django.mo differ diff --git a/loko/interventions/locale/nl/LC_MESSAGES/django.po b/loko/interventions/locale/nl/LC_MESSAGES/django.po index bb35d8d..fb4b5b1 100644 --- a/loko/interventions/locale/nl/LC_MESSAGES/django.po +++ b/loko/interventions/locale/nl/LC_MESSAGES/django.po @@ -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… --" + diff --git a/loko/interventions/static/interventions/interventions_index.js b/loko/interventions/static/interventions/interventions_index.js index 8f8c25c..d9656e4 100644 --- a/loko/interventions/static/interventions/interventions_index.js +++ b/loko/interventions/static/interventions/interventions_index.js @@ -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)); +function getRolePersonsData() { + const scriptEl = document.getElementById('role-persons-data'); + if (!scriptEl) return { manager: [], controller: [], inspector: [] }; + try { + return JSON.parse(scriptEl.textContent) || { manager: [], controller: [], inspector: [] }; + } catch (e) { + console.error("Error parsing role-persons data", e); + return { manager: [], controller: [], inspector: [] }; + } +} - // Gather unique managers - const managersMap = new Map(); - let anyUsesManager = false; +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'); - 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) { - try { - const managers = JSON.parse(managersStr); - managers.forEach(mgr => { - managersMap.set(String(mgr.id), mgr.name); - }); - } catch (e) { - console.error("Error parsing managers JSON", e); - } + 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 = ``; + + 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 += ` -
- - -
- `; + 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 = ``; + 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); + }); + + personSelect.addEventListener('change', function() { + syncRolePersonHiddenInputs(); + }); + + if (clearBtn) { + clearBtn.addEventListener('click', function() { + resetRolePersonFilter(); }); - container.innerHTML = html; - row.classList.remove('d-none'); + } + + // 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; } } diff --git a/loko/interventions/templates/interventions/interventions_index.html b/loko/interventions/templates/interventions/interventions_index.html index e54fb2c..8ba11e9 100644 --- a/loko/interventions/templates/interventions/interventions_index.html +++ b/loko/interventions/templates/interventions/interventions_index.html @@ -770,21 +770,6 @@ - -
-
-
- -
- - -
-
-
- -
-
-
{% endif %} @@ -825,6 +810,42 @@ + +
+
+
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+
@@ -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 }}'; + {{ role_persons_data|json_script:"role-persons-data" }} + diff --git a/loko/interventions/tests/test_general.py b/loko/interventions/tests/test_general.py index 09c01ea..0687b9d 100644 --- a/loko/interventions/tests/test_general.py +++ b/loko/interventions/tests/test_general.py @@ -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) + + diff --git a/loko/interventions/views/general.py b/loko/interventions/views/general.py index a6fb862..ee639c4 100644 --- a/loko/interventions/views/general.py +++ b/loko/interventions/views/general.py @@ -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) diff --git a/loko/interventions/views/helpers.py b/loko/interventions/views/helpers.py index 55caea9..b21399e 100644 --- a/loko/interventions/views/helpers.py +++ b/loko/interventions/views/helpers.py @@ -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):