397 lines
14 KiB
PHP
397 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries;
|
|
|
|
/**
|
|
* Renders simple chart PNGs with GD for ABHI MIS PDF (mPDF cannot run Chart.js).
|
|
*/
|
|
class AbhiMisChartImageService
|
|
{
|
|
private string $dir;
|
|
|
|
public function __construct(?string $dir = null)
|
|
{
|
|
$this->dir = $dir ?: (rtrim(WRITEPATH, '/\\') . '/cache/abhi_mis_charts');
|
|
if (!is_dir($this->dir)) {
|
|
@mkdir($this->dir, 0775, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $vm
|
|
* @return array<string,string> chart key => absolute PNG path
|
|
*/
|
|
public function buildAll(array $vm): array
|
|
{
|
|
$out = [];
|
|
|
|
$status = $vm['claimStatus'] ?? [];
|
|
$out['donut'] = $this->pie(
|
|
'donut',
|
|
array_map(static fn ($r) => (string) ($r['status'] ?? ''), $status),
|
|
array_map(static fn ($r) => (float) ($r['reported'] ?? 0), $status),
|
|
['#2E86DE', '#1B3F8C', '#E8862C', '#6A3D9A']
|
|
);
|
|
|
|
$pre = $vm['preAuthTAT'] ?? ['within' => 100, 'above' => 0];
|
|
$out['preAuth'] = $this->pie(
|
|
'preauth',
|
|
['Within 2 Hours %', 'Above 2 Hours %'],
|
|
[(float) ($pre['within'] ?? 0), (float) ($pre['above'] ?? 0)],
|
|
['#2E86DE', '#1B3F8C']
|
|
);
|
|
|
|
$bars = $vm['premiumBars'] ?? [0, 0, 0];
|
|
$out['premiumBars'] = $this->vBars(
|
|
'premium_bars',
|
|
['Premium', 'Earn Premium', 'Claim Incurred'],
|
|
[(float) ($bars[0] ?? 0), (float) ($bars[1] ?? 0), (float) ($bars[2] ?? 0)],
|
|
['#2E86DE', '#1B3F8C', '#E8862C']
|
|
);
|
|
|
|
$gender = $vm['gender'] ?? [];
|
|
$gLabels = [];
|
|
$gCounts = [];
|
|
$gAmts = [];
|
|
$gColors = [];
|
|
foreach (['Male' => '#6A3D9A', 'Female' => '#1A3399'] as $label => $color) {
|
|
$row = null;
|
|
foreach ($gender as $g) {
|
|
if (($g['label'] ?? '') === $label) {
|
|
$row = $g;
|
|
break;
|
|
}
|
|
}
|
|
$gLabels[] = $label;
|
|
$gCounts[] = (float) ($row['count'] ?? 0);
|
|
$gAmts[] = (float) ($row['amount'] ?? 0);
|
|
$gColors[] = $color;
|
|
}
|
|
$out['genderCount'] = $this->hBars('gender_count', $gLabels, $gCounts, $gColors, false);
|
|
$out['genderAmt'] = $this->hBars('gender_amt', $gLabels, $gAmts, $gColors, true);
|
|
|
|
$pc = $vm['paidCharts'] ?? [];
|
|
$cbt = $pc['countByType'] ?? [];
|
|
$out['paidCount'] = $this->vBars(
|
|
'paid_count',
|
|
['Cashless', 'Reimbursement'],
|
|
[(float) ($pc['paidCount'][0] ?? 0), (float) ($pc['paidCount'][1] ?? 0)],
|
|
['#2E86DE', '#1B3F8C']
|
|
);
|
|
$out['paidAmtAcs'] = $this->groupedVBars(
|
|
'paid_amt_acs',
|
|
['Cashless', 'Reimbursement'],
|
|
[
|
|
['label' => 'Paid Amt', 'values' => [(float) ($pc['paidAmt'][0] ?? 0), (float) ($pc['paidAmt'][1] ?? 0)], 'color' => '#2E86DE'],
|
|
['label' => 'ACS', 'values' => [(float) ($pc['acs'][0] ?? 0), (float) ($pc['acs'][1] ?? 0)], 'color' => '#E8862C'],
|
|
]
|
|
);
|
|
$out['countStatus'] = $this->groupedVBars(
|
|
'count_status',
|
|
['Settled', 'Outstanding', 'Rejected'],
|
|
[
|
|
[
|
|
'label' => 'Cashless',
|
|
'values' => [
|
|
(float) ($cbt['Settled']['Cashless'] ?? 0),
|
|
(float) ($cbt['Outstanding']['Cashless'] ?? 0),
|
|
(float) ($cbt['Rejected']['Cashless'] ?? 0),
|
|
],
|
|
'color' => '#2E86DE',
|
|
],
|
|
[
|
|
'label' => 'Reimbursement',
|
|
'values' => [
|
|
(float) ($cbt['Settled']['Reimbursement'] ?? 0),
|
|
(float) ($cbt['Outstanding']['Reimbursement'] ?? 0),
|
|
(float) ($cbt['Rejected']['Reimbursement'] ?? 0),
|
|
],
|
|
'color' => '#1B3F8C',
|
|
],
|
|
]
|
|
);
|
|
$out['amtStatus'] = $this->vBars(
|
|
'amt_status',
|
|
['Settled', 'Outstanding', 'Rejected'],
|
|
[
|
|
(float) ($pc['amtByStatus'][0] ?? 0),
|
|
(float) ($pc['amtByStatus'][1] ?? 0),
|
|
(float) ($pc['amtByStatus'][2] ?? 0),
|
|
],
|
|
['#2E86DE', '#E8862C', '#6A3D9A']
|
|
);
|
|
|
|
$months = $vm['months'] ?? [];
|
|
$out['monthCount'] = $this->groupedVBars(
|
|
'month_count',
|
|
$months,
|
|
[
|
|
['label' => 'Paid Count', 'values' => array_map('floatval', $vm['paidCountByMonth'] ?? []), 'color' => '#2E86DE'],
|
|
['label' => 'Outstanding Count', 'values' => array_map('floatval', $vm['outCountByMonth'] ?? []), 'color' => '#1B3F8C'],
|
|
],
|
|
720,
|
|
280
|
|
);
|
|
$out['monthAmt'] = $this->groupedVBars(
|
|
'month_amt',
|
|
$months,
|
|
[
|
|
['label' => 'Paid Amount', 'values' => array_map('floatval', $vm['paidAmtByMonth'] ?? []), 'color' => '#2E86DE'],
|
|
['label' => 'Outstanding Amount', 'values' => array_map('floatval', $vm['outAmtByMonth'] ?? []), 'color' => '#1B3F8C'],
|
|
],
|
|
720,
|
|
280
|
|
);
|
|
|
|
return array_filter($out);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $labels
|
|
* @param list<float> $values
|
|
* @param list<string> $colors
|
|
*/
|
|
public function pie(string $key, array $labels, array $values, array $colors, int $w = 420, int $h = 260): string
|
|
{
|
|
$img = imagecreatetruecolor($w, $h);
|
|
$white = imagecolorallocate($img, 255, 255, 255);
|
|
imagefilledrectangle($img, 0, 0, $w, $h, $white);
|
|
|
|
$total = array_sum($values);
|
|
$cx = (int) ($w * 0.32);
|
|
$cy = (int) ($h / 2);
|
|
$rx = 95;
|
|
$ry = 70;
|
|
$start = 0.0;
|
|
|
|
foreach ($values as $i => $val) {
|
|
$slice = $total > 0 ? ($val / $total) * 360.0 : 0.0;
|
|
if ($slice <= 0) {
|
|
continue;
|
|
}
|
|
$rgb = $this->hex($colors[$i % count($colors)]);
|
|
$col = imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
|
|
$this->filledArc($img, $cx, $cy, $rx * 2, $ry * 2, (int) $start, (int) ($start + $slice), $col);
|
|
$start += $slice;
|
|
}
|
|
|
|
// hole for donut
|
|
$hole = imagecolorallocate($img, 255, 255, 255);
|
|
imagefilledellipse($img, $cx, $cy, (int) ($rx * 0.9), (int) ($ry * 0.9), $hole);
|
|
|
|
$black = imagecolorallocate($img, 40, 40, 40);
|
|
$lx = (int) ($w * 0.58);
|
|
$ly = 40;
|
|
foreach ($labels as $i => $label) {
|
|
$rgb = $this->hex($colors[$i % count($colors)]);
|
|
$col = imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
|
|
imagefilledrectangle($img, $lx, $ly, $lx + 12, $ly + 12, $col);
|
|
$pct = $total > 0 ? round(($values[$i] / $total) * 100, 1) : 0;
|
|
imagestring($img, 3, $lx + 18, $ly - 1, $this->fit($label . ' ' . $pct . '%', 28), $black);
|
|
$ly += 22;
|
|
}
|
|
|
|
return $this->save($key, $img);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $labels
|
|
* @param list<float> $values
|
|
* @param list<string> $colors
|
|
*/
|
|
public function hBars(string $key, array $labels, array $values, array $colors, bool $indianFmt, int $w = 480, int $h = 200): string
|
|
{
|
|
$img = imagecreatetruecolor($w, $h);
|
|
$white = imagecolorallocate($img, 255, 255, 255);
|
|
$black = imagecolorallocate($img, 40, 40, 40);
|
|
imagefilledrectangle($img, 0, 0, $w, $h, $white);
|
|
|
|
$max = max(1.0, ...array_map('floatval', $values ?: [1]));
|
|
$left = 70;
|
|
$barH = 36;
|
|
$gap = 28;
|
|
$y = 40;
|
|
$maxW = $w - $left - 30;
|
|
|
|
foreach ($labels as $i => $label) {
|
|
$val = (float) ($values[$i] ?? 0);
|
|
$bw = (int) (($val / $max) * $maxW);
|
|
$rgb = $this->hex($colors[$i % count($colors)]);
|
|
$col = imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
|
|
imagestring($img, 3, 8, $y + 10, $this->fit($label, 10), $black);
|
|
imagefilledrectangle($img, $left, $y, $left + max(2, $bw), $y + $barH, $col);
|
|
$txt = $indianFmt ? $this->fmtIn($val) : (string) (int) $val;
|
|
$tx = $left + (int) ($bw / 2) - (int) (strlen($txt) * 3);
|
|
$tx = max($left + 4, min($tx, $w - 80));
|
|
$box = imagecolorallocate($img, 255, 255, 255);
|
|
imagefilledrectangle($img, $tx - 2, $y + 10, $tx + strlen($txt) * 7 + 2, $y + 26, $box);
|
|
imagestring($img, 3, $tx, $y + 12, $txt, $black);
|
|
$y += $barH + $gap;
|
|
}
|
|
|
|
return $this->save($key, $img);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $labels
|
|
* @param list<float> $values
|
|
* @param list<string> $colors
|
|
*/
|
|
public function vBars(string $key, array $labels, array $values, array $colors, int $w = 480, int $h = 240): string
|
|
{
|
|
$img = imagecreatetruecolor($w, $h);
|
|
$white = imagecolorallocate($img, 255, 255, 255);
|
|
$black = imagecolorallocate($img, 40, 40, 40);
|
|
$grid = imagecolorallocate($img, 230, 230, 230);
|
|
imagefilledrectangle($img, 0, 0, $w, $h, $white);
|
|
|
|
$max = max(1.0, ...array_map('floatval', $values ?: [1]));
|
|
$top = 20;
|
|
$bottom = $h - 40;
|
|
$left = 40;
|
|
$right = $w - 20;
|
|
$plotH = $bottom - $top;
|
|
$n = max(1, count($labels));
|
|
$slot = ($right - $left) / $n;
|
|
$barW = max(12, (int) ($slot * 0.45));
|
|
|
|
for ($g = 0; $g <= 4; $g++) {
|
|
$gy = (int) ($bottom - ($plotH * $g / 4));
|
|
imageline($img, $left, $gy, $right, $gy, $grid);
|
|
}
|
|
|
|
foreach ($labels as $i => $label) {
|
|
$val = (float) ($values[$i] ?? 0);
|
|
$bh = (int) (($val / $max) * $plotH);
|
|
$x = (int) ($left + $slot * $i + ($slot - $barW) / 2);
|
|
$y = $bottom - $bh;
|
|
$rgb = $this->hex($colors[$i % count($colors)]);
|
|
$col = imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
|
|
imagefilledrectangle($img, $x, $y, $x + $barW, $bottom, $col);
|
|
$txt = $this->fmtShort($val);
|
|
imagestring($img, 2, $x, max(4, $y - 14), $txt, $black);
|
|
imagestring($img, 2, $x - 4, $bottom + 8, $this->fit($label, 12), $black);
|
|
}
|
|
|
|
return $this->save($key, $img);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $labels
|
|
* @param list<array{label:string,values:list<float>,color:string}> $series
|
|
*/
|
|
public function groupedVBars(string $key, array $labels, array $series, int $w = 520, int $h = 240): string
|
|
{
|
|
$img = imagecreatetruecolor($w, $h);
|
|
$white = imagecolorallocate($img, 255, 255, 255);
|
|
$black = imagecolorallocate($img, 40, 40, 40);
|
|
$grid = imagecolorallocate($img, 230, 230, 230);
|
|
imagefilledrectangle($img, 0, 0, $w, $h, $white);
|
|
|
|
$all = [];
|
|
foreach ($series as $s) {
|
|
foreach ($s['values'] as $v) {
|
|
$all[] = (float) $v;
|
|
}
|
|
}
|
|
$max = max(1.0, ...($all ?: [1.0]));
|
|
$top = 28;
|
|
$bottom = $h - 50;
|
|
$left = 30;
|
|
$right = $w - 20;
|
|
$plotH = $bottom - $top;
|
|
$n = max(1, count($labels));
|
|
$slot = ($right - $left) / $n;
|
|
$seriesN = max(1, count($series));
|
|
$barW = max(8, (int) (($slot * 0.7) / $seriesN));
|
|
|
|
for ($g = 0; $g <= 4; $g++) {
|
|
$gy = (int) ($bottom - ($plotH * $g / 4));
|
|
imageline($img, $left, $gy, $right, $gy, $grid);
|
|
}
|
|
|
|
foreach ($labels as $i => $label) {
|
|
foreach ($series as $si => $s) {
|
|
$val = (float) ($s['values'][$i] ?? 0);
|
|
$bh = (int) (($val / $max) * $plotH);
|
|
$x = (int) ($left + $slot * $i + ($slot - $barW * $seriesN) / 2 + $si * $barW);
|
|
$y = $bottom - $bh;
|
|
$rgb = $this->hex($s['color']);
|
|
$col = imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
|
|
imagefilledrectangle($img, $x, $y, $x + $barW - 2, $bottom, $col);
|
|
}
|
|
imagestring($img, 2, (int) ($left + $slot * $i + 4), $bottom + 6, $this->fit((string) $label, 10), $black);
|
|
}
|
|
|
|
$lx = $left;
|
|
foreach ($series as $s) {
|
|
$rgb = $this->hex($s['color']);
|
|
$col = imagecolorallocate($img, $rgb[0], $rgb[1], $rgb[2]);
|
|
imagefilledrectangle($img, $lx, 8, $lx + 10, 18, $col);
|
|
imagestring($img, 2, $lx + 14, 7, $this->fit($s['label'], 18), $black);
|
|
$lx += 120;
|
|
}
|
|
|
|
return $this->save($key, $img);
|
|
}
|
|
|
|
/** @return array{0:int,1:int,2:int} */
|
|
private function hex(string $hex): array
|
|
{
|
|
$hex = ltrim($hex, '#');
|
|
if (strlen($hex) === 3) {
|
|
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
|
|
}
|
|
if (strlen($hex) < 6) {
|
|
return [46, 134, 222];
|
|
}
|
|
|
|
return [
|
|
hexdec(substr($hex, 0, 2)),
|
|
hexdec(substr($hex, 2, 2)),
|
|
hexdec(substr($hex, 4, 2)),
|
|
];
|
|
}
|
|
|
|
private function filledArc($img, int $cx, int $cy, int $w, int $h, int $s, int $e, $col): void
|
|
{
|
|
if ($e <= $s) {
|
|
return;
|
|
}
|
|
imagefilledarc($img, $cx, $cy, $w, $h, $s, $e, $col, IMG_ARC_PIE);
|
|
}
|
|
|
|
private function fit(string $s, int $max): string
|
|
{
|
|
$s = preg_replace('/[^\x20-\x7E]/', '', $s) ?? $s;
|
|
return strlen($s) > $max ? substr($s, 0, $max - 1) . '.' : $s;
|
|
}
|
|
|
|
private function fmtIn(float $n): string
|
|
{
|
|
return number_format($n, 0, '.', ',');
|
|
}
|
|
|
|
private function fmtShort(float $n): string
|
|
{
|
|
if (abs($n) >= 100000) {
|
|
return number_format($n / 100000, 1) . 'L';
|
|
}
|
|
if (abs($n) >= 1000) {
|
|
return number_format($n / 1000, 1) . 'k';
|
|
}
|
|
|
|
return (string) (int) $n;
|
|
}
|
|
|
|
/** @param resource|\GdImage $img */
|
|
private function save(string $key, $img): string
|
|
{
|
|
$path = $this->dir . DIRECTORY_SEPARATOR . $key . '_' . uniqid('', true) . '.png';
|
|
imagepng($img, $path);
|
|
imagedestroy($img);
|
|
|
|
return $path;
|
|
}
|
|
}
|