ppd & Go digit

This commit is contained in:
Gowtham M 2026-07-30 14:03:40 +05:30
parent bd10f8800f
commit 767f154010
12 changed files with 999 additions and 42 deletions

View File

@ -587,6 +587,8 @@ $routes->group("rfq", ["filter" => "authMVC"], function ($routes) {
$routes->post("create", "LeadsController::createRFQ"); $routes->post("create", "LeadsController::createRFQ");
$routes->post("savePolicyInfo", "LeadsController::savePolicyInfo"); $routes->post("savePolicyInfo", "LeadsController::savePolicyInfo");
$routes->post("createQCR", "LeadsController::createQCR"); $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("list/(:any)", "LeadsController::viewRFQ/$1");
$routes->get("nonEB","LeadsController::rfqNonEB"); $routes->get("nonEB","LeadsController::rfqNonEB");
// Non-EB dedicated RFQ/QCR endpoints (do not alter existing ones) // Non-EB dedicated RFQ/QCR endpoints (do not alter existing ones)

View File

@ -78,6 +78,8 @@ class DigitMotorController extends BaseController
$data['title'] = 'Digit Motor Journey'; $data['title'] = 'Digit Motor Journey';
$data['quote'] = null; $data['quote'] = null;
$data['quote_id'] = null; $data['quote_id'] = null;
$data['vehicle_master'] = null;
$data['policyholder'] = [];
if ($quoteId) { if ($quoteId) {
$data['quote'] = $this->quoteModel->getDetail((int) $quoteId); $data['quote'] = $this->quoteModel->getDetail((int) $quoteId);
@ -85,6 +87,17 @@ class DigitMotorController extends BaseController
if (!$data['quote']) { if (!$data['quote']) {
return redirect()->to(base_url('digit-motor/list'))->with('error', 'Quote not found.'); 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); return $this->loadLayout('digit_motor/journey', $data);
@ -347,6 +360,9 @@ class DigitMotorController extends BaseController
'message' => $e->getMessage(), 'message' => $e->getMessage(),
'digit_code' => $e->getDigitCode(), 'digit_code' => $e->getDigitCode(),
'infra' => $e->isInfraError(), 'infra' => $e->isInfraError(),
'data' => is_array($e->getResponseBody())
? ($e->getResponseBody()['_nhance_shell'] ?? null)
: null,
], min($http, 599)); ], min($http, 599));
} catch (\Throwable $e) { } catch (\Throwable $e) {
log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage()); log_message('error', 'DigitMotor unexpected error: ' . $e->getMessage());

View File

