Fix Parked title and add contact photo (#7825)

This commit is contained in:
frytimo
2026-03-31 19:42:53 -03:00
committed by GitHub
parent 61c3040ec6
commit 51b161044a
4 changed files with 93 additions and 58 deletions
+28 -26
View File
@@ -431,32 +431,32 @@ $text['label-call_direction']['zh-cn'] = "通话方向";
$text['label-call_direction']['ja-jp'] = "通話方向";
$text['label-call_direction']['ko-kr'] = "통화 방향";
$text['label-parked_calls']['en-us'] = "Parked Calls";
$text['label-parked_calls']['en-gb'] = "Parked Calls";
$text['label-parked_calls']['ar-eg'] = "المكالمات المركونة";
$text['label-parked_calls']['de-at'] = "Geparkte Anrufe";
$text['label-parked_calls']['de-ch'] = "Geparkte Anrufe";
$text['label-parked_calls']['de-de'] = "Geparkte Anrufe";
$text['label-parked_calls']['el-gr'] = "Σταθμευμένες κλήσεις";
$text['label-parked_calls']['es-cl'] = "Llamadas aparcadas";
$text['label-parked_calls']['es-mx'] = "Llamadas aparcadas";
$text['label-parked_calls']['fr-ca'] = "Appels parqués";
$text['label-parked_calls']['fr-fr'] = "Appels parqués";
$text['label-parked_calls']['he-il'] = "שיחות מושהות";
$text['label-parked_calls']['it-it'] = "Chiamate parcheggiate";
$text['label-parked_calls']['ka-ge'] = "დაპარკებული ზარები";
$text['label-parked_calls']['nl-nl'] = "Geparkeerde oproepen";
$text['label-parked_calls']['pl-pl'] = "Zaparkowane połączenia";
$text['label-parked_calls']['pt-br'] = "Chamadas estacionadas";
$text['label-parked_calls']['pt-pt'] = "Chamadas estacionadas";
$text['label-parked_calls']['ro-ro'] = "Apeluri parcate";
$text['label-parked_calls']['ru-ru'] = "Припаркоованные вызовы";
$text['label-parked_calls']['sv-se'] = "Parkerade samtal";
$text['label-parked_calls']['uk-ua'] = "Припарковані дзвінки";
$text['label-parked_calls']['tr-tr'] = "Park edilmiş aramalar";
$text['label-parked_calls']['zh-cn'] = "已停泊通话";
$text['label-parked_calls']['ja-jp'] = "保留中の通話";
$text['label-parked_calls']['ko-kr'] = "주차된 통화";
$text['label-parked_calls']['en-us'] = "Parked";
$text['label-parked_calls']['en-gb'] = "Parked";
$text['label-parked_calls']['ar-eg'] = "مركون";
$text['label-parked_calls']['de-at'] = "Geparkt";
$text['label-parked_calls']['de-ch'] = "Geparkt";
$text['label-parked_calls']['de-de'] = "Geparkt";
$text['label-parked_calls']['el-gr'] = "Παρκαρισμένο";
$text['label-parked_calls']['es-cl'] = "Aparcado";
$text['label-parked_calls']['es-mx'] = "Aparcado";
$text['label-parked_calls']['fr-ca'] = "Parqué";
$text['label-parked_calls']['fr-fr'] = "Parqué";
$text['label-parked_calls']['he-il'] = "מושהה";
$text['label-parked_calls']['it-it'] = "Parcheggiato";
$text['label-parked_calls']['ka-ge'] = "დაპარკებული";
$text['label-parked_calls']['nl-nl'] = "Geparkeerd";
$text['label-parked_calls']['pl-pl'] = "Zaparkowane";
$text['label-parked_calls']['pt-br'] = "Estacionado";
$text['label-parked_calls']['pt-pt'] = "Estacionado";
$text['label-parked_calls']['ro-ro'] = "Parcat";
$text['label-parked_calls']['ru-ru'] = "Припарковано";
$text['label-parked_calls']['sv-se'] = "Parkerat";
$text['label-parked_calls']['uk-ua'] = "Припарковано";
$text['label-parked_calls']['tr-tr'] = "Park edilmiş";
$text['label-parked_calls']['zh-cn'] = "已停泊";
$text['label-parked_calls']['ja-jp'] = "保留中";
$text['label-parked_calls']['ko-kr'] = "주차";
$text['label-no_parked_calls']['en-us'] = "No parked calls";
$text['label-no_parked_calls']['en-gb'] = "No parked calls";
@@ -583,6 +583,8 @@ $text['button-intercept']['en-us'] = "Intercept";
$text['button-intercept']['en-gb'] = "Intercept";
$text['button-call']['en-us'] = "Call";
$text['button-call']['en-gb'] = "Call";
$text['button-call_voicemail']['en-us'] = "Call Voicemail";
$text['button-call_voicemail']['en-gb'] = "Call Voicemail";
$text['button-reject']['en-us'] = "Reject";
$text['button-reject']['en-gb'] = "Reject";
$text['button-hangup_caller']['en-us'] = "Hangup Caller";
+32 -21
View File
@@ -194,6 +194,9 @@
// Default auto-park destination for drag/drop parking
const park_destination = <?= json_encode($park_destination) ?>;
// Session ID used in contact photo URLs for cache control
const contact_image_sid = <?= json_encode(session_id()) ?>;
</script>
<script src="resources/javascript/websocket_client.js?v=<?= $ws_client_hash ?>"></script>
@@ -478,6 +481,14 @@
line-height: 1;
color: inherit;
}
.op-ext-contact-photo {
width: 36px;
height: 36px;
border-radius: 50%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.op-ext-info {
flex: 1;
padding: 5px 8px 5px 8px;
@@ -731,7 +742,7 @@ body.op-dragging, body.op-dragging * {
flex: 1;
}
/* Top row in Extensions tab: My Extensions + Parked Calls */
/* Top row in Extensions tab: My Extensions + Parked */
.op-top-row {
display: flex;
align-items: stretch;
@@ -750,34 +761,34 @@ body.op-dragging, body.op-dragging * {
padding-bottom: 0;
}
/* Parked calls side panel */
/* Parked side panel */
.op-parked-card {
border: 1px solid #d0d8e5;
border-radius: 5px;
background-color: #fff;
box-shadow: 0 1px 3px #d0d8e5;
overflow: hidden;
display: flex;
flex-direction: row;
}
.op-parked-header {
background-color: #e5e9f0;
padding: 8px 4px;
font-size: 12px;
font-weight: 600;
color: #444;
border-right: 1px solid #d0d8e5;
font-family: Calibri, Candara, Segoe, 'Segoe UI', Optima, Arial, sans-serif;
writing-mode: vertical-rl;
text-orientation: mixed;
transform: rotate(180deg);
letter-spacing: .6px;
text-transform: uppercase;
white-space: nowrap;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #e5e9f0;
border-bottom: 1px solid #d0d8e5;
padding: 6px 10px;
font-size: 12px;
font-weight: 700;
color: #444;
text-transform: uppercase;
letter-spacing: .4px;
}
.op-parked-badge {
font-size: 11px;
font-weight: 600;
background: #6c757d;
color: #fff;
padding: 2px 6px;
border-radius: 10px;
justify-content: center;
min-width: 34px;
}
.op-parked-list {
padding: 8px;
@@ -955,7 +966,7 @@ body.op-dragging, body.op-dragging * {
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-parked" data-bs-toggle="tab" data-bs-target="#panel-parked"
type="button" role="tab" aria-controls="panel-parked" aria-selected="false">
<?= htmlspecialchars($text['label-parked_calls'] ?? 'Parked Calls') ?>
<?= htmlspecialchars($text['label-parked_calls'] ?? 'Parked') ?>
<span id="parked_count" class="badge ms-1" style="background:#6c757d;color:#fff;">0</span>
</button>
</li>
@@ -1033,7 +1044,7 @@ body.op-dragging, body.op-dragging * {
</div>
<?php endif; ?>
<!-- PARKED CALLS TAB -->
<!-- PARKED TAB -->
<?php if ($perm['operator_panel_extensions']): ?>
<div class="tab-pane fade" id="panel-parked" role="tabpanel" aria-labelledby="tab-parked">
<div id="parked_filter_bar" class="op-filter-bar" style="display:none;">
@@ -534,14 +534,19 @@ class operator_panel_service extends base_websocket_system_service implements we
$user_status_map = [];
try {
$t_us0 = microtime(true);
$sql = "SELECT e.extension, eu.user_uuid, COALESCE(u.user_status, '') AS user_status "
$sql = "SELECT e.extension, eu.user_uuid, COALESCE(u.user_status, '') AS user_status,"
. " ca.contact_attachment_uuid AS contact_image "
. "FROM v_extensions AS e "
. "LEFT JOIN v_domains AS d ON e.domain_uuid = d.domain_uuid "
. "LEFT JOIN ("
. "SELECT extension_uuid, MIN(user_uuid) AS user_uuid "
. "FROM v_extension_users GROUP BY extension_uuid"
. "SELECT DISTINCT ON (extension_uuid) extension_uuid, user_uuid "
. "FROM v_extension_users ORDER BY extension_uuid"
. ") AS eu ON eu.extension_uuid = e.extension_uuid "
. "LEFT JOIN v_users AS u ON u.user_uuid = eu.user_uuid "
. "LEFT JOIN v_contact_attachments AS ca ON ca.contact_uuid = u.contact_uuid "
. "AND ca.attachment_primary = true "
. "AND ca.attachment_filename IS NOT NULL "
. "AND ca.attachment_content IS NOT NULL "
. "WHERE d.domain_name = :domain_name AND e.enabled = 'true'";
$rows = $database->select($sql, [':domain_name' => $domain_name], 'all');
@@ -557,8 +562,9 @@ class operator_panel_service extends base_websocket_system_service implements we
$ext_num = $row['extension'] ?? '';
if ($ext_num === '') continue;
$user_status_map[$ext_num] = [
'user_uuid' => $row['user_uuid'] ?? null,
'user_status' => $row['user_status'] ?? '',
'user_uuid' => $row['user_uuid'] ?? null,
'user_status' => $row['user_status'] ?? '',
'contact_image' => $row['contact_image'] ?? null,
];
}
@@ -611,6 +617,7 @@ class operator_panel_service extends base_websocket_system_service implements we
$ext['registration_count'] = $registered_map[$ext_num] ?? 0;
$ext['user_uuid'] = $user_status_map[$ext_num]['user_uuid'] ?? null;
$ext['user_status'] = $user_status_map[$ext_num]['user_status'] ?? '';
$ext['contact_image'] = $user_status_map[$ext_num]['contact_image'] ?? null;
}
unset($ext);
$this->debug('extensions_active trace [step6] annotation complete: extensions=' . count($extensions));
@@ -888,14 +888,11 @@ function render_parked_side_card() {
if (!container) return;
const parked_calls = get_sorted_parked_calls();
const title = esc(text['label-parked_calls'] || 'Parked Calls');
const title = esc(text['label-parked_calls'] || 'Parked');
const no_parked = esc(text['label-no_parked_calls'] || 'No parked calls');
let html = `<div class="op-parked-card" ondragover="on_parked_dragover(event)" ondragleave="on_parked_dragleave(event)" ondrop="on_parked_drop(event)">`;
html += `<div class="op-parked-header">`;
html += `<span>${title}</span>`;
html += `<span class="op-parked-badge">${parked_calls.length}</span>`;
html += `</div>`;
html += `<div class="op-parked-header">${title}</div>`;
if (!parked_calls.length) {
html += `<div class="op-parked-empty">${no_parked}</div>`;
@@ -1735,6 +1732,8 @@ function on_ext_contextmenu(event, ext_num) {
const is_mine = !!(block && block.classList.contains('op-ext-mine'));
const { state, call_info } = get_extension_call_state(ext_num);
const has_call = !!uuid;
const ext_data = extensions_map.get(ext_num) || {};
const voicemail_enabled = (ext_data.voicemail_enabled || '') === 'true';
// Derive call direction for the ringing extension to suppress Reject on outbound calls.
const call_dest = ((call_info || {}).caller_destination_number || '').trim();
@@ -1762,6 +1761,11 @@ function on_ext_contextmenu(event, ext_num) {
items.push({ label: text['button-call'] || 'Call', icon_class: 'fa-solid fa-phone',
fn: function () { action_call_extension(ext_num); }
});
if (voicemail_enabled) {
items.push({ label: text['button-call_voicemail'] || 'Call Voicemail', icon_class: 'fa-solid fa-voicemail',
fn: function () { action_call_voicemail(ext_num); }
});
}
}
}
} else if (state === 'ringing') {
@@ -1888,6 +1892,15 @@ function action_call_extension(ext_num) {
send_action('originate', { source: from_ext, destination: ext_num }).catch(console.error);
}
function action_call_voicemail(ext_num) {
if (!Array.isArray(user_own_extensions) || user_own_extensions.length === 0) return;
const from_ext = user_own_extensions.length === 1
? user_own_extensions[0]
: prompt(text['label-your_extension'] || 'Your extension:');
if (!from_ext) return;
send_action('originate', { source: from_ext, destination: '*99' + ext_num }).catch(console.error);
}
function action_intercept_icon(uuid, target_ext) {
if (!uuid) return;
const from_ext = (Array.isArray(user_own_extensions) && user_own_extensions.length === 1)
@@ -2585,7 +2598,9 @@ function render_ext_block(ext, is_mine) {
` ondragleave="event.currentTarget.classList.remove('op-ext-drop-over')"` +
` ondrop="on_ext_drop('${esc(num)}', event)"` +
` oncontextmenu="on_ext_contextmenu(event, '${esc(num)}')">` +
`<div class="op-ext-icon" title="${esc(status_hover)}"><img class="op-ext-status-icon" src="../operator_panel/resources/images/${status_icon}.png" width="28" height="28" alt="${esc(status_hover)}"></div>` +
(ext.contact_image
? `<div class="op-ext-icon" title="${esc(status_hover)}"><div class="op-ext-contact-photo" style="background-image: url('/core/contacts/contact_attachment.php?id=${esc(ext.contact_image)}&action=download&sid=${esc(contact_image_sid)}')"></div></div>`
: `<div class="op-ext-icon" title="${esc(status_hover)}"><img class="op-ext-status-icon" src="../operator_panel/resources/images/${status_icon}.png" width="28" height="28" alt="${esc(status_hover)}"></div>`) +
`<div class="op-ext-info${has_live_call ? ' op-has-live-call' : ''}">` +
`<div class="op-ext-number">${esc(num)}</div>` +
dialpad_html +