nhance/app/Libraries/AbhiMisChartImageService.php
2026-08-07 10:19:31 +05:30

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;
}
}