diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e1868f0f..3612ebe0 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -587,6 +587,8 @@ $routes->group("rfq", ["filter" => "authMVC"], function ($routes) { $routes->post("create", "LeadsController::createRFQ"); $routes->post("savePolicyInfo", "LeadsController::savePolicyInfo"); $routes->post("createQCR", "LeadsController::createQCR"); + $routes->post("generateGmcQcrPdf", "LeadsController::generateGmcQcrPdf"); + $routes->get("downloadGmcQcrPdf/(:num)", "LeadsController::downloadGmcQcrPdf/$1"); $routes->get("list/(:any)", "LeadsController::viewRFQ/$1"); $routes->get("nonEB","LeadsController::rfqNonEB"); // Non-EB dedicated RFQ/QCR endpoints (do not alter existing ones) diff --git a/app/Controllers/DigitMotorController.php b/app/Controllers/DigitMotorController.php index 7035a7f1..343e499a 100644 --- a/app/Controllers/DigitMotorController.php +++ b/app/Controllers/DigitMotorController.php @@ -78,6 +78,8 @@ class DigitMotorController extends BaseController $data['title'] = 'Digit Motor Journey'; $data['quote'] = null; $data['quote_id'] = null; + $data['vehicle_master'] = null; + $data['policyholder'] = []; if ($quoteId) { $data['quote'] = $this->quoteModel->getDetail((int) $quoteId); @@ -85,6 +87,17 @@ class DigitMotorController extends BaseController if (!$data['quote']) { return redirect()->to(base_url('digit-motor/list'))->with('error', 'Quote not found.'); } + $data['policyholder'] = is_array($data['quote']['policyholder_details'] ?? null) + ? $data['quote']['policyholder_details'] + : []; + $vcode = $data['quote']['vehicle']['vehicle_maincode'] ?? ''; + if ($vcode !== '') { + try { + $data['vehicle_master'] = $this->vehicleMaster->findActive((string) $vcode); + } catch (\Throwable $e) { + $data['vehicle_master'] = null; + } + } } return $this->loadLayout('digit_motor/journey', $data); @@ -347,6 +360,9 @@ class DigitMotorController extends BaseController 'message' => $e->getMessage(), 'digit_code' => $e->getDigitCode(), 'infra' => $e->isInfraError(), + 'data' => is_array($e->getResponseBody()) + ? ($e->getResponseBody()['_nhance_shell'] ?? null) + : null, ], min($http, 599)); } catch (\Throwable $e) { log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage()); diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index 1c1b154a..78ff86e1 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -38,6 +38,7 @@ use App\Helpers\MailHelper; use App\Helpers\ExcelMergeHelper; use App\Helpers\ExcelSanitizeHelper; use App\Libraries\GoogleSheetLib; +use App\Libraries\GmcQcrPdfService; class LeadsController extends BaseController { @@ -1840,6 +1841,59 @@ class LeadsController extends BaseController return $this->respond(['status' => false, 'id' => $result, 'message' => "Failed to create QCR", 'data' => $data], 200); } + /** + * Generate GMC Quote Comparison Report PDF from saved QCR data. + * POST /rfq/generateGmcQcrPdf body: lead_id + */ + public function generateGmcQcrPdf() + { + $leadId = (int) ($this->request->getPost('lead_id') ?? $this->request->getGet('lead_id') ?? 0); + if ($leadId <= 0) { + return $this->respond(['status' => false, 'message' => 'lead_id is required'], 200); + } + + $proposals = $this->request->getPost('proposals'); + if (! is_array($proposals)) { + $proposals = null; + } + + try { + $service = new GmcQcrPdfService($this->RFQModel, $this->leadsModel, $this->clientModel); + $result = $service->generate($leadId, $proposals); + return $this->respond($result, 200); + } catch (\Throwable $e) { + $this->myLogger->logme('error', 'generateGmcQcrPdf | ' . $e->getMessage()); + return $this->respond(['status' => false, 'message' => 'Failed to generate PDF: ' . $e->getMessage()], 200); + } + } + + /** + * Download last generated GMC QCR PDF. + * GET /rfq/downloadGmcQcrPdf/(:num) + */ + public function downloadGmcQcrPdf($leadId = null) + { + $leadId = (int) ($leadId ?? $this->request->getGet('lead_id') ?? 0); + if ($leadId <= 0) { + return $this->respond(['status' => false, 'message' => 'lead_id is required'], 400); + } + + try { + $service = new GmcQcrPdfService($this->RFQModel, $this->leadsModel, $this->clientModel); + $result = $service->resolveDownload($leadId); + if (! ($result['status'] ?? false)) { + return $this->respond($result, 404); + } + + return $this->response + ->download($result['file_path'], null) + ->setFileName($result['download_name']); + } catch (\Throwable $e) { + $this->myLogger->logme('error', 'downloadGmcQcrPdf | ' . $e->getMessage()); + return $this->respond(['status' => false, 'message' => 'Failed to download PDF: ' . $e->getMessage()], 500); + } + } + public function reorderProposalsByInsurerTotal(array $data): array { diff --git a/app/Database/digit_motor_tables.sql b/app/Database/digit_motor_tables.sql index 2bc7a99b..74e5675f 100644 --- a/app/Database/digit_motor_tables.sql +++ b/app/Database/digit_motor_tables.sql @@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS motor_quote ( end_date DATE DEFAULT NULL, pincode VARCHAR(6) NOT NULL, coverage_details JSON DEFAULT NULL, + policyholder_details JSON DEFAULT NULL, premium DECIMAL(12,2) DEFAULT NULL, idv DECIMAL(12,2) DEFAULT NULL, status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', diff --git a/app/Libraries/DigitMotor/DigitExecutorService.php b/app/Libraries/DigitMotor/DigitExecutorService.php index a9198297..3f6e19a3 100644 --- a/app/Libraries/DigitMotor/DigitExecutorService.php +++ b/app/Libraries/DigitMotor/DigitExecutorService.php @@ -41,17 +41,54 @@ class DigitExecutorService */ public function quickQuote(array $input): array { - $enquiryId = $input['enquiry_id'] ?? ('NH' . date('ymdHis') . random_int(100, 999)); - $payload = $this->buildQuickQuotePayload($input, $enquiryId); + $quoteIdIn = (int) ($input['quote_id'] ?? 0); + $existing = $quoteIdIn > 0 ? $this->quoteModel->find($quoteIdIn) : null; + // Prefer existing enquiry when updating; only mint a new one for brand-new journeys. + $enquiryId = null; + if ($existing && !empty($existing['enquiry_id'])) { + $enquiryId = (string) $existing['enquiry_id']; + } elseif (!empty($input['enquiry_id']) && $input['enquiry_id'] !== '— new —') { + $enquiryId = (string) $input['enquiry_id']; + if (!$existing) { + $existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first(); + } + } + if ($enquiryId === null || $enquiryId === '') { + $enquiryId = 'NH' . date('ymdHis') . random_int(100, 999); + } + + $input['quote_id'] = $existing ? (int) $existing['id'] : $quoteIdIn; + $input['enquiry_id'] = $enquiryId; + + $payload = $this->buildQuickQuotePayload($input, $enquiryId); $quoteId = $this->persistQuoteShell($input, $enquiryId, 'DRAFT'); - $response = $this->api->post( - $this->config->executorPath, - $payload, - $this->config->integrationIds['quickQuote'], - $quoteId - ); + try { + $response = $this->api->post( + $this->config->executorPath, + $payload, + $this->config->integrationIds['quickQuote'], + $quoteId + ); + } catch (DigitApiException $e) { + // Keep shell continuity on Digit failures so retries do not spawn duplicates. + throw new DigitApiException( + $e->getMessage(), + $e->getDigitCode(), + $e->getHttpStatus(), + [ + '_nhance_shell' => [ + 'quote_id' => $quoteId, + 'enquiry_id' => $enquiryId, + ], + 'digit' => $e->getResponseBody(), + ], + $e, + $e->getRequestUrl(), + $e->getRequestBody() + ); + } $premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']); $idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']); @@ -61,10 +98,13 @@ class DigitExecutorService $vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? []; + // Re-quote resets create/KYC progress so journey stays consistent. $this->quoteModel->update($quoteId, [ - 'premium' => $premium, - 'idv' => $idv, - 'status' => 'QUOTED', + 'premium' => $premium, + 'idv' => $idv, + 'status' => 'QUOTED', + 'quote_number' => null, + 'application_id' => null, 'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []), ]); @@ -123,6 +163,21 @@ class DigitExecutorService $input['vehicle_identification_number'] = $vin; $input['engine_number'] = $engine; + $policyholder = [ + 'first_name' => $input['first_name'] ?? null, + 'last_name' => $input['last_name'] ?? null, + 'mobile' => $input['mobile'] ?? null, + 'email' => $input['email'] ?? null, + 'pan' => $input['pan'] ?? null, + 'dob' => $input['dob'] ?? null, + 'address' => $input['address'] ?? null, + ]; + // Persist locally before Digit call so Back/reload keeps form data even if API fails. + $this->quoteModel->update($quoteId, [ + 'policyholder_details' => json_encode($policyholder), + 'coverage_details' => json_encode($input['coverages'] ?? ($detail['coverage_details'] ?? [])), + ]); + $payload = $this->buildCreateQuotePayload($detail, $input); $response = $this->api->post( @@ -137,12 +192,13 @@ class DigitExecutorService $premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']) ?? $detail['premium']; $this->quoteModel->update($quoteId, [ - 'quote_number' => $quoteNumber, - 'application_id' => $applicationId, - 'premium' => $premium, - 'start_date' => $input['start_date'] ?? $detail['start_date'], - 'end_date' => $input['end_date'] ?? $detail['end_date'], - 'status' => 'CREATED', + 'quote_number' => $quoteNumber, + 'application_id' => $applicationId, + 'premium' => $premium, + 'start_date' => $input['start_date'] ?? $detail['start_date'], + 'end_date' => $input['end_date'] ?? $detail['end_date'], + 'status' => 'CREATED', + 'policyholder_details' => json_encode($policyholder), ]); return [ @@ -472,10 +528,18 @@ class DigitExecutorService protected function persistQuoteShell(array $input, string $enquiryId, string $status): int { - $existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first(); + $quoteIdIn = (int) ($input['quote_id'] ?? 0); + $existing = null; + + if ($quoteIdIn > 0) { + $existing = $this->quoteModel->find($quoteIdIn); + } + if (!$existing && $enquiryId !== '') { + $existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first(); + } $quoteData = [ - 'enquiry_id' => $enquiryId, + 'enquiry_id' => $existing['enquiry_id'] ?? $enquiryId, 'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL', 'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'), 'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'), @@ -487,9 +551,13 @@ class DigitExecutorService 'end_date' => $input['end_date'] ?? null, 'pincode' => (string) ($input['pincode'] ?? ''), 'coverage_details' => json_encode($input['coverages'] ?? []), - 'status' => $status, ]; + // Only stamp DRAFT on brand-new rows; existing rows keep status until QQ success. + if (!$existing) { + $quoteData['status'] = $status; + } + if ($existing) { $quoteId = (int) $existing['id']; $this->quoteModel->update($quoteId, $quoteData); diff --git a/app/Libraries/GmcQcrPdfService.php b/app/Libraries/GmcQcrPdfService.php new file mode 100644 index 00000000..22ecea63 --- /dev/null +++ b/app/Libraries/GmcQcrPdfService.php @@ -0,0 +1,582 @@ +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 NamePremiumGSTTotal Premium
' . $this->escape($row['insurer']) . '' . $this->escape($row['premium']) . '' . $this->escape($row['gst']) . '' . $this->escape($row['total']) . '
'; + } + + return ' + +
+
Quote Comparison
+ ' . $body . ' +
+ +'; + } +} diff --git a/app/Models/MotorQuoteModel.php b/app/Models/MotorQuoteModel.php index 90d52d55..3e817dcb 100644 --- a/app/Models/MotorQuoteModel.php +++ b/app/Models/MotorQuoteModel.php @@ -17,7 +17,7 @@ class MotorQuoteModel extends Model 'insurance_product_code', 'sub_insurance_product_code', 'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number', 'is_ncb_transfer', 'start_date', 'end_date', 'pincode', - 'coverage_details', 'premium', 'idv', 'status', + 'coverage_details', 'policyholder_details', 'premium', 'idv', 'status', 'created_at', 'updated_at', ]; protected $useTimestamps = true; @@ -67,6 +67,12 @@ class MotorQuoteModel extends Model if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) { $quote['coverage_details'] = json_decode($quote['coverage_details'], true); } + if (!empty($quote['policyholder_details']) && is_string($quote['policyholder_details'])) { + $quote['policyholder_details'] = json_decode($quote['policyholder_details'], true); + } + if (!is_array($quote['policyholder_details'] ?? null)) { + $quote['policyholder_details'] = []; + } return $quote; } diff --git a/app/Views/digit_motor/journey.php b/app/Views/digit_motor/journey.php index 9bbdae10..98aecdb6 100644 --- a/app/Views/digit_motor/journey.php +++ b/app/Views/digit_motor/journey.php @@ -6,6 +6,11 @@ $kyc = $quote['kyc'] ?? []; $payment = $quote['payment'] ?? []; $policy = $quote['policy'] ?? []; $status = $quote['status'] ?? 'DRAFT'; +$policyholder = $policyholder ?? ($quote['policyholder_details'] ?? []); +if (!is_array($policyholder)) { + $policyholder = []; +} +$vehicleMaster = $vehicle_master ?? null; $statusToStep = [ 'DRAFT' => 0, @@ -17,6 +22,18 @@ $statusToStep = [ ]; $initialStep = $statusToStep[$status] ?? 0; $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_details'] : []; +// Normalize addon flags from either UI-shaped or Digit-shaped coverage JSON +$addonOn = [ + 'personal_accident' => !empty($coverage['personal_accident']) || !empty($coverage['personalAccident']['selection']), + 'zero_dep' => !empty($coverage['zero_dep']) || !empty($coverage['addons']['partsDepreciation']['selection']), + 'engine_protect' => !empty($coverage['engine_protect']) || !empty($coverage['addons']['engineProtection']['selection']), + 'rsa' => !empty($coverage['rsa']) || !empty($coverage['addons']['roadSideAssistance']['selection']), + 'consumables' => !empty($coverage['consumables']) || !empty($coverage['addons']['consumables']['selection']), + 'key_protect' => !empty($coverage['key_protect']) || !empty($coverage['addons']['keyAndLockProtect']['selection']), +]; +if ($coverage === []) { + $addonOn['personal_accident'] = true; // default matches chip markup +} ?>