@ -38,6 +38,7 @@ use App\Helpers\MailHelper;
use App\Helpers\ExcelMergeHelper; use App\Helpers\ExcelMergeHelper;
use App\Helpers\ExcelSanitizeHelper; use App\Helpers\ExcelSanitizeHelper;
use App\Libraries\GoogleSheetLib; use App\Libraries\GoogleSheetLib;
use App\Libraries\GmcQcrPdfService;
class LeadsController extends BaseController 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); 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 public function reorderProposalsByInsurerTotal(array $data): array
{ {

View File

@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS motor_quote (
end_date DATE DEFAULT NULL, end_date DATE DEFAULT NULL,
pincode VARCHAR(6) NOT NULL, pincode VARCHAR(6) NOT NULL,
coverage_details JSON DEFAULT NULL, coverage_details JSON DEFAULT NULL,
policyholder_details JSON DEFAULT NULL,
premium DECIMAL(12,2) DEFAULT NULL, premium DECIMAL(12,2) DEFAULT NULL,
idv DECIMAL(12,2) DEFAULT NULL, idv DECIMAL(12,2) DEFAULT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT', status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',

View File

@ -41,17 +41,54 @@ class DigitExecutorService
*/ */
public function quickQuote(array $input): array public function quickQuote(array $input): array
{ {
$enquiryId = $input['enquiry_id'] ?? ('NH' . date('ymdHis') . random_int(100, 999)); $quoteIdIn = (int) ($input['quote_id'] ?? 0);
$payload = $this->buildQuickQuotePayload($input, $enquiryId); $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'); $quoteId = $this->persistQuoteShell($input, $enquiryId, 'DRAFT');
try {
$response = $this->api->post( $response = $this->api->post(
$this->config->executorPath, $this->config->executorPath,
$payload, $payload,
$this->config->integrationIds['quickQuote'], $this->config->integrationIds['quickQuote'],
$quoteId $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']); $premium = $this->pickNumber($response, ['grossPremium', 'premium', 'netPremium', 'totalPremium']);
$idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']); $idv = $this->pickNumber($response, ['idv', 'vehicleIDV', 'insuredDeclaredValue']);
@ -61,10 +98,13 @@ class DigitExecutorService
$vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? []; $vehicleIdv = $response['vehicle']['vehicleIDV'] ?? $response['vehicleIDV'] ?? [];
// Re-quote resets create/KYC progress so journey stays consistent.
$this->quoteModel->update($quoteId, [ $this->quoteModel->update($quoteId, [
'premium' => $premium, 'premium' => $premium,
'idv' => $idv, 'idv' => $idv,
'status' => 'QUOTED', 'status' => 'QUOTED',
'quote_number' => null,
'application_id' => null,
'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []), 'coverage_details' => json_encode($input['coverages'] ?? $payload['contract']['coverages'] ?? []),
]); ]);
@ -123,6 +163,21 @@ class DigitExecutorService
$input['vehicle_identification_number'] = $vin; $input['vehicle_identification_number'] = $vin;
$input['engine_number'] = $engine; $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); $payload = $this->buildCreateQuotePayload($detail, $input);
$response = $this->api->post( $response = $this->api->post(
@ -143,6 +198,7 @@ class DigitExecutorService
'start_date' => $input['start_date'] ?? $detail['start_date'], 'start_date' => $input['start_date'] ?? $detail['start_date'],
'end_date' => $input['end_date'] ?? $detail['end_date'], 'end_date' => $input['end_date'] ?? $detail['end_date'],
'status' => 'CREATED', 'status' => 'CREATED',
'policyholder_details' => json_encode($policyholder),
]); ]);
return [ return [
@ -472,10 +528,18 @@ class DigitExecutorService
protected function persistQuoteShell(array $input, string $enquiryId, string $status): int protected function persistQuoteShell(array $input, string $enquiryId, string $status): int
{ {
$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(); $existing = $this->quoteModel->where('enquiry_id', $enquiryId)->first();
}
$quoteData = [ $quoteData = [
'enquiry_id' => $enquiryId, 'enquiry_id' => $existing['enquiry_id'] ?? $enquiryId,
'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL', 'policy_holder_type' => $input['policy_holder_type'] ?? 'INDIVIDUAL',
'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'), 'insurance_product_code' => (string) ($input['insurance_product_code'] ?? '20101'),
'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'), 'sub_insurance_product_code' => (string) ($input['sub_insurance_product_code'] ?? 'PB'),
@ -487,9 +551,13 @@ class DigitExecutorService
'end_date' => $input['end_date'] ?? null, 'end_date' => $input['end_date'] ?? null,
'pincode' => (string) ($input['pincode'] ?? ''), 'pincode' => (string) ($input['pincode'] ?? ''),
'coverage_details' => json_encode($input['coverages'] ?? []), '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) { if ($existing) {
$quoteId = (int) $existing['id']; $quoteId = (int) $existing['id'];
$this->quoteModel->update($quoteId, $quoteData); $this->quoteModel->update($quoteId, $quoteData);

View File

@ -0,0 +1,582 @@
<?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>';
}
}

View File

@ -17,7 +17,7 @@ class MotorQuoteModel extends Model
'insurance_product_code', 'sub_insurance_product_code', 'insurance_product_code', 'sub_insurance_product_code',
'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number', 'previous_insurer_code', 'previous_policy_expiry_date', 'external_policy_number',
'is_ncb_transfer', 'start_date', 'end_date', 'pincode', 'is_ncb_transfer', 'start_date', 'end_date', 'pincode',
'coverage_details', 'premium', 'idv', 'status', 'coverage_details', 'policyholder_details', 'premium', 'idv', 'status',
'created_at', 'updated_at', 'created_at', 'updated_at',
]; ];
protected $useTimestamps = true; protected $useTimestamps = true;
@ -67,6 +67,12 @@ class MotorQuoteModel extends Model
if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) { if (!empty($quote['coverage_details']) && is_string($quote['coverage_details'])) {
$quote['coverage_details'] = json_decode($quote['coverage_details'], true); $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; return $quote;
} }

View File

@ -6,6 +6,11 @@ $kyc = $quote['kyc'] ?? [];
$payment = $quote['payment'] ?? []; $payment = $quote['payment'] ?? [];
$policy = $quote['policy'] ?? []; $policy = $quote['policy'] ?? [];
$status = $quote['status'] ?? 'DRAFT'; $status = $quote['status'] ?? 'DRAFT';
$policyholder = $policyholder ?? ($quote['policyholder_details'] ?? []);
if (!is_array($policyholder)) {
$policyholder = [];
}
$vehicleMaster = $vehicle_master ?? null;
$statusToStep = [ $statusToStep = [
'DRAFT' => 0, 'DRAFT' => 0,
@ -17,6 +22,18 @@ $statusToStep = [
]; ];
$initialStep = $statusToStep[$status] ?? 0; $initialStep = $statusToStep[$status] ?? 0;
$coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_details'] : []; $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
}
?> ?>
<style> <style>
:root{ :root{
@ -105,8 +122,10 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
.dm-section:first-of-type{margin-top:0;padding-top:0;border-top:none;} .dm-section:first-of-type{margin-top:0;padding-top:0;border-top:none;}
.dm-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px 16px;} .dm-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px 16px;}
.dm-grid.g3{grid-template-columns:repeat(3,1fr);} .dm-grid.g3{grid-template-columns:repeat(3,1fr);}
.dm-grid.g4{grid-template-columns:repeat(4,1fr);}
.dm-field{display:flex;flex-direction:column;gap:6px;} .dm-field{display:flex;flex-direction:column;gap:6px;}
.dm-field.full{grid-column:1/-1;} .dm-field.full{grid-column:1/-1;}
.dm-field.span2{grid-column:span 2;}
.dm-field label{font-size:12px;color:var(--dm-muted);font-weight:500;margin:0;} .dm-field label{font-size:12px;color:var(--dm-muted);font-weight:500;margin:0;}
.dm-field label .req{color:var(--dm-red);margin-left:2px;font-weight:700;} .dm-field label .req{color:var(--dm-red);margin-left:2px;font-weight:700;}
.dm-field input,.dm-field select{ .dm-field input,.dm-field select{
@ -188,7 +207,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
.dm-suggest .meta{color:var(--dm-muted);font-size:11px;} .dm-suggest .meta{color:var(--dm-muted);font-size:11px;}
.dm-vcode-tag{font-family:monospace;font-size:12px;color:var(--dm-teal-dark);margin-top:4px;} .dm-vcode-tag{font-family:monospace;font-size:12px;color:var(--dm-teal-dark);margin-top:4px;}
@media(max-width:768px){ @media(max-width:768px){
.dm-grid,.dm-grid.g3,.dm-quote-cards{grid-template-columns:1fr;} .dm-grid,.dm-grid.g3,.dm-grid.g4,.dm-quote-cards{grid-template-columns:1fr;}
.dm-stop{width:52px;} .dm-stop{width:52px;}
.dm-stop-label{font-size:10px;} .dm-stop-label{font-size:10px;}
.dm-road{left:22px;right:22px;} .dm-road{left:22px;right:22px;}
@ -227,7 +246,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div> </div>
<div class="dm-section">Vehicle</div> <div class="dm-section">Vehicle</div>
<div class="dm-grid g3"> <div class="dm-grid g4">
<div class="dm-field"><label>Registration number <span class="req">*</span></label><input id="f_reg" value="<?= esc($vehicle['license_plate_number'] ?? '') ?>" placeholder="GJ04DA8726" data-required="1" data-label="Registration number"><div class="dm-err">Required</div></div> <div class="dm-field"><label>Registration number <span class="req">*</span></label><input id="f_reg" value="<?= esc($vehicle['license_plate_number'] ?? '') ?>" placeholder="GJ04DA8726" data-required="1" data-label="Registration number"><div class="dm-err">Required</div></div>
<div class="dm-field"> <div class="dm-field">
<label>Make <span class="req">*</span></label> <label>Make <span class="req">*</span></label>
@ -268,7 +287,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div> </div>
<div class="dm-section">Previous policy</div> <div class="dm-section">Previous policy</div>
<div class="dm-grid g3"> <div class="dm-grid g4">
<div class="dm-field"><label>Previous insurer</label> <div class="dm-field"><label>Previous insurer</label>
<select id="f_prev_ins"><option value=""> none / unknown </option></select> <select id="f_prev_ins"><option value=""> none / unknown </option></select>
</div> </div>
@ -289,12 +308,12 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
<div class="dm-section">Coverage add-ons</div> <div class="dm-section">Coverage add-ons</div>
<div class="dm-chips" id="addonChips"> <div class="dm-chips" id="addonChips">
<div class="dm-chip on" data-key="personal_accident" onclick="this.classList.toggle('on')">Personal accident</div> <div class="dm-chip<?= !empty($addonOn['personal_accident']) ? ' on' : '' ?>" data-key="personal_accident" onclick="this.classList.toggle('on')">Personal accident</div>
<div class="dm-chip" data-key="zero_dep" onclick="this.classList.toggle('on')">Zero depreciation</div> <div class="dm-chip<?= !empty($addonOn['zero_dep']) ? ' on' : '' ?>" data-key="zero_dep" onclick="this.classList.toggle('on')">Zero depreciation</div>
<div class="dm-chip" data-key="engine_protect" onclick="this.classList.toggle('on')">Engine protect</div> <div class="dm-chip<?= !empty($addonOn['engine_protect']) ? ' on' : '' ?>" data-key="engine_protect" onclick="this.classList.toggle('on')">Engine protect</div>
<div class="dm-chip" data-key="rsa" onclick="this.classList.toggle('on')">Roadside assistance</div> <div class="dm-chip<?= !empty($addonOn['rsa']) ? ' on' : '' ?>" data-key="rsa" onclick="this.classList.toggle('on')">Roadside assistance</div>
<div class="dm-chip" data-key="consumables" onclick="this.classList.toggle('on')">Consumables</div> <div class="dm-chip<?= !empty($addonOn['consumables']) ? ' on' : '' ?>" data-key="consumables" onclick="this.classList.toggle('on')">Consumables</div>
<div class="dm-chip" data-key="key_protect" onclick="this.classList.toggle('on')">Key & lock</div> <div class="dm-chip<?= !empty($addonOn['key_protect']) ? ' on' : '' ?>" data-key="key_protect" onclick="this.classList.toggle('on')">Key & lock</div>
</div> </div>
<div class="dm-actions"> <div class="dm-actions">
@ -330,14 +349,14 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div> </div>
<div class="dm-section">Policyholder</div> <div class="dm-section">Policyholder</div>
<div class="dm-grid"> <div class="dm-grid g4">
<div class="dm-field"><label>First name <span class="req">*</span></label><input id="f_fname" value="" data-required="1" data-label="First name"><div class="dm-err">Required</div></div> <div class="dm-field"><label>First name <span class="req">*</span></label><input id="f_fname" value="<?= esc($policyholder['first_name'] ?? '') ?>" data-required="1" data-label="First name"><div class="dm-err">Required</div></div>
<div class="dm-field"><label>Last name</label><input id="f_lname" value=""></div> <div class="dm-field"><label>Last name</label><input id="f_lname" value="<?= esc($policyholder['last_name'] ?? '') ?>"></div>
<div class="dm-field"><label>Mobile <span class="req">*</span></label><input id="f_mobile" value="" data-required="1" data-label="Mobile" data-len="10"><div class="dm-err">Required (10 digits)</div></div> <div class="dm-field"><label>Mobile <span class="req">*</span></label><input id="f_mobile" value="<?= esc($policyholder['mobile'] ?? '') ?>" data-required="1" data-label="Mobile" data-len="10"><div class="dm-err">Required (10 digits)</div></div>
<div class="dm-field"><label>Email <span class="req">*</span></label><input id="f_email" type="email" value="" data-required="1" data-label="Email"><div class="dm-err">Required</div></div> <div class="dm-field"><label>Email <span class="req">*</span></label><input id="f_email" type="email" value="<?= esc($policyholder['email'] ?? '') ?>" data-required="1" data-label="Email"><div class="dm-err">Required</div></div>
<div class="dm-field"><label>PAN <span class="req">*</span></label><input id="f_pan" value="" data-required="1" data-label="PAN"><div class="dm-err">Required</div></div> <div class="dm-field"><label>PAN <span class="req">*</span></label><input id="f_pan" value="<?= esc($policyholder['pan'] ?? '') ?>" data-required="1" data-label="PAN"><div class="dm-err">Required</div></div>
<div class="dm-field"><label>Date of birth <span class="req">*</span></label><input type="date" id="f_dob" value="" data-required="1" data-label="Date of birth"><div class="dm-err">Required</div></div> <div class="dm-field"><label>Date of birth <span class="req">*</span></label><input type="date" id="f_dob" value="<?= esc($policyholder['dob'] ?? '') ?>" data-required="1" data-label="Date of birth"><div class="dm-err">Required</div></div>
<div class="dm-field full"><label>Address <span class="req">*</span></label><input id="f_address" value="" data-required="1" data-label="Address"><div class="dm-err">Required</div></div> <div class="dm-field span2"><label>Address <span class="req">*</span></label><input id="f_address" value="<?= esc($policyholder['address'] ?? '') ?>" data-required="1" data-label="Address"><div class="dm-err">Required</div></div>
</div> </div>
<div class="dm-actions"> <div class="dm-actions">
@ -470,11 +489,16 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
<script> <script>
window.DM = { window.DM = {
quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>, quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>,
enquiryId: <?= json_encode($quote['enquiry_id'] ?? null) ?>,
base: '<?= rtrim(base_url('digit-motor'), '/') ?>', base: '<?= rtrim(base_url('digit-motor'), '/') ?>',
step: <?= (int)$initialStep ?>, step: <?= (int)$initialStep ?>,
selectedProduct: '<?= esc($quote['insurance_product_code'] ?? '20102') ?>', selectedProduct: '<?= esc($quote['insurance_product_code'] ?? '20102') ?>',
selectedInsurer: '<?= esc($quote['previous_insurer_code'] ?? '') ?>', selectedInsurer: '<?= esc($quote['previous_insurer_code'] ?? '') ?>',
selectedVehicleCode: '<?= esc($vehicle['vehicle_maincode'] ?? '') ?>' selectedVehicleCode: '<?= esc($vehicle['vehicle_maincode'] ?? '') ?>',
vehicleMake: <?= json_encode($vehicleMaster['make'] ?? '') ?>,
vehicleModel: <?= json_encode($vehicleMaster['model'] ?? '') ?>,
vehicleVariant: <?= json_encode($vehicleMaster['variant'] ?? '') ?>,
busy: false
}; };
const dmSteps = ["Quote","Create","KYC","Pay","Policy"]; const dmSteps = ["Quote","Create","KYC","Pay","Policy"];
@ -495,8 +519,23 @@ let dmSearchTimer = null;
dmGo(window.DM.step, true); dmGo(window.DM.step, true);
dmLoadMasters(); dmLoadMasters();
dmBindMasterUi(); dmBindMasterUi();
dmHydrateVehicle();
})(); })();
function dmHydrateVehicle(){
const make = window.DM.vehicleMake || '';
const model = window.DM.vehicleModel || '';
const code = window.DM.selectedVehicleCode || '';
if (!code && !make) return;
if (make) {
$('#f_make').val(make);
dmLoadModels(make, model, code);
} else if (code) {
$('#f_vcode').val(code);
$('#f_vcode_tag').text('Code ' + code);
}
}
function dmFillSelect(sel, items, valueKey, labelFn, selected, emptyLabel){ function dmFillSelect(sel, items, valueKey, labelFn, selected, emptyLabel){
const $s = $(sel); const $s = $(sel);
$s.empty(); $s.empty();
@ -694,7 +733,18 @@ function dmAlert(msg, ok){
} }
function dmBusy(on){ function dmBusy(on){
document.getElementById('dmPanel').classList.toggle('dm-loading', !!on); window.DM.busy = !!on;
const panel = document.getElementById('dmPanel');
if (panel) panel.classList.toggle('dm-loading', !!on);
// NHANCE global page loader (layout header)
if (on) {
$('.loader').show();
$('.loader-mask').fadeIn();
} else {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
}
} }
function dmAddonFlags(){ function dmAddonFlags(){
@ -777,6 +827,8 @@ function dmValidateRequired(fieldIds){
} }
function dmQuickQuote(){ function dmQuickQuote(){
if (window.DM.busy) return;
const claimChip = document.querySelector('.dm-chip[data-group="claim"].on'); const claimChip = document.querySelector('.dm-chip[data-group="claim"].on');
const missing = dmValidateRequired([ const missing = dmValidateRequired([
'f_reg', 'f_make', 'f_model', 'f_variant', 'f_vcode', 'f_reg', 'f_make', 'f_model', 'f_variant', 'f_vcode',
@ -789,7 +841,13 @@ function dmQuickQuote(){
return; return;
} }
const tagEnquiry = ($('#enquiryIdTag').text() || '').trim();
const enquiryId = window.DM.enquiryId
|| (tagEnquiry && tagEnquiry !== '— new —' ? tagEnquiry : null);
const body = { const body = {
quote_id: window.DM.quoteId || null,
enquiry_id: enquiryId,
license_plate_number: $('#f_reg').val().trim(), license_plate_number: $('#f_reg').val().trim(),
vehicle_maincode: $('#f_vcode').val().trim(), vehicle_maincode: $('#f_vcode').val().trim(),
registration_date: $('#f_reg_date').val(), registration_date: $('#f_reg_date').val(),
@ -805,8 +863,7 @@ function dmQuickQuote(){
previous_ncb: $('#f_prev_ncb').val() || 'ZERO', previous_ncb: $('#f_prev_ncb').val() || 'ZERO',
previous_policy_type: $('#f_prev_ptype').val() || '', previous_policy_type: $('#f_prev_ptype').val() || '',
is_claim_in_last_year: claimChip ? claimChip.dataset.val === '1' : false, is_claim_in_last_year: claimChip ? claimChip.dataset.val === '1' : false,
coverages: dmAddonFlags(), coverages: dmAddonFlags()
enquiry_id: window.DM.quoteId ? ($('#enquiryIdTag').text().trim() !== '— new —' ? $('#enquiryIdTag').text().trim() : null) : null
}; };
if (body.start_date){ if (body.start_date){
@ -821,6 +878,7 @@ function dmQuickQuote(){
.done(function(res){ .done(function(res){
if (!res.status){ dmAlert(res.message || 'Quick quote failed'); return; } if (!res.status){ dmAlert(res.message || 'Quick quote failed'); return; }
window.DM.quoteId = res.data.quote_id; window.DM.quoteId = res.data.quote_id;
window.DM.enquiryId = res.data.enquiry_id;
$('#enquiryIdTag').text(res.data.enquiry_id); $('#enquiryIdTag').text(res.data.enquiry_id);
const prem = res.data.premium != null ? Number(res.data.premium) : null; const prem = res.data.premium != null ? Number(res.data.premium) : null;
const idv = res.data.idv != null ? Number(res.data.idv) : null; const idv = res.data.idv != null ? Number(res.data.idv) : null;
@ -840,12 +898,21 @@ function dmQuickQuote(){
const parts = Object.keys(errs).map(function(k){ return errs[k]; }); const parts = Object.keys(errs).map(function(k){ return errs[k]; });
if (parts.length) msg = parts.join(' | '); if (parts.length) msg = parts.join(' | ');
} }
// If shell was created, keep continuity for retry
if (xhr.responseJSON && xhr.responseJSON.data) {
if (xhr.responseJSON.data.quote_id) window.DM.quoteId = xhr.responseJSON.data.quote_id;
if (xhr.responseJSON.data.enquiry_id) {
window.DM.enquiryId = xhr.responseJSON.data.enquiry_id;
$('#enquiryIdTag').text(xhr.responseJSON.data.enquiry_id);
}
}
dmAlert(msg); dmAlert(msg);
}) })
.always(function(){ dmBusy(false); }); .always(function(){ dmBusy(false); });
} }
function dmCreateQuote(){ function dmCreateQuote(){
if (window.DM.busy) return;
if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; } if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; }
// Keep QQ + CQ chassis/engine in sync when editing on step 2 // Keep QQ + CQ chassis/engine in sync when editing on step 2

View File

@ -73,6 +73,8 @@ function openFilterNav(){ document.getElementById('digit-filter-sidebar').style.
function closeFilterNav(){ document.getElementById('digit-filter-sidebar').style.width = '0'; } function closeFilterNav(){ document.getElementById('digit-filter-sidebar').style.width = '0'; }
function applyFilters(){ function applyFilters(){
$('.loader').show();
$('.loader-mask').fadeIn();
$.ajax({ $.ajax({
url: '<?= base_url('digit-motor/list') ?>', url: '<?= base_url('digit-motor/list') ?>',
type: 'POST', type: 'POST',
@ -92,6 +94,10 @@ function applyFilters(){
error: function(){ error: function(){
if (window.toastr) toastr.error('Failed to load filtered list.', 'Error'); if (window.toastr) toastr.error('Failed to load filtered list.', 'Error');
else alert('Failed to load filtered list.'); else alert('Failed to load filtered list.');
},
complete: function(){
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
} }
}); });
} }

View File

@ -535,6 +535,24 @@
<button id="submitPlacement" class="btn btn-primary" onclick="checkTheTableDataChanged(6)">Placement</button> <button id="submitPlacement" class="btn btn-primary" onclick="checkTheTableDataChanged(6)">Placement</button>
<button id="viewDemography" class="btn btn-primary" onclick="viewDemography()">Demography</button> <button id="viewDemography" class="btn btn-primary" onclick="viewDemography()">Demography</button>
<button id="viewDocs" class="btn btn-primary" onclick="viewDocs()">Docs</button> <button id="viewDocs" class="btn btn-primary" onclick="viewDocs()">Docs</button>
<?php if (in_array((int) ($lead_data['policy_type_id'] ?? 0), [2, 3, 4, 5], true)) { ?>
<div class="btn-group" id="pptBtnGroup" style="display:none;">
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
PPT
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="#" onclick="startPptGeneration(); return false;">Generate</a>
<a class="dropdown-item" href="#" onclick="downloadGmcQcrPdf(); return false;">Download</a>
</div>
</div>
<?php } ?>
<!-- Floating Generate PDF button (hidden until proposal selection mode) -->
<div id="pptFloatingBar" style="display:none; position:fixed; bottom:20px; left:50%; transform:translateX(-50%); z-index:9999; background:#fff; border-radius:8px; box-shadow:0 4px 20px rgba(0,0,0,0.25); padding:10px 20px; text-align:center;">
<span style="font-size:13px; color:#555; margin-right:10px;">Select proposals to include in PDF, then click:</span>
<button type="button" class="btn btn-success" onclick="confirmGeneratePdf()">Generate PDF</button>
<button type="button" class="btn btn-secondary ml-2" onclick="cancelPptSelection()">Cancel</button>
</div>
<input type="hidden" id="lead_id" name="lead_id" value="<?= isset($lead_id) ? $lead_id : '' ?>"> <input type="hidden" id="lead_id" name="lead_id" value="<?= isset($lead_id) ? $lead_id : '' ?>">
<input type="hidden" id="rfq_primaryKey" name="rfq_primaryKey" value="<?= isset($rfq_data['id']) ? $rfq_data['id'] : '' ?>"> <input type="hidden" id="rfq_primaryKey" name="rfq_primaryKey" value="<?= isset($rfq_data['id']) ? $rfq_data['id'] : '' ?>">
<input type="hidden" id="qcr_count" value="<?= isset($qcr_count) ? $qcr_count : 0 ?>"> <input type="hidden" id="qcr_count" value="<?= isset($qcr_count) ? $qcr_count : 0 ?>">
@ -1328,6 +1346,7 @@
$('#submitData').text('Save RFQ'); $('#submitData').text('Save RFQ');
$('#submitMail').text('Send Insurer Mail'); $('#submitMail').text('Send Insurer Mail');
$('#submitPlacement').hide(); $('#submitPlacement').hide();
$('#pptBtnGroup').hide();
document.getElementById("second_table")?.remove(); document.getElementById("second_table")?.remove();
if(data){ if(data){
@ -1353,6 +1372,7 @@
$('#second_table').show(); $('#second_table').show();
$('#submitPlacement').show(); $('#submitPlacement').show();
$('#submitQCRData').hide(); $('#submitQCRData').hide();
$('#pptBtnGroup').show();
} }
if(RFQ_or_QCR == 1 && insurer_count > 0){ if(RFQ_or_QCR == 1 && insurer_count > 0){
@ -7942,6 +7962,141 @@ function appendMultiFileData(data) {
} }
var pptSelectionMode = false;
function startPptGeneration() {
pptSelectionMode = true;
// Add checkboxes to each proposal header (top-level th in first row of rfqTable)
let headerRow = document.querySelector('#rfqTable thead tr');
if (!headerRow) return;
let ths = headerRow.querySelectorAll('th');
ths.forEach(function(th, idx) {
// Skip Sno, Item Key, Particulars, Action columns
let keySpan = th.querySelector('.key_name');
let displaySpan = th.querySelector('.display_name');
if (!keySpan && !displaySpan) return;
let keyName = (keySpan ? keySpan.innerText.trim() : '');
let displayName = (displaySpan ? displaySpan.innerText.trim() : '');
if (!keyName || keyName === 'Sno') return;
if (keyName.toLowerCase() === 'action') return;
// Already has checkbox?
if (th.querySelector('.ppt-select-cb')) return;
let cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = true;
cb.className = 'ppt-select-cb';
cb.dataset.proposalKey = keyName;
cb.style.cssText = 'margin-left:6px; transform:scale(1.3); vertical-align:middle; cursor:pointer;';
cb.title = 'Include in PDF';
th.appendChild(cb);
});
$('#pptFloatingBar').fadeIn();
toastr.info('Select the proposals you want to include in the PDF', 'PPT Generation');
}
function cancelPptSelection() {
pptSelectionMode = false;
document.querySelectorAll('.ppt-select-cb').forEach(function(cb) { cb.remove(); });
$('#pptFloatingBar').fadeOut();
}
function confirmGeneratePdf() {
let selected = [];
document.querySelectorAll('.ppt-select-cb:checked').forEach(function(cb) {
selected.push(cb.dataset.proposalKey);
});
if (selected.length === 0) {
toastr.warning('Please select at least one proposal', 'Warning');
return;
}
// Clean up selection UI
cancelPptSelection();
let lead_id = $('#lead_id').val();
if (!lead_id) {
toastr.error('Lead ID is required', 'Error');
return;
}
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
$.ajax({
url: '<?= base_url('rfq/generateGmcQcrPdf') ?>',
type: 'POST',
data: { lead_id: lead_id, proposals: selected },
dataType: 'json',
success: function (res) {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
if (res && res.status) {
toastr.success(res.message || 'PDF generated successfully', 'Success');
} else {
toastr.error((res && res.message) ? res.message : 'Failed to generate PDF', 'Error');
}
},
error: function (xhr) {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
let msg = 'Failed to generate PDF';
try {
const parsed = JSON.parse(xhr.responseText);
if (parsed && parsed.message) msg = parsed.message;
} catch (e) {}
toastr.error(msg, 'Error');
}
});
}
function downloadGmcQcrPdf() {
let lead_id = $('#lead_id').val();
if (!lead_id) {
toastr.error('Lead ID is required', 'Error');
return;
}
const url = '<?= base_url('rfq/downloadGmcQcrPdf/') ?>' + lead_id;
$('.loader').fadeIn();
$('.loader-mask').fadeIn();
fetch(url, { credentials: 'same-origin' })
.then(async function (response) {
const contentType = response.headers.get('Content-Type') || '';
if (!response.ok || contentType.indexOf('application/json') !== -1) {
let message = 'No generated PDF found. Please click Generate first.';
try {
const data = await response.json();
if (data && data.message) message = data.message;
} catch (e) {}
throw new Error(message);
}
const disposition = response.headers.get('Content-Disposition') || '';
let filename = 'GMC Quote comparison.pdf';
const match = disposition.match(/filename=\"?([^\";]+)\"?/i);
if (match && match[1]) filename = match[1];
const blob = await response.blob();
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(link.href);
})
.catch(function (err) {
toastr.error(err.message || 'Failed to download PDF', 'Error');
})
.finally(function () {
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
});
}
function toggleButtons(status) { function toggleButtons(status) {
$("#save_placement_btn").show(); $("#save_placement_btn").show();

Binary file not shown.

Binary file not shown.