MERGE_TEST_HR_BRANCH_TOKEN&MINOR_ISSUES

This commit is contained in:
Ubuntu 2026-02-26 17:37:32 +05:30
commit 2a6ecc0469
40 changed files with 2893 additions and 2652 deletions

View File

@ -30,6 +30,10 @@ class Acl
'#^/util/download_log#' => ['roles' => [ADMIN_ROLE_ID]],
'#^/metaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
// ===================== PUBLIC DOWNLOADS / FORMS =====================
'#^/download-#' => ['public' => true],

View File

@ -115,4 +115,19 @@ define('ACCOUNT_MANAGER_ROLE_ID', 3);
define('STAFF_ROLE_ID', 4);
define('HEAD_ROLE_ID', 5);
/**
* @Upload Allowed Extensions by Business Context
*/
define('UPLOAD_ALLOWED_EXTENSIONS', [
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg',
'pdf', 'doc', 'docx', 'odt', 'rtf',
'xls', 'xlsx', 'ods', 'csv', 'txt',
]);
define('UPLOAD_EXT_IMAGES', ['jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_KYC_DOCS', ['pdf', 'jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_CLAIM_DOCS', ['pdf', 'jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_POLICY_DOCS', ['pdf', 'jpg', 'jpeg', 'png', 'xls', 'xlsx']);
define('UPLOAD_EXT_LEAD_FILES', ['xls', 'xlsx', 'pdf', 'jpg', 'jpeg', 'png']);
define('UPLOAD_EXT_EXCEL', ['xls', 'xlsx', 'ods', 'csv']);
define('UPLOAD_EXT_MAIL_ATTACHMENTS', ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx']);

View File

@ -11,10 +11,11 @@ $routes->options('(:any)', function() {
});
$routes->group('sheet', function ($routes) {
$routes->get('(:any)', 'GoogleSheetController::editor/$1');
$routes->get('(:any)/fetch', 'GoogleSheetController::fetch/$1');
$routes->post('(:any)/save', 'GoogleSheetController::save/$1');
$routes->get('(:any)/download', 'GoogleSheetController::download/$1');
$routes->post('create', 'GoogleSheetController::create');
$routes->get('(:segment)/fetch', 'GoogleSheetController::fetch/$1');
$routes->post('(:segment)/save', 'GoogleSheetController::save/$1');
$routes->get('(:segment)/download', 'GoogleSheetController::download/$1');
$routes->get('(:segment)', 'GoogleSheetController::editor/$1');
});
@ -912,7 +913,7 @@ $routes->group('sales', function($routes) {
$routes->get('/', 'SalesController::index');
$routes->get('activities', 'SalesController::activities');
$routes->get('loadactivities', 'SalesController::loadactivities');
$routes->get('page/(:segment)', 'SalesController::noPage/$1');
@ -996,6 +997,7 @@ $routes->group('sales', function($routes) {
//Dashboard
$routes->get('dashboard', 'SalesController::dashboard');
$routes->get('branchLevelDashboard', 'SalesController::branchLevelDashboard');
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
});

View File

@ -101,23 +101,22 @@ class AppContentManagementController extends AdminController
$file = $this->request->getFile('advertise_image');
$client_id = $sanitized_post_data['client_id'] ?? null;
//1) original file name for vaildations
$fileName = $file->getClientName(); //original file name for vaildations
$existing = $this->addImgModel->where('name', $fileName)->where('client_id', $fileName)->where('is_active', 1)->first();
if($existing){ return $this->respond(['status' => false, 'message' => 'This file has already been uploaded in active state.'], 400); }
//skip 1) and use this
// $fileName = $file->getRandomName(); // Same Name Multiple time upload means different name
if (!$file || !$file->isValid()) {
return $this->respond(['status' => false, 'message' => 'No file uploaded or invalid file.'], 400);
}
// $uploadPath = WRITEPATH . 'uploads/advertiseImage/';
if (!validate_upload_extension($file, UPLOAD_EXT_IMAGES)) {
return $this->respond(['status' => false, 'message' => 'Only JPG, JPEG, PNG files are allowed.'], 400);
}
$fileName = sanitize_upload_filename($file->getClientName());
$existing = $this->addImgModel->where('name', $fileName)->where('client_id', $client_id)->where('is_active', 1)->first();
if($existing){ return $this->respond(['status' => false, 'message' => 'This file has already been uploaded in active state.'], 400); }
$uploadPath = ROOTPATH . 'public/uploads/add_image_upload/';
if (!is_dir($uploadPath)) mkdir($uploadPath, 0755, true);
$file->move($uploadPath, $fileName);
$id = $sanitized_post_data['add_image_id'] ?? null;

View File

@ -26,6 +26,13 @@ class ClaimsUploadController extends BaseController
], 400);
}
if (!validate_upload_extension($file, UPLOAD_EXT_EXCEL)) {
return $this->respond([
'status' => 'failed',
'message' => 'Only Excel files (xls, xlsx, ods, csv) are allowed.'
], 400);
}
$data = [
'client_id' => $this->request->getPost('client_id'),
'tpa_id' => $this->request->getPost('tpa_id'),

View File

@ -1051,7 +1051,7 @@ class ClientController extends AdminController
$uploadFilePath = ROOTPATH . 'public/uploads/logo/';
$file_name = file_Upload($this->request->getFile('client_logo'), $uploadFilePath);
$file_name = file_Upload($this->request->getFile('client_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES);
$data = $this->request->getPost();
$sanitized_post_data = sanitizeInputArrayAdvanced($data);
$sanitized_post_data['created_by'] = get_session_userid();
@ -1149,7 +1149,7 @@ class ClientController extends AdminController
$this->myLogger->logme('error', 'edit client general info function called');
$uploadFilePath = ROOTPATH . 'public/uploads/logo/';
$file_name = file_Upload($this->request->getFile('client_logo'), $uploadFilePath);
$file_name = file_Upload($this->request->getFile('client_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES);
$id = $this->request->getPost('PrimaryKey');
$data = $this->request->getPost();
@ -1191,7 +1191,7 @@ class ClientController extends AdminController
// print_r($data); die;
unset($data['file_name']);
$uploadFilePath = WRITEPATH . 'uploads/client_kyc_documents';
$File = file_Upload($this->request->getFile('file_name'), $uploadFilePath);
$File = file_Upload($this->request->getFile('file_name'), $uploadFilePath, UPLOAD_EXT_KYC_DOCS);
if (!empty($File)) {
$data['file_name'] = $File;
@ -1257,7 +1257,7 @@ class ClientController extends AdminController
$file = $this->request->getFile('file_name');
$fileName = file_Upload($file, $uploadFilePath);
$fileName = file_Upload($file, $uploadFilePath, UPLOAD_EXT_KYC_DOCS);
if (!empty($fileName)) {
$sanitized_data['file_name'] = $fileName;
@ -1313,7 +1313,7 @@ class ClientController extends AdminController
$file = $this->request->getFile('file_name');
$fileName = file_Upload($file, $uploadFilePath);
$fileName = file_Upload($file, $uploadFilePath, UPLOAD_EXT_KYC_DOCS);
if (!empty($fileName)) {
$sanitized_data['file_name'] = $fileName;
@ -1400,7 +1400,7 @@ class ClientController extends AdminController
$file = $this->request->getFile('file_name');
$uploadFilePath = WRITEPATH . 'uploads/client_kyc_documents';
$fileName = file_Upload($file, $uploadFilePath);
$fileName = file_Upload($file, $uploadFilePath, UPLOAD_EXT_KYC_DOCS);
if (!empty($fileName)) {
$sanitized_data['file_name'] = $fileName;
@ -1472,7 +1472,7 @@ class ClientController extends AdminController
if ($uploadedFile && $uploadedFile->isValid() && !$uploadedFile->hasMoved()) {
$new_file_name = file_Upload($uploadedFile, $uploadFilePath);
$new_file_name = file_Upload($uploadedFile, $uploadFilePath, UPLOAD_EXT_KYC_DOCS);
if (!$new_file_name) {
return $this->respond(['status' => false, 'code' => 500, 'message' => 'New file upload failed on server.'], 200);
}
@ -3479,7 +3479,7 @@ class ClientController extends AdminController
if (!empty($docName) && $file->isValid() && !$file->hasMoved()) {
// Upload the file
$uploadedFileName = file_Upload_for_lead($file, $uploadFilePath);
$uploadedFileName = file_Upload_for_lead($file, $uploadFilePath, UPLOAD_EXT_KYC_DOCS);
if ($uploadedFileName) {
// Prepare data for each document upload

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,35 @@ use CodeIgniter\Controller;
class GoogleSheetController extends Controller
{
protected GoogleSheetLib $sheetLib;
public $config = [
'permissions' => [
'editors' => [
'vitvelz@gmail.com', 'velz1990@gmail.com','venkateshraman786@gmail.com'
// 'group:rfq-editors@company.com'
],
'viewers' => [],
],
'protections' => [
[
'range' => 'RFQ Page!B12:C12',
'users' => [ //allowed users to edit this cell range
'velz1990@gmail.com','vitvelz@gmail.com','firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com'
],
'groups' => []
],
[
'range' => 'Claims Page!A1',
'users' => [
'velz1990@gmail.com','venkateshraman786@gmail.com','firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com'
],
'groups' => []
]
],
'parentFolderId' => '19uySI-PSFQfFpZvMSnBtirXbwfCTXFBL', // RFQ folder
];
public function __construct()
{
@ -23,10 +52,16 @@ class GoogleSheetController extends Controller
/* ---------- FETCH ---------- */
public function fetch(string $sheetId)
{
// $data = $this->sheetLib->read($sheetId);
$data = $this->sheetLib->read($sheetId);
// print_rr($data);die;
// return $this->response->setJSON($data);
echo 'Hi';
return $this->response->setJSON($data);
// echo 'Hi';
}
public function sample(string $sheetId)
{
echo $sheetId;
}
/* ---------- SAVE ---------- */
@ -63,4 +98,39 @@ class GoogleSheetController extends Controller
)
->setBody($content);
}
public function create()
{
try {
$config = config('RfqConfig');
// #$lib = new RfqGoogleSheetLib();
// $templateId = $this->request->getPost('templateId');
// if (!$templateId) {
// throw new \Exception('Template ID missing');
// }
// $newSheetId = $this->sheetLib->copyTemplate(
// $templateId,
// 'RFQ_' . date('Ymd_His'),
// $this->config['parentFolderId']
// );
$newSheetId = '1GXNDNXoWriClb5HCqPYd2GAY1T8aie_Hos0yAOoTaC0';
// $this->sheetLib->applyPermissions($newSheetId, $this->config['permissions']);
$this->sheetLib->applyProtections($newSheetId, $this->config['protections']);
return $this->response->setJSON([
'status' => 'success',
'url' => $this->sheetLib->sheetUrl($newSheetId)
]);
} catch (\Throwable $e) {
return $this->response
->setStatusCode(500)
->setJSON([
'status' => 'error',
'message' => $e->getMessage()
]);
}
}
}

View File

@ -302,8 +302,8 @@ class LeadsController extends BaseController
public function viewLeadsList()
{
$data['tab_name'] = 'Leads';
$data['page_name'] = 'Leads';
$data['tab_name'] = 'Opportunities';
$data['page_name'] = 'Opportunities';
// Set basic data
$data['issuer'] = $this->issuer;
@ -372,7 +372,7 @@ class LeadsController extends BaseController
'lead_type' => [
'rules' => 'integer',
'errors' => ['required' => 'Lead Type is required']
'errors' => ['required' => 'Opportunity Type is required']
],
'issuer' => [
'rules' => 'required',
@ -825,6 +825,7 @@ class LeadsController extends BaseController
$last_3_years_claims = $data['finyear'];
$processedData[] = [
'actual_lead_id' => $data['actual_lead_id'] ?? null,
'lead_type' => $data['lead_type'],
'issuer' => $data['issuer'],
'client_type' => $data['client_type'],
@ -934,10 +935,10 @@ class LeadsController extends BaseController
}
if (count($insertCount) > 0) {
return $this->respond(['status' => true, 'lead_id' => $insert, 'message' => 'New Lead created successfully', 'data' => $data], 200);
return $this->respond(['status' => true, 'lead_id' => $insert, 'message' => 'New Opportunity created successfully', 'data' => $data], 200);
}
return $this->respond(['status' => false, 'lead_id' => $insert, 'message' => "Failed to create Lead", 'data' => $data], 200);
return $this->respond(['status' => false, 'lead_id' => $insert, 'message' => "Failed to create Opportunity", 'data' => $data], 200);
}
private function updateOldLead($id, $data)
@ -946,9 +947,9 @@ class LeadsController extends BaseController
$this->insertMultiFilesData($data[0]['multi_file_data'], $id, $data[0]['lead_form_type']);
$this->insertLeadStatus($id, $data[0]['status'], 3);
return $this->respond(['status' => true, 'lead_id' => $id, 'message' => "Lead updated successfully", 'data' => $data], 200);
return $this->respond(['status' => true, 'lead_id' => $id, 'message' => "Opportunity updated successfully", 'data' => $data], 200);
}
return $this->respond(['status' => false, 'lead_id' => $id, 'message' => "Failed to update Lead", 'data' => $data], 200);
return $this->respond(['status' => false, 'lead_id' => $id, 'message' => "Failed to update Opportunity", 'data' => $data], 200);
}
// Get the Single Lead data for edit uisng ajax (do not delete)
@ -1035,7 +1036,7 @@ class LeadsController extends BaseController
$multi_file_data = [];
foreach ($files as $index => $value) {
$file_name = file_Upload_for_lead($value, $uploadFilePath);
$file_name = file_Upload_for_lead($value, $uploadFilePath, UPLOAD_EXT_LEAD_FILES);
$multi_file_data[] = [
'file_name' => $file_name,
'docs_name' => $docs_names[$index],
@ -1305,7 +1306,7 @@ class LeadsController extends BaseController
if (!$lead_data) {
// Handle case where lead doesn't exist
throw new \Exception('Lead not found');
throw new \Exception('Opportunity not found');
}
// 4. Conditional query - only fetch if needed
@ -1494,7 +1495,7 @@ class LeadsController extends BaseController
$lead_id = $data['lead_id'] ?? null;
if (!$lead_id) {
return $this->respond(['status' => false, 'message' => 'Lead ID is required'], 400);
return $this->respond(['status' => false, 'message' => 'Opportunity ID is required'], 400);
}
$rfq_created = $this->RFQModel->where('lead_id', $lead_id)->where('is_active', 1)->countAllResults();
@ -2796,7 +2797,7 @@ class LeadsController extends BaseController
$file_name_with_path = WRITEPATH . "/uploads/lead_files/" . $lead_data['file_name'];
if (!$lead_data) {
return ['status' => 'failed', 'message' => 'Lead data not found'];
return ['status' => 'failed', 'message' => 'Opportunity data not found'];
}
try{
@ -5583,7 +5584,7 @@ class LeadsController extends BaseController
// dd($file_name_with_path);
if (!$lead_data) {
return ['status' => 'failed', 'message' => 'Lead data not found'];
return ['status' => 'failed', 'message' => 'Opportunity data not found'];
}
if ($lead_data['file_name']) {
@ -6579,7 +6580,7 @@ class LeadsController extends BaseController
return $this->respond([
'status' => false,
'code' => 400,
'message' => 'Lead ID is required'
'message' => 'Opportunity ID is required'
], 400);
}

View File

@ -367,7 +367,7 @@ class MasterController extends AdminController
}
$uploadFilePath = ROOTPATH . 'public/uploads/logo/';
$file_name = file_Upload($this->request->getFile('insurer_logo'), $uploadFilePath);
$file_name = file_Upload($this->request->getFile('insurer_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES);
$data = $this->request->getPost();
$sanitized_post_data = sanitizeInputArrayAdvanced($data);
@ -593,7 +593,7 @@ class MasterController extends AdminController
}
$uploadFilePath = ROOTPATH . 'public/uploads/logo/';
$file_name = file_Upload($this->request->getFile('insurer_logo'), $uploadFilePath);
$file_name = file_Upload($this->request->getFile('insurer_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES);
$data = $this->request->getPost();
$sanitized_post_data = sanitizeInputArrayAdvanced($data);
$id = $sanitized_post_data['PrimaryKey'];
@ -974,11 +974,11 @@ class MasterController extends AdminController
$uploadFilePath = ROOTPATH . 'public/uploads/logo/';
$file_name = file_Upload($this->request->getFile('tpa_logo'), $uploadFilePath);
$file_name = file_Upload($this->request->getFile('tpa_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES);
$template_bg_path = ROOTPATH . 'public/uploads/template_bg';
$front_card_file_name = file_Upload($this->request->getFile('fc'), $template_bg_path);
$back_card_file_name = file_Upload($this->request->getFile('bc'), $template_bg_path);
$front_card_file_name = file_Upload($this->request->getFile('fc'), $template_bg_path, UPLOAD_EXT_IMAGES);
$back_card_file_name = file_Upload($this->request->getFile('bc'), $template_bg_path, UPLOAD_EXT_IMAGES);
$eCardTemplate = $sanitized_post_data['ecard_content'];
@ -1264,11 +1264,11 @@ class MasterController extends AdminController
$uploadFilePath = ROOTPATH . 'public/uploads/logo/';
$file_name = file_Upload($this->request->getFile('tpa_logo'), $uploadFilePath);
$file_name = file_Upload($this->request->getFile('tpa_logo'), $uploadFilePath, UPLOAD_EXT_IMAGES);
$template_bg_path = ROOTPATH . 'public/uploads/template_bg';
$front_card_file_name = file_Upload($this->request->getFile('fc'), $template_bg_path);
$back_card_file_name = file_Upload($this->request->getFile('bc'), $template_bg_path);
$front_card_file_name = file_Upload($this->request->getFile('fc'), $template_bg_path, UPLOAD_EXT_IMAGES);
$back_card_file_name = file_Upload($this->request->getFile('bc'), $template_bg_path, UPLOAD_EXT_IMAGES);
$id = $sanitized_post_data['PrimaryKey'] ?? null;

View File

@ -364,7 +364,7 @@ class NotificationController extends AdminController
// Define upload path and attempt file upload
$uploadFilePath = WRITEPATH . 'uploads/attachments';
$uploadedFile = $this->request->getFile('file');
$fileName = file_Upload_for_lead($uploadedFile, $uploadFilePath); // Assume file_Upload handles file saving
$fileName = file_Upload_for_lead($uploadedFile, $uploadFilePath, UPLOAD_EXT_MAIL_ATTACHMENTS);
if ($fileName) {
// Prepare data for insertion

View File

@ -3411,7 +3411,7 @@ class PolicyTransactionController extends BaseController
if (!empty($docName) && $file->isValid() && !$file->hasMoved()) {
// Upload the file
$uploadedFileName = file_Upload_for_lead($file, $uploadFilePath);
$uploadedFileName = file_Upload_for_lead($file, $uploadFilePath, UPLOAD_EXT_POLICY_DOCS);
if ($uploadedFileName) {
// Prepare data for each document upload

View File

@ -648,7 +648,8 @@ class RestAuthenticationController extends AdminController
'function' => $th->getTrace()[0]['function'] ?? null,
'class' => $th->getTrace()[0]['class'] ?? null,
];
return $this->respond(['status' => 'failed','code' => 500,'data' => $th, 'error_data' => $errorData],500);
log_message('error', 'Error Data: ' . json_encode($errorData));
return $this->respond(['status' => 'failed','code' => 500,'data' => $th],500);
}
}
@ -2345,7 +2346,7 @@ class RestAuthenticationController extends AdminController
}
$decoded = $result['decoded'];
$userId = $decoded['id'] ?? null;
$userId = $decoded['post_hr_id'] ?? null;
if (!$userId) {
return $this->respond([

View File

@ -104,6 +104,10 @@ class RuleImportController extends AdminController
return $this->response->setJSON(['status'=>false,'message'=>'No file uploaded or upload error']);
}
if (!validate_upload_extension($file, UPLOAD_EXT_EXCEL)) {
return $this->response->setJSON(['status'=>false,'message'=>'Only Excel/CSV files (xls, xlsx, ods, csv) are allowed.']);
}
// Move uploaded file to writable temp location
$tmpPath = WRITEPATH . 'uploads/' . $file->getRandomName();
$file->move(WRITEPATH . 'uploads', $file->getName()); // keep original name inside uploads
@ -140,6 +144,10 @@ class RuleImportController extends AdminController
return $this->respond(['status' => false, 'code' => 404, 'message' => 'No file uploaded or upload error.'], 200);
}
if (!validate_upload_extension($file, UPLOAD_EXT_EXCEL)) {
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Only Excel/CSV files (xls, xlsx, ods, csv) are allowed.'], 200);
}
// ---------------------------------------------------------
// 2. Read POST fields
// ---------------------------------------------------------

View File

@ -7,6 +7,7 @@ use App\Models\SalesActualLeadModel;
use App\Models\SalesContactPersonModel;
use App\Models\SalesActivityModel;
use App\Models\SalesLeadNoteModel;
use App\Models\UserModel;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\API\ResponseTrait;
@ -18,6 +19,7 @@ class SalesController extends BaseController
protected $contactModel;
protected $activityModel;
protected $noteModel;
protected $userModel;
public function __construct()
{
@ -25,6 +27,7 @@ class SalesController extends BaseController
$this->contactModel = new SalesContactPersonModel();
$this->activityModel = new SalesActivityModel();
$this->noteModel = new SalesLeadNoteModel();
$this->userModel = new UserModel();
}
@ -33,11 +36,10 @@ class SalesController extends BaseController
$data['tab_name'] = 'Leads';
$data['page_name'] = 'Leads';
return $this->loadLayout('sales/tracker_view', $data);
}
public function activities(){
public function loadactivities(){
$data = $this->getSalesStaffData();
$data['tab_name'] = 'Activities';
@ -47,6 +49,7 @@ class SalesController extends BaseController
->orderBy('lead_id', 'DESC')
->findAll();
return $this->loadLayout('sales/activity_view', $data);
}
@ -57,41 +60,35 @@ class SalesController extends BaseController
{
$db = \Config\Database::connect();
$logged_user_id = get_session_userid();
$role = get_role_id();
$team_id = user_team();
$data = [
'users' => [],
'sales_manager_ids' => []
];
// Get the Branch ID, Role, Team, and Name of the logged-in user
$row = $db->table('user_profiles up')
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id, up.role, ut.team_id')
->join('user_teams ut', 'ut.user_id = up.id', 'left')
->where('up.is_active', 1)
->where('up.id', $logged_user_id)
->get()
->getRow();
$row = $db->table('user_profiles')->select('*')
->where('is_active', 1)->where('id', $logged_user_id)
->get()->getRow();
$nhance_branch_id = $row ? $row->nhance_branch_id : null;
$role = $row ? $row->role : null;
$team_id = $row ? $row->team_id : null;
// Is the logged-in user a Sales Manager? (Role 4, Team 5)
if ($role == 4 && $team_id == 5) {
if ($role == 4 && in_array(5, $team_id)) {
$data['sales_manager_ids'] = [$logged_user_id];
$data['users'] = [
[
'id' => $row->id,
'sales_manager' => trim($row->first_name . ' ' . $row->last_name),
'first_name' => $row->first_name,
'nhance_branch_id' => $nhance_branch_id
]
];
}
// Otherwise fetch ALL sales managers in this branch
elseif ($nhance_branch_id) {
elseif (in_array($role,[1,5])) {
$data['users'] = $db->table('user_profiles up')
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id')
@ -142,9 +139,11 @@ class SalesController extends BaseController
]);
// 2. Handle follow-up if requested
$google_calender_response = [];
if (!empty($data['schedule_followup']) && $data['schedule_followup'] === 'yes') {
$activity = $this->activityModel->find($id);
$this->activityModel->insert([
$activity_data = [
'lead_id' => $activity['lead_id'],
'activity_type' => $data['followup_type'],
'notes' => $data['followup_notes'],
@ -152,10 +151,14 @@ class SalesController extends BaseController
'assigned_to' => $activity['assigned_to'],
'status' => 'pending',
'created_by' => $this->getUserId()
]);
];
$this->activityModel->insert($activity_data);
// add google calender event
$google_calender_response = $this->addCalenderEvent($activity_data);
}
return $this->respond(['status' => 'success', 'message' => 'Activity updated']);
return $this->respond(['status' => 'success', 'message' => 'Activity updated', 'google_calender_response' => $google_calender_response]);
} catch (\Exception $e) {
return $this->failServerError($e->getMessage());
}
@ -463,7 +466,8 @@ class SalesController extends BaseController
'total' => $result['total'],
'limit' => $limit,
'offset' => $offset
]);
], 200);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
@ -543,13 +547,17 @@ class SalesController extends BaseController
return $this->fail($this->activityModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
}
// add google calender event
$response = $this->addCalenderEvent($data);
$activityId = $this->activityModel->getInsertID();
$activity = $this->activityModel->find((int)$activityId);
return $this->respondCreated([
'status' => 'success',
'message' => 'Activity created successfully',
'data' => $activity
'data' => $activity,
'google_calender_response' => $response
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
@ -765,53 +773,160 @@ class SalesController extends BaseController
// ==================== Dashboard ====================
public function dashboard(){
public function branchLevelDashboard()
$logged_user_id = get_session_userid();
$role = get_role_id();
$team_id = user_team();
$payload = $this->request->getGet();
$db = \Config\Database::connect();
$row = $db->table('user_profiles')
->select('*')
->where('is_active', 1)
->where('id', $logged_user_id)
->get()
->getRowArray();
$nhance_branch_id = $row ? $row['nhance_branch_id'] : null;
// dd($logged_user_id, $nhance_branch_id, $role, $team_id );
if (in_array($role,[1,5])) {
$sales_manager_ids = array_column(
$db->table('user_profiles up')
->select('up.id')
->join('user_teams ut', 'ut.user_id = up.id')
->where([
'up.is_active' => 1,
'ut.is_active' => 1,
'up.role' => 4,
'ut.team_id' => 5,
'up.nhance_branch_id' => $nhance_branch_id
])
->get()
->getResultArray(),
'id'
);
$this->branchLevelDashboard($nhance_branch_id,$sales_manager_ids);
}
elseif ($role == 4 && in_array(5, $team_id)) {
$sales_manager_ids = [$logged_user_id];
$this->salesManagerLevelDashboard($logged_user_id,$sales_manager_ids, $payload);
}
}
public function branchLevelDashboard($branchId,$sales_manager_ids)
{
// Hardcoded branch ID as requested
$branchId = 1;
// $branchId = 1;
try {
$sales_manager_ids = array_values(array_map('intval', $sales_manager_ids));
// Final safe check
if (empty($sales_manager_ids)) {
// No valid IDs — skip queries or return empty
$total_leads = 0;
$total_activity = 0;
$total_completed_activity = 0;
$total_pending_activity = 0;
$pending_activities = [];
$recent_activities = [];
$teamPerformance = [];
$leadsOverview = [];
} else {
if (empty($sales_manager_ids) || !is_array($sales_manager_ids)) {
$sales_manager_ids = array_filter((array) $sales_manager_ids); // removes null, "", 0
}
// 1. Lead Statistics
$stats = $this->leadModel->getLeadStats(); // Using existing model method
$total_leads = $this->leadModel->whereIn('assigned_to', $sales_manager_ids)->countAllResults(); // Use countAllResults, NOT countAll
// echo $this->leadModel->getLastQuery();die();
// 2. Activity Statistics
$activityStats = [
'total' => $this->activityModel->countAllResults(),
'completed' => $this->activityModel->where('status', 'completed')->countAllResults(),
'pending' => $this->activityModel->where('status', 'pending')->countAllResults(),
];
// 2. Total activity
$total_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->countAllResults();
// 3. Completed activity
$total_completed_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->where('status', 'completed')->countAllResults();
// 4. Pending activity
$total_pending_activity = $this->activityModel->whereIn('assigned_to', $sales_manager_ids)->where('status', 'pending')->countAllResults();
// 3. Team Performance (Aggregating activity counts per user)
$db = \Config\Database::connect();
// 5. Team Performance
$teamPerformance = $db->table('user_profiles as u')
->select('u.first_name, u.last_name, u.profile as role,
->select('u.first_name, u.last_name, r.role,
(SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id) as total_acts,
(SELECT COUNT(*) FROM sales_activities WHERE assigned_to = u.id AND status = "completed") as done_acts')
->join('roles r', 'r.id = u.role')
->where('u.nhance_branch_id', $branchId)
->whereIn('u.id', $sales_manager_ids)
->where('u.is_active', 1)
->get()->getResultArray();
// 4. Recent Activities (Joining for Lead Names)
$recentActivities = $this->activityModel->select('sales_activities.*, sales_actual_leads.company_name')
// 6. Recent Activities (Joining for Lead Names)
$recent_activities = $this->activityModel->select('sales_activities.*, sales_actual_leads.company_name')
->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id')
->orderBy('sales_activities.scheduled_date', 'DESC')
->limit(6)
->findAll();
// 5. All Leads Overview
$leadsOverview = $this->leadModel->select('sales_actual_leads.*, user_profiles.first_name, user_profiles.last_name')
->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left')
->findAll();
// 7. Pending Activities (List)
$pending_activities = $db->table('sales_activities sa')
->select('sa.activity_id,sa.lead_id,sa.activity_type,sa.scheduled_date,sa.status,sa.assigned_to,sal.company_name,up.first_name AS assigned_to_name,sa.notes')
->join('user_profiles up', 'up.id = sa.assigned_to', 'left')
->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left') // ✅ ADD THIS
->orderBy('sa.scheduled_date', 'DESC')
->whereIn('sa.assigned_to', $sales_manager_ids)
->where('sa.status', 'pending')
->get()->getResultArray();
// 8. All Leads Overview
$leadsOverview = $db->table('sales_actual_leads sal')
->select('sal.lead_id,sal.company_name,sal.status,up.first_name AS assigned_to,
COUNT(DISTINCT sa.activity_id) AS activities,
COUNT(DISTINCT l.id) AS opportunities
')
->join('user_profiles up', 'up.id = sal.assigned_to', 'left')
->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left')
->join('leads l', 'l.actual_lead_id = sal.lead_id', 'left')
->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name')
// ->having('COUNT(DISTINCT sa.activity_id) + COUNT(DISTINCT l.id) >', 0) // ← this line
->orderBy('sal.created_at', 'DESC')
->whereIn('sal.assigned_to', $sales_manager_ids)
->get()
->getResultArray();
// 9. Activity BrakDown
$activityBreakdown = $db->table('sales_activities')
->select("activity_type, COUNT(*) AS total, ROUND(COUNT(*) * 100.0 / {$total_activity}, 0) AS percentage", false)
->whereIn('assigned_to', $sales_manager_ids)
->groupBy('activity_type')
->orderBy('total', 'DESC')
->get()
->getResultArray();
}
$data = [
'total_leads' => $stats['total'],
'total_activities' => $activityStats['total'],
'completed_acts' => $activityStats['completed'],
'total_leads' => $total_leads,
'total_acts' => $total_activity,
'total_completed_acts' => $total_completed_activity,
'total_pending_acts'=> $total_pending_activity,
'pipeline_value' => '15.0L', // Hardcoded placeholder from PDF [cite: 14]
'team' => $teamPerformance,
'recent_acts' => $recentActivities,
'leads_overview' => $leadsOverview
'recent_acts' => $recent_activities,
'pending_acts' => $pending_activities,
'leads_overview' => $leadsOverview,
'activity_breakdown'=> $activityBreakdown,
'tab_name' => "Sales Dashboard",
'page_name' => "Sales Dashboard"
];
// dd($data);
@ -825,16 +940,15 @@ class SalesController extends BaseController
}
}
public function salesManagerLevelDashboard()
public function salesManagerLevelDashboard($userId,$sales_manager_ids, $payload = [])
{
$userId = get_session_userid();
// $userId = get_session_userid();
// $userId = 1;
$db = \Config\Database::connect();
try {
$payload = $this->request->getGet();
// $payload = $this->request->getGet();
$current_fin_year = $payload['fy'] ?? getCurrentFinancialYear();
@ -844,8 +958,12 @@ class SalesController extends BaseController
->orderBy('fy_year', 'desc')
->get()
->getResultArray();
$fin_years = array_column($fin_years, 'fy_year');
$fin_years[] = '2024-2025';
if(empty($fin_years)){
$fin_years[] = $current_fin_year;
}
$target = $db->table('sales_target')
->where('user_id', $userId)
@ -893,7 +1011,9 @@ class SalesController extends BaseController
'recent_leads' => $recentLeads,
'fin_years' => $fin_years,
'display_fin_years' => format_financial_year($current_fin_year),
'user_name' => get_session_userdata()->first_namee ?? ''
'user_name' => get_session_userdata()->first_namee ?? '',
'tab_name' => "Sales Dashboard",
'page_name' => "Sales Dashboard"
];
// dd($data);
@ -928,4 +1048,84 @@ class SalesController extends BaseController
return $achievedAmountData[0]['achieved_amount'] ?? 0.00;
}
public function addCalenderEvent($input)
{
try {
log_message('error', '[GOOGLE_CALENDER] Calendar Event Input: ' . json_encode($input));
/* ---------------- VALIDATION ---------------- */
if (empty($input['lead_id']) || empty($input['assigned_to']) || empty($input['scheduled_date'])) {
log_message('error', '[GOOGLE_CALENDER] Calendar Event Missing Required Data');
return [
'status' => false,
'message' => 'Required data missing'
];
}
/* ---------------- FETCH DATA ---------------- */
$lead_data = $this->leadModel
->where('lead_id', $input['lead_id'])
->first();
if (!$lead_data) {
log_message('error', '[GOOGLE_CALENDER] Lead Not Found: ' . $input['lead_id']);
return [
'status' => false,
'message' => 'Lead not found'
];
}
$user_data = $this->userModel
->where('id', $input['assigned_to'])
->first();
if (!$user_data) {
log_message('error', '[GOOGLE_CALENDER] User Not Found: ' . $input['assigned_to']);
return [
'status' => false,
'message' => 'Assigned user not found'
];
}
/* ---------------- SUMMARY ---------------- */
$summary = ucfirst($input['activity_type']) .
' with ' .
$lead_data['company_name'];
/* ---------------- GOOGLE PAYLOAD ---------------- */
$eventData = [
'summary' => $summary,
'meeting_date' => $input['scheduled_date'],
'description' => $input['notes'] ?? '',
'emails' => [$user_data['email']],
];
log_message('error', '[GOOGLE_CALENDER] Google Calendar Payload: ' . json_encode($eventData));
/* ---------------- API CALL ---------------- */
$response = add_google_calender_event($eventData);
log_message('error', '[GOOGLE_CALENDER] Google Calendar Response: ' . json_encode($response));
return $response;
} catch (\Throwable $e) {
log_message('error', '[GOOGLE_CALENDER] Calendar Event Exception: ' . $e->getMessage());
log_message('error', '[GOOGLE_CALENDER] Calendar Event Trace: ' . $e->getTraceAsString());
return [
'status' => false,
'message' => 'Calendar event failed',
'error' => $e->getMessage()
];
}
}
}

View File

@ -1002,7 +1002,7 @@ class TestingController extends BaseController
// 'dashboard' => 1
'dashboard' => $database_id
],
'exp' => time() + (10 * 60) // 10 minutes
'exp' => time() + (10 * 60), // 10 minutes
];
if(!empty($policy_id)){

View File

@ -3237,7 +3237,7 @@ class TicketController extends BaseController
$file_data = [];
if(isset($get_file_data) && !empty($get_file_data)){
$file_path = WRITEPATH . 'uploads/claim_files/';
$file_data = multi_file_Upload($get_file_data, $file_path, $get_docs_name);
$file_data = multi_file_Upload($get_file_data, $file_path, $get_docs_name, UPLOAD_EXT_CLAIM_DOCS);
}
if(!empty($file_data)){
@ -3932,7 +3932,7 @@ class TicketController extends BaseController
if ($is_moved) {
$file_path = WRITEPATH.'uploads/claims_mis';
$filename = file_Upload_for_lead($file, $file_path);
$filename = file_Upload_for_lead($file, $file_path, UPLOAD_EXT_EXCEL);
$fileSize = $file->getSize(); // File size in bytes
$fileSize = $fileSize / (1024 * 1024); // Convert to MB
@ -3983,7 +3983,7 @@ class TicketController extends BaseController
}
$file_path = WRITEPATH.'uploads/claims_mis';
$file_name = file_Upload_for_lead($file, $file_path);
$file_name = file_Upload_for_lead($file, $file_path, UPLOAD_EXT_EXCEL);
if(!empty($file_name)){
$data['file_name'] = $file_name;

View File

@ -1010,7 +1010,14 @@ class UserController extends AdminController
if ($file && $file->isValid() && !$file->hasMoved()) {
$originalName = $file->getClientName();
if (!validate_upload_extension($file, UPLOAD_EXT_EXCEL)) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Only Excel files (xls, xlsx, ods, csv) are allowed.',
])->setStatusCode(400);
}
$originalName = sanitize_upload_filename($file->getClientName());
$extension = $file->getExtension();
$fileName = pathinfo($originalName, PATHINFO_FILENAME) . '_' . date('Ymd_His') . '.' . $extension;

View File

@ -115,9 +115,19 @@ class AuthJWT implements FilterInterface
}
// Refresh sliding expiration
$model->update($id, [
'token_time_out' => time() + getenv('TOKENTIMEOUT')
]);
$newExpiry = time() + getenv('TOKENTIMEOUT');
if (!isset($decoded['emp_code'])) {
// HR user: refresh token_time_out across all branches (same mobile/email)
// so switching branches doesn't cause an unexpected expiry
$field = !empty($user['mobile']) ? 'mobile' : 'email';
$model->where($field, $user[$field])
->where('contact_type', 'client')
->where('is_active', 1)
->update(null, ['token_time_out' => $newExpiry]);
} else {
$model->update($id, ['token_time_out' => $newExpiry]);
}
return true;
}

View File

@ -136,7 +136,7 @@ class JWTToken
$token = $jwtParts[0];
try {
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
return json_encode(['status' => true, 'message' => 'Token is valid', 'data' =>$decoded->data->id ]);
} catch (ExpiredException $e) {
return json_encode(['status' => false, 'message' => 'Token has expired']);
@ -158,7 +158,7 @@ class JWTToken
$jwtParts = explode(' ', $jwt);
$token = $jwtParts[1];
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
return $decoded->id;
}
@ -169,7 +169,7 @@ class JWTToken
$jwtParts = explode(' ', $jwt);
$token = $jwtParts[1];
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
return $decoded->role;
}
@ -181,7 +181,7 @@ class JWTToken
$jwtParts = explode(' ', $jwt);
$token = $jwtParts[1];
$decoded = JWT::decode($token, new Key("secret", 'HS512'));
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS512'));
return $decoded;
}

View File

@ -1,16 +1,14 @@
<?php
use App\Models\ClientModel;
use App\Models\UserTeamsModel;
use App\Models\FileModel;
use App\Models\BatchFileModelFileModel;
use App\Controllers\ApiServiceController;
use App\Controllers\GoogleDriveController;
use App\Models\BatchFileModel;
use App\Controllers\ApiServiceController;
use App\Models\FileModel;
use App\Models\TicketMasterModel;
use App\Models\UserTeamsModel;
// File: app/Helpers/Uuid_helper.php
if (!function_exists('generate_uuid')) {
if (! function_exists('generate_uuid')) {
function generate_uuid($version = 4, $format = 'hex')
{
$data = random_bytes(16);
@ -26,7 +24,7 @@ if (!function_exists('generate_uuid')) {
}
}
if (!function_exists('change_date_format2')) {
if (! function_exists('change_date_format2')) {
function change_date_format2($data, $source_format, $output_format)
{
@ -53,13 +51,47 @@ if (!function_exists('change_date_format2')) {
}
}
if (!function_exists('file_Upload')) {
function file_Upload($fileToUpload, $filepath)
if (! function_exists('sanitize_upload_filename')) {
function sanitize_upload_filename(string $fileName): string
{
if ($fileToUpload !== null && $fileToUpload->isValid() && !$fileToUpload->hasMoved()) {
$fileToUpload->move($filepath);
$fileName = $fileToUpload->getName();
$fileName = preg_replace('/[\s\x{00A0}\x{200B}-\x{200D}\x{FEFF}]/u', '', $fileName);
$fileName = preg_replace('/[\x00-\x1F\x7F]/u', '', $fileName);
$fileName = preg_replace('/[\x{00A0}\x{200B}-\x{200D}\x{FEFF}\x{00AD}\x{2060}\x{180E}\x{2028}\x{2029}]/u', '', $fileName);
$fileName = preg_replace('/[\/\\\\:*?"<>|;`${}()\'&!#]/', '', $fileName);
$fileName = preg_replace('/\.{2,}/', '.', $fileName);
$fileName = trim($fileName, ". \t\n\r");
if ($fileName === '' || strlen($fileName) > 200) {
$fileName = time() . '_' . bin2hex(random_bytes(8));
}
return $fileName;
}
}
if (! function_exists('validate_upload_extension')) {
function validate_upload_extension($file, array $allowedExtensions = UPLOAD_ALLOWED_EXTENSIONS): bool
{
$ext = strtolower($file->getClientExtension());
if (! in_array($ext, $allowedExtensions, true)) {
log_message('critical', '[UPLOAD_HELPER] Blocked extension: {ext} | File: {name}', [
'ext' => $ext,
'name' => $file->getClientName(),
]);
return false;
}
return true;
}
}
if (! function_exists('file_Upload')) {
function file_Upload($fileToUpload, $filepath, array $allowedExtensions = UPLOAD_ALLOWED_EXTENSIONS)
{
if ($fileToUpload !== null && $fileToUpload->isValid() && ! $fileToUpload->hasMoved()) {
if (! validate_upload_extension($fileToUpload, $allowedExtensions)) {
return "";
}
$fileName = sanitize_upload_filename($fileToUpload->getName());
$fileToUpload->move($filepath, $fileName);
return $fileName;
} else {
return "";
@ -67,12 +99,15 @@ if (!function_exists('file_Upload')) {
}
}
if (!function_exists('file_Upload_for_lead')) {
function file_Upload_for_lead($fileToUpload, $filepath)
if (! function_exists('file_Upload_for_lead')) {
function file_Upload_for_lead($fileToUpload, $filepath, array $allowedExtensions = UPLOAD_ALLOWED_EXTENSIONS)
{
if ($fileToUpload !== null && $fileToUpload->isValid() && !$fileToUpload->hasMoved()) {
$fileToUpload->move($filepath);
$fileName = $fileToUpload->getName();
if ($fileToUpload !== null && $fileToUpload->isValid() && ! $fileToUpload->hasMoved()) {
if (! validate_upload_extension($fileToUpload, $allowedExtensions)) {
return "";
}
$fileName = sanitize_upload_filename($fileToUpload->getName());
$fileToUpload->move($filepath, $fileName);
return $fileName;
} else {
return "";
@ -115,8 +150,8 @@ if (!function_exists('file_Upload_for_lead')) {
// }
// function for only using in the Flutter Claim File Upload
if (!function_exists('multi_file_Upload')) {
function multi_file_Upload($fileToUpload, $filepath, $docs_name = [])
if (! function_exists('multi_file_Upload')) {
function multi_file_Upload($fileToUpload, $filepath, $docs_name = [], array $allowedExtensions = UPLOAD_ALLOWED_EXTENSIONS)
{
$uploadedFiles = [];
@ -127,26 +162,32 @@ if (!function_exists('multi_file_Upload')) {
// If file is itself an array (multiple under same input name)
if (is_array($file)) {
foreach ($file as $index => $f) {
if ($f !== null && $f->isValid() && !$f->hasMoved()) {
$fileRandomName = $f->getRandomName(); // safer unique name
$fileName = $f->getName();
if ($f !== null && $f->isValid() && ! $f->hasMoved()) {
if (! validate_upload_extension($f, $allowedExtensions)) {
continue;
}
$fileRandomName = $f->getRandomName();
$fileName = sanitize_upload_filename($f->getName());
$f->move($filepath, $fileRandomName);
$uploadedFiles[] = [
'file_name' => $fileName,
'doc_name' => $docs_name[$index] ?? $fileName,
// 'file_path' => $filepath . $fileRandomName
'file_path' => $fileRandomName
'file_path' => $fileRandomName,
];
}
}
} else {
// Single file
if ($file !== null && $file->isValid() && !$file->hasMoved()) {
$fileName = $file->getRandomName();
$file->move($filepath, $fileName);
if ($file !== null && $file->isValid() && ! $file->hasMoved()) {
if (! validate_upload_extension($file, $allowedExtensions)) {
continue;
}
$fileName = sanitize_upload_filename($file->getName());
$diskName = $file->getRandomName();
$file->move($filepath, $diskName);
$uploadedFiles[] = [
'file_name' => $fileName,
'file_path' => $filepath . $fileName
'file_path' => $filepath . $diskName,
];
}
}
@ -156,8 +197,7 @@ if (!function_exists('multi_file_Upload')) {
}
}
if (!function_exists('file_unlink')) {
if (! function_exists('file_unlink')) {
function file_unlink($filepath)
{
if (is_file($filepath) && file_exists($filepath)) {
@ -166,7 +206,7 @@ if (!function_exists('file_unlink')) {
}
}
if (!function_exists('compressImage')) {
if (! function_exists('compressImage')) {
function compressImage($file, $destinationPath, $newWidth = 100, $newHeight = 100)
{
// Load the image manipulation library
@ -181,8 +221,9 @@ if (!function_exists('compressImage')) {
}
}
if (!function_exists('fancy_date_time_format')) {
function fancy_date_time_format($datetime,$return_type = 'fancy') {
if (! function_exists('fancy_date_time_format')) {
function fancy_date_time_format($datetime, $return_type = 'fancy')
{
date_default_timezone_set('Asia/Kolkata');
$currentDateTime = new DateTime();
@ -215,7 +256,7 @@ if (!function_exists('fancy_date_time_format')) {
}
if(!function_exists('check_string_date')){
if (! function_exists('check_string_date')) {
function check_string_date($str)
{
if (DateTime::createFromFormat('Y-m-d H:i:s', $str) !== false) {
@ -225,9 +266,10 @@ if(!function_exists('check_string_date')){
}
}
if (!function_exists('generate_download_link')) {
if (! function_exists('generate_download_link')) {
function generate_download_link($rand_string) {
function generate_download_link($rand_string)
{
// Generate the link using provided emp_code and client_policy_id
$link = htmlspecialchars(base_url('download-e-card/' . $rand_string));
@ -238,8 +280,9 @@ if (!function_exists('generate_download_link')) {
}
}
if (!function_exists('generate_random_alphanumeric')) {
function generate_random_alphanumeric($length = 12) {
if (! function_exists('generate_random_alphanumeric')) {
function generate_random_alphanumeric($length = 12)
{
$random_string = '';
for ($i = 0; $i < $length; $i++) {
$random_ascii = rand(0, 61);
@ -256,7 +299,7 @@ if (!function_exists('generate_random_alphanumeric')) {
}
}
if (!function_exists('get_base64_image')) {
if (! function_exists('get_base64_image')) {
function get_base64_image($path)
{
@ -273,8 +316,9 @@ if (!function_exists('get_base64_image')) {
}
}
if (!function_exists('format_indian_number')) {
function format_indian_number($number) {
if (! function_exists('format_indian_number')) {
function format_indian_number($number)
{
// Round the number to two decimal places
$number = isset($number) ? $number : 0;
$number = round($number, 2);
@ -306,8 +350,9 @@ if (!function_exists('format_indian_number')) {
}
}
if (!function_exists('get_username')) {
function get_username($user_id) {
if (! function_exists('get_username')) {
function get_username($user_id)
{
// Connect to the database
$db = \Config\Database::connect();
@ -325,16 +370,18 @@ if (!function_exists('get_username')) {
}
}
if (!function_exists('get_role_id')) {
function get_role_id() {
if (! function_exists('get_role_id')) {
function get_role_id()
{
$role_id = isset(get_session_userdata()->role) ? get_session_userdata()->role : null;
return $role_id;
}
}
if (!function_exists('teams')) {
function teams() {
if (! function_exists('teams')) {
function teams()
{
// Load the UserTeamsModel
$teamModel = new \App\Models\UserTeamsModel();
// Get the user ID from the session
@ -348,7 +395,7 @@ if (!function_exists('teams')) {
// log_message('info', 'User Teams: ' . json_encode($user_teams)); // Optionally log it
// Check if the result is not empty
if (!empty($user_teams)) {
if (! empty($user_teams)) {
// Extract only the 'team_id' values
$team_ids = array_column($user_teams, 'team_id');
return $team_ids; // Return array of team IDs
@ -358,10 +405,9 @@ if (!function_exists('teams')) {
}
}
if (!function_exists('generate_client_code')) {
function generate_client_code($string = 'GC') {
if (! function_exists('generate_client_code')) {
function generate_client_code($string = 'GC')
{
$clientModel = new \App\Models\ClientModel();
@ -378,8 +424,9 @@ if (!function_exists('generate_client_code')) {
}
}
if (!function_exists('generate_tsi_code')) {
function generate_tsi_code($type) {
if (! function_exists('generate_tsi_code')) {
function generate_tsi_code($type)
{
$PolicyTransactionModel = new \App\Models\PolicyTransactionModel();
@ -390,9 +437,9 @@ if (!function_exists('generate_tsi_code')) {
$month = date('m');
$string = 'P';
if($type == 2){
if ($type == 2) {
$string2 = 'R';
}else{
} else {
$string2 = 'F';
}
@ -404,34 +451,36 @@ if (!function_exists('generate_tsi_code')) {
}
}
if (!function_exists('generateRandomCode')) {
function generateRandomCode($prefix = 'RTL-', $length = 6) {
if (! function_exists('generateRandomCode')) {
function generateRandomCode($prefix = 'RTL-', $length = 6)
{
// Generate a random number with the specified length
$randomNumber = str_pad(mt_rand(0, pow(10, $length)-1), $length, '0', STR_PAD_LEFT);
$randomNumber = str_pad(mt_rand(0, pow(10, $length) - 1), $length, '0', STR_PAD_LEFT);
// Return the code with the prefix
return $prefix . $randomNumber;
}
}
if (!function_exists('excelFileGDriveUpload')) {
if (! function_exists('excelFileGDriveUpload')) {
function excelFileGDriveUpload($file_id, $table_name) {
function excelFileGDriveUpload($file_id, $table_name)
{
$doc_type = "UPLOADS";
$uploadFilePath = WRITEPATH . 'uploads/' . ($table_name == 'batch_file' ? 'import_excel' : 'excel');
$models = [
'batch_file' => [
'model' => new BatchFileModel(),
'select' => "client_id, client_policy_id, file_name"
'select' => "client_id, client_policy_id, file_name",
],
'files' => [
'model' => new FileModel(),
'select' => "client_id, policy_id as client_policy_id, file_name"
'select' => "client_id, policy_id as client_policy_id, file_name",
],
];
if (!array_key_exists($table_name, $models)) {
if (! array_key_exists($table_name, $models)) {
return;
}
@ -442,8 +491,6 @@ if (!function_exists('excelFileGDriveUpload')) {
->where('is_active', 1)
->first();
if ($data) {
$uploadFilePath .= '/' . $data['file_name'];
// dd($data, $uploadFilePath);
@ -452,44 +499,45 @@ if (!function_exists('excelFileGDriveUpload')) {
$result = $GoogleDriveController->uploadFiletoGdrive(
// client_id : $data['client_id'],
client_policy_id: $data['client_policy_id'],
doc_type : $doc_type,
file_path : $uploadFilePath,
file_name : $data['file_name']
doc_type: $doc_type,
file_path: $uploadFilePath,
file_name: $data['file_name']
);
// dd($result);
return true;
}else{
} else {
return false;
}
}
}
if(!function_exists('checkFamilyFloaters')){
if (! function_exists('checkFamilyFloaters')) {
function checkFamilyFloaters($policy_premium_data, $client_policy_data, $emp_data){
function checkFamilyFloaters($policy_premium_data, $client_policy_data, $emp_data)
{
if($policy_premium_data['premium_type'] == 1){
if ($policy_premium_data['premium_type'] == 1) {
//only family floater
if ($emp_data['relationship'] == 'Self') {
return true;
}
if($client_policy_data['policy_type_id'] == 3 && $client_policy_data['is_addon'] == 3){
if($emp_data['rata_premimum'] > 0){
if ($client_policy_data['policy_type_id'] == 3 && $client_policy_data['is_addon'] == 3) {
if ($emp_data['rata_premimum'] > 0) {
return true;
}else{
} else {
return false;
}
}
return false;
}else{
} else {
//individual
return true;
}
@ -497,10 +545,10 @@ if(!function_exists('checkFamilyFloaters')){
}
}
if (!function_exists('numberToWords')) {
if (! function_exists('numberToWords')) {
function numberToWords($number)
{
$words = array(
$words = [
'0' => 'Zero',
'1' => 'One',
'2' => 'Two',
@ -528,30 +576,30 @@ if (!function_exists('numberToWords')) {
'60' => 'Sixty',
'70' => 'Seventy',
'80' => 'Eighty',
'90' => 'Ninety'
);
'90' => 'Ninety',
];
if ($number < 21) {
return $words[$number];
}
if ($number < 100) {
$tens = (int)($number / 10) * 10;
$tens = (int) ($number / 10) * 10;
$units = $number % 10;
return $words[$tens] . ($units ? ' ' . $words[$units] : '');
}
if ($number < 1000) {
$hundreds = (int)($number / 100);
$hundreds = (int) ($number / 100);
$remainder = $number % 100;
return $words[$hundreds] . ' Hundred' . ($remainder ? ' and ' . numberToWords($remainder) : '');
}
$levels = array('', ' Thousand', ' Million', ' Billion', ' Trillion', ' Quadrillion', ' Quintillion');
$levels = ['', ' Thousand', ' Million', ' Billion', ' Trillion', ' Quadrillion', ' Quintillion'];
for ($i = 0, $unit = 1; $i < count($levels); $i++, $unit *= 1000) {
if ($number < $unit * 1000) {
$current = (int)($number / $unit);
$current = (int) ($number / $unit);
$remainder = $number % $unit;
return numberToWords($current) . $levels[$i] . ($remainder ? ' ' . numberToWords($remainder) : '');
}
@ -561,7 +609,7 @@ if (!function_exists('numberToWords')) {
}
}
if (!function_exists('print_rr')) {
if (! function_exists('print_rr')) {
function print_rr($data)
{
echo "<pre>";
@ -570,7 +618,7 @@ if (!function_exists('print_rr')) {
}
}
if (!function_exists('get_server_details')) {
if (! function_exists('get_server_details')) {
/**
* Get the hostname and server name.
*
@ -588,7 +636,7 @@ if (!function_exists('get_server_details')) {
}
}
if (!function_exists('isJsonString')) {
if (! function_exists('isJsonString')) {
function isJsonString($input)
{
@ -597,16 +645,18 @@ if (!function_exists('isJsonString')) {
}
}
if (!function_exists('isValidDate')) {
function isValidDate($date, $format) {
if (! function_exists('isValidDate')) {
function isValidDate($date, $format)
{
$parsed_date = DateTime::createFromFormat($format, $date);
return $parsed_date && $parsed_date->format($format) === $date;
}
}
if (!function_exists('change_date_format')) {
if (! function_exists('change_date_format')) {
function change_date_format($date_str, $source_format = null, $output_format = 'Y-m-d') {
function change_date_format($date_str, $source_format = null, $output_format = 'Y-m-d')
{
$date_str = trim($date_str);
// Allowed date formats
@ -649,7 +699,7 @@ if (!function_exists('change_date_format')) {
// Case 1: Source and Output formats are provided
if ($source_format !== null && $output_format !== null) {
$date = DateTime::createFromFormat($source_format, $date_str);
if (!$date) {
if (! $date) {
// throw new Exception("Invalid date string for source format: $source_format");
// log_message('error', "❌ Date format error : Invalid date string | Params => date_str: {$date_str}, source_format: {$source_format}, output_format: {$output_format}");
return null;
@ -660,7 +710,7 @@ if (!function_exists('change_date_format')) {
// Case 2: Source format is provided, Output format is null
if ($source_format !== null && $output_format === null) {
$date = DateTime::createFromFormat($source_format, $date_str);
if (!$date) {
if (! $date) {
// throw new Exception("Invalid date string for source format: $source_format");
// log_message('error', "❌ Date format error : Invalid date string | Params => date_str: {$date_str}, source_format: {$source_format}, output_format: {$output_format}");
return null;
@ -726,20 +776,20 @@ if (!function_exists('change_date_format')) {
// }
// }
if (!function_exists('check_pay_by_employee_or_company')) {
if (! function_exists('check_pay_by_employee_or_company')) {
function check_pay_by_employee_or_company($policy_terms = null, $relationship = null)
{
// dd($policy_terms, $relationship);
if (!$policy_terms || !($policy_terms = json_decode($policy_terms, true))) {
if (! $policy_terms || ! ($policy_terms = json_decode($policy_terms, true))) {
return 0;
}
// dd($policy_terms, $relationship);
if (!isset($policy_terms['is_payable_employee'])) {
if (! isset($policy_terms['is_payable_employee'])) {
return 0;
}
@ -757,7 +807,7 @@ if (!function_exists('check_pay_by_employee_or_company')) {
'father' => 'elders',
'mother' => 'elders',
'father_in_law' => 'elders',
'mother_in_law' => 'elders'
'mother_in_law' => 'elders',
];
if (array_key_exists($relationship, $relationshipMap)) {
@ -771,10 +821,10 @@ if (!function_exists('check_pay_by_employee_or_company')) {
}
if (!function_exists('is_json_string')) {
if (! function_exists('is_json_string')) {
function is_json_string($string)
{
if (!is_string($string)) {
if (! is_string($string)) {
return false;
}
@ -783,7 +833,7 @@ if (!function_exists('is_json_string')) {
}
}
if (!function_exists('check_cd_entry_exist')) {
if (! function_exists('check_cd_entry_exist')) {
function check_cd_entry_exist($params)
{
$db = db_connect();
@ -823,7 +873,7 @@ if (!function_exists('check_cd_entry_exist')) {
if (count($entries) > 1) {
// Only one entry found (truncated)
return true;
}else{
} else {
return false;
}
} else {
@ -838,7 +888,7 @@ if (!function_exists('check_cd_entry_exist')) {
->get()
->getRowArray();
if (!empty($entry)) {
if (! empty($entry)) {
return true;
}
}
@ -847,7 +897,7 @@ if (!function_exists('check_cd_entry_exist')) {
}
}
if (!function_exists('expected_amount_calc')) {
if (! function_exists('expected_amount_calc')) {
function expected_amount_calc($data, $index)
{
$agreed_amount = (float) ($data['agreed_amount'][$index] ?? 0);
@ -859,12 +909,11 @@ if (!function_exists('expected_amount_calc')) {
$standard_tp_per = (float) ($data['standard_tp'][$index] ?? 0);
$standard_tep_per = (float) ($data['standard_ter'][$index] ?? 0);
if($data['bro_payable_by'] == 0){
if ($data['bro_payable_by'] == 0) {
$base_premium = (float) ($data['co_premium'][$index] ?? 0);
$third_part_premium = (float) ($data['co_tp_premium'][$index] ?? 0);
$terrisom_premium = (float) ($data['co_ter_premium'][$index] ?? 0);
}else{
} else {
$base_premium = (float) ($data['base_premium'][$index] ?? 0);
$third_part_premium = (float) ($data['tp_premium'][$index] ?? 0);
$terrisom_premium = (float) ($data['ter_premium'][$index] ?? 0);
@ -892,21 +941,21 @@ if (!function_exists('expected_amount_calc')) {
}
}
if (!function_exists('getFileIfExists')) {
if (! function_exists('getFileIfExists')) {
function getFileIfExists($path)
{
return file_exists(FCPATH . $path) ? base_url($path) : '';
}
}
if (!function_exists('removeNumberFormatting')) {
if (! function_exists('removeNumberFormatting')) {
function removeNumberFormatting($number)
{
return (float) str_replace(',', '', trim($number));
}
}
if (!function_exists('formatKey')) {
if (! function_exists('formatKey')) {
function formatKey($key, $len = 3)
{
$words = explode('_', $key);
@ -917,7 +966,7 @@ if (!function_exists('formatKey')) {
}
}
if (!function_exists('getMimeTypeByFileName')) {
if (! function_exists('getMimeTypeByFileName')) {
function getMimeTypeByFileName($file_name)
{
$ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
@ -943,7 +992,7 @@ if (!function_exists('getMimeTypeByFileName')) {
}
}
if (!function_exists('validateExcelFile')) {
if (! function_exists('validateExcelFile')) {
function validateExcelFile($file)
{
@ -963,7 +1012,7 @@ if (!function_exists('validateExcelFile')) {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.oasis.opendocument.spreadsheet',
'application/zip',
'application/octet-stream'
'application/octet-stream',
];
$allowedExtensions = ['xls', 'xlsx', 'ods', 'xlsm'];
@ -981,23 +1030,24 @@ if (!function_exists('validateExcelFile')) {
}
}
if (!function_exists('generate_ecard_download_link_based_on_tpa')) {
if (! function_exists('generate_ecard_download_link_based_on_tpa')) {
function generate_ecard_download_link_based_on_tpa($params) {
function generate_ecard_download_link_based_on_tpa($params)
{
$apiServiceController = new ApiServiceController();
$data = $apiServiceController->ecardRequest($params, $return_type = 'internal');
log_message('error', 'E-card request call from the helper for mail send');
if(isset($data['eCardDownload']) && !empty($data['eCardDownload'])){
if (isset($data['eCardDownload']) && ! empty($data['eCardDownload'])) {
return $data['eCardDownload'];
}else{
} else {
return $data['message'];
}
}
}
if (!function_exists('canSendOtp')) {
if (! function_exists('canSendOtp')) {
function canSendOtp(array $row, int $limitSeconds = 60): array
{
// If OTP does not exist → allow
@ -1015,7 +1065,7 @@ if (!function_exists('canSendOtp')) {
if ($currentTime < $allowedAfter) {
return [
'allowed' => false,
'retry_after' => $allowedAfter - $currentTime
'retry_after' => $allowedAfter - $currentTime,
];
}
@ -1023,14 +1073,14 @@ if (!function_exists('canSendOtp')) {
}
}
if (!function_exists('checkDuplicateClaim')) {
if (! function_exists('checkDuplicateClaim')) {
function checkDuplicateClaim(array $params): bool
{
$ticketMaster = new TicketMasterModel();
$query = $ticketMaster->where('is_active', 1);
if(empty($params['doa']) && empty($params['claim_amount'])){
if (empty($params['doa']) && empty($params['claim_amount'])) {
return false;
}
@ -1043,26 +1093,25 @@ if (!function_exists('checkDuplicateClaim')) {
}
}
if (!$hasValidCondition) {
if (! $hasValidCondition) {
return false;
}
$result = $query->countAllResults();
// print_r($ticketMaster->getLastQuery()->getQuery()); die;
if($result > 0){ return true; }else{ return false; }
if ($result > 0) {return true;} else {return false;}
}
}
function getRealClientIP()
{
$request = service('request');
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
if (! empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
if (! empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
}
@ -1097,16 +1146,13 @@ function generateFingerprint(bool $exclude_ua = false): string
$ipGroup = 'unknown';
}
if($exclude_ua){
if ($exclude_ua) {
return hash('sha256', $ipGroup);
}
return hash('sha256', $ua . '|' . $ipGroup);
}
if (!function_exists('convertGoogleDriveToDownloadLink')) {
if (! function_exists('convertGoogleDriveToDownloadLink')) {
function convertGoogleDriveToDownloadLink(?string $url): ?string
{
if (empty($url)) {
@ -1120,7 +1166,7 @@ if (!function_exists('convertGoogleDriveToDownloadLink')) {
$patterns = [
'#https?://drive\.google\.com/file/d/([^/]+)/?#',
'#https?://drive\.google\.com/open\?id=([^&]+)#',
'#https?://drive\.google\.com/uc\?id=([^&]+)#'
'#https?://drive\.google\.com/uc\?id=([^&]+)#',
];
foreach ($patterns as $pattern) {
@ -1137,7 +1183,7 @@ if (!function_exists('convertGoogleDriveToDownloadLink')) {
}
}
if (!function_exists('get_cd_balance')) {
if (! function_exists('get_cd_balance')) {
function get_cd_balance(): array
{
$session = session();
@ -1151,7 +1197,7 @@ if (!function_exists('get_cd_balance')) {
}
}
if (!function_exists('clear_cd_balance_session')) {
if (! function_exists('clear_cd_balance_session')) {
function clear_cd_balance_session(): void
{
$session = session();
@ -1164,15 +1210,24 @@ if (!function_exists('clear_cd_balance_session')) {
}
}
if (!function_exists('format_gender_v2')) {
function format_gender_v2($gender) {
if (empty($gender)) return null;
if (! function_exists('format_gender_v2')) {
function format_gender_v2($gender)
{
if (empty($gender)) {
return null;
}
$g = strtoupper(trim($gender));
// Direct-ah check pannuvom
if (str_starts_with($g, 'M')) return 'M'; // Male, M
if (str_starts_with($g, 'F')) return 'F'; // Female, F
if (str_starts_with($g, 'M')) {
return 'M';
}
// Male, M
if (str_starts_with($g, 'F')) {
return 'F';
}
// Female, F
// Others, Transgender, O - ivatrai 'O' ena return seiyum
if (str_starts_with($g, 'O') || str_starts_with($g, 'T')) {
@ -1183,20 +1238,22 @@ if (!function_exists('format_gender_v2')) {
}
}
if (!function_exists('map_relationship')) {
if (! function_exists('map_relationship')) {
/**
* Employee -> self, WIFE -> spouse ena maatri return seiyum.
*/
function map_relationship($relation) {
if (empty($relation)) return '';
function map_relationship($relation)
{
if (empty($relation)) {
return '';
}
// Case prechanai varaamal irukka lowercase-kku maatri check seivom
$r = strtolower(trim($relation));
if ($r == 'employee') {
return 'self';
}
else if ($r == 'wife') {
} else if ($r == 'wife') {
return 'spouse';
}
@ -1205,13 +1262,12 @@ if (!function_exists('map_relationship')) {
}
}
/**
* Extract identity from POST body or GET params.
* Looks for 'email' or 'mobile_number'.
*/
function resolveIdentity($request): ?string
{
function resolveIdentity($request): ?string
{
// Try POST body first
$email = $request->getPost('email');
// print_r($email);die;
@ -1232,8 +1288,7 @@ if (!function_exists('map_relationship')) {
$email = $req_data->email ?? null;
if (!$email)
{
if (! $email) {
$email = $req_data->email_id ?? null;
}
// return trim($email);
@ -1248,41 +1303,35 @@ if (!function_exists('map_relationship')) {
}
return null;
}
}
function recordRateLimitFailure(string $context = 'authApi'): void
{
function recordRateLimitFailure(string $context = 'authApi'): void
{
/** @var IncomingRequest $request */
$request = \Config\Services::request();
$limiter = \Config\Services::limiter(); // or your custom limiter service
$fingerprint = $request->getVar('rateLimitFingerprint')
?? generateFingerprint(exclude_ua: true);
$fingerprint = $request->getVar('rateLimitFingerprint') ?? generateFingerprint(exclude_ua: true);
$identity = $request->getVar('rateLimitIdentity')
?? resolveIdentity($request);
$identity = $request->getVar('rateLimitIdentity') ?? resolveIdentity($request);
// Record IP-level failure
$limiter->recordIpFailure($fingerprint);
// Record user-level failure
if (!empty($identity)) {
if (! empty($identity)) {
$limiter->recordUserFailure($identity, $context);
}
}
}
if (!function_exists('getCurrentFinancialYear')) {
if (! function_exists('getCurrentFinancialYear')) {
function getCurrentFinancialYear()
{
// Get current year and month
$date = new DateTime();
$currentYear = (int)$date->format('Y');
$currentMonth = (int)$date->format('m');
$currentYear = (int) $date->format('Y');
$currentMonth = (int) $date->format('m');
// If month is Jan, Feb, or March, we are still in the previous year's FY
if ($currentMonth < 4) {
@ -1295,13 +1344,12 @@ if (!function_exists('map_relationship')) {
return $startYear . '-' . $endYear;
}
}
}
if (!function_exists('format_financial_year')) {
if (! function_exists('format_financial_year')) {
function format_financial_year(string $financialYear): string
{
if (empty($financialYear) || !str_contains($financialYear, '-')) {
if (empty($financialYear) || ! str_contains($financialYear, '-')) {
return $financialYear;
}
@ -1313,27 +1361,24 @@ if (!function_exists('map_relationship')) {
return "APR " . $startYear . " - MAR " . $endYear;
}
}
}
if(!function_exists('add_google_calender_event')){
function add_google_calender_event($data){
if (! function_exists('add_google_calender_event')) {
function add_google_calender_event($data)
{
$calendar = new \App\Libraries\GoogleCalendarService();
// Check if user is authenticated without passing tokens manually
if (!$calendar->isReady()) {
return ['status' => 'failed', 'code' => '404' , 'message' => 'Google Access Token Expired'];
if (! $calendar->isReady()) {
return ['status' => 'failed', 'code' => '404', 'message' => 'Google Access Token Expired'];
}
try {
$response = $calendar->createEvent($data);
return ['status' => 'success', 'code' => '200' , 'message' => 'Follow-up Saved', 'response' => $response];
return ['status' => 'success', 'code' => '200', 'message' => 'Follow-up Saved', 'response' => $response];
} catch (\Exception $e) {
return ['status' => 'failed', 'code' => '500' , 'message' => 'Error: ' . $e->getMessage()];
return ['status' => 'failed', 'code' => '500', 'message' => 'Error: ' . $e->getMessage()];
}
}
}
}

View File

@ -4,6 +4,7 @@ use Google_Client;
use Google_Service_Sheets;
use Google_Service_Drive;
use Google_Service_Sheets_ValueRange;
use Google_Service_Sheets_BatchUpdateSpreadsheetRequest;
class GoogleSheetLib
{
@ -11,13 +12,14 @@ class GoogleSheetLib
protected Google_Service_Sheets $sheets;
protected Google_Service_Drive $drive;
public function __construct()
{
$this->client = new Google_Client();
// Service account JSON
$this->client->setAuthConfig(
ROOTPATH . 'gdrive-demo-394007-5b1d856b0c5b.json'
ROOTPATH . 'nhance-ee8d1-e3c5269b1ec7.json'
);
// IMPORTANT for service account
@ -32,11 +34,12 @@ class GoogleSheetLib
// Init services
$this->sheets = new Google_Service_Sheets($this->client);
$this->drive = new Google_Service_Drive($this->client);
}
/* ===================== READ ===================== */
public function read(string $spreadsheetId, string $range = 'Sheet1')
public function read(string $spreadsheetId, string $range = 'RFQ Page')
{
$response = $this->sheets
->spreadsheets_values
@ -77,4 +80,205 @@ class GoogleSheetLib
return $response->getBody()->getContents();
}
/* ================= COPY TEMPLATE ================= */
public function copyTemplate(string $templateId, string $name, string $folderId): string
{
$file = $this->drive->files->copy(
$templateId,
new \Google_Service_Drive_DriveFile([
'name' => $name,
'parents' => [$folderId],
]),[
'supportsAllDrives' => true,
'fields' => 'id, name, parents'
]
);
return $file->id;
}
/* ================= PERMISSIONS ================= */
public function applyPermissions(string $fileId, array $permissions)
{
foreach ($permissions['editors'] as $email) {
$this->createPermission($fileId, $email, 'writer');
}
foreach ($permissions['viewers'] as $email) {
$this->createPermission($fileId, $email, 'reader');
}
}
private function createPermission(string $fileId, string $email, string $role)
{
$type = str_starts_with($email, 'group:') ? 'group' : 'user';
$email = str_replace('group:', '', $email);
$this->drive->permissions->create(
$fileId,
new \Google_Service_Drive_Permission([
'type' => $type,
'role' => $role,
'emailAddress' => $email
]),
['sendNotificationEmail' => false,'supportsAllDrives' => true]
);
}
/* ================= PROTECTIONS ================= */
public function applyProtectionsold(string $spreadsheetId, array $ranges)
{
$spreadsheet = $this->sheets->spreadsheets->get($spreadsheetId);
$sheetId = $spreadsheet->getSheets()[0]->getProperties()->getSheetId();
$requests = [];
foreach ($ranges as $range) {
[$sheetName, $a1] = explode('!', $range);
$requests[] = [
'addProtectedRange' => [
'protectedRange' => [
'range' => [
'sheetId' => $sheetId
],
'warningOnly' => false
]
]
];
}
$this->sheets->spreadsheets->batchUpdate(
$spreadsheetId,
new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
'requests' => $requests
])
);
}
public function applyProtections(string $spreadsheetId, array $protections)
{
// Fetch spreadsheet metadata
$spreadsheet = $this->sheets->spreadsheets->get(
$spreadsheetId,
['fields' => 'sheets(properties(sheetId,title,gridProperties))']
);
// Map sheet names
$sheetMap = [];
foreach ($spreadsheet->getSheets() as $sheet) {
$props = $sheet->getProperties();
$sheetMap[$props->getTitle()] = [
'sheetId' => $props->getSheetId(),
'rowCount' => $props->getGridProperties()->getRowCount(),
'colCount' => $props->getGridProperties()->getColumnCount(),
];
}
$requests = [];
foreach ($protections as $protection) {
$rangeStr = $protection['range'];
if (!str_contains($rangeStr, '!')) {
throw new \Exception("Invalid range format: {$rangeStr}");
}
[$sheetName, $a1] = explode('!', $rangeStr, 2);
if (!isset($sheetMap[$sheetName])) {
throw new \Exception("Sheet not found: {$sheetName}");
}
$sheetMeta = $sheetMap[$sheetName];
$gridRange = $this->convertA1ToGridRange(
$a1,
$sheetMeta['sheetId'],
$sheetMeta['rowCount'],
$sheetMeta['colCount']
);
$protectedRange = [
'range' => $gridRange,
'description' => 'RFQ Protected Area',
'warningOnly' => false,
'editors' => [
'users' => $protection['users'] ?? [],
'groups' => $protection['groups'] ?? []
]
];
$requests[] = [
'addProtectedRange' => [
'protectedRange' => $protectedRange
]
];
}
if (!empty($requests)) {
$batch = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
'requests' => $requests
]);
$this->sheets->spreadsheets->batchUpdate($spreadsheetId, $batch);
}
return true;
}
/* ================= URL ================= */
public function sheetUrl(string $sheetId): string
{
return "https://docs.google.com/spreadsheets/d/{$sheetId}/edit";
}
private function convertA1ToGridRange($a1, $sheetId, $maxRows, $maxCols)
{
if (preg_match('/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i', $a1, $m)) {
$startCol = $this->colToIndex($m[1]);
$startRow = intval($m[2]) - 1;
if (!empty($m[3])) {
$endCol = $this->colToIndex($m[3]) + 1;
$endRow = intval($m[4]);
} else {
$endCol = $startCol + 1;
$endRow = $startRow + 1;
}
return [
'sheetId' => $sheetId,
'startRowIndex' => $startRow,
'endRowIndex' => $endRow,
'startColumnIndex' => $startCol,
'endColumnIndex' => $endCol
];
}
throw new \Exception("Unsupported A1 format: {$a1}");
}
private function colToIndex($letters)
{
$letters = strtoupper($letters);
$index = 0;
for ($i = 0; $i < strlen($letters); $i++) {
$index = $index * 26 + (ord($letters[$i]) - 64);
}
return $index - 1;
}
}

View File

@ -68,6 +68,7 @@
<div class="form-group col-md-6">
<label for="mobile">CD Account Number<span class="text-danger">*</span></label>
<input type="text" class="form-control" id="cd_ac_no_for_cd_master" name="cd_ac_no" onkeypress="return onlyNumbers(event)" required data-parsley-type="digits">
<small style="margin-left: 10px;font-size: 10px;"><span class="text-danger" id="cd_ac_no_for_cd_master_errorr"></span></small>
</div>
<div class="form-group col-md-6">
@ -126,12 +127,15 @@
console.log(res)
if(res.status == true){
toastr.warning(res.message, 'warning');
$('#cd_ac_no_for_cd_master_errorr').text(res.message);
// toastr.warning(res.message, 'warning');
// $('#cd_ac_no').val('');
$('#cd_master_btn_Submit').prop('disabled',true);
return;
}
$('#cd_master_btn_Submit').prop('disabled',false);
$('#cd_ac_no_for_cd_master_errorr').text('');
},
error: function (xhr, status, error) {
console.error(xhr.responseText);

View File

@ -273,9 +273,7 @@
<div class="card-body">
<div class="row" style="padding-bottom: 10px;">
<div class="col-6" style="align-self: center;">
<h4 style="position: relative;">Client Onboarding <span id="client_heading"><?php if (isset($client)) {
echo ' - ' . $client['client_name'];
} ?></h4></span>
<h4 style="position: relative;">Client Onboarding <span id="client_heading"><?php if (isset($client)) { echo ' - ' . $client['client_name']; } ?></span> </h4>
</div>
<div class="col-6" style="text-align: right;">
<a href="<?= base_url("client/list"); ?>"><i class="mdi mdi-arrow-left" style="font-size: 17px;"></i></a>

View File

@ -16,6 +16,28 @@
<button onclick="save()">💾 Save</button>
<button onclick="download()"> Download</button>
<button onclick="createRfq()">🚀 Create RFQ</button>
<div id="result" style="margin-top:15px;"></div>
<script>
function createRfq() {
fetch('<?php echo base_url() ?>' +'/sheet/create', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'templateId=1efHU8EBkjTZTzOx115xRrtgz3kwvvVp0p5GbrvREfiw'
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
document.getElementById('result').innerHTML =
`<a href="${res.url}" target="_blank">Open RFQ Sheet</a>`;
} else {
alert(res.message);
}
});
}
</script>
<hr>
@ -26,9 +48,9 @@
const sheetId = "<?= esc($sheetId) ?>";
alert('second');
/* ---------- LOAD ---------- */
fetch('<?php echo base_url() ?>' + `/sheet/${sheetId}/fetch`)
.then(r => r.json())
fetch('<?php echo base_url() ?>' + `sheet/${sheetId}/fetch`)
.then(r => r.json())
// .then(r => r.json())
.then(render);
function render(data) {

View File

@ -2073,11 +2073,11 @@
<a href="<?= base_url('/leads/list') ?>" class="img-inactive">
<img
src="<?= base_url() . "public"; ?>/assets/images/leads_sb.png" alt="Logo" height="20">
<span> Leads </span>
<span> Opportunities </span>
</a>
</li>
<?php } ?>
<?php if (in_array(get_role_id(), [1, 2, 3, 5])) { ?>
<?php if (in_array(get_role_id(), [1, 5]) || (get_role_id() == 4 && in_array(SALES_TEAM_ID, user_team()))) { ?>
<li class="li-seperate" id="app-sales-tracker-li">
<a id="app_sales_tracker" href="#sidebarSalesTrackermenu" data-toggle="collapse" class="waves-effect img-inactive" style="color: grey;">
<img
@ -2087,13 +2087,13 @@
<div class="collapse" id="sidebarSalesTrackermenu">
<ul class="nav-second-level">
<li>
<a href="<?= base_url('/sales/page/Dashboard') ?>"> <i class="ri-dashboard-line"></i> Dashboard </a>
<a href="<?= base_url('/sales/dashboard') ?>"> <i class="ri-dashboard-line"></i> Dashboard </a>
</li>
<li>
<a href="<?= base_url('/sales') ?>"> <i class="ri-group-line"></i> Leads </a>
</li>
<li>
<a href="<?= base_url('/sales/activities') ?>"><i class="ri-flashlight-line"></i> Activities </a>
<a href="<?= base_url('/sales/loadactivities') ?>"><i class="ri-flashlight-line"></i> Activities </a>
</li>
<!-- <li>
<a href="<?= base_url('/sales/page/Opportunities') ?>"> <i class="ri-checkbox-circle-line"></i> Opportunities </a>

View File

@ -39,7 +39,7 @@
</div>
<div class="form-group col-md-3">
<label for="filter_lead_type"> Lead Type <span class="text-danger"></span></label>
<label for="filter_lead_type"> Opportunity Type <span class="text-danger"></span></label>
<select class="form-control" id="filter_lead_type" name="filter_lead_type">
<option value="0">Select</option>
<?php

View File

@ -165,10 +165,10 @@ $isActive = get_role_id() == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID, user_
<div class="leadTypeTile">
<div class="card dash-card" onclick="hide_and_show_tile(1,1)">
<div class="card-block">
<h4 class="mb-3">Leads</h4>
<h4 class="mb-3">opportunities</h4>
<h2 ><span class="span-color"><?= isset($lead_data) ? $lead_data['total'] : 0 ?></span>
</h2>
<h6><span class="text-danger">Lead Tracker</span></h6>
<h6><span class="text-danger">Opportunity Tracker</span></h6>
</div>
</div>
</div>

View File

@ -124,7 +124,7 @@
<a href="<?= base_url('leads/list') ?>" style="cursor: pointer;">
<i class="mdi mdi-chevron-left" style="font-size: 43px;"></i>
</a>
<span class="title-text" id="page_title" style="position: relative;">Add Lead</span>
<span class="title-text" id="page_title" style="position: relative;">Add Opportunity</span>
</h4>
</div>
<form role="form" class="parsley-examples" method="post" id="leads_form_id"
@ -139,9 +139,9 @@
<div class="form-row">
<div class="form-group col-md-4">
<label for="lead_type">Lead Type<span class="text-danger">*</span></label>
<label for="lead_type">Opportunity Type<span class="text-danger">*</span></label>
<select class="form-control" id="lead_type" name="lead_type" required>
<option value="">Select Lead Type</option>
<option value="">Select Opportunity Type</option>
<?php
if (isset($lead_type) && count($lead_type)) {
foreach ($lead_type as $key => $value) {
@ -739,7 +739,7 @@
hide_list_show_add();
var page_title = 'Edit Lead';
var page_title = 'Edit Opportunity';
$('#page_title').text(page_title);
$('#leads_primarykey').val(res.data.id);

View File

@ -604,7 +604,8 @@ if (isset($selected_lead_type)) {
let isFirstField = container.childElementCount === 0; // Check if it's the first field
let placeholder = isFirstField ? 'First file must be Demography.' : '';
let accept = isFirstField ? '.xls,.xlsx' : '';
let accept = isFirstField ? '.xls,.xlsx' : '.xls,.xlsx,.pdf,.jpg,.jpeg,.png';
let accept_text = isFirstField ? 'First file: Excel only (.xls, .xlsx)' : 'Files: PDF, Excel (.xlsx, .xls), Images (.png, .jpg, .jpeg)';
if(selected_lead_form_type != 1){
@ -640,12 +641,12 @@ if (isset($selected_lead_type)) {
<div class="col-auto" style="align-content: center;">
<label>File Upload </label> <br>
<span class="text-danger">PDF | Excel (XLSX, XLS) | Images (PNG, JPG)</span>
</div>
<div class="col-md-3">
<div class="input-icon">
<input type="file" class="form-control" id="file_name_${fileIndex}" name="file_name_${increment}[]" accept="${accept}" style="background: #F5FFFF !important;">
<i class="mdi mdi-upload additional-icon"></i>
<small style="margin-left: 10px;font-size: 10px;"><span class="text-danger">${accept_text}</span></small>
</div>
</div>
@ -954,7 +955,7 @@ if (isset($selected_lead_type)) {
hide_list_show_add();
$('#page_title').text('Edit Lead');
$('#page_title').text('Edit Opportunity');
$('#leads_primarykey').val(data.id || '');
$('#actual_lead_id').val(data.actual_lead_id || 0);
$('#policy_start_date').val(data.policy_end_date || '');
@ -1129,7 +1130,7 @@ if (isset($selected_lead_type)) {
}
}
$('#page_title').text('Edit Lead');
$('#page_title').text('Edit Opportunity');
$('#leads_primarykey').val(data.id || '');
$('#actual_lead_id').val(data.actual_lead_id || 0);
$('#policy_start_date').val(data.policy_end_date || '');

View File

@ -151,7 +151,7 @@ table.dataTable tbody td {
<div class="card-body">
<div class="row" style="margin-bottom:1rem;">
<div class="col-6" style="align-self: center;">
<h4 style="position: relative;">Leads List</h4>
<h4 style="position: relative;">Opportunities List</h4>
</div>
<div class="col-3" id="status_change" style="text-align: right; position: relative;top: 56px; left: 291px;">
@ -165,7 +165,7 @@ table.dataTable tbody td {
<thead class="bg-light">
<tr>
<th><div class="column-header">S.No &nbsp;</div></th>
<th><div class="column-header">Lead Type &nbsp;</div></th>
<th><div class="column-header">Opportunity Type &nbsp;</div></th>
<th><div class="column-header">Issuer &nbsp;</div></th>
<th><div class="column-header">Client Type &nbsp;</div></th>
<th><div class="column-header">Client / Branch &nbsp;</div></th>
@ -257,7 +257,7 @@ table.dataTable tbody td {
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myCenterModalLabel">Select Lead Type</h4>
<h4 class="modal-title" id="myCenterModalLabel">Select Opportunity Type</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
</div>
<div class="modal-body">
@ -265,14 +265,14 @@ table.dataTable tbody td {
<div class="radio-option selected" onclick="selectOption(this, 'lead_eb')">
<div class="form-check">
<input class="form-check-input" type="radio" name="lead_form_type" id="lead_eb" value="1" checked>
<label class="form-check-label" for="lead_eb">Lead EB</label>
<label class="form-check-label" for="lead_eb">EB</label>
</div>
</div>
<div class="radio-option" onclick="selectOption(this, 'lead_non_eb')">
<div class="form-check">
<input class="form-check-input" type="radio" name="lead_form_type" id="lead_non_eb" value="2">
<label class="form-check-label" for="lead_non_eb">Lead Non-EB</label>
<label class="form-check-label" for="lead_non_eb">Non-EB</label>
</div>
</div>
</div>
@ -467,7 +467,7 @@ table.dataTable tbody td {
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
buttons: [
{
text: 'Add',
text: '<i class="mdi mdi-plus" ></i><span class=" btn-custom"> Add </span>',
className: 'btn app-btn-primary mr-2',
action: function(e, dt, node, config) {
openLeadTypeAskModal();
@ -481,7 +481,7 @@ table.dataTable tbody td {
{
extend: 'csv',
text: '<i class="mdi mdi-file-delimited " ></i><span class=" btn-custom"> CSV </span>',
title: 'Policy-Tranction-Inception-List',
title: 'Opportunity-List',
className: 'app-btn-primary ',
exportOptions: {
columns: ':not(:last-child)'
@ -523,7 +523,7 @@ table.dataTable tbody td {
// let Lead_type = $('#openLeadTypeAskModal').val()
let Lead_type = $('input[name="lead_form_type"]:checked').val();
console.log('Lead_type ', Lead_type)
let url = '<?=base_url('/util/getLeadNonEB/')?>' + Lead_type
let url = '<?=base_url('/util/getLeadNonEB/')?>' + Lead_type + '/0';
console.log('url ', url)
window.location.href = url;

View File

@ -66,7 +66,7 @@
<a href="<?= base_url('leads/list') ?>" style="cursor: pointer;" type="button" id="btnAdd">
<i class="mdi mdi-chevron-left" style="font-size: 43px;"></i>
</a>
<span class="title-text" id="page_title" style="position: relative;">Add Lead</span>
<span class="title-text" id="page_title" style="position: relative;">Add Opportunity</span>
</h4>
</div>
<form role="form" class="parsley-examples" method="post" id="leads_non_eb_form_id"
@ -81,9 +81,9 @@
<div class="form-row">
<div class="form-group col-md-4">
<label for="lead_type">Lead Type<span class="text-danger">*</span></label>
<label for="lead_type">Opportunity Type<span class="text-danger">*</span></label>
<select class="form-control" id="lead_type" name="lead_type" required>
<option value="">Select Lead Type</option>
<option value="">Select Opportunity Type</option>
<?php
if (isset($lead_type) && count($lead_type)) {
foreach ($lead_type as $key => $value) {

View File

@ -8,6 +8,8 @@ $increment = 1;
$isFirstField = ($index === 0);
$placeholder = $isFirstField ? 'First file must be Demography.' : '';
$accept = $isFirstField ? '.xls,.xlsx' : '.xls,.xlsx,.pdf,.jpg,.jpeg,.png';
$accept_text = $isFirstField ? 'First file: Excel only (.xls, .xlsx)' : 'Files: PDF, Excel (.xlsx, .xls), Images (.png, .jpg, .jpeg)';
$displayIndex = $index + 1;
?>
@ -35,6 +37,7 @@ $increment = 1;
<div class="input-icon">
<input type="file" class="form-control" id="file_name_<?= $displayIndex ?>" name="file_name_<?= $increment ?>[]" accept="<?= $accept ?>" onchange="showFileName(this, <?= $displayIndex ?>)" style="background: #F5FFFF !important;">
<i class="mdi mdi-upload additional-icon"></i>
<small style="margin-left: 10px;font-size: 10px;"><span class="text-danger"><?= $accept_text ?></span></small>
</div>
</div>
@ -67,6 +70,7 @@ $increment = 1;
<input type="file" class="form-control" id="file_name_1"
name="file_name_1[]" accept=".xls,.xlsx" onchange="showFileName(this, 1)" style="background: #F5FFFF !important;">
<i class="mdi mdi-upload additional-icon"></i>
<small style="margin-left: 10px;font-size: 10px;"><span class="text-danger">First file: Excel only (.xls, .xlsx)</span></small>
</div>
</div>
<div class="col-auto">

View File

@ -75,6 +75,7 @@
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
.status-pending { background: #fff3e0; color: #f57c00; }
.status-completed { background: #e8f5e9; color: #388e3c; }
.status-unknown { background: #000; color: #fff; }
/* Modals */
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
@ -143,7 +144,7 @@
</style>
<!-- copy of tracker_view -->
<div class="main-content">
<hr class="my-0">
<div class="lead-header">
@ -159,7 +160,7 @@
<div class="lead-actions">
<input type="text" class="search-input" id="mainSearch"
placeholder="Search activities..." onkeyup="fetchActivities(false)">
<button class="btn-primary" onclick="openModal('addActivityModal')">
<button class="btn-primary" onclick="openMainActivityModal()">
+ Add Activity
</button>
</div>
@ -183,64 +184,6 @@
</div>
<div class="modal" id="addActivityModal">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add Lead</h4>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('addActivityModal')">&times;</button>
</div>
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<div class="form-group">
<div class="col-12 mb-1">
<div class="col-xl-12 col-lg-12 col-md-12">
<label class="form-label">Company Name<span class="text-danger">*</span></label>
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Phone</label>
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')">
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Status<span class="text-danger">*</span></label>
<select name="status" class="form-control" style="width: 100%;" required>
<option value="New">New</option>
<option value="Potential">Potential</option>
<option value="Prospects">Prospects</option>
<option value="Not a Prospects">Not a Prospects</option>
</select>
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Assign To<span class="text-danger">*</span></label>
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
<option value="">Select User</option>
<?php foreach($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('addActivityModal')">Cancel</button>
<button type="submit" class="btn-primary">Create Lead</button>
</div>
</div>
</form>
</div>
</div>
<div class="modal" id="leadDetailModal">
<div class="modal-content" style="max-width: 850px;">
<div class="modal-header">
@ -295,7 +238,7 @@
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<input type="hidden" id="act_string_flag"> <!-- ADD THIS -->
<div class="form-group">
<label class="form-label">Select Lead <span class="text-danger">*</span></label>
@ -306,15 +249,15 @@
</select>
</div>
<div class="form-group">
<label class="form-label">Activity Type</label>
<label class="form-label">Activity Type <span class="text-danger">*</span></label>
<div class="activity-types" id="typeButtons">
<button type="button" class="activity-type-btn active" onclick="selectType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn" onclick="selectType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn" onclick="selectType('Demo', this)">🎬 Demo</button>
<button type="button" class="activity-type-btn" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn" onclick="selectType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn d_activity_type active" data-type="Call" onclick="selectType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Email" onclick="selectType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)"> To Do</button>
</div>
</div>
<div class="form-group">
@ -331,8 +274,8 @@
</div>
</div>
<div class="form-group">
<label class="form-label">Assigned To</label>
<select id="act_owner" class="search-input searchable" style="width:100%">
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
<select id="act_owner" class="search-input searchable" style="width:100%" required>
<?php foreach($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
<?php endforeach; ?>
@ -362,6 +305,7 @@
<input type="hidden" id="comp_id">
<input type="hidden" id="lead_id">
<input type="hidden" id="string_flag">
<div class="form-group">
<label class="form-label">Outcome / Notes <span class="text-danger">*</span></label>
<textarea id="comp_notes" class="search-input" style="width:100%; height:80px;" required placeholder="What happened? Any next steps?" rows="3" required></textarea>
@ -381,7 +325,7 @@
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🎬 Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Todo', this)"> To Do</button>
@ -456,124 +400,6 @@
</div>
</div>
<div class="modal" id="editLeadModal">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Edit Lead</h4>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('editLeadModal')">&times;</button>
</div>
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
<hr class="my-0">
<input type="hidden" class="form-control" id="hidden_lead_id" name="lead_id">
<div class="modal-body p-4">
<div class="form-group">
<div class="col-12 mb-1">
<div class="col-xl-12 col-lg-12 col-md-12">
<label class="form-label">Company Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" name="company_name" style="width: 100%;" placeholder="Enter Company Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')" required>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Email</label>
<input type="email" class="form-control" name="email" style="width: 100%;" placeholder="Enter the Email" oninput="this.value = this.value.replace(/[^a-zA-Z0-9@.\-_+]/g, '')">
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Phone</label>
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')">
</div>
</div>
<div class="col-12 mb-1">
<div class="col-xl-12 col-lg-12 col-md-12">
<label class="form-label">Address</label>
<textarea id="address" name="address" class="form-control" style="width:100%; height:100px;" placeholder="Enter full Address"></textarea>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">WebSite</label>
<input type="text" class="form-control" name="website" style="width: 100%;" placeholder="Enter the Website">
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">GST Number</label>
<input type="text" class="form-control" name="gst_number" style="width: 100%;" placeholder="Enter the GST Number" oninput="this.value = this.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase()" maxlength="15"
pattern="^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$">
</div>
</div>
<div class="col-12 mb-1" style="padding-left: 25px;">
<label class="form-label">Contact Persons</label>
<div id="contactPersonsContainer">
<div id="savedContactsContainer"> <span id="NoData"> <center><i> No contact persons added yet </i> </center> </span> </div>
</div>
</div>
<div class="col-12 mb-1 ml-1 ">
<div class="row g-2 align-items-center">
<div class="col-md-5" style="padding-left: 20px;">
<input type="text"
class="form-control"
id="contact_name"
placeholder="Contact Person Name"
oninput="this.value=this.value.replace(/[^A-Za-z\s]/g,'')">
</div>
<div class="col-md-4">
<input type="text"
class="form-control"
id="contact_mobile"
placeholder="Contact Mobile Number"
maxlength="10"
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
</div>
<div class="col-md-3" style="padding-right: 4%;">
<button type="button"
id="btnSaveContact"
class="btn btn-sm w-100"
style="background:#ff6a3d;border:none;color:white;">
+ Save Contact
</button>
</div>
</div>
</div>
<div class="form-row mb-1">
<div class="col-md-12 ml-3" style="width: 97%;">
<label class="form-label">Status <span class="text-danger">*</span></label>
<select name="status" class="form-control" style="width: 100%;" required>
<option value="New">New</option>
<option value="Potential">Potential</option>
<option value="Prospects">Prospects</option>
<option value="Not a Prospects">Not a Prospects</option>
</select>
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
<option value="">Select User</option>
<?php foreach($users as $user): ?>
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('editLeadModal')">Cancel</button>
<button type="submit" class="btn-primary">Update Lead</button>
</div>
</div>
</form>
</div>
</div>
<script>
$(document).ready(function() {
resetFlatpicker();
@ -603,8 +429,9 @@ function resetFlatpicker(){
});
}
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🎬", Share: "📄", Todo: "" };
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🖥️", Share: "📄", Todo: "" };
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
const API = '<?= base_url('sales') ?>';
let filter = 'all';
let lead_id = null;
@ -617,8 +444,6 @@ let currentOffset = 0;
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
// function closeModal(id) { document.getElementById(id).classList.remove('active'); }
function closeModal(id) {
const modal = document.getElementById(id);
@ -633,9 +458,9 @@ function closeModal(id) {
// Specific cleanup for your "Activity" logic
if (id === 'activityModal') {
selectedType = 'Call'; // Reset your global activity type variable
$('.activity-type-btn').removeClass('active'); // Remove 'active' from all activity buttons
$(`.activity-type-btn[data-type="${selectedType}"]`).addClass('active'); // Find the specific button for 'Call' and make it active
selectedType = 'Call';
$('#typeButtons .d_activity_type').removeClass('active');
$('#typeButtons .d_activity_type[data-type="Call"]').addClass('active');
}
// Specific cleanup for "Complete" modal (hidden follow-up sections)
@ -649,18 +474,26 @@ function closeModal(id) {
// Specific action for leadDetailModal
if (id === 'leadDetailModal') {
switchTab('activity'); // Reset the tab back to 'activity'
document.getElementById('btn_add_opportunity').classList.style.display = 'none'; // Hide the button
document.getElementById('btn_add_opportunity').style.display = 'none';
}
}
}
function openMainActivityModal() {
document.getElementById('act_owner').value = "";
document.getElementById('act_owner').dispatchEvent(new Event('change'));
document.getElementById('act_lead_id').value = "";
document.getElementById('act_lead_id').dispatchEvent(new Event('change'));
document.getElementById('act_string_flag').value = "frommain"; // ✅ flag for main
openModal('activityModal');
}
function selectType(val, el) {
document.querySelectorAll('.activity-type-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
selectedType = val;
}
function selectFollowUpActivityType(val, el) {
document.querySelectorAll('.f_activity_type').forEach(b => b.classList.remove('active'));
el.classList.add('active');
@ -675,7 +508,6 @@ function setFilter(val, el) {
fetchActivities();
}
async function fetchActivities(isLoadMore = false) {
const q = document.getElementById('mainSearch').value;
const grid = document.getElementById('activitiesGrid');
@ -692,6 +524,9 @@ async function fetchActivities(isLoadMore = false) {
</div>
</div>`;
console.log("function here");
console.log(salesManagerIds);
// return;
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
if (typeof salesManagerIds === 'undefined' || salesManagerIds.length === 0) {
console.log("No Sales Manager IDs found. Skipping API call.");
@ -718,7 +553,7 @@ async function fetchActivities(isLoadMore = false) {
}
// 5. Build URL with dynamic offset
const url = `${API}/activities?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
let url = `${API}/activities?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
// url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
url += `&assigned_to=${salesManagerIds.join(',')}`;
@ -774,7 +609,7 @@ async function fetchActivities(isLoadMore = false) {
</div>
<div class="activity-actions">
<button class="btn-complete" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">
<button style="display:${a.status == 'completed' ? 'none' : 'block'};" class="btn-complete" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frommain')">
Complete
</button>
<button class="btn-view" onclick="viewDetail(${a.lead_id})">
@ -815,13 +650,13 @@ async function fetchActivities(isLoadMore = false) {
}
}
// 2. Detail Logic
async function viewDetail(id) {
console.log("i am here");
lead_id = id;
const res = await fetch(`${API}/leads/${id}`);
const json = await res.json();
const l = json.data;
let res = await fetch(`${API}/leads/${id}`);
let json = await res.json();
let l = json.data;
document.getElementById('det_company').innerText = l.company_name;
document.getElementById('det_email').innerText = l.email || 'N/A';
@ -843,8 +678,12 @@ async function viewDetail(id) {
} else { document.getElementById('box_owner').style.display = 'none'; }
const badge = document.getElementById('det_status_badge');
badge.innerText = l.status;
badge.className = `lead-status status-${l.status.toLowerCase().replace(' ', '-')}`;
// Use a fallback string like 'unknown' or 'pending'
let status = l.status || 'unknown';
badge.innerText = status;
badge.className = `lead-status status-${status.toLowerCase().replace(' ', '-')}`;
const oppBtn = document.getElementById('btn_add_opportunity');
if (l.status === 'Prospects') {
oppBtn.style.display = 'inline-block';
@ -856,6 +695,7 @@ async function viewDetail(id) {
renderCard(l.opportunities || []);
openModal('leadDetailModal');
}
function renderCard(opps) {
const cont = document.getElementById('opportunitiesContainer');
@ -897,6 +737,7 @@ function renderCard(opps) {
</div>`;
}).join('') : `<div class="empty-state"> <div class="empty-icon">💼</div> No opportunities yet. Add one to get started!</div>`;
}
function renderTimeline(acts) {
const cont = document.getElementById('timelineContainer');
@ -919,8 +760,8 @@ function renderTimeline(acts) {
hour12: true
});
// Then use the variable in your HTML:
// <span style="font-size:11px; color:#999">${formattedDate}</span>
// Then use the variable in your HTML:
// <span style="font-size:11px; color:#999">${formattedDate}</span>
return `
<div class="timeline-item">
@ -935,7 +776,7 @@ function renderTimeline(acts) {
</div>
<div style="font-size:13px; color:#444;"></div>
${a.status === 'pending' ?
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frompopup')">Mark Complete</button>` :
`<div style="font-size:12px; color:#388e3c; margin-top:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
}
</div>
@ -943,15 +784,17 @@ function renderTimeline(acts) {
`;
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
}
function openActivityModal() {
document.getElementById('act_owner').value = global_lead_assigned_to;
document.getElementById('act_owner').dispatchEvent(new Event('change'));
document.getElementById('act_lead_id').value = lead_id;
document.getElementById('act_lead_id').dispatchEvent(new Event('change'));
document.getElementById('act_string_flag').value = "frompopup";
openModal('activityModal');
}
function openComp(id, assigned_to, lead_id) {
function openComp(id, assigned_to, lead_id, string_flag) {
document.getElementById('completeForm').reset();
document.getElementById('f_notes').removeAttribute('required');
document.getElementById('f_date').removeAttribute('required');
@ -959,6 +802,7 @@ function openComp(id, assigned_to, lead_id) {
document.getElementById('comp_id').value = id;
document.getElementById('lead_id').value = lead_id;
document.getElementById('f_assigned_to').value = assigned_to;
document.getElementById('string_flag').value = string_flag;
document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
document.getElementById('f_typeButtons').querySelectorAll('.f_activity_type')
.forEach(btn => btn.classList.remove('active'));
@ -971,7 +815,6 @@ function openOpportunityModal() {
openModal('opportunityModal');
}
function switchTab(tabName) {
//Default Activity Tab how to resset here
// 1. Hide all tab content
@ -993,260 +836,6 @@ function switchTab(tabName) {
document.getElementById('tab_' + tabName).classList.add('active');
}
// Add Lead Form Submissions
document.getElementById('addLeadForm').onsubmit = async (e) => {
e.preventDefault();
// Just grab the button variables first, DO NOT disable yet
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
const data = Object.fromEntries(new FormData(e.target).entries());
let companyName = data.company_name?.trim();
let email = data.email?.trim();
let phone = data.phone?.trim();
let companyRegex = /^[A-Za-z\s]+$/;
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
let phoneRegex = /^\+?[0-9\s]{10,20}$/; // Phone regex (+, numbers, spaces allowed, 10-20 length)
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
function showError(message, fieldName) {
toastr.warning(message, 'Validation Error');
setTimeout(function() {
$('[name="' + fieldName + '"]').focus();
}, 100);
}
// --- VALIDATION CHECKS (Button is still normal here) ---
if (!companyName && !email && !phone && !data.assigned_to) {
toastr.warning('Please fill in all the required fields.');
return;
}
if (!companyName) {
return showError('Company Name is required.', 'company_name');
}
if (!companyRegex.test(companyName)) {
return showError('Company Name can contain only letters and spaces.', 'company_name');
}
if (companyName.length < 2) {
return showError('Company Name must be at least 2 characters.', 'company_name');
}
if (!email && !phone) {
return showError("Either Email or Phone number is required.", 'email');
}
if (email && !emailRegex.test(email)) {
return showError("Please enter a valid email address.", 'email');
}
if (phone && !phoneRegex.test(phone)) {
return showError("Phone number can contain only +, numbers and spaces (1020 digits).", 'phone');
}
if (!data.status) {
return showError("Please select a status.", 'status');
}
if (!data.assigned_to) {
return showError("Please Select the user to be Assigned.", 'assigned_to');
}
// --- ALL VALIDATION PASSED: Now disable button ---
submitBtn.disabled = true;
submitBtn.innerText = 'Creating...';
try {
const res = await fetch(`${API}/leads`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if(res.ok) {
toastr.success('Leads Created Successfully');
closeModal('addActivityModal');
fetchActivities();
e.target.reset();
$(e.target).find('.searchable').trigger('change');
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
let isFirstError = true;
$('.form-control').removeClass('is-invalid');
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
let inputElement = $('[name="' + field + '"]');
if (inputElement.length > 0) {
inputElement.addClass('is-invalid');
if (isFirstError) {
setTimeout(function() { inputElement.focus(); }, 100);
isFirstError = false;
}
}
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
} else {
toastr.error(err.message || 'Error adding lead');
}
}
} catch (err) {
toastr.error("A network error occurred.");
} finally {
// ALWAYS runs to reset button
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
};
// Edit Lead Form Submissions
document.getElementById('editLeadForm').onsubmit = async (e) => {
e.preventDefault();
// Just grab the button variables first, DO NOT disable yet
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
const data = Object.fromEntries(new FormData(e.target).entries());
console.log("Lead :", data);
let leadId = data.lead_id?.trim();
let companyName = data.company_name?.trim();
let email = data.email?.trim();
let phone = data.phone?.trim();
let gst = data.gst_number?.trim();
let companyRegex = /^[A-Za-z\s]+$/;
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
let phoneRegex = /^\+?[0-9\s]{10,20}$/;
let gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/;
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
function showError(message, fieldName) {
toastr.warning(message, 'Validation Error');
setTimeout(function() {
$('[name="' + fieldName + '"]').focus();
}, 100);
}
// --- VALIDATION CHECKS (Button is still normal here) ---
if (!companyName && !email && !phone && !data.assigned_to) {
toastr.warning('Please fill in all the required fields.');
return;
}
if (!companyName) {
return showError('Company Name is required.', 'company_name');
}
if (!companyRegex.test(companyName)) {
return showError('Company Name can contain only letters and spaces.', 'company_name');
}
if (companyName.length < 2) {
return showError('Company Name must be at least 2 characters.', 'company_name');
}
if (!email && !phone) {
return showError("Either Email or Phone number is required.", 'email');
}
if (email && !emailRegex.test(email)) {
return showError("Please enter a valid email address.", 'email');
}
if (phone && !phoneRegex.test(phone)) {
return showError("Phone number can contain only +, numbers and spaces (1020 digits).", 'phone');
}
if (gst && !gstRegex.test(gst)) {
return showError("Please enter a valid GST Number (e.g., 22AAAAA0000A1Z5).", 'gst_number');
}
if (!data.status) {
return showError("Please select a status.", 'status');
}
if (!data.assigned_to) {
return showError("Please Select the user to be Assigned.", 'assigned_to');
}
// --- ALL VALIDATION PASSED: Now disable button ---
submitBtn.disabled = true;
submitBtn.innerText = 'Updating...';
try {
const res = await fetch(`${API}/leads/${leadId}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if(res.ok) {
toastr.success('Leads Updated Successfully');
closeModal('editLeadModal');
fetchActivities();
e.target.reset();
$('#savedContactsContainer').empty();
$('#NoData').show();
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
let isFirstError = true;
$('.form-control').removeClass('is-invalid');
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
let inputElement = $('[name="' + field + '"]');
if (inputElement.length > 0) {
inputElement.addClass('is-invalid');
if (isFirstError) {
setTimeout(function() { inputElement.focus(); }, 100);
isFirstError = false;
}
}
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
} else {
toastr.error(err.message || 'Error updating lead');
}
}
} catch (err) {
toastr.error("A network error occurred.");
} finally {
// ALWAYS runs to reset button
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
};
// Activity Form Submissions
document.getElementById('activityForm').onsubmit = async (e) => {
e.preventDefault();
@ -1256,6 +845,8 @@ document.getElementById('activityForm').onsubmit = async (e) => {
let actNotes = document.getElementById('act_notes').value.trim();
let actOwner = document.getElementById('act_owner').value.trim();
let actLead = document.getElementById('act_lead_id').value.trim();
let flag = document.getElementById('act_string_flag').value;
// 2. Perform Validation
if (!actLead) {
@ -1268,8 +859,13 @@ document.getElementById('activityForm').onsubmit = async (e) => {
return;
}
const hasDefaultActiveType = document.querySelector('#typeButtons .d_activity_type.active');
if (!hasDefaultActiveType) {
toastr.warning('Please select an activity type.');
return;
}
if (!selectedType) {
toastr.warning('Please select activity type');
toastr.warning('Please select an activity type.');
return;
}
@ -1311,7 +907,11 @@ document.getElementById('activityForm').onsubmit = async (e) => {
if (res.ok) {
toastr.success('Activity Created Successfully');
closeModal('activityModal');
viewDetail(document.getElementById('act_lead_id').value); // Make sure lead_id is passed correctly
if (flag === "frompopup") {
viewDetail(document.getElementById('act_lead_id').value);
} else {
window.location.reload();
}
} else {
toastr.error('Failed to create activity.');
}
@ -1330,6 +930,8 @@ document.getElementById('activityForm').onsubmit = async (e) => {
document.getElementById('completeForm').onsubmit = async (e) => {
e.preventDefault();
let flag = document.getElementById('string_flag').value; //"frompopup"
// 1. Grab button variables first, DO NOT disable yet
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
@ -1354,9 +956,14 @@ document.getElementById('completeForm').onsubmit = async (e) => {
let fDate = document.getElementById('f_date').value.trim();
let fAssigned = document.getElementById('f_assigned_to').value.trim();
const hasActiveType = document.querySelector('#f_typeButtons .f_activity_type.active');
if (!hasActiveType) {
toastr.warning('Please select the next activity type.');
return;
}
// FIX: Check your global variable, not the undefined DOM element
if (!selectedFollowUpActivityType) {
toastr.warning('Please Pickup Next Activity Type');
toastr.warning('Please select the next activity type.');
return;
}
@ -1425,7 +1032,13 @@ document.getElementById('completeForm').onsubmit = async (e) => {
// Final Actions
closeModal('completeModal');
if (flag === "frompopup") {
viewDetail(leadId);
} else if (flag === "frommain") {
window.location.reload();
}
e.target.reset();
selectedFollowUpActivityType = ''; // Reset global variable
@ -1458,6 +1071,7 @@ document.getElementById('completeForm').onsubmit = async (e) => {
submitBtn.innerText = originalBtnText;
}
};
function submitToRedirectwithactualLeadIDUrl(){
@ -1515,203 +1129,6 @@ document.getElementById('do_follow').addEventListener('change', function () {
}
});
document.getElementById('btnSaveContact').onclick = async (e) => {
// Use .value instead of .val()
let name = document.getElementById('contact_name').value.trim();
let mobile = document.getElementById('contact_mobile').value.trim();
let lead_id = document.getElementById('lead_id').value.trim();
if (!name || !mobile) {
return toastr.warning('Please enter both contact person name and mobile number.');
}
let payload = { lead_id: lead_id, name: name, mobile: mobile };
try {
const res = await fetch(`${API}/contacts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json(); // Get the response body
if (res.ok) {
// Clear inputs
document.getElementById('contact_name').value = '';
document.getElementById('contact_mobile').value = '';
// Hide "No Data" message
const noData = document.getElementById('NoData');
if(noData) noData.style.display = 'none';
// Create the HTML string
let contactHtml = `
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-2"
style="background: #f8f8f8; border-radius: 8px;"
data-id="${result.data.contact_id}">
<div>
<div class="fw-bold">${result.data.name}</div>
<div class="text-muted">${result.data.mobile}</div>
</div>
<button type="button"
class="btn btn-danger btn-sm btnRemoveContact"
data-id="${result.data.contact_id}">
Remove
</button>
</div>
`;
// <button type="button"
// class="btn btn-secondary btn-sm btnEditContact"
// data-id="${result.data.contact_id}">
// Update
// </button>
// Append to container using Vanilla JS
document.getElementById('savedContactsContainer').insertAdjacentHTML('beforeend', contactHtml);
toastr.success('Contact saved successfully');
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
let inputElement = $('[name="' + field + '"]');
if (inputElement.length > 0) {
// Make the field box turn red so the user sees it immediately
inputElement.addClass('is-invalid');
// Focus on the first error field
if (isFirstError) {
// The 100ms delay safely bypasses Bootstrap's modal focus block
setTimeout(function() {
inputElement.focus();
}, 100);
isFirstError = false;
}
}
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
}
else {
toastr.error(err.message || 'Error adding lead');
}
}
} catch (error) {
console.error(error);
toastr.error('An error occurred');
}
};
document.getElementById('savedContactsContainer').onclick = async (e) => {
// Check if the clicked element is a Remove button
if (e.target.classList.contains('btnRemoveContact')) {
const btn = e.target;
const contactId = btn.dataset.id; // Get data-id
const card = btn.closest('.contact-card'); // Find the parent card
try {
const res = await fetch(`${API}/contacts/${contactId}`, {
method: 'DELETE',
});
if (res.ok) {
card.remove(); // Remove from DOM
toastr.success('Contact removed successfully');
// Check if container is empty to show "No Data"
const container = document.getElementById('savedContactsContainer');
if (container.querySelectorAll('.contact-card').length === 0) {
const noData = document.getElementById('NoData');
if(noData) noData.style.display = 'block';
}
} else {
toastr.error('Failed to remove contact');
}
} catch (error) {
console.error(error);
toastr.error('An error occurred');
}
}
};
async function openEditLeadModal(id) {
// 1. Reset UI State
document.getElementById('hidden_lead_id').value = id;
const container = $('#savedContactsContainer');
// Remove only previous contact cards, keep the NoData span for now
container.find('.contact-card').remove();
try {
// 2. Fetch Lead Details
const response = await fetch(`${API}/leads/${id}`);
if (!response.ok) throw new Error('Failed to fetch lead');
const result = await response.json();
// IMPORTANT: Your data is nested inside result.data
const lead = result.data;
// 3. Populate Form Fields
const form = document.getElementById('editLeadForm');
// Mapping fields carefully
form.querySelector('[name="company_name"]').value = lead.company_name || '';
form.querySelector('[name="email"]').value = lead.email || '';
form.querySelector('[name="phone"]').value = lead.phone || '';
form.querySelector('[name="address"]').value = lead.address || '';
form.querySelector('[name="website"]').value = lead.website || '';
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
form.querySelector('[name="status"]').value = lead.status || 'New';
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
$(form.querySelector('[name="assigned_to"]')).trigger('change');
// 4. Handle Contact Persons (Looping through the nested array)
const contacts = lead.contact_persons; // Array from your JSON
if (contacts && contacts.length > 0) {
$('#NoData').hide();
contacts.forEach(contact => {
let contactHtml = `
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
style="background: #f8f8f8; border-radius: 8px;"
data-id="${contact.contact_id}">
<div>
<div class="fw-bold">${contact.name}</div>
<div class="text-muted">${contact.mobile}</div>
</div>
<button type="button"
class="btn btn-danger btn-sm btnRemoveContact"
data-id="${contact.contact_id}">
Remove
</button>
</div>`;
container.append(contactHtml);
});
} else {
$('#NoData').show();
}
// 5. Open the Modal
openModal('editLeadModal');
} catch (error) {
console.error("Error loading lead data:", error);
alert("Could not load lead details. Check console for details.");
}
}
function convertDBFormatted(input) {
if (!input) return null;
@ -1739,6 +1156,7 @@ function convertDBFormatted(input) {
seconds.padStart(2, '0')
);
}
fetchActivities();
</script>

View File

@ -9,6 +9,38 @@
.stat-change { font-size: 11px; margin-top: 8px; font-weight: 600; }
.text-success { color: #48bb78; }
.card-hero {
background: white;
padding: 25px;
border-radius: 12px;
border: 1px solid #edf2f7;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
position: relative;
overflow: hidden;
transition: transform 0.2s ease;
}
/* The Gradient Overlay Effect */
.card-hero::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px; /* Thin line at the top */
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
}
.card-hero:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
}
/* Optional: Subtle Background Gradient */
.card-hero.gradient-bg {
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
}
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; }
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
@ -29,10 +61,18 @@
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
.status-completed { background: #f0fff4; color: #38a169; }
.status-pending { background: #fffaf0; color: #dd6b20; }
.status-new { background: #e3f2fd; color: #1976d2; }
.status-potential { background: #fff3e0; color: #f57c00; }
.status-prospects { background: #e8f5e9; color: #388e3c; }
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
</style>
<div class="dash-container">
<div class="top-header">
<!-- <div class="top-header">
<div>
<h2 style="font-weight: 800; color: #1a202c; font-size: 24px;">Dashboard</h2>
<p style="color: #718096; font-size: 14px; margin-top: 4px;">Overview of your branch</p>
@ -40,36 +80,41 @@
<div style="position: relative;">
<input type="text" placeholder="Search leads, activities....." style="background:white; padding:12px 20px; border-radius:10px; border:1px solid #e2e8f0; width: 350px; font-size: 13px;">
</div>
</div>
</div> -->
<div class="stat-cards">
<div class="card">
<div class="stat-val">4</div>
<div class="card-hero">
<div class="stat-val"><?php echo $total_leads; ?></div>
<div class="stat-label">Total Leads</div>
<div class="stat-change text-success"> 12% this month</div>
<!-- <div class="stat-change text-success"> 12% this month</div> -->
</div>
<div class="card">
<div class="stat-val">6</div>
<div class="card-hero">
<div class="stat-val"><?php echo $total_acts ?></div>
<div class="stat-label">Total Activities</div>
<div class="stat-change text-success"> 5% this month</div>
<!-- <div class="stat-change text-success"> 5% this month</div> -->
</div>
<div class="card">
<div class="stat-val">1</div>
<div class="card-hero">
<div class="stat-val"><?php echo $total_pending_acts ?></div>
<div class="stat-label">Pending Activities</div>
<!-- <div class="stat-change text-success"> 15% this month</div> -->
</div>
<div class="card-hero">
<div class="stat-val"><?php echo $total_completed_acts ?></div>
<div class="stat-label">Completed Activities</div>
<div class="stat-change text-success"> 15% this month</div>
<!-- <div class="stat-change text-success"> 15% this month</div> -->
</div>
<div class="card">
<!-- <div class="card">
<div class="stat-val">₹15.0L</div>
<div class="stat-label">Pipeline Value</div>
<div class="stat-change text-success"> 18% this month</div>
</div>
</div> -->
</div>
<div class="main-grid">
<div class="card">
<div class="table-header">
<h3 style="font-size: 16px; font-weight: 700;">Recent Activities - All Team</h3>
<span style="color: #718096; font-size: 12px; font-weight: 600;">6 activities</span>
<h3 style="font-size: 16px; font-weight: 700;">Pending Activities - All Team</h3>
<span style="color: #718096; font-size: 12px; font-weight: 600;"><?= count($pending_acts) . ' ' . (count($pending_acts) == 1 ? 'activity' : 'activities') ?></span>
</div>
<table>
<thead>
@ -81,70 +126,136 @@
</tr>
</thead>
<tbody>
<?php if (!empty($pending_acts)): ?>
<?php foreach ($pending_acts as $a): ?>
<?php
$activityIcons = [
'Call' => '📞',
'Email' => '✉️',
'Meeting' => '📅',
'Visit' => '🚗',
'Demo' => '🖥️',
'Share' => '📄',
'Todo' => '✓'
];
$icon = $activityIcons[$a['activity_type']] ?? '📌';
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($a['scheduled_date']));
?>
<tr>
<td><strong>Venba Infotech</strong></td>
<td><strong><?= esc($a['company_name']) ?></strong></td>
<td>
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">CALL</div>
<div style="font-size: 12px; color: #718096;">John Doe</div>
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">
<?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?>
</div>
<div style="font-size: 12px; color: #718096;">
<?= esc($a['assigned_to_name'] ?? 'ID: ' . $a['assigned_to']) ?>
</div>
<!-- <div style="font-size: 12px; color: #718096;">
<?= esc($a['notes'] ?? 'ID: ' . $a['assigned_to']) ?>
</div> -->
</td>
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
<td><span class="status-pill status-completed">Completed</span></td>
<td style="color: #4a5568; font-weight: 500;"><?= $formattedscheduledDate ?></td>
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td><strong>Acme Corporation</strong></td>
<td>
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">EMAIL</div>
<div style="font-size: 12px; color: #718096;">John Doe</div>
<td colspan="4">
<div class="empty-state">
<div class="empty-icon"></div> No activities found
</div>
</td>
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
<td><span class="status-pill status-pending">Pending</span></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<div>
<div class="card">
<h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px;">Sales Team Performance</h3>
<div class="team-member">
<div class="member-img" style="background: #ff6b35;">V</div>
<div style="flex: 1;">
<div style="font-weight: 700; font-size: 14px;">Venkat 3.0</div>
<div style="font-size: 11px; color: #718096;">Sales Manager</div>
</div>
<div class="act-stats">
<div class="total">6 acts</div>
<div class="done">1 done</div>
</div>
</div>
<div class="team-member">
<div class="member-img" style="background: #4a5568;">P</div>
<div style="flex: 1;">
<div style="font-weight: 700; font-size: 14px;">Pavi_the_Staff V</div>
<div style="font-size: 11px; color: #718096;">Sales Staff</div>
</div>
<div class="act-stats">
<div class="total">0 acts</div>
<div class="done">0 done</div>
</div>
<?php foreach ($team as $member) {
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
$fullName = $member['first_name'] . ' ' . $member['last_name'];
$totalActs = $member['total_acts'];
$doneActs = $member['done_acts'];
$role = $member['role']; // fix your DB first
// Random or consistent color based on name
$colors = ['#ff6b35', '#667eea', '#48bb78', '#ed8936', '#9f7aea'];
$colorIndex = abs(crc32($member['first_name'])) % count($colors);
$color = $colors[$colorIndex];
echo "
<div class='team-member'>
<div class='member-img' style='background: {$color};'>{$firstLetter}</div>
<div style='flex: 1;'>
<div style='font-weight: 700; font-size: 14px;'>{$fullName}</div>
<div style='font-size: 11px; color: #718096;'>{$role}</div>
</div>
<div class='act-stats'>
<div class='total'>{$totalActs} acts</div>
<div class='done'>{$doneActs} done</div>
</div>
</div>";
} ?>
<div class="card breakdown-card">
<h3 style="font-size: 15px; font-weight: 700; margin-bottom: 20px;">Activity Breakdown</h3>
<div class="breakdown-item"><span><span class="dot" style="background: #ff6b35;"></span> Call</span><strong>42%</strong></div>
<!-- <div class="breakdown-item"><span><span class="dot" style="background: #ff6b35;"></span> Call</span><strong>42%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #48bb78;"></span> Email</span><strong>25%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #4299e1;"></span> Meeting</span><strong>18%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #ecc94b;"></span> Visit</span><strong>10%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #9f7aea;"></span> Demo</span><strong>5%</strong></div>
<div class="breakdown-item"><span><span class="dot" style="background: #9f7aea;"></span> Demo</span><strong>5%</strong></div> -->
<?php
$activityConfig = [
'Call' => ['color' => '#ff6b35', 'icon' => '📞'],
'Email' => ['color' => '#48bb78', 'icon' => '✉️'],
'Meeting' => ['color' => '#4299e1', 'icon' => '📅'],
'Visit' => ['color' => '#ecc94b', 'icon' => '🚗'],
'Demo' => ['color' => '#9f7aea', 'icon' => '🖥️'],
'Share' => ['color' => '#ed8936', 'icon' => '📄'],
'Todo' => ['color' => '#718096', 'icon' => '✓'],
];
?>
<?php if (!empty($activity_breakdown)): ?>
<?php foreach ($activity_breakdown as $item): ?>
<?php
$type = $item['activity_type'];
$color = $activityConfig[$type]['color'] ?? '#718096';
$icon = $activityConfig[$type]['icon'] ?? '📌';
?>
<div class="breakdown-item">
<span>
<span style="background: <?= $color ?>;"></span>
<?= $icon ?> <?= esc($type) ?>
</span>
<strong><?= $item['percentage'] ?>%</strong>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-state">
<div class="empty-icon"></div> No activities found
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="card">
<div class="card" style="grid-column: 1 / -1; width: 100%; box-sizing: border-box;">
<div class="table-header">
<h3 style="font-size: 16px; font-weight: 700;">All Leads Overview</h3>
<span style="color: #718096; font-size: 12px; font-weight: 600;">Click a lead to see details</span>
<h3 style="font-size: 16px; font-weight: 700;">All Leads Overview <?php echo !empty($leads_overview) ? "<i>(".count($leads_overview).")</i>" : ""; ?></h3>
<!-- <span style="color: #718096; font-size: 12px; font-weight: 600;">Click a lead to see details</span> -->
<a href="<?= base_url('sales') ?>"
style="color: #718096; font-size: 12px; font-weight: 600; text-decoration: none; padding: 4px 8px; transition: color 0.2s ease; display: inline-block; cursor: pointer;"
onmouseover="this.style.color='#ff6b35';"
onmouseout="this.style.color='#718096';">
Click a lead to see details
</a>
</div>
<table>
<thead>
@ -152,25 +263,31 @@
<th>Company</th>
<th>Status</th>
<th>Assigned To</th>
<th>Activities</th>
<th>Opportunities</th>
<th style="text-align: center !important;">Activities</th>
<th style="text-align: center !important;">Opportunities</th>
</tr>
</thead>
<tbody>
<?php if (!empty($leads_overview)): ?>
<?php foreach ($leads_overview as $lead): ?>
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
<tr>
<td><strong>Venba Infotech</strong></td>
<td><span style="background: #edf2f7; color: #4a5568; font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700; text-transform: uppercase;">New</span></td>
<td style="color: #4a5568;">John Doe</td>
<td><div style="text-align: center; font-weight: 700;">1</div></td>
<td><div style="text-align: center; font-weight: 700;">1</div></td>
<td><strong><?= esc($lead['company_name']) ?></strong></td>
<td><span class="status-pill status-<?= $statusClass ?>"><?= esc($lead['status']) ?></span></td>
<td><?= esc($lead['assigned_to'] ?? 'Unassigned') ?></td>
<td><div style="text-align: center; font-weight: 700;"><?= $lead['activities'] ?></div></td>
<td><div style="text-align: center; font-weight: 700;"><?= $lead['opportunities'] ?></div></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td><strong>Acme Corporation</strong></td>
<td><span style="background: #fff3e0; color: #f57c00; font-size: 10px; padding: 3px 10px; border-radius: 12px; font-weight: 700; text-transform: uppercase;">Potential</span></td>
<td style="color: #4a5568;">John Doe</td>
<td><div style="text-align: center; font-weight: 700;">1</div></td>
<td><div style="text-align: center; font-weight: 700;">0</div></td>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon">👤</div> No leads found
</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -19,6 +19,52 @@
.lead-initial { width: 35px; height: 35px; background: #f0f0f0; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #ff6b35; }
.status-pill { font-size: 10px; padding: 2px 10px; border-radius: 12px; font-weight: 600; }
/* Modals */
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 2000; align-items: center; justify-content: center; }
.modal.active { display: flex; }
.modal-content { background: white; border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; display: flex; flex-direction: column; }
.modal-header { padding: 5px 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 25px; flex: 1; }
.form-group { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
input, select, textarea { background-color: white !important; border-radius: 6px !important; box-shadow: none !important; border-color: #ddd !important; }
input, select, textarea { width: 400px; padding: 10px 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; }
input:focus, select:focus, textarea:focus { border-color: #ff6b35; outline: none; /* removes blue browser outline */ }
.modal .form-control { transition: 0.2s ease; }
.modal .form-control:focus { border-color: #ff6b35; box-shadow: 0 0 0 2px rgba(255, 107, 53, 0.2);
outline: none;
}
.btn-primary { background: #ff6b35; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #ff5722; transform: translateY(-1px); }
.btn-complete { background: #4caf50; color: white; border: none;padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
.btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
.btn-view:hover { background: #f0f0f0; transform: translateY(-1px); }
/* Fix for the grid layout on smaller screens */
@media (max-width: 992px) {
.dashboard-grid { grid-template-columns: 1fr; }
}
/* Fix for inputs inside modal rows */
/* input, select, textarea {
width: 100%; * Change from 400px to 100% *
box-sizing: border-box; * Ensures padding doesn't add to width *
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
} */
</style>
<div class="my-dash">
@ -70,27 +116,57 @@
</div>
</div>
<div class="dashboard-grid">
<div class="list-card">
<div style="display:flex; justify-content:space-between; margin-bottom:15px; align-items: center;">
<h3 style="font-size:16px; font-weight: 700; margin: 0;">My Upcoming Activities</h3>
<span style="color:#ff6b35; font-size:12px; font-weight: 600;"><?= $acts['pending'] ?> pending</span>
</div>
<?php foreach($upcoming as $u): ?>
<!-- <?php foreach($upcoming as $u): ?>
<div class="list-item">
<div>
<div style="font-weight:600; font-size:14px; color: #333;"><?= $u['company_name'] ?></div>
<div style="font-size:11px; color:#999; margin-top: 3px;"><?= strtoupper($u['activity_type'] ?? 'EMAIL') ?> - <?= date('d Mar Y', strtotime($u['scheduled_date'])) ?></div>
</div>
<button class="badge-done"> Mark as completed</button>
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)"> Mark as completed</button>
</div>
<?php endforeach; ?> -->
<?php foreach($upcoming as $u):
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share' => '📄', 'Todo' => '✓' ];
$icon = $activityIcons[$u['activity_type']] ?? '📌';
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($u['scheduled_date']));
?>
<div class="list-item" style="display: flex; align-items: center; gap: 15px;">
<div class="lead-initial" style="flex-shrink: 0;">
<?= $icon ?>
</div>
<div style="flex-grow: 1; display: flex; flex-direction: column; min-width: 0;">
<div style="font-weight: 700; font-size: 14px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<?= esc($u['company_name']) ?>
</div>
<div style="font-size: 11px; color: #999; margin-top: 2px; white-space: nowrap;">
<span style="color: #ff6b35; font-weight: 600;"><?= strtoupper($u['activity_type'] ?? 'EMAIL') ?></span>
- <?= $formattedscheduledDate ?>
</div>
</div>
<div style="flex-shrink: 0;">
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>, <?= $u['lead_id'] ?>)">
Mark as completed
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<div class="list-card">
<h3 style="font-size:16px; font-weight: 700; margin-bottom:20px;">My Recent Leads</h3>
<?php foreach($recent_leads as $rl): ?>
<div class="list-item">
<!-- <div class="list-item">
<div style="display:flex; gap:12px; align-items:center;">
<div class="lead-initial"><?= substr($rl['company_name'], 0, 1) ?></div>
<div>
@ -99,26 +175,176 @@
</div>
</div>
<span class="status-pill" style="background: #e3f2fd; color: #1976d2;"><?= $rl['status'] ?></span>
</div> -->
<div class="list-item" style="display: flex; align-items: center; gap: 15px; padding: 12px 0; border-bottom: 1px solid #f9f9f9;">
<div style="flex-shrink: 0;">
<div class="avatar-title"
style="background-color: #f1f3fa; width: 42px; height: 42px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #1976d2; font-weight: 700; font-size: 16px;">
<?php
$firstLetter = !empty($rl['company_name']) ? substr($rl['company_name'], 0, 1) : '?';
echo strtoupper(esc($firstLetter));
?>
</div>
</div>
<div style="flex-grow: 1; min-width: 0;">
<div style="font-weight: 600; font-size: 14px; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<?= esc($rl['company_name']) ?>
</div>
<div style="font-size: 11px; color: #999; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<?= esc($rl['email']) ?>
</div>
</div>
<div style="flex-shrink: 0;">
<span class="status-pill"
style="background: #e3f2fd; color: #1976d2; padding: 4px 10px; border-radius: 12px; font-size: 10px; font-weight: 600; white-space: nowrap;">
<?= esc($rl['status']) ?>
</span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="modal" id="completeModal">
<div class="modal-content" style="max-width: 675px;">
<div class="modal-header">
<h3>Complete Activity</h3>
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">&times;</button>
</div>
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
<hr class="my-0">
<div class="modal-body p-4">
<input type="hidden" id="comp_id">
<input type="hidden" id="lead_id">
<div class="form-group">
<label class="form-label">Outcome / Notes <span class="text-danger">*</span></label>
<textarea id="comp_notes" class="search-input" style="width:100%; height:80px;" required placeholder="What happened? Any next steps?" rows="3" required></textarea>
</div>
</div>
<div class="modal-footer">
<div class="form-group text-right mb-0">
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('completeModal')">Cancel</button>
<button type="submit" class="btn-primary">Submit Outcome</button>
</div>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const API = '<?= base_url('sales') ?>';
function openModal(id) { document.getElementById(id).classList.add('active'); }
function openComp(id, lead_id) {
document.getElementById('completeForm').reset();
document.getElementById('comp_id').value = id;
document.getElementById('lead_id').value = lead_id;
openModal('completeModal');
}
document.getElementById('completeForm').onsubmit = async (e) => {
e.preventDefault();
// 1. Grab button variables first, DO NOT disable yet
const submitBtn = e.target.querySelector('button[type="submit"]');
const originalBtnText = submitBtn.innerText;
// 2. Capture values into variables
const activityId = document.getElementById('comp_id').value;
const leadId = document.getElementById('lead_id').value;
// This creates a TRUE or FALSE boolean
let compNotes = document.getElementById('comp_notes').value.trim();
// --- VALIDATION CHECKS (Button is still normal here) ---
if (compNotes === '') {
toastr.warning('Completion notes is required');
return;
}
// --- ALL VALIDATION PASSED: Now disable button ---
submitBtn.disabled = true;
submitBtn.innerText = 'Submitting...';
try {
const payload = {
completion_notes: compNotes, // Use the already trimmed variable
create_followup: 'no',
};
// Mark current activity complete
const res = await fetch(`${API}/activities/${activityId}/complete`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (res.ok) {
toastr.success('Updated Successfully');
// Final Actions
closeModal('completeModal');
e.target.reset();
window.location.reload();
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
if (!seenMessages.includes(message)) {
errorMessages += `• ${message}<br>`;
seenMessages.push(message);
}
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
}
} else {
toastr.error(err.message || 'Error completing activity');
}
}
} catch (err) {
toastr.error("A network error occurred.");
} finally {
// ALWAYS runs to reset button
submitBtn.disabled = false;
submitBtn.innerText = originalBtnText;
}
};
function closeModal(id) {
const modal = document.getElementById(id);
if (modal) {
modal.classList.remove('active'); // Hide the modal
const form = modal.querySelector('form'); // Find the form inside this specific modal
}
}
document.addEventListener('DOMContentLoaded', function () {
const params = new URLSearchParams(window.location.search);
const fy = params.get('fy');
if (fy) {
document.getElementById('financial_year').value = fy;
}
});
});
function onFinancialYearChange(select) {
function onFinancialYearChange(select) {
const url = new URL(window.location.href);
url.searchParams.set('fy', select.value);
window.location.href = url.toString();
}
}
</script>

View File

@ -185,7 +185,8 @@
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Phone</label>
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')">
<!-- <input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')"> -->
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
</div>
</div>
<div class="form-row mb-1">
@ -276,15 +277,17 @@
<div class="modal-body p-4">
<input type="hidden" id="act_lead_id">
<label class="form-label">Activity Type</label>
<label class="form-label">Activity Type <span class="text-danger">*</span></label>
<div class="activity-types" id="typeButtons">
<button type="button" class="activity-type-btn active" onclick="selectType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn" onclick="selectType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn" onclick="selectType('Demo', this)">🎬 Demo</button>
<button type="button" class="activity-type-btn" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn" onclick="selectType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn d_activity_type active" data-type="Call" onclick="selectType('Call', this)">📞 Call</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Email" onclick="selectType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)"> To Do</button>
</div>
<div class="form-group">
<label class="form-label">Notes <span class="text-danger">*</span></label>
@ -350,7 +353,7 @@
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Email', this)">✉️ Email</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🎬 Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Todo', this)"> To Do</button>
@ -500,7 +503,8 @@
</div>
<div class="col-md-12" style="width: 94%;">
<label class="form-label">Phone</label>
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')">
<!-- <input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" oninput="this.value = this.value.replace(/[^\d\s+]/g, '')"> -->
<input type="text" class="form-control" name="phone" style="width: 100%;" placeholder="Enter the Phone Number" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
</div>
</div>
<div class="col-12 mb-1">
@ -650,9 +654,9 @@ function closeModal(id) {
// Specific cleanup for your "Activity" logic
if (id === 'activityModal') {
selectedType = 'Call'; // Reset your global activity type variable
$('.activity-type-btn').removeClass('active'); // Remove 'active' from all activity buttons
$(`.activity-type-btn[data-type="${selectedType}"]`).addClass('active'); // Find the specific button for 'Call' and make it active
selectedType = 'Call';
$('#typeButtons .d_activity_type').removeClass('active');
$('#typeButtons .d_activity_type[data-type="Call"]').addClass('active'); // ✅
}
// Specific cleanup for "Complete" modal (hidden follow-up sections)
@ -666,7 +670,7 @@ function closeModal(id) {
// Specific action for leadDetailModal
if (id === 'leadDetailModal') {
switchTab('activity'); // Reset the tab back to 'activity'
document.getElementById('btn_add_opportunity').classList.style.display = 'none'; // Hide the button
document.getElementById('btn_add_opportunity').style.display = 'none';
}
}
}
@ -932,7 +936,7 @@ function renderCard(opps) {
}
function renderTimeline(acts) {
const cont = document.getElementById('timelineContainer');
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🎬", Share: "📄", Todo: "" };
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🖥️", Share: "📄", Todo: "" };
if (acts.length === 0) {
cont.classList.add('no-line');
@ -1042,7 +1046,8 @@ document.getElementById('addLeadForm').onsubmit = async (e) => {
let companyRegex = /^[A-Za-z\s]+$/;
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
let phoneRegex = /^\+?[0-9\s]{10,20}$/; // Phone regex (+, numbers, spaces allowed, 10-20 length)
// let phoneRegex = /^\+?[0-9\s]{0,10}$/; // Phone regex (+, numbers, spaces allowed, 10-20 length)
let phoneRegex = /^\d{10}$/;
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
function showError(message, fieldName) {
@ -1079,7 +1084,8 @@ document.getElementById('addLeadForm').onsubmit = async (e) => {
}
if (phone && !phoneRegex.test(phone)) {
return showError("Phone number can contain only +, numbers and spaces (1020 digits).", 'phone');
// return showError("Phone number can contain only +, numbers and spaces (1020 digits).", 'phone');
return showError("Phone number can contain only numbers (10 digits)", 'phone');
}
if (!data.status) {
@ -1168,7 +1174,8 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
let companyRegex = /^[A-Za-z\s]+$/;
let emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
let phoneRegex = /^\+?[0-9\s]{10,20}$/;
// let phoneRegex = /^\+?[0-9\s]{10,20}$/;
let phoneRegex = /^\d{10}$/;
let gstRegex = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/;
// 1. HELPER FUNCTION: Shows Toastr and focuses the specific field
@ -1206,7 +1213,8 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
}
if (phone && !phoneRegex.test(phone)) {
return showError("Phone number can contain only +, numbers and spaces (1020 digits).", 'phone');
// return showError("Phone number can contain only +, numbers and spaces (1020 digits).", 'phone');
return showError("Phone number can contain only numbers (10 digits)", 'phone');
}
if (gst && !gstRegex.test(gst)) {
@ -1294,9 +1302,13 @@ document.getElementById('activityForm').onsubmit = async (e) => {
toastr.warning('Notes is required');
return;
}
const hasDefaultActiveType = document.querySelector('#typeButtons .d_activity_type.active');
if (!hasDefaultActiveType) {
toastr.warning('Please select an activity type.');
return;
}
if (!selectedType) {
toastr.warning('Please select activity type');
toastr.warning('Please select an activity type.');
return;
}
@ -1337,6 +1349,11 @@ document.getElementById('activityForm').onsubmit = async (e) => {
if (res.ok) {
toastr.success('Activity Created Successfully');
selectedType = 'Call'; // Reset your global activity type variable
// Remove 'active' from all activity buttons
$('.activity-type-btn').removeClass('active');
// Add 'active' to the specific button using backticks for the template literal
$(`.activity-type-btn[onclick*="'${selectedType}'"]`).addClass('active');
closeModal('activityModal');
viewDetail(document.getElementById('act_lead_id').value); // Make sure lead_id is passed correctly
} else {
@ -1382,8 +1399,13 @@ document.getElementById('completeForm').onsubmit = async (e) => {
let fAssigned = document.getElementById('f_assigned_to').value.trim();
// FIX: Check your global variable, not the undefined DOM element
const hasActiveType = document.querySelector('#f_typeButtons .f_activity_type.active');
if (!hasActiveType) {
toastr.warning('Please select the next activity type.');
return;
}
if (!selectedFollowUpActivityType) {
toastr.warning('Please Pickup Next Activity Type');
toastr.warning('Please select the next activity type.');
return;
}
// if (!selectedFollowUpActivityType) {
@ -1564,7 +1586,9 @@ document.getElementById('do_follow').addEventListener('change', function () {
}
});
document.getElementById('btnSaveContact').onclick = async (e) => {
const btnSaveContact = document.getElementById('btnSaveContact');
if (btnSaveContact) {
btnSaveContact.onclick = async (e) => {
// Use .value instead of .val()
let name = document.getElementById('contact_name').value.trim();
let mobile = document.getElementById('contact_mobile').value.trim();
@ -1661,8 +1685,11 @@ document.getElementById('btnSaveContact').onclick = async (e) => {
toastr.error('An error occurred');
}
};
}
const savedContactsContainer = document.getElementById('savedContactsContainer');
if (savedContactsContainer) {
savedContactsContainer.onclick = async (e) => {
document.getElementById('savedContactsContainer').onclick = async (e) => {
// Check if the clicked element is a Remove button
if (e.target.classList.contains('btnRemoveContact')) {
const btn = e.target;
@ -1693,6 +1720,7 @@ document.getElementById('savedContactsContainer').onclick = async (e) => {
}
}
};
}
async function openEditLeadModal(id) {
// 1. Reset UI State

View File

@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "gdrive-demo-394007",
"private_key_id": "5b1d856b0c5b13e52b5210d381ce7ae02204f666",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9tqkwe8XEuvDJ\ngDAKHn7FzFgkmor9sEZPkjofVJ1dK0RpD1mMVw38BzzMsEo8Y8aojNKj9FcgJPI+\nMiSwvoHDVGxPuyz2Q7o8BS7WdM83Sn69CBGn+0s+YjsyQ5pQBeu1YZ3erjckyA1M\nX0a0qXSFRm7dN0pwDIk0SP9/pl5iAKEtakuXn/Q+lSIpHz6OUgQy8bCjn91poMtF\nhL1YR/7k8ZttjQiFSCTzQ6e3IJmVbqY9UZ2hc4zVicLVSLM2x17M1CCVrsXoceOj\nQoYEeCqP2qJjGA29CB66xpXetuOFcll/ZHiNBhSekOBbJIKFG8WfTKaFn4hu2HX6\nzbJLse5FAgMBAAECggEAXK8qxWcS3eQ+0xLvZWI0qUoGHgvqr7o4/5L/FmNuZiBH\nUdSP+UJmsKSQjafq/Mn6OkpidntfPXMPbldtGXRZTSanq+RUORQpnj0h/uAehHK+\nrHeOuLTKs/Wl2g6xCzt5RqokSLBwfGXIKXG6x3SqWppoe2cR1OArAAJR4PlUzyeM\nP0HkZcVMDrXkshpbi/7yk1Yol5CTJjUrXT4cH2eFSih+eu5UxI/uEdxu86XnaB+V\nBDS94nSQffaMem3YLRSQpPWMHJts3NM2eoxpVy3NqbyHH0Jzr47T5+pdnk5AZX8v\nMu7L0FYgUmGli8W9/jV43lUi9z147EpNC1ugySICGQKBgQDeT3i9mBO1HQB+3y05\n4ao9YKWtR7RHTIiMBkekRs58xxWIq75+bsJtLMy0GsHOJHbpdfDU2AtcasYXVm/L\nc960AR7Qm0mdhQi5CG7XfFTvkc+RhsFoCAYOnbdInr1D+s7NsyOgdYvzh9HYIiJ/\nm8MzeGQia/4tlO+UNq7UK0sfowKBgQDadpe0OCynXEiziSY/aBT9mrjplXJSJUeR\nX5/pUrBV++mYt+LJU0Q+4op0Qf+PUJwp72O1v3T9h4ox2BcrYUMI17dJZ5HG46BG\n6Sjh+mZzLCTN9L6AVgRzK/5CUpsL+oPpClzGQK+1uJ+YKZD086YBuQi2JiG6uBxk\n7VLzATZ49wKBgQC3ZHgGb95SGoq+Hv4AMdluqLwEJpLh/pDmcofHTWIqLVHmXUfY\npSZfSgXUzf3zQMGX9mOmMlOs+ahQuE2hWQTvGb2B+ZjRCV4Yxowp17d5qp/BPZlv\naK8Wf6Ujk1AvNEhGCPHq/Q1m6TSDSCWNf8GYREjW3J/immrJqhKvlMd0YQKBgQCM\nf2CpQsdVCwCmljnG5YU6ZFsvvjE7q0YPtFP/lnJZmh1tXjW4DJkDaGZqxlc5MDp+\nrbqOlIcE1jqGO9cKyw51jWYPC1CxfIsDj8f/LS7eOzGgUxqBJtDN0SlANigI2CAl\nq8hmqAtY71eUYIcdQeUtjnaPzo46q1V3gzmplsoVmQKBgDCzaBbFY1bKW+xPPC98\nutYS7cGYtTq241zafSXx/qmpZB2QwFsR3rmZ9msCEBb3TY4Ew+hUi7+SP3ycSU1s\nkwIO/9DaZhFBRA9jbwbu4zdB2Niamfo79epqJ8vhJ6TA2d5xRSGbHWkGymWV/e0p\na74mh5hQ0lqI4MSeArFrJwwF\n-----END PRIVATE KEY-----\n",
"client_email": "gsheet@gdrive-demo-394007.iam.gserviceaccount.com",
"client_id": "108808196910972902964",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gsheet%40gdrive-demo-394007.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -1,13 +0,0 @@
{
"type": "service_account",
"project_id": "nhance-app-4fedb",
"private_key_id": "e764472bb4a0c9226d172f7aebdd2eb785c5fca8",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzyKk6yQqLZHnL\n8+qRwMHtIhV3kR6bYuCol7LTU/aRyTZpFafewJ/6y5bMvvHaRSaohuuTjDhp5H+w\nf7DAiWO06C/YdgZ/tv3dPM8FnkOEnwxUXVgJskK+srJ4g6zsYkT+0DNI0qMJRW5m\n0jv+HKPJ0axCAPcUZUVtraV/HFHF7pJp4oWMFD5X7Jhv7kbW3TetJofSN3SvRvr8\nyfCiVGzqgF0Gx0Tgd25GTMOuRXTGoTlQeDHFMm19g8YY1de0Ol9WqFey6E8e6a/h\nw8KHGdywgsI+q5yF0EIUdxKc6U9Bea25k0YnKHSCsJjJTVlKW+xlxR6wA1yuBb66\n2dAhEzldAgMBAAECggEACEssKpb5dadVPdt+PJ2r58CueD1JfRL8i/p98hhjY9bo\niJEXSFDfI6aYFp22ZYKfWNK3wMimr5SuPIB5F6C4gn3b+y95ERPQtg/AkyDgB39M\nBjZNW4EnabAWFWlc2FJknjnd+JcmLnhoy+h/gpnKoOkbYISDFKQWJfJtRvLE/1on\nhKvnhEkO++93HZp6KPYA21BQIWt6q6gYjjjwxQIHE0Vg30bZC8ciNVaVlmOnpoKW\nlsQ3+TL+b6vDsSxez4hSqWlua7wWL6JgKvyGw6FGiDLGjvTEvVQ0cvfykCDmO5td\nWvVihTMOMXEMx+iVAUOXJLrSPOFGXxSHL9jilqCn0QKBgQDu4P64sE5T15rA+83B\neRMyQUfLru/oxyQ7pAbgRk4nwQj1mXcdD3/RpHF6jg0rt0msRjkzkqyWiJN0qsZ9\nlQY7h0I8pu8dxFhbd7LLcPaNV+OpNAokG06dBK8qhuMcZBlRfvJ3ye5L/Dk0Z7A4\nPw6/OxySns+kzfxtPdpJTNeqFQKBgQDAq2BXm+MsjqjZm2Z1LFMgZcjRLUu4ylMC\n9W3jqd35dBypcafAJ3TqhPLsx+HyWuC+GLMWohx3XGU7oZabGg1zuWAS8A4hnogZ\nk47OT3OYHDaT65HFmSAGJy1UFVvESUdiVzPqO3Xn9dYvRIw6By2b09w+kD1oxmE0\nvAh5UFAMKQKBgHg06plfxxqzzWE9lS290qYgaZOaxYla5OXKRdeIKX4hynNktab/\nDLAfUyd74i7UfhNeBxznu0fJFILKCTZazpcYGoHQ81UEX/4vPt7XSoqX5q1XzZ6b\nyzSCje8Vj6XSzVbQTg1vpSXBl7vCTdAQE0ix40/48L6bFWKXlIf8Ti59AoGAd8UR\nKuT5H6W/SSbVVlmrgyRC5eWmgMUlPV2cZj4egwevGZZRlZ3xjCgBazgGOUZNF177\nVUKJN5n0RFF68ggL/LhsBNm9ryCBsoSL7axuS0yekO2LvK4nvit0fiSY2zhCa9uR\npjY4YW3tK40NLrVvoMLe0vWPdyZ7HvEUw3UtjaECgYAMoonkNACD7EyWesAQzjpD\ntAL5ByXtFkMqMMr8ELLoDU2D5+qzb6gu0+GuYzGvxtYQHz2L+cP7yonn8IRBrAO1\njZCi2Mn1qOYecSGBnv6rotcagMALXwExkymjjVxPLiOae7A01sxSFDpqtRBww2JA\ngFsHNMpkQ1KmZu7LW0q1fg==\n-----END PRIVATE KEY-----\n",
"client_email": "only-drive-access@nhance-app-4fedb.iam.gserviceaccount.com",
"client_id": "116214376249781337850",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/only-drive-access%40nhance-app-4fedb.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "nhance-ee8d1",
"private_key_id": "e3c5269b1ec7d22e7a297f5273edf3eda6010c0a",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC4+7yRGBAow7GR\nrcBSwXVkBd7U001YC8nZ6GmbfhZSktn4pd4C7r/VGmJMcHdo4CVkt+omY9hEWy3Y\n2k7xvwTBJOxo+A4GENduzUpRucVD5UNs4CKajVmI97FV+2wgeLYR9lLswrSXkQvo\nCRDi/w0EwQ7ZdfPyYfuUUBSdAytiwtGHEJaHBQ4qanNvstFu6FJcCkaq+Sbn6Ool\nHN7lgS/V5NhuBakNOm8EBBaN8Xp13afjHvyFEHalnY8xpNNXNVlBljYIPzYFxXFN\nFL/Iz7XX8dBD0tVwfQGDpD5OCPh8FeIg39dgbRxU3Q+Awv3CX9eoEW1j3bPYo7Lw\nmh8K3fohAgMBAAECggEAEQXPCfxQmSrwOPg0zEjHDTyb3DKGGdw0p772YChy7gfB\nudrfR2s1SWtWo8y7tSTfVaZPcvEmnzYgZ3LB56EAIBs0pP1viO/20aVgBlpI99Q6\ntEobiEs9DViOpkHVs71EzMb1Emcugde6sUOLg5NPkHu8BDErERSJBFJ3bliy0heN\nwAwYKXCB3ro3ryUE5+pRS6ibt8vLEBbk3T3bEW8A1wfWnUo0V+Wf5KXsCO8BvD8Z\nE/i0JgpAkZksWs+keBSLbdWBONNV1GrbxezoZXA8aPPlcaD0bZdLDlszXguq8DGO\nbjr0SI7Hx78/jAQhiFkdNMkLvZMIoU7VVnQSibsBKwKBgQD04PMi6CO6kpKG/ZDw\nR6tuAU8DkUgSjvOquUeGkguyfhK/G5fg3w8yCpLAPoh8ZEGL4zZy+ZoAsHWLv44O\nmXjTOKut5o1puswM8vPobDAinddCWs6U6VWKkkRw27AuyKcnLCpvnRBg/Iw+lSTQ\nzG0KCd8Zn2+a0Cjo9hlF6F2pAwKBgQDBYmvV+cADTRjhostukDK71aKsRhMu1OH4\nRK2uX3f9rde1aSnIcp0PDzn24V7I9skT6zhOCPTF6SEFH35eO3z7kOA26hokOMb2\nz7Bh+EDnx5zQmWwcj0yNwbCeTl7gKWz2o3AuMc8zK6I2+buYJJnjoazUyqQtKhqz\nnPs3woY9CwKBgDHjwqVR9jWEtyWZc3YApAR9b8OiTbS6OxqFNPVNu+RZmygkTwUZ\nbNcdIFjaZKQzKMd/OxChmaaaTNhz5lVDH0KpQRDk79qim//nX5nysLvcvIZgScY8\n45ifxCHaIELnzmZEsUCcF0IrMcduS1nezDhHWpS1zt8TmcIcoXmEpdBRAoGAEtKs\nomAz415evJ+m43Ufqw7JTbFobpeEAzFUInPibwu7wkmhKoSVawDVaIVZP4Bd5BVy\nHo5anOTrNN9y4mMx8B6S5GV28+2e2CkxBuguESFpzxgP4NvF3MpskYwZSgJeO8d+\nxNBOVbG6kVVPgCiX3gM/mlq7DjZZ8P+nqC7D+C8CgYAMeN/ByvGaYSN5bVVntOcT\noDiMWfBBToc9irhNSZsCwghXmNugPKguDbRTi7n/Iqf/HljwUx0HBtXkIgZvDDiW\nQrGSCozd9g2WFx/2OSJv4GU+eUM1GFr0mLFDaAUNzvjwHTkG+2STof8VowaGRx68\n7Z50l5hXKv3i0n5ykhPWUg==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com",
"client_id": "105441165052433248287",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-mcdfe%40nhance-ee8d1.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}