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 2–7 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}> */ 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}> */ 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 = '
'; } $nhance = is_file($nhanceLogo) ? '' : 'Nhance'; return '
 
' . $logoHtml . '
' . $this->escape($clientName) . '
Group Medical Insurance Quote
'; } /** * Render a coverage page (2-column: Particulars + value) matching original style. * * @param list $rows */ private function renderCoverageHtml(string $title, array $rows, string $nhanceLogo, bool $showTitle): string { $nhance = is_file($nhanceLogo) ? '' : 'Nhance'; $thead = 'ParticularsExpiring'; $tbody = ''; $i = 0; foreach ($rows as $row) { $bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa'; $tbody .= ''; $tbody .= '' . $this->escape($row['particular']) . ''; $tbody .= '' . $this->escape($row['value']) . ''; $tbody .= ''; $i++; } $titleBlock = $showTitle ? '
GMC ' . $this->escape(str_replace('GMC ', '', $title)) . '
' : '
GMC ' . $this->escape(str_replace('GMC ', '', $title)) . ' (continued)
'; return '
' . $titleBlock . ' ' . $thead . '' . $tbody . '
'; } /** * @param list}> $sections */ private function renderPremiumHtml(array $sections, string $nhanceLogo): string { $nhance = is_file($nhanceLogo) ? '' : 'Nhance'; $body = ''; foreach ($sections as $section) { $body .= '
' . $this->escape($section['title']) . '
'; $body .= ''; $i = 0; foreach ($section['rows'] as $row) { $bg = ($i % 2 === 0) ? '#ffffff' : '#f7fafa'; $body .= ''; $i++; } $body .= '
Insurer Name Premium GST Total Premium
' . $this->escape($row['insurer']) . ' ' . $this->escape($row['premium']) . ' ' . $this->escape($row['gst']) . ' ' . $this->escape($row['total']) . '
'; } return '
Quote Comparison
' . $body . '
'; } }