diff --git a/app/Config/Acl.php b/app/Config/Acl.php index 80f7a2bd..2960595f 100644 --- a/app/Config/Acl.php +++ b/app/Config/Acl.php @@ -17,6 +17,7 @@ class Acl '#^/getVerifiedPosUserData#' => ['public' => true], '#^/swagger#' => ['roles' => [ADMIN_ROLE_ID]], '#^/getEmployeeActiveOrInactivePolicy#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], + '#^/bulkEcardDownloadAsZip#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/sheet#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/sendextraparam#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], '#^/test/testingquerys#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]], @@ -28,6 +29,11 @@ class Acl '#^/util/view_log#' => ['roles' => [ADMIN_ROLE_ID]], '#^/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], diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 6c031ae7..b219e60e 100755 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -69,7 +69,7 @@ class Filters extends BaseConfig 'before' => [ 'HttpRequestLog' => ['except' => 'cli/*'], 'Cors', - 'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','employeeRest/*','processjob','getCommission']], + 'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','employeeRest/*','processjob','getCommission','downloadEmployeeEcardZip']], 'SecurityInputFilter' => ['except' => ['/client/notification/create','/ticket/crud_mail_template/*','test_mail','leads/sendMail'] ], 'GlobalPostFileUploadGuard' // 'csrf', diff --git a/app/Config/Routes.php b/app/Config/Routes.php index fc5ee993..d9a05733 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -33,6 +33,7 @@ $routes->get('/fedeploy', 'DeployController::fedeploy_view', ['filter' => 'authM $routes->post('/fedeploy', 'DeployController::fedeploy', ['filter' => 'authMVC']); $routes->get('/visitOffBoardCheck', 'EmployeeController::visitOffBoardCheck'); $routes->get('/metaDashboardDemo', 'TestingController::metaDashboardDemo'); +$routes->get('/metaTpaDashboardDemo', 'TestingController::metaTpaDashboardDemo'); // Reminder Mail Notification @@ -88,6 +89,7 @@ $routes->get('claim-form-download/(:any)', 'TicketController::downloadClaimForm/ $routes->get('downloadClaimFile/(:any)', 'TicketController::downloadClaimFile/$1'); $routes->match (['get','post'],"claims-feedback-form/(:any)/(:any)", "TicketController::viewClaimFeedbackForm/$1/$2"); $routes->match (['get','post'],"claims-feedback-form/(:any)", "TicketController::viewClaimFeedbackForm/$1"); +$routes->get('downloadEmployeeEcardZip/(:any)', 'EmployeeController::downloadEmployeeEcardZip/$1'); @@ -417,7 +419,7 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('getTheEmpDataForClaim/(:any)', 'ClientController::getTheEmpDataForClaim/$1'); // $routes->get('getTheEmpDataForClaimSearchByMobile/(:any)', 'ClientController::getTheEmpDataForClaimSearchByMobile/$1'); $routes->get('getTheEmpDataForClaimSearchByMobile', 'ClientController::getTheEmpDataForClaimSearchByMobile'); - $routes->get('getLeadNonEB/(:any)', 'LeadsController::getLeadNonEB/$1'); + $routes->get('getLeadNonEB/(:any)/(:any)', 'LeadsController::getLeadNonEB/$1/$2'); $routes->get('getPolicyTypeFields', 'LeadsController::getPolicyTypeFields'); $routes->get('removeMultiFile', 'LeadsController::removeMultiFile'); $routes->get('removeInstallments', 'LeadsController::removeInstallments'); @@ -670,6 +672,7 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->post("hrFileUpload", "EmployeeRestController::hrFileUpload"); $routes->post("updateHrFileUploadData", "EmployeeRestController::updateHrFileUploadData"); $routes->post("getHrDashboad", "EmployeeRestController::getHrDashboad"); + $routes->post("getHrTpaDashboard", "EmployeeRestController::getHrTpaDashboard"); //thz_master's @@ -707,6 +710,7 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get("downloadSampleExcel/(:any)", "EmployeeController::downloadSampleExcelFile/$1"); }); +$routes->post("bulkEcardDownloadAsZip", "EmployeeRestController::bulkEcardDownloadAsZip"); $routes->get("hrFileDownload", "EmployeeRestController::hrFileDownload"); $routes->post("hrFileList", "EmployeeRestController::hrFileList"); $routes->get("getEmployeeActiveOrInactivePolicy", "EmployeeRestController::getEmployeeActiveOrInactivePolicy"); @@ -909,6 +913,10 @@ $routes->group('sales', function($routes) { $routes->get('/', 'SalesController::index'); + $routes->get('loadactivities', 'SalesController::loadactivities'); + + $routes->get('page/(:segment)', 'SalesController::noPage/$1'); + // Get all leads with filters $routes->get('leads', 'SalesController::getLeads'); @@ -989,6 +997,7 @@ $routes->group('sales', function($routes) { //Dashboard + $routes->get('dashboard', 'SalesController::dashboard'); $routes->get('branchLevelDashboard', 'SalesController::branchLevelDashboard'); $routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard'); }); diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index 4e7ddef7..01664e76 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -41,6 +41,7 @@ use App\Controllers\EmployeeServiceController; use App\Controllers\EmpDataServiceController; use App\Models\LeadsModel; use App\Models\ThzMasterModel; +use App\Models\UserActivityHistoryModel; use CodeIgniter\API\ResponseTrait; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -78,6 +79,7 @@ class EmployeeController extends AdminController protected $userModel; protected $partnerEndorsementRequestModel; protected $LevelContactModel; + protected $UserActivityHistoryModel; public function __construct() { @@ -102,6 +104,7 @@ class EmployeeController extends AdminController $this->userModel = new userModel(); $this->partnerEndorsementRequestModel = new PartnerEndorsementRequestModel(); $this->LevelContactModel = new LevelContactModel(); + $this->UserActivityHistoryModel = new UserActivityHistoryModel(); } public function list() @@ -4862,8 +4865,133 @@ class EmployeeController extends AdminController return $mail_response; } - public function downloadZip(){ + + public function bulkEcardDownloadAsZipFromS3New($params) + { + try { + + $limit = $params['limit'] ?? 100; + $last_id = $params['last_id'] ?? 0; + $zip_id = $params['zip_id']; + + $employee_data = $this->employeePolicyModel->getEmployeeDataWithPolicyUsingClientPolicyIdOrEmployeePolicyIds($params, $limit, $last_id); + + if (empty($employee_data)) { + // No more records, move to Zipping phase + return Jobs::addJob(['job_name' => 'getEmployeeEcardFromTmpFolderAndZipToS3', 'payload' => $params]); + } + + $tempPath = FCPATH . 'tmp/' . $params['folder_name'] . '/'; + if (!is_dir($tempPath)) mkdir($tempPath, 0777, true); + + $s3 = \Config\Services::getS3Service(); + $current_last_id = $last_id; + $pdf_count = 0; + + foreach ($employee_data as $emp) { + $current_last_id = $emp['emp_policy_id']; + + + $s3_key = 'ecard_' . $emp['name'] . '(' . $emp['emp_code'] . ')' . '_' . $emp['tpa_id'] . '.pdf'; + $s3_key = $this->sanitizeFilePart($s3_key); + + if ($s3->exists($s3_key)) { + $s3_url = $s3->getPresignedUrl($s3_key); + $pdf_content = file_get_contents($s3_url['url']); + + if ($pdf_content !== false) { + file_put_contents($tempPath . $s3_key, $pdf_content); + $pdf_count++; + } + } + } + + // Check if we need more batches + $params['last_id'] = $current_last_id; + $params['pdf_count'] = $pdf_count; + $params['batch_no'] = ($params['batch_no'] ?? 1) + 1; + + if (count($employee_data) == $limit) { + return Jobs::addJob(['job_name' => 'bulkEcardDownloadAsZipFromS3', 'payload' => $params]); + } else { + return Jobs::addJob(['job_name' => 'getEmployeeEcardFromTmpFolderAndZipToS3', 'payload' => $params]); + } + + } catch (\Throwable $e) { + $this->logAndNotifyError($e, $params); + } + } + + public function getEmployeeEcardFromTmpFolderAndZipToS3New($params) + { + try { + $zipService = new \App\Libraries\ZipService(); + $source = FCPATH . 'tmp/' . $params['folder_name']; + $zipName = $params['folder_name'] . '.zip'; + $s3_dest_key = 'exports/zips/' . $zipName; + + // 1. Create Zip and Upload + $result = $zipService->zipAndUploadS3($source, $s3_dest_key); + + if ($result['status']) { + // 2. IMPORTANT: Update the Activity Record with the S3 Key + $history = $this->UserActivityHistoryModel->find($params['zip_id']); + $misc = json_decode($history['misc_data'], true); + $misc['status'] = 'completed'; + $misc['s3_key'] = $s3_dest_key; + + $this->UserActivityHistoryModel->update($params['zip_id'], [ + 'misc_data' => json_encode($misc) + ]); + + $params['url'] = base_url('downloadEmployeeEcardZip/' . md5($params['zip_id'])); + } + + // 3. Cleanup local files + $this->recursiveRemoveDir($source); + + // 4. Email HR + return $this->sendMailToHrWithZipAttachments($params); + + } catch (\Throwable $e) { + $this->logAndNotifyError($e, $params); + } + } + + public function downloadEmployeeEcardZip($md5Id) + { + // Search by MD5 of ID + $data = $this->UserActivityHistoryModel->where('MD5(CAST(id AS CHAR))', $md5Id)->first(); + + if (!$data) return "File link expired or invalid."; + + $misc = json_decode($data['misc_data'], true); + $s3Key = $misc['s3_key'] ?? null; + + if (!$s3Key) return "File is still processing or failed to generate."; + + $s3 = \Config\Services::getS3Service(); + $presigned = $s3->getPresignedUrl($s3Key, 15); // 15 mins expiry + + return redirect()->to($presigned['url']); + } + + private function logAndNotifyError($e, $params) + { + $this->myLogger->logme('error', "[ECARD_ZIP]: " . $e->getMessage()); + $params['url'] = null; // Forces "Failed" email + $this->sendMailToHrWithZipAttachments($params); + } + + private function recursiveRemoveDir($dir) + { + if (!is_dir($dir)) return; + $files = array_diff(scandir($dir), array('.', '..')); + foreach ($files as $file) { + (is_dir("$dir/$file")) ? $this->recursiveRemoveDir("$dir/$file") : unlink("$dir/$file"); + } + return rmdir($dir); } } diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index fb17d59a..5c87df6c 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -63,6 +63,7 @@ use App\Models\ClaimFilesModel; use App\Models\PolicyTransactionModel; use App\Models\TicketMailTemplateModel; use App\Models\TpaApiSeviceModel; +use App\Models\UserActivityHistoryModel; use Composer\Pcre\Preg; use Kreait\Firebase\Factory; use Kreait\Firebase\Messaging\CloudMessage; @@ -106,6 +107,7 @@ class EmployeeRestController extends AdminController protected $ticketMailTemplateModel; protected $employeeRetailPolicy; protected $batchFileModel; + protected $UserActivityHistoryModel; public function __construct() @@ -141,6 +143,7 @@ class EmployeeRestController extends AdminController $this->hrFileUploadModel = new HrFileUploadModel(); $this->ticketMailTemplateModel = new TicketMailTemplateModel(); $this->batchFileModel = new BatchFileModel(); + $this->UserActivityHistoryModel = new UserActivityHistoryModel(); } @@ -5498,9 +5501,15 @@ class EmployeeRestController extends AdminController // 🔐 Move this to .env in real projects $METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY'); + $is_tpa_dashboard_enable = $this->clientPolicyModel + ->select('tpa.dashboard_id') + ->join('tpa', 'client_policy.tpa_id = tpa.id') + ->where('client_policy.id', $received_data['client_policy_id']) + ->where('tpa.dashboard_id IS NOT NULL') + ->first(); + $payload = [ 'resource' => [ - // 'dashboard' => 1 'dashboard' => 2 ], 'params' => (object)['client_policy' => $received_data['client_policy_id']], // MUST be object for Metabase @@ -5520,8 +5529,10 @@ class EmployeeRestController extends AdminController 'status' => 'success', 'message' => 'Form data received successfully!', 'data' => [ - 'metabaseToken' => $token, - 'metabaseUrl' => 'https://nsights.nhanceindia.in'] + 'metabaseToken' => $token, + 'metabaseUrl' => 'https://nsights.nhanceindia.in' + ], + 'is_tpa_dashboard_enable' => !empty($is_tpa_dashboard_enable ?? []), ]); // } @@ -5530,6 +5541,73 @@ class EmployeeRestController extends AdminController 'metabaseUrl' => 'https://nsights.nhanceindia.in', ]); } + + public function getHrTpaDashboard($client_policy_id = null, $return_type = 'api') + { + $METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY'); + + if ($return_type === 'api') { + + $received_data = $this->request->getJSON(true) ?? []; + $policy_id = $received_data['client_policy_id'] ?? null; + + } else { + // Internal call + $policy_id = $client_policy_id; + } + + if (empty($policy_id)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'Client Policy ID is required.', + 'data' => [] + ]); + } + + $client_policy_data = $this->clientPolicyModel + ->select('tpa.dashboard_id') + ->join('tpa', 'client_policy.tpa_id = tpa.id') + ->where('client_policy.id', $policy_id) + ->first(); + + $database_id = isset($client_policy_data['dashboard_id']) ? (int) $client_policy_data['dashboard_id'] : null; + + if (empty($database_id)) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'There is no dashboard for this TPA.', + 'data' => [] + ]); + } + + $payload = [ + 'resource' => [ + 'dashboard' => $database_id + ], + 'exp' => time() + (10 * 60), + 'params' => (object)[] + ]; + + $token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256'); + + if ($return_type === 'internal') { + + // Return raw data for internal usage + return [ + 'metabaseToken' => $token, + 'metabaseUrl' => 'https://nsights.nhanceindia.in' + ]; + } + + return $this->respond([ + 'status' => 'success', + 'message' => 'Dashboard fetched successfully.', + 'data' => [ + 'metabaseToken' => $token, + 'metabaseUrl' => 'https://nsights.nhanceindia.in' + ] + ]); + } // get policy files @@ -5648,5 +5726,55 @@ class EmployeeRestController extends AdminController } + public function bulkEcardDownloadAsZipNew() + { + $received_data = $this->request->getJSON(true) ?? []; + + if (empty($received_data['client_policy_id']) && empty($received_data['emp_policy_ids'])) { + return $this->respond(['status' => false, 'message' => 'client_policy_id or employee_policy_ids is required'], 400); + } + + if (empty($received_data['hr_id'])) { + return $this->respond(['status' => false, 'message' => 'hr_id is required'], 400); + } + + $employee_data = $this->employeePolicyModel->getEmployeeDataWithPolicyUsingClientPolicyIdOrEmployeePolicyIds($received_data, 1); + + if (empty($employee_data)) { + return $this->respond(['status' => false, 'message' => 'No employee data found'], 404); + } + + // Define naming convention + $folderName = 'bulk_ecards_' . ($employee_data[0]['client_short_name'] ?? 'export') . '_' . date('Ymd_His'); + + $insert_data = [ + 'user_id' => $received_data['hr_id'], + 'user_type' => 'hr', + 'activity' => 'download_employees_ecard_zip', + 'misc_data' => json_encode([ + 'folder_name' => $folderName, + 'status' => 'pending', + 's3_key' => null + ]) + ]; + + $zip_id = $this->UserActivityHistoryModel->insert($insert_data); + + $payload = array_merge($received_data, [ + 'zip_id' => $zip_id, + 'folder_name' => $folderName, + 'batch_no' => 1, + 'last_id' => 0, + 'limit' => 100 + ]); + + // Dispatch job + Jobs::addJob(['job_name' => 'bulkEcardDownloadAsZipFromS3', 'payload' => $payload]); + + return $this->respond([ + 'status' => true, + 'message' => 'Process started. You will receive an email with the download link shortly.' + ], 200); + } } diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index b4eaac06..2c2d9bcf 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -363,8 +363,9 @@ class LeadsController extends BaseController public function createLead() { - // print_r($this->request->getPost()); die; $id = $this->request->getPost('id'); + $actual_lead_id = $this->request->getPost('actual_lead_id'); + $actual_lead_id = !empty($actual_lead_id) ? $actual_lead_id : null; $postData = $this->request->getPost(); $data = $this->prepareLeadData(); $rules = [ @@ -4349,8 +4350,10 @@ class LeadsController extends BaseController } // get lead data for edit both EB and NON-EB - public function getLeadNonEB($type, $id = null) + public function getLeadNonEB($type, $actual_lead_id = null ,$id = null) { + // Convert '0' to null so it doesn't break your existing DB checks + $actual_lead_id = ($actual_lead_id == 0 || $actual_lead_id == '0') ? null : $actual_lead_id; // Set basic data $data = [ 'issuer' => $this->issuer, @@ -4365,6 +4368,7 @@ class LeadsController extends BaseController 'gpaClaimType' => $this->claim_type_for_gpa, 'causeOfDeath' => $this->cause_of_death, 'selected_lead_type' => $type, + 'actual_lead_id' => $actual_lead_id, ]; // Fetch sales team members who are active in team 5 diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index f79fd170..3de595f8 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -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,17 +27,104 @@ class SalesController extends BaseController $this->contactModel = new SalesContactPersonModel(); $this->activityModel = new SalesActivityModel(); $this->noteModel = new SalesLeadNoteModel(); + $this->userModel = new UserModel(); } + public function index() { - $db = \Config\Database::connect(); - // Fetch users for the assignment dropdowns - $data['users'] = $db->table('user_profiles') - ->select('id, first_name, last_name') - ->where('is_active', 1) - ->get()->getResultArray(); + $data = $this->getSalesStaffData(); + + $data['tab_name'] = 'Leads'; + $data['page_name'] = 'Leads'; + return $this->loadLayout('sales/tracker_view', $data); + } - $this->loadLayout('sales/tracker_view', $data); + public function loadactivities(){ + $data = $this->getSalesStaffData(); + + $data['tab_name'] = 'Activities'; + $data['page_name'] = 'Activities'; + + $data['leads'] = $this->leadModel->select('lead_id, company_name') + ->orderBy('lead_id', 'DESC') + ->findAll(); + + + return $this->loadLayout('sales/activity_view', $data); + } + + /** + * HELPER: Fetches Sales Managers based on the logged-in user's role and branch + */ + private function getSalesStaffData(): array + { + $db = \Config\Database::connect(); + $logged_user_id = get_session_userid(); + $role = get_role_id(); + $team_id = user_team(); + + $data = [ + 'users' => [], + 'sales_manager_ids' => [] + ]; + + $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; + + // Is the logged-in user a Sales Manager? (Role 4, Team 5) + if ($role == 4 && in_array(5, $team_id)) { + $data['sales_manager_ids'] = [$logged_user_id]; + + $data['users'] = [ + [ + 'id' => $row->id, + 'first_name' => $row->first_name, + 'nhance_branch_id' => $nhance_branch_id + ] + ]; + + } + // Otherwise fetch ALL sales managers in this branch + elseif (in_array($role,[1,2,3,4,5])) { + + $data['users'] = $db->table('user_profiles up') + ->select('up.id, up.first_name, up.last_name, up.nhance_branch_id') + ->join('user_teams ut', 'ut.user_id = up.id') + ->where('up.is_active', 1) + ->where('ut.is_active', 1) + ->where('up.role', 4) + ->where('ut.team_id', 5) + ->where('up.nhance_branch_id', $nhance_branch_id) + ->get() + ->getResultArray(); + + $data['sales_manager_ids'] = array_column($data['users'], 'id'); + } + + return $data; + } + + public function noPage($type = null) + { + + $page_type = $type ?? 'Page'; + + $data['tab_name'] = 'Sales ' . $page_type; + $data['page_name'] = 'Sales ' . $page_type; + + echo view('layout/header', $data); + echo '
+
+
+

