nhance/app/Libraries/GmcQcrPdfService.php
2026-07-30 14:03:40 +05:30

583 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Libraries;
use App\Models\ClientModel;
use App\Models\LeadsModel;
use App\Models\RFQModel;
use Mpdf\Mpdf;
use Mpdf\Output\Destination;
/**
* Builds the GMC Quote Comparison Report PDF:
* cover (client) + static front + dynamic QCR pages + static back.
*/
class GmcQcrPdfService
{
public const GMC_POLICY_TYPE_IDS = [2, 3, 4, 5];
private const PAGE_W_MM = 338.6667; // 960 pt
private const PAGE_H_MM = 190.5; // 540 pt
private const ROWS_PER_PAGE = 18;
private const COLS_PER_PAGE = 5;
private RFQModel $rfqModel;
private LeadsModel $leadsModel;
private ClientModel $clientModel;
public function __construct(?RFQModel $rfqModel = null, ?LeadsModel $leadsModel = null, ?ClientModel $clientModel = null)
{
$this->rfqModel = $rfqModel ?? new RFQModel();
$this->leadsModel = $leadsModel ?? new LeadsModel();
$this->clientModel = $clientModel ?? new ClientModel();
}
public static function isGmc(int $policyTypeId): bool
{
return in_array($policyTypeId, self::GMC_POLICY_TYPE_IDS, true);
}
/**
* @return array{status:bool,message:string,file_name?:string,file_path?:string,download_name?:string}
*/
/**
* @param int $leadId
* @param array|null $selectedProposals If provided, only include these proposal names
*/
public function generate(int $leadId, ?array $selectedProposals = null): array
{
$lead = $this->leadsModel
->select('leads.*, policy_type.policy_type, policy_type.long_name')
->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left')
->where('leads.id', $leadId)
->where('leads.is_active', 1)
->first();
if (! $lead) {
return ['status' => false, 'message' => 'Opportunity not found'];
}
if (! self::isGmc((int) ($lead['policy_type_id'] ?? 0))) {
return ['status' => false, 'message' => 'GMC QCR PDF is only available for GMC policies'];
}
// Primary: explicit QCR row (type=2)
$rfq = $this->rfqModel
->where('lead_id', $leadId)
->where('type', 2)
->where('is_active', 1)
->orderBy('id', 'desc')
->first();
// Fallback: some older leads/pages show latest active RFQ row on QCR view.
// If explicit QCR is missing, use latest active saved table JSON.
if (! $rfq || empty($rfq['json'])) {
$rfq = $this->rfqModel
->where('lead_id', $leadId)
->where('is_active', 1)
->orderBy('id', 'desc')
->first();
}
if (! $rfq || empty($rfq['json'])) {
return ['status' => false, 'message' => 'No saved proposal table data found. Please save this page first.'];
}
$qcrJson = json_decode($rfq['json'], true);
if (! is_array($qcrJson) || empty($qcrJson['table_data'])) {
return ['status' => false, 'message' => 'QCR data is invalid or empty'];
}
$clientName = trim((string) ($lead['client_name'] ?? 'Client'));
$logoPath = $this->resolveClientLogoPath((int) ($lead['client_id'] ?? 0));
$nhanceLogo = ROOTPATH . 'public/assets/images/Nhance-Logo-Final.png';
$coveragePages = $this->buildCoveragePages($qcrJson, $selectedProposals);
$premiumSections = $this->buildPremiumSections($qcrJson, $selectedProposals);
if (empty($coveragePages) && empty($premiumSections)) {
return ['status' => false, 'message' => 'No QCR-enabled proposals found to include in the PDF'];
}
$frontPdf = ROOTPATH . 'public/assets/qcr_gmc/static_front.pdf';
$backPdf = ROOTPATH . 'public/assets/qcr_gmc/static_back.pdf';
if (! is_file($frontPdf) || ! is_file($backPdf)) {
return ['status' => false, 'message' => 'Static GMC QCR template PDFs are missing'];
}
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'gmc_qcr_mpdf';
if (! is_dir($tempDir)) {
@mkdir($tempDir, 0775, true);
}
$mpdfNested = $tempDir . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfNested)) {
@mkdir($mpdfNested, 0775, true);
}
$outDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qcr_pdf' . DIRECTORY_SEPARATOR;
if (! is_dir($outDir)) {
@mkdir($outDir, 0775, true);
}
$safeClient = preg_replace('/[^A-Za-z0-9_\- ]+/', '', $clientName) ?: 'Client';
$safeClient = trim(preg_replace('/\s+/', ' ', $safeClient));
$downloadName = $safeClient . ' GMC Quote comparison.-' . date('d-F-Y') . '.pdf';
$storedName = 'gmc_qcr_' . $leadId . '_' . time() . '_' . bin2hex(random_bytes(3)) . '.pdf';
$outPath = $outDir . $storedName;
try {
$mpdf = new Mpdf([
'tempDir' => $tempDir,
'mode' => 'utf-8',
'format' => [self::PAGE_W_MM, self::PAGE_H_MM],
'orientation' => 'P',
'margin_left' => 0,
'margin_right' => 0,
'margin_top' => 0,
'margin_bottom'=> 0,
]);
// 1) Cover
$mpdf->AddPageByArray($this->pageArray());
$mpdf->WriteHTML($this->renderCoverHtml($clientName, $logoPath, $nhanceLogo));
// 2) Static front (pages 27 from master)
$this->importPdfPages($mpdf, $frontPdf, $tempDir);
// 3) Dynamic coverage pages — one set per proposal (Quote Asked only)
foreach ($coveragePages as $page) {
$rowChunks = array_chunk($page['rows'], self::ROWS_PER_PAGE);
foreach ($rowChunks as $chunkIdx => $chunk) {
$mpdf->AddPageByArray($this->pageArray());
$mpdf->WriteHTML($this->renderCoverageHtml(
$page['title'],
$chunk,
$nhanceLogo,
$chunkIdx === 0
));
}
}
// 4) Quote Comparison — insurers with premium data
if (! empty($premiumSections)) {
$mpdf->AddPageByArray($this->pageArray());
$mpdf->WriteHTML($this->renderPremiumHtml($premiumSections, $nhanceLogo));
}
// 5) Static back (last 3)
$this->importPdfPages($mpdf, $backPdf, $tempDir);
$mpdf->Output($outPath, Destination::FILE);
} catch (\Throwable $e) {
log_message('error', 'GmcQcrPdfService::generate failed | lead_id=' . $leadId . ' | ' . $e->getMessage());
return ['status' => false, 'message' => 'PDF generation failed: ' . $e->getMessage()];
}
if (! is_file($outPath)) {
return ['status' => false, 'message' => 'PDF file was not created'];
}
$this->persistGeneratedPath($lead, $storedName, $downloadName);
return [
'status' => true,
'message' => 'GMC QCR PDF generated successfully',
'file_name' => $storedName,
'file_path' => $outPath,
'download_name' => $downloadName,
];
}
/**
* @return array{status:bool,message:string,file_path?:string,download_name?:string}
*/
public function resolveDownload(int $leadId): array
{
$lead = $this->leadsModel->where('id', $leadId)->where('is_active', 1)->first();
if (! $lead) {
return ['status' => false, 'message' => 'Opportunity not found'];
}
if (! self::isGmc((int) ($lead['policy_type_id'] ?? 0))) {
return ['status' => false, 'message' => 'GMC QCR PDF is only available for GMC policies'];
}
$misc = [];
if (! empty($lead['misc'])) {
$decoded = json_decode($lead['misc'], true);
if (is_array($decoded)) {
$misc = $decoded;
}
}
$storedName = $misc['gmc_qcr_pdf'] ?? null;
$downloadName = $misc['gmc_qcr_pdf_download_name'] ?? null;
if (! $storedName) {
return ['status' => false, 'message' => 'No generated PDF found. Please click Generate first.'];
}
$path = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qcr_pdf' . DIRECTORY_SEPARATOR . $storedName;
if (! is_file($path)) {
return ['status' => false, 'message' => 'Generated PDF file is missing. Please Generate again.'];
}
if (! $downloadName) {
$clientName = trim((string) ($lead['client_name'] ?? 'Client'));
$downloadName = $clientName . ' GMC Quote comparison.-' . date('d-F-Y') . '.pdf';
}
return [
'status' => true,
'message' => 'OK',
'file_path' => $path,
'download_name' => $downloadName,
];
}
private function pageArray(): array
{
return [
'orientation' => 'P',
'sheet-size' => [self::PAGE_W_MM, self::PAGE_H_MM],
'margin-left' => 0,
'margin-right' => 0,
'margin-top' => 0,
'margin-bottom'=> 0,
];
}
private function resolveClientLogoPath(int $clientId): ?string
{
if ($clientId <= 0) {
return null;
}
$client = $this->clientModel->select('client_logo')->where('id', $clientId)->where('is_active', 1)->first();
if (empty($client['client_logo'])) {
return null;
}
$path = ROOTPATH . 'public/uploads/logo/' . $client['client_logo'];
return is_file($path) ? $path : null;
}
/**
* Build one coverage page set per QCR-enabled proposal.
* Each page shows only the "Quote Asked" column (2-col table like original).
*
* @return list<array{title:string,rows:list<array{particular:string,value:string}>}>
*/
private function buildCoveragePages(array $qcrJson, ?array $selectedProposals = null): array
{
$proposalMeta = $qcrJson['proposal_data']['over_all_column_data'] ?? [];
$headers = $qcrJson['table_data']['headers'] ?? [];
$dataRows = $qcrJson['table_data']['data'] ?? [];
// Collect QCR-enabled proposals that have a Quote Asked column
$proposals = [];
foreach ($headers as $header) {
$parent = (string) ($header['parentHeader'] ?? '');
if (in_array($parent, ['Sno', 'Item Key', 'Particulars', 'Action', ''], true)) {
continue;
}
$meta = $proposalMeta[$parent] ?? null;
$proposalQcr = $meta === null ? 1 : (int) ($meta['qcr'] ?? 0);
if ($proposalQcr !== 1) {
continue;
}
// Filter by user-selected proposals if provided
if ($selectedProposals !== null && ! in_array($parent, $selectedProposals, true)) {
continue;
}
$subs = $header['subHeaders'] ?? [];
if (in_array('Quote Asked', $subs, true) && ! isset($proposals[$parent])) {
$proposals[$parent] = true;
}
}
$pages = [];
foreach (array_keys($proposals) as $proposalName) {
$rows = [];
foreach ($dataRows as $row) {
$actionQcr = 1;
$particular = '';
$value = '';
foreach (($row['data'] ?? []) as $cell) {
$parent = (string) ($cell['parentth'] ?? '');
$sub = (string) ($cell['subth'] ?? '');
$raw = $cell['value'] ?? $cell['input_value'] ?? '';
if (is_array($raw)) {
if (isset($raw['qcr'])) {
$actionQcr = (int) $raw['qcr'];
}
$raw = '';
}
$text = trim(html_entity_decode(strip_tags((string) $raw)));
if ($parent === 'Particulars') {
$particular = $text;
} elseif ($parent === $proposalName && $sub === 'Quote Asked') {
$value = $text;
} elseif ($parent === 'Action' && is_array($cell['value'] ?? $cell['input_value'] ?? null)) {
$actionQcr = (int) (($cell['value'] ?? $cell['input_value'] ?? [])['qcr'] ?? 1);
}
}
if ($actionQcr !== 1 || $particular === '') {
continue;
}
$rows[] = ['particular' => $particular, 'value' => $value];
}
if (! empty($rows)) {
$pages[] = [
'title' => 'GMC Policy Coverage ' . $proposalName,
'rows' => $rows,
];
}
}
return $pages;
}
/**
* Build Quote Comparison sections from premium_data — insurers only (not Quote Asked).
*
* @return list<array{title:string,rows:list<array{insurer:string,premium:string,gst:string,total:string}>}>
*/
private function buildPremiumSections(array $qcrJson, ?array $selectedProposals = null): array
{
$premiumData = $qcrJson['premium_data']['data'] ?? [];
$proposalMeta = $qcrJson['proposal_data']['over_all_column_data'] ?? [];
if (! is_array($premiumData) || empty($premiumData)) {
return [];
}
$sections = [];
foreach ($proposalMeta as $proposalName => $meta) {
if ((int) ($meta['qcr'] ?? 0) !== 1) {
continue;
}
if ($selectedProposals !== null && ! in_array($proposalName, $selectedProposals, true)) {
continue;
}
$insMap = $premiumData[$proposalName] ?? [];
if (! is_array($insMap)) {
continue;
}
$sectionRows = [];
foreach ($insMap as $insurerName => $metrics) {
if ($insurerName === 'Quote Asked' || $insurerName === '' || ! is_array($metrics)) {
continue;
}
$premium = trim((string) ($metrics['Premium'] ?? ''));
$gst = trim((string) ($metrics['GST Amount (₹)'] ?? $metrics['GST Amount'] ?? ''));
$total = trim((string) ($metrics['Total'] ?? ''));
if ($premium === '' && $total === '') {
continue;
}
if ($premium === 'Premium') {
continue;
}
$sectionRows[] = [
'insurer' => $insurerName,
'premium' => $premium,
'gst' => $gst,
'total' => $total,
];
}
if (! empty($sectionRows)) {
$sections[] = [
'title' => 'GMC Quote ' . $proposalName,
'rows' => $sectionRows,
];
}
}
return $sections;
}
private function importPdfPages(Mpdf $mpdf, string $pdfPath, string $tempDir): void
{
helper('merge_pdf');
try {
merge_ticket_pdf_add_pdf_pages($mpdf, $pdfPath, $tempDir);
} catch (\Throwable $e) {
// Fallback: import page-by-page with explicit sheet size
$pageCount = $mpdf->setSourceFile($pdfPath);
for ($p = 1; $p <= $pageCount; $p++) {
$tplId = $mpdf->importPage($p);
$mpdf->AddPageByArray($this->pageArray());
$mpdf->useTemplate($tplId, 0, 0, self::PAGE_W_MM, self::PAGE_H_MM, false);
}
}
}
private function persistGeneratedPath(array $lead, string $storedName, string $downloadName): void
{
$misc = [];
if (! empty($lead['misc'])) {
$decoded = json_decode($lead['misc'], true);
if (is_array($decoded)) {
$misc = $decoded;
}
}
// Remove previous file if present
if (! empty($misc['gmc_qcr_pdf'])) {
$old = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'qcr_pdf' . DIRECTORY_SEPARATOR . $misc['gmc_qcr_pdf'];
if (is_file($old) && $misc['gmc_qcr_pdf'] !== $storedName) {
@unlink($old);
}
}
$misc['gmc_qcr_pdf'] = $storedName;
$misc['gmc_qcr_pdf_download_name'] = $downloadName;
$misc['gmc_qcr_pdf_generated_at'] = date('Y-m-d H:i:s');
$this->leadsModel->update((int) $lead['id'], [
'misc' => json_encode($misc),
]);
}
private function escape(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
private function renderCoverHtml(string $clientName, ?string $logoPath, string $nhanceLogo): string
{
$logoHtml = '';
if ($logoPath) {
$logoHtml = '<div style="margin-bottom:8mm;"><img src="' . $this->escape($logoPath) . '" style="max-height:55mm;max-width:100mm;" /></div>';
}
$nhance = is_file($nhanceLogo)
? '<img src="' . $this->escape($nhanceLogo) . '" style="height:10mm;" />'
: '<span style="color:#0aa3a0;font-size:14pt;font-weight:bold;">Nhance</span>';
return '
<html><head><style>
@page { margin: 0; }
body { margin:0; padding:0; font-family: freeserif, DejaVu Serif, serif; }
.wrap { width:100%; height:190mm; position:relative; text-align:center; }
.spacer { height:48mm; }
.client { color:#0b7f7c; font-size:16pt; margin-bottom:6mm; font-family: freeserif, DejaVu Serif, serif; }
.title { font-size:24pt; color:#1a2744; font-family: freeserif, DejaVu Serif, serif; }
.title em { color:#0b7f7c; font-style:italic; }
.footer-logo { position:absolute; left:12mm; bottom:8mm; }
.footer-bar { position:absolute; right:0; bottom:0; width:28mm; height:8mm; background:#0aa3a0; }
</style></head><body>
<div class="wrap">
<div class="spacer">&nbsp;</div>
' . $logoHtml . '
<div class="client">' . $this->escape($clientName) . '</div>
<div class="title"><strong>Group</strong> <em>Medical Insurance</em> <strong>Quote</strong></div>
<div class="footer-logo">' . $nhance . '</div>
<div class="footer-bar"></div>
</div>
</body></html>';
}
/**
* Render a coverage page (2-column: Particulars + value) matching original style.
*
* @param list<array{particular:string,value:string}> $rows
*/
private function renderCoverageHtml(string $title, array $rows, string $nhanceLogo, bool $showTitle): string
{
$nhance = is_file($nhanceLogo)
? '<img src="' . $this->escape($nhanceLogo) . '" style="height:8mm;" />'
: '<span style="color:#0aa3a0;font-size:11pt;font-weight:bold;">Nhance</span>';
$thead = '<tr><th style="width:55%;">Particulars</th><th style="width:45%;">Expiring</th></tr>';
$tbody = '';
$i = 0;
foreach ($rows as $row) {
$bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa';
$tbody .= '<tr style="background:' . $bg . ';">';
$tbody .= '<td class="part">' . $this->escape($row['particular']) . '</td>';
$tbody .= '<td>' . $this->escape($row['value']) . '</td>';
$tbody .= '</tr>';
$i++;
}
$titleBlock = $showTitle
? '<div class="title"><span class="bar"></span><strong>GMC</strong> <em>' . $this->escape(str_replace('GMC ', '', $title)) . '</em></div>'
: '<div class="title-sm"><strong>GMC</strong> <em>' . $this->escape(str_replace('GMC ', '', $title)) . '</em> <span style="color:#666;font-size:9pt;">(continued)</span></div>';
return '
<html><head><style>
body { margin:0; padding:0; font-family: dejavusans, sans-serif; color:#222; }
.page { padding:8mm 14mm 14mm 14mm; }
.title { font-family: freeserif, DejaVu Serif, serif; color:#1a2744; font-size:20pt; margin:0 0 5mm 0; padding-left:5mm; border-left:3mm solid #0aa3a0; }
.title em { color:#0aa3a0; font-style:italic; }
.title-sm { font-family: freeserif, DejaVu Serif, serif; color:#1a2744; font-size:16pt; margin:0 0 4mm 0; padding-left:5mm; border-left:3mm solid #0aa3a0; }
.title-sm em { color:#0aa3a0; font-style:italic; }
table { width:100%; border-collapse:collapse; table-layout:fixed; margin-top:2mm; }
th { background:#1a7a6e; color:#fff; font-size:9pt; padding:2.8mm 3mm; text-align:center; vertical-align:middle; font-family: dejavusans, sans-serif; }
td { font-size:8pt; padding:2.2mm 3mm; border-bottom:0.15mm solid #e0eded; text-align:center; vertical-align:middle; word-wrap:break-word; }
td.part { text-align:center; color:#333; }
.footer { position:absolute; left:12mm; bottom:5mm; }
</style></head><body>
<div class="page">
' . $titleBlock . '
<table><thead>' . $thead . '</thead><tbody>' . $tbody . '</tbody></table>
</div>
<div class="footer">' . $nhance . '</div>
</body></html>';
}
/**
* @param list<array{title:string,rows:list<array{insurer:string,premium:string,gst:string,total:string}>}> $sections
*/
private function renderPremiumHtml(array $sections, string $nhanceLogo): string
{
$nhance = is_file($nhanceLogo)
? '<img src="' . $this->escape($nhanceLogo) . '" style="height:8mm;" />'
: '<span style="color:#0aa3a0;font-size:11pt;font-weight:bold;">Nhance</span>';
$body = '';
foreach ($sections as $section) {
$body .= '<div class="section-title">' . $this->escape($section['title']) . '</div>';
$body .= '<table><thead><tr>
<th style="width:40%;">Insurer Name</th>
<th style="width:20%;">Premium</th>
<th style="width:20%;">GST</th>
<th style="width:20%;">Total Premium</th>
</tr></thead><tbody>';
$i = 0;
foreach ($section['rows'] as $row) {
$bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa';
$body .= '<tr style="background:' . $bg . ';">
<td class="left">' . $this->escape($row['insurer']) . '</td>
<td>' . $this->escape($row['premium']) . '</td>
<td>' . $this->escape($row['gst']) . '</td>
<td>' . $this->escape($row['total']) . '</td>
</tr>';
$i++;
}
$body .= '</tbody></table>';
}
return '
<html><head><style>
body { margin:0; padding:0; font-family: dejavusans, sans-serif; color:#222; }
.page { padding:8mm 14mm 14mm 14mm; }
.title { font-family: freeserif, DejaVu Serif, serif; color:#1a2744; font-size:20pt; margin:0 0 6mm 0; border-left:3mm solid #0aa3a0; padding-left:5mm; }
.title em { color:#0aa3a0; font-style:italic; }
.section-title { font-size:10pt; font-weight:bold; margin:5mm 0 2mm 0; font-family: dejavusans, sans-serif; }
table { width:100%; border-collapse:collapse; margin-bottom:4mm; }
th { background:#1a7a6e; color:#fff; font-size:9pt; padding:2.8mm 3mm; text-align:center; font-family: dejavusans, sans-serif; }
td { font-size:8.5pt; padding:2.5mm 3mm; border-bottom:0.15mm solid #e0eded; text-align:center; }
td.left { text-align:left; }
.footer { position:absolute; left:12mm; bottom:5mm; }
</style></head><body>
<div class="page">
<div class="title"><strong>Quote</strong> <em>Comparison</em></div>
' . $body . '
</div>
<div class="footer">' . $nhance . '</div>
</body></html>';
}
}