CDR Advanced Search: Add extension multi select dropdown (#7780)

* CDR Advanced Search: Add extension multi select dropdown

* Update app_languages.php

* Update template.php

* Update css.php

* Update xml_cdr.php

* Update xml_cdr_search.php

* Update xml_cdr_inc.php
This commit is contained in:
Alex
2026-03-11 23:55:13 +00:00
committed by GitHub
parent bb5240a1cc
commit 737650ff74
6 changed files with 361 additions and 25 deletions

View File

@@ -17,7 +17,7 @@
The Initial Developer of the Original Code is
Mark J Crane <markjcrane@fusionpbx.com>
Portions created by the Initial Developer are Copyright (C) 2008-2025
Portions created by the Initial Developer are Copyright (C) 2008-2026
the Initial Developer. All Rights Reserved.
Contributor(s):
@@ -141,8 +141,8 @@
if ($permission['xml_cdr_search_extension']) {
$sql = "select extension_uuid, extension, number_alias from v_extensions ";
$sql .= "where domain_uuid = :domain_uuid ";
if (!$permission['xml_cdr_domain'] && is_array($extension_uuids) && @sizeof($extension_uuids != 0)) {
$sql .= "and extension_uuid in ('".implode("','",$extension_uuids)."') "; //only show the user their extensions
if (!$permission['xml_cdr_domain'] && is_array($assigned_extension_uuids) && @sizeof($assigned_extension_uuids) != 0) {
$sql .= "and extension_uuid in ('".implode("','",$assigned_extension_uuids)."') "; //only show the user their extensions
}
$sql .= "order by extension asc, number_alias asc ";
$parameters['domain_uuid'] = $_SESSION['domain_uuid'];
@@ -393,11 +393,11 @@
echo " ".$text['label-extension']."\n";
echo " </div>\n";
echo " <div class='field'>\n";
echo " <select class='formfld' name='extension_uuid' id='extension_uuid'>\n";
echo " <select class='formfld' name='extension_uuids[]' id='extension_uuid'>\n";
echo " <option value=''></option>";
if (is_array($extensions) && @sizeof($extensions) != 0) {
foreach ($extensions as $row) {
$selected = ($row['extension_uuid'] == $extension_uuid) ? "selected" : null;
$selected = ($row['extension_uuid'] == $extension_uuid[0]) ? "selected" : null;
echo " <option value='".escape($row['extension_uuid'])."' ".escape($selected).">".((is_numeric($row['extension'])) ? escape($row['extension']) : escape($row['number_alias'])." (".escape($row['extension']).")")."</option>";
}
}

View File

@@ -108,7 +108,7 @@
$caller_id_name = $_REQUEST["caller_id_name"] ?? '';
$caller_id_number = $_REQUEST["caller_id_number"] ?? '';
$caller_destination = $_REQUEST["caller_destination"] ?? '';
$extension_uuid = $_REQUEST["extension_uuid"] ?? '';
$extension_uuids = $_REQUEST["extension_uuids"] ?? '';
$destination_number = $_REQUEST["destination_number"] ?? '';
$context = $_REQUEST["context"] ?? '';
$start_stamp_begin = $_REQUEST["start_stamp_begin"] ?? '';
@@ -197,7 +197,7 @@
if (!$permission['xml_cdr_domain'] && isset($_SESSION['user']['extension']) && is_array($_SESSION['user']['extension'])) {
foreach ($_SESSION['user']['extension'] as $row) {
if (is_uuid($row['extension_uuid'])) {
$extension_uuids[] = $row['extension_uuid'];
$assigned_extension_uuids[] = $row['extension_uuid'];
}
}
}
@@ -209,7 +209,11 @@
$param .= "&caller_id_name=".urlencode($caller_id_name ?? '');
$param .= "&caller_id_number=".urlencode($caller_id_number ?? '');
$param .= "&caller_destination=".urlencode($caller_destination ?? '');
$param .= "&extension_uuid=".urlencode($extension_uuid ?? '');
foreach ($extension_uuids as $key => $value) {
if (is_uuid($value)) {
$param .= "&extension_uuids[]=".urlencode($value);
}
}
$param .= "&destination_number=".urlencode($destination_number ?? '');
$param .= "&context=".urlencode($context ?? '');
$param .= "&start_stamp_begin=".urlencode($start_stamp_begin ?? '');
@@ -379,8 +383,8 @@
$parameters['domain_uuid'] = $domain_uuid;
}
if (!$permission['xml_cdr_domain']) { //only show the user their calls
if (isset($extension_uuids) && is_array($extension_uuids) && @sizeof($extension_uuids)) {
$sql .= "and (c.extension_uuid = '".implode("' or c.extension_uuid = '", $extension_uuids)."') \n";
if (isset($assigned_extension_uuids) && is_array($assigned_extension_uuids) && @sizeof($assigned_extension_uuids)) {
$sql .= "and (c.extension_uuid = '".implode("' or c.extension_uuid = '", $assigned_extension_uuids)."') \n";
}
else {
$sql .= "and false \n";
@@ -422,10 +426,8 @@
$parameters['caller_id_number'] = $mod_caller_id_number;
}
}
if (!empty($extension_uuid) && is_uuid($extension_uuid)) {
$sql .= "and e.extension_uuid = :extension_uuid \n";
$parameters['extension_uuid'] = $extension_uuid;
if (!empty($extension_uuids)) {
$sql .= "and e.extension_uuid in ('".implode("','",$extension_uuids)."') \n";
}
if (!empty($caller_destination)) {
$mod_caller_destination = str_replace("*", "%", $caller_destination);

View File

@@ -174,21 +174,36 @@
echo " <input type='text' class='formfld' name='caller_id_number' style='min-width: 115px; width: 115px;' placeholder=\"".$text['label-number']."\" value='".escape($caller_id_number)."'>\n";
echo " </td>\n";
echo " </tr>\n";
echo " <tr>";
echo " <td class='vncell'>".$text['label-extension']."</td>"; //source number
echo " <td class='vtable'>";
echo " <select class='formfld' name='extension_uuid' id='extension_uuid'>\n";
echo " <option value=''></option>";
echo " <tr>\n";
echo " <td class='vncell'>".$text['label-extension']."</td>\n"; //source number
echo " <td class='vtable'>\n";
echo " <div class='multiselect_container'>\n";
echo " <div class='selected_values' id='selected_values'>\n";
echo " <span class='placeholder_text'>".$text['label-select']."...</span>\n";
echo " </div>\n";
echo " <div class='dropdown_list' id='dropdown_list'>\n";
echo " <input type='text' class='search_box' id='search_input' placeholder='".$text['label-search']."'>\n";
echo " <div id='no_results' class='no_results'>".$text['label-no_results']."</div>\n";
echo " <div class='options_list' id='options_list'>\n";
if (is_array($extensions) && @sizeof($extensions) != 0) {
foreach ($extensions as $row) {
$selected = (!empty($caller_extension_uuid) && $row['extension_uuid'] == $caller_extension_uuid) ? "selected" : null;
echo " <option value='".escape($row['extension_uuid'])."' ".escape($selected).">".((is_numeric($row['extension'])) ? escape($row['extension']) : escape($row['number_alias'])." (".escape($row['extension']).")")."</option>";
echo " <label class='option_item' data-value='".escape($row['extension'])."'>\n";
$selected = (!empty($caller_extension_uuid) && $row['extension_uuid'] == $caller_extension_uuid) ? "checked" : null;
echo " <input type='checkbox' value='".escape($row['extension_uuid'])."' ".escape($selected).">\n";
echo " ".((is_numeric($row['extension'])) ? escape($row['extension']) : escape($row['number_alias'])." (".escape($row['extension']).")")."\n";
echo " </label>\n";
}
}
unset($sql, $parameters, $extensions, $row, $selected);
echo " </select>\n";
echo " </td>";
echo " </tr>";
echo " </div>\n";
echo " </div>\n";
echo " </div>\n";
echo " <input type='text' class='formfld' style='display: none;' name='caller_id_numbers[]' id='caller_id_number_0' value='".escape($caller_id_number)."'>\n";
echo " </td>\n";
echo " </tr>\n";
echo " <tr>";
echo " <td class='vncell'>".$text['label-destination']."</td>";
echo " <td class='vtable'><input type='text' class='formfld' name='destination_number' value='".escape($destination_number)."'></td>";

View File

@@ -7947,3 +7947,30 @@ $text['label-ar']['tr-tr'] = "Arapça";
$text['label-ar']['zh-cn'] = "阿拉伯语";
$text['label-ar']['ja-jp'] = "アラビア語";
$text['label-ar']['ko-kr'] = "아랍어";
$text['label-no_results']['en-us'] = "No matches found";
$text['label-no_results']['en-gb'] = "No matches found";
$text['label-no_results']['ar-eg'] = "لم يتم العثور على نتائج";
$text['label-no_results']['de-at'] = "Keine Übereinstimmungen gefunden";
$text['label-no_results']['de-ch'] = "Keine Übereinstimmungen gefunden";
$text['label-no_results']['de-de'] = "Keine Übereinstimmungen gefunden";
$text['label-no_results']['el-gr'] = "Δεν βρέθηκαν αποτελέσματα";
$text['label-no_results']['es-cl'] = "No se encontraron coincidencias";
$text['label-no_results']['es-mx'] = "No se encontraron coincidencias";
$text['label-no_results']['fr-ca'] = "Aucune correspondance trouvée";
$text['label-no_results']['fr-fr'] = "Aucune correspondance trouvée";
$text['label-no_results']['he-il'] = "לא נמצאו תוצאות";
$text['label-no_results']['it-it'] = "Nessun risultato trovato";
$text['label-no_results']['ka-ge'] = "არ ნახება შედეგები";
$text['label-no_results']['nl-nl'] = "Geen overeenkomsten gevonden";
$text['label-no_results']['pl-pl'] = "Nie znaleziono dopasowań";
$text['label-no_results']['pt-br'] = "Nenhuma correspondência encontrada";
$text['label-no_results']['pt-pt'] = "Não foram encontrados resultados";
$text['label-no_results']['ro-ro'] = "Nu s-au găsit potriviri";
$text['label-no_results']['ru-ru'] = "Совпадений не найдено";
$text['label-no_results']['sv-se'] = "Inga träffar hittades";
$text['label-no_results']['uk-ua'] = "Збігів знайдено не було";
$text['label-no_results']['tr-tr'] = "Eşleşme bulunamadı";
$text['label-no_results']['zh-cn'] = "未找到匹配项";
$text['label-no_results']['ja-jp'] = "一致するものが見つかりません";
$text['label-no_results']['ko-kr'] = "일치하는 항목이 없습니다";

View File

@@ -3988,6 +3988,158 @@ else { //default: white
opacity: 1.0;
}
/* MULTI SELECT DROPDOWN ********************************************************/
.multiselect_container {
position: relative;
width: 200px;
user-select: none;
}
.selected_values {
display: flex;
align-items: center;
flex-wrap: wrap;
cursor: pointer;
font-family: <?=$input_text_font?>;
font-size: <?=$input_text_size?>;
color: <?=$input_text_color?>;
text-align: left;
min-height: <?=$input_height?>;
min-width: <?=$input_width?>;
padding: 4px 6px;
margin: 1px;
border-width: <?=$input_border_size?>;
border-style: <?=$input_border_style?>;
border-color: <?=$input_border_color?>;
outline-width: <?=$input_outline_size?>;
<?php if (!empty($input_outline_style)) { ?>
outline-style: <?=$input_outline_style?>;
<?php } ?>
<?php if (!empty($input_outline_color)) { ?>
outline-color: <?=$input_outline_color?>;
<?php } ?>
background: <?=$input_background_color?>;
<?php
if (!empty($input_shadow_inner_color)) { $shadows[] = $input_shadow_inner_color; }
if (!empty($input_shadow_outer_color)) { $shadows[] = $input_shadow_outer_color; }
if (!empty($shadows)) {
?>
-webkit-box-shadow: <?php echo implode(',', $shadows); ?>;
-moz-box-shadow: <?php echo implode(',', $shadows); ?>;
box-shadow: <?php echo implode(',', $shadows); ?>;
<?php
}
unset($shadows);
?>
<?php $br = format_border_radius($input_border_radius, '3px'); ?>
-moz-border-radius: <?php echo $br['tl']['n'].$br['tl']['u']; ?> <?php echo $br['tr']['n'].$br['tr']['u']; ?> <?php echo $br['br']['n'].$br['br']['u']; ?> <?php echo $br['bl']['n'].$br['bl']['u']; ?>;
-webkit-border-radius: <?php echo $br['tl']['n'].$br['tl']['u']; ?> <?php echo $br['tr']['n'].$br['tr']['u']; ?> <?php echo $br['br']['n'].$br['br']['u']; ?> <?php echo $br['bl']['n'].$br['bl']['u']; ?>;
-khtml-border-radius: <?php echo $br['tl']['n'].$br['tl']['u']; ?> <?php echo $br['tr']['n'].$br['tr']['u']; ?> <?php echo $br['br']['n'].$br['br']['u']; ?> <?php echo $br['bl']['n'].$br['bl']['u']; ?>;
border-radius: <?php echo $br['tl']['n'].$br['tl']['u']; ?> <?php echo $br['tr']['n'].$br['tr']['u']; ?> <?php echo $br['br']['n'].$br['br']['u']; ?> <?php echo $br['bl']['n'].$br['bl']['u']; ?>;
<?php unset($br); ?>
<?php if (!empty($input_outline_radius)) { ?>
outline-radius: <?=$input_outline_radius?>
<?php } ?>
vertical-align: middle;
}
.selected_values:hover {
border-color: <?=$input_border_color_hover?>;
}
.tag {
background: #007bff25;
color: #007bff;
padding: 2px 8px;
margin: 2px;
border-radius: 15px;
font-size: 13px;
display: flex;
align-items: center;
}
.tag span {
margin-left: 5px;
cursor: pointer;
font-weight: bold;
color: #007bff;
}
.tag span:hover {
color: #d32f2f;
}
.placeholder_text {
color: <?=$input_text_placeholder_color?>;
}
.dropdown_list {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: <?=$input_background_color?>;
border-width: <?=$input_border_size?>;
border-style: <?=$input_border_style?>;
border-color: <?=$input_border_color?>;
border-top: none;
border-radius: 0 0 5px 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
z-index: 1000;
max-height: 300px;
overflow-y: auto;
box-sizing: border-box;
margin: 0 1px;
}
.dropdown_list.open {
display: block;
}
.search_box {
background-color: <?=$input_background_color?>;
color: <?=$input_text_color?>;
width: 100%;
padding: 10px;
border: none;
border-bottom: 1px solid #00000010;
box-sizing: border-box;
font-size: 14px;
outline: none;
}
.option_item {
display: flex;
align-items: center;
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #00000010;
margin: 0;
}
.option_item:hover {
background-color: <?=$table_row_background_color_hover?>;
}
.option_item input[type="checkbox"] {
margin-right: 10px;
transform: scale(1.2);
cursor: pointer;
}
.hidden {
display: none !important;
}
.no_results {
display: none;
padding: 15px;
text-align: center;
color: <?=$input_text_placeholder_color?>;
}
<?php
//output custom css

View File

@@ -19,7 +19,7 @@
<link rel='stylesheet' type='text/css' href='{$project_path}/resources/bootstrap/css/bootstrap-tempusdominus.min.css.php'>
<link rel='stylesheet' type='text/css' href='{$project_path}/resources/bootstrap/css/bootstrap-colorpicker.min.css.php'>
<link rel='stylesheet' type='text/css' href='{$project_path}/resources/fontawesome/css/all.min.css.php'>
<link rel='stylesheet' type='text/css' href='{$project_path}/themes/default/css.php?updated=202512160230'>
<link rel='stylesheet' type='text/css' href='{$project_path}/themes/default/css.php?updated=202603110200'>
{*//link to custom css file *}
{if !empty($settings.theme.custom_css)}
<link rel='stylesheet' type='text/css' href='{$settings.theme.custom_css}'>
@@ -734,6 +734,146 @@
}
{/literal}
//multi select box with search
{literal}
const container = document.querySelector('.multiselect_container');
const trigger_btn = container.querySelector('.selected_values');
const dropdown_list = container.querySelector('.dropdown_list');
const search_input = container.querySelector('.search_box');
const options_list = container.querySelector('.options_list');
const no_results = container.querySelector('#no_results');
const placeholder = container.querySelector('.placeholder_text');
let is_open = false;
//toggle Dropdown Open/Close
trigger_btn.addEventListener('click', (event) => {
event.stopPropagation();
is_open = !is_open;
if (is_open) {
dropdown_list.classList.add('open');
search_input.focus();
} else {
dropdown_list.classList.remove('open');
}
});
//close dropdown if clicked outside
document.addEventListener('click', (event) => {
if (!container.contains(event.target)) {
is_open = false;
dropdown_list.classList.remove('open');
}
});
//prevent dropdown from closing when clicking inside the dropdown
dropdown_list.addEventListener('click', (event) => {
event.stopPropagation();
});
//handle Search Filtering
search_input.addEventListener('input', (event) => {
const search_term = event.target.value.toLowerCase();
const option_items = document.querySelectorAll('.option_item');
let visible_count = 0;
option_items.forEach(item => {
const text = item.innerText.toLowerCase();
if (text.includes(search_term)) {
item.classList.remove('hidden');
visible_count++;
} else {
item.classList.add('hidden');
}
});
if (visible_count === 0) {
no_results.style.display = 'block';
} else {
no_results.style.display = 'none';
}
});
//handle Checkbox Selection
container.addEventListener('change', (event) => {
if (event.target.type === 'checkbox') {
update_selected_values();
}
});
//also handle clicking the text part of the option
container.addEventListener('click', (event) => {
if (event.target.classList.contains('option_item')) {
const checkbox = event.target.querySelector('input[type="checkbox"]');
if (checkbox) {
checkbox.checked = !checkbox.checked;
update_selected_values();
}
}
});
//update display Logic (tags & hidden input)
function update_selected_values() {
const checked_boxes = document.querySelectorAll('.option_item input:checked');
const selected_count = checked_boxes.length;
//update visual tags
if (selected_count === 0) {
placeholder.style.display = 'block';
trigger_btn.innerHTML = `<span class="placeholder_text">{/literal}{$text.label_select}{literal}...</span>`;
} else {
placeholder.style.display = 'none';
let html = '';
checked_boxes.forEach(box => {
const label = box.parentElement.innerText;
const clean_label = box.parentElement.textContent.trim();
// Create a hidden input for each selected tag
create_hidden_input_for_tag(clean_label, box.value);
html += `<span class="tag" data-value="${box.value}">`;
html += ` ${clean_label}`;
html += ` <span onclick="remove_option('${box.value}')">&times;</span>`;
html += `</span>`;
});
trigger_btn.innerHTML = html;
}
}
//helper function to remove a tag when clicked (External to scope)
window.remove_option = function(value) {
const checkbox = document.querySelector(`input[value="${value}"]`);
if (checkbox) {
checkbox.checked = false;
// Remove the hidden input corresponding to this tag
const hidden_input = document.querySelector(`input[name="extension_uuids[]"][value="${value}"]`);
if (hidden_input) {
hidden_input.remove();
}
update_selected_values();
}
};
// Function to create a hidden input for each selected tag
function create_hidden_input_for_tag(label, value) {
const existing_hidden_input = document.querySelector(`input[name="extension_uuids[]"][value="${value}"]`);
if (!existing_hidden_input) {
const hidden_input = document.createElement('input');
hidden_input.type = 'hidden';
hidden_input.name = 'extension_uuids[]';
hidden_input.value = value;
container.appendChild(hidden_input);
}
}
//initialize state
update_selected_values();
{/literal}
{literal}
}); //document ready end
{/literal}