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("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)

View File

@ -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());

View File

@ -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
{

View File

@ -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',

View File

@ -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);

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

View File

@ -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
}
?>
<style>
: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-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px 16px;}
.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.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 .req{color:var(--dm-red);margin-left:2px;font-weight:700;}
.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-vcode-tag{font-family:monospace;font-size:12px;color:var(--dm-teal-dark);margin-top:4px;}
@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-label{font-size:10px;}
.dm-road{left:22px;right:22px;}
@ -227,7 +246,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</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>Make <span class="req">*</span></label>
@ -268,7 +287,7 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</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>
<select id="f_prev_ins"><option value=""> none / unknown </option></select>
</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-chips" id="addonChips">
<div class="dm-chip 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" 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" 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['personal_accident']) ? ' on' : '' ?>" data-key="personal_accident" onclick="this.classList.toggle('on')">Personal accident</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<?= !empty($addonOn['engine_protect']) ? ' on' : '' ?>" data-key="engine_protect" onclick="this.classList.toggle('on')">Engine protect</div>
<div class="dm-chip<?= !empty($addonOn['rsa']) ? ' on' : '' ?>" data-key="rsa" onclick="this.classList.toggle('on')">Roadside assistance</div>
<div class="dm-chip<?= !empty($addonOn['consumables']) ? ' on' : '' ?>" data-key="consumables" onclick="this.classList.toggle('on')">Consumables</div>
<div class="dm-chip<?= !empty($addonOn['key_protect']) ? ' on' : '' ?>" data-key="key_protect" onclick="this.classList.toggle('on')">Key & lock</div>
</div>
<div class="dm-actions">
@ -330,14 +349,14 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
</div>
<div class="dm-section">Policyholder</div>
<div class="dm-grid">
<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>Last name</label><input id="f_lname" value=""></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>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>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>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 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-grid g4">
<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="<?= esc($policyholder['last_name'] ?? '') ?>"></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="<?= 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="<?= 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="<?= esc($policyholder['dob'] ?? '') ?>" data-required="1" data-label="Date of birth"><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 class="dm-actions">
@ -470,11 +489,16 @@ $coverage = is_array($quote['coverage_details'] ?? null) ? $quote['coverage_deta
<script>
window.DM = {
quoteId: <?= $quoteId ? (int)$quoteId : 'null' ?>,
enquiryId: <?= json_encode($quote['enquiry_id'] ?? null) ?>,
base: '<?= rtrim(base_url('digit-motor'), '/') ?>',
step: <?= (int)$initialStep ?>,
selectedProduct: '<?= esc($quote['insurance_product_code'] ?? '20102') ?>',
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"];
@ -495,8 +519,23 @@ let dmSearchTimer = null;
dmGo(window.DM.step, true);
dmLoadMasters();
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){
const $s = $(sel);
$s.empty();
@ -694,7 +733,18 @@ function dmAlert(msg, ok){
}
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(){
@ -777,6 +827,8 @@ function dmValidateRequired(fieldIds){
}
function dmQuickQuote(){
if (window.DM.busy) return;
const claimChip = document.querySelector('.dm-chip[data-group="claim"].on');
const missing = dmValidateRequired([
'f_reg', 'f_make', 'f_model', 'f_variant', 'f_vcode',
@ -789,7 +841,13 @@ function dmQuickQuote(){
return;
}
const tagEnquiry = ($('#enquiryIdTag').text() || '').trim();
const enquiryId = window.DM.enquiryId
|| (tagEnquiry && tagEnquiry !== '— new —' ? tagEnquiry : null);
const body = {
quote_id: window.DM.quoteId || null,
enquiry_id: enquiryId,
license_plate_number: $('#f_reg').val().trim(),
vehicle_maincode: $('#f_vcode').val().trim(),
registration_date: $('#f_reg_date').val(),
@ -805,8 +863,7 @@ function dmQuickQuote(){
previous_ncb: $('#f_prev_ncb').val() || 'ZERO',
previous_policy_type: $('#f_prev_ptype').val() || '',
is_claim_in_last_year: claimChip ? claimChip.dataset.val === '1' : false,
coverages: dmAddonFlags(),
enquiry_id: window.DM.quoteId ? ($('#enquiryIdTag').text().trim() !== '— new —' ? $('#enquiryIdTag').text().trim() : null) : null
coverages: dmAddonFlags()
};
if (body.start_date){
@ -821,6 +878,7 @@ function dmQuickQuote(){
.done(function(res){
if (!res.status){ dmAlert(res.message || 'Quick quote failed'); return; }
window.DM.quoteId = res.data.quote_id;
window.DM.enquiryId = res.data.enquiry_id;
$('#enquiryIdTag').text(res.data.enquiry_id);
const prem = res.data.premium != null ? Number(res.data.premium) : 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]; });
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);
})
.always(function(){ dmBusy(false); });
}
function dmCreateQuote(){
if (window.DM.busy) return;
if (!window.DM.quoteId){ dmAlert('Run quick quote first.'); return; }
// 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 applyFilters(){
$('.loader').show();
$('.loader-mask').fadeIn();
$.ajax({
url: '<?= base_url('digit-motor/list') ?>',
type: 'POST',
@ -92,6 +94,10 @@ function applyFilters(){
error: function(){
if (window.toastr) toastr.error('Failed to load filtered list.', 'Error');
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="viewDemography" class="btn btn-primary" onclick="viewDemography()">Demography</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="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 ?>">
@ -1328,6 +1346,7 @@
$('#submitData').text('Save RFQ');
$('#submitMail').text('Send Insurer Mail');
$('#submitPlacement').hide();
$('#pptBtnGroup').hide();
document.getElementById("second_table")?.remove();
if(data){
@ -1353,6 +1372,7 @@
$('#second_table').show();
$('#submitPlacement').show();
$('#submitQCRData').hide();
$('#pptBtnGroup').show();
}
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) {
$("#save_placement_btn").show();

Binary file not shown.

Binary file not shown.