MERGE_UAT_HR_CD_MAIL_NOTIFY
This commit is contained in:
commit
a489a8aa2b
@ -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();
|
||||
|
||||
|
||||
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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, '"') + '">' + String(label).replace(/</g, '<') + '</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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
251
hr-dashboard.md
251
hr-dashboard.md
@ -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 (232–236)
|
||||
**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 **232–236** 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 232–236
|
||||
|
||||
| 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).
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user