MERGE_UAT_HR_CD_MAIL_NOTIFY

This commit is contained in:
Ubuntu 2026-06-04 09:50:45 +05:30
commit a489a8aa2b
8 changed files with 650 additions and 107 deletions

View File

@ -1032,8 +1032,9 @@ class EmpDataServiceController extends BaseController
return false;
}
//for cd tranction and cd master data is empty check
if(!empty($inceptionData) && $inceptionData != null){
if(!empty($inceptionData) && $inceptionData != null || !empty($additionData) && $additionData != null || !empty($dependentAdditionData) && $dependentAdditionData != null){
$policy_details = $this->clientPolicyModel->where('id', $export_data['client_policy_id'])->first();
@ -1065,6 +1066,14 @@ class EmpDataServiceController extends BaseController
$totals = $totals + $item->total;
}
foreach ($additionData as $item) {
$totals = $totals + $item->total;
}
foreach ($dependentAdditionData as $item) {
$totals = $totals + $item->total;
}
$totals = round($totals, 2);
@ -1127,8 +1136,7 @@ class EmpDataServiceController extends BaseController
AND `insurer_excel_export_template`.`type_name` = :type_name:
LIMIT 1";
$binds = ['client_policy_id' => (int)$export_data['client_policy_id'],
'type_name' => $export_data['actions']];
$binds = ['client_policy_id' => (int)$export_data['client_policy_id'],'type_name' => $export_data['actions']];
$query = db_connect()->query($sql,$binds);
$template_json = $query->getRowArray();

View File

@ -2028,6 +2028,35 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s
ORDER BY l.created_at DESC
", [$uid, $fyStart, $fyEnd])->getResultArray();
// ── Lost Leads for this user in FY (Opportunities tab — toggle off) ──
$lossLeads = $db->query("
SELECT
l.id AS opportunities_id,
l.actual_lead_id,
COALESCE(l.lead_form_type, 1) AS lead_form_type_id,
l.lead_type AS lead_type_id,
sal.company_name AS company,
l.client_name AS client_name,
CASE WHEN COALESCE(l.lead_form_type, 1) = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_form_type,
CASE
WHEN l.lead_type = 1 THEN 'Fresh'
WHEN l.lead_type = 2 THEN 'Renewal'
WHEN l.lead_type = 3 THEN 'Roll Over'
ELSE ''
END AS lead_type,
l.created_at AS created_at,
l.status,
l.lost_reason AS lost_reason
FROM leads l
INNER JOIN sales_actual_leads sal
ON sal.lead_id = l.actual_lead_id
WHERE l.status = 'lost'
AND sal.assigned_to = ?
AND l.created_at >= ?
AND l.created_at <= ?
ORDER BY l.created_at DESC
", [$uid, $fyStart, $fyEnd])->getResultArray();
// -- NOTE: AND TRIM(sal.company_name) = TRIM(l.client_name)
$result[$uid] = [
'total_policies' => $totalPolicies,
@ -2035,6 +2064,7 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s
'target_amt' => $targetAmt,
'policies' => $policies,
'won_leads' => $wonLeads, // for Table 2 in modal Tab 2
'loss_leads' => $lossLeads,
];
}

View File

@ -46,6 +46,11 @@ class ClaimsCollectionV2DashboardModel extends Model
229 => 'cataract_avg_exceeded_amount',
230 => 'hospital_city_wise_si_limit_cataract',
231 => 'total_incurred_by_cliam_status',
232 => 'inception_emp_lives',
233 => 'current_emp_lives',
234 => 'claim_value_by_month',
235 => 'claim_amount_by_relationship',
236 => 'top_10_ailments_by_claim_count',
];
/**
@ -83,6 +88,11 @@ class ClaimsCollectionV2DashboardModel extends Model
'cataract_avg_exceeded_amount' => 'Cataract avg exceeded amount',
'hospital_city_wise_si_limit_cataract' => 'Hospital city wise SI Limit - Cataract',
'total_incurred_by_cliam_status' => 'Total Incurred by Cliam Status ',
'inception_emp_lives' => 'Inception Employees & Lives',
'current_emp_lives' => 'Current Employees & Lives',
'claim_value_by_month' => 'Claim Value by Month',
'claim_amount_by_relationship' => 'Claim Amount by Relationship',
'top_10_ailments_by_claim_count' => 'Top 10 Ailments by Claim Count',
];
protected function runKpiQuery(string $sql, int $policyId): array
@ -2083,6 +2093,199 @@ WHERE tm.client_policy_id = :policy_id:
AND tm.is_active = 1
GROUP BY tcs.display_name, totals.total_count, totals.total_value
-- ORDER BY claim_count DESC;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #232: Inception Employees & Lives */
public function inception_emp_lives(int $policyId): array
{
$sql = <<<'SQL'
SELECT
COUNT(DISTINCT CASE
WHEN e.relationship = 'Self' THEN e.id
END) AS inception_emp,
COUNT(DISTINCT e.id) AS inception_lives
FROM employee_polices ep
JOIN employees e ON e.id = ep.employee_id
WHERE ep.client_policy_id = :policy_id:
AND LOWER(e.change_event) LIKE '%inception%'
AND ep.is_active = 1
AND e.is_active = 1;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #233: Current Employees & Lives */
public function current_emp_lives(int $policyId): array
{
$sql = <<<'SQL'
SELECT
COUNT(DISTINCT CASE
WHEN e.relationship = 'Self' THEN e.id
END) AS current_emp,
COUNT(DISTINCT e.id) AS current_lives,
ROUND(
COUNT(DISTINCT e.id)
/ NULLIF(COUNT(DISTINCT CASE
WHEN e.relationship = 'Self' THEN e.id
END), 0)
, 2) AS avg_family_size
FROM employee_polices ep
JOIN employees e ON e.id = ep.employee_id
WHERE ep.client_policy_id = :policy_id:
AND ep.status = 'active'
AND ep.is_active = 1
AND e.emp_status = 'active'
AND e.is_active = 1;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #234: Claim Value by Month */
public function claim_value_by_month(int $policyId): array
{
$sql = <<<'SQL'
SELECT
DATE_FORMAT(tm.created_at, '%b %Y') AS claim_month,
DATE_FORMAT(tm.created_at, '%Y-%m') AS month_sort,
-- COUNT(tm.id) AS claim_count,
-- CONCAT(ROUND(COUNT(tm.id) / totals.total_count * 100, 2), '%') AS count_pct,
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
AS claim_value
-- CONCAT(ROUND(
-- COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
-- / NULLIF(totals.total_value, 0) * 100
-- , 2), '%') AS value_pct
FROM ticket_master tm
JOIN (
SELECT
COUNT(id) AS total_count,
COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
FROM ticket_master
WHERE client_policy_id = :policy_id: AND is_active = 1
) totals ON 1=1
WHERE tm.client_policy_id = :policy_id:
AND tm.is_active = 1
GROUP BY
DATE_FORMAT(tm.created_at, '%b %Y'),
DATE_FORMAT(tm.created_at, '%Y-%m'),
totals.total_count,
totals.total_value
ORDER BY month_sort;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #235: Claim Amount by Relationship */
public function claim_amount_by_relationship(int $policyId): array
{
$sql = <<<'SQL'
SELECT
COALESCE(tm.relationship, 'Unknown') AS relationship,
COUNT(tm.id) AS claim_count,
ROUND(COUNT(tm.id) / NULLIF(totals.total_count, 0) * 100, 2)
AS count_pct,
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0) AS claim_value,
ROUND(
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
/ NULLIF(totals.total_value, 0) * 100,
2)
AS value_pct
FROM ticket_master tm
JOIN (
SELECT
COUNT(id) AS total_count,
COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
FROM ticket_master
WHERE client_policy_id = :policy_id: AND is_active = 1
) totals ON 1=1
WHERE tm.client_policy_id = :policy_id:
AND tm.is_active = 1
GROUP BY tm.relationship
ORDER BY claim_count DESC;
-- SELECT
-- COALESCE(
-- CONCAT(UPPER(SUBSTRING(tm.relationship, 1, 1)), LOWER(SUBSTRING(tm.relationship, 2))),
-- 'Unknown'
-- ) AS relationship,
-- COUNT(tm.id) AS claim_count,
-- ROUND(COUNT(tm.id) / NULLIF(totals.total_count, 0) * 100, 2) AS count_pct,
-- COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0) AS claim_value,
-- ROUND(
-- COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
-- / NULLIF(totals.total_value, 0) * 100,
-- 2) AS value_pct
-- FROM ticket_master tm
-- JOIN (
-- SELECT
-- COUNT(id) AS total_count,
-- COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
-- FROM ticket_master
-- WHERE client_policy_id = :policy_id: AND is_active = 1
-- ) totals ON 1=1
-- WHERE tm.client_policy_id = :policy_id:
-- AND tm.is_active = 1
-- GROUP BY tm.relationship
-- ORDER BY
-- CASE LOWER(tm.relationship)
-- WHEN 'self' THEN 1
-- WHEN 'spouse' THEN 2
-- WHEN 'son' THEN 3
-- WHEN 'daughter' THEN 4
-- WHEN 'father' THEN 5
-- WHEN 'mother' THEN 6
-- WHEN 'father-in-law' THEN 7
-- WHEN 'mother-in-law' THEN 8
-- WHEN 'unknown' THEN 9
-- ELSE 10
-- END;
SQL;
return $this->runKpiQuery($sql, $policyId);
}
/** Metabase #236: Top 10 Ailments by Claim Count */
public function top_10_ailments_by_claim_count(int $policyId): array
{
$sql = <<<'SQL'
SELECT
COALESCE(tm.tpa_ailments, 'Not Specified') AS ailment,
COUNT(tm.id) AS claim_count,
CONCAT(ROUND(COUNT(tm.id) / totals.total_count * 100, 2), '%')
AS count_pct,
FORMAT(
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
, 0) AS claim_value,
CONCAT(ROUND(
COALESCE(SUM(CAST(NULLIF(tm.claim_amount, '') AS DECIMAL(15,2))), 0)
/ NULLIF(totals.total_value, 0) * 100
, 2), '%') AS value_pct
FROM ticket_master tm
JOIN (
SELECT
COUNT(id) AS total_count,
COALESCE(SUM(CAST(NULLIF(claim_amount, '') AS DECIMAL(15,2))), 0) AS total_value
FROM ticket_master
WHERE client_policy_id = :policy_id: AND is_active = 1
) totals ON 1=1
WHERE tm.client_policy_id = :policy_id:
AND tm.is_active = 1
AND tm.tpa_ailments IS NOT NULL
GROUP BY tm.tpa_ailments, totals.total_count, totals.total_value
ORDER BY claim_count DESC
LIMIT 10;
SQL;
return $this->runKpiQuery($sql, $policyId);
}

View File

@ -14,7 +14,8 @@
text-overflow: ellipsis;
}
.select2-hidden-accessible + .select2-container .select2-dropdown {
/* Hide dropdown list only for the insurer copy select (search-only UX) */
#insurer.select2-hidden-accessible + .select2-container .select2-dropdown {
display: none !important;
}
@ -53,6 +54,14 @@
z-index: 1060;
}
#template_modal .select2-container {
z-index: 1061;
}
#template_modal .select2-dropdown {
z-index: 1062;
}
#template_modal.show {
display: flex !important;
align-items: center;
@ -276,6 +285,44 @@
<script>
var insurerTemplateDataTable = null;
var templateDebugPrefix = '[InsurerTemplate]';
var insurerDbColumnOptions = <?= json_encode($db_column_name ?? [], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;
function buildDbColumnSelectOptions() {
var html = '<option value="" selected>Select</option>';
if (!insurerDbColumnOptions || typeof insurerDbColumnOptions !== 'object') {
return html;
}
Object.keys(insurerDbColumnOptions).forEach(function (label) {
var value = insurerDbColumnOptions[label];
html += '<option value="' + String(value).replace(/"/g, '&quot;') + '">' + String(label).replace(/</g, '&lt;') + '</option>';
});
return html;
}
function initDbColumnSelect2($select) {
if (!$select || !$select.length || typeof $.fn.select2 !== 'function') {
return;
}
if ($select.hasClass('select2-hidden-accessible')) {
$select.select2('destroy');
}
$select.select2({
placeholder: 'Select',
allowClear: true,
width: '100%',
dropdownParent: $('#template_modal')
});
}
function destroyDbColumnSelect2InContainer(container) {
var $container = container ? $(container) : $('#dynamic-form-container');
$container.find('.db-column-name-select').each(function () {
var $el = $(this);
if ($el.hasClass('select2-hidden-accessible')) {
$el.select2('destroy');
}
});
}
function showTemplateModal() {
var $modal = $('#template_modal');
@ -662,6 +709,7 @@
input.preventDefault();
}
$('#myCenterModalLabel').text('Create Template');
destroyDbColumnSelect2InContainer();
$('#dynamic-form-container').empty();
addHTMLInput();
showTemplateModal();
@ -672,6 +720,10 @@
const container = document.getElementById('dynamic-form-container');
const newRow = document.createElement('div');
newRow.className = 'form-row dynamic-form-row';
const disableDbSelect = data !== null && data !== undefined && data !== '' && data.default_value != "" && data.default_value != null;
const headerValue = data !== null && data !== undefined && data !== '' ? (data.column_name || '') : '';
const defaultValue = data != null && data != undefined && data != '' && data.default_value != "" && data.default_value != null ? data.default_value : '';
newRow.innerHTML = `
<div class="mt-3 p-3" style="width: 100%; background: #F7F7F7;
border-radius: 30px !important;
@ -681,24 +733,19 @@
">
<div class="form-group col-md-3">
<label for="db_column_name">DataBase Column Name<span class="text-danger"></span></label>
<select class="form-control db-column-name-select dbColumn" name="db_column_name[]" onchange="checkForDuplicates(this); handleDbColumnChange(this)" ${data !== null && data !== undefined && data !== '' && data.default_value != "" && data.default_value != null ? 'disabled' : ''}>
<option value="" selected >Select</option>
<?php if (!empty($db_column_name)){ ?>
<?php foreach ($db_column_name as $key => $value) { ?>
<option value="<?= $value ?>"><?= $key ?></option>
<?php } ?>
<?php } ?>
<select class="form-control db-column-name-select" name="db_column_name[]" onchange="checkForDuplicates(this); handleDbColumnChange(this)"${disableDbSelect ? ' disabled' : ''}>
${buildDbColumnSelectOptions()}
</select>
</div>
<div class="form-group col-md-4">
<label for="excel_column_name">Header Name<span class="text-danger">*</span></label>
<input id="excel_column_name" value="${data !== null && data !== undefined && data !== '' ? data.column_name : ''}" class="form-control" type="text" name="excel_column_name[]" placeholder="Excel Header Column Name">
<input id="excel_column_name" value="${headerValue}" class="form-control" type="text" name="excel_column_name[]" placeholder="Excel Header Column Name">
</div>
<div class="form-group col-md-3">
<label for="default_value">Default Value<span class="text-danger"></span></label>
<input id="default_value" value="${data != null && data != undefined && data != '' && data.default_value != "" && data.default_value != null ? data.default_value : ''}" class="form-control" type="text" name="default_value[]" placeholder="Excel Default value">
<input id="default_value" value="${defaultValue}" class="form-control" type="text" name="default_value[]" placeholder="Excel Default value">
</div>
<div class="form-group col-md-2" style="position: relative;left: 28px;">
@ -708,13 +755,14 @@
</div>
`;
container.appendChild(newRow);
var $dbSelect = $(newRow).find('.db-column-name-select');
if (data != null && data.db_column_name != undefined) {
const selectElement = newRow.querySelector('.db-column-name-select');
selectElement.value = data.db_column_name;
$('.dbColumn').select2();
$dbSelect.val(data.db_column_name);
}
initDbColumnSelect2($dbSelect);
if (data != null && data.db_column_name != undefined) {
$dbSelect.trigger('change');
}
$('.dbColumn').select2();
}
// function removeHTMLInput(element)
@ -731,6 +779,12 @@
if (rows.length > 1) {
const row = element.closest('.dynamic-form-row');
$(row).find('.db-column-name-select').each(function () {
var $el = $(this);
if ($el.hasClass('select2-hidden-accessible')) {
$el.select2('destroy');
}
});
row.remove();
}
}
@ -813,6 +867,7 @@
var data = JSON.parse(res.data.jsoncolumns)
// console.log(JSON.parse(res.data.jsoncolumns));
destroyDbColumnSelect2InContainer();
$('#dynamic-form-container').empty();
$.each(data, function(index, item) {
@ -855,6 +910,7 @@
$('.close').click(function()
{
destroyDbColumnSelect2InContainer();
$('#dynamic-form-container').empty();
$('#policy_type').val('');
$('#event').val('').change();
@ -926,9 +982,9 @@
let $dbSelect = $row.find('select[name="db_column_name[]"]');
if ($defaultInput.val().trim() !== '') {
$dbSelect.val('').trigger('change').prop('disabled', true);
$dbSelect.val(null).trigger('change').prop('disabled', true);
} else {
$dbSelect.prop('disabled', false);
$dbSelect.prop('disabled', false).trigger('change');
}
});

View File

@ -1256,11 +1256,20 @@
// Start the interval
function startInterval() {
if (!submitInterval) {
messageShown = false; // Reset message flag
messageShown = false;
submitInterval = setInterval(function() {
checkCDBalance();
}, 2000);
console.log("Checking CD balance started...");
// Stop after 15 seconds
setTimeout(function() {
clearInterval(submitInterval);
submitInterval = null;
console.log("Checking CD balance stopped after 15 seconds.");
}, 15000);
}
}

View File

@ -230,6 +230,17 @@
.opp-card { background: #f8fafd; border-radius: 12px; padding: 16px; border: 1px solid #e4eaf5; text-align: center; }
.opp-card-val { font-size: 22px; font-weight: 800; color: #0f172a; }
.opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; }
.opp-section-head { display: flex; justify-content: space-between; align-items: center; margin: 24px 0 12px; gap: 12px; flex-wrap: wrap; }
.opp-section-head .split-section-title { margin: 0; }
.opp-toggle-wrap { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.opp-toggle-lbl { font-size: 12px; font-weight: 700; color: #94a3b8; transition: color .2s; }
.opp-toggle-lbl.active { color: #0f172a; }
.opp-toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; }
.opp-toggle-switch input { opacity: 0; width: 0; height: 0; }
.opp-toggle-slider { position: absolute; cursor: pointer; inset: 0; background: #cbd5e1; border-radius: 99px; transition: background .2s; }
.opp-toggle-slider::before { content: ""; position: absolute; height: 18px; width: 18px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: transform .2s; box-shadow: 0 1px 3px rgba(15,23,42,0.15); }
.opp-toggle-switch input:checked + .opp-toggle-slider { background: #4f46e5; }
.opp-toggle-switch input:checked + .opp-toggle-slider::before { transform: translateX(20px); }
@media(max-width:900px) { .list-col-head,.achieve-row { grid-template-columns: 220px 1fr 110px 110px 110px; } .col-lbl:nth-child(6),.col-lbl:nth-child(7),.achieve-row>*:nth-child(6),.achieve-row>*:nth-child(7) { display: none; } }
@media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .target-edit-panel { grid-template-columns: 1fr; } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
@ -502,7 +513,9 @@ const TEAM = <?php echo json_encode($team_achievement ?? []); ?>;
const OPP_DATA = <?php echo json_encode($opp_achievement ?? []); ?>;
/* OPP_DATA shape per userId:
{ total_policies, total_exp_amt, target_amt,
policies: [{ policy_no, issue_date, amount, created_at }] } */
policies: [{ policy_no, issue_date, amount, created_at }],
won_leads: [...], loss_leads: [...] } */
let currentModalOppUserId = null;
/* ── JS Helpers ── */
const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'};
@ -545,6 +558,89 @@ function redirectToOpportunity(leadTypeId, actualLeadId, opportunityId) {
+ encodeURIComponent(opportunityId);
}
function escOppCell(text) {
return String(text ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function updateOppTableHeader(showWon) {
const headRow = document.getElementById('oppTableHead');
if (!headRow) return;
if (showWon) {
headRow.innerHTML =
'<th>Company</th>'
+ '<th>Opportunity Type</th>'
+ '<th>Date</th>';
} else {
headRow.innerHTML =
'<th>Company</th>'
+ '<th>Opportunity Type</th>'
+ '<th>Lost Reason</th>'
+ '<th>Date</th>';
}
}
function buildOpportunityTableRows(leads, showWon) {
return (leads || []).map(function(l) {
const opportunityType = [
l.lead_form_type || '',
].filter(Boolean).join('').toUpperCase();
const typeBadgeClass = l.lead_form_type === 'EB'
? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'
: 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"';
const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id;
const rowAttrs = canRedirect
? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity('
+ JSON.stringify(String(l.lead_form_type_id)) + ','
+ JSON.stringify(String(l.actual_lead_id)) + ','
+ JSON.stringify(String(l.opportunities_id)) + ')\''
: '';
const lostReasonCell = showWon ? '' :
'<td style="font-size:12px;color:#64748b;max-width:220px;white-space:normal;">'
+ escOppCell((l.lost_reason || '').trim() || '—')
+ '</td>';
return '<tr' + rowAttrs + '>'
+ '<td style="font-weight:700;color:#0f172a;">' + escOppCell(l.company || '—') + '</td>'
+ '<td><span ' + typeBadgeClass + '>' + (opportunityType || '—') + '</span></td>'
+ lostReasonCell
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
+ '</tr>';
}).join('');
}
function updateOppToggleLabels(showWon) {
const lostLbl = document.getElementById('oppToggleLblLost');
const wonLbl = document.getElementById('oppToggleLblWon');
if (lostLbl) lostLbl.classList.toggle('active', !showWon);
if (wonLbl) wonLbl.classList.toggle('active', showWon);
}
function renderOpportunitiesTable(userId, showWon) {
const opp = OPP_DATA[userId];
const tbody = document.getElementById('oppTableBody');
if (!tbody || !opp) return;
const leads = showWon ? (opp.won_leads || []) : (opp.loss_leads || []);
const colSpan = showWon ? 3 : 4;
updateOppTableHeader(showWon);
const rows = buildOpportunityTableRows(leads, showWon);
const emptyMsg = showWon ? 'No won opportunities found' : 'No lost opportunities found';
tbody.innerHTML = rows || '<tr><td colspan="' + colSpan + '" style="text-align:center;color:#aaa;padding:24px;">' + emptyMsg + '</td></tr>';
updateOppToggleLabels(showWon);
}
function onOppStatusToggle(checked) {
if (currentModalOppUserId) {
renderOpportunitiesTable(currentModalOppUserId, checked);
}
}
function goSalesPage(page, status, memberId) {
const url = new URL(page === 'activities' ? SALES_ACTIVITIES_URL : SALES_LEADS_URL, window.location.origin);
url.searchParams.set('fy', getSelectedDashboardFY());
@ -939,48 +1035,33 @@ function openModal(id) {
// + '</table>'
// + '</div>';
/* ── Won Leads Table (Table 2) ── */
const wonLeads = opp.won_leads || [];
const wonRows = wonLeads.map(function(l) {
// const opportunityType = [
// l.lead_form_type || '',
// l.lead_type || ''
// ].filter(Boolean).join('/').toUpperCase();
const opportunityType = [
l.lead_form_type || '',
].filter(Boolean).join('').toUpperCase();
const typeBadgeClass = l.lead_form_type === 'EB'
? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'
: 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"';
const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id;
const rowAttrs = canRedirect
? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity('
+ JSON.stringify(String(l.lead_form_type_id)) + ','
+ JSON.stringify(String(l.actual_lead_id)) + ','
+ JSON.stringify(String(l.opportunities_id)) + ')\''
: '';
return '<tr' + rowAttrs + '>'
+ '<td style="font-weight:700;color:#0f172a;">' + (l.company || '—') + '</td>'
+ '<td><span ' + typeBadgeClass + '>' + (opportunityType || '—') + '</span></td>'
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
+ '</tr>';
}).join('');
currentModalOppUserId = id;
tab2Html +=
'<div class="split-section-title" style="margin-top:24px;"> Opportunities </div>'
'<div class="opp-section-head">'
+ '<div class="split-section-title">Opportunities</div>'
+ '<div class="opp-toggle-wrap">'
+ '<span class="opp-toggle-lbl" id="oppToggleLblLost">Lost</span>'
+ '<label class="opp-toggle-switch" title="Toggle between won and lost opportunities">'
+ '<input type="checkbox" id="oppStatusToggle" checked onchange="onOppStatusToggle(this.checked)">'
+ '<span class="opp-toggle-slider"></span>'
+ '</label>'
+ '<span class="opp-toggle-lbl active" id="oppToggleLblWon">Won</span>'
+ '</div>'
+ '</div>'
+ '<div style="overflow-x:auto;">'
+ '<table class="split-table">'
+ '<thead><tr>'
+ '<thead><tr id="oppTableHead">'
+ '<th>Company</th>'
+ '<th>Opportunity Type</th>'
+ '<th>Date</th>'
+ '</tr></thead>'
+ '<tbody>' + (wonRows || '<tr><td colspan="3" style="text-align:center;color:#aaa;padding:24px;">No leads found</td></tr>') + '</tbody>'
+ '<tbody id="oppTableBody"></tbody>'
+ '</table>'
+ '</div>';
} else {
currentModalOppUserId = null;
tab2Html = '<div class="empty-state"><div class="empty-icon">📋</div>No opportunities data available</div>';
}
@ -996,6 +1077,14 @@ function openModal(id) {
document.getElementById('modalOverlay').classList.add('open');
document.body.style.overflow = 'hidden';
if (currentModalOppUserId && OPP_DATA[currentModalOppUserId]) {
const toggle = document.getElementById('oppStatusToggle');
if (toggle) toggle.checked = true;
renderOpportunitiesTable(currentModalOppUserId, true);
} else {
currentModalOppUserId = null;
}
}
function switchTab(idx, btn) {
@ -1007,6 +1096,7 @@ function switchTab(idx, btn) {
function closeModal() {
document.getElementById('modalOverlay').classList.remove('open');
document.body.style.overflow = '';
currentModalOppUserId = null;
}
document.getElementById('modalOverlay').addEventListener('click', function(e) { if (e.target === e.currentTarget) closeModal(); });

View File

@ -1,22 +1,25 @@
# Claims Collection V2 — HR Dashboard API
**Created:** 2026-06-02
**Updated:** 2026-06-03
**Controller:** `App\Controllers\ClaimsCollectionV2DashboardController`
**Model:** `App\Models\ClaimsCollectionV2DashboardModel`
**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2`
**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php`
**Source queries:** `metabase_raw_queries.csv` → collection `Claims Collection V2` (31 original) + 5 KPIs added in-model (232236)
**Base app URL (local):** `https://localhost/PHP828APPS/ruc/nhance/index.php`
**Total KPIs:** 36
---
## Overview
Each Metabase question in the CSV is a separate PHP method on the model.
Each KPI is a separate PHP method on the model, registered in `KPI_MAP` (Metabase question id → slug) and `KPI_LABELS` (slug → display label).
The API exposes them in three ways:
| What | URL fragment | Use case |
|------|-------------|----------|
| **Single KPI** | `kpi/{slug or Metabase id}` | FE loads one card at a time |
| **All KPIs** | `all` | FE loads entire dashboard in one call |
| **All KPIs** | `all` | FE loads entire dashboard in one call (36 KPIs) |
| **Debug / preview** | `debug` / `preview` | Admin checks raw output in browser |
All endpoints require `client_policy` or `client_policy_id` as a query param
@ -36,7 +39,7 @@ Prefix: `util/claims-collection-v2`
| GET | `util/claims-collection-v2/preview/{policy_id}` | `::preview` | Same, policy in URL |
| GET | `util/claims-collection-v2/debug` | `::debug` | Raw JSON dump (all KPIs) |
| GET | `util/claims-collection-v2/debug/{policy_id}` | `::debug` | Same, policy in URL |
| GET | `util/claims-collection-v2/all` | `::all` | JSON — all 31 KPIs |
| GET | `util/claims-collection-v2/all` | `::all` | JSON — all 36 KPIs |
| GET | `util/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI |
**Filters applied:** `authMVC`, `AclFilter`, `HttpRequestLog`, `Cors`, `SecurityInputFilter`
@ -49,7 +52,7 @@ Prefix: `employeeRest/claims-collection-v2`
| Method | Path | Handler | Purpose |
|--------|------|---------|---------|
| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 31 KPIs |
| GET | `employeeRest/claims-collection-v2/all` | `::all` | JSON — all 36 KPIs |
| GET | `employeeRest/claims-collection-v2/kpi/{slug\|id}` | `::kpi` | JSON — single KPI |
| GET | `employeeRest/claims-collection-v2/preview` | `::preview` | UI grid (FE debug) |
| GET | `employeeRest/claims-collection-v2/preview/{policy_id}` | `::preview` | Same, policy in URL |
@ -85,7 +88,7 @@ GET /index.php/util/claims-collection-v2/preview?client_policy=4687
# Preview with policy in URL
GET /index.php/util/claims-collection-v2/preview/4687
# Raw JSON — all 31 KPIs
# Raw JSON — all 36 KPIs
GET /index.php/util/claims-collection-v2/debug?client_policy=4687
# Raw JSON — single KPI by slug
@ -93,6 +96,13 @@ GET /index.php/util/claims-collection-v2/kpi/incurred_ratio?client_policy=4687
# Raw JSON — single KPI by Metabase question id
GET /index.php/util/claims-collection-v2/kpi/207?client_policy=4687
# Exposure KPIs (added 2026-06-03)
GET /index.php/util/claims-collection-v2/kpi/inception_emp_lives?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/current_emp_lives?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/claim_value_by_month?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/claim_amount_by_relationship?client_policy=4687
GET /index.php/util/claims-collection-v2/kpi/top_10_ailments_by_claim_count?client_policy=4687
```
---
@ -113,7 +123,19 @@ X-App-Signature: <app_signature>
```
```http
GET /index.php/employeeRest/claims-collection-v2/kpi/207?client_policy=4687
GET /index.php/employeeRest/claims-collection-v2/kpi/inception_emp_lives?client_policy=4687
Authorization: Bearer <jwt_token>
X-App-Signature: <app_signature>
```
```http
GET /index.php/employeeRest/claims-collection-v2/kpi/claim_amount_by_relationship?client_policy=4687
Authorization: Bearer <jwt_token>
X-App-Signature: <app_signature>
```
```http
GET /index.php/employeeRest/claims-collection-v2/kpi/235?client_policy=4687
Authorization: Bearer <jwt_token>
X-App-Signature: <app_signature>
```
@ -122,7 +144,7 @@ X-App-Signature: <app_signature>
## Sample responses
### `kpi/{slug}` or `kpi/{id}`
### `kpi/{slug}` — single-row KPI
```json
{
@ -137,6 +159,54 @@ X-App-Signature: <app_signature>
}
```
### `kpi/inception_emp_lives` — exposure (single row)
```json
{
"status": true,
"policy_id": 4687,
"kpi_id": 232,
"kpi": "inception_emp_lives",
"label": "Inception Employees & Lives",
"rows": [
{ "inception_emp": "150", "inception_lives": "420" }
]
}
```
### `kpi/current_emp_lives` — exposure (single row)
```json
{
"status": true,
"policy_id": 4687,
"kpi_id": 233,
"kpi": "current_emp_lives",
"label": "Current Employees & Lives",
"rows": [
{ "current_emp": "145", "current_lives": "410", "avg_family_size": "2.83" }
]
}
```
### `kpi/claim_value_by_month` — time series (multi-row)
```json
{
"status": true,
"policy_id": 4687,
"kpi_id": 234,
"kpi": "claim_value_by_month",
"label": "Claim Value by Month",
"rows": [
{ "claim_month": "Jan 2024", "month_sort": "2024-01", "claim_value": "125000.00" },
{ "claim_month": "Feb 2024", "month_sort": "2024-02", "claim_value": "98000.50" }
]
}
```
> Sort charts by `month_sort` (ISO `YYYY-MM`), not `claim_month` (display label).
### `all`
```json
@ -160,6 +230,23 @@ X-App-Signature: <app_signature>
"id": 207,
"label": "Incurred Ratio",
"rows": [{ "incurred_ratio": "72.34%" }]
},
"inception_emp_lives": {
"id": 232,
"label": "Inception Employees & Lives",
"rows": [{ "inception_emp": "150", "inception_lives": "420" }]
},
"current_emp_lives": {
"id": 233,
"label": "Current Employees & Lives",
"rows": [{ "current_emp": "145", "current_lives": "410", "avg_family_size": "2.83" }]
},
"claim_value_by_month": {
"id": 234,
"label": "Claim Value by Month",
"rows": [
{ "claim_month": "Jan 2024", "month_sort": "2024-01", "claim_value": "125000.00" }
]
}
}
}
@ -183,80 +270,140 @@ X-App-Signature: <app_signature>
"message": "Unknown KPI. Pass Metabase id or method slug.",
"allowed": {
"181": "policy_exposure_summary",
"207": "incurred_ratio"
"207": "incurred_ratio",
"232": "inception_emp_lives",
"233": "current_emp_lives",
"234": "claim_value_by_month",
"235": "claim_amount_by_relationship",
"236": "top_10_ailments_by_claim_count"
}
}
```
**HTTP 404**
**HTTP 404**`allowed` lists the full `KPI_MAP` (36 entries).
---
## All 31 KPIs
## All 36 KPIs
| Metabase ID | Method slug | Label |
|-------------|-------------|-------|
| 181 | `policy_exposure_summary` | POLICY & EXPOSURE SUMMARY |
| 185 | `premium_as_on_date` | PREMIUM AS ON DATE |
| 186 | `claims_experience_summary` | CLAIMS EXPERIENCE SUMMARY |
| 190 | `claim_amount_by_gender` | Claim Amount by Gender |
| 191 | `age_band` | Age Band |
| 194 | `top_5_hospitals_by_incurred_amount` | Top 5 Hospitals by Incurred amount |
| 197 | `claims_incidence_rate` | Claims Incidence Rate |
| 198 | `policy_start_date` | Policy Start Date |
| 199 | `policy_end_date` | Policy End Date |
| 200 | `insurer` | Insurer |
| 201 | `tpa` | TPA |
| 203 | `earned_premium` | Earned Premium |
| 204 | `total_claims` | Total Claims |
| 206 | `incurred_amount` | Incurred Amount |
| 207 | `incurred_ratio` | Incurred Ratio |
| 208 | `projected_claims` | Projected Claims |
| 209 | `projected_ratio` | Projected Ratio |
| 213 | `total_reimbursement_amount` | Total Reimbursement Amount |
| 214 | `total_reimbursement_amt_pct` | Total Reimbursement Amt % |
| 215 | `cashless_claim_amt` | Cashless Claim Amt |
| 216 | `cashless_claim_amt_pct` | Cashless Claim Amt % |
| 217 | `total_incurred_by_city` | Total Incurred by city |
| 219 | `claim_amount_by_claim_status` | Claim Amount by Claim Status |
| 220 | `hospitals_in_detail` | Hospitals in detail |
| 221 | `hospital_city_wise_si_limit_pregnancy` | Hospital city wise SI Limit - Pregnancy |
| 224 | `s_pregnancy_normal_delivery_exceeded_amt` | S-PREGNANCY - NORMAL DELIVERY Exceeded Amt |
| 225 | `s_pregnancy_c_sec_avg_exceeded_amt` | S-Pregnancy C-Sec avg exceeded amt |
| 228 | `cataract_exceeded_claim_amount` | Cataract exceeded claim amount |
| 229 | `cataract_avg_exceeded_amount` | Cataract avg exceeded amount |
| 230 | `hospital_city_wise_si_limit_cataract` | Hospital city wise SI Limit - Cataract |
| 231 | `total_incurred_by_cliam_status` | Total Incurred by Cliam Status |
| Metabase ID | Method slug | Label | Rows |
|-------------|-------------|-------|------|
| 181 | `policy_exposure_summary` | POLICY & EXPOSURE SUMMARY | single |
| 185 | `premium_as_on_date` | PREMIUM AS ON DATE | single |
| 186 | `claims_experience_summary` | CLAIMS EXPERIENCE SUMMARY | single |
| 190 | `claim_amount_by_gender` | Claim Amount by Gender | multi |
| 191 | `age_band` | Age Band | multi |
| 194 | `top_5_hospitals_by_incurred_amount` | Top 5 Hospitals by Incurred amount | multi |
| 197 | `claims_incidence_rate` | Claims Incidence Rate | single |
| 198 | `policy_start_date` | Policy Start Date | single |
| 199 | `policy_end_date` | Policy End Date | single |
| 200 | `insurer` | Insurer | single |
| 201 | `tpa` | TPA | single |
| 203 | `earned_premium` | Earned Premium | single |
| 204 | `total_claims` | Total Claims | single |
| 206 | `incurred_amount` | Incurred Amount | single |
| 207 | `incurred_ratio` | Incurred Ratio | single |
| 208 | `projected_claims` | Projected Claims | single |
| 209 | `projected_ratio` | Projected Ratio | single |
| 213 | `total_reimbursement_amount` | Total Reimbursement Amount | single |
| 214 | `total_reimbursement_amt_pct` | Total Reimbursement Amt % | single |
| 215 | `cashless_claim_amt` | Cashless Claim Amt | single |
| 216 | `cashless_claim_amt_pct` | Cashless Claim Amt % | single |
| 217 | `total_incurred_by_city` | Total Incurred by city | multi |
| 219 | `claim_amount_by_claim_status` | Claim Amount by Claim Status | multi |
| 220 | `hospitals_in_detail` | Hospitals in detail | multi |
| 221 | `hospital_city_wise_si_limit_pregnancy` | Hospital city wise SI Limit - Pregnancy | multi |
| 224 | `s_pregnancy_normal_delivery_exceeded_amt` | S-PREGNANCY - NORMAL DELIVERY Exceeded Amt | multi |
| 225 | `s_pregnancy_c_sec_avg_exceeded_amt` | S-Pregnancy C-Sec avg exceeded amt | multi |
| 228 | `cataract_exceeded_claim_amount` | Cataract exceeded claim amount | multi |
| 229 | `cataract_avg_exceeded_amount` | Cataract avg exceeded amount | multi |
| 230 | `hospital_city_wise_si_limit_cataract` | Hospital city wise SI Limit - Cataract | multi |
| 231 | `total_incurred_by_cliam_status` | Total Incurred by Cliam Status | multi |
| 232 | `inception_emp_lives` | Inception Employees & Lives | single |
| 233 | `current_emp_lives` | Current Employees & Lives | single |
| 234 | `claim_value_by_month` | Claim Value by Month | multi |
| 235 | `claim_amount_by_relationship` | Claim Amount by Relationship | multi |
| 236 | `top_10_ailments_by_claim_count` | Top 10 Ailments by Claim Count | multi |
> **Note:** KPIs 214 and 216 were renamed from the auto-generated slug to avoid collision with 213 and 215.
> **Note:** IDs **232236** were assigned in-app for KPIs added outside the original Metabase CSV export. Confirm real Metabase question IDs and update `KPI_MAP` if they differ.
---
## Output columns — KPIs 232236
| Slug | Row fields | Description |
|------|------------|-------------|
| `inception_emp_lives` | `inception_emp`, `inception_lives` | Distinct Self employees and all lives at inception (`change_event` contains inception) |
| `current_emp_lives` | `current_emp`, `current_lives`, `avg_family_size` | Active employees/lives; `avg_family_size` = lives ÷ employees (2 dp) |
| `claim_value_by_month` | `claim_month`, `month_sort`, `claim_value` | Monthly sum of `claim_amount`; ordered by `month_sort` |
| `claim_amount_by_relationship` | `relationship`, `claim_count`, `count_pct`, `claim_value`, `value_pct` | Claims by `ticket_master.relationship`; `count_pct` / `value_pct` are numeric (not `%` strings) |
| `top_10_ailments_by_claim_count` | `ailment`, `claim_count`, `count_pct`, `claim_value`, `value_pct` | Top 10 `tpa_ailments` (non-null); `claim_value` formatted via `FORMAT()`; pct fields as `%` strings |
---
### `kpi/claim_amount_by_relationship` — breakdown (multi-row)
```json
{
"status": true,
"policy_id": 4687,
"kpi_id": 235,
"kpi": "claim_amount_by_relationship",
"label": "Claim Amount by Relationship",
"rows": [
{
"relationship": "Self",
"claim_count": "42",
"count_pct": "35.00",
"claim_value": "850000.00",
"value_pct": "40.50"
},
{
"relationship": "Spouse",
"claim_count": "28",
"count_pct": "23.33",
"claim_value": "520000.00",
"value_pct": "24.80"
}
]
}
```
> Not the same as `claim_amount_by_gender` (#190), which groups by employee `gender` via join — not `ticket_master.relationship`.
---
## Files
| File | Purpose |
|------|---------|
| `app/Models/ClaimsCollectionV2DashboardModel.php` | 31 KPI query methods, `KPI_MAP`, `KPI_LABELS`, `getAllKpis()`, `getKpi()` |
| `app/Models/ClaimsCollectionV2DashboardModel.php` | 36 KPI query methods, `KPI_MAP`, `KPI_LABELS`, `getAllKpis()`, `getKpi()` |
| `app/Controllers/ClaimsCollectionV2DashboardController.php` | `kpi()`, `all()`, `preview()`, `debug()` |
| `app/Views/claims_collection_v2_dashboard.php` | Admin/FE debug preview UI (KPI card grid) |
| `app/Config/Routes.php` | Both route groups (search `claims-collection-v2`) |
| `tests/smoke_claims_collection_v2.php` | CLI smoke test — run: `php tests/smoke_claims_collection_v2.php 4687` |
| `metabase_raw_queries.csv` | Source of truth for all SQL queries |
| `metabase_raw_queries.csv` | Source of truth for original Metabase SQL queries |
| `hr-dashboard.md` | This document |
---
## FE integration notes
- Call `all` once on dashboard mount; render each `data[method].rows` into its card.
- Call `all` once on dashboard mount; render each `data[method].rows` into its card (36 keys under `data`).
- Call `kpi/{slug}` for lazy/on-demand loading of individual cards.
- `policy_id` should come from the HR session / selected policy context — never hardcoded.
- All rows are raw arrays; formatting (currency, %, dates) is already applied inside the SQL (`FORMAT()`, `CONCAT()`).
- `rows` may be empty `[]` if no claims exist for that policy — handle gracefully in UI.
- All rows are raw arrays; formatting (currency, %, dates) is already applied inside the SQL where applicable (`FORMAT()`, `CONCAT()`, `DATE_FORMAT()`).
- `rows` may be empty `[]` if no data exists for that policy — handle gracefully in UI.
- **Single-row KPIs** (e.g. `incurred_ratio`, `inception_emp_lives`): use `rows[0]`.
- **Multi-row KPIs** (e.g. `claim_value_by_month`, `age_band`): iterate `rows`; for time series use `month_sort` for sort order.
- **Exposure block:** `inception_emp_lives` + `current_emp_lives` pair for inception vs current headcount.
---
## BE notes
- To add a new KPI: add an entry to `KPI_MAP` + `KPI_LABELS` in the model and write the corresponding method `public function my_kpi(int $policyId): array`.
- To add a new KPI: add an entry to `KPI_MAP` + `KPI_LABELS` in the model, write `public function my_kpi(int $policyId): array`, update this doc and bump the smoke test KPI count.
- All queries use named binding `:policy_id:` (CodeIgniter style, replaces Metabase `{{policy_id}}`).
- Literal `\t` / `\n` in CSV SQL is normalized in `runKpiQuery()` — safe to re-generate from CSV.
- Run `php tests/smoke_claims_collection_v2.php {policy_id}` after any model change.
- Run `php tests/smoke_claims_collection_v2.php {policy_id}` after any model change (expects `KPI_MAP` count === 36).

View File

@ -51,10 +51,10 @@ function ok(string $label, bool $cond, string $detail = ''): void
$model = new ClaimsCollectionV2DashboardModel();
$kpiMap = ClaimsCollectionV2DashboardModel::KPI_MAP;
ok('KPI_MAP count', count($kpiMap) === 31, (string) count($kpiMap));
ok('KPI_MAP count', count($kpiMap) === 36, (string) count($kpiMap));
$uniqueMethods = array_unique(array_values($kpiMap));
ok('unique KPI method names', count($uniqueMethods) === 31, count($uniqueMethods) . ' methods');
ok('unique KPI method names', count($uniqueMethods) === 36, count($uniqueMethods) . ' methods');
foreach (['policy_exposure_summary', 'incurred_ratio'] as $slug) {
ok("slug map contains {$slug}", in_array($slug, $kpiMap, true));
@ -93,7 +93,7 @@ $badResp = json_decode($controller->kpi('not_a_kpi')->getJSON(), true);
ok('controller unknown kpi 404', ($badResp['status'] ?? true) === false);
$allResp = json_decode($controller->all()->getJSON(), true);
ok('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === 31);
ok('controller all KPIs', ($allResp['status'] ?? false) === true && count($allResp['data'] ?? []) === 36);
$debugOut = $controller->debug($policyId);
$debugBody = is_string($debugOut) ? $debugOut : $debugOut->getBody();