diff --git a/app/Config/Routes.php b/app/Config/Routes.php index cc323406..4764e7e1 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -55,7 +55,6 @@ $routes->post("add_advertise_image", "AppContentManagementController::add_advert $routes->post('remove_advertise_image', 'AppContentManagementController::remove_advertise_image'); $routes->get("add_image_index", "AppContentManagementController::add_image_index"); $routes->get("getAdvertiseImage/(:any)", "AppContentManagementController::getAdvertiseImage/$1"); -$routes->get("frontend_content", "AppContentManagementController::frontend_content"); $routes->get('showAdvertiseImage/(:any)', 'AppContentManagementController::showAdvertiseImage/$1'); @@ -169,6 +168,7 @@ $routes->group("/client", ["filter" => "authMVC"], function ($routes) { $routes->post("create_2", "ClientController::createClientKYCInfo_2"); $routes->post("edit_2", "ClientController::editClientKYCInfo_2"); $routes->post("delete_2", "ClientController::deleteClientKycDocs_2/$1"); + $routes->get("download_2/(:any)", "ClientController::downloadClientKycDocs_2/$1"); }); @@ -421,6 +421,7 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->match(['get', 'post', 'delete'], 'nhanceBranchMaster', 'MasterController::nhanceBranchMaster'); $routes->match(['get', 'post', 'delete'], 'vehicleTypeMaster', 'MasterController::vehicleTypeMaster'); $routes->match(['get', 'post', 'delete'], 'rtoMaster', 'MasterController::rtoMaster'); + $routes->match(['get', 'post', 'delete'], 'partnerPOS', 'MasterController::partnerPOS'); $routes->get('checkDuplicateCdAccount', 'MasterController::checkDuplicateCdAccount'); $routes->get('proceedExcelFileDataValidation', 'EmployeeController::proceedExcelFileDataValidation'); $routes->get('checkTpaApiEnable', 'EmployeeRestController::checkTpaApiEnable'); @@ -647,7 +648,13 @@ $routes->group("employeeRest", ["filter" => ['appSignature' , 'authJWT']], funct $routes->get("ticketHistoryList", "ThzController::ticketHistoryList"); $routes->match(['get','post','put'], 'ticketType', 'ThzController::ticketType'); - + //this route for nhance website contact form (sales/service) + $routes->group("employeeRest", ['filter' => ['appSignature'] ], function ($routes) { + $routes->post("ticketSave", "ThzController::ticketSave"); + }); + + //API FAQ + $routes->match(['get','post','delete'], 'FAQ', 'AppContentManagementController::FAQ'); //claims $routes->post('initiateClaim',"EmployeeRestController::initiateClaim"); @@ -777,7 +784,7 @@ $routes->get('testTracelog','TestBusinessController::a'); $routes->get("claimView", "EmployeeRestController::claimView"); // General Tickets -$routes->post("ticketSave", "ThzController::ticketSave"); +$routes->post("ticketSave", "ThzController::ticketSave"); // $routes->get("ticketList", "ThzController::ticketList"); $routes->post("ticketConversationSave", "ThzController::ticketConversationSave"); $routes->get("ticketConversationList", "ThzController::ticketConversationList"); @@ -785,6 +792,10 @@ $routes->post('ticketAutoFetchDetails',"ThzController::ticketAutoFetchDetails"); $routes->match(['get','post','put'], 'ticketType', 'ThzController::ticketType'); $routes->get("ticketHistoryList", "ThzController::ticketHistoryList"); +// General FAQ +$routes->match(['get','post','delete'], 'FAQ', 'AppContentManagementController::FAQ'); +$routes->match(['get','post','delete'], 'frontend_content', 'AppContentManagementController::frontend_content'); +$routes->get("getFEContent", "EmployeeRestController::getFEContent"); //for testing $routes->group('test', function($routes) { @@ -853,3 +864,89 @@ $routes->group('logs', function($routes) { $routes->get('delete/(:segment)', 'LogController::delete/$1'); $routes->get('clearAll', 'LogController::clearAll'); }); + +// ----------------------------------------------------------------------------------------------------------------- +$routes->group('sales', function($routes) { + + // ==================== LEAD ROUTES ==================== + + // Get all leads with filters + $routes->get('leads', 'SalesController::getLeads'); + + // Get lead statistics + $routes->get('leads/stats', 'SalesController::getLeadStats'); + + // Get single lead with complete details + $routes->get('leads/(:num)', 'SalesController::getLead/$1'); + + // Create new lead + $routes->post('leads', 'SalesController::createLead'); + + // Update lead + $routes->put('leads/(:num)', 'SalesController::updateLead/$1'); + + // Delete lead + $routes->delete('leads/(:num)', 'SalesController::deleteLead/$1'); + + // Get contacts for a lead + $routes->get('leads/(:num)/contacts', 'SalesController::getContacts/$1'); + + // Get activities for a lead + $routes->get('leads/(:num)/activities', 'SalesController::getLeadActivities/$1'); + + // Get activity timeline for a lead + $routes->get('leads/(:num)/timeline', 'SalesController::getActivityTimeline/$1'); + + // Get notes for a lead + $routes->get('leads/(:num)/notes', 'SalesController::getLeadNotes/$1'); + + // ==================== CONTACT PERSON ROUTES ==================== + + // Create contact person + $routes->post('contacts', 'SalesController::createContact'); + + // Update contact person + $routes->put('contacts/(:num)', 'SalesController::updateContact/$1'); + + // Delete contact person + $routes->delete('contacts/(:num)', 'SalesController::deleteContact/$1'); + + // Set primary contact + $routes->put('contacts/(:num)/set-primary', 'SalesController::setPrimaryContact/$1'); + + // ==================== ACTIVITY ROUTES ==================== + + // Get all activities with filters + $routes->get('activities', 'SalesController::getActivities'); + + // Get upcoming activities + $routes->get('activities/upcoming', 'SalesController::getUpcomingActivities'); + + // Get pending activities count + $routes->get('activities/pending-count', 'SalesController::getPendingCount'); + + // Create activity + $routes->post('activities', 'SalesController::createActivity'); + + // Update activity + $routes->put('activities/(:num)', 'SalesController::updateActivity/$1'); + + // Complete activity + $routes->post('activities/(:num)/complete', 'SalesController::completeActivity/$1'); + + // Delete activity + $routes->delete('activities/(:num)', 'SalesController::deleteActivity/$1'); + + // ==================== NOTE ROUTES ==================== + + // Create note + $routes->post('notes', 'SalesController::createNote'); + + // Update note + $routes->put('notes/(:num)', 'SalesController::updateNote/$1'); + + // Delete note + $routes->delete('notes/(:num)', 'SalesController::deleteNote/$1'); +}); + + diff --git a/app/Controllers/AppContentManagementController.php b/app/Controllers/AppContentManagementController.php index da65a7b1..0dc36282 100755 --- a/app/Controllers/AppContentManagementController.php +++ b/app/Controllers/AppContentManagementController.php @@ -11,6 +11,7 @@ use CodeIgniter\API\ResponseTrait; use App\Models\AddImgModel; use App\Models\FEContentModel; use App\Models\ClientModel; +use App\Models\FAQModel; class AppContentManagementController extends AdminController { @@ -19,6 +20,7 @@ class AppContentManagementController extends AdminController protected $addImgModel; protected $feContentModel; protected $clientModel; + protected $faqModel; public function __construct() @@ -27,6 +29,7 @@ class AppContentManagementController extends AdminController $this->addImgModel = new AddImgModel(); $this->feContentModel = new FEContentModel(); $this->clientModel = new ClientModel(); + $this->faqModel = new FAQModel(); } //listing public function add_image_index() @@ -133,27 +136,191 @@ class AppContentManagementController extends AdminController - // public function getAdvertiseImage($image_id){ - // $image_data = $this->addImgModel->select('name')->where(['id' => $image_id])->first(); - // if($image_data){ - // return $image_data['name']; - // }else{ - // return ; - // } - // } + public function frontend_content() + { + + $method = $this->request->getMethod(); + + if ($this->request->getMethod() === 'post') { + $id = $this->request->getPost('fe_id'); + $data = $this->request->getPost(); + unset($data['fe_id']); - // Front End Content Area - public function frontend_content(){ + foreach ($data as $k => $v) { + if ($v === '' || $v === null) {unset($data[$k]);} + } + + // INSERT / UPDATE + if (empty($id)) { + $status = $this->feContentModel->insert($data); + $text = "Created"; + } else { + $status = $this->feContentModel->update($id, $data); + $text = "Updated"; + } - $data['test'] = $this->feContentModel->findAll(); - $headerData['tab_name'] = 'Front-End Content'; - $headerData['page_name'] = 'Front-End Content'; + return $this->respond([ + 'status' => $status ? true : false, + 'message' => "Frontend Content $text " . ($status ? 'successfully' : 'failed'), + 'code' => $status ? 200 : 400, + 'data' => $data, + ] , $status ? 200 : 400 ); + } + elseif ($method === 'get') { - echo view('layout/header', $headerData); - echo view('frontend_content_list', $data); - echo view('layout/footer'); + $id = $this->request->getGet('fe_id') ?? null; + + if (!empty($id)) { + + $data['fe_list'] = $this->feContentModel->where('id', $id)->orderBy('id', 'DESC')->findAll(); + + if (!empty($data)) { + return $this->respond(['status' => true, 'code' => 200, 'data' => $data], 200); + } else { + return $this->respond(['status' => false, 'code' => 400, 'message' => 'No data found'], 200); + } + } + + $data['fe_list'] = $this->feContentModel->orderBy('id', 'DESC')->findAll(); + + return $this->loadLayout('frontend_content_list', ['data' => $data,'tab_name' => 'Front-End Content','page_name' => 'Front-End Content']); + + } elseif ($method === 'delete') { + + // $input = $this->request->getRawInput(); + $id = $this->request->getGet('fe_id'); // ✅ THIS + $id = $id ?? null; + + if (empty($id)) { + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'No ID provided for deletion' + ], 200); + } + + $update_status = $this->feContentModel->where('id', $id)->set(['is_active' => 0])->update(); + + if ($update_status) { + return $this->respond([ + 'status' => true, + 'code' => 200, + 'message' => 'Data removed successfully', + 'fe_id' => $id + ], 200); + } else { + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'Failed to remove data', + 'fe_id' => $id + ], 200); + } + } + } + + + public function FAQ() + { + $method = strtolower($this->request->getMethod()); + $returnType = strtolower($this->request->getGet('return_type') ?? 'api'); + $ref = ['timestamp' => date('Y-m-d H:i:s')]; + + try { + // --- 1. POST: CREATE OR UPDATE --- + if ($method === 'post') { + $id = $this->request->getPost('faq_id'); + $data = array_filter($this->request->getPost(), fn($v) => $v !== '' && $v !== null); + + if (empty($id)) { + $status = $this->faqModel->insert($data); + $msg = "Created"; + } else { + $status = $this->faqModel->update($id, $data); + $msg = "Updated"; + } + + // if ($returnType === 'web') { + // return redirect()->back()->with($status ? 'success' : 'error', "FAQ $msg " . ($status ? 'successfully' : 'failed')); + // } + + // return $this->response->setJSON([ + // ])->setStatusCode($result ? 200 : 400); + return $this->response->setJSON([ + 'status' => $status ? 'success' : 'error', + 'message' => "FAQ $msg " . ($status ? 'successfully' : 'failed'), + 'code' => $status ? 200 : 400, + 'data' => $data, + 'ref' => $ref + ])->setStatusCode($status ? 200 : 400); + } + + // --- 2. GET: FETCH LIST OR SINGLE --- + elseif ($method === 'get') { + + $id = $this->request->getGet('faq_id'); + + if (!empty($id)) { + // $row = $this->faqModel->where('is_active', 1)->find($id); + $row = $this->faqModel->find($id); + $data['faq_list'] = $row ? [$row] : []; + } else { + // $data['faq_list'] = $this->faqModel->where('is_active', 1)->orderBy('id', 'desc')->findAll(); + $data['faq_list'] = $this->faqModel->orderBy('id', 'desc')->findAll(); + } + + if ($returnType === 'web') { + $data['tab_name'] = "FAQ's"; + $data['page_name'] = "FAQ's"; + // print_r($data);die; + return $this->loadLayout('faq_list', $data); + } + + // API Response Logic + if (empty($data['faq_list'])) { + return $this->response->setJSON([ + 'status' => $returnType === 'web' ? false : 'error', + 'message' => 'No data found', + 'code' => 404, + 'data' => [], + 'ref' => $ref + ])->setStatusCode(200); // Using 200 with error status is common for mobile apps to prevent crashes + } + + return $this->response->setJSON([ + 'status' => $returnType === 'web' ? true : 'success', + 'message' => 'Data retrieved', + 'code' => 200, + 'data' => $data, + 'ref' => $ref + ])->setStatusCode(200); + } + + // --- 3. DELETE: SOFT DELETE --- + elseif ($method === 'delete') { + $id = $this->request->getGet('faq_id'); + $status = (!empty($id)) ? $this->faqModel->update($id, ['is_active' => 0]) : false; + + return $this->response->setJSON([ + 'status' => $status ? ($returnType === 'web' ? true : 'success') : ($returnType === 'web' ? false : 'error'), + 'message' => $status ? 'Data removed successfully' : 'Failed to remove or ID missing', + 'code' => $status ? 200 : 400, + 'data' => [], + 'ref' => $ref + ])->setStatusCode( 200 ); + } + + } catch (\Throwable $e) { + $msg = $e->getMessage(); + return $this->response->setJSON([ + 'status' => $returnType === 'web' ? false : 'error', + 'code' => 500, + 'message' => $msg, + 'ref' => ['file' => $e->getFile(), 'line' => $e->getLine()] + ])->setStatusCode(500); + } } } \ No newline at end of file diff --git a/app/Controllers/ClientController.php b/app/Controllers/ClientController.php index 69b977b4..dea5763b 100755 --- a/app/Controllers/ClientController.php +++ b/app/Controllers/ClientController.php @@ -1121,37 +1121,43 @@ class ClientController extends AdminController } } - public function fetch_dropdown($client_id){ - - $db = db_connect(); + public function fetch_dropdown($client_id) + { + $db = db_connect(); - - $submitted_ids_subquery = $db->table('client_kyc_documents ckd') - ->select('ckd.kyc_doc_type_id') - ->where('ckd.client_id', $client_id) - ->where('ckd.is_active', 1) - ->getCompiledSelect(); - - - $result = $db->table('kyc_docs kd') - ->select('kd.*') - ->join('clients c', 'kd.kyc_type_id = c.entity_type_id') - ->where('c.id', $client_id) - ->where("kd.kyc_type_id NOT IN ({$submitted_ids_subquery})") - ->groupBy('kd.id') - ->get() - ->getResultArray(); + // Already uploaded docs + $uploadedData = $db->table('client_kyc_documents') + ->select('kyc_doc_type_id') + ->where('client_id', $client_id) + ->where('is_active', 1) + ->get() + ->getResultArray(); - - $dropdown = ''; - $dropdown .= ''; + $excludedIds = array_column($uploadedData, 'kyc_doc_type_id'); + + // Available docs + $builder = $db->table('kyc_docs kd'); + $builder->select('kd.id, kd.file_name'); + $builder->join('clients c', 'kd.kyc_type_id = c.entity_type_id'); + $builder->where('c.id', $client_id); + $builder->where('kd.is_active', 1); + + if (!empty($excludedIds)) { + $builder->whereNotIn('kd.id', $excludedIds); + } + + $result = $builder->get()->getResultArray(); // ✅ ARRAY + + // Build dropdown + $dropdown = ''; + $dropdown .= ''; + + foreach ($result as $row) { + $dropdown .= ''; + } - foreach ($result as $row) { - $dropdown .= ''; - } - return $dropdown; } @@ -2392,13 +2398,27 @@ class ClientController extends AdminController - public function uploadVehicleFile() + public function uploadVehicleFile($params = null) { $this->myLogger->logme('error', 'uploadVehicleFile function called'); - $data = $this->request->getPost(); - $files = $this->request->getFiles(); - // $data['client_id'] = 12; - // $data['vehicle_id'] = 1; + + $client_id = null; + $vehicle_id = null; + $files = null; + $docs_name = null; + + if(empty($params) && $this->request){ + $data = $this->request->getPost(); + $files = $this->request->getFiles(); + $client_id = $data['client_id'] ?? null; + $vehicle_id = $data['vehicle_id'] ?? null; + $docs_name = $data['other_docs_name'] ?? null; + }else { + $client_id = $params['client_id'] ?? null; + $vehicle_id = $params['vehicle_id'] ?? null; + $docs_name = $params['other_docs_name'] ?? null; + $files = $params['file_data'] ?? null; + } // upload path $uploadFilePath = WRITEPATH . 'uploads/client_kyc_documents'; @@ -2406,20 +2426,21 @@ class ClientController extends AdminController $insertedDocs = []; // Check document names and files - if (isset($data['other_docs_name']) && isset($files['file_name'])) { - foreach ($data['other_docs_name'] as $key => $docName) { + if (!empty($docs_name) && !empty($files) && !empty($client_id) && !empty($vehicle_id)) { + + foreach ($docs_name as $key => $docName) { // Get the corresponding file for this document name $file = $files['file_name'][$key]; if (!empty($docName) && $file->isValid() && !$file->hasMoved()) { // Upload the file - $uploadedFileName = file_Upload($file, $uploadFilePath); + $uploadedFileName = file_Upload_for_lead($file, $uploadFilePath); if ($uploadedFileName) { // Prepare data for each document upload $docData = [ - 'client_id' => $data['client_id'], - 'vehicle_id' => $data['vehicle_id'], + 'client_id' => $client_id, + 'vehicle_id' => $vehicle_id, 'other_docs_name' => $docName, 'file_name' => $uploadedFileName, 'created_by' => get_session_userid(), @@ -2435,16 +2456,21 @@ class ClientController extends AdminController } } + if(!empty($params) && !$this->request){ + return true; + } + if (!empty($insertedDocs)) { // Fetch all documents for the client $vehicleDocs = $this->clientKYCDocsModel ->where('client_id', $data['client_id']) ->where('vehicle_id', $data['vehicle_id']) ->findAll(); - return $this->respond(['status' => true, 'code' => 200, 'vehicle_docs' => $vehicleDocs, 'inserted_docs' => $insertedDocs], 200); + return $this->respond(['status' => true, 'code' => 200, 'vehicle_docs' => $vehicleDocs, 'inserted_docs' => $insertedDocs, 'message' => 'File uploaded successfully'], 200); } else { return $this->respond(['status' => false, 'code' => 400, 'message' => 'No documents uploaded or inserted'], 200); } + } else { return $this->respond(['status' => false, 'code' => 400, 'message' => 'Invalid data submission'], 200); } @@ -2517,22 +2543,57 @@ class ClientController extends AdminController { - $result['ckdlist'] = db_connect()->table('client_kyc_documents ckd') - ->select("ckd.id,ckd.client_id,ckd.kyc_doc_type_id,ckd.file_name,kd.file_name AS kd_docs_name,ckd.other_docs_name,ckd.vehicle_id,ckd.is_active, - CASE - WHEN ckd.kyc_doc_type_id IS NULL - OR ckd.kyc_doc_type_id = 0 - OR ckd.kyc_doc_type_id = '' - THEN ckd.other_docs_name - ELSE kd.file_name - END AS ui_docs_name") - ->join('kyc_docs kd','ckd.kyc_doc_type_id = kd.kyc_type_id','left') - ->where('ckd.client_id',$client_id) - ->where('ckd.is_active',1) - ->groupBy('ckd.id') - ->get() - ->getResultArray(); + // $result['ckdlist'] = db_connect()->table('client_kyc_documents ckd') + // ->select("ckd.id,ckd.client_id,ckd.kyc_doc_type_id,ckd.file_name,kd.file_name AS kd_docs_name,ckd.other_docs_name,ckd.vehicle_id,ckd.is_active, + // CASE + // WHEN ckd.kyc_doc_type_id IS NULL + // OR ckd.kyc_doc_type_id = 0 + // OR ckd.kyc_doc_type_id = '' + // THEN ckd.other_docs_name + // ELSE kd.file_name + // END AS ui_docs_name") + // ->join('kyc_docs kd','ckd.kyc_doc_type_id = kd.id','left') + // ->where('ckd.client_id',$client_id) + // ->where('ckd.is_active',1) + // ->groupBy('ckd.id') + // ->get() + // ->getResultArray(); + + $builder = db_connect()->table('client_kyc_documents AS ckd'); + + // Select the standard fields + $builder->select(' + ckd.id, + ckd.client_id, + ckd.kyc_doc_type_id, + ckd.file_name, + ckd.other_docs_name, + ckd.vehicle_id, + ckd.is_active + '); + + // Add the CASE statement for ui_docs_name + // Using false as the second parameter tells CI4 not to escape the string + $builder->select(" + CASE + WHEN ckd.kyc_doc_type_id IS NULL OR ckd.kyc_doc_type_id = 0 OR ckd.kyc_doc_type_id = '' + THEN ckd.other_docs_name + ELSE kd.file_name + END AS ui_docs_name + ", false); + + // Join with kyc_docs + $builder->join('kyc_docs AS kd', 'ckd.kyc_doc_type_id = kd.id', 'left'); + + // Where clauses + $builder->where('ckd.client_id', $client_id); + $builder->where('ckd.is_active', 1); + + // Group By + $builder->groupBy('ckd.id'); + + $result['ckdlist'] = $builder->get()->getResultArray(); $result['client_id'] = $client_id; $table = view('client_kyc_single_table', $result); @@ -8338,6 +8399,32 @@ class ClientController extends AdminController return $default_templates_inserted ? true : false; } + // public function downloadClientKycDocs_2() + // { + + // $file = WRITEPATH . 'uploads/client_kyc_documents/' . $file_name; // Example file path + // if (file_exists($file)) { + // return $this->response->download($file, null)->setFileName($file_name); + // } else { + // return "File not found."; + // } + // } + public function downloadClientKycDocs_2($file_name = null) + { + if (!$file_name) { + return "No file specified."; + } + + // Use absolute path to your upload folder + $file = WRITEPATH . 'uploads/client_kyc_documents/' . $file_name; + + if (file_exists($file)) { + // download() takes the path as first param and null (or data) as second + return $this->response->download($file, null); + } else { + return "File not found at: " . $file; + } + } } diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index d9e639e2..a672bf8f 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -2844,7 +2844,7 @@ class EmployeeRestController extends AdminController $emp_reatail_policy_data = $this->getEmpRetailPolicy($retailUserData); $wellness_data = ['status' => 'failed','message' => 'Coming soon........!']; - return $this->respond(['status' => 'success', 'code' => 200, 'data' => [], 'emp_name' => $emp_reatail_policy_data[0]['insurerd_name'] ?? "", 'pre_policy_count' => 0, 'retail_policy_data' => $emp_reatail_policy_data, 'wellness_data' => $wellness_data], 200); + return $this->respond(['status' => 'success', 'code' => 200, 'data' => [], 'emp_name' => $emp_reatail_policy_data[0]['insurerd_name'] ?? "", 'pre_policy_count' => 0, 'emp_not_enrolled_count' => 0, 'retail_policy_data' => $emp_reatail_policy_data, 'wellness_data' => $wellness_data], 200); } } @@ -3828,14 +3828,19 @@ class EmployeeRestController extends AdminController } } - if ($pdf_exist_in_the_file && $tpa_claim_push == true) { + $count = $this->ticketMaster->where('id', $ticket_id)->where('is_active', 1)->where('tpa_claim_push_reference_no IS NULL')->countAllResults(); + log_message('error', 'Ticket validation | Ticket ID: ' . $ticket_id . ' | Matching active tickets with NULL TPA reference: ' . $count); + + if ($count > 0 && $pdf_exist_in_the_file && $tpa_claim_push == true) { // this call for TPA integration $apiServiceController = new ApiServiceController(); $apiServiceController->pushClaims($ticket_id); log_message('error', "pushClaims function called with Ticket ID: {$ticket_id}, In Employee Rest Controller"); } else { if($tpa_claim_push == false){ - log_message('error', 'Skip the TPA claim push'); + log_message('error', 'Skip the TPA claim push for IR DOCUMENTS'); + } else if($count == 0){ + log_message('error', 'TPA claim push skipped as the claim reference number is already generated.'); }else{ log_message('error', "Failed to call the pushClaims function in EmployeeRestController for Ticket ID: {$ticket_id}, because the .pdf file does not exist."); } diff --git a/app/Controllers/MasterController.php b/app/Controllers/MasterController.php index fec5dc4e..2c243fee 100755 --- a/app/Controllers/MasterController.php +++ b/app/Controllers/MasterController.php @@ -38,6 +38,7 @@ use App\Models\SettingsModel; use App\Models\VehicleModel; use App\Models\VehicleTypeModel; use App\Models\RTOModel; +use App\Models\PartnerPosModel; use CodeIgniter\CLI\CLI; @@ -2320,4 +2321,148 @@ class MasterController extends AdminController } } -} \ No newline at end of file + public function partnerPOS() + { + $posModel = new PartnerPosModel(); + $method = $this->request->getMethod(); + + if ($this->request->getMethod() === 'post') { + + $id = $this->request->getPost('pk'); + $data = $this->request->getPost(); + + unset($data['pk']); + + try { + // Certificate + $certificate = $this->uploadPOSFile('certificate_file_name', 'pos_certificate_files'); + if ($certificate !== null) { $data['certificate_file_name'] = $certificate; } else { unset($data['certificate_file_name']); } + + // PAN file + $panFile = $this->uploadPOSFile('pan_file_name', 'pos_certificate_files'); + if ($panFile !== null) { $data['pan_file_name'] = $panFile; } else { unset($data['pan_file_name']);} + + // Aadhaar file + $aadharFile = $this->uploadPOSFile('aadhar_file_name', 'pos_certificate_files'); + if ($aadharFile !== null) { $data['aadhar_file_name'] = $aadharFile; } else { unset($data['aadhar_file_name']);} + + } catch (\RuntimeException $e) { + return $this->respond([ 'status' => false, 'code' => 400, 'message' => $e->getMessage(), 'data' => $data ], 400); + } + + foreach ($data as $k => $v) { + if ($v === '' || $v === null) { + unset($data[$k]); + } + } + + + // INSERT / UPDATE + if (empty($id)) { + $status = $posModel->insert($data); + } else { + $status = $posModel->update($id, $data); + } + + if ($status) { + return $this->respond([ 'status' => true, 'code' => 200, 'message' => 'POS updated successfully', 'data' => $data ], 200); + } + + return $this->respond([ 'status' => false, 'code' => 400, 'message' => 'Failed to update', 'data' => $data ], 400); + } + elseif ($method === 'get') { + + $id = $this->request->getGet('pk') ?? null; + + $db = db_connect(); + + $builder = $db->table('partner_staff'); + + $data['manager_list'] = $builder->select('partner_staff.id, partner_staff.name') + ->where('partner_staff.is_active', 1) + ->where('partner_staff.role_id', 1) + ->get() + ->getResultArray(); + + + if (!empty($id)) { + + $data['pos_list'] = $posModel + ->select('partner_pos.*, partner_staff.name AS manager_name') + ->join('partner_staff', 'partner_staff.id = partner_pos.manager_id', 'left') + ->where('partner_pos.id', $id) + ->orderBy('partner_pos.id', 'DESC') + ->findAll(); + + if (!empty($data)) { + return $this->respond(['status' => true, 'code' => 200, 'data' => $data], 200); + } else { + return $this->respond(['status' => false, 'code' => 400, 'message' => 'No data found'], 200); + } + } + + $data['pos_list'] = $posModel + ->select('partner_pos.*, ps.name AS manager_name') + ->join('partner_staff ps', 'ps.id = partner_pos.manager_id', 'left') + ->orderBy('partner_pos.id', 'DESC') + ->findAll(); + + + return $this->loadLayout('pos_list', ['data' => $data,'tab_name' => 'POS details','page_name' => 'POS details']); + + } elseif ($method === 'delete') { + + // $input = $this->request->getRawInput(); + $id = $this->request->getGet('pk'); // ✅ THIS + $id = $id ?? null; + + if (empty($id)) { + return $this->respond([ + 'status' => false, + 'code' => 404, + 'message' => 'No ID provided for deletion' + ], 200); + } + + $update_status = $posModel->where('id', $id)->set(['is_active' => 0])->update(); + + if ($update_status) { + return $this->respond([ + 'status' => true, + 'code' => 200, + 'message' => 'Data removed successfully', + 'pk' => $id + ], 200); + } else { + return $this->respond([ + 'status' => false, + 'code' => 400, + 'message' => 'Failed to remove data', + 'pk' => $id + ], 200); + } + } + } + + private function uploadPOSFile(string $fieldName, string $uploadDir) + { + $file = $this->request->getFile($fieldName); + + if (!$file || !$file->isValid()) { return null; } + + $allowedMime = [ 'image/jpg', 'image/jpeg', 'image/png', 'image/webp', 'application/pdf']; + + if (!in_array($file->getMimeType(), $allowedMime)) { throw new \RuntimeException('Invalid file format'); } + + $path = ROOTPATH . 'public/uploads/' . $uploadDir . '/'; + + if (!is_dir($path)) { mkdir($path, 0755, true); } + + $newName = $file->getRandomName(); + $file->move($path, $newName); + + return $newName; + } + + + } \ No newline at end of file diff --git a/app/Controllers/PayoutController.php b/app/Controllers/PayoutController.php index 29e8b555..90d501dc 100644 --- a/app/Controllers/PayoutController.php +++ b/app/Controllers/PayoutController.php @@ -51,12 +51,14 @@ class PayoutController extends BaseController $data = $this->request->getPost(); // print_r($data); die; - $agent_id = $data['agent_id'] ?? null; + // $agent_id = $data['agent_id'] ?? null; because dropdown hided + $pos_id = $data['pos_id'] ?? null; $status_id = $data['status_id'] ?? null; $start_date = $data['start_date'] ?? null; $end_date = $data['end_date'] ?? null; - $payout_data = $this->invoiceModel->invoiceList($agent_id, $status_id, $start_date, $end_date); + // $payout_data = $this->invoiceModel->invoiceList($agent_id, $status_id, $start_date, $end_date); // because dropdown hided + $payout_data = $this->invoiceModel->invoiceList($pos_id, $status_id, $start_date, $end_date); $payout_data['payout_list_data'] = $payout_data; $payout_data = view('payout_list', $payout_data); @@ -86,7 +88,8 @@ class PayoutController extends BaseController // for list $data['payout_status'] = $this->payout_status; - $data['agent_list'] = $this->invoiceModel->agentList(); + // $data['agent_list'] = $this->invoiceModel->agentList(); // because dropdown hided + $data['pos_list'] = $this->invoiceModel->posList(); $data['page_name'] = "Invoices"; $payout_data['payout_list_data'] = $this->invoiceModel->invoiceList(); $data['payout_list'] = view('payout_list', $payout_data); @@ -204,8 +207,9 @@ class PayoutController extends BaseController $title = $type === 'add' ? 'Add Payouts' : ($type === 'edit' ? 'Edit Payouts' + : ($type === 'view' ? 'View Payouts' : ($type === 'adjustment' ? 'Payouts Adjustment' - : 'Payouts')); + : 'Payouts'))); $data['tab_name'] = $title; $data['page_name'] = $title; @@ -215,10 +219,12 @@ class PayoutController extends BaseController if($type == 'add') { $data['agents'] = $this->invoiceModel->agentList(['is_active' => 1]); // common both add and edit + $data['pos_list'] = $this->invoiceModel->posList(['is_active' => 1]); // common both add and edit } else { $data['agents'] = $this->invoiceModel->agentList(); // common both add and edit + $data['pos_list'] = $this->invoiceModel->posList(); // common both add and edit } // $data['checked_policy_numbers'] = []; // $data['invoice'] = []; @@ -236,16 +242,21 @@ class PayoutController extends BaseController return $this->loadLayout('invoice_policy_mapping_add', $data); } - if (($type === 'edit' || $type === 'adjustment') && !empty($id)) { + if (($type === 'edit' || $type === 'view' || $type === 'adjustment') && !empty($id)) { $invoice = $this->invoiceModel->where('id', $id)->first(); + // print_r($invoice);die; $data['freeze_edit'] = $this->auditHistory->where('table_name', 'partner_invoice')->where('pk', $id)->countAllResults(); - + if($type === 'view'){ $data['freeze_edit'] = 1; } - $agentId = $invoice['agent_id'] ?? null; + // $agentId = $invoice['agent_id'] ?? null; + $posId = $invoice['pos_id'] ?? null; - $data['payouts'] = $this->invoiceModel->payoutList(2 ,$agentId,$id); - $data['extra_payouts'] = $agentId ? $this->invoiceModel->payoutList(3, $agentId) : []; + // $data['payouts'] = $this->invoiceModel->payoutList(2 ,$agentId,$id); + // $data['extra_payouts'] = $agentId ? $this->invoiceModel->payoutList(3, $agentId) : []; + $data['payouts'] = $this->invoiceModel->payoutList(2 ,$posId,$id); + // print_r($data['payouts']);die; + $data['extra_payouts'] = $posId ? $this->invoiceModel->payoutList(3, $posId) : []; $invoice_items = $this->invoiceItemModel->where('invoice_id', $id)->findAll(); @@ -518,6 +529,7 @@ class PayoutController extends BaseController // Get invoice data from database $invoiceData = $this->getInvoiceData($invoiceId); + // print_r($invoiceData);die; if (empty($invoiceData)) { return $this->respond([ @@ -591,6 +603,27 @@ class PayoutController extends BaseController } + // private function getInvoiceData($invoiceId) + // { + // $invoice_data = $this->invoiceModel + // ->select(' + // partner_invoice.*, + // pa.name as agent_name, + // pa.email as agent_email, + // pa.mobile as agent_mobile, + // pa.address as agent_address, + // pa.agent_code, + // pa.certificate_file_name, + // pa.commission_retain + // ') + // ->join('partner_agent pa', 'partner_invoice.agent_id = pa.id') + // ->where('partner_invoice.is_active', 1) + // ->where('partner_invoice.id', $invoiceId) + // ->first(); + + // return $invoice_data; + // } + private function getInvoiceData($invoiceId) { $invoice_data = $this->invoiceModel @@ -604,9 +637,20 @@ class PayoutController extends BaseController pa.address as agent_address, pa.agent_code, pa.certificate_file_name, - pa.commission_retain + pa.commission_retain, + pos.name as pos_name, + pos.email as pos_email, + pos.mobile as pos_mobile, + pos.address as pos_address, + pos.city as pos_city, + pos.pincode as pos_pincode, + pos.state as pos_state, + pos.pos_code, + pos.certificate_file_name as pos_certificate_file_name, + pos.bank_name,pos.account_holder_name,pos.account_number,pos.ifsc_code, ') - ->join('partner_agent pa', 'partner_invoice.agent_id = pa.id') + ->join('partner_agent pa', 'partner_invoice.agent_id = pa.id','left') + ->join('partner_pos pos', 'partner_invoice.pos_id = pos.id') ->where('partner_invoice.is_active', 1) ->where('partner_invoice.id', $invoiceId) ->first(); diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 7cc91aca..000d9931 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -669,14 +669,43 @@ ->where('is_active', 1) ->findAll(); + // // Fetch Sales Team + // $data['salse_team'] = $this->userModel + // ->select('user_profiles.*,nhance_branch.id as nhance_branch_id, nhance_branch.branch_name as nhance_branch_name') + // ->join('user_teams', 'user_profiles.id = user_teams.user_id') + // ->join('nhance_branch', 'user_profiles.nhance_branch_id = nhance_branch.id','left') + // ->where('user_teams.team_id', 5) + // ->where('user_teams.is_active', 1) + // ->where('user_profiles.is_active', 1) + // ->findAll(); + + // // Fetch Partner Agent + // $data['partner_agent'] = db_connect()->table('partner_agent') + // ->where('is_active', 1) + // ->get() + // ->getResultArray(); + + // // Fetch ACM + // $data['ACM'] = $this->userModel + // ->where('role', 3) // Assuming `role` is in `user_profiles` + // ->where('is_active', 1) + // ->findAll(); + // Fetch Sales Team $data['salse_team'] = $this->userModel - ->select('user_profiles.*,nhance_branch.id as nhance_branch_id, nhance_branch.branch_name as nhance_branch_name') + ->select(' + user_profiles.*, + nhance_branch.id as nhance_branch_id, + nhance_branch.branch_name as nhance_branch_name, + rm.first_name AS rm_name + ') ->join('user_teams', 'user_profiles.id = user_teams.user_id') ->join('nhance_branch', 'user_profiles.nhance_branch_id = nhance_branch.id','left') + ->join('user_profiles AS rm', 'user_profiles.rm_id = rm.id', 'left') ->where('user_teams.team_id', 5) ->where('user_teams.is_active', 1) ->where('user_profiles.is_active', 1) + ->groupBy('user_profiles.id', 'asc') ->findAll(); // Fetch Partner Agent @@ -687,12 +716,20 @@ // Fetch ACM $data['ACM'] = $this->userModel - ->where('role', 3) // Assuming `role` is in `user_profiles` - ->where('is_active', 1) - ->findAll(); - // echo '
';
+                    ->select('
+                        user_profiles.*, 
+                        nhance_branch.id AS nhance_branch_id, 
+                        nhance_branch.branch_name AS nhance_branch_name, 
+                        rm.first_name AS rm_name
+                    ')
+                    ->join('user_teams', 'user_profiles.id = user_teams.user_id')
+                    ->join('nhance_branch', 'user_profiles.nhance_branch_id = nhance_branch.id', 'left')
+                    ->join('user_profiles AS rm', 'user_profiles.rm_id = rm.id', 'left')
+                    ->where('user_profiles.role', 3)  
+                    ->where('user_profiles.is_active', 1)
+                    ->groupBy('user_profiles.id', 'asc')
+                    ->findAll();
             
-            // print_r($data['ppTeamsData']); die;
 
             // dd($data);
 
@@ -897,6 +934,12 @@
         {
             $data = $this->request->getPost();
 
+            $file_data = $this->request->getFiles() ?? null;
+            $data['file_data'] = $file_data ?? null;
+
+            $vehicle_file_data = $this->request->getFiles() ?? null;
+            $data['vehicle_file_data'] = $vehicle_file_data ?? null;
+
             // print_r($data); die;
 
             if (empty($data['policy_issue_date'])) {
@@ -1006,6 +1049,28 @@
                     }
                 }
 
+                // policy file upload 
+                if(isset($data['doc_name']) && isset($data['file_data']) && !empty($data['doc_name']) && !empty($data['file_data'])){
+                    $this->uploadFile($data['doc_name'], $data['file_data'], $insert);
+                }
+
+                // policy file upload 
+                if(
+                    isset($data['other_docs_name']) && isset($data['vehicle_file_data']) && isset($data['client_id']) && isset($data['vehicle_id']) && 
+                    !empty($data['other_docs_name']) && !empty($data['vehicle_file_data']) && !empty($data['client_id']) && !empty($data['vehicle_id'])
+                )
+                {
+                    $vdd = [
+                        'other_docs_name' => $data['other_docs_name'],
+                        'file_data' => $data['vehicle_file_data'],
+                        'client_id' => $data['client_id'],
+                        'vehicle_id' => $data['vehicle_id'],
+                    ];
+                    $clientController = new ClientController();
+                    $clientController->uploadVehicleFile($vdd);
+                }
+                
+
                 $client_data = $this->clientModel->where('id', $data['client_id'])->first();
                 $data['client_kyc'] = $this->clientKYCDocsModel->where('client_id',  $data['client_id'])->findAll();
                 
@@ -2834,20 +2899,23 @@
         //------------------------------------------------------------------------------------------------
 
         //file upload function 
-        public function uploadFile()
+        public function uploadFile($docs_name = null, $files = null, $pt_id = null)
         {
             $GoogleDriveController = new GoogleDriveController();
-            $files = $this->request->getFiles();
-            $doc_name = $this->request->getPost('doc_name[]');
-            $pt_id = $this->request->getPost('pt_id');
-            $client_policy_id = $this->request->getPost('client_policy_id');
-            $data = $this->request->getPost();
+
+
+            if(empty($docs_name) && empty($files) && empty($pt_id) && $this->request){
+                $files = $this->request->getFiles();
+                $docs_name = $this->request->getPost('doc_name');
+                $pt_id = $this->request->getPost('pt_id');
+                $client_policy_id = $this->request->getPost('client_policy_id');
+            }
 
             $uploadFilePath = WRITEPATH . 'uploads/client_kyc_documents';
 
             $uploadData = [];
 
-            foreach ($data['doc_name'] as $key => $docName) {
+            foreach ($docs_name as $key => $docName) {
 
                 // Get the corresponding file for this document name
                 $file = $files['file'][$key];
@@ -2855,7 +2923,7 @@
                 if (!empty($docName) && $file->isValid() && !$file->hasMoved()) {
 
                     // Upload the file
-                    $uploadedFileName = file_Upload($file, $uploadFilePath);
+                    $uploadedFileName = file_Upload_for_lead($file, $uploadFilePath);
 
                     if ($uploadedFileName) {
                         // Prepare data for each document upload
@@ -2880,6 +2948,10 @@
                 }
             }
 
+            if(!empty($docs_name) && !empty($files) && !empty($pt_id) && !$this->request){
+                return true;
+            }
+
             // print_r($uploadData); die;
 
             // $uploadData = uploadFilesToGoogleDrive($files['file'], $doc_name, $pt_id);
@@ -4961,7 +5033,7 @@
                     $policy_transaction_data = [
 
                         'issuer'                => 2,
-                        'issuer_branch'         => $current_nhance_branch['id'] ?? null,
+                        'issuer_branch'         => $salse['nhance_branch_id'] ?? null,
                         'client_id'             => $client_id,
                         'policy_type_id'        => 8,
                         'insurer_id'            => $current_insurer_branch_data['insurer_id'] ?? null,
@@ -4981,10 +5053,12 @@
                         'month'                 => change_date_format($policy_issue_date, 'd/M/Y', 'Y-m-d'),
                         'sales_generated_by'    => $salse['id'] ?? null,
                         'serviced_by'           => $service['id'] ?? null,
+                        'salse_person_manager_id'   => $salse['rm_id'] ?? null,
+                        'service_person_manager_id' => $service['rm_id'] ?? null,
+                        'service_person_branch_id'  => $service['nhance_branch_id'] ?? null,
                         'agent_id'              => $current_agent_data['id'] ?? null,
                         'agent_code'            => $current_agent_data['agent_code'] ?? null,
                         'file_id'               => $params['file_id'],
-
                         'endorsement_no'        => null,
                         'client_branch_id'      => null,
                         'client_policy_id'      => null,
diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php
new file mode 100644
index 00000000..17a60b0e
--- /dev/null
+++ b/app/Controllers/SalesController.php
@@ -0,0 +1,695 @@
+leadModel = new SalesActualLeadModel();
+        $this->contactModel = new SalesContactPersonModel();
+        $this->activityModel = new SalesActivityModel();
+        $this->noteModel = new SalesLeadNoteModel();
+    }
+
+    // ==================== LEAD APIs ====================
+
+    /**
+     * Get all leads with pagination and filters
+     * GET /api/sales/leads
+     */
+    public function getLeads()
+    {
+        try {
+            $limit = $this->request->getGet('limit') ?? 10;
+            $offset = $this->request->getGet('offset') ?? 0;
+            
+            $filters = [
+                'status' => $this->request->getGet('status'),
+                'assigned_to' => $this->request->getGet('assigned_to'),
+                'search' => $this->request->getGet('search'),
+            ];
+
+            $result = $this->leadModel->getLeadsWithFilters($filters, $limit, $offset);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $result['data'],
+                'total' => $result['total'],
+                'limit' => $limit,
+                'offset' => $offset
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Get single lead with complete details
+     * GET /api/sales/leads/{id}
+     */
+    public function getLead($id)
+    {
+        try {
+            $lead = $this->leadModel->getLeadComplete($id);
+
+            if (!$lead) {
+                return $this->failNotFound('Lead not found');
+            }
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $lead
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Create new lead
+     * POST /api/sales/leads
+     */
+    public function createLead()
+    {
+        try {
+            $data = $this->request->getJSON(true);
+            
+            // Set created_by and updated_by from authenticated user
+            $data['created_by'] = $this->getUserId();
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->leadModel->insert($data)) {
+                return $this->fail($this->leadModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $leadId = $this->leadModel->getInsertID();
+            
+            // Insert contact persons if provided
+            if (!empty($data['contact_persons'])) {
+                foreach ($data['contact_persons'] as $contact) {
+                    $contact['lead_id'] = $leadId;
+                    $contact['created_by'] = $this->getUserId();
+                    $this->contactModel->insert($contact);
+                }
+            }
+
+            $lead = $this->leadModel->getLeadComplete($leadId);
+
+            return $this->respondCreated([
+                'status' => 'success',
+                'message' => 'Lead created successfully',
+                'data' => $lead
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Update lead
+     * PUT /api/sales/leads/{id}
+     */
+    public function updateLead($id)
+    {
+        try {
+            if (!$this->leadModel->find($id)) {
+                return $this->failNotFound('Lead not found');
+            }
+
+            $data = $this->request->getJSON(true);
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->leadModel->update($id, $data)) {
+                return $this->fail($this->leadModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $lead = $this->leadModel->getLeadComplete($id);
+
+            return $this->respond([
+                'status' => 'success',
+                'message' => 'Lead updated successfully',
+                'data' => $lead
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Delete lead
+     * DELETE /api/sales/leads/{id}
+     */
+    public function deleteLead($id)
+    {
+        try {
+            if (!$this->leadModel->find($id)) {
+                return $this->failNotFound('Lead not found');
+            }
+
+            $this->leadModel->delete($id);
+
+            return $this->respondDeleted([
+                'status' => 'success',
+                'message' => 'Lead deleted successfully'
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Get lead statistics
+     * GET /api/sales/leads/stats
+     */
+    public function getLeadStats()
+    {
+        try {
+            $userId = $this->request->getGet('user_id');
+            $stats = $this->leadModel->getLeadStats($userId);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $stats
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    // ==================== CONTACT PERSON APIs ====================
+
+    /**
+     * Get contacts for a lead
+     * GET /api/sales/leads/{leadId}/contacts
+     */
+    public function getContacts($leadId)
+    {
+        try {
+            $contacts = $this->contactModel->getContactsByLead($leadId);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $contacts
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Create contact person
+     * POST /api/sales/contacts
+     */
+    public function createContact()
+    {
+        try {
+            $data = $this->request->getJSON(true);
+            $data['created_by'] = $this->getUserId();
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->contactModel->insert($data)) {
+                return $this->fail($this->contactModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $contactId = $this->contactModel->getInsertID();
+            $contact = $this->contactModel->find($contactId);
+
+            return $this->respondCreated([
+                'status' => 'success',
+                'message' => 'Contact created successfully',
+                'data' => $contact
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Update contact person
+     * PUT /api/sales/contacts/{id}
+     */
+    public function updateContact($id)
+    {
+        try {
+            if (!$this->contactModel->find($id)) {
+                return $this->failNotFound('Contact not found');
+            }
+
+            $data = $this->request->getJSON(true);
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->contactModel->update($id, $data)) {
+                return $this->fail($this->contactModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $contact = $this->contactModel->find($id);
+
+            return $this->respond([
+                'status' => 'success',
+                'message' => 'Contact updated successfully',
+                'data' => $contact
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Delete contact person
+     * DELETE /api/sales/contacts/{id}
+     */
+    public function deleteContact($id)
+    {
+        try {
+            if (!$this->contactModel->find($id)) {
+                return $this->failNotFound('Contact not found');
+            }
+
+            $this->contactModel->delete($id);
+
+            return $this->respondDeleted([
+                'status' => 'success',
+                'message' => 'Contact deleted successfully'
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Set primary contact
+     * PUT /api/sales/contacts/{id}/set-primary
+     */
+    public function setPrimaryContact($id)
+    {
+        try {
+            $contact = $this->contactModel->find($id);
+            
+            if (!$contact) {
+                return $this->failNotFound('Contact not found');
+            }
+
+            $this->contactModel->setPrimaryContact($id, $contact['lead_id']);
+
+            return $this->respond([
+                'status' => 'success',
+                'message' => 'Primary contact updated successfully'
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    // ==================== ACTIVITY APIs ====================
+
+    /**
+     * Get all activities with filters
+     * GET /api/sales/activities
+     */
+    public function getActivities()
+    {
+        try {
+            $limit = $this->request->getGet('limit') ?? 10;
+            $offset = $this->request->getGet('offset') ?? 0;
+            
+            $filters = [
+                'status' => $this->request->getGet('status'),
+                'activity_type' => $this->request->getGet('activity_type'),
+                'assigned_to' => $this->request->getGet('assigned_to'),
+                'date_from' => $this->request->getGet('date_from'),
+                'date_to' => $this->request->getGet('date_to'),
+            ];
+
+            $result = $this->activityModel->getActivitiesWithFilters($filters, $limit, $offset);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $result['data'],
+                'total' => $result['total'],
+                'limit' => $limit,
+                'offset' => $offset
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Get activities for a specific lead
+     * GET /api/sales/leads/{leadId}/activities
+     */
+    public function getLeadActivities($leadId)
+    {
+        try {
+            $status = $this->request->getGet('status');
+            $activities = $this->activityModel->getActivitiesByLead($leadId, $status);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $activities
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Get activity timeline for a lead
+     * GET /api/sales/leads/{leadId}/timeline
+     */
+    public function getActivityTimeline($leadId)
+    {
+        try {
+            $timeline = $this->activityModel->getActivityTimeline($leadId);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $timeline
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Get upcoming activities for user
+     * GET /api/sales/activities/upcoming
+     */
+    public function getUpcomingActivities()
+    {
+        try {
+            $userId = $this->getUserId();
+            $days = $this->request->getGet('days') ?? 7;
+            $limit = $this->request->getGet('limit') ?? 10;
+
+            $activities = $this->activityModel->getUpcomingActivities($userId, $days, $limit);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $activities
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Create activity
+     * POST /api/sales/activities
+     */
+    public function createActivity()
+    {
+        try {
+            $data = $this->request->getJSON(true);
+            $data['created_by'] = $this->getUserId();
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->activityModel->insert($data)) {
+                return $this->fail($this->activityModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $activityId = $this->activityModel->getInsertID();
+            $activity = $this->activityModel->find($activityId);
+
+            return $this->respondCreated([
+                'status' => 'success',
+                'message' => 'Activity created successfully',
+                'data' => $activity
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Update activity
+     * PUT /api/sales/activities/{id}
+     */
+    public function updateActivity($id)
+    {
+        try {
+            if (!$this->activityModel->find($id)) {
+                return $this->failNotFound('Activity not found');
+            }
+
+            $data = $this->request->getJSON(true);
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->activityModel->update($id, $data)) {
+                return $this->fail($this->activityModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $activity = $this->activityModel->find($id);
+
+            return $this->respond([
+                'status' => 'success',
+                'message' => 'Activity updated successfully',
+                'data' => $activity
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Complete activity
+     * POST /api/sales/activities/{id}/complete
+     */
+    public function completeActivity($id)
+    {
+        try {
+            $activity = $this->activityModel->find($id);
+            
+            if (!$activity) {
+                return $this->failNotFound('Activity not found');
+            }
+
+            if ($activity['status'] === 'completed') {
+                return $this->fail('Activity is already completed', ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $data = $this->request->getJSON(true);
+            $data['updated_by'] = $this->getUserId();
+
+            $this->activityModel->completeActivity($id, $data);
+
+            // Create follow-up activity if requested
+            if (!empty($data['create_followup']) && $data['create_followup'] === true) {
+                $followupData = [
+                    'lead_id' => $activity['lead_id'],
+                    'activity_type' => $data['followup_type'] ?? 'Call',
+                    'notes' => $data['followup_notes'] ?? '',
+                    'scheduled_date' => $data['followup_date'] ?? null,
+                    'assigned_to' => $activity['assigned_to'],
+                    'parent_activity_id' => $id,
+                    'created_by' => $this->getUserId(),
+                    'updated_by' => $this->getUserId(),
+                ];
+
+                $this->activityModel->insert($followupData);
+            }
+
+            $updatedActivity = $this->activityModel->find($id);
+
+            return $this->respond([
+                'status' => 'success',
+                'message' => 'Activity completed successfully',
+                'data' => $updatedActivity
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Delete activity
+     * DELETE /api/sales/activities/{id}
+     */
+    public function deleteActivity($id)
+    {
+        try {
+            if (!$this->activityModel->find($id)) {
+                return $this->failNotFound('Activity not found');
+            }
+
+            $this->activityModel->delete($id);
+
+            return $this->respondDeleted([
+                'status' => 'success',
+                'message' => 'Activity deleted successfully'
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Get pending activities count
+     * GET /api/sales/activities/pending-count
+     */
+    public function getPendingCount()
+    {
+        try {
+            $userId = $this->getUserId();
+            $count = $this->activityModel->getPendingActivitiesCount($userId);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => ['count' => $count]
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    // ==================== LEAD NOTES APIs ====================
+
+    /**
+     * Get notes for a lead
+     * GET /api/sales/leads/{leadId}/notes
+     */
+    public function getLeadNotes($leadId)
+    {
+        try {
+            $notes = $this->noteModel->getNotesByLead($leadId);
+
+            return $this->respond([
+                'status' => 'success',
+                'data' => $notes
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Create note
+     * POST /api/sales/notes
+     */
+    public function createNote()
+    {
+        try {
+            $data = $this->request->getJSON(true);
+            $data['user_id'] = $this->getUserId();
+            $data['created_by'] = $this->getUserId();
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->noteModel->insert($data)) {
+                return $this->fail($this->noteModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $noteId = $this->noteModel->getInsertID();
+            $note = $this->noteModel->find($noteId);
+
+            return $this->respondCreated([
+                'status' => 'success',
+                'message' => 'Note created successfully',
+                'data' => $note
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Update note
+     * PUT /api/sales/notes/{id}
+     */
+    public function updateNote($id)
+    {
+        try {
+            $note = $this->noteModel->find($id);
+            
+            if (!$note) {
+                return $this->failNotFound('Note not found');
+            }
+
+            // Check if user owns the note
+            if ($note['user_id'] != $this->getUserId()) {
+                return $this->failUnauthorized('You are not authorized to update this note');
+            }
+
+            $data = $this->request->getJSON(true);
+            $data['updated_by'] = $this->getUserId();
+
+            if (!$this->noteModel->update($id, $data)) {
+                return $this->fail($this->noteModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
+            }
+
+            $updatedNote = $this->noteModel->find($id);
+
+            return $this->respond([
+                'status' => 'success',
+                'message' => 'Note updated successfully',
+                'data' => $updatedNote
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    /**
+     * Delete note
+     * DELETE /api/sales/notes/{id}
+     */
+    public function deleteNote($id)
+    {
+        try {
+            $note = $this->noteModel->find($id);
+            
+            if (!$note) {
+                return $this->failNotFound('Note not found');
+            }
+
+            // Check if user owns the note
+            if ($note['user_id'] != $this->getUserId()) {
+                return $this->failUnauthorized('You are not authorized to delete this note');
+            }
+
+            $this->noteModel->delete($id);
+
+            return $this->respondDeleted([
+                'status' => 'success',
+                'message' => 'Note deleted successfully'
+            ]);
+        } catch (\Exception $e) {
+            return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    // ==================== HELPER METHODS ====================
+
+    /**
+     * Get current user ID from session or JWT token
+     * You'll need to implement this based on your authentication system
+     */
+    private function getUserId()
+    {
+        // If using session
+        if (session()->has('user_id')) {
+            return session()->get('user_id');
+        }
+
+        // If using JWT, decode token and get user_id
+        // Example: return $this->decodeJWT()['user_id'];
+
+        // For development/testing, you can return a default value
+        return 1;
+    }
+}
\ No newline at end of file
diff --git a/app/Controllers/UserController.php b/app/Controllers/UserController.php
index 3879b49d..22451e89 100755
--- a/app/Controllers/UserController.php
+++ b/app/Controllers/UserController.php
@@ -585,44 +585,79 @@ class UserController extends AdminController
         try{
             // Listing
             if ($method === 'get') {  
-                $types = $this->partnerStaffModel->findAll();
-                if (empty($types)) {
-                    return $this->response->setJSON(['status'  => 'error','message' => 'No Staff found'])->setStatusCode(404);
+                $manager = $this->partnerStaffModel->where('role_id',1)->findAll();
+                if (empty($manager)) {
+                    return $this->response->setJSON(['status'  => 'error','message' => 'No Manager found'])->setStatusCode(404);
                 }
-                return $this->response->setJSON(['status' => 'success','data'   => $types])->setStatusCode(200);
+                return $this->response->setJSON(['status' => 'success','data'   => $manager])->setStatusCode(200);
             } 
             
             // add/update
             if ($method === 'post') {
-    
                 $data = $this->request->getPost();
-                if (!empty($data['id'])) {
-                    $text   = "update";            
-                    $data['updated_by'] =  get_session_userid();
-                    $result = $this->partnerStaffModel->update($data['id'], $data);
-                    $updateID = $data['id'];
-                } else {
-                    $text   = "create";        
-                    $data['role_id'] = 1;
-                    $data['created_by'] =  get_session_userid();
-                    $insertID  = $this->partnerStaffModel->insert($data);
-                    if($insertID){
-                        $details['manager_id'] = $insertID;
-                        $details['updated_by'] =  get_session_userid();
-                        $this->partnerStaffModel->update($insertID, $details);
+                $id = !empty($data['PrimaryKey']) ? $data['PrimaryKey'] : null;
+                
+                // don't forgot same means just unset the key because partner_staff some UNIQUE KEY sets in table thats why
+                if ($id) {
+                    $existing = $this->partnerStaffModel->find($id);
+                    if ($existing) {
+                        if ($data['email'] === $existing['email']) { unset($data['email']); }
+                        if ($data['mobile'] === $existing['mobile']) { unset($data['mobile']); }
                     }
-                    $result = true;
                 }
 
-                $id = isset($insertID) && !empty($insertID) ? $insertID : ($updateID ?? null);
+                $errors = [];
+
+                // Check Email Duplicate (if it wasn't unset)
+                if (isset($data['email'])) {
+                    $count = $this->partnerStaffModel->where('email', $data['email'])->countAllResults();
+                    if ($count > 0) $errors['email'] = "This email is already taken by another user.";
+                }
+
+                // Check Mobile Duplicate (if it wasn't unset)
+                if (isset($data['mobile'])) {
+                    $count = $this->partnerStaffModel->where('mobile', $data['mobile'])->countAllResults();
+                    if ($count > 0) $errors['mobile'] = "This mobile is already taken by another user.";
+                }
+
+                // If there are duplicates, stop and return error
+                if (!empty($errors)) {
+                    return $this->response->setJSON([
+                        'status'  => 'error',
+                        'message' => 'Duplicate detected',
+                        'errors'  => $errors
+                    ])->setStatusCode(400);
+                }
+
+                // --- Save/Update ---
+                if ($id) {
+                    $text   = "update";            
+                    $data['updated_by'] =  get_session_userid();
+                    
+                    $result = $this->partnerStaffModel->update($updateID, $data);
+                } else {
+                    $text = "create";
+                    $data['role_id'] = 1;
+                    $data['created_by'] = get_session_userid();
+                    $id = $this->partnerStaffModel->insert($data);
+                    if($id){
+                        $details['manager_id'] = $id;
+                        $details['updated_by'] =  get_session_userid();
+                        $this->partnerStaffModel->update($id, $details);
+                    }
+                    $result = true;
+                    
+                }
+
+                 
 
                 return $this->response->setJSON([ 
                     'status'  => $id ? 'success' : 'error',
                     'message' => $id ? "Staff {$text}d successfully" : "Unable to {$text} staff. Please try again.",
                     'id'      => $id
                 ])->setStatusCode($id ? 200 : 400);
-            }
 
+            }
             // delete
             if ($method === 'put') {
                     $data = $this->request->getRawInput();
diff --git a/app/Helpers/excel_util_helper.php b/app/Helpers/excel_util_helper.php
index 918a04c3..f1269f3d 100755
--- a/app/Helpers/excel_util_helper.php
+++ b/app/Helpers/excel_util_helper.php
@@ -3041,6 +3041,11 @@ if (!function_exists('validate_indian_vehicle_number')) {
     function validate_indian_vehicle_number($number)
     {
         $number = strtoupper(trim($number));
+        $new_vehicle = strtolower(trim($number));
+
+        if($new_vehicle == "new"){
+            return ['status' => true, 'error' => null];
+        }
 
         // Normal Format:
         // 2 letters (state) + 2 digits (district) + 1 or 2 letters (series) + 4 digits
@@ -3120,9 +3125,18 @@ if (!function_exists('check_agent_exist')) {
 
 if (!function_exists('check_rto_data')) {
     function check_rto_data($row, $rto_master)
-    {
+    {   
         // Vehicle number from uploaded row
         $vehicle_no = strtoupper(trim($row[2]));  // Example: TN10AB1234
+        $new_vehicle = strtolower(trim($row[2]));
+
+        if($new_vehicle == "new"){
+            return [
+                'status' => true,
+                'error'  => null,
+                'rto_data'  => []
+            ];
+        }
 
         // Must be at least 4 characters to extract RTO
         if (strlen($vehicle_no) < 4) {
diff --git a/app/Models/FAQModel.php b/app/Models/FAQModel.php
new file mode 100644
index 00000000..7fe5dd48
--- /dev/null
+++ b/app/Models/FAQModel.php
@@ -0,0 +1,55 @@
+select("
                 partner_invoice.*,
@@ -105,14 +105,21 @@ class InvoiceModel extends Model
                     WHEN payout_status = 2 THEN 'Completed'
                 END AS status_text,
 
-                partner_agent.name as agent_name
+                partner_agent.name as agent_name,
+                partner_pos.name as pos_name
+
             ")
-        ->join('partner_agent', 'partner_invoice.agent_id = partner_agent.id')
+        ->join('partner_agent', 'partner_invoice.agent_id = partner_agent.id','left') 
+        ->join('partner_pos', 'partner_invoice.pos_id = partner_pos.id')
         ->where('partner_invoice.is_active', 1)
         ->where('partner_invoice.broker_id', 1);
 
-        if(!empty($agent_id)){
-            $data->where('partner_invoice.agent_id', $agent_id);
+        // if(!empty($agent_id)){
+        //     $data->where('partner_invoice.agent_id', $agent_id);
+        // } // because dropdown hided
+
+        if(!empty($pos_id)){
+            $data->where('partner_invoice.pos_id', $pos_id);
         }
 
         if(!empty($status_id)){
@@ -128,7 +135,8 @@ class InvoiceModel extends Model
                 ->where('partner_invoice.invoice_date <=', $endDate);
         } 
 
-        if(!empty($agent_id) && !empty($status_id) && !empty($start_date) && !empty($end_date)){
+        // if(!empty($agent_id) && !empty($status_id) && !empty($start_date) && !empty($end_date)){
+        if(!empty($pos_id) && !empty($status_id) && !empty($start_date) && !empty($end_date)){
 
             $fromDate = date('Y-m-d', strtotime('-60 days'));
             $toDate = date('Y-m-d 23:59:59');
@@ -153,6 +161,21 @@ class InvoiceModel extends Model
         return $this->db->table('partner_agent')->get()->getResultArray();
     }
 
+    public function posList(array $params = [])
+    {
+            $builder = $this->db->table('partner_pos')
+                ->select('partner_pos.*, partner_staff.name AS manager_name')
+                ->join('partner_staff', 'partner_staff.id = partner_pos.manager_id', 'left')
+                ->orderBy('partner_pos.id', 'DESC');
+
+            if (isset($params['is_active'])) {
+                $builder->where('partner_pos.is_active', $params['is_active']);
+            }
+
+            return $builder->get()->getResultArray();
+    }
+
+
     public function utrSummary($invoice_id)
     {
         $data = $this->select("
@@ -194,7 +217,7 @@ class InvoiceModel extends Model
     }
 
     
-        public function payoutList($flag, $agentId = null, $invoiceId = null)
+        public function payoutList($flag, $posId = null, $invoiceId = null)
         {
             $builder = $this->db->table('policy_transaction pt')
                 ->select('
@@ -210,13 +233,15 @@ class InvoiceModel extends Model
                     pii.commission_amount as paid_amount,
                     pi.payout_status,
                     pp.id as partner_policy_id,
-                    pp.policy_transaction_id
+                    pp.policy_transaction_id,
+                    pt.pos_id as posId
                 ')
                 ->join('partner_invoice_items pii','pii.policy_no = pt.policy_no','left')
                 ->join('partner_invoice pi','pi.id = pii.invoice_id','left')
                 ->join('partner_policy pp','pt.policy_no = pp.policy_number AND pt.agent_id = pp.agent_id AND pt.id = pp.policy_transaction_id')
                 ->where('pt.is_active',1)
-                ->where('pt.agent_id IS NOT NULL');
+                ->where('pt.pos_id IS NOT NULL');
+                // ->where('pt.agent_id IS NOT NULL');
 
             if ($flag == 1) { // Add mode
                     // EXCLUDE all policies that exist in partner_invoice_items            
@@ -230,16 +255,22 @@ class InvoiceModel extends Model
                 if ($invoiceId) {
                     $builder->where('pi.id', $invoiceId); // only policies of this invoice
                 }
-                if ($agentId) {
-                    $builder->where('pt.agent_id', $agentId);
+                // if ($agentId) {
+                //     $builder->where('pt.agent_id', $agentId);
+                // }
+                if ($posId) {
+                    $builder->where('pt.pos_id', $posId);
                 }
             }
 
             if ($flag == 3) { // Extra policies
                 // Policies not assigned to any invoice
                 $builder->where('pii.id IS NULL', null, false);
-                if ($agentId) {
-                    $builder->where('pt.agent_id', $agentId);
+                // if ($agentId) {
+                //     $builder->where('pt.agent_id', $agentId);
+                // }
+                if ($posId) {
+                    $builder->where('pt.pos_id', $posId);
                 }
             }
 
diff --git a/app/Models/PartnerPosModel.php b/app/Models/PartnerPosModel.php
new file mode 100644
index 00000000..4b16c161
--- /dev/null
+++ b/app/Models/PartnerPosModel.php
@@ -0,0 +1,135 @@
+ 'required|min_length[3]',
+    //     'email' => 'permit_empty|valid_email|is_unique[partner_pos.email,id,{id}]',
+    //     'mobile' => 'permit_empty|numeric|max_length[20]|is_unique[partner_pos.mobile,id,{id}]',
+    //     'manager_id' => 'required|integer',
+    // ];
+
+    // protected $validationMessages = [
+    //     'email' => [
+    //         'is_unique' => 'Sorry, that email address is already registered.',
+    //     ],
+    // ];
+
+    /**
+     * @var bool Whether to skip validation during inserts and updates.
+     */
+    protected $skipValidation = false;
+    
+    /**
+     * @var bool Whether to clean data for SQL Injection.
+     */
+    protected $cleanValidationRules = true;
+
+    protected $beforeInsert         = ["checkAndADDCreatedByValue"];
+    protected $beforeUpdate         = ["checkAndUpdateUpdatedByValue"];
+    
+        protected function checkAndADDCreatedByValue(array $data)
+    {
+        if (empty($data['data']['created_by'])) {
+            $data['data']['created_by'] = get_session_userid();
+        }
+
+        return $data;
+    }
+
+    protected function checkAndUpdateUpdatedByValue(array $data)
+    {
+        if (empty($data['data']['updated_by'])) {
+            $data['data']['updated_by'] = get_session_userid();
+        }
+
+        $data['data']['updated_on'] = date('Y-m-d H:i:s');
+        
+        return $data;
+    }
+
+}
\ No newline at end of file
diff --git a/app/Models/PartnerStaffModel.php b/app/Models/PartnerStaffModel.php
index 340a0ec5..8c5ca862 100755
--- a/app/Models/PartnerStaffModel.php
+++ b/app/Models/PartnerStaffModel.php
@@ -16,7 +16,7 @@ class PartnerStaffModel extends Model
     protected $useSoftDeletes = false;
 
     // allowed fields for insert/update
-    protected $allowedFields = ['name','email','mobile','emp_id','role_id','email_otp','is_active','created_by','updated_by','manager_id'];
+    protected $allowedFields = ['name','email','mobile','emp_id','role_id','email_otp','is_active','created_by','updated_by','manager_id','retention_rate','nhance_branch_id'];
 
     // automatic timestamps
     protected $useTimestamps = true;
diff --git a/app/Models/PolicyTransactionModel.php b/app/Models/PolicyTransactionModel.php
index 1ac58a29..f6822911 100644
--- a/app/Models/PolicyTransactionModel.php
+++ b/app/Models/PolicyTransactionModel.php
@@ -2572,6 +2572,7 @@
                         cb.address1 AS client_address,
                         ptype.policy_type,
                         ptype.bap,
+                        ins.id AS insurer_id,
                         ins.name AS insurer_name,
                         ins.short_name AS insurer_short_name,
                         ib.branch_name AS insurer_branch_name,
@@ -2596,7 +2597,6 @@
                         ROUND(pcsd.cotp_amt + pcsd.cotep_amt, 2) AS tp_or_ter,
                         DATEDIFF(pt.policy_end_date, CURDATE()) AS days,
                         (pcsd.agreed_tp_per + pcsd.agreed_tep_per) AS agreed_tp_or_ter_per,
-                        pcsd.agreed_bp_per,
 
                         ROUND(pcsd.exp_amt, 2) AS total_irda_amt,
                         0.00 AS reward,
@@ -2607,7 +2607,55 @@
                             WHEN pcsd.co_share_type > 1 THEN 
                                 IFNULL(NULLIF(pcsd.follower_policy_no,''), pt.policy_no)
                             ELSE pt.policy_no
-                        END AS policy_no
+                        END AS policy_no,
+
+                        CASE 
+                            WHEN pt.co_share = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS co_share,
+
+                        CASE 
+                            WHEN pt.bro_payable_by = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS bro_payable_by,
+
+                        pcsd.non_comm_per_amt,
+
+                        pcsd.bp_igst,
+                        pcsd.bp_sgst,
+                        pcsd.bp_cgst, 
+
+                        pcsd.agreed_bp_per,    
+                        ROUND(pcsd.agreed_tp_per + pcsd.agreed_tep_per,2) AS agreed_tp_per, 
+
+                        pcsd.standerd_bp_per,
+                        ROUND(pcsd.standerd_tp_per + pcsd.standerd_tep_per,2) AS standerd_tp_per, 
+
+                        0 AS actual_bp_amt,
+                        ROUND(0, 2) AS actual_tp_amt,
+
+                        0 AS actual_bp_per,
+                        ROUND(0, 2) AS actual_tp_per,
+
+                        0 AS actual_tep_brokerage_amt,
+                        ROUND(0, 2) AS actual_tp_brokerage_amt,
+         
+                        service_branch.branch_name as service_branch,
+
+                        salse_manager.first_name as salse_manager_name,
+                        service_manager.first_name as service_manager_name,
+                        cd_master.cd_ac_no,
+                        pt.rollover_date,
+                        pt.policy_holder_name,
+
+                        CASE 
+                            WHEN pt.same_as_proposer = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS same_as_proposer,
+
+                        pcsd.follower_policy_no,
+                        pcsd.co_share_per,
+                        pcsd.stamp_duty
 
                     FROM policy_transaction pt
                     LEFT JOIN pt_co_share_details pcsd ON pt.id = pcsd.pt_id
@@ -2626,6 +2674,11 @@
                     LEFT JOIN user_profiles created_user ON pt.created_by = created_user.id
                     LEFT JOIN nhance_branch ON pt.issuer_branch = nhance_branch.id
 
+                    LEFT JOIN nhance_branch service_branch ON pt.service_person_branch_id = service_branch.id
+                    LEFT JOIN user_profiles salse_manager ON pt.salse_person_manager_id = salse_manager.id
+                    LEFT JOIN user_profiles service_manager ON pt.service_person_manager_id = service_manager.id
+                    LEFT JOIN cd_master ON pt.cd_ac_pk = cd_master.id
+
                     WHERE pt.is_active = 1
                     AND pcsd.is_active = 1
                     $default_date_filter
@@ -2675,20 +2728,13 @@
                         cb.address1 AS client_address,
                         ptype.policy_type,
                         ptype.bap,
+                        ins.id AS insurer_id,
                         ins.name AS insurer_name,
                         ins.short_name AS insurer_short_name,
                         ib.branch_name AS insurer_branch_name,
                         ib.branch_code AS insurer_branch_code,
 
-                        CASE 
-                            WHEN 
-                                IFNULL(cssd.actual_tep_brokerage_amt,0) = 0 AND
-                                IFNULL(cssd.actual_tp_brokerage_amt,0) = 0 AND
-                                IFNULL(cssd.actual_bp_brokerage_amt,0) = 0 AND
-                                IFNULL(cssd.reward,0) != 0
-                            THEN 'Rewards'
-                            ELSE created_user.first_name
-                        END AS user_name,
+                        created_user.first_name as user_name,
                         
                         v.vehicle_no,
                         tpa.name AS tpa_name,
@@ -2708,29 +2754,19 @@
                         ROUND(pcsd.cotp_amt + pcsd.cotep_amt,2) AS tp_or_ter,
                         DATEDIFF(pt.policy_end_date, CURDATE()) AS days,
                         (pcsd.agreed_tp_per + pcsd.agreed_tep_per) AS agreed_tp_or_ter_per,
-                        pcsd.agreed_bp_per,
 
                         CASE 
                             WHEN insq.month = pt.month THEN pcsd.exp_amt
                             ELSE 0
                         END AS total_irda_amt_2,
 
-                        COALESCE((
-                            SELECT SUM(cs.reward)
-                            FROM co_share_stmt_details cs
-                            JOIN insurer_statements i ON cs.statement_id = i.id
-                            WHERE cs.co_share_id = pcsd.id AND i.month = insq.month
-                            AND i.is_active = 1
-                            AND cs.is_active = 1
-                            GROUP BY pt.policy_no, i.month, pcsd.insurer_id
-                        ),0) AS reward,
+                        0 AS reward,
 
                         COALESCE((
                             SELECT SUM(
                                 COALESCE(cs.actual_bp_brokerage_amt,0) +
                                 COALESCE(cs.actual_tp_brokerage_amt,0) +
-                                COALESCE(cs.actual_tep_brokerage_amt,0) +
-                                COALESCE(cs.reward,0)
+                                COALESCE(cs.actual_tep_brokerage_amt,0)
                             )
                             FROM co_share_stmt_details cs
                             JOIN insurer_statements i ON cs.statement_id = i.id
@@ -2747,7 +2783,55 @@
                             WHEN pcsd.co_share_type > 1 
                                 THEN IFNULL(NULLIF(pcsd.follower_policy_no,''), pt.policy_no)
                             ELSE pt.policy_no
-                        END AS policy_no
+                        END AS policy_no,
+
+                        CASE 
+                            WHEN pt.co_share = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS co_share,
+
+                        CASE 
+                            WHEN pt.bro_payable_by = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS bro_payable_by,
+
+                        pcsd.non_comm_per_amt,
+
+                        pcsd.bp_igst,
+                        pcsd.bp_sgst,
+                        pcsd.bp_cgst, 
+
+                        pcsd.agreed_bp_per,    
+                        ROUND(pcsd.agreed_tp_per + pcsd.agreed_tep_per,2) AS agreed_tp_per, 
+
+                        pcsd.standerd_bp_per,
+                        ROUND(pcsd.standerd_tp_per + pcsd.standerd_tep_per,2) AS standerd_tp_per, 
+
+                        cssd.actual_bp_amt,
+                        ROUND(cssd.actual_tp_amt + cssd.actual_tep_amt,2) AS actual_tp_amt,    
+
+                        cssd.actual_bp_per,
+                        ROUND(cssd.actual_tp_per + cssd.actual_tep_per,2) AS actual_tp_per,    
+
+                        cssd.actual_tep_brokerage_amt,
+                        ROUND(cssd.actual_tp_brokerage_amt + cssd.actual_bp_brokerage_amt,2) AS actual_tp_brokerage_amt,
+                        
+                        service_branch.branch_name as service_branch,
+
+                        salse_manager.first_name as salse_manager_name,
+                        service_manager.first_name as service_manager_name,
+                        cd_master.cd_ac_no,
+                        pt.rollover_date,
+                        pt.policy_holder_name,
+
+                        CASE 
+                            WHEN pt.same_as_proposer = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS same_as_proposer,
+
+                        pcsd.follower_policy_no,
+                        pcsd.co_share_per,
+                        pcsd.stamp_duty
 
                     FROM policy_transaction pt
                     LEFT JOIN pt_co_share_details pcsd ON pt.id = pcsd.pt_id
@@ -2768,6 +2852,12 @@
                     LEFT JOIN user_profiles created_user ON pt.created_by = created_user.id
                     LEFT JOIN nhance_branch ON pt.issuer_branch = nhance_branch.id
 
+                    LEFT JOIN nhance_branch service_branch ON pt.service_person_branch_id = service_branch.id
+                    LEFT JOIN user_profiles salse_manager ON pt.salse_person_manager_id = salse_manager.id
+                    LEFT JOIN user_profiles service_manager ON pt.service_person_manager_id = service_manager.id
+                    LEFT JOIN cd_master ON pt.cd_ac_pk = cd_master.id
+
+
                     WHERE pt.is_active = 1
                     AND pcsd.is_active = 1
                     AND cssd.is_active = 1
@@ -2777,11 +2867,184 @@
 
                     GROUP BY pt.policy_no, pt.endorsement_no, pcsd.insurer_id, insq.month
 
+
+                    UNION ALL
+
+                    SELECT 
+
+                        pt.id AS id,
+                        pt.endorsement_no,
+                        pt.ref,
+
+                        pt.data_received_date,
+                        pt.renewal_date,
+                        pt.installment,
+                        nhance_branch.branch_name as nhance_branch,
+
+                        DATE_FORMAT(pt.policy_issue_date, '%d %b %Y') AS policy_issue_date,
+                        DATE_FORMAT(IF(insq.month IS NULL, pcsd.pt_policy_issue_date, insq.month),'%b %Y') AS policy_issue_month,
+                        insq.month as statement_month,
+
+                        'statement uploaded' AS statement_uploaded,
+
+                        CASE 
+                            WHEN c.client_type = 1 THEN 'Group'
+                            WHEN c.client_type = 2 THEN 'Retail'
+                            ELSE '-'
+                        END AS client_type,
+
+                        CASE 
+                            WHEN pt.revenue_type = 'NA' THEN 'Fresh'
+                            ELSE 'Renewal'
+                        END AS revenue_type,
+
+                        CASE 
+                            WHEN IFNULL(cssd.reward,0) <> 0 THEN 'Rewards'
+                            ELSE 'Rewards'
+                        END AS action_type,
+
+
+                        pt.action_type as action_type_string,
+
+                        c.client_name,
+                        c.short_name AS client_short_name,
+                        cb.branch_name AS client_branch_name,
+                        cb.address1 AS client_address,
+                        ptype.policy_type,
+                        ptype.bap,
+                        ins.id AS insurer_id,
+                        ins.name AS insurer_name,
+                        ins.short_name AS insurer_short_name,
+                        ib.branch_name AS insurer_branch_name,
+                        ib.branch_code AS insurer_branch_code,
+
+                        created_user.first_name as user_name,
+                        
+                        v.vehicle_no,
+                        tpa.name AS tpa_name,
+
+                        pcsd.remark AS remarks,
+                        pcsd.cop_amt AS bp_amt,
+                        pcsd.exp_amt,
+                        pcsd.id AS pt_id,
+
+                        su.first_name AS salse_person_name,
+                        se.first_name AS service_person_name,
+
+                        ROUND(pcsd.cop_amt + pcsd.cotp_amt + pcsd.cotep_amt,2) AS premium_wo_gst,
+                        ROUND((pcsd.cop_amt + pcsd.cotp_amt + pcsd.cotep_amt) * 0.18,2) AS gst_amount,
+                        ROUND((pcsd.cop_amt + pcsd.cotp_amt + pcsd.cotep_amt) * 1.18,2) AS total_premium,
+
+                        ROUND(pcsd.cotp_amt + pcsd.cotep_amt,2) AS tp_or_ter,
+                        DATEDIFF(pt.policy_end_date, CURDATE()) AS days,
+                        (pcsd.agreed_tp_per + pcsd.agreed_tep_per) AS agreed_tp_or_ter_per,
+
+                        0 AS total_irda_amt_2,
+
+                        COALESCE((
+                            SELECT SUM(cs.reward)
+                            FROM co_share_stmt_details cs
+                            JOIN insurer_statements i ON cs.statement_id = i.id
+                            WHERE cs.co_share_id = pcsd.id
+                            AND i.month = insq.month
+                            AND i.is_active = 1
+                            AND cs.is_active = 1
+                            GROUP BY pt.policy_no, i.month, pcsd.insurer_id
+                        ),0) AS reward,
+
+                        0 AS billed_amt,
+
+                        CASE 
+                            WHEN pcsd.co_share_type IN (0,1) THEN pt.policy_no
+                            WHEN pcsd.co_share_type > 1 
+                                THEN IFNULL(NULLIF(pcsd.follower_policy_no,''), pt.policy_no)
+                            ELSE pt.policy_no
+                        END AS policy_no,
+
+                        CASE 
+                            WHEN pt.co_share = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS co_share,
+
+                        CASE 
+                            WHEN pt.bro_payable_by = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS bro_payable_by,
+
+                        pcsd.non_comm_per_amt,
+
+                        pcsd.bp_igst,
+                        pcsd.bp_sgst,
+                        pcsd.bp_cgst, 
+
+                        pcsd.agreed_bp_per,    
+                        ROUND(pcsd.agreed_tp_per + pcsd.agreed_tep_per,2) AS agreed_tp_per, 
+
+                        pcsd.standerd_bp_per,
+                        ROUND(pcsd.standerd_tp_per + pcsd.standerd_tep_per,2) AS standerd_tp_per, 
+
+                        cssd.actual_bp_amt,
+                        ROUND(cssd.actual_tp_amt + cssd.actual_tep_amt,2) AS actual_tp_amt,    
+
+                        cssd.actual_bp_per,
+                        ROUND(cssd.actual_tp_per + cssd.actual_tep_per,2) AS actual_tp_per,    
+
+                        cssd.actual_tep_brokerage_amt,
+                        ROUND(cssd.actual_tp_brokerage_amt + cssd.actual_bp_brokerage_amt,2) AS actual_tp_brokerage_amt,
+                        
+                        service_branch.branch_name as service_branch,
+
+                        salse_manager.first_name as salse_manager_name,
+                        service_manager.first_name as service_manager_name,
+                        cd_master.cd_ac_no,
+                        pt.rollover_date,
+                        pt.policy_holder_name,
+
+                        CASE 
+                            WHEN pt.same_as_proposer = 1 THEN 'Yes'
+                            ELSE 'No'
+                        END AS same_as_proposer,
+
+                        pcsd.follower_policy_no,
+                        pcsd.co_share_per,
+                        pcsd.stamp_duty
+
+                    FROM policy_transaction pt
+                    LEFT JOIN pt_co_share_details pcsd ON pt.id = pcsd.pt_id
+                    LEFT JOIN co_share_stmt_details cssd ON pcsd.id = cssd.co_share_id
+                    LEFT JOIN insurer_statements insq ON cssd.statement_id = insq.id
+                    JOIN clients c ON c.id = pt.client_id
+                    LEFT JOIN client_branch cb ON pt.client_branch_id = cb.id
+                    LEFT JOIN client_policy cp ON pt.client_policy_id = cp.id
+                    LEFT JOIN user_profiles up ON pt.created_by = up.id
+                    LEFT JOIN vehicle v ON pt.vehicle_id = v.id
+                    LEFT JOIN policy_type ptype ON pt.policy_type_id = ptype.id
+                    LEFT JOIN insurers ins ON pcsd.insurer_id = ins.id
+                    LEFT JOIN insurer_branch ib  ON pcsd.insurer_branch_id = ib.id
+                    LEFT JOIN tpa ON pt.tpa_id = tpa.id
+                    LEFT JOIN tpa_branch tb ON pt.tpa_branch_id = tb.id
+                    LEFT JOIN user_profiles su ON pt.sales_generated_by = su.id
+                    LEFT JOIN user_profiles se ON pt.serviced_by = se.id
+                    LEFT JOIN user_profiles created_user ON pt.created_by = created_user.id
+                    LEFT JOIN nhance_branch ON pt.issuer_branch = nhance_branch.id
+
+                    LEFT JOIN nhance_branch service_branch ON pt.service_person_branch_id = service_branch.id
+                    LEFT JOIN user_profiles salse_manager ON pt.salse_person_manager_id = salse_manager.id
+                    LEFT JOIN user_profiles service_manager ON pt.service_person_manager_id = service_manager.id
+                    LEFT JOIN cd_master ON pt.cd_ac_pk = cd_master.id
+
+                    WHERE pt.is_active = 1
+                    AND pcsd.is_active = 1
+                    AND cssd.is_active = 1
+                    AND insq.is_active = 1
+                    $default_date_filter
+                    $conditions
+
                 ) AS final_result
                    
                    $statement_month_condition
                 -- GROUP BY statement_month, insurer_name, policy_no
-                -- WHERE id = 6063
+                -- WHERE id = 6143
 
                 ORDER BY 
                     id DESC,
@@ -2796,62 +3059,159 @@
             // dd($result);
             // dd($this->db->getLastQuery());
 
-            $keys = [];
-            $filtered = [];
 
-            foreach ($result as $row) {
+            foreach ($result as $key => &$row) {
 
-                $endorsement_number = "-";
-                if(!empty($row['endorsement_no'])){
-                    $endorsement_number = $row['endorsement_no'];
+                // If both rewards and billed amount are zero, remove the row
+                if (
+                    ($row['reward'] ?? 0) == 0.00 &&
+                    ($row['billed_amt'] ?? 0) == 0.00
+                ) {
+                    unset($result[$key]); // ✅ correct way
                 }
 
-                $key = $row['statement_month'].'|'.$row['insurer_name'].'|'.$row['policy_no'] . '|' .  $endorsement_number;
+                // If rewards exist, zero the IRDA amount
+                // if (($row['reward'] ?? 0) != 0.00) {
+                //     $row['total_irda_amt'] = '0.00';
+                //     $row['billed_amt'] = $row['reward'];
+                // }
 
-                // If uploaded record exists, always keep it and override previous
-                if ($row['statement_uploaded'] === 'statement uploaded') {
-                    $filtered[$key] = $row;  // overwrite no-statement row if it exists
-                    $keys[$key] = true;
+                $reward = (float) ($row['reward'] ?? 0);
 
-                } 
-                // Keep "no statement uploaded" only if uploaded one not added yet
-                elseif (!isset($keys[$key])) {
-                    $filtered[$key] = $row;
+                if ($reward > 0) {
+                    $row['total_irda_amt'] = '0.00';
+                    $row['billed_amt']    = $reward;
+                }
+
+            }
+            unset($row);
+            
+            // $result = array_values($result);
+
+            usort($result, function ($a, $b) {
+
+                // First: group by policy_issue_date
+                if ($a['policy_issue_month'] === $b['policy_issue_month']) {
+
+                    // Priority order for action_type
+                    $priority = [
+                        'Policy'      => 1,
+                        'Endorsement' => 2,
+                        'Rewards'     => 3
+                    ];
+
+                    $aPriority = $priority[$a['action_type']] ?? 99;
+                    $bPriority = $priority[$b['action_type']] ?? 99;
+
+                    return $aPriority <=> $bPriority;
+                }
+
+                // Otherwise sort by policy_issue_date
+                return strtotime($a['policy_issue_month']) <=> strtotime($b['policy_issue_month']);
+            });
+
+            // dd($result);
+
+            // --------------------------------------------------------------------------------------------------------
+
+            // $keys = [];
+            // $filtered = [];
+
+            // foreach ($result as $row) {
+
+            //     $endorsement_number = "-";
+            //     if(!empty($row['endorsement_no'])){
+            //         $endorsement_number = $row['endorsement_no'];
+            //     }
+
+            //     $key = $row['statement_month'].'|'.$row['insurer_name'].'|'.$row['policy_no'] . '|' .  $endorsement_number;
+
+            //     // If uploaded record exists, always keep it and override previous
+            //     if ($row['statement_uploaded'] === 'statement uploaded') {
+            //         $filtered[$key] = $row;  // overwrite no-statement row if it exists
+            //         $keys[$key] = true;
+
+            //     } 
+            //     // Keep "no statement uploaded" only if uploaded one not added yet
+            //     elseif (!isset($keys[$key])) {
+            //         $filtered[$key] = $row;
+            //     }
+            // }
+
+            // $result = array_values($filtered);
+            // dd($result);
+
+            // --------------------------------------------------------------------------------------------------------
+
+
+            // $runningBilled = [];
+            // $totalIrdaMap = [];
+            // $final = [];
+
+            // foreach ($result as $row) {
+            //     $ptId = $row['pt_id'];
+
+            //     // Store total_irda_amt only once (first non-zero value)
+            //     if (!isset($totalIrdaMap[$ptId]) && $row['total_irda_amt'] > 0) {
+            //         $totalIrdaMap[$ptId] = $row['total_irda_amt'];
+            //     }
+
+            //     // Initialize running sum
+            //     if (!isset($runningBilled[$ptId])) {
+            //         $runningBilled[$ptId] = 0;
+            //     }
+
+            //     // Add billed amount to running total
+            //     $runningBilled[$ptId] += (float)$row['billed_amt'];
+
+            //     // Calculate unbilled amount
+            //     $row['unbilled_amount'] = ($totalIrdaMap[$ptId] ?? 0) - $runningBilled[$ptId];
+
+            //     $final[] = $row;
+            // }
+
+            // --------------------------------------------------------------------------------------------------------
+
+            $totalBilled  = [];
+            $totalIrdaMap = [];
+
+            foreach ($result as $row) {
+                $key = $row['pt_id'].'-'.$row['insurer_id'];
+
+                // Sum billed amount
+                $totalBilled[$key] = ($totalBilled[$key] ?? 0)
+                    + (float) ($row['billed_amt'] ?? 0);
+
+                // Store total_irda_amt once
+                if (!isset($totalIrdaMap[$key]) && ($row['total_irda_amt'] ?? 0) > 0) {
+                    $totalIrdaMap[$key] = (float) $row['total_irda_amt'];
                 }
             }
 
-            $result = array_values($filtered);
-            // dd($result);
+            // dd($totalBilled, $totalIrdaMap);
 
-            $runningBilled = [];
-            $totalIrdaMap = [];
-            $final = [];
+
+            $ptSeen = [];
+            $final  = [];
 
             foreach ($result as $row) {
-                $ptId = $row['pt_id'];
+                $ptId = $row['pt_id'].'-'.$row['insurer_id'];
+                if (!isset($ptSeen[$ptId])) {
+                    // First entry → set unbilled amount
+                    $row['unbilled_amount'] = (float) (($totalIrdaMap[$ptId] ?? 0) - ($totalBilled[$ptId] ?? 0)) ?? '0.00';
 
-                // Store total_irda_amt only once (first non-zero value)
-                if (!isset($totalIrdaMap[$ptId]) && $row['total_irda_amt'] > 0) {
-                    $totalIrdaMap[$ptId] = $row['total_irda_amt'];
+                    $ptSeen[$ptId] = true;
+                } else {
+                    // Other entries → zero
+                    $row['unbilled_amount'] = '0.00';
                 }
 
-                // Initialize running sum
-                if (!isset($runningBilled[$ptId])) {
-                    $runningBilled[$ptId] = 0;
-                }
-
-                // Add billed amount to running total
-                $runningBilled[$ptId] += (float)$row['billed_amt'];
-
-                // Calculate unbilled amount
-                $row['unbilled_amount'] = ($totalIrdaMap[$ptId] ?? 0) - $runningBilled[$ptId] ;
-
                 $final[] = $row;
             }
 
             $result = $final;
 
-            // print_rr($result); die;
+            // dd($result); die;
             return $result;
         }
 
diff --git a/app/Models/SalesActivityModel.php b/app/Models/SalesActivityModel.php
new file mode 100644
index 00000000..dc70086c
--- /dev/null
+++ b/app/Models/SalesActivityModel.php
@@ -0,0 +1,166 @@
+ 'required|integer',
+        'activity_type'  => 'required|in_list[Call,Email,Meeting,Demo,Share,Todo]',
+        'notes'          => 'required',
+        'scheduled_date' => 'required|valid_date',
+        'assigned_to'    => 'required|integer',
+        'status'         => 'in_list[pending,completed]',
+    ];
+
+    protected $validationMessages = [
+        'lead_id' => [
+            'required' => 'Lead ID is required',
+        ],
+        'activity_type' => [
+            'required' => 'Activity type is required',
+        ],
+        'notes' => [
+            'required' => 'Activity notes are required',
+        ],
+    ];
+
+    protected $skipValidation = false;
+
+    /**
+     * Get activities by lead with user details
+     */
+    public function getActivitiesByLead($leadId, $status = null)
+    {
+        $builder = $this->select('activities.*, user_profiles.username as assigned_to_name')
+            ->join('user_profiles', 'user_profiles.id = activities.assigned_to', 'left')
+            ->where('activities.lead_id', $leadId);
+
+        if ($status) {
+            $builder->where('activities.status', $status);
+        }
+
+        return $builder->orderBy('activities.scheduled_date', 'DESC')->findAll();
+    }
+
+    /**
+     * Get all activities with filters
+     */
+    public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0)
+    {
+        $builder = $this->select('activities.*, actual_leads.company_name, user_profiles.username as assigned_to_name')
+            ->join('actual_leads', 'actual_leads.lead_id = activities.lead_id', 'left')
+            ->join('user_profiles', 'user_profiles.id = activities.assigned_to', 'left');
+
+        if (!empty($filters['status'])) {
+            $builder->where('activities.status', $filters['status']);
+        }
+
+        if (!empty($filters['activity_type'])) {
+            $builder->where('activities.activity_type', $filters['activity_type']);
+        }
+
+        if (!empty($filters['assigned_to'])) {
+            $builder->where('activities.assigned_to', $filters['assigned_to']);
+        }
+
+        if (!empty($filters['date_from'])) {
+            $builder->where('activities.scheduled_date >=', $filters['date_from']);
+        }
+
+        if (!empty($filters['date_to'])) {
+            $builder->where('activities.scheduled_date <=', $filters['date_to']);
+        }
+
+        return [
+            'data'  => $builder->orderBy('activities.scheduled_date', 'DESC')
+                ->limit($limit, $offset)->findAll(),
+            'total' => $builder->countAllResults(false)
+        ];
+    }
+
+    /**
+     * Complete an activity
+     */
+    public function completeActivity($activityId, $completionData)
+    {
+        return $this->update($activityId, [
+            'status'           => 'completed',
+            'completion_notes' => $completionData['completion_notes'],
+            'completed_date'   => date('Y-m-d H:i:s'),
+            'updated_by'       => $completionData['updated_by'] ?? null
+        ]);
+    }
+
+    /**
+     * Get pending activities count by user
+     */
+    public function getPendingActivitiesCount($userId)
+    {
+        return $this->where('assigned_to', $userId)
+            ->where('status', 'pending')
+            ->where('scheduled_date <=', date('Y-m-d H:i:s'))
+            ->countAllResults();
+    }
+
+    /**
+     * Get upcoming activities for a user
+     */
+    public function getUpcomingActivities($userId, $days = 7, $limit = 10)
+    {
+        $endDate = date('Y-m-d H:i:s', strtotime("+{$days} days"));
+
+        return $this->select('activities.*, actual_leads.company_name')
+            ->join('actual_leads', 'actual_leads.lead_id = activities.lead_id', 'left')
+            ->where('activities.assigned_to', $userId)
+            ->where('activities.status', 'pending')
+            ->where('activities.scheduled_date <=', $endDate)
+            ->orderBy('activities.scheduled_date', 'ASC')
+            ->limit($limit)
+            ->findAll();
+    }
+
+    /**
+     * Get activity timeline for a lead
+     */
+    public function getActivityTimeline($leadId)
+    {
+        return $this->select('activities.*, user_profiles.username as assigned_to_name')
+            ->join('user_profiles', 'user_profiles.id = activities.assigned_to', 'left')
+            ->where('activities.lead_id', $leadId)
+            ->orderBy('activities.scheduled_date', 'DESC')
+            ->findAll();
+    }
+}
\ No newline at end of file
diff --git a/app/Models/SalesActualLeadModel.php b/app/Models/SalesActualLeadModel.php
new file mode 100644
index 00000000..6b8aee8f
--- /dev/null
+++ b/app/Models/SalesActualLeadModel.php
@@ -0,0 +1,145 @@
+ '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]',
+    ];
+
+    protected $validationMessages = [
+        'company_name' => [
+            'required' => 'Company name is required',
+        ],
+        'email' => [
+            'required'    => 'Email is required',
+            'valid_email' => 'Please provide a valid email address',
+        ],
+        'phone' => [
+            'required' => 'Phone number is required',
+        ],
+    ];
+
+    protected $skipValidation = false;
+
+    /**
+     * Get lead with assigned user details
+     */
+    public function getLeadWithUser($leadId)
+    {
+        return $this->select('actual_leads.*, user_profiles.username as assigned_to_name, user_profiles.email as assigned_to_email')
+            ->join('user_profiles', 'user_profiles.id = actual_leads.assigned_to', 'left')
+            ->where('actual_leads.lead_id', $leadId)
+            ->first();
+    }
+
+    /**
+     * Get all leads with pagination and filters
+     */
+    public function getLeadsWithFilters($filters = [], $limit = 10, $offset = 0)
+    {
+        $builder = $this->select('actual_leads.*, user_profiles.username as assigned_to_name')
+            ->join('user_profiles', 'user_profiles.id = actual_leads.assigned_to', 'left');
+
+        if (!empty($filters['status'])) {
+            $builder->where('actual_leads.status', $filters['status']);
+        }
+
+        if (!empty($filters['assigned_to'])) {
+            $builder->where('actual_leads.assigned_to', $filters['assigned_to']);
+        }
+
+        if (!empty($filters['search'])) {
+            $builder->groupStart()
+                ->like('actual_leads.company_name', $filters['search'])
+                ->orLike('actual_leads.email', $filters['search'])
+                ->orLike('actual_leads.phone', $filters['search'])
+                ->groupEnd();
+        }
+
+        return [
+            'data'  => $builder->limit($limit, $offset)->findAll(),
+            'total' => $builder->countAllResults(false)
+        ];
+    }
+
+    /**
+     * Get lead with all related data (contacts, activities, notes)
+     */
+    public function getLeadComplete($leadId)
+    {
+        $lead = $this->getLeadWithUser($leadId);
+        
+        if (!$lead) {
+            return null;
+        }
+
+        $contactModel = new SalesContactPersonModel();
+        $activityModel = new SalesActivityModel();
+        $noteModel = new SalesLeadNoteModel();
+
+        $lead['contact_persons'] = $contactModel->where('lead_id', $leadId)->findAll();
+        $lead['activities'] = $activityModel->getActivitiesByLead($leadId);
+        $lead['notes'] = $noteModel->getNotesByLead($leadId);
+
+        return $lead;
+    }
+
+    /**
+     * Get leads statistics
+     */
+    public function getLeadStats($userId = null)
+    {
+        $builder = $this->builder();
+        
+        if ($userId) {
+            $builder->where('assigned_to', $userId);
+        }
+
+        $stats = [
+            'total' => $builder->countAllResults(false),
+            'new' => $builder->where('status', 'New')->countAllResults(false),
+            'potential' => $builder->where('status', 'Potential')->countAllResults(false),
+            'prospects' => $builder->where('status', 'Prospects')->countAllResults(false),
+            'non_prospects' => $builder->where('status', 'Non prospects')->countAllResults(false),
+        ];
+
+        return $stats;
+    }
+}
\ No newline at end of file
diff --git a/app/Models/SalesContactPersonModel.php b/app/Models/SalesContactPersonModel.php
new file mode 100644
index 00000000..3a7073bb
--- /dev/null
+++ b/app/Models/SalesContactPersonModel.php
@@ -0,0 +1,95 @@
+ 'required|integer',
+        'name'    => 'required|min_length[2]|max_length[100]',
+        'mobile'  => 'required|min_length[10]|max_length[20]',
+    ];
+
+    protected $validationMessages = [
+        'lead_id' => [
+            'required' => 'Lead ID is required',
+        ],
+        'name' => [
+            'required' => 'Contact person name is required',
+        ],
+        'mobile' => [
+            'required' => 'Mobile number is required',
+        ],
+    ];
+
+    protected $skipValidation = false;
+
+    /**
+     * Get all contacts for a lead
+     */
+    public function getContactsByLead($leadId)
+    {
+        return $this->where('lead_id', $leadId)
+            ->orderBy('is_primary', 'DESC')
+            ->findAll();
+    }
+
+    /**
+     * Get primary contact for a lead
+     */
+    public function getPrimaryContact($leadId)
+    {
+        return $this->where('lead_id', $leadId)
+            ->where('is_primary', 1)
+            ->first();
+    }
+
+    /**
+     * Set a contact as primary (unset others)
+     */
+    public function setPrimaryContact($contactId, $leadId)
+    {
+        $this->db->transStart();
+
+        // Unset all primary contacts for this lead
+        $this->where('lead_id', $leadId)
+            ->set(['is_primary' => 0])
+            ->update();
+
+        // Set the specified contact as primary
+        $this->update($contactId, ['is_primary' => 1]);
+
+        $this->db->transComplete();
+
+        return $this->db->transStatus();
+    }
+}
\ No newline at end of file
diff --git a/app/Models/SalesLeadNoteModel.php b/app/Models/SalesLeadNoteModel.php
new file mode 100644
index 00000000..c8d9654f
--- /dev/null
+++ b/app/Models/SalesLeadNoteModel.php
@@ -0,0 +1,75 @@
+ 'required|integer',
+        'user_id'   => 'required|integer',
+        'note_text' => 'required|min_length[3]',
+    ];
+
+    protected $validationMessages = [
+        'lead_id' => [
+            'required' => 'Lead ID is required',
+        ],
+        'note_text' => [
+            'required' => 'Note text is required',
+        ],
+    ];
+
+    protected $skipValidation = false;
+
+    /**
+     * Get notes by lead with user details
+     */
+    public function getNotesByLead($leadId)
+    {
+        return $this->select('lead_notes.*, user_profiles.username')
+            ->join('user_profiles', 'user_profiles.id = lead_notes.user_id', 'left')
+            ->where('lead_notes.lead_id', $leadId)
+            ->orderBy('lead_notes.created_at', 'DESC')
+            ->findAll();
+    }
+
+    /**
+     * Get notes by user
+     */
+    public function getNotesByUser($userId, $limit = 20, $offset = 0)
+    {
+        return $this->select('lead_notes.*, actual_leads.company_name')
+            ->join('actual_leads', 'actual_leads.lead_id = lead_notes.lead_id', 'left')
+            ->where('lead_notes.user_id', $userId)
+            ->orderBy('lead_notes.created_at', 'DESC')
+            ->limit($limit, $offset)
+            ->findAll();
+    }
+}
\ No newline at end of file
diff --git a/app/Views/UserList.php b/app/Views/UserList.php
index 1382369b..e1f0f127 100755
--- a/app/Views/UserList.php
+++ b/app/Views/UserList.php
@@ -489,9 +489,9 @@ table.dataTable tbody td {
 
                     
- + +
@@ -623,9 +623,27 @@ table.dataTable tbody td {
+ + + +
+
+ +
+ +
+ + +
+
@@ -872,12 +890,18 @@ table.dataTable tbody td { $(document).ready(function () { $('#nhance_branch_id').select2(); + $('#partner_nhance_branch_id').select2(); + $('#rm_id').select2(); $('#nhance_branch_id') .val(null) // ✅ MUST be null .trigger('change.select2'); // ✅ correct trigger + $('#partner_nhance_branch_id') + .val(null) // ✅ MUST be null + .trigger('change.select2'); // ✅ correct trigger + var incentiveMonthPicker = flatpickr("#incentive_month", { dateFormat: "Y-m-d", // real value stored (hidden) @@ -1144,9 +1168,21 @@ table.dataTable tbody td { $('#partner_name').val(user.name); $('#email_addr').val(user.email); $('#mobile_no').val(user.mobile); - $('#locn').val(user.address); + // $('#locn').val(user.address); + $('#retention_rate').val(user.retention_rate); + if (user.nhance_branch_id !== null && + user.nhance_branch_id !== "" && + user.nhance_branch_id !== 0) { + $('#partner_nhance_branch_id') + .val(user.nhance_branch_id) + .trigger('change.select2'); + } else { + $('#partner_nhance_branch_id') + .val(null) // ✅ MUST be null + .trigger('change.select2'); // ✅ correct trigger + } // $('#btnPartnerSubmit').html('Update'); - $('.modal-title').html('Update Nhance Partner'); + $('.modal-title').html('Update Nhance Partner Manager'); let myModal = new bootstrap.Modal(document.getElementById('nhance-partner-modal')); myModal.show(); // open modal }); @@ -1154,10 +1190,12 @@ table.dataTable tbody td { $('body').on('click', '.btnPartnerDelete', function () { var partner_role = $(this).attr('data-role'); var alertText = "You need to remove this user"; - if (partner_role == 2) { + if (partner_role == 3) { alertText = "You need to remove this Staff"; + } else if (partner_role == 2) { + alertText = "You need to remove this Handler"; } else if (partner_role == 1) { - alertText = "You need to remove this Manager and related agent and staff are also delete"; + alertText = "You need to remove this Manager and related Handler and Staff are also delete"; } Swal.fire({ @@ -1237,23 +1275,25 @@ table.dataTable tbody td { var name = document.getElementById('partner_name').value.trim(); var mobile = document.getElementById('mobile_no').value.trim(); var email = document.getElementById('email_addr').value.trim(); - var address = document.getElementById('locn').value.trim(); - + // var address = document.getElementById('locn').value.trim(); + var rate = document.getElementById('retention_rate').value.trim(); + var nb = document.getElementById('partner_nhance_branch_id').value.trim(); + // Validation checks - if (name === "" || mobile === "" || email === "") { - console.log(name); - console.log(mobile); - console.log(email); + if (name === "" || mobile === "" || email === "" || rate === "" || nb === "") { + // console.log(name); + // console.log(mobile); + // console.log(email); toastr.warning('Please fill all required fields.', 'Warning'); form.classList.add('was-validated'); return; } - if (address.length > 1500) { - toastr.error('Address cannot exceed 1500 characters', 'Warning'); - form.classList.add('was-validated'); - return; - } + // if (address.length > 1500) { + // toastr.error('Address cannot exceed 1500 characters', 'Warning'); + // form.classList.add('was-validated'); + // return; + // } var mobilePattern = /^[6-9]\d{9}$/; // must start with 6–9 and be 10 digits if (!mobilePattern.test(mobile)) { @@ -1395,11 +1435,18 @@ table.dataTable tbody td { ` ]; } else { // partner + let roleName = ""; + switch(parseInt(row.role_id)) { + case 1: roleName = "Manager"; break; + case 2: roleName = "Handler"; break; + case 3: roleName = "Staff"; break; + default: roleName = "Others"; + } rowData = [ row.name, row.email, row.mobile, - row.role_id == 1 ? "Manager" : "Staff", + roleName, `${statusText}`, ` + + + + + + diff --git a/app/Views/pt_form_file_upload_tab.php b/app/Views/pt_form_file_upload_tab.php index f5f6f10a..a0180843 100644 --- a/app/Views/pt_form_file_upload_tab.php +++ b/app/Views/pt_form_file_upload_tab.php @@ -21,7 +21,7 @@