diff --git a/src/PhonebookManager.php b/src/PhonebookManager.php index a84ecb3..05a32e8 100644 --- a/src/PhonebookManager.php +++ b/src/PhonebookManager.php @@ -48,14 +48,22 @@ class PhonebookManager CREATE INDEX IF NOT EXISTS idx_phonebook_group ON phonebook_contacts(group_name); "); - // 1. Récupère la cartographie des groupes d'extensions - $egRows = $db->query("SELECT id, name, members FROM extension_groups")->fetchAll(); - $extToGroup = []; + // 1. Récupère la cartographie complète des groupes d'extensions (un poste peut appartenir à plusieurs groupes) + $egRows = $db->query("SELECT id, name, members FROM extension_groups ORDER BY name COLLATE NOCASE ASC")->fetchAll(); + $extToGroups = []; foreach ($egRows as $eg) { + $grpName = trim((string)($eg['name'] ?? '')); + if ($grpName === '') { + continue; + } $members = json_decode((string)$eg['members'], true) ?: []; foreach ($members as $extNum) { - if (!isset($extToGroup[(string)$extNum])) { - $extToGroup[(string)$extNum] = trim((string)$eg['name']); + $k = (string)$extNum; + if (!isset($extToGroups[$k])) { + $extToGroups[$k] = []; + } + if (!in_array($grpName, $extToGroups[$k], true)) { + $extToGroups[$k][] = $grpName; } } } @@ -80,7 +88,7 @@ class PhonebookManager $extNum = trim((string)$ext['extension']); $extName = trim((string)$ext['name']); $email = trim((string)($ext['voicemail_email'] ?? '')); - $group = $extToGroup[$extNum] ?? 'Général'; + $group = !empty($extToGroups[$extNum]) ? implode(', ', $extToGroups[$extNum]) : 'Général'; $activeExtIds[] = $extId; $findStmt->execute([':ext_id' => $extId, ':num' => $extNum]); @@ -122,33 +130,48 @@ class PhonebookManager { self::syncExtensions($db); - $params = []; - $sql = "SELECT * FROM phonebook_contacts"; + $stmt = $db->query("SELECT * FROM phonebook_contacts ORDER BY display_name COLLATE NOCASE ASC"); + $contacts = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : []; - if ($group !== null && trim($group) !== '' && strtolower(trim($group)) !== 'all' && strtolower(trim($group)) !== 'tous') { - $cleanGroup = trim($group); - $sql .= " WHERE LOWER(group_name) = LOWER(:grp) OR LOWER(group_name) LIKE LOWER(:grp_like) OR LOWER(group_name) IN ('tous', 'all')"; - $params[':grp'] = $cleanGroup; - $params[':grp_like'] = "%{$cleanGroup}%"; - } + $cleanGroup = ($group !== null) ? trim($group) : ''; + $filterByGroup = ($cleanGroup !== '' && !in_array(mb_strtolower($cleanGroup), ['all', 'tous'], true)); + $targetLower = mb_strtolower($cleanGroup); - $sql .= " ORDER BY display_name COLLATE NOCASE ASC"; - $stmt = $db->prepare($sql); - $stmt->execute($params); - $contacts = $stmt->fetchAll(PDO::FETCH_ASSOC); - - // Vérification de la présence réelle du fichier sur le disque - foreach ($contacts as &$c) { + $result = []; + foreach ($contacts as $c) { if (!empty($c['photo_url'])) { $filename = basename((string)$c['photo_url']); if (!file_exists(self::AVATARS_DIR . '/' . $filename)) { $c['photo_url'] = ''; } } - } - unset($c); - return $contacts; + $rawGroups = preg_split('/\s*,\s*/', trim((string)($c['group_name'] ?? ''))) ?: []; + $groupsList = array_values(array_unique(array_filter($rawGroups, fn($g) => $g !== ''))); + if (empty($groupsList)) { + $groupsList = ['Général']; + } + $c['groups'] = $groupsList; + $c['group_name'] = implode(', ', $groupsList); + + if ($filterByGroup) { + $matched = false; + foreach ($groupsList as $g) { + $gLower = mb_strtolower($g); + if ($gLower === $targetLower || in_array($gLower, ['tous', 'all'], true)) { + $matched = true; + break; + } + } + if (!$matched) { + continue; + } + } + + $result[] = $c; + } + + return $result; } /** @@ -163,9 +186,11 @@ class PhonebookManager if ($g !== '') $groups[$g] = true; } $pbRows = $db->query("SELECT DISTINCT group_name FROM phonebook_contacts WHERE group_name != ''")->fetchAll(PDO::FETCH_COLUMN); - foreach ($pbRows as $g) { - $g = trim((string)$g); - if ($g !== '') $groups[$g] = true; + foreach ($pbRows as $raw) { + foreach (preg_split('/\s*,\s*/', trim((string)$raw)) ?: [] as $g) { + $g = trim((string)$g); + if ($g !== '') $groups[$g] = true; + } } // Ajoute Issy-Les-Moulineaux si pas encore présent diff --git a/ucp/assets/ucp.js b/ucp/assets/ucp.js index a8b8ba8..b77ed35 100644 --- a/ucp/assets/ucp.js +++ b/ucp/assets/ucp.js @@ -756,14 +756,26 @@ async function loadPhonebook() { } } +function getContactGroups(contact) { + if (Array.isArray(contact?.groups) && contact.groups.length > 0) { + return contact.groups.map(g => String(g || '').trim()).filter(Boolean); + } + const split = String(contact?.group_name || '') + .split(',') + .map(g => g.trim()) + .filter(Boolean); + return split.length > 0 ? Array.from(new Set(split)) : ['Général']; +} + function renderPhonebookGroupTabs(allContacts) { const tabsEl = document.getElementById('ucp-pb-group-tabs'); if (!tabsEl) return; const counts = {}; for (const c of allContacts) { - const grp = String(c.group_name || '').trim() || 'Général'; - counts[grp] = (counts[grp] || 0) + 1; + for (const grp of getContactGroups(c)) { + counts[grp] = (counts[grp] || 0) + 1; + } } const groups = Object.keys(counts).sort((a, b) => { @@ -821,8 +833,7 @@ function renderPhonebookGrid() { const activeGroup = ucpState.selectedPbGroup || '__all__'; const q = (document.getElementById('ucp-pb-search')?.value || '').trim().toLowerCase(); const list = allContacts.filter(c => { - const grp = String(c.group_name || '').trim() || 'Général'; - if (activeGroup !== '__all__' && grp !== activeGroup) return false; + if (activeGroup !== '__all__' && !getContactGroups(c).includes(activeGroup)) return false; if (!q) return true; return [c.display_name, c.internal_number, c.mobile_number, c.other_number, c.email, c.group_name] .some(v => String(v || '').toLowerCase().includes(q)); diff --git a/version.json b/version.json index a5f0fa5..cd15a5e 100644 --- a/version.json +++ b/version.json @@ -1,9 +1,10 @@ { - "version": "2.6.3", + "version": "2.6.4", "codename": "Pulsar UCP", "release_date": "2026-11-12", "min_php_version": "8.2", "changelog": [ + "Correctif v2.6.4 : prise en charge multi-groupes dans l'annuaire UCP & Admin (un poste appartenant à plusieurs groupes apparaît bien dans chacun des onglets correspondants)", "UI v2.6.3 : onglets dynamiques par groupe (Tous, Général, Paris, New-York...) avec compteurs dans l'Annuaire UCP", "Correctif v2.6.2 : sélection stricte de GroundWire (NAT si renseigné, sinon VIP HA si cluster actif, sinon IP locale + port d'écoute local)", "UI v2.6.1 : suppression de l'icône smartphone sur les éléments GroundWire et refonte des boutons d'action du tableau Postes (Redémarrer / Modifier / Supprimer)",