FEAT_NON_EB_RFQ/QCR
This commit is contained in:
parent
65394d7aea
commit
e8e79c3cea
@ -63,7 +63,8 @@ NHANCE_LOGO =
|
|||||||
helpdeskURL =
|
helpdeskURL =
|
||||||
TOKENTIMEOUT =
|
TOKENTIMEOUT =
|
||||||
POST_ENROLLMENT_APP_LINK =
|
POST_ENROLLMENT_APP_LINK =
|
||||||
GDRIVE_ROOT_FOLDER_ID =
|
GDRIVE_ROOT_FOLDER_ID =
|
||||||
|
RFQ_PARENT_FOLDER_ID =
|
||||||
|
|
||||||
|
|
||||||
email.fromEmail =
|
email.fromEmail =
|
||||||
|
|||||||
@ -7,9 +7,17 @@ class RfqConfig extends BaseConfig
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Google Drive parent folder IDs for RFQ and QCR sheets.
|
* Google Drive parent folder IDs for RFQ and QCR sheets.
|
||||||
|
* Values are loaded from .env (RFQ_PARENT_FOLDER_ID).
|
||||||
*/
|
*/
|
||||||
public string $rfqParentFolderId = '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1';
|
public string $rfqParentFolderId = '';
|
||||||
public string $qcrParentFolderId = '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1';
|
public string $qcrParentFolderId = '';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->rfqParentFolderId = env('RFQ_PARENT_FOLDER_ID', '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1');
|
||||||
|
$this->qcrParentFolderId = env('RFQ_PARENT_FOLDER_ID', '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default permissions for created sheets.
|
* Default permissions for created sheets.
|
||||||
@ -48,4 +56,27 @@ class RfqConfig extends BaseConfig
|
|||||||
'groups' => [],
|
'groups' => [],
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder token for past 3 years claims table (written as multi-row values at that cell).
|
||||||
|
*/
|
||||||
|
public string $rfqClaimsPlaceholder = '{{CLAIMS_DETAILS}}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps Google Sheet placeholder => internal key from buildRfqPlaceholderData().
|
||||||
|
* Add these exact strings in your RFQ templates (any sheet/tab).
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
public array $rfqPlaceholders = [
|
||||||
|
'{{INSURED_NAME}}' => 'insured_name',
|
||||||
|
'{{COMMUNICATION_ADDRESS}}' => 'communication_address',
|
||||||
|
'{{GST}}' => 'gst',
|
||||||
|
'{{PAN}}' => 'pan',
|
||||||
|
'{{POLICY_PERIOD}}' => 'policy_period',
|
||||||
|
'{{OPPORTUNITY_TYPE}}' => 'opportunity_type',
|
||||||
|
'{{RISK_LOCATION}}' => 'risk_location',
|
||||||
|
'{{OCCUPANCY}}' => 'occupancy',
|
||||||
|
'{{CLAIMS_DETAILS}}' => 'claims_details',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -511,6 +511,8 @@ $routes->group("leads", ["filter" => "authMVC"], function ($routes) {
|
|||||||
$routes->post("create", "LeadsController::createLead");
|
$routes->post("create", "LeadsController::createLead");
|
||||||
$routes->get("list/(:any)", "LeadsController::getLeadDataForEdit/$1");
|
$routes->get("list/(:any)", "LeadsController::getLeadDataForEdit/$1");
|
||||||
$routes->get("createRfqSheet", "LeadsController::createRfqSheet");
|
$routes->get("createRfqSheet", "LeadsController::createRfqSheet");
|
||||||
|
$routes->get("createQcrSheet", "LeadsController::createQcrSheet");
|
||||||
|
$routes->post("uploadLeadAttachment", "LeadsController::uploadLeadAttachment");
|
||||||
$routes->get("mailTemplate", "LeadsController::getLeadMailTemplate");
|
$routes->get("mailTemplate", "LeadsController::getLeadMailTemplate");
|
||||||
$routes->get("sendMail", "LeadsController::sendMailWithAttachement");
|
$routes->get("sendMail", "LeadsController::sendMailWithAttachement");
|
||||||
$routes->post("sendMail", "LeadsController::sendMailWithAttachement");
|
$routes->post("sendMail", "LeadsController::sendMailWithAttachement");
|
||||||
@ -550,6 +552,8 @@ $routes->cli('cli/sendZeptoMail', 'MasterController::testZeptoSMTP');
|
|||||||
$routes->cli('cli/check_bounce_mail_cli', 'MasterController::testCheckBounceMails');
|
$routes->cli('cli/check_bounce_mail_cli', 'MasterController::testCheckBounceMails');
|
||||||
$routes->cli('cli/app_check_list', 'MasterController::appCheckList');
|
$routes->cli('cli/app_check_list', 'MasterController::appCheckList');
|
||||||
$routes->cli('cli/new_gdrive_token', 'GoogleDriveController::generateNewGoogleDriveAccessToken');
|
$routes->cli('cli/new_gdrive_token', 'GoogleDriveController::generateNewGoogleDriveAccessToken');
|
||||||
|
$routes->cli('cli/list-sheet-folder-files', 'GoogleSheetController::listFolderSheetFilesCli');
|
||||||
|
$routes->cli('cli/list-sheet-folder-files/(:any)', 'GoogleSheetController::listFolderSheetFilesCli/$1');
|
||||||
$routes->cli('cli/check_env', 'MasterController::checkEnv');
|
$routes->cli('cli/check_env', 'MasterController::checkEnv');
|
||||||
|
|
||||||
//crone job
|
//crone job
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<?php namespace App\Controllers;
|
<?php namespace App\Controllers;
|
||||||
|
|
||||||
use App\Libraries\GoogleSheetLib;
|
use App\Libraries\GoogleSheetLib;
|
||||||
|
use CodeIgniter\CLI\CLI;
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
|
|
||||||
class GoogleSheetController extends Controller
|
class GoogleSheetController extends Controller
|
||||||
@ -133,4 +134,23 @@ class GoogleSheetController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI: php public/index.php cli/list-sheet-folder-files [folderId]
|
||||||
|
*/
|
||||||
|
public function listFolderSheetFilesCli(string $folderId = '')
|
||||||
|
{
|
||||||
|
if (! is_cli()) {
|
||||||
|
return $this->response
|
||||||
|
->setStatusCode(405)
|
||||||
|
->setJSON(['status' => 'error', 'message' => 'CLI only route']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$folderId = CLI::getOption('folderId') ?: $folderId;
|
||||||
|
$json = $this->sheetLib->listFolderSheetIdsAsJson($folderId);
|
||||||
|
file_put_contents('sheetid.json', $json);
|
||||||
|
CLI::write($json);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -627,7 +627,6 @@ class LeadsController extends BaseController
|
|||||||
$request_data = $this->request->getPost();
|
$request_data = $this->request->getPost();
|
||||||
$data = sanitizeInputArrayAdvanced($request_data);
|
$data = sanitizeInputArrayAdvanced($request_data);
|
||||||
$data['client_type'] = 1;
|
$data['client_type'] = 1;
|
||||||
$data['pan'] = "";
|
|
||||||
|
|
||||||
if ($data['lead_type'] != 1) {
|
if ($data['lead_type'] != 1) {
|
||||||
$client_data = $this->clientModel->where('id', $data['client_id'])->where('is_active', 1)->first();
|
$client_data = $this->clientModel->where('id', $data['client_id'])->where('is_active', 1)->first();
|
||||||
@ -843,6 +842,7 @@ class LeadsController extends BaseController
|
|||||||
'source_policy_id' => $data['source_policy_id'] ?? 0,
|
'source_policy_id' => $data['source_policy_id'] ?? 0,
|
||||||
'policy_type_id' => $value,
|
'policy_type_id' => $value,
|
||||||
'salse_person_id' => $data['salse_person_id'] ?? 0,
|
'salse_person_id' => $data['salse_person_id'] ?? 0,
|
||||||
|
'rfq_qcr_viewers' => $data['rfq_qcr_viewers'] ?? null,
|
||||||
'insurer_id' => $insurer_id ?? 0,
|
'insurer_id' => $insurer_id ?? 0,
|
||||||
'insurer_branch_id' => $insurer_branch_id ?? 0,
|
'insurer_branch_id' => $insurer_branch_id ?? 0,
|
||||||
'tpa_id' => $tpa_id ?? 0,
|
'tpa_id' => $tpa_id ?? 0,
|
||||||
@ -1120,6 +1120,59 @@ class LeadsController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function uploadLeadAttachment()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$leadId = (int) $this->request->getPost('lead_id');
|
||||||
|
$docsName = trim($this->request->getPost('docs_name') ?? '');
|
||||||
|
|
||||||
|
if ($leadId <= 0) {
|
||||||
|
return $this->respond(['status' => 'error', 'code' => 400, 'message' => 'Invalid lead id'], 400);
|
||||||
|
}
|
||||||
|
if (empty($docsName)) {
|
||||||
|
return $this->respond(['status' => 'error', 'code' => 400, 'message' => 'Document label is required'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $this->request->getFile('attachment_file');
|
||||||
|
if (! $file || ! $file->isValid() || $file->hasMoved()) {
|
||||||
|
return $this->respond(['status' => 'error', 'code' => 400, 'message' => 'Invalid or missing file'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uploadFilePath = WRITEPATH . 'uploads/lead_files/';
|
||||||
|
$fileName = file_Upload_for_lead($file, $uploadFilePath, UPLOAD_EXT_LEAD_FILES);
|
||||||
|
|
||||||
|
if (empty($fileName)) {
|
||||||
|
return $this->respond(['status' => 'error', 'code' => 400, 'message' => 'File upload failed. Check file type or size.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->leadFilesModel->insert([
|
||||||
|
'lead_id' => $leadId,
|
||||||
|
'docs_name' => $docsName,
|
||||||
|
'file_name' => $fileName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return refreshed attachment HTML
|
||||||
|
$attachments = $this->leadFilesModel
|
||||||
|
->where('lead_id', $leadId)
|
||||||
|
->where('type !=', 2)
|
||||||
|
->where('is_active', 1)
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
$attachmentHtml = view('rfq/attachment_files', ['multi_file_data' => $attachments]);
|
||||||
|
|
||||||
|
return $this->respond([
|
||||||
|
'status' => 'success',
|
||||||
|
'code' => 200,
|
||||||
|
'message' => 'File uploaded successfully',
|
||||||
|
'attachment_html' => $attachmentHtml,
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->myLogger->logme('error', 'uploadLeadAttachment: ' . $e->getMessage());
|
||||||
|
return $this->respond(['status' => 'error', 'code' => 500, 'message' => 'Upload failed'], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//--------RFQ-----------------------------------------------------------------------------------------------
|
//--------RFQ-----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// public function viewRFQ($id, $type = 1)
|
// public function viewRFQ($id, $type = 1)
|
||||||
@ -3348,6 +3401,15 @@ class LeadsController extends BaseController
|
|||||||
// Handle case where param_cc_mail is not valid
|
// Handle case where param_cc_mail is not valid
|
||||||
// return $this->respond(['status' => 'failed','code' => 400,'data' => '','message' => 'CC mail not found!'], 200);
|
// return $this->respond(['status' => 'failed','code' => 400,'data' => '','message' => 'CC mail not found!'], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append external CC emails for client mail (sent as email strings, not user IDs)
|
||||||
|
if ($recipient_type === 'client' && ! empty($params['external_cc'])) {
|
||||||
|
$externalCCs = json_decode($params['external_cc'], true);
|
||||||
|
if (is_array($externalCCs)) {
|
||||||
|
$cc_mails = array_merge($cc_mails, array_values(array_filter(array_map('trim', $externalCCs))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else if ($recipient_type == 'placement') {
|
} else if ($recipient_type == 'placement') {
|
||||||
|
|
||||||
if(isset($params['cc']) && !empty($params['cc'])) {
|
if(isset($params['cc']) && !empty($params['cc'])) {
|
||||||
@ -3399,9 +3461,12 @@ class LeadsController extends BaseController
|
|||||||
|
|
||||||
//get file path to attach
|
//get file path to attach
|
||||||
|
|
||||||
if ($lead_data["lead_form_type"] == 2) {
|
if ($lead_data["lead_form_type"] == 2 && $recipient_type === 'placement') {
|
||||||
// For Non-EB, download RFQ/QCR file from Google Sheet (if configured)
|
// For Non-EB placement: copy QCR sheet as placement file, then download it
|
||||||
$file_info = $this->downloadFileFromGoogleSheet($lead_data);
|
$file_info = $this->createAndDownloadPlacementSheet($lead_data);
|
||||||
|
} else if ($lead_data["lead_form_type"] == 2) {
|
||||||
|
// For Non-EB, download RFQ/QCR file from Google Sheet based on recipient type
|
||||||
|
$file_info = $this->downloadFileFromGoogleSheet($lead_data, $recipient_type);
|
||||||
} else {
|
} else {
|
||||||
$file_info = $this->constructExcelToSaveTemp($lead_id, ($file_type == 'rfq' ? 1 : 2), $propsal_and_insurer);
|
$file_info = $this->constructExcelToSaveTemp($lead_id, ($file_type == 'rfq' ? 1 : 2), $propsal_and_insurer);
|
||||||
}
|
}
|
||||||
@ -3442,8 +3507,17 @@ class LeadsController extends BaseController
|
|||||||
// print_r($attachments); die;
|
// print_r($attachments); die;
|
||||||
|
|
||||||
//get recipient address
|
//get recipient address
|
||||||
if ($recipient_type == 'insurer' || $recipient_type == 'placement') {
|
if ($recipient_type == 'placement') {
|
||||||
|
// Placement TO is client contact email sent directly
|
||||||
|
if(!is_array($recipient_mail)) {
|
||||||
|
$recipient_mail = [$recipient_mail];
|
||||||
|
}
|
||||||
|
$recipient_data = [];
|
||||||
|
foreach ($recipient_mail as $mail) {
|
||||||
|
$recipient_data[] = ['name' => $lead_data['contact_person_name'] ?? '', 'email' => trim($mail)];
|
||||||
|
}
|
||||||
|
} else if ($recipient_type == 'insurer') {
|
||||||
|
|
||||||
if(!is_array($recipient_mail)) {
|
if(!is_array($recipient_mail)) {
|
||||||
$recipient_mail = [$recipient_mail];
|
$recipient_mail = [$recipient_mail];
|
||||||
}
|
}
|
||||||
@ -4535,7 +4609,17 @@ class LeadsController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client contact from lead data for placement TO
|
||||||
|
$clientContact = null;
|
||||||
|
if (! empty($lead['contact_person_email'])) {
|
||||||
|
$clientContact = [
|
||||||
|
'name' => $lead['contact_person_name'] ?? '',
|
||||||
|
'email' => $lead['contact_person_email'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$data['insurer_contacts'] = $insurerContacts;
|
$data['insurer_contacts'] = $insurerContacts;
|
||||||
|
$data['client_contact'] = $clientContact;
|
||||||
|
|
||||||
return $this->respond(
|
return $this->respond(
|
||||||
[
|
[
|
||||||
@ -4761,15 +4845,21 @@ class LeadsController extends BaseController
|
|||||||
|
|
||||||
$this->myLogger->logme('error', "RFQ Sheet: starting for lead_id={$leadId}");
|
$this->myLogger->logme('error', "RFQ Sheet: starting for lead_id={$leadId}");
|
||||||
|
|
||||||
// Fetch lead + policy type (including misc JSON from both)
|
// Fetch lead + policy type + client address (LEFT JOIN for communication address on EB leads)
|
||||||
$lead = $this->leadsModel
|
$lead = $this->leadsModel
|
||||||
->select('
|
->select('
|
||||||
leads.*,
|
leads.*,
|
||||||
policy_type.policy_type,
|
policy_type.policy_type,
|
||||||
policy_type.long_name,
|
policy_type.long_name,
|
||||||
policy_type.misc as policy_misc
|
policy_type.misc as policy_misc,
|
||||||
|
clients.address1 as client_address1,
|
||||||
|
clients.address2 as client_address2,
|
||||||
|
clients.city as client_city,
|
||||||
|
clients.state as client_state,
|
||||||
|
clients.pincode as client_pincode
|
||||||
')
|
')
|
||||||
->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left')
|
->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left')
|
||||||
|
->join('clients', 'leads.client_id = clients.id', 'left')
|
||||||
->where('leads.id', $leadId)
|
->where('leads.id', $leadId)
|
||||||
->where('leads.is_active', 1)
|
->where('leads.is_active', 1)
|
||||||
->first();
|
->first();
|
||||||
@ -4892,22 +4982,76 @@ class LeadsController extends BaseController
|
|||||||
$newSheetId = $sheetLib->copyTemplate($templateId, $sheetName, $parentFolderId);
|
$newSheetId = $sheetLib->copyTemplate($templateId, $sheetName, $parentFolderId);
|
||||||
$this->myLogger->logme('error', "RFQ Sheet: template copied | lead_id={$leadId}, template_id={$templateId}, new_sheet_id={$newSheetId}");
|
$this->myLogger->logme('error', "RFQ Sheet: template copied | lead_id={$leadId}, template_id={$templateId}, new_sheet_id={$newSheetId}");
|
||||||
|
|
||||||
|
// Fill placeholders across all sheets (see Config\RfqConfig::$rfqPlaceholders)
|
||||||
|
try {
|
||||||
|
/** @var \Config\RfqConfig $rfqCfg */
|
||||||
|
$rfqCfg = config('RfqConfig');
|
||||||
|
$placeholderData = $this->buildRfqPlaceholderData($lead);
|
||||||
|
$claimsPh = $rfqCfg->rfqClaimsPlaceholder ?? '{{CLAIMS_DETAILS}}';
|
||||||
|
$map = $rfqCfg->rfqPlaceholders ?? [];
|
||||||
|
$textReplacements = [];
|
||||||
|
|
||||||
|
foreach ($map as $placeholder => $key) {
|
||||||
|
if ($placeholder === $claimsPh) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$textReplacements[$placeholder] = (string) ($placeholderData['values'][$key] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetLib->findAndReplaceAllSheets($newSheetId, $textReplacements);
|
||||||
|
|
||||||
|
if ($placeholderData['claims_table'] !== null) {
|
||||||
|
$written = $sheetLib->writeTableAtPlaceholder(
|
||||||
|
$newSheetId,
|
||||||
|
$claimsPh,
|
||||||
|
$placeholderData['claims_table']
|
||||||
|
);
|
||||||
|
|
||||||
|
if (! $written) {
|
||||||
|
$sheetLib->findAndReplaceAllSheets($newSheetId, [
|
||||||
|
$claimsPh => $placeholderData['claims_placeholder_fallback'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$sheetLib->findAndReplaceAllSheets($newSheetId, [
|
||||||
|
$claimsPh => $placeholderData['claims_placeholder_fallback'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', "RFQ Sheet: placeholders applied | lead_id={$leadId}, sheet_id={$newSheetId}");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->myLogger->logme(
|
||||||
|
'error',
|
||||||
|
"RFQ Sheet: placeholder fill failed | lead_id={$leadId}, error=" . $e->getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply permissions: get sales users from DB (team_id 5) and add as editors
|
// Apply permissions: get sales users from DB (team_id 5) and add as editors
|
||||||
$salesTeam = $this->userModel
|
// $salesTeam = $this->userModel
|
||||||
->select('user_profiles.email')
|
// ->select('user_profiles.email')
|
||||||
->join('user_teams', 'user_profiles.id = user_teams.user_id')
|
// ->join('user_teams', 'user_profiles.id = user_teams.user_id')
|
||||||
->where('user_teams.team_id', 5)
|
// ->where('user_teams.team_id', 5)
|
||||||
->where('user_teams.is_active', 1)
|
// ->where('user_teams.is_active', 1)
|
||||||
->where('user_profiles.is_active', 1)
|
// ->where('user_profiles.is_active', 1)
|
||||||
->findAll();
|
// ->findAll();
|
||||||
$editorEmails = array_values(array_filter(array_unique(array_column($salesTeam, 'email'))));
|
// $editorEmails = array_values(array_filter(array_unique(array_column($salesTeam, 'email'))));
|
||||||
if (! empty($editorEmails)) {
|
|
||||||
|
// Decode rfq_qcr_viewers from lead and add as viewers
|
||||||
|
$editorEmails = [];
|
||||||
|
if (! empty($lead['rfq_qcr_viewers'])) {
|
||||||
|
$decoded = json_decode($lead['rfq_qcr_viewers'], true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$editorEmails = array_values(array_filter(array_unique($decoded)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! empty($editorEmails)) {
|
||||||
try {
|
try {
|
||||||
$sheetLib->applyPermissions($newSheetId, [
|
$sheetLib->applyPermissions($newSheetId, [
|
||||||
'editors' => $editorEmails,
|
'editors' => $editorEmails,
|
||||||
'viewers' => [],
|
// 'viewers' => $viewerEmails,
|
||||||
]);
|
]);
|
||||||
$this->myLogger->logme('error', "RFQ Sheet: permissions applied (sales team) | sheet_id={$newSheetId}, count=" . count($editorEmails));
|
$this->myLogger->logme('error', "RFQ Sheet: permissions applied | sheet_id={$newSheetId}, editors=" . count($editorEmails) );
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->myLogger->logme(
|
$this->myLogger->logme(
|
||||||
'error',
|
'error',
|
||||||
@ -4976,17 +5120,344 @@ class LeadsController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build values for RFQ Google Sheet placeholders (see RfqConfig::$rfqPlaceholders).
|
||||||
|
*
|
||||||
|
* @return array{values: array<string, string>, claims_table: ?list<list<string|int|float>>, claims_placeholder_fallback: string}
|
||||||
|
*/
|
||||||
|
private function buildRfqPlaceholderData(array $lead): array
|
||||||
|
{
|
||||||
|
helper('excel_util_helper');
|
||||||
|
|
||||||
|
$custom = [];
|
||||||
|
|
||||||
|
if (! empty($lead['custom_fields'])) {
|
||||||
|
$decoded = json_decode($lead['custom_fields'], true);
|
||||||
|
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||||
|
$custom = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientParts = array_filter([
|
||||||
|
trim((string) ($lead['client_address1'] ?? '')),
|
||||||
|
trim((string) ($lead['client_address2'] ?? '')),
|
||||||
|
trim((string) ($lead['client_city'] ?? '')),
|
||||||
|
trim((string) ($lead['client_state'] ?? '')),
|
||||||
|
trim((string) ($lead['client_pincode'] ?? '')),
|
||||||
|
], static function ($s) {
|
||||||
|
return $s !== '';
|
||||||
|
});
|
||||||
|
$clientAddr = implode(', ', $clientParts);
|
||||||
|
|
||||||
|
$cfParts = array_filter([
|
||||||
|
trim((string) ($custom['address'] ?? '')),
|
||||||
|
trim((string) ($custom['pincode'] ?? '')),
|
||||||
|
], static function ($s) {
|
||||||
|
return $s !== '';
|
||||||
|
});
|
||||||
|
$cfAddr = implode(', ', $cfParts);
|
||||||
|
|
||||||
|
$communicationAddress = $clientAddr !== '' ? $clientAddr : $cfAddr;
|
||||||
|
|
||||||
|
$policyPeriod = '';
|
||||||
|
|
||||||
|
if (! empty($lead['policy_start_date']) && ! empty($lead['policy_end_date'])) {
|
||||||
|
$policyPeriod = date('d-m-Y', strtotime((string) $lead['policy_start_date']))
|
||||||
|
. ' to '
|
||||||
|
. date('d-m-Y', strtotime((string) $lead['policy_end_date']));
|
||||||
|
}
|
||||||
|
|
||||||
|
$lt = (int) ($lead['lead_type'] ?? 0);
|
||||||
|
$opportunityType = $this->leadType[$lt] ?? '';
|
||||||
|
|
||||||
|
$riskRaw = (string) ($custom['risk_location'] ?? '');
|
||||||
|
$riskLocation = $riskRaw !== ''
|
||||||
|
? ucwords(str_replace('_', ' ', strtolower($riskRaw)))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$occupancyRaw = (string) ($custom['business_description'] ?? $custom['occupancy_risk'] ?? '');
|
||||||
|
$occupancy = $occupancyRaw !== ''
|
||||||
|
? ucwords(str_replace('_', ' ', strtolower($occupancyRaw)))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
$claimsTable = null;
|
||||||
|
$claimsFallback = 'No claims data available';
|
||||||
|
|
||||||
|
if (! empty($lead['fin_years_claims'])) {
|
||||||
|
$claimJson = json_decode($lead['fin_years_claims'], true);
|
||||||
|
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && is_array($claimJson)) {
|
||||||
|
$finyear = $claimJson['finyear'] ?? null;
|
||||||
|
|
||||||
|
if (is_array($finyear) && $finyear !== []) {
|
||||||
|
$claimsTable = $this->buildRfqClaimsTableValues($finyear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = [
|
||||||
|
'insured_name' => trim((string) ($lead['client_name'] ?? $lead['client_short_name'] ?? '')),
|
||||||
|
'communication_address' => $communicationAddress,
|
||||||
|
'gst' => trim((string) ($lead['gst'] ?? '')),
|
||||||
|
'pan' => trim((string) ($lead['pan'] ?? '')),
|
||||||
|
'policy_period' => $policyPeriod,
|
||||||
|
'opportunity_type' => $opportunityType,
|
||||||
|
'risk_location' => $riskLocation,
|
||||||
|
'occupancy' => $occupancy,
|
||||||
|
'claims_details' => '',
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'values' => $values,
|
||||||
|
'claims_table' => $claimsTable,
|
||||||
|
'claims_placeholder_fallback' => $claimsFallback,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $finyear
|
||||||
|
*
|
||||||
|
* @return list<list<string|int|float>>
|
||||||
|
*/
|
||||||
|
private function buildRfqClaimsTableValues(array $finyear): array
|
||||||
|
{
|
||||||
|
helper('excel_util_helper');
|
||||||
|
|
||||||
|
$first = $finyear[0] ?? null;
|
||||||
|
|
||||||
|
if (! is_array($first) || $first === []) {
|
||||||
|
return [['No data available']];
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
|
||||||
|
foreach (array_keys($first) as $key) {
|
||||||
|
$headers[] = ucwords(str_replace('_', ' ', (string) $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
$rows[] = $headers;
|
||||||
|
|
||||||
|
foreach ($finyear as $record) {
|
||||||
|
if (! is_array($record)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = [];
|
||||||
|
|
||||||
|
foreach ($record as $arrayKey => $value) {
|
||||||
|
if (in_array($arrayKey, ['sum_insured', 'claim_amount', 'settled'], true)) {
|
||||||
|
$line[] = function_exists('formatIndianCurrency')
|
||||||
|
? formatIndianCurrency((int) $value)
|
||||||
|
: (string) $value;
|
||||||
|
} else {
|
||||||
|
$line[] = $value === null || $value === '' ? '' : (string) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createQcrSheet()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$leadId = (int) $this->request->getGet('lead_id');
|
||||||
|
|
||||||
|
if ($leadId <= 0) {
|
||||||
|
return $this->respond(
|
||||||
|
['status' => 'error', 'code' => 400, 'message' => 'Invalid lead id'],
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: starting for lead_id={$leadId}");
|
||||||
|
|
||||||
|
$lead = $this->leadsModel
|
||||||
|
->select('
|
||||||
|
leads.*,
|
||||||
|
policy_type.policy_type,
|
||||||
|
policy_type.long_name,
|
||||||
|
policy_type.misc as policy_misc
|
||||||
|
')
|
||||||
|
->join('policy_type', 'leads.policy_type_id = policy_type.id', 'left')
|
||||||
|
->where('leads.id', $leadId)
|
||||||
|
->where('leads.is_active', 1)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $lead) {
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: lead not found | lead_id={$leadId}");
|
||||||
|
return $this->respond(
|
||||||
|
['status' => 'error', 'code' => 404, 'message' => 'Lead not found'],
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$leadMisc = [];
|
||||||
|
if (! empty($lead['misc'])) {
|
||||||
|
$decoded = json_decode($lead['misc'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||||
|
$leadMisc = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If QCR sheet already exists, just return it
|
||||||
|
if (! empty($leadMisc['qcr_sheet_id'])) {
|
||||||
|
$sheetId = (string) $leadMisc['qcr_sheet_id'];
|
||||||
|
$sheetLib = new GoogleSheetLib();
|
||||||
|
$sheetUrl = $sheetLib->sheetUrl($sheetId);
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: existing sheet found | lead_id={$leadId}, sheet_id={$sheetId}");
|
||||||
|
|
||||||
|
return $this->respond(
|
||||||
|
[
|
||||||
|
'status' => 'success',
|
||||||
|
'code' => 200,
|
||||||
|
'sheet_id' => $sheetId,
|
||||||
|
'sheet_url' => $sheetUrl,
|
||||||
|
'message' => 'QCR sheet already exists',
|
||||||
|
],
|
||||||
|
200
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source is the existing RFQ sheet for this lead
|
||||||
|
$rfqSheetId = $leadMisc['rfq_sheet_id'] ?? null;
|
||||||
|
if (empty($rfqSheetId)) {
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: rfq_sheet_id missing in leads.misc | lead_id={$leadId}");
|
||||||
|
return $this->respond(
|
||||||
|
['status' => 'error', 'code' => 400, 'message' => 'RFQ sheet not found. Please create the RFQ sheet first.'],
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config for folder id and permissions
|
||||||
|
$parentFolderId = null;
|
||||||
|
$permissions = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
/** @var \Config\RfqConfig $rfqConfig */
|
||||||
|
$rfqConfig = config('RfqConfig');
|
||||||
|
if ($rfqConfig) {
|
||||||
|
$parentFolderId = $rfqConfig->rfqParentFolderId ?? null;
|
||||||
|
$permissions = $rfqConfig->permissions ?? [];
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->myLogger->logme('error', 'QCR Sheet: error loading RfqConfig | ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $parentFolderId) {
|
||||||
|
return $this->respond(
|
||||||
|
['status' => 'error', 'code' => 400, 'message' => 'Google config not correct, contact administrator'],
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filename: same as RFQ naming but with QCR
|
||||||
|
$string = 'QCR';
|
||||||
|
$current_year = date('Y');
|
||||||
|
$next_year = $current_year + 1;
|
||||||
|
$policy_year = "{$current_year}-{$next_year}";
|
||||||
|
|
||||||
|
$clientName = trim($lead['client_name'] ?? $lead['client_short_name'] ?? '');
|
||||||
|
$policyType = trim($lead['policy_type'] ?? '');
|
||||||
|
|
||||||
|
if (! empty($lead['policy_end_date'])) {
|
||||||
|
$policy_expiry = strtotime($lead['policy_end_date']);
|
||||||
|
$formatted_policy = date('d-m-Y', $policy_expiry);
|
||||||
|
$sheetName = "{$clientName}_{$policyType}_{$string}_{$policy_year}(Due On {$formatted_policy}).xlsx";
|
||||||
|
} else {
|
||||||
|
$sheetName = "{$clientName}_{$policyType}_{$string}_{$policy_year}_.xlsx";
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheetLib = new GoogleSheetLib();
|
||||||
|
$newSheetId = $sheetLib->copyTemplate($rfqSheetId, $sheetName, $parentFolderId);
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: copied from RFQ sheet | lead_id={$leadId}, rfq_sheet_id={$rfqSheetId}, new_sheet_id={$newSheetId}");
|
||||||
|
|
||||||
|
// Apply permissions: sales team as editors
|
||||||
|
$salesTeam = $this->userModel
|
||||||
|
->select('user_profiles.email')
|
||||||
|
->join('user_teams', 'user_profiles.id = user_teams.user_id')
|
||||||
|
->where('user_teams.team_id', 5)
|
||||||
|
->where('user_teams.is_active', 1)
|
||||||
|
->where('user_profiles.is_active', 1)
|
||||||
|
->findAll();
|
||||||
|
$editorEmails = array_values(array_filter(array_unique(array_column($salesTeam, 'email'))));
|
||||||
|
|
||||||
|
// Decode rfq_qcr_viewers from lead and add as viewers
|
||||||
|
$viewerEmails = [];
|
||||||
|
if (! empty($lead['rfq_qcr_viewers'])) {
|
||||||
|
$decoded = json_decode($lead['rfq_qcr_viewers'], true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$viewerEmails = array_values(array_filter(array_unique($decoded)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($editorEmails) || ! empty($viewerEmails)) {
|
||||||
|
try {
|
||||||
|
$sheetLib->applyPermissions($newSheetId, [
|
||||||
|
'editors' => $editorEmails,
|
||||||
|
'viewers' => $viewerEmails,
|
||||||
|
]);
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: permissions applied | sheet_id={$newSheetId}, editors=" . count($editorEmails) . ", viewers=" . count($viewerEmails));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: failed to apply permissions | sheet_id={$newSheetId}, error=" . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No protections applied for QCR as of now
|
||||||
|
|
||||||
|
// Save qcr_sheet_id to leads.misc and update status
|
||||||
|
$leadMisc['qcr_sheet_id'] = $newSheetId;
|
||||||
|
$this->leadsModel->update($leadId, [
|
||||||
|
'misc' => json_encode($leadMisc),
|
||||||
|
'status' => 'qcr_created',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sheetUrl = $sheetLib->sheetUrl($newSheetId);
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', "QCR Sheet: created and stored | lead_id={$leadId}, sheet_id={$newSheetId}");
|
||||||
|
|
||||||
|
return $this->respond(
|
||||||
|
[
|
||||||
|
'status' => 'success',
|
||||||
|
'code' => 200,
|
||||||
|
'sheet_id' => $newSheetId,
|
||||||
|
'sheet_url' => $sheetUrl,
|
||||||
|
'message' => 'QCR sheet created successfully',
|
||||||
|
],
|
||||||
|
200
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$errorDetails = [
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
'file' => $e->getFile(),
|
||||||
|
'line' => $e->getLine(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', 'QCR Sheet: exception occurred | ' . json_encode($errorDetails));
|
||||||
|
|
||||||
|
return $this->respond(
|
||||||
|
['status' => 'error', 'code' => 500, 'message' => 'Failed to create QCR sheet'],
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download RFQ/QCR Excel file from Google Sheet for Non-EB leads.
|
* Download RFQ/QCR Excel file from Google Sheet for Non-EB leads.
|
||||||
*
|
*
|
||||||
* Expects $lead_data to contain misc JSON with rfq_sheet_id or qcr_sheet_id.
|
* Sheet selection based on recipient_type:
|
||||||
* Returns an array compatible with constructNonEbExcelToSaveTemp:
|
* - 'insurer' → rfq_sheet_id
|
||||||
* [
|
* - 'client' → qcr_sheet_id
|
||||||
* 'filePath' => string,
|
* - 'internal' → based on lead status: qcr_created/qcr_sent → qcr_sheet_id, else rfq_sheet_id
|
||||||
* 'fileName' => string,
|
*
|
||||||
* ]
|
* @param string $recipientType insurer|client|internal
|
||||||
*/
|
*/
|
||||||
protected function downloadFileFromGoogleSheet(array $lead_data): array
|
protected function downloadFileFromGoogleSheet(array $lead_data, string $recipientType = 'insurer'): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$leadId = (int) ($lead_data['id'] ?? 0);
|
$leadId = (int) ($lead_data['id'] ?? 0);
|
||||||
@ -5002,17 +5473,31 @@ class LeadsController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer RFQ sheet id, fallback to QCR if needed
|
// Pick sheet id based on recipient type
|
||||||
$sheetId = $misc['rfq_sheet_id'] ?? ($misc['qcr_sheet_id'] ?? null);
|
$sheetId = null;
|
||||||
|
$sheetType = '';
|
||||||
|
|
||||||
|
if ($recipientType === 'client') {
|
||||||
|
$sheetId = $misc['qcr_sheet_id'] ?? null;
|
||||||
|
$sheetType = 'qcr_sheet_id';
|
||||||
|
} elseif ($recipientType === 'internal') {
|
||||||
|
$leadStatus = $lead_data['status'] ?? '';
|
||||||
|
if (in_array($leadStatus, ['qcr_created', 'qcr_sent'])) {
|
||||||
|
$sheetId = $misc['qcr_sheet_id'] ?? null;
|
||||||
|
$sheetType = 'qcr_sheet_id';
|
||||||
|
} else {
|
||||||
|
$sheetId = $misc['rfq_sheet_id'] ?? null;
|
||||||
|
$sheetType = 'rfq_sheet_id';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// insurer or default
|
||||||
|
$sheetId = $misc['rfq_sheet_id'] ?? null;
|
||||||
|
$sheetType = 'rfq_sheet_id';
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($sheetId)) {
|
if (empty($sheetId)) {
|
||||||
$this->myLogger->logme('error', "GSHEET DOWNLOAD: sheet id missing in misc | lead_id={$leadId}");
|
$this->myLogger->logme('error', "GSHEET DOWNLOAD: {$sheetType} missing in leads.misc | lead_id={$leadId}, recipient_type={$recipientType}");
|
||||||
// Fallback: behave like old flow and construct Non-EB Excel locally
|
throw new \RuntimeException("{$sheetType} not found in leads. Please create the sheet first.");
|
||||||
// $file_info = $this->constructNonEbExcelToSaveTemp($leadId, 1, null);
|
|
||||||
// return [
|
|
||||||
// 'filePath' => $file_info['filePath'],
|
|
||||||
// 'fileName' => $file_info['fileName'],
|
|
||||||
// ];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->myLogger->logme('error', "GSHEET DOWNLOAD: starting download | lead_id={$leadId}, sheet_id={$sheetId}");
|
$this->myLogger->logme('error', "GSHEET DOWNLOAD: starting download | lead_id={$leadId}, sheet_id={$sheetId}");
|
||||||
@ -5071,6 +5556,143 @@ class LeadsController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a placement Google Sheet by copying the QCR sheet (Non-EB only).
|
||||||
|
*
|
||||||
|
* - Reads qcr_sheet_id from leads.misc
|
||||||
|
* - If placement_sheet_id already exists, reuses it
|
||||||
|
* - Copies the QCR sheet, replacing "QCR" with "Placement" in the filename
|
||||||
|
* - Applies same editor/viewer permissions as QCR
|
||||||
|
* - Saves placement_sheet_id to leads.misc
|
||||||
|
* - Downloads the placement sheet as Excel and returns file info
|
||||||
|
*/
|
||||||
|
protected function createAndDownloadPlacementSheet(array $lead_data): array
|
||||||
|
{
|
||||||
|
$leadId = (int) ($lead_data['id'] ?? 0);
|
||||||
|
$sheetLib = new GoogleSheetLib();
|
||||||
|
|
||||||
|
// Decode leads.misc
|
||||||
|
$leadMisc = [];
|
||||||
|
if (! empty($lead_data['misc'])) {
|
||||||
|
$decoded = json_decode($lead_data['misc'], true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
||||||
|
$leadMisc = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If placement sheet already exists, download and return it
|
||||||
|
if (! empty($leadMisc['placement_sheet_id'])) {
|
||||||
|
$placementSheetId = (string) $leadMisc['placement_sheet_id'];
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: existing sheet found | lead_id={$leadId}, sheet_id={$placementSheetId}");
|
||||||
|
return $this->downloadPlacementExcel($sheetLib, $placementSheetId, $leadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// QCR sheet must exist to create placement
|
||||||
|
$qcrSheetId = $leadMisc['qcr_sheet_id'] ?? null;
|
||||||
|
if (empty($qcrSheetId)) {
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: qcr_sheet_id missing in leads.misc | lead_id={$leadId}");
|
||||||
|
throw new \RuntimeException('QCR sheet not found. Please create the QCR sheet first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get QCR filename and replace "QCR" with "Placement"
|
||||||
|
$qcrFileName = $sheetLib->getFileName($qcrSheetId);
|
||||||
|
$placementFileName = str_ireplace('QCR', 'Placement', $qcrFileName);
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: renaming | qcr_name={$qcrFileName}, placement_name={$placementFileName}");
|
||||||
|
|
||||||
|
// Load folder ID from config
|
||||||
|
$parentFolderId = null;
|
||||||
|
try {
|
||||||
|
$rfqConfig = config('RfqConfig');
|
||||||
|
if ($rfqConfig) {
|
||||||
|
$parentFolderId = $rfqConfig->rfqParentFolderId ?? null;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->myLogger->logme('error', 'Placement Sheet: error loading RfqConfig | ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $parentFolderId) {
|
||||||
|
throw new \RuntimeException('Google config not correct, contact administrator');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy QCR sheet as placement
|
||||||
|
$placementSheetId = $sheetLib->copyTemplate($qcrSheetId, $placementFileName, $parentFolderId);
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: copied from QCR | lead_id={$leadId}, qcr_sheet_id={$qcrSheetId}, placement_sheet_id={$placementSheetId}");
|
||||||
|
|
||||||
|
// Apply same permissions as QCR (editors: sales team, viewers: rfq_qcr_viewers)
|
||||||
|
$salesTeam = $this->userModel
|
||||||
|
->select('user_profiles.email')
|
||||||
|
->join('user_teams', 'user_profiles.id = user_teams.user_id')
|
||||||
|
->where('user_teams.team_id', 5)
|
||||||
|
->where('user_teams.is_active', 1)
|
||||||
|
->where('user_profiles.is_active', 1)
|
||||||
|
->findAll();
|
||||||
|
$editorEmails = array_values(array_filter(array_unique(array_column($salesTeam, 'email'))));
|
||||||
|
|
||||||
|
$viewerEmails = [];
|
||||||
|
if (! empty($lead_data['rfq_qcr_viewers'])) {
|
||||||
|
$decoded = json_decode($lead_data['rfq_qcr_viewers'], true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
$viewerEmails = array_values(array_filter(array_unique($decoded)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($editorEmails) || ! empty($viewerEmails)) {
|
||||||
|
try {
|
||||||
|
$sheetLib->applyPermissions($placementSheetId, [
|
||||||
|
'editors' => $editorEmails,
|
||||||
|
'viewers' => $viewerEmails,
|
||||||
|
]);
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: permissions applied | sheet_id={$placementSheetId}, editors=" . count($editorEmails) . ", viewers=" . count($viewerEmails));
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: failed to apply permissions | sheet_id={$placementSheetId}, error=" . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save placement_sheet_id to leads.misc
|
||||||
|
$leadMisc['placement_sheet_id'] = $placementSheetId;
|
||||||
|
$this->leadsModel->update($leadId, [
|
||||||
|
'misc' => json_encode($leadMisc),
|
||||||
|
]);
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: saved to leads.misc | lead_id={$leadId}, placement_sheet_id={$placementSheetId}");
|
||||||
|
|
||||||
|
return $this->downloadPlacementExcel($sheetLib, $placementSheetId, $leadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a placement Google Sheet as Excel to tmp directory.
|
||||||
|
*/
|
||||||
|
private function downloadPlacementExcel(GoogleSheetLib $sheetLib, string $sheetId, int $leadId): array
|
||||||
|
{
|
||||||
|
$binary = $sheetLib->downloadExcel($sheetId);
|
||||||
|
|
||||||
|
if (! $binary) {
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: empty content from Google | lead_id={$leadId}, sheet_id={$sheetId}");
|
||||||
|
throw new \RuntimeException('Empty content from Google Sheet for placement');
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = "lead_{$leadId}_placement_" . date('Ymd_His') . '.xlsx';
|
||||||
|
$uploadDir = WRITEPATH . 'tmp/';
|
||||||
|
$uploadFilePath = $uploadDir . $filename;
|
||||||
|
|
||||||
|
if (! is_dir($uploadDir)) {
|
||||||
|
if (! mkdir($uploadDir, 0777, true) && ! is_dir($uploadDir)) {
|
||||||
|
throw new \RuntimeException('Failed to create temporary directory');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = file_put_contents($uploadFilePath, $binary);
|
||||||
|
if ($bytes === false) {
|
||||||
|
throw new \RuntimeException('Failed to write placement Excel file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->myLogger->logme('error', "Placement Sheet: downloaded | lead_id={$leadId}, path={$uploadFilePath}");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'filePath' => $uploadFilePath,
|
||||||
|
'fileName' => $filename,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function generateViewPageHtml($policy_type_id, $data = [])
|
public function generateViewPageHtml($policy_type_id, $data = [])
|
||||||
{
|
{
|
||||||
$data['insurer'] = $this->insurerBranchModel->getInsurerBranchesWithInsurerNames();
|
$data['insurer'] = $this->insurerBranchModel->getInsurerBranchesWithInsurerNames();
|
||||||
|
|||||||
@ -81,6 +81,53 @@ class GoogleSheetLib
|
|||||||
return $response->getBody()->getContents();
|
return $response->getBody()->getContents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================= LIST FOLDER SHEETS ===================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists files inside a Google Drive folder and returns JSON with
|
||||||
|
* filename and sheet id (Drive file id / spreadsheet id).
|
||||||
|
*/
|
||||||
|
public function listFolderSheetIdsAsJson(string $folderId = ''): string
|
||||||
|
{
|
||||||
|
if ($folderId === '') {
|
||||||
|
$folderId = '19uySI-PSFQfFpZvMSnBtirXbwfCTXFBL';
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = [];
|
||||||
|
$pageToken = null;
|
||||||
|
$folderIdEsc = str_replace("'", "\\'", $folderId);
|
||||||
|
$query = "'{$folderIdEsc}' in parents and trashed = false";
|
||||||
|
$listParams = [
|
||||||
|
'q' => $query,
|
||||||
|
'fields' => 'nextPageToken, files(id,name,mimeType)',
|
||||||
|
'pageSize' => 1000,
|
||||||
|
'supportsAllDrives' => true,
|
||||||
|
'includeItemsFromAllDrives' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ($pageToken) {
|
||||||
|
$listParams['pageToken'] = $pageToken;
|
||||||
|
} else {
|
||||||
|
unset($listParams['pageToken']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->drive->files->listFiles($listParams);
|
||||||
|
|
||||||
|
foreach (($response->getFiles() ?? []) as $file) {
|
||||||
|
// "sheetId" here is the Drive file id (spreadsheetId for Google Sheets)
|
||||||
|
$results[] = [
|
||||||
|
'name' => (string) $file->getName(),
|
||||||
|
'sheetId' => (string) $file->getId(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageToken = $response->getNextPageToken();
|
||||||
|
} while (! empty($pageToken));
|
||||||
|
|
||||||
|
return json_encode($results, JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= COPY TEMPLATE ================= */
|
/* ================= COPY TEMPLATE ================= */
|
||||||
|
|
||||||
public function copyTemplate(string $templateId, string $name, string $folderId): string
|
public function copyTemplate(string $templateId, string $name, string $folderId): string
|
||||||
@ -100,6 +147,184 @@ class GoogleSheetLib
|
|||||||
return $file->id;
|
return $file->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find/replace text across all sheets in the spreadsheet (batchUpdate).
|
||||||
|
*
|
||||||
|
* @param array<string, string> $replacements placeholder => replacement (empty string allowed)
|
||||||
|
*/
|
||||||
|
public function findAndReplaceAllSheets(string $spreadsheetId, array $replacements): void
|
||||||
|
{
|
||||||
|
if ($replacements === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requests = [];
|
||||||
|
|
||||||
|
foreach ($replacements as $find => $replacement) {
|
||||||
|
if ($find === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$requests[] = [
|
||||||
|
'findReplace' => [
|
||||||
|
'find' => (string) $find,
|
||||||
|
'replacement' => (string) $replacement,
|
||||||
|
'allSheets' => true,
|
||||||
|
'matchCase' => false,
|
||||||
|
'matchEntireCell' => false,
|
||||||
|
'includeFormulas' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requests === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sheets->spreadsheets->batchUpdate(
|
||||||
|
$spreadsheetId,
|
||||||
|
new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
||||||
|
'requests' => $requests,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quote a sheet title for A1 notation ranges.
|
||||||
|
*/
|
||||||
|
private function quoteSheetTitleForRange(string $title): string
|
||||||
|
{
|
||||||
|
return "'" . str_replace("'", "''", $title) . "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1-based column index to Excel column letters (A, B, …, Z, AA, …).
|
||||||
|
*/
|
||||||
|
private function columnNumberToLetters(int $n): string
|
||||||
|
{
|
||||||
|
$s = '';
|
||||||
|
|
||||||
|
while ($n > 0) {
|
||||||
|
$m = ($n - 1) % 26;
|
||||||
|
$s = chr(65 + $m) . $s;
|
||||||
|
$n = intdiv($n - 1, 26);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sheet title from an A1 range like 'RFQ Page'!A1:ZZ3000 or Sheet1!A1.
|
||||||
|
*/
|
||||||
|
private function parseSheetTitleFromBatchRange(string $rangeStr): string
|
||||||
|
{
|
||||||
|
$parts = explode('!', $rangeStr, 2);
|
||||||
|
$sheet = $parts[0] ?? '';
|
||||||
|
|
||||||
|
if ($sheet === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (str_starts_with($sheet, "'")) {
|
||||||
|
return str_replace("''", "'", trim($sheet, "'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate a placeholder in any sheet and write a 2D table starting at that cell.
|
||||||
|
* First row of $tableValues is treated as headers.
|
||||||
|
*
|
||||||
|
* @param list<list<string|int|float>> $tableValues
|
||||||
|
*/
|
||||||
|
public function writeTableAtPlaceholder(string $spreadsheetId, string $placeholder, array $tableValues): bool
|
||||||
|
{
|
||||||
|
if ($placeholder === '' || $tableValues === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = $this->sheets->spreadsheets->get(
|
||||||
|
$spreadsheetId,
|
||||||
|
['fields' => 'sheets(properties(title))']
|
||||||
|
);
|
||||||
|
|
||||||
|
$titles = [];
|
||||||
|
|
||||||
|
foreach ($meta->getSheets() ?? [] as $sheet) {
|
||||||
|
$props = $sheet->getProperties();
|
||||||
|
|
||||||
|
if ($props !== null) {
|
||||||
|
$titles[] = (string) $props->getTitle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($titles === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ranges = [];
|
||||||
|
|
||||||
|
// Use row-based range so API returns row 1 == index 0 (leading empty rows are not dropped).
|
||||||
|
foreach ($titles as $title) {
|
||||||
|
$ranges[] = $this->quoteSheetTitleForRange($title) . '!1:3000';
|
||||||
|
}
|
||||||
|
|
||||||
|
$batch = $this->sheets->spreadsheets_values->batchGet($spreadsheetId, [
|
||||||
|
'ranges' => $ranges,
|
||||||
|
'majorDimension' => 'ROWS',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$valueRanges = $batch->getValueRanges() ?? [];
|
||||||
|
|
||||||
|
foreach ($valueRanges as $vr) {
|
||||||
|
$title = $this->parseSheetTitleFromBatchRange((string) ($vr->getRange() ?? ''));
|
||||||
|
|
||||||
|
if ($title === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = $vr->getValues() ?? [];
|
||||||
|
|
||||||
|
foreach ($values as $r => $row) {
|
||||||
|
if (! is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($row as $c => $cell) {
|
||||||
|
if (! is_string($cell) && ! is_numeric($cell)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text = (string) $cell;
|
||||||
|
|
||||||
|
if (! str_contains($text, $placeholder)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startCol = $this->columnNumberToLetters((int) $c + 1);
|
||||||
|
$startRow = (int) $r + 1;
|
||||||
|
$q = $this->quoteSheetTitleForRange($title);
|
||||||
|
$range = "{$q}!{$startCol}{$startRow}";
|
||||||
|
|
||||||
|
$body = new Google_Service_Sheets_ValueRange([
|
||||||
|
'values' => $tableValues,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->sheets->spreadsheets_values->update(
|
||||||
|
$spreadsheetId,
|
||||||
|
$range,
|
||||||
|
$body,
|
||||||
|
['valueInputOption' => 'USER_ENTERED']
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= PERMISSIONS ================= */
|
/* ================= PERMISSIONS ================= */
|
||||||
|
|
||||||
public function applyPermissions(string $fileId, array $permissions)
|
public function applyPermissions(string $fileId, array $permissions)
|
||||||
@ -233,6 +458,18 @@ class GoogleSheetLib
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================= FILE NAME ================= */
|
||||||
|
|
||||||
|
public function getFileName(string $fileId): string
|
||||||
|
{
|
||||||
|
$file = $this->drive->files->get($fileId, [
|
||||||
|
'supportsAllDrives' => true,
|
||||||
|
'fields' => 'name',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $file->getName() ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= URL ================= */
|
/* ================= URL ================= */
|
||||||
|
|
||||||
public function sheetUrl(string $sheetId): string
|
public function sheetUrl(string $sheetId): string
|
||||||
|
|||||||
@ -109,6 +109,7 @@ class LeadsModel extends Model
|
|||||||
'acm_id',
|
'acm_id',
|
||||||
'policy_with_correction',
|
'policy_with_correction',
|
||||||
'agreed_percentage', 'misc',
|
'agreed_percentage', 'misc',
|
||||||
|
'rfq_qcr_viewers',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
|
|||||||
@ -164,6 +164,9 @@ if (isset($selected_lead_type)) {
|
|||||||
|
|
||||||
$('#client_id').select2();
|
$('#client_id').select2();
|
||||||
$('#client_branch_id').select2();
|
$('#client_branch_id').select2();
|
||||||
|
$('#rfq_qcr_viewers').select2({
|
||||||
|
placeholder: "Select Viewers",
|
||||||
|
});
|
||||||
$('#salse_person_id').select2({
|
$('#salse_person_id').select2({
|
||||||
placeholder: "Select Sales Person",
|
placeholder: "Select Sales Person",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -236,13 +236,13 @@ table.dataTable tbody td {
|
|||||||
<a href="#" class="dropdown-item btnEdit2 btnRfqSheetList" data-id="<?php echo $row['id']; ?>">
|
<a href="#" class="dropdown-item btnEdit2 btnRfqSheetList" data-id="<?php echo $row['id']; ?>">
|
||||||
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>RFQ
|
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>RFQ
|
||||||
</a>
|
</a>
|
||||||
<!-- <?php if (! in_array($row['status'], ['queued', 'rfq_created', 'rfq_sent'])) {?>
|
<?php if (! in_array($row['status'], ['queued', 'rfq_created', 'rfq_sent'])) {?>
|
||||||
<?php if (in_array(get_role_id(), [1, 5, 2, 3]) || in_array(BUSINESS_SUPPORT_TEAM_ID, user_team())) {?>
|
<?php if (in_array(get_role_id(), [1, 5, 2, 3]) || in_array(BUSINESS_SUPPORT_TEAM_ID, user_team())) {?>
|
||||||
<a href="<?php echo base_url('/rfq/nonEB/qcr/') . $row['id']; ?>" class="dropdown-item btnEdit3" data-id="<?php echo $row['id']; ?>">
|
<a href="#" class="dropdown-item btnEdit3 btnQcrSheetList" data-id="<?php echo $row['id']; ?>">
|
||||||
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>QCR
|
<i class="mdi mdi-note-text mr-2 text-muted font-18 vertical-middle"></i>QCR
|
||||||
</a>
|
</a>
|
||||||
<?php }?>
|
<?php }?>
|
||||||
<?php }?> -->
|
<?php }?>
|
||||||
|
|
||||||
<?php if ($row['status'] != 'queued' ){?>
|
<?php if ($row['status'] != 'queued' ){?>
|
||||||
<a href="#" class="dropdown-item btnInternalMailList" data-id="<?php echo $row['id']; ?>">
|
<a href="#" class="dropdown-item btnInternalMailList" data-id="<?php echo $row['id']; ?>">
|
||||||
@ -295,9 +295,34 @@ table.dataTable tbody td {
|
|||||||
</div><!-- end col -->
|
</div><!-- end col -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- External CC Disclaimer Confirmation Modal (z-index elevated to appear above other open modals) -->
|
||||||
|
<div class="modal fade" id="external_cc_disclaimer_modal" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static" data-keyboard="false" style="z-index: 1070;">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title"><i class="mdi mdi-alert-outline text-warning mr-2"></i>External Mail Warning</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>This mail will be sent to <strong>external email address(es)</strong>. Please review before proceeding.</p>
|
||||||
|
<p id="external_cc_disclaimer_list" class="text-muted small mb-0"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="confirm_external_send_btn">Yes, Send Mail</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Hidden field to hold selected lead id for mail modals -->
|
<!-- Hidden field to hold selected lead id for mail modals -->
|
||||||
<input type="hidden" id="lead_id" value="">
|
<input type="hidden" id="lead_id" value="">
|
||||||
|
|
||||||
|
<!-- Sheet creation wait message (shown over loader overlay) -->
|
||||||
|
<div id="sheet-wait-msg" style="display:none; position:fixed; z-index:99999; left:0; right:0; bottom:calc(50% - 70px); text-align:center; color:#fff; font-size:14px; pointer-events:none;">
|
||||||
|
Creating RFQ/QCR file, this may take a while, please wait...
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="New_Lead_modal" tabindex="-1" role="dialog" aria-hidden="true">
|
<div class="modal fade" id="New_Lead_modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@ -486,10 +511,28 @@ table.dataTable tbody td {
|
|||||||
<h4>Attachments Files</h4>
|
<h4>Attachments Files</h4>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="form-group" id="placement_mail_attachment">
|
<div class="attachment-upload-wrap" data-attachment-target="#placement_mail_attachment">
|
||||||
<!-- <div class="form-row">
|
<div class="form-row align-items-end">
|
||||||
<?php echo isset($attachment_html) && !empty($attachment_html) ? $attachment_html : ''; ?>
|
<div class="form-group col-md-4 mb-2">
|
||||||
</div> -->
|
<label>Document Label <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control att-docs-name" placeholder="Enter document label">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-5 mb-2">
|
||||||
|
<label>File <span class="text-danger">*</span></label>
|
||||||
|
<input type="file" class="form-control att-file-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-3 mb-2">
|
||||||
|
<label> </label><br>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm btn-upload-att">Upload & Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="placement_mail_attachment"></div>
|
||||||
|
|
||||||
|
<div class="form-group" id="external_cc_placement_wrap">
|
||||||
|
<label for="external_cc_placement">External CC <small class="text-muted">(comma-separated email addresses)</small></label>
|
||||||
|
<input type="text" class="form-control" id="external_cc_placement" placeholder="e.g. john@external.com, jane@external.com">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- if status is won disable the button -->
|
<!-- if status is won disable the button -->
|
||||||
@ -522,12 +565,34 @@ table.dataTable tbody td {
|
|||||||
<h4>Attachments Files</h4>
|
<h4>Attachments Files</h4>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<div class="attachment-upload-wrap" data-attachment-target="#insurer_or_clinet_mail_attachment">
|
||||||
|
<div class="form-row align-items-end">
|
||||||
|
<div class="form-group col-md-4 mb-2">
|
||||||
|
<label>Document Label <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control att-docs-name" placeholder="Enter document label">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-5 mb-2">
|
||||||
|
<label>File <span class="text-danger">*</span></label>
|
||||||
|
<input type="file" class="form-control att-file-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-3 mb-2">
|
||||||
|
<label> </label><br>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm btn-upload-att">Upload & Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="insurer_or_clinet_mail_attachment">
|
<div class="form-group" id="insurer_or_clinet_mail_attachment">
|
||||||
<div class="form-row attachmet_row">
|
<div class="form-row attachmet_row">
|
||||||
<?php echo isset($attachment_html) && ! empty($attachment_html) ? $attachment_html : ''; ?>
|
<?php echo isset($attachment_html) && ! empty($attachment_html) ? $attachment_html : ''; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="external_cc_client_wrap" style="display:none;">
|
||||||
|
<label for="external_cc_client">External CC <small class="text-muted">(comma-separated email addresses)</small></label>
|
||||||
|
<input type="text" class="form-control" id="external_cc_client" placeholder="e.g. john@external.com, jane@external.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group text-right m-b-0" id="hide_smbt_btn">
|
<div class="form-group text-right m-b-0" id="hide_smbt_btn">
|
||||||
<button type="submit" class="btn btn-primary" onclick="constructURL(1)">Send Mail</button>
|
<button type="submit" class="btn btn-primary" onclick="constructURL(1)">Send Mail</button>
|
||||||
</div>
|
</div>
|
||||||
@ -595,6 +660,23 @@ table.dataTable tbody td {
|
|||||||
<h4>Attachments Files</h4>
|
<h4>Attachments Files</h4>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
<div class="attachment-upload-wrap" data-attachment-target="#internal_mail_attachment">
|
||||||
|
<div class="form-row align-items-end">
|
||||||
|
<div class="form-group col-md-4 mb-2">
|
||||||
|
<label>Document Label <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control att-docs-name" placeholder="Enter document label">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-5 mb-2">
|
||||||
|
<label>File <span class="text-danger">*</span></label>
|
||||||
|
<input type="file" class="form-control att-file-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-md-3 mb-2">
|
||||||
|
<label> </label><br>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm btn-upload-att">Upload & Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="internal_mail_attachment">
|
<div class="form-group" id="internal_mail_attachment">
|
||||||
<div class="form-row attachmet_row">
|
<div class="form-row attachmet_row">
|
||||||
<?php echo isset($attachment_html) && ! empty($attachment_html) ? $attachment_html : ''; ?>
|
<?php echo isset($attachment_html) && ! empty($attachment_html) ? $attachment_html : ''; ?>
|
||||||
@ -602,7 +684,7 @@ table.dataTable tbody td {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div class="form-group text-right m-b-0" id="hide_smbt_btn">
|
<div class="form-group text-right m-b-0" id="hide_smbt_btn">
|
||||||
<button type="submit" class="btn btn-primary" onclick="constructURL(2)">Send Mail</button>
|
<button type="submit" class="btn btn-primary" onclick="constructURL(2)">Send Mail</button>
|
||||||
</div>
|
</div>
|
||||||
@ -768,6 +850,12 @@ table.dataTable tbody td {
|
|||||||
createRfqSheetFromList(leadId);
|
createRfqSheetFromList(leadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}else if (this.classList.contains('btnQcrSheetList')) {
|
||||||
|
const leadId = this.getAttribute('data-id');
|
||||||
|
if (leadId) {
|
||||||
|
createQcrSheetFromList(leadId);
|
||||||
|
}
|
||||||
|
|
||||||
}else if (this.classList.contains('btnInternalMailList')) {
|
}else if (this.classList.contains('btnInternalMailList')) {
|
||||||
const leadId = this.getAttribute('data-id');
|
const leadId = this.getAttribute('data-id');
|
||||||
if (leadId) {
|
if (leadId) {
|
||||||
@ -807,6 +895,7 @@ table.dataTable tbody td {
|
|||||||
href !== '#' &&
|
href !== '#' &&
|
||||||
!this.classList.contains('btnEdit') &&
|
!this.classList.contains('btnEdit') &&
|
||||||
!this.classList.contains('btnRfqSheetList') &&
|
!this.classList.contains('btnRfqSheetList') &&
|
||||||
|
!this.classList.contains('btnQcrSheetList') &&
|
||||||
!this.classList.contains('btnInternalMailList') &&
|
!this.classList.contains('btnInternalMailList') &&
|
||||||
!this.classList.contains('btnInsurerMailList') &&
|
!this.classList.contains('btnInsurerMailList') &&
|
||||||
!this.classList.contains('btnClientMailList') &&
|
!this.classList.contains('btnClientMailList') &&
|
||||||
@ -1123,11 +1212,15 @@ table.dataTable tbody td {
|
|||||||
$('#myCenterModalLabelForClientAndInsurer').html(modalTitle);
|
$('#myCenterModalLabelForClientAndInsurer').html(modalTitle);
|
||||||
console.log('modalTitle' + modalTitle);
|
console.log('modalTitle' + modalTitle);
|
||||||
url = '<?php echo base_url('leads/list/') ?>' + lead_id;
|
url = '<?php echo base_url('leads/list/') ?>' + lead_id;
|
||||||
|
$('#external_cc_client_wrap').show();
|
||||||
|
$('#external_cc_client').val('');
|
||||||
} else {
|
} else {
|
||||||
modalTitle = 'Insurer Mail';
|
modalTitle = 'Insurer Mail';
|
||||||
$('#myCenterModalLabelForClientAndInsurer').html(modalTitle);
|
$('#myCenterModalLabelForClientAndInsurer').html(modalTitle);
|
||||||
console.log('modalTitle' + modalTitle);
|
console.log('modalTitle' + modalTitle);
|
||||||
url = '<?php echo base_url('util/getLevelContects') ?>';
|
url = '<?php echo base_url('util/getLevelContects') ?>';
|
||||||
|
$('#external_cc_client_wrap').hide();
|
||||||
|
$('#external_cc_client').val('');
|
||||||
}
|
}
|
||||||
|
|
||||||
ajaxRequestForGetMailData(url);
|
ajaxRequestForGetMailData(url);
|
||||||
@ -1336,20 +1429,84 @@ table.dataTable tbody td {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateExternalCC(rawValue) {
|
||||||
|
if (!rawValue || rawValue.trim() === '') return { valid: true, emails: [] };
|
||||||
|
var emails = rawValue.split(',').map(function(e) { return e.trim(); }).filter(function(e) { return e !== ''; });
|
||||||
|
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
var invalid = emails.filter(function(e) { return !emailRegex.test(e); });
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
return { valid: false, emails: emails, invalid: invalid };
|
||||||
|
}
|
||||||
|
return { valid: true, emails: emails };
|
||||||
|
}
|
||||||
|
|
||||||
|
function showExternalCCDisclaimer(emails, onConfirmCallback) {
|
||||||
|
var listHtml = 'Sending to: <strong>' + emails.join(', ') + '</strong>';
|
||||||
|
$('#external_cc_disclaimer_list').html(listHtml);
|
||||||
|
window._externalCCSendCallback = onConfirmCallback;
|
||||||
|
var disclaimerModalEl = document.getElementById('external_cc_disclaimer_modal');
|
||||||
|
var disclaimerModal = new bootstrap.Modal(disclaimerModalEl);
|
||||||
|
disclaimerModal.show();
|
||||||
|
// Elevate the latest backdrop so it sits above the parent mail modal
|
||||||
|
setTimeout(function() {
|
||||||
|
$('.modal-backdrop').last().css('z-index', 1065);
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).on('click', '#confirm_external_send_btn', function () {
|
||||||
|
// Close disclaimer modal first
|
||||||
|
$('#external_cc_disclaimer_modal').modal('hide');
|
||||||
|
|
||||||
|
// Execute the pending send callback
|
||||||
|
if (typeof window._externalCCSendCallback === 'function') {
|
||||||
|
window._externalCCSendCallback();
|
||||||
|
window._externalCCSendCallback = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function constructURL(url_type) {
|
function constructURL(url_type) {
|
||||||
|
|
||||||
if (url_type == 1) {
|
if (url_type == 1) {
|
||||||
var validationStatus = checkMailValidation();
|
var validationStatus = checkMailValidation();
|
||||||
|
if (!validationStatus) {
|
||||||
if (validationStatus) {
|
|
||||||
constructURL_ForInsurerAndClientMailSend();
|
|
||||||
} else {
|
|
||||||
toastr.error("All Client Mail ID's Should Contain Same Domain", "Error");
|
toastr.error("All Client Mail ID's Should Contain Same Domain", "Error");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RFQ_or_QCR == 2) {
|
||||||
|
var externalCCRaw = $('#external_cc_client').val().trim();
|
||||||
|
var result = validateExternalCC(externalCCRaw);
|
||||||
|
if (!result.valid) {
|
||||||
|
toastr.error('Invalid email(s) in External CC: ' + result.invalid.join(', '), 'Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.emails.length > 0) {
|
||||||
|
showExternalCCDisclaimer(result.emails, function() {
|
||||||
|
constructURL_ForInsurerAndClientMailSend();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
constructURL_ForInsurerAndClientMailSend();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
constructURL_ForInsurerAndClientMailSend();
|
||||||
|
}
|
||||||
|
|
||||||
} else if (url_type == 2) {
|
} else if (url_type == 2) {
|
||||||
constructURL_ForInternalMailSend();
|
constructURL_ForInternalMailSend();
|
||||||
} else if (url_type == 3) {
|
} else if (url_type == 3) {
|
||||||
constructURL_ForPlacementMailSend();
|
var externalCCRaw = $('#external_cc_placement').val().trim();
|
||||||
|
var result = validateExternalCC(externalCCRaw);
|
||||||
|
if (!result.valid) {
|
||||||
|
toastr.error('Invalid email(s) in External CC: ' + result.invalid.join(', '), 'Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.emails.length > 0) {
|
||||||
|
showExternalCCDisclaimer(result.emails, function() {
|
||||||
|
constructURL_ForPlacementMailSend();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
constructURL_ForPlacementMailSend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1386,6 +1543,8 @@ table.dataTable tbody td {
|
|||||||
formData.append('selected_attachment_files', JSON.stringify(selectedFiles));
|
formData.append('selected_attachment_files', JSON.stringify(selectedFiles));
|
||||||
|
|
||||||
if (RFQ_or_QCR == 2) {
|
if (RFQ_or_QCR == 2) {
|
||||||
|
var externalCCClient = validateExternalCC($('#external_cc_client').val().trim()).emails;
|
||||||
|
formData.append('external_cc', JSON.stringify(externalCCClient));
|
||||||
formData.append('contact_mail', contact_mail);
|
formData.append('contact_mail', contact_mail);
|
||||||
formData.append('file_type', 'qcr');
|
formData.append('file_type', 'qcr');
|
||||||
formData.append('recipient_type', 'client');
|
formData.append('recipient_type', 'client');
|
||||||
@ -1519,6 +1678,10 @@ table.dataTable tbody td {
|
|||||||
formData.append('policy_end_date', policy_end_date);
|
formData.append('policy_end_date', policy_end_date);
|
||||||
formData.append('policy_start_date', policy_start_date);
|
formData.append('policy_start_date', policy_start_date);
|
||||||
|
|
||||||
|
var externalCCPlacement = validateExternalCC($('#external_cc_placement').val().trim()).emails;
|
||||||
|
var mergedPlacementCC = (cc || []).concat(externalCCPlacement);
|
||||||
|
formData.set('cc', JSON.stringify(mergedPlacementCC));
|
||||||
|
|
||||||
ajaxRequest(formData);
|
ajaxRequest(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1526,6 +1689,7 @@ table.dataTable tbody td {
|
|||||||
|
|
||||||
var apiURL = '<?php echo base_url("leads/createRfqSheet") ?>';
|
var apiURL = '<?php echo base_url("leads/createRfqSheet") ?>';
|
||||||
|
|
||||||
|
$('#sheet-wait-msg').fadeIn();
|
||||||
$('.loader').fadeIn();
|
$('.loader').fadeIn();
|
||||||
$('.loader-mask').fadeIn();
|
$('.loader-mask').fadeIn();
|
||||||
|
|
||||||
@ -1536,6 +1700,7 @@ table.dataTable tbody td {
|
|||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
success: function(res) {
|
success: function(res) {
|
||||||
|
|
||||||
|
$('#sheet-wait-msg').fadeOut();
|
||||||
$('.loader').fadeOut();
|
$('.loader').fadeOut();
|
||||||
$('.loader-mask').delay(350).fadeOut('slow');
|
$('.loader-mask').delay(350).fadeOut('slow');
|
||||||
|
|
||||||
@ -1547,6 +1712,7 @@ table.dataTable tbody td {
|
|||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function(xhr, status, error) {
|
||||||
|
|
||||||
|
$('#sheet-wait-msg').fadeOut();
|
||||||
$('.loader').fadeOut();
|
$('.loader').fadeOut();
|
||||||
$('.loader-mask').delay(350).fadeOut('slow');
|
$('.loader-mask').delay(350).fadeOut('slow');
|
||||||
console.error(xhr.responseText);
|
console.error(xhr.responseText);
|
||||||
@ -1556,6 +1722,101 @@ table.dataTable tbody td {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createQcrSheetFromList(leadId) {
|
||||||
|
|
||||||
|
var apiURL = '<?php echo base_url("leads/createQcrSheet") ?>';
|
||||||
|
|
||||||
|
$('#sheet-wait-msg').fadeIn();
|
||||||
|
$('.loader').fadeIn();
|
||||||
|
$('.loader-mask').fadeIn();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: apiURL,
|
||||||
|
type: "GET",
|
||||||
|
data: { lead_id: leadId },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(res) {
|
||||||
|
|
||||||
|
$('#sheet-wait-msg').fadeOut();
|
||||||
|
$('.loader').fadeOut();
|
||||||
|
$('.loader-mask').delay(350).fadeOut('slow');
|
||||||
|
|
||||||
|
if (res.status === 'success' && res.code === 200 && res.sheet_url) {
|
||||||
|
window.open(res.sheet_url, '_blank');
|
||||||
|
} else {
|
||||||
|
toastr.error(res.message || 'Failed to create QCR sheet', 'Error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
|
||||||
|
$('#sheet-wait-msg').fadeOut();
|
||||||
|
$('.loader').fadeOut();
|
||||||
|
$('.loader-mask').delay(350).fadeOut('slow');
|
||||||
|
console.error(xhr.responseText);
|
||||||
|
console.error(status, error);
|
||||||
|
toastr.error('Failed to create QCR sheet', 'Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegated click handler for all modal upload buttons
|
||||||
|
$(document).on('click', '.btn-upload-att', function () {
|
||||||
|
var wrap = $(this).closest('.attachment-upload-wrap');
|
||||||
|
var attachTarget = wrap.data('attachment-target');
|
||||||
|
var docsName = wrap.find('.att-docs-name').val().trim();
|
||||||
|
var fileInput = wrap.find('.att-file-input')[0];
|
||||||
|
var file = fileInput && fileInput.files[0];
|
||||||
|
var leadId = $('#lead_id').val();
|
||||||
|
|
||||||
|
if (!leadId) {
|
||||||
|
toastr.error('Lead not selected.', 'Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!docsName) {
|
||||||
|
toastr.error('Please enter a document label.', 'Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
toastr.error('Please select a file to upload.', 'Error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('lead_id', leadId);
|
||||||
|
formData.append('docs_name', docsName);
|
||||||
|
formData.append('attachment_file', file);
|
||||||
|
|
||||||
|
$('.loader').fadeIn();
|
||||||
|
$('.loader-mask').fadeIn();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '<?php echo base_url("leads/uploadLeadAttachment") ?>',
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (res) {
|
||||||
|
$('.loader').fadeOut();
|
||||||
|
$('.loader-mask').delay(350).fadeOut('slow');
|
||||||
|
if (res.status === 'success') {
|
||||||
|
$(attachTarget).html(res.attachment_html || '');
|
||||||
|
wrap.find('.att-docs-name').val('');
|
||||||
|
wrap.find('.att-file-input').val('');
|
||||||
|
toastr.success('File uploaded successfully.', 'Success');
|
||||||
|
} else {
|
||||||
|
toastr.error(res.message || 'Upload failed.', 'Error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
$('.loader').fadeOut();
|
||||||
|
$('.loader-mask').delay(350).fadeOut('slow');
|
||||||
|
console.error(xhr.responseText, status, error);
|
||||||
|
toastr.error('Upload failed.', 'Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function fetchPlacementData(leadId, callback) {
|
function fetchPlacementData(leadId, callback) {
|
||||||
|
|
||||||
var apiURL = '<?php echo base_url("rfq/placementData") ?>/' + leadId;
|
var apiURL = '<?php echo base_url("rfq/placementData") ?>/' + leadId;
|
||||||
@ -1698,33 +1959,19 @@ table.dataTable tbody td {
|
|||||||
$('#placement_mail_btn').prop('disabled', false);
|
$('#placement_mail_btn').prop('disabled', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate TO with insurer contacts (id -> contact id)
|
// Populate TO with client contact from lead data
|
||||||
var $placementTo = $('#placement_to');
|
var $placementTo = $('#placement_to');
|
||||||
$placementTo.empty().append('<option value="">Select TO Mail</option>');
|
$placementTo.empty().append('<option value="">Select TO Mail</option>');
|
||||||
if (Array.isArray(data.insurer_contacts)) {
|
if (data.client_contact && data.client_contact.email) {
|
||||||
data.insurer_contacts.forEach(function (c) {
|
var displayText = (data.client_contact.name || '') + ' - ' + data.client_contact.email;
|
||||||
if (c.id) {
|
$placementTo.append(
|
||||||
$placementTo.append(
|
$('<option>', {
|
||||||
$('<option>', {
|
value: data.client_contact.email,
|
||||||
value: c.id,
|
text: displayText.replace(/^- /, '')
|
||||||
text: c.display || c.email || ''
|
})
|
||||||
})
|
);
|
||||||
);
|
// Select by default
|
||||||
}
|
$placementTo.val(data.client_contact.email);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select all insurer contacts by default, if any
|
|
||||||
var defaultTo = [];
|
|
||||||
if (Array.isArray(data.insurer_contacts)) {
|
|
||||||
data.insurer_contacts.forEach(function (c) {
|
|
||||||
if (c.id) {
|
|
||||||
defaultTo.push(String(c.id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (defaultTo.length) {
|
|
||||||
$placementTo.val(defaultTo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.RFQ_or_QCR = 2; // Placement behaves like QCR for file_type
|
window.RFQ_or_QCR = 2; // Placement behaves like QCR for file_type
|
||||||
|
|||||||
@ -371,6 +371,29 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" data-toggle="toolt
|
|||||||
<!-- other row -->
|
<!-- other row -->
|
||||||
<div class="card" id="collapseOne">
|
<div class="card" id="collapseOne">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="form-row">
|
||||||
|
<?php
|
||||||
|
$savedViewers = [];
|
||||||
|
if (!empty($lead_edit_data['rfq_qcr_viewers'] ?? null)) {
|
||||||
|
$decoded = json_decode($lead_edit_data['rfq_qcr_viewers'], true);
|
||||||
|
if (is_array($decoded)) $savedViewers = $decoded;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="form-group col-md-12">
|
||||||
|
<label for="rfq_qcr_viewers">RFQ/QCR File Viewers <span class="text-danger">*</span></label>
|
||||||
|
<select class="form-control" id="rfq_qcr_viewers" name="rfq_qcr_viewers" multiple required>
|
||||||
|
<?php if (isset($salse_team)) { ?>
|
||||||
|
<?php foreach ($salse_team as $member) { ?>
|
||||||
|
<?php $email = $member['email'] ?? ''; if (empty($email)) continue; ?>
|
||||||
|
<option value="<?= htmlspecialchars($email) ?>" <?= in_array($email, $savedViewers) ? 'selected' : '' ?>>
|
||||||
|
<?= htmlspecialchars($member['first_name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php } ?>
|
||||||
|
<?php } ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|
||||||
<!-- <div class="form-group col-md-3">
|
<!-- <div class="form-group col-md-3">
|
||||||
@ -443,6 +466,14 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" data-toggle="toolt
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
$('#rfq_qcr_viewers').parsley({
|
||||||
|
errorsContainer: function(ParsleyField) {
|
||||||
|
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
|
||||||
|
return ParsleyField.$element.siblings('.select2-container');
|
||||||
|
}
|
||||||
|
return ParsleyField.$element;
|
||||||
|
}
|
||||||
|
});
|
||||||
$('#salse_person_id').parsley({
|
$('#salse_person_id').parsley({
|
||||||
errorsContainer: function(ParsleyField) {
|
errorsContainer: function(ParsleyField) {
|
||||||
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
|
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
|
||||||
@ -596,6 +627,9 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" data-toggle="toolt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rfq_qcr_viewers = $("#rfq_qcr_viewers").val();
|
||||||
|
console.log('rfq_qcr_viewers : ', rfq_qcr_viewers);
|
||||||
|
|
||||||
var salse_person_id = $("#salse_person_id").val();
|
var salse_person_id = $("#salse_person_id").val();
|
||||||
console.log('salse_person_id : ', salse_person_id);
|
console.log('salse_person_id : ', salse_person_id);
|
||||||
|
|
||||||
@ -639,6 +673,7 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" data-toggle="toolt
|
|||||||
formData.append('claim_history',cliam_history_status);
|
formData.append('claim_history',cliam_history_status);
|
||||||
|
|
||||||
// Append the JSON string to the FormData object
|
// Append the JSON string to the FormData object
|
||||||
|
formData.append('rfq_qcr_viewers', JSON.stringify(rfq_qcr_viewers));
|
||||||
formData.append('salse_person_id', jsonString);
|
formData.append('salse_person_id', jsonString);
|
||||||
formData.append('custom_fields', JSON.stringify(customFieldData));
|
formData.append('custom_fields', JSON.stringify(customFieldData));
|
||||||
|
|
||||||
|
|||||||
61
public/dev_logs/2026-03-20.md
Normal file
61
public/dev_logs/2026-03-20.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Release Summary — 2026-03-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. QCR Sheet Creation (Non-EB Lead)
|
||||||
|
|
||||||
|
- Added **QCR** button in the Non-EB lead row dropdown (leads list page).
|
||||||
|
- Clicking QCR copies the existing RFQ Google Sheet for that lead, renames the copy by replacing "RFQ" with "QCR" in the filename, and places it in the same parent folder.
|
||||||
|
- The created sheet's Google File ID is saved to `leads.misc` (as `qcr_sheet_id`).
|
||||||
|
- Lead status is updated to `qcr_created` upon successful creation.
|
||||||
|
- If a QCR sheet already exists for the lead, the existing sheet URL is returned directly (no duplicate creation).
|
||||||
|
- A full-screen "Creating RFQ/QCR file, this may take a while, please wait..." overlay is shown during the AJAX call and hidden on completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. RFQ/QCR Parent Folder ID — Environment Variable
|
||||||
|
|
||||||
|
- `RFQ_PARENT_FOLDER_ID` is now loaded from the `.env` file instead of being hardcoded in `RfqConfig.php`.
|
||||||
|
- Placeholder key added to `.env.sample` for environment setup reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. RFQ/QCR File Viewers (Non-EB Lead Form)
|
||||||
|
|
||||||
|
- Added a **"RFQ/QCR File Viewers"** searchable multi-select field (Select2) on the Non-EB lead create/edit form, positioned before the Sales Person and Status fields.
|
||||||
|
- Populated with sales team email addresses.
|
||||||
|
- At least one viewer must be selected (validated on submit).
|
||||||
|
- Selected viewer emails are stored as a JSON array in a new database column `leads.rfq_qcr_viewers`.
|
||||||
|
- When an RFQ or QCR sheet is created, the selected viewer emails are applied as Google Sheet **viewer** permissions automatically.
|
||||||
|
|
||||||
|
> **DB Change Required:**
|
||||||
|
> ```sql
|
||||||
|
> ALTER TABLE leads ADD COLUMN rfq_qcr_viewers TEXT NULL;
|
||||||
|
> ```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Lead Attachment File Upload (All Mail Modals)
|
||||||
|
|
||||||
|
- Added a **file upload control** (label, file input, "Upload & Add" button) inside the attachment section of all four mail modals: Internal Mail, Client Mail, Insurer Mail, and Placement Mail.
|
||||||
|
- On upload:
|
||||||
|
- File is saved to the server under `uploads/lead_files/`.
|
||||||
|
- A record is inserted into the `lead_files` table.
|
||||||
|
- The attachment selection list in the modal is refreshed automatically to include the newly uploaded file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. External CC Field (Client Mail & Placement Mail)
|
||||||
|
|
||||||
|
- Added a manual **External CC** input field (comma-separated email addresses) to:
|
||||||
|
- **Client Mail modal** — visible only when sending a Client Mail (hidden for Insurer Mail).
|
||||||
|
- **Placement Mail modal** — always visible.
|
||||||
|
- On send:
|
||||||
|
1. The field value is validated as a comma-separated list of valid email addresses. Invalid entries show an error toast.
|
||||||
|
2. If valid external emails are present, a **Bootstrap confirmation modal** warns the user that the mail will be sent to external addresses.
|
||||||
|
3. On confirmation, the mail is sent; on cancel, the action is aborted.
|
||||||
|
- **Merge strategy:**
|
||||||
|
- **Placement Mail**: external CC emails are merged directly into the CC array (both are email strings) on the client side before the AJAX call.
|
||||||
|
- **Client Mail**: external CC emails are kept as a separate field and merged into the CC list on the server side, after internal user CC addresses are resolved from user IDs.
|
||||||
|
|
||||||
|
---
|
||||||
334
public/sheetid.json
Normal file
334
public/sheetid.json
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Office Package",
|
||||||
|
"sheetId": "1TNYCzLLjC-RoEBXBKHwYMAeku5wU-_2UVSkhhPDD6hw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation – Loss of License Insurance Policy",
|
||||||
|
"sheetId": "1MQAzdirR-ecthYcmEuDSJ0CPgrSwMJgGfZaQAVTK1VE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Marine Hull - All Risk Insurance Policy",
|
||||||
|
"sheetId": "10_0j4IAC4IhFD216FqnNb41aUiuJIQUnJEhtncMnRzk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_HH Policy",
|
||||||
|
"sheetId": "1k-wNlYx1TwCc9GDSlZ1pO3ZxMzQNFj32u2YOY-f7A88"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Industrial All Risk",
|
||||||
|
"sheetId": "1Fnh0Qz1T9isSc2tFqgP4BK6Om6PJkMwWJTZowt1RTyU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_FLOP",
|
||||||
|
"sheetId": "1SA4TP7qEkOugkacapzB5vQ8Odn5nXiTxaaGmix0p0mo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Boiler",
|
||||||
|
"sheetId": "1rLl0u7rYpeRXcZirxISTVORdsEGq-CRc7N5R8VlUh_g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Aviation Hull & War Risk",
|
||||||
|
"sheetId": "1tvZVH6o_YjEpehbG5FsZaMy61dH02uUJ2y2rcQFRp_M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Public Liability",
|
||||||
|
"sheetId": "117HIyRMctJbLtfll_wdVsEt5iKYRJjCwsZvmv_CBm74"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Marine STOP",
|
||||||
|
"sheetId": "16WYQS1Xr9ssolXDJepL0UfQi6v1kS4FFMEjI8DdDnKA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_Fidelity",
|
||||||
|
"sheetId": "1QGsLNYiQPxhk3Q3Jzz3HcFAxevvU0HuUbHhNtjmPuCs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Windmill Package Insurance",
|
||||||
|
"sheetId": "1UIMfsByDM_MhjC3bpSzX9GmQcvU2g8zzJQmQteDJKqM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Transport Operators Liability Insurance",
|
||||||
|
"sheetId": "14TEVfNDRSKDSkVpiAfRPYoNOAEnMX9dbd5tkdtmnNyA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Weather & Climate Risk Insurance Policy",
|
||||||
|
"sheetId": "1nEUmqks08CUDG3UH0LYH4C_BRlILDfVltaRNaAxDHCs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Speciality Insurance Policy",
|
||||||
|
"sheetId": "1c8h64urogtBlUOITJ678KxZCZjduw5lUW7QYCNFsqpI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Student Travel Insurance Policy",
|
||||||
|
"sheetId": "10EaGQSU7162pe-BqB8wX9MIQJaCu6YCHEfivMzvBzmI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Students Safety Insurance Policy",
|
||||||
|
"sheetId": "1s39fQO0yjJytMJOU5l_hqUqA7cYrIFUds59M8448Gww"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Trade Credit Insurance Policy",
|
||||||
|
"sheetId": "1u74-M497HRUrq-bqksc2ISzSN87uMlerHNaRXAvEejY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Title & Deed Insurance Policy",
|
||||||
|
"sheetId": "16nK9DDYY2JGedCVDIipoA2IWxRMF9RC-W6T6kS7fZjQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Special Contingency Insurance Policy",
|
||||||
|
"sheetId": "1SLqEpsHTImR7weNh4NT3euL_ojG6x2kSw2P3O_nepss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Surety Bond Insurance",
|
||||||
|
"sheetId": "1Yp1tvIuv4FQeW4bf011XIrepZLAtM1YZHLlSuWP_5WE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ_All Risk",
|
||||||
|
"sheetId": "1V-GfgiGMUR3sEbELbmmBLRqP7IBqEuEbuvrq-ue62Fs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Solar Panel Insurance Policy",
|
||||||
|
"sheetId": "1rl_d1zCGZWWvaiTDiT14MaSsGvR9AT26x67zAxMGaJA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Senior Citizen Travel Insurance Policy",
|
||||||
|
"sheetId": "1F7_bWtZVPOrC0DXSPjoklMZvWzW_LAqmC7EdWgnKhe8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Raasta Aapatti Kavach Personal Accident & Hospitalization Policy",
|
||||||
|
"sheetId": "1fG9QknL7E08VvJfbj61pb6pgBfY5wOaUM23HoVF_1X0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - SAT Policy",
|
||||||
|
"sheetId": "18lk62vz7-6NiO1-brjYvNKZmE1T-H9usZx43yMhIHx8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Product Recall Insurance Policy",
|
||||||
|
"sheetId": "1wkn13u-2kfWkN1W7JhSc9x5MN6bXLmrD3fnG08_opxs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Pet Insurance Policy",
|
||||||
|
"sheetId": "1Jz7uRMd6Ln-8iKIj2hwVM7dfkarPbGcS8U2NHUP9hmo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Mobile & Smart Device Insurance",
|
||||||
|
"sheetId": "1n7HVOMzBciHLpcykLUkYSgAg4SrcjuG6598VS6SVbyI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Neon SIgn",
|
||||||
|
"sheetId": "1kBv9hFWh1-IIgvXXHhcxUj7aqP4E8rWrAdmcS7lVSpU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Port Package Insurance Policy",
|
||||||
|
"sheetId": "1hXA2RYCeUiMDFLBbNs_C3rdA2eIUlKBf63QiB2sbqJo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Shopkeeper_s Insurance Policy",
|
||||||
|
"sheetId": "1-l5VAocxtI1VXBESHeK9jC4nQ32Ifvbgv_zlCc71shM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Pedal Cycle",
|
||||||
|
"sheetId": "1lNRU0JQaIhR-cqICKAa9HMnaCkFM6si6szpZGCCggdg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Plate Glass",
|
||||||
|
"sheetId": "19Rt8pGT1FD2yAukecDR_10LZ1PQSbqrw2T8NCvms1Tk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - MLOP Insurance",
|
||||||
|
"sheetId": "12Fdo42afIqvZcUCMOlcn2Gd-1CDHAfvF6CLDH1xGMBg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Mega Risk Insurance Policy",
|
||||||
|
"sheetId": "17a1eg9FghbrF2PRK-hMXs8Zm46VynW7ACRqjjh1WXcM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - MBD",
|
||||||
|
"sheetId": "1EtZY6PqEvDUjIhi78_Bfr4IULk8IcJA3zE6FC508Sk0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Protection and Indemnity Insurance Policy",
|
||||||
|
"sheetId": "1RtesIhxwWq67lClvNYpVJyKCC6N2AETe_CEjRteChzw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Poultry Insurance Policy",
|
||||||
|
"sheetId": "1GuxPTtbYXSlktOLJEO6vyiEsBs-ic2v9dMV7IgH2Ecg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Marine Specific Voyage",
|
||||||
|
"sheetId": "1PlIEq-BoHKYAxIw6mWv0nyWE6QCmH8ZO16Tvi8mTTaU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Marine Open Policy",
|
||||||
|
"sheetId": "1ynm1JQAXczbka1ry3gRPghfGBnjnwgW3bfj0oJEtaq4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Marine Hull and Machinery Insurance Policy",
|
||||||
|
"sheetId": "1kP_4Esgq_5rxCAS-dRw5cdJl3njLcoKfHW20B4x5Xs8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Marine Hull - War Risk Insurance Policy",
|
||||||
|
"sheetId": "1NoLy0yHdPMnhPKPBwdfB9RCjqrNCdrcRD7eBRKqgO5Q"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Marine Cum Erection Insurance Policy",
|
||||||
|
"sheetId": "1lNfSpu2ME30ALlAT6WDgeHf-DuCNQ5VyrS6kjiNUOOc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Marine Builders Risk Insurance Policy",
|
||||||
|
"sheetId": "1L5ndbcuH52pVbo3nMTtzXlKcmAQLavOg8v72x4fwd5g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Lift Insurance",
|
||||||
|
"sheetId": "19yECPX4-O0ZwyzfOr4IxKKnqFMbL64qYDoDwKbGKzHg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Group Travel Insurance Policy",
|
||||||
|
"sheetId": "1awP1gvx4tN33jDJa6eNBqoR6w10w7EbLxK5n6ff_pmE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Individual Travel Insurance Policy",
|
||||||
|
"sheetId": "1GFPUHB_cC6gFNX_guPphnZwqx-DJWBxv0NfgWyXOoZU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Home Insurance",
|
||||||
|
"sheetId": "1E-L0kyD2J7pXEBl5VIdallcDJpnSvE-LkcmOYA4nNTM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Livestock Insurance Policy",
|
||||||
|
"sheetId": "1OYLD5SclbhoDzIrY5f9Mbj-jAP107dsItPnT9a3bJGk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Kidnap and Ransom Insurance Policy",
|
||||||
|
"sheetId": "1HPt2xYCvFV-6Rw_hStiinvmxdJpXsHhm08vgP-SuoIo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Jewellery Package Policy",
|
||||||
|
"sheetId": "1cEr1IJos0N8Z2VXkR8vtfyP8yaw2qe5XFQHm-C0GbPk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - FLOP",
|
||||||
|
"sheetId": "1_SDiTcBRMDTtuze2uPMq96s7ikl4IEje5aMvAbjaCuY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Gratuity Insurance Policy",
|
||||||
|
"sheetId": "1Db7c18YBzDDJQ2M7ZyzJaN-yUSfhYO4A90SZaQJ9b9M"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Farmer_s Package Insurance Policy",
|
||||||
|
"sheetId": "10sNZmmfpyIfXzAZZr8mt5DrVR4OFttkVBbkKhLAytqE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Exhibition Insurance Policy",
|
||||||
|
"sheetId": "1fANuP8iKjGvdYkRCdXjLKfxt3jTNorXEOnmX23FHJ5w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Event Insurance",
|
||||||
|
"sheetId": "1IU-0FzPTy5JpL8HDSB0rfVlh-_AYwEGRAn35YhUh6F4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - EEI",
|
||||||
|
"sheetId": "1Pp5tmJBDIwGnTK2qCMHBeXDL0Du6BM7njVIDH-s_5KE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Duty Package Policy",
|
||||||
|
"sheetId": "1j6aLHKLfG3JVRIxBqd8i57Q4FE_ZMoUbrUEGTC5vnKU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Drone Insurance",
|
||||||
|
"sheetId": "1lyq2oZqEDDncdFzNwYB5hJFrn4cO7AlOULbkNnJBZDw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Custom Duty",
|
||||||
|
"sheetId": "1YbfmBAHknv-GL8Ng0za7AjIW1mRJhfbABpOmeijDqws"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Commercial Crime Policy",
|
||||||
|
"sheetId": "1CW9mbxomw16TLwtioypbn5pxIdDxWhJyNIYBNMNmHX0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Cattle Insurance Policy",
|
||||||
|
"sheetId": "1sb0zzEV_jTZoevVzVTXwkUZU2FXwVw4hC99cwwYjtRY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Charters Liability Insurance Policy",
|
||||||
|
"sheetId": "1Z1b4h_SqUC-NNFTFePdJucRD3uIswp-dIQyVMv5fibg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Compact Insurance Policy",
|
||||||
|
"sheetId": "1IgfwJkfsIBGzvXm-RWIVW1e8QRGDIyz-pYrzdxn7xzI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Burglary",
|
||||||
|
"sheetId": "1k5DPWbH6G3vBNKyueNjV9WJ02JzjoPWcwkNHwKyDnsU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Crop Insurance",
|
||||||
|
"sheetId": "1pv66ONaiDm-Q6_DdKl4HOKX_dgEy58Zx3eFDWxcDtdA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Clinical Trials Liability Insurance Policy",
|
||||||
|
"sheetId": "10LdVu03QEBUhTrKsfhXnsJMEfaypLGXrKrOtD1t2Kds"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Bankers Indemnity Insurance Policy",
|
||||||
|
"sheetId": "1cDUKTDt6NfReGAdu_wx2BVJ0ymu1d1MVdpT8g8-eWzs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Cyber Policy",
|
||||||
|
"sheetId": "1dwRvokO1Y9JezANq-2_3jmEqxPX_p3qtqhhvUd4HsIA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Affinity Insurance Policy",
|
||||||
|
"sheetId": "1THrtbhZDrg7aAxSPqcOvHCowngg5xg8RDy1s53Z8tPk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Carriers Legal Liability Insurance Policy",
|
||||||
|
"sheetId": "10QvpiJHI96p4vPL-byzUvgiHB8rGlbUmc2IjsZmT6N0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Baggage Insurance Policy",
|
||||||
|
"sheetId": "1eFVCtWrUOB98Nl74cKXerlaw-odpotmE-KbaS9aLEPo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation Spares All Risk Insurance Policy",
|
||||||
|
"sheetId": "1gVh0Qe-ZOJKavi6XSWVbCQ2OqOA6r3wGcimIAnXQv80"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Advance Loss of Profit (ALOP) Insurance",
|
||||||
|
"sheetId": "12Gg94H4JeWc_QexrSoJB7hIhxyVg216cd5uvE3kj_8Y"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Agriculture Pumpset Insurance Policy",
|
||||||
|
"sheetId": "1HwaJQqBLHvo9THSNPkHnZEDSPlJoO-c9ob4u0ImKrds"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation Airline Hull Spares Liability Insurance Policy",
|
||||||
|
"sheetId": "1rZvQJtOudmEwnyRZ_gHK0Yo57qXhYnjCPblpurD3PPs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation Liability Insurance Policy",
|
||||||
|
"sheetId": "11OcS0uBo8I881T9DmxNE96s5W_30ktFQ2JB4l9b5bg8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation Airline Hull War Excess Liability Insurance Policy",
|
||||||
|
"sheetId": "1NHnpzhRCkw09oGcZnDXbg_C6WPQAxsoPxqnJanZgSrQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FAR format(2)",
|
||||||
|
"sheetId": "1uHtzIna1z2FazkRNrj1Zh7qnt0LXYJko0BLO_jBBEwI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation Hull Deductible Insurance Policy",
|
||||||
|
"sheetId": "1jJhP02u_tGYVzHwv9Xz8M4blf9031KMqsx6D5QWtBTk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Aviation - Personal Accident",
|
||||||
|
"sheetId": "1ytviSYcnf2GjJM-lmrGGDVFxMx0Wx9bwACap4FHVw3Y"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RFQ - Art & Valuables",
|
||||||
|
"sheetId": "1NFTDv4hGAKXEXyk3bnOjOVu4p1EVWp0RsGdSPylgPdk"
|
||||||
|
}
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue
Block a user