Oops! ' . esc($page_type) . ' is under construction.

+ Error Image +
+
+
'; + echo view('layout/footer', $data); } public function completeActivity($id) { @@ -50,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'], @@ -60,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()); } @@ -371,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); } @@ -450,6 +546,9 @@ class SalesController extends BaseController if (!$this->activityModel->insert($data)) { 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); @@ -457,7 +556,8 @@ class SalesController extends BaseController 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); @@ -672,12 +772,60 @@ class SalesController extends BaseController } - // ==================== Dashboard ==================== + // ==================== 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,[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) { + $data['tab_name'] = 'Dashboard'; + $data['page_name'] = 'Dashboard'; // Hardcoded branch ID as requested - $branchId = 1; + // $branchId = 1; try { // 1. Lead Statistics @@ -692,11 +840,24 @@ class SalesController extends BaseController // 3. Team Performance (Aggregating activity counts per user) $db = \Config\Database::connect(); + // Final safe check + if (empty($sales_manager_ids)) { + // No valid IDs — skip queries or return empty + $teamPerformance = []; + $recentActivities = []; + $leadsOverview = []; + } else { + + if (empty($sales_manager_ids) || !is_array($sales_manager_ids)) { + $sales_manager_ids = array_filter((array) $sales_manager_ids); // removes null, "", 0 + } + $teamPerformance = $db->table('user_profiles as u') ->select('u.first_name, u.last_name, u.profile as 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') ->where('u.nhance_branch_id', $branchId) + ->whereIn('u.id', $sales_manager_ids) ->where('u.is_active', 1) ->get()->getResultArray(); @@ -706,11 +867,48 @@ class SalesController extends BaseController ->orderBy('sales_activities.scheduled_date', 'DESC') ->limit(6) ->findAll(); + + $sales_manager_ids = array_values(array_map('intval', $sales_manager_ids)); + $recentActivities = $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') + ->join('(SELECT lead_id, MAX(company_name) AS company_name FROM sales_actual_leads GROUP BY lead_id) sal','sal.lead_id = sa.lead_id','left') + ->join('user_profiles up', 'up.id = sa.assigned_to', 'left') + ->orderBy('sa.scheduled_date', 'DESC') + ->limit(6) + ->whereIn('sa.assigned_to', $sales_manager_ids) + ->get()->getResultArray(); + + // 5. 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(); + } + + // 6. activityBreakdown + $total = $db->table('sales_activities')->countAll(); + + $activityBreakdown = $db->table('sales_activities') + ->select("activity_type, COUNT(*) AS total, ROUND(COUNT(*) * 100.0 / {$total}, 0) AS percentage", false) + ->groupBy('activity_type') + ->orderBy('total', 'DESC') + ->get() + ->getResultArray(); + + $teamPerformance = array_filter($teamPerformance, function($row) { + return ($row['total_acts'] + $row['done_acts']) > 0; + }); - // 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(); $data = [ 'total_leads' => $stats['total'], @@ -719,7 +917,10 @@ class SalesController extends BaseController 'pipeline_value' => '15.0L', // Hardcoded placeholder from PDF [cite: 14] 'team' => $teamPerformance, 'recent_acts' => $recentActivities, - 'leads_overview' => $leadsOverview + 'leads_overview' => $leadsOverview, + 'activity_breakdown'=> $activityBreakdown, + 'tab_name' => "Sales Dashboard", + 'page_name' => "Sales Dashboard" ]; // dd($data); @@ -733,22 +934,45 @@ class SalesController extends BaseController } } - - public function salesManagerLevelDashboard() + public function salesManagerLevelDashboard($userId,$sales_manager_ids, $payload = []) { - $userId = 1; + // $userId = get_session_userid(); + // $userId = 1; $db = \Config\Database::connect(); try { + + // $payload = $this->request->getGet(); + $current_fin_year = $payload['fy'] ?? getCurrentFinancialYear(); + + + $fin_years = $db->table('sales_target') + ->select('fy_year') + ->where('user_id', $userId) + ->orderBy('fy_year', 'desc') + ->get() + ->getResultArray(); + + $fin_years = array_column($fin_years, 'fy_year'); + + if(empty($fin_years)){ + $fin_years[] = $current_fin_year; + } + $target = $db->table('sales_target') ->where('user_id', $userId) - ->where('fy_year', '2025-2026') - ->get()->getRowArray(); + ->where('fy_year', $current_fin_year) + ->get() + ->getRowArray(); + + $targetAmount = $target['target_amount'] ?? 0.00; + + // get achieved amount from leads table + $achievedAmount = $this->getUserAchievedAmount($current_fin_year, $userId); - $targetAmount = $target['target_amount'] ?? 2500000.00; - $achievedAmount = 600000.00; $remainingAmount = $targetAmount - $achievedAmount; - $achievementPercent = ($targetAmount > 0) ? round(($achievedAmount / $targetAmount) * 100) : 0; + // $achievementPercent = ($targetAmount > 0) ? round(($achievedAmount / $targetAmount) * 100) : 0; + $achievementPercent = ($targetAmount > 0) ? min(100, round(($achievedAmount / $targetAmount) * 100)) : 0; $activitySummary = [ 'total' => $this->activityModel->where('assigned_to', $userId)->countAllResults(), @@ -779,9 +1003,15 @@ class SalesController extends BaseController 'lead_count' => $myLeadsCount, 'upcoming' => $upcomingActivities, 'recent_leads' => $recentLeads, - 'user_name' => session()->get('first_name') ?? 'John Doe' + 'fin_years' => $fin_years, + 'display_fin_years' => format_financial_year($current_fin_year), + 'user_name' => get_session_userdata()->first_namee ?? '', + 'tab_name' => "Sales Dashboard", + 'page_name' => "Sales Dashboard" ]; + // dd($data); + // return view('sales/my_dashboard_view', $data); $this->loadLayout('sales/sales_manager_level_dashboard', $data); @@ -789,4 +1019,107 @@ class SalesController extends BaseController return $this->failServerError($e->getMessage()); } } + + public function getUserAchievedAmount($financialYear, $userId) + { + // Split the string into two years + $years = explode('-', $financialYear); + $startYear = $years[0]; // 2025 + $endYear = $years[1]; // 2026 + + // Create the timestamps + $startFY = $startYear . '-04-01 00:00:00'; + $endFY = $endYear . '-03-31 23:59:59'; + + $achievedAmountData = $this->leadModel + ->select('SUM(leads.premium_amount) as achieved_amount') + ->join('leads', 'sales_actual_leads.lead_id = leads.actual_lead_id') + ->where('sales_actual_leads.assigned_to', $userId) + ->where('leads.status', 'won') + ->where('leads.updated_at >=', $startFY) + ->where('leads.updated_at <=', $endFY) + ->findAll(); + + 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() + ]; + } + } } \ No newline at end of file diff --git a/app/Controllers/TestingController.php b/app/Controllers/TestingController.php index 098c2105..daae4293 100644 --- a/app/Controllers/TestingController.php +++ b/app/Controllers/TestingController.php @@ -4,6 +4,7 @@ namespace App\Controllers; use App\Controllers\BaseController; use App\Models\EmployeePolicyModel; +use App\Models\ClientPolicyModel; use App\Models\InsurerBranchModel; use App\Models\RFQModel; use CodeIgniter\HTTP\ResponseInterface; @@ -23,10 +24,12 @@ class TestingController extends BaseController { use ResponseTrait; protected $myLogger; + protected $ClientPolicyModel; public function __construct() { $this->myLogger = \Config\Services::mylogger(); + $this->ClientPolicyModel = new ClientPolicyModel(); } public function saveForm() @@ -1060,4 +1063,59 @@ class TestingController extends BaseController return $this->respond(['status' => 'failed', 'code' => '500' , 'message' => 'Error: ' . $e->getMessage()], 200); } } + + public function metaTpaDashboardDemo() + { + $METABASE_SECRET_KEY = getenv('METABASE_SECRET_KEY'); + $policy_id = $this->request->getGet('client_policy') ?? 4687; + + $client_policy_data = $this->ClientPolicyModel + ->select('tpa.dashboard_id') + ->join('tpa', 'client_policy.tpa_id = tpa.id') + ->where('client_policy.id', $policy_id) + ->first(); + + $database_id = isset($client_policy_data['dashboard_id']) ? (int) $client_policy_data['dashboard_id'] : null; + + if (empty($database_id)) { + + if ($this->request->getGet('api') == 1) { + return $this->respond([ + 'status' => 'failed', + 'message' => 'There is no dashboard for this TPA.', + 'data' => [] + ]); + } + + return view('errors/404', [ + 'message' => 'There is no dashboard for this TPA.' + ]); + } + + $payload = [ + 'resource' => [ + 'dashboard' => $database_id + ], + 'exp' => time() + (10 * 60), // 10 minutes + 'params' => (object)[] + ]; + + $token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256'); + + if ($this->request->getGet('api') == 1) { + return $this->respond([ + 'status' => 'success', + 'message' => 'Form data received successfully!', + 'data' => [ + 'metabaseToken' => $token, + 'metabaseUrl' => 'https://nsights.nhanceindia.in' + ] + ]); + } + + return view('meta_dashboard_demo_one', [ + 'metabaseToken' => $token, + 'metabaseUrl' => 'https://nsights.nhanceindia.in', + ]); + } } diff --git a/app/Helpers/utility_helper.php b/app/Helpers/utility_helper.php index 467f0338..efc2842e 100755 --- a/app/Helpers/utility_helper.php +++ b/app/Helpers/utility_helper.php @@ -1210,7 +1210,7 @@ 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'); @@ -1251,7 +1251,7 @@ if (!function_exists('map_relationship')) { } - function recordRateLimitFailure(string $context = 'authApi'): void + function recordRateLimitFailure(string $context = 'authApi'): void { /** @var IncomingRequest $request */ $request = \Config\Services::request(); @@ -1276,4 +1276,64 @@ if (!function_exists('map_relationship')) { } + if (!function_exists('getCurrentFinancialYear')) { + function getCurrentFinancialYear() + { + // Get current year and month + $date = new DateTime(); + $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) { + $startYear = $currentYear - 1; + $endYear = $currentYear; + } else { + $startYear = $currentYear; + $endYear = $currentYear + 1; + } + + return $startYear . '-' . $endYear; + } + } + + + if (!function_exists('format_financial_year')) { + function format_financial_year(string $financialYear): string + { + if (empty($financialYear) || !str_contains($financialYear, '-')) { + return $financialYear; + } + + $years = explode('-', $financialYear); + + // Ensure we have both parts + $startYear = $years[0] ?? ''; + $endYear = $years[1] ?? ''; + + return "APR " . $startYear . " - MAR " . $endYear; + } + } + + + 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']; + } + + try { + $response = $calendar->createEvent($data); + return ['status' => 'success', 'code' => '200' , 'message' => 'Follow-up Saved', 'response' => $response]; + } catch (\Exception $e) { + return ['status' => 'failed', 'code' => '500' , 'message' => 'Error: ' . $e->getMessage()]; + } + } + } + + diff --git a/app/Models/EmployeePolicyModel.php b/app/Models/EmployeePolicyModel.php index 908d1698..8171df0c 100755 --- a/app/Models/EmployeePolicyModel.php +++ b/app/Models/EmployeePolicyModel.php @@ -2260,9 +2260,12 @@ class EmployeePolicyModel extends Model employees.id as emp_id, tpa.short_name, employee_polices.tpa_id, - client_policy.policy_no + client_policy.policy_no, + clients.client_name as client_name, + clients.short_name as client_short_name ') ->join('client_policy', 'client_policy.id = employee_polices.client_policy_id') + ->join('clients', 'clients.id = client_policy.client_id') ->join('employees', 'employees.id = employee_polices.employee_id') ->join('tpa', 'tpa.id = client_policy.tpa_id') ->where([ diff --git a/app/Models/LeadsModel.php b/app/Models/LeadsModel.php index 1c028645..72ac2f59 100644 --- a/app/Models/LeadsModel.php +++ b/app/Models/LeadsModel.php @@ -10,6 +10,7 @@ class LeadsModel extends Model protected $primaryKey = 'id'; protected $allowedFields = [ 'id', + 'actual_lead_id', 'lead_type', 'issuer', 'client_type', diff --git a/app/Models/SalesActivityModel.php b/app/Models/SalesActivityModel.php index 80b8aef6..8d82097f 100644 --- a/app/Models/SalesActivityModel.php +++ b/app/Models/SalesActivityModel.php @@ -38,9 +38,9 @@ class SalesActivityModel extends Model // Validation protected $validationRules = [ 'lead_id' => 'required|integer', - 'activity_type' => 'required|in_list[Call,Email,Meeting,Demo,Share,Todo]', + 'activity_type' => 'required|in_list[Call,Email,Meeting,Demo,Share,Todo,Visit]', 'notes' => 'required', - 'scheduled_date' => 'required|valid_date', + 'scheduled_date' => 'required', 'assigned_to' => 'required|integer', 'status' => 'in_list[pending,completed]', ]; @@ -80,35 +80,63 @@ class SalesActivityModel extends Model */ public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0) { - $builder = $this->select('sales_activities.*, actual_leads.company_name, user_profiles.first_name as assigned_to_name') - ->join('actual_leads', 'actual_leads.lead_id = sales_activities.lead_id', 'left') + $this->select('sales_activities.*, sales_actual_leads.company_name, user_profiles.first_name as assigned_to_name') + ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left') ->join('user_profiles', 'user_profiles.id = sales_activities.assigned_to', 'left'); if (!empty($filters['status'])) { - $builder->where('sales_activities.status', $filters['status']); + $this->where('sales_activities.status', $filters['status']); } if (!empty($filters['activity_type'])) { - $builder->where('sales_activities.activity_type', $filters['activity_type']); + $this->where('sales_activities.activity_type', $filters['activity_type']); } + // if (!empty($filters['assigned_to'])) { + // $this->where('sales_activities.assigned_to', $filters['assigned_to']); + // } if (!empty($filters['assigned_to'])) { - $builder->where('sales_activities.assigned_to', $filters['assigned_to']); + $assigned_to = $filters['assigned_to']; + + // If it's already an array, use it. If it's a string, explode it into an array. + $assignedToIds = is_array($assigned_to) ? $assigned_to : explode(',', $assigned_to); + + // Now it is guaranteed to be an array, making whereIn perfectly safe + $this->whereIn('sales_actual_leads.assigned_to', $assignedToIds); } + + if (!empty($filters['date_from'])) { - $builder->where('sales_activities.scheduled_date >=', $filters['date_from']); + $this->where('sales_activities.scheduled_date >=', $filters['date_from']); } if (!empty($filters['date_to'])) { - $builder->where('sales_activities.scheduled_date <=', $filters['date_to']); + $this->where('sales_activities.scheduled_date <=', $filters['date_to']); } - return [ - 'data' => $builder->orderBy('sales_activities.scheduled_date', 'DESC') - ->limit($limit, $offset)->findAll(), - 'total' => $builder->countAllResults(false) - ]; + if (!empty($filters['search'])) { + $this->groupStart() + ->like('sales_actual_leads.company_name', $filters['search']) + ->orLike('sales_actual_leads.email', $filters['search']) + ->orLike('sales_actual_leads.phone', $filters['search']) + ->orLike('user_profiles.first_name', $filters['search']) + ->groupEnd(); + } + + $this->orderBy('sales_activities.created_at', 'DESC'); + + $total = $this->countAllResults(false); + + $data = $this->findAll($limit, $offset); + + return ['data' => $data,'total' => $total]; + + // return [ + // 'data' => $builder->orderBy('sales_activities.scheduled_date', 'DESC') + // ->limit($limit, $offset)->findAll(), + // 'total' => $builder->countAllResults(false) + // ]; } /** @@ -142,8 +170,8 @@ class SalesActivityModel extends Model { $endDate = date('Y-m-d H:i:s', strtotime("+{$days} days")); - return $this->select('sales_activities.*, actual_leads.company_name') - ->join('actual_leads', 'actual_leads.lead_id = sales_activities.lead_id', 'left') + return $this->select('sales_activities.*, sales_actual_leads.company_name') + ->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left') ->where('sales_activities.assigned_to', $userId) ->where('sales_activities.status', 'pending') ->where('sales_activities.scheduled_date <=', $endDate) diff --git a/app/Models/SalesActualLeadModel.php b/app/Models/SalesActualLeadModel.php index 25603740..22db6a2a 100644 --- a/app/Models/SalesActualLeadModel.php +++ b/app/Models/SalesActualLeadModel.php @@ -37,23 +37,37 @@ class SalesActualLeadModel extends Model // Validation protected $validationRules = [ - 'company_name' => 'required|min_length[2]|max_length[255]', - 'email' => 'required|valid_email|max_length[255]', - 'phone' => 'required|min_length[10]|max_length[20]', - 'status' => 'in_list[New,Potential,Prospects,Non prospects]', + 'company_name' => 'required|alpha_space|min_length[2]|max_length[255]', + 'email' => 'required_without[phone]|permit_empty|valid_email|max_length[255]', + 'phone' => 'required_without[email]|permit_empty|regex_match[/^[0-9+\s]+$/]|min_length[10]|max_length[20]', + 'status' => 'in_list[New,Potential,Prospects,Not a Prospects]', + 'assigned_to' => 'required', + 'gst_number' => 'permit_empty|exact_length[15]|regex_match[/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][0-9A-Z]Z[0-9A-Z]$/]', ]; protected $validationMessages = [ 'company_name' => [ - 'required' => 'Company name is required', + 'required' => 'Company Name is Missing', + 'alpha_space' => 'Company Name must contain only letters and spaces', + 'min_length' => 'Company Name must be at least 2 characters long', + 'max_length' => 'Company Name must be at most 255 characters long' ], 'email' => [ - 'required' => 'Email is required', + 'required_without' => 'Either Email or Mobile Number is Needed.', 'valid_email' => 'Please provide a valid email address', + 'max_length' => 'Email must be at most 255 characters long', ], 'phone' => [ - 'required' => 'Phone number is required', + 'required_without' => 'Either Email or Mobile Number is Needed.', + 'regex_match' => 'Phone number can contain only digits, + and spaces.', + 'min_length' => 'Mobile Number must be at least 10 characters long', + 'max_length' => 'Mobile Number must be at most 20 characters long', ], + 'status' => [ 'in_list' => 'Please select a valid Status'], + 'assigned_to'=> [ 'required' => 'Please select a valid User' ], + 'gst_number' => [ 'exact_length' => 'GST Number must be exactly 15 characters long.', + 'regex_match' => 'Please enter a valid GST Number (e.g., 22AAAAA0000A1Z5).' + ], ]; protected $skipValidation = false; @@ -74,29 +88,42 @@ class SalesActualLeadModel extends Model */ public function getLeadsWithFilters($filters = [], $limit = 10, $offset = 0) { - $builder = $this->select('sales_actual_leads.*, user_profiles.first_name as assigned_to_name') + $this->select('sales_actual_leads.*, user_profiles.first_name as assigned_to_name, user_profiles.email as assigned_to_email') ->join('user_profiles', 'user_profiles.id = sales_actual_leads.assigned_to', 'left'); if (!empty($filters['status'])) { - $builder->where('sales_actual_leads.status', $filters['status']); + $this->where('sales_actual_leads.status', $filters['status']); } + // if (!empty($filters['assigned_to'])) { + // $this->where('sales_actual_leads.assigned_to', $filters['assigned_to']); + // } if (!empty($filters['assigned_to'])) { - $builder->where('sales_actual_leads.assigned_to', $filters['assigned_to']); + $assigned_to = $filters['assigned_to']; + + // If it's already an array, use it. If it's a string, explode it into an array. + $assignedToIds = is_array($assigned_to) ? $assigned_to : explode(',', $assigned_to); + + // Now it is guaranteed to be an array, making whereIn perfectly safe + $this->whereIn('sales_actual_leads.assigned_to', $assignedToIds); } + $this->orderBy('sales_actual_leads.created_at', 'DESC'); + if (!empty($filters['search'])) { - $builder->groupStart() + $this->groupStart() ->like('sales_actual_leads.company_name', $filters['search']) ->orLike('sales_actual_leads.email', $filters['search']) ->orLike('sales_actual_leads.phone', $filters['search']) + ->orLike('user_profiles.first_name', $filters['search']) ->groupEnd(); } - return [ - 'data' => $builder->limit($limit, $offset)->findAll(), - 'total' => $builder->countAllResults(false) - ]; + $total = $this->countAllResults(false); + + $data = $this->findAll($limit, $offset); + + return ['data' => $data,'total' => $total]; } /** @@ -113,9 +140,11 @@ class SalesActualLeadModel extends Model $contactModel = new SalesContactPersonModel(); $activityModel = new SalesActivityModel(); $noteModel = new SalesLeadNoteModel(); + $leadsModel = new LeadsModel(); $lead['contact_persons'] = $contactModel->where('lead_id', $leadId)->findAll(); $lead['activities'] = $activityModel->getActivitiesByLead($leadId); + $lead['opportunities'] = $leadsModel->where('actual_lead_id', $leadId)->findAll(); $lead['notes'] = $noteModel->getNotesByLead($leadId); return $lead; diff --git a/app/Models/TPAModel.php b/app/Models/TPAModel.php index fe3f30cd..679cae2a 100755 --- a/app/Models/TPAModel.php +++ b/app/Models/TPAModel.php @@ -19,5 +19,6 @@ class TPAModel extends Model "created_by", "updated_by", "is_active", + "dashboard_id", ]; } diff --git a/app/Views/layout/header.php b/app/Views/layout/header.php index 57aff2c2..67198829 100755 --- a/app/Views/layout/header.php +++ b/app/Views/layout/header.php @@ -2077,6 +2077,37 @@ + +
  • + + /assets/images/sales_tracker_dark_sb.png" alt="Logo" height="20"> + Sales Tracker + +
    + +
    +
  • +
  • diff --git a/app/Views/leads_form.php b/app/Views/leads_form.php index ef30ba59..87868d39 100644 --- a/app/Views/leads_form.php +++ b/app/Views/leads_form.php @@ -131,6 +131,8 @@ enctype="multipart/form-data"> + +
    @@ -741,6 +743,7 @@ $('#page_title').text(page_title); $('#leads_primarykey').val(res.data.id); + $('#actual_lead_id').val(res.data.actual_lead_id || 0); $('#policy_start_date').val(res.data.policy_end_date); $('#lead_type').val(res.data.lead_type); $('#issuer').val(res.data.issuer); diff --git a/app/Views/leads_form_handler.php b/app/Views/leads_form_handler.php index 6677520d..01960410 100644 --- a/app/Views/leads_form_handler.php +++ b/app/Views/leads_form_handler.php @@ -956,6 +956,7 @@ if (isset($selected_lead_type)) { $('#page_title').text('Edit Lead'); $('#leads_primarykey').val(data.id || ''); + $('#actual_lead_id').val(data.actual_lead_id || 0); $('#policy_start_date').val(data.policy_end_date || ''); $('#lead_type').val(data.lead_type || ''); $('#issuer').val(data.issuer || ''); @@ -1130,6 +1131,7 @@ if (isset($selected_lead_type)) { $('#page_title').text('Edit Lead'); $('#leads_primarykey').val(data.id || ''); + $('#actual_lead_id').val(data.actual_lead_id || 0); $('#policy_start_date').val(data.policy_end_date || ''); $('#lead_type').val(data.lead_type || ''); $('#issuer').val(data.issuer || ''); diff --git a/app/Views/leads_list.php b/app/Views/leads_list.php index 896a1117..98b9f232 100644 --- a/app/Views/leads_list.php +++ b/app/Views/leads_list.php @@ -533,8 +533,11 @@ table.dataTable tbody td { console.log('lead_id', lead_id); console.log('lead_form_type', lead_form_type); - - let url = '' + lead_form_type + '/' + lead_id + let actual_lead_id = 0; // Hardcoded to 0 since this is an edit operation + //Here After Edit URL: /type / actual_lead_id (0) / lead_id + let url = '' + lead_form_type + '/' + actual_lead_id + '/' + lead_id; + // old URL + // let url = '' + lead_form_type + '/' + lead_id console.log('url ', url) window.location.href = url; diff --git a/app/Views/leads_non_eb.php b/app/Views/leads_non_eb.php index 6456935a..6d2049f0 100644 --- a/app/Views/leads_non_eb.php +++ b/app/Views/leads_non_eb.php @@ -74,6 +74,7 @@ +
    diff --git a/app/Views/policy_transaction_endorsement_form.php b/app/Views/policy_transaction_endorsement_form.php index 4b4ab0ba..0c9c579b 100644 --- a/app/Views/policy_transaction_endorsement_form.php +++ b/app/Views/policy_transaction_endorsement_form.php @@ -621,7 +621,7 @@ console.log('Business Team Logged in.'); } - getURLParamsForReport() + // getURLParamsForReport() }) $(document).ready(function(){ @@ -1086,10 +1086,10 @@ if(res.data.client_type == 2){ let data1 = policyListByClient[res.data.client_id]; - appendPolicies(data1, res.pt_id); + appendPolicies(data1, res.data.client_policy_id); }else{ let data2 = policy_list[res.data.client_branch_id]; - appendPolicies(data2, res.pt_id); + appendPolicies(data2, res.data.client_policy_id); } setTimeout(function(){ @@ -2957,9 +2957,9 @@ if (pt_id != 0) { hide_list_show_add(); - setTimeout(() => - getPolicyTransactionDataForEndorsementEdit(pt_id), - 1500); // Reduced timeout + // setTimeout(() => + getPolicyTransactionDataForEndorsementEdit(pt_id) + // 1500); // Reduced timeout } else { show_list_hide_add(); } diff --git a/app/Views/policy_transaction_endorsement_list.php b/app/Views/policy_transaction_endorsement_list.php index 0365496b..eed97902 100644 --- a/app/Views/policy_transaction_endorsement_list.php +++ b/app/Views/policy_transaction_endorsement_list.php @@ -460,7 +460,11 @@ table.dataTable thead th { // select2 document ready $(document).ready(function() { - getClientAndBranchAndPolicy() + // getClientAndBranchAndPolicy() + getClientAndBranchAndPolicy(function() { + getURLParamsForReport(); + }); + addHTMLInput(); has('create_failed')) : ?> @@ -632,7 +636,7 @@ table.dataTable thead th { } - function getClientAndBranchAndPolicy() { + function getClientAndBranchAndPolicy(callback) { $.ajax({ url: '', type: "GET", @@ -652,6 +656,11 @@ table.dataTable thead th { console.log('No data found'); } + // ✅ Data ellam set aanadhuku aparam callback call panrom + if (typeof callback === "function") { + callback(); + } + }, error: function(xhr, status, error) { console.error(xhr.responseText); diff --git a/app/Views/sales/activity_view.php b/app/Views/sales/activity_view.php new file mode 100644 index 00000000..46833e9f --- /dev/null +++ b/app/Views/sales/activity_view.php @@ -0,0 +1,1300 @@ + + +
    +
    +
    + + +
    +
    All
    +
    Pending
    +
    Completed
    +
    + + +
    + + +
    + +
    + +
    + + + +
    + + + + + + + + + + \ No newline at end of file diff --git a/app/Views/sales/branch_level_dashboard_view.php b/app/Views/sales/branch_level_dashboard_view.php index 3a9d13cc..66d98d13 100644 --- a/app/Views/sales/branch_level_dashboard_view.php +++ b/app/Views/sales/branch_level_dashboard_view.php @@ -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; } +
    -
    +
    -
    -
    4
    +
    +
    Total Leads
    -
    ↑ 12% this month
    +
    -
    -
    6
    +
    +
    Total Activities
    -
    ↑ 5% this month
    +
    -
    -
    1
    +
    +
    +
    Pending Activities
    + +
    +
    +
    Completed Activities
    -
    ↑ 15% this month
    +
    -
    +
    @@ -81,62 +126,117 @@ - - Venba Infotech - -
    CALL
    -
    John Doe
    - - 20 Feb 2026 - Completed - - - Acme Corporation - -
    EMAIL
    -
    John Doe
    - - 20 Feb 2026 - Pending - + + + '📞', + 'Email' => '✉️', + 'Meeting' => '📅', + 'Visit' => '🚗', + 'Demo' => '🎬', + 'Share' => '📄', + 'Todo' => '✓' + ]; + $icon = $activityIcons[$a['activity_type']] ?? '📌'; + $statusClass = strtolower(str_replace(' ', '-', $a['status'])); + $formattedDate = date('d M Y', strtotime($a['scheduled_date'])); + ?> + + + +
    + +
    +
    + +
    + + + + + + + + +
    +
    No activities found +
    + + +
    +

    Sales Team Performance

    -
    -
    V
    -
    -
    Venkat 3.0
    -
    Sales Manager
    -
    -
    -
    6 acts
    -
    1 done
    -
    -
    -
    -
    P
    -
    -
    Pavi_the_Staff V
    -
    Sales Staff
    -
    -
    -
    0 acts
    -
    0 done
    -
    -
    -
    + +
    {$firstLetter}
    +
    +
    {$fullName}
    +
    {$role}
    +
    +
    +
    {$totalActs} acts
    +
    {$doneActs} done
    +
    +
    "; + } ?> +

    Activity Breakdown

    -
    Call42%
    + + ['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' => '✓'], + ]; + ?> + + + + +
    + + + + + % +
    + + +
    No activities found
    +
    @@ -157,20 +257,24 @@ - - Venba Infotech - New - John Doe -
    1
    -
    1
    - - - Acme Corporation - Potential - John Doe -
    1
    -
    0
    - + + + + + + + +
    +
    + + + + + +
    No leads found
    + + +
    diff --git a/app/Views/sales/sales_manager_level_dashboard.php b/app/Views/sales/sales_manager_level_dashboard.php index 5b75fc42..5fc0483e 100644 --- a/app/Views/sales/sales_manager_level_dashboard.php +++ b/app/Views/sales/sales_manager_level_dashboard.php @@ -1,5 +1,5 @@
    -

    My Dashboard

    -

    Welcome back,

    +
    - +
    -
    YEARLY TARGET 2025
    +
    YEARLY TARGET
    L
    -
    Apr 2025 - Mar 2026
    +
    -
    - % +
    + + + + + %
    +
    Achieved L Remaining L @@ -69,7 +111,7 @@
    -
    - +
    @@ -90,4 +132,143 @@
    -
    \ No newline at end of file +
    + + + + \ No newline at end of file diff --git a/app/Views/sales/tracker_view.php b/app/Views/sales/tracker_view.php index b317712c..3ceb43a6 100644 --- a/app/Views/sales/tracker_view.php +++ b/app/Views/sales/tracker_view.php @@ -2,19 +2,20 @@ /* POC Exact Styling */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; } - .main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; height: 100vh; } - .top-bar { background: white; padding: 15px 30px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } - .search-input { width: 400px; padding: 10px 15px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; outline: none; } - .search-input:focus { border-color: #ff6b35; } + .main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; } + /* .top-bar { background: white; padding: 15px 30px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } */ + .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); } /* Filter Tabs */ - .filter-tabs { display: flex; gap: 10px; padding: 20px 30px; background: #f5f5f5; } + .filter-tabs { display: flex; gap: 10px; padding: 20px 30px; } .tab { padding: 10px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; } .tab.active { background: #ff6b35; color: white; border-color: #ff6b35; } /* Leads Grid */ + .lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; } + .lead-actions { display: flex; align-items: center; gap: 10px; } .leads-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 0 30px 30px; overflow-y: auto; } .lead-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; cursor: pointer; transition: all 0.2s; } .lead-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); } @@ -22,12 +23,13 @@ .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; } /* 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: 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } + .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; } @@ -39,6 +41,14 @@ .activity-type-btn:hover { border-color: #ff6b35; background: #fff5f2; } .activity-type-btn.active { border-color: #ff6b35; background: #ff6b35; color: white; } + .activity-types { display: grid; grid-template-columns: repeat(12, 1fr); /* 12-column grid */ gap: 10px; margin-bottom: 20px; } + + /* First 3 buttons → 4 columns each (3 × 4 = 12) */ + .activity-types button:nth-child(-n+3) { grid-column: span 4; } + + /* Next 4 buttons → 3 columns each (4 × 3 = 12) */ + .activity-types button:nth-child(n+4) { grid-column: span 3; } + /* Timeline Styling */ .timeline { position: relative; padding-left: 30px; margin-top: 20px; } .timeline-item { position: relative; padding-bottom: 25px; } @@ -47,71 +57,167 @@ .timeline-dot.completed { background: #4caf50; box-shadow: 0 0 0 1px #4caf50; } .timeline-content { background: #f8f8f8; padding: 15px; border-radius: 8px; } .timeline-item {display : block !important;} + .timeline::before { display: none !important; content: none !important; } + .timeline.no-line::before { display: none; } + .timeline { border-left: none !important; } + .timeline-item:last-child { border-left: none !important;} + .timeline-item:last-child::before { display: none !important;} + + /* opportunities Styling */ + .opportunities-list { display: grid; gap: 15px;} + .opportunity-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; transition: all 0.2s;} + .opportunity-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;} + .opportunity-title { font-weight: 600; font-size: 16px; margin-bottom: 5px;} + .opportunity-amount { font-size: 20px; font-weight: 700; color: #4caf50;} + .opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;} + .opportunity-detail-item { font-size: 13px; color: #666;} + .opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; } + + /* Base style for both tabs */ + .tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; } + + /* Style for the SELECTED tab */ + .tab-item.active { color: #333; /* Darker text */ border-bottom: 2px solid #ff6b35; /* Orange underline */ } + + .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; + } + + /* The animation definition */ + @keyframes validation-blink { + 0% { transform: translateX(0); box-shadow: 0 0 0px red; } + 25% { transform: translateX(-5px); box-shadow: 0 0 10px red; } + 50% { transform: translateX(5px); box-shadow: 0 0 10px red; } + 75% { transform: translateX(-5px); box-shadow: 0 0 10px red; } + 100% { transform: translateX(0); box-shadow: 0 0 0px red; } + } + + /* The class we will add via JavaScript */ + .blink-error { + animation: validation-blink 0.3s ease-in-out; + border-color: #ff4d4d !important; + } +
    -
    + +
    +
    + + +
    +
    All
    +
    New
    +
    Potential
    +
    Prospects
    +
    Not a Prospects
    +
    + + +
    + + +
    +
    + + +