diff --git a/app/operator_panel/app_config.php b/app/operator_panel/app_config.php index 89a1dd28e..816f64c57 100644 --- a/app/operator_panel/app_config.php +++ b/app/operator_panel/app_config.php @@ -56,19 +56,48 @@ $apps[$x]['description']['sv-se'] = ""; $apps[$x]['description']['uk-ua'] = ""; -// Permissions are inherited from the basic_operator_panel app (uuid: dd3d173a-5d51-4231-ab22-b18c5b712bb2). -// The following permissions are reused: -// operator_panel_view - view the panel -// operator_panel_manage - supervisor: transfer, agent management -// operator_panel_originate - originate calls via drag-and-drop -// operator_panel_eavesdrop - listen to calls -// operator_panel_hangup - disconnect calls -// operator_panel_record - record calls -// operator_panel_call_details - view caller/callee details -// operator_panel_on_demand - on-demand availability status +// Core permissions (migrated from basic_operator_panel uuid: dd3d173a-5d51-4231-ab22-b18c5b712bb2) + $y = 0; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_view"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_manage"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_originate"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_hangup"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_eavesdrop"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_coach"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_record"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_call_details"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_on_demand"; + $y++; + $apps[$x]['permissions'][$y]['name'] = "operator_panel_transfer_attended"; + $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; + $apps[$x]['permissions'][$y]['groups'][] = "admin"; + $y++; // Tab visibility permissions - $y = 0; $apps[$x]['permissions'][$y]['name'] = "operator_panel_extensions"; $apps[$x]['permissions'][$y]['groups'][] = "superadmin"; $apps[$x]['permissions'][$y]['groups'][] = "admin"; diff --git a/app/operator_panel/app_languages.php b/app/operator_panel/app_languages.php index ba54fc964..46e16a086 100644 --- a/app/operator_panel/app_languages.php +++ b/app/operator_panel/app_languages.php @@ -485,6 +485,15 @@ $text['label-no_parked_calls']['zh-cn'] = "没有已停泊通话"; $text['label-no_parked_calls']['ja-jp'] = "保留中の通話はありません"; $text['label-no_parked_calls']['ko-kr'] = "주차된 통화 없음"; +$text['label-parking_lot']['en-us'] = "Parking Lot"; +$text['label-parking_lot']['en-gb'] = "Parking Lot"; + +$text['label-parked_by']['en-us'] = "Parked By"; +$text['label-parked_by']['en-gb'] = "Parked By"; + +$text['label-original_destination']['en-us'] = "Original Destination"; +$text['label-original_destination']['en-gb'] = "Original Destination"; + $text['label-recording']['en-us'] = "Recording"; $text['label-recording']['en-gb'] = "Recording"; $text['label-recording']['ar-eg'] = "تسجيل"; @@ -566,6 +575,45 @@ $text['label-eavesdrop']['zh-cn'] = "监听"; $text['label-eavesdrop']['ja-jp'] = "聞く"; $text['label-eavesdrop']['ko-kr'] = "듣기"; +$text['label-choose_action']['en-us'] = "Choose Action"; +$text['label-choose_action']['en-gb'] = "Choose Action"; +$text['label-ringing_action_desc']['en-us'] = "Extension {target} is ringing. What would you like to do from {source}?"; +$text['label-ringing_action_desc']['en-gb'] = "Extension {target} is ringing. What would you like to do from {source}?"; +$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-reject']['en-us'] = "Reject"; +$text['button-reject']['en-gb'] = "Reject"; +$text['button-hangup_caller']['en-us'] = "Hangup Caller"; +$text['button-hangup_caller']['en-gb'] = "Hangup Caller"; +$text['label-confirm_hangup_caller']['en-us'] = "Hang up the caller?"; +$text['label-confirm_hangup_caller']['en-gb'] = "Hang up the caller?"; + +$text['label-transfer_mode']['en-us'] = "Transfer"; +$text['label-transfer_mode']['en-gb'] = "Transfer"; +$text['label-transfer_mode_title']['en-us'] = "Default transfer type used when transferring calls"; +$text['label-transfer_mode_title']['en-gb'] = "Default transfer type used when transferring calls"; +$text['label-blind_transfer']['en-us'] = "Blind"; +$text['label-blind_transfer']['en-gb'] = "Blind"; +$text['label-blind_transfer_title']['en-us'] = "Blind transfer: immediately connect the call to the destination"; +$text['label-blind_transfer_title']['en-gb'] = "Blind transfer: immediately connect the call to the destination"; +$text['label-attended_transfer']['en-us'] = "Conference"; +$text['label-attended_transfer']['en-gb'] = "Conference"; +$text['label-attended_transfer_title']['en-us'] = "Conference transfer: destination is called while the caller is still connected; also called a warm transfer"; +$text['label-attended_transfer_title']['en-gb'] = "Conference transfer: destination is called while the caller is still connected; also called a warm transfer"; + +$text['label-consulting_with']['en-us'] = "Consulting with"; +$text['label-consulting_with']['en-gb'] = "Consulting with"; +$text['message-transfer_completed']['en-us'] = "Transfer completed"; +$text['message-transfer_completed']['en-gb'] = "Transfer completed"; +$text['message-transfer_cancelled']['en-us'] = "Transfer cancelled"; +$text['message-transfer_cancelled']['en-gb'] = "Transfer cancelled"; +$text['button-complete_transfer']['en-us'] = "Complete Transfer"; +$text['button-complete_transfer']['en-gb'] = "Complete Transfer"; +$text['button-cancel_transfer']['en-us'] = "Cancel"; +$text['button-cancel_transfer']['en-gb'] = "Cancel"; + $text['label-hangup']['en-us'] = "Hangup"; $text['label-hangup']['en-gb'] = "Hangup"; $text['label-hangup']['ar-eg'] = "إنهاء"; @@ -1756,6 +1804,9 @@ $text['message-action_failed']['zh-cn'] = "操作失败"; $text['message-action_failed']['ja-jp'] = "操作に失敗しました"; $text['message-action_failed']['ko-kr'] = "작업에 실패했습니다"; +$text['label-dial_number']['en-us'] = "Dial a Number"; +$text['label-dial_number']['en-gb'] = "Dial a Number"; + $text['message-permission_denied']['en-us'] = "Permission denied"; $text['message-permission_denied']['en-gb'] = "Permission denied"; $text['message-permission_denied']['ar-eg'] = "تم رفض الإذن"; diff --git a/app/operator_panel/index.php b/app/operator_panel/index.php index 9579a776f..490d7e245 100644 --- a/app/operator_panel/index.php +++ b/app/operator_panel/index.php @@ -43,6 +43,11 @@ $token = (new token())->create($_SERVER['PHP_SELF']); subscriber::save_token($token, ['active.operator.panel']); +// Get the status for the current user + $sql = 'select user_status from v_users where user_uuid = :user_uuid'; + $parameters = ['user_uuid' => $_SESSION['user_uuid'] ?? '']; + $user_status = $database->select($sql, $parameters, 'column') ?? ''; + // Gather user permissions for the JS side $perm = [ 'operator_panel_view' => permission_exists('operator_panel_view'), @@ -51,6 +56,10 @@ 'operator_panel_eavesdrop' => permission_exists('operator_panel_eavesdrop'), 'operator_panel_record' => permission_exists('operator_panel_record'), 'operator_panel_originate' => permission_exists('operator_panel_originate'), + 'operator_panel_coach' => permission_exists('operator_panel_coach'), + 'operator_panel_call_details' => permission_exists('operator_panel_call_details'), + 'operator_panel_on_demand' => permission_exists('operator_panel_on_demand'), + 'operator_panel_transfer_attended' => permission_exists('operator_panel_transfer_attended'), 'operator_panel_extensions' => permission_exists('operator_panel_extensions'), 'operator_panel_calls' => permission_exists('operator_panel_calls'), 'operator_panel_conferences' => permission_exists('operator_panel_conferences'), @@ -108,6 +117,12 @@ // Optional polling reconciliation of registration state (can be disabled). $registrations_reconcile_enabled = $settings->get('operator_panel', 'registrations_reconcile_enabled', 'false') === 'true'; +// Default auto-park destination for drag/drop parking. + $park_destination = (string)$settings->get('operator_panel', 'park_destination', '*5900'); + if (!preg_match('/^[0-9*#+]+$/', $park_destination)) { + $park_destination = '*5900'; + } + // Get the logged-in user's own extension numbers (shown at top of Extensions panel) // and primary eavesdrop destination extension $user_own_extensions = []; @@ -162,7 +177,7 @@ const user_uuid = ; // User status options - const user_statuses = ; + const user_status = ; // The logged-in user's own extension numbers — shown first / highlighted in the Extensions panel const user_own_extensions = ; @@ -176,6 +191,9 @@ // Optional registrations-state reconciliation polling const registrations_reconcile_enabled = ; + // Default auto-park destination for drag/drop parking + const park_destination = ; + @@ -318,6 +336,116 @@ } .op-edit-btn:hover { background: #e9ecef; } .op-edit-btn.active { background: #0d6efd; color: #fff; border-color: #0d6efd; } +/* Transfer mode toggle */ +.op-transfer-mode { + display: inline-flex; + align-items: center; + gap: 5px; + margin-left: auto; + font-size: 12px; + color: #555; + white-space: nowrap; +} +.op-transfer-mode-label { + font-weight: 600; + color: #444; +} +.op-transfer-mode-btn { + padding: 2px 10px; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + font-size: 11px; + font-weight: 600; + cursor: pointer; + color: #555; + transition: background .15s, color .15s, border-color .15s; +} +.op-transfer-mode-btn:hover { background: #e9ecef; } +.op-transfer-mode-btn.active { background: #0d6efd; color: #fff; border-color: #0d6efd; } +/* Context menu */ +.op-ctx-menu { + position: fixed; + z-index: 9990; + background: #fff; + border: 1px solid #d0d7de; + border-radius: 6px; + box-shadow: 0 8px 24px rgba(0,0,0,.18); + min-width: 165px; + padding: 4px 0; + display: none; + margin: 0; +} +.op-ctx-header { + font-size: 11px; + color: #888; + padding: 3px 14px 2px; + text-transform: uppercase; + letter-spacing: .5px; + font-weight: 600; + user-select: none; +} +.op-ctx-separator { + height: 1px; + background: #e0e0e0; + margin: 4px 0; +} +.op-ctx-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 14px; + font-size: 13px; + cursor: pointer; + color: #24292f; + white-space: nowrap; + border: none; + background: none; + width: 100%; + text-align: left; + line-height: 1.3; +} +.op-ctx-item:hover { background: #f0f6ff; } +.op-ctx-item .op-ctx-icon { font-size: 12px; opacity: .7; flex-shrink: 0; } +.op-ctx-danger { color: #c9242d; } +.op-ctx-danger:hover { background: #fff1f0; } +/* Attended transfer consultation bar */ +.op-att-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 9980; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 20px; + background: #198754; + color: #fff; + font-size: 14px; + font-weight: 600; + box-shadow: 0 -2px 8px rgba(0,0,0,.2); +} +.op-att-icon { font-size: 18px; } +.op-att-label { flex: 1; } +.op-att-btn { + padding: 6px 16px; + border: none; + border-radius: 4px; + font-size: 13px; + font-weight: 600; + cursor: pointer; +} +.op-att-complete { + background: #fff; + color: #198754; +} +.op-att-complete:hover { background: #e9ecef; } +.op-att-cancel { + background: rgba(255,255,255,.2); + color: #fff; +} +.op-att-cancel:hover { background: rgba(255,255,255,.35); } .op-ext-block { display: flex; width: 235px; @@ -602,6 +730,174 @@ body.op-dragging, body.op-dragging * { padding: 10px 8px 4px; flex: 1; } + +/* Top row in Extensions tab: My Extensions + Parked Calls */ +.op-top-row { + display: flex; + align-items: stretch; + gap: 14px; + flex-wrap: wrap; + margin-bottom: 14px; +} +#my_extensions_container, +#parked_side_container { + flex: 1 1 420px; + min-width: 320px; +} +#my_extensions_container:not(:empty), +#parked_side_container:not(:empty) { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Parked calls side panel */ +.op-parked-card { + border: 1px solid #d0d8e5; + border-radius: 5px; + background-color: #fff; + box-shadow: 0 1px 3px #d0d8e5; + overflow: hidden; +} +.op-parked-header { + 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; +} +.op-parked-list { + padding: 8px; + max-height: 280px; + overflow: auto; +} +.op-parked-item { + border: 1px solid #d0d8e5; + border-radius: 4px; + background: #f8fbff; + padding: 6px 8px; + margin-bottom: 7px; + cursor: grab; + user-select: none; + line-height: 1.25; +} +.op-parked-item:last-child { margin-bottom: 0; } +.op-parked-item:hover { background: #eef5ff; border-color: #80bdff; } +.op-parked-main { font-size: 12px; font-weight: 600; color: #2c3e50; } +.op-parked-sub { font-size: 11px; color: #555; margin-top: 2px; } +.op-parked-drop-over { + box-shadow: 0 0 0 3px #0d6efd; + border-color: #0d6efd; +} +.op-parked-empty { + padding: 10px; + font-size: 12px; + color: #6c757d; + font-style: italic; +} + +@media (max-width: 980px) { + #my_extensions_container, + #parked_side_container { + flex-basis: 100%; + min-width: 0; + } +} + +/* HTML5 dialog styles */ +.op-dialog { + border: 1px solid #ccc; + border-radius: 8px; + padding: 0; + max-width: 400px; + width: 90%; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} +.op-dialog::backdrop { + background: rgba(0,0,0,0.4); +} +.op-dialog-sm { + max-width: 320px; +} +.op-dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #ddd; +} +.op-dialog-header h5 { + margin: 0; + font-size: 1.1rem; +} +.op-dialog-close { + background: none; + border: none; + font-size: 1.4rem; + cursor: pointer; + line-height: 1; + padding: 0 4px; + opacity: 0.6; +} +.op-dialog-close:hover { opacity: 1; } +.op-dialog-body { + padding: 16px; +} +.op-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 12px 16px; + border-top: 1px solid #ddd; +} +.op-dialog-input { + width: 100%; + padding: 6px 10px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 0.95rem; + box-sizing: border-box; +} +.op-dialog-input:focus { + outline: 2px solid #0d6efd; + outline-offset: -1px; +} +.op-dialog-actions { + display: flex; + flex-direction: column; + gap: 8px; +} +.op-dialog-btn { + display: block; + width: 100%; + padding: 8px 16px; + border: none; + border-radius: 4px; + font-size: 0.95rem; + cursor: pointer; + color: #fff; +} +.op-btn-primary { background: #0d6efd; } +.op-btn-primary:hover { background: #0b5ed7; } +.op-btn-success { background: #198754; } +.op-btn-success:hover { background: #157347; } +.op-btn-info { background: #0dcaf0; color: #000; } +.op-btn-info:hover { background: #31d2f2; } +.op-btn-secondary { background: #6c757d; } +.op-btn-secondary:hover { background: #5c636a; } @@ -624,6 +920,15 @@ body.op-dragging, body.op-dragging * { + + +