Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
velz 2026-02-26 12:28:43 +05:30
commit 9404a7a5bc
26 changed files with 4207 additions and 326 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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,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 '<div class="col-12">
<div class="card text-center">
<div class="card-body">
<h3>Oops! ' . esc($page_type) . ' is under construction.</h3>
<img src="https://omjsblog.files.wordpress.com/2023/07/errorimg.png" alt="Error Image" style="max-width: 100%; height: auto;">
</div>
</div>
</div>';
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()
];
}
}
}

View File

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

View File

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

View File

@ -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([

View File

@ -10,6 +10,7 @@ class LeadsModel extends Model
protected $primaryKey = 'id';
protected $allowedFields = [
'id',
'actual_lead_id',
'lead_type',
'issuer',
'client_type',

View File

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

View File

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

View File

@ -19,5 +19,6 @@ class TPAModel extends Model
"created_by",
"updated_by",
"is_active",
"dashboard_id",
];
}

View File

@ -2077,6 +2077,37 @@
</a>
</li>
<?php } ?>
<?php if (in_array(get_role_id(), [1, 2, 3, 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
src="<?= base_url() . "public"; ?>/assets/images/sales_tracker_dark_sb.png" alt="Logo" height="20">
<span style="color: #02a8b5;">Sales Tracker</span>
</a>
<div class="collapse" id="sidebarSalesTrackermenu">
<ul class="nav-second-level">
<li>
<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/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>
</li> -->
<!-- <li>
<a href="<?= base_url('sales/page/Reports') ?>"><i class="ri-bar-chart-line"></i> Reports </a>
</li> -->
<!-- <li>
<a href="<?= base_url('sales/page/Team') ?>"><i class="ri-bar-chart-line"></i> Sales Team </a>
</li> -->
</ul>
</div>
</li>
<?php } ?>
<!-- Tickets -->
<?php if (in_array(get_role_id(), [1, 2, 3, 4, 5]) || in_array(CLAIMS_TEAM_ID, user_team())) { ?>
<li class="li-seperate" id="tickets-li">

View File

@ -131,6 +131,8 @@
enctype="multipart/form-data">
<input type="hidden" name="id" id="leads_primarykey">
<input type="hidden" name="actual_lead_id" id="actual_lead_id" value="<?= isset($actual_lead_id) ? $actual_lead_id : 0 ?>">
<div class="card" id="collapseOne">
<div class="card-body">
@ -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);

View File

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

View File

@ -533,8 +533,11 @@ table.dataTable tbody td {
console.log('lead_id', lead_id);
console.log('lead_form_type', lead_form_type);
let url = '<?=base_url('/util/getLeadNonEB/')?>' + 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 = '<?=base_url('/util/getLeadNonEB/')?>' + lead_form_type + '/' + actual_lead_id + '/' + lead_id;
// old URL
// let url = '<?=base_url('/util/getLeadNonEB/')?>' + lead_form_type + '/' + lead_id
console.log('url ', url)
window.location.href = url;

View File

@ -74,6 +74,7 @@
<input type="hidden" name="id" id="leads_primarykey">
<input type="hidden" name="lead_form_type" id="lead_form_type_id" value="2">
<input type="hidden" name="actual_lead_id" id="actual_lead_id" value="<?= isset($actual_lead_id) ? $actual_lead_id : 0 ?>">
<div class="card" id="collapseOne">
<div class="card-body">

View File

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

View File

@ -460,7 +460,11 @@ table.dataTable thead th {
// select2 document ready
$(document).ready(function() {
getClientAndBranchAndPolicy()
// getClientAndBranchAndPolicy()
getClientAndBranchAndPolicy(function() {
getURLParamsForReport();
});
addHTMLInput();
<?php if (session()->has('create_failed')) : ?>
@ -632,7 +636,7 @@ table.dataTable thead th {
}
function getClientAndBranchAndPolicy() {
function getClientAndBranchAndPolicy(callback) {
$.ajax({
url: '<?= base_url("/util/getClientAndBranchAndPolicy") ?>',
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);

File diff suppressed because it is too large Load Diff

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,29 +80,34 @@
<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_activities ?></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_activities - $completed_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 $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">
@ -81,62 +126,117 @@
</tr>
</thead>
<tbody>
<tr>
<td><strong>Venba Infotech</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>
</td>
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
<td><span class="status-pill status-completed">Completed</span></td>
</tr>
<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>
<td style="color: #4a5568; font-weight: 500;">20 Feb 2026</td>
<td><span class="status-pill status-pending">Pending</span></td>
</tr>
<?php if (!empty($recent_acts)): ?>
<?php foreach ($recent_acts as $a): ?>
<?php
$activityIcons = [
'Call' => '📞',
'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']));
?>
<tr>
<td><strong><?= esc($a['company_name']) ?></strong></td>
<td>
<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>
</td>
<td style="color: #4a5568; font-weight: 500;"><?= $formattedDate ?></td>
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="4">
<div class="empty-state">
<div class="empty-icon"></div> No activities found
</div>
</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>
</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 class="dot" style="background: <?= $color ?>;"></span>
<?= $icon ?> <?= esc($type) ?>
</span>
<strong><?= $item['percentage'] ?>%</strong>
</div>
<?php endforeach; ?>
<?php else: ?>
<div class="empty-state">No activities found</div>
<?php endif; ?>
</div>
</div>
</div>
@ -157,20 +257,24 @@
</tr>
</thead>
<tbody>
<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>
</tr>
<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>
</tr>
<?php if (!empty($leads_overview)): ?>
<?php foreach ($leads_overview as $lead): ?>
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
<tr>
<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 colspan="5">
<div class="empty-state">No leads found</div>
</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -1,5 +1,5 @@
<style>
.my-dash { padding: 25px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #fff; }
.my-dash { padding: 25px; margin-top: -21px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #fff; }
.target-card { background: #1e252b; color: white; padding: 30px; border-radius: 15px; position: relative; margin-bottom: 30px; }
.target-amount { font-size: 32px; font-weight: bold; margin: 10px 0; }
.progress-container { background: #333; height: 8px; border-radius: 4px; margin: 20px 0; overflow: hidden; position: relative; }
@ -10,7 +10,7 @@
.stat-box .num { font-size: 20px; font-weight: bold; display: block; margin-bottom: 2px; }
.stat-box .lbl { font-size: 11px; color: #999; }
.chart-circle { position: absolute; right: 40px; top: 35px; width: 75px; height: 75px; border: 6px solid #333; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 18px; border-top-color: #ff6b35; }
/* .chart-circle { position: absolute; right: 40px; top: 35px; width: 75px; height: 75px; border: 6px solid #333; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 18px; border-top-color: #ff6b35; } */
.dashboard-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; }
.list-card { border: 1px solid #f0f0f0; border-radius: 12px; padding: 25px; background: #fff; }
@ -19,31 +19,73 @@
.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); }
</style>
<div class="my-dash">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:25px;">
<div>
<h1 style="font-size:22px; margin:0; font-weight: 700;">My Dashboard</h1>
<p style="color:#666; font-size:13px; margin-top: 4px;">Welcome back, <?= $user_name ?></p>
<!-- <h1 style="font-size:22px; margin:0; font-weight: 700;">My Dashboard</h1>
<p style="color:#666; font-size:13px; margin-top: 4px;">Welcome back, <?= $user_name ?></p> -->
</div>
<div style="position: relative;">
<input type="text" placeholder="Search leads, activities..." style="background:#f5f5f5; padding:10px 20px; border-radius:8px; border:none; width: 250px; font-size: 13px;">
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)" style="background:#f5f5f5; padding:10px 20px; border-radius:8px; border:none; width: 250px; font-size: 13px; cursor: pointer;">
<?php if(!empty($fin_years)): ?>
<?php foreach($fin_years as $year): ?>
<option value="<?= $year; ?>"><?= $year; ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
</div>
<div class="target-card">
<div style="font-size:11px; color:#999; letter-spacing: 1px; font-weight: 600;">YEARLY TARGET 2025</div>
<div style="font-size:11px; color:#999; letter-spacing: 1px; font-weight: 600;">YEARLY TARGET</div>
<div class="target-amount"><?= number_format($target / 100000, 1) ?>L</div>
<div style="font-size:12px; color:#777;">Apr 2025 - Mar 2026</div>
<div style="font-size:12px; color:#777;"><?= $display_fin_years ?></div>
<div class="chart-circle">
<span style="color:#ff6b35"><?= $percent ?>%</span>
<div class="chart-circle" style="position: absolute; right: 40px; top: 35px; width: 75px; height: 75px;">
<svg width="75" height="75" viewBox="0 0 75 75" style="transform: rotate(-90deg);">
<circle cx="37.5" cy="37.5" r="30" fill="none" stroke="#333" stroke-width="6"/>
<circle cx="37.5" cy="37.5" r="30" fill="none" stroke="#ff6b35" stroke-width="6"
stroke-dasharray="<?= round(2 * M_PI * 30 * $percent / 100, 2) ?> 188.5"
stroke-linecap="round"/>
</svg>
<span style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:#ff6b35; font-weight:bold; font-size:14px;"><?= $percent ?>%</span>
</div>
<div class="progress-container">
<div class="progress-bar" style="width: <?= $percent ?>%"></div>
</div>
<div style="display:flex; justify-content:space-between; font-size:12px; color: #999;">
<span>Achieved <strong style="color:white"><?= number_format($achieved / 100000, 1) ?>L</strong></span>
<span>Remaining <strong style="color:white"><?= number_format($remaining / 100000, 1) ?>L</strong></span>
@ -69,7 +111,7 @@
<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"> Done</button>
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)"> Mark as completed</button>
</div>
<?php endforeach; ?>
</div>
@ -90,4 +132,143 @@
<?php endforeach; ?>
</div>
</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>
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) {
const url = new URL(window.location.href);
url.searchParams.set('fy', select.value);
window.location.href = url.toString();
}
</script>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB