MERGE_TEST_NONEB_CLAIMS_FIRST_CUT
This commit is contained in:
commit
58cdfb76b8
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(find /c/xampp/htdocs/PHP828APPS/ruc/nhance -type f -name *.php -path */migration*)",
|
||||
"Bash(find /c/xampp/htdocs/PHP828APPS/ruc/nhance -type f \\\\\\(-name *.sql -o -name schema* \\\\\\))"
|
||||
]
|
||||
}
|
||||
}
|
||||
26
README.md
26
README.md
@ -10,4 +10,28 @@ Run speeific method
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\PremiumCalculationTest.php --filter testPremiumCalculationWithPrimaryRackRateAndAdditionalRackRate`
|
||||
|
||||
Test Comments
|
||||
Test Comments
|
||||
|
||||
## Non-EB Claims API Tests
|
||||
|
||||
Run all 4 Non-EB API tests at once:
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api`
|
||||
|
||||
Run a single test file:
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api\CreateClaimTest.php`
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api\ListClaimsTest.php`
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api\ClaimHistoryTest.php`
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api\UploadRequiredDocTest.php`
|
||||
|
||||
Run a specific test method:
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api\CreateClaimTest.php --filter testReturns401WhenNoAuth`
|
||||
|
||||
Run with verbose output (shows each test name):
|
||||
|
||||
`php vendor/bin/phpunit tests\unit\Api --testdox`
|
||||
@ -199,6 +199,10 @@ class Acl
|
||||
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
|
||||
'teams' => [CLAIMS_TEAM_ID]
|
||||
],
|
||||
'#^/non-eb-claim#' => [
|
||||
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
|
||||
'teams' => [CLAIMS_TEAM_ID]
|
||||
],
|
||||
'#^/departments#' => [
|
||||
'roles' => [ HEAD_ROLE_ID,ADMIN_ROLE_ID, MANAGER_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID],
|
||||
'teams' => [CLAIMS_TEAM_ID]
|
||||
|
||||
@ -132,3 +132,4 @@ define('UPLOAD_EXT_LEAD_FILES', ['xls', 'xlsx', 'pdf', 'jpg', 'jpeg', 'png']);
|
||||
define('UPLOAD_EXT_EXCEL', ['xls', 'xlsx', 'ods', 'csv']);
|
||||
define('UPLOAD_EXT_MAIL_ATTACHMENTS', ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx']);
|
||||
define('UPLOAD_EXT_NON_EB_RACK_RATE', ['pdf', 'xls', 'xlsx']);
|
||||
define('UPLOAD_EXT_ASSET_FILES', ['pdf', 'xls', 'xlsx', 'csv']);
|
||||
|
||||
@ -69,7 +69,7 @@ class Filters extends BaseConfig
|
||||
'before' => [
|
||||
'HttpRequestLog' => ['except' => 'cli/*'],
|
||||
'Cors',
|
||||
'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','employeeRest/*','processjob','getCommission','downloadEmployeeEcardZip', 'downloadClaimFile/*']],
|
||||
'AclFilter' => ['except' => ['login', 'logout', 'auth/*', 'oauth2callback','claim-form-download', 'claims-feedback-form', 'autobookstackLogin','employeeRest/*','processjob','getCommission','downloadEmployeeEcardZip', 'downloadClaimFile/*', 'api/v1/*']],
|
||||
'SecurityInputFilter' => ['except' => ['/client/notification/create','/ticket/crud_mail_template/*','test_mail','leads/sendMail', 'ticket/reply'] ],
|
||||
'GlobalPostFileUploadGuard'
|
||||
// 'csrf',
|
||||
|
||||
@ -603,6 +603,16 @@ $routes->group("/api", ["filter" => [ 'ratelimit' , 'authJWT']], function ($rout
|
||||
$routes->post("getId", "RestAuthenticationController::getUserIdFromToken");
|
||||
});
|
||||
|
||||
// Non-EB Claims External API v1
|
||||
$routes->group("api/v1", ["filter" => ['ratelimit', 'authJWT']], function ($routes) {
|
||||
$routes->group("non-eb-claim", function ($routes) {
|
||||
$routes->post('create', 'Api\NonEbClaimApiController::createClaim');
|
||||
$routes->post('list', 'Api\NonEbClaimApiController::listClaims');
|
||||
$routes->get('history/(:num)', 'Api\NonEbClaimApiController::claimHistory/$1');
|
||||
$routes->post('(:num)/upload-required-doc', 'Api\NonEbClaimApiController::uploadRequiredDoc/$1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
$routes->get("getSSORedirectUrl", "ApiServiceController::getSSORedirectUrl");
|
||||
@ -803,6 +813,30 @@ $routes->group("/ticket", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->post('manualTpaClaimPush',"ApiServiceController::manualTpaClaimPush");
|
||||
});
|
||||
|
||||
// Non-EB Claims
|
||||
$routes->group("/non-eb-claim", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->match(['get', 'post'], 'list', 'NonEbClaimController::claimList');
|
||||
$routes->get('remove', 'NonEbClaimController::removeClaim');
|
||||
$routes->get('new', 'NonEbClaimController::claimForm/50');
|
||||
$routes->get('new/(:any)', 'NonEbClaimController::claimForm/$1');
|
||||
$routes->post('create', 'NonEbClaimController::createClaim');
|
||||
$routes->post('update', 'NonEbClaimController::updateClaim');
|
||||
$routes->get('view/(:any)', 'NonEbClaimController::view_claim/$1');
|
||||
$routes->get('mail_template', 'NonEbClaimController::mailTemplate');
|
||||
$routes->post('crud_mail_template/(:any)', 'NonEbClaimController::crudTemplate/$1');
|
||||
$routes->post('note/(:any)', 'NonEbClaimController::crudNote/$1');
|
||||
$routes->post('reply', 'NonEbClaimController::saveReply');
|
||||
$routes->match(['get', 'post'], 'reports', 'NonEbClaimController::claimReports');
|
||||
$routes->post('getBranchAndPolicy', 'NonEbClaimController::getBranchAndPolicyByClientID');
|
||||
$routes->post('getVisibleSections', 'NonEbClaimController::getVisibleSectionsAjax');
|
||||
$routes->post('getMoreInfo', 'NonEbClaimController::getMoreInfo');
|
||||
$routes->post('uploadFile', 'NonEbClaimController::uploadFile');
|
||||
$routes->post('getClaimFiles', 'NonEbClaimController::getClaimFiles');
|
||||
$routes->get('removeFile', 'NonEbClaimController::removeFile');
|
||||
$routes->post('saveIRDocs', 'NonEbClaimController::saveIRDocs');
|
||||
$routes->get('testAutoMail/(:num)', 'NonEbClaimController::testAutoMailTrigger/$1');
|
||||
});
|
||||
|
||||
$routes->group("/claim_mis", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->get('list','TicketController::claimMisFileList');
|
||||
$routes->get('download','TicketController::downloadClaimMisFile');
|
||||
|
||||
620
app/Controllers/Api/NonEbClaimApiController.php
Normal file
620
app/Controllers/Api/NonEbClaimApiController.php
Normal file
@ -0,0 +1,620 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\Api;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
use App\Models\NonEbTicketMasterModel;
|
||||
use App\Models\NonEbClaimAssetModel;
|
||||
use App\Models\TicketHistoryModel;
|
||||
use App\Models\TicketClaimStatusModel;
|
||||
use App\Models\ClaimFilesModel;
|
||||
use App\Models\ClientPolicyModel;
|
||||
use App\Models\PolicyTypeModel;
|
||||
use App\Models\ClientRMModel;
|
||||
use App\Models\EmployeeModel;
|
||||
use App\Models\LevelContactModel;
|
||||
use App\Helpers\JWTToken;
|
||||
|
||||
class NonEbClaimApiController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected $nonEbTicketModel;
|
||||
protected $assetModel;
|
||||
protected $ticketHistoryModel;
|
||||
protected $claimStatusModel;
|
||||
protected $claimFilesModel;
|
||||
protected $clientPolicyModel;
|
||||
protected $policyTypeModel;
|
||||
protected $clientRMModel;
|
||||
protected $myLogger;
|
||||
|
||||
// API system user ID — used as created_by for all inserts (no session)
|
||||
const API_SYSTEM_USER_ID = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->nonEbTicketModel = new NonEbTicketMasterModel();
|
||||
$this->assetModel = new NonEbClaimAssetModel();
|
||||
$this->ticketHistoryModel = new TicketHistoryModel();
|
||||
$this->claimStatusModel = new TicketClaimStatusModel();
|
||||
$this->claimFilesModel = new ClaimFilesModel();
|
||||
$this->clientPolicyModel = new ClientPolicyModel();
|
||||
$this->policyTypeModel = new PolicyTypeModel();
|
||||
$this->clientRMModel = new ClientRMModel();
|
||||
$this->myLogger = \Config\Services::mylogger();
|
||||
}
|
||||
|
||||
// ===================== AUTH HELPER =====================
|
||||
|
||||
/**
|
||||
* Decode the JWT from Authorization header and return the resolved user row.
|
||||
* Returns array with normalised keys: id, name, email, mobile.
|
||||
* Returns null if token is missing or invalid.
|
||||
*/
|
||||
protected function getAuthUser(): ?array
|
||||
{
|
||||
$authHeader = $this->request->getHeaderLine('Authorization');
|
||||
if (empty($authHeader)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = JWTToken::validateJWT($authHeader);
|
||||
if ($result['status'] !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = $result['decoded'];
|
||||
|
||||
if (isset($decoded['emp_code'])) {
|
||||
$model = new EmployeeModel();
|
||||
$user = $model->find($decoded['id'] ?? null);
|
||||
if (!$user) return null;
|
||||
return [
|
||||
'id' => $user['id'],
|
||||
'name' => $user['name'] ?? '',
|
||||
'email' => $user['email_corporate'] ?? $user['email'] ?? '',
|
||||
'mobile' => $user['mobile'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$model = new LevelContactModel();
|
||||
$user = $model->find($decoded['post_hr_id'] ?? null);
|
||||
if (!$user) return null;
|
||||
return [
|
||||
'id' => $user['id'],
|
||||
'name' => $user['name'] ?? '',
|
||||
'email' => $user['email'] ?? '',
|
||||
'mobile' => $user['mobile'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
// ===================== SHARED HELPERS =====================
|
||||
|
||||
/**
|
||||
* Convert DD-MM-YYYY date fields to Y-m-d for DB storage.
|
||||
*/
|
||||
protected function formatDatesForClaim(array $data): array
|
||||
{
|
||||
$dateFields = ['loss_date', 'intimation_recd_date', 'intimated_to_insurer_date', 'eta_for_documents'];
|
||||
foreach ($dateFields as $field) {
|
||||
if (empty($data[$field])) {
|
||||
$data[$field] = null;
|
||||
} else {
|
||||
$data[$field] = change_date_format($data[$field], null, 'Y-m-d');
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate check: same client + loss_date + policy_no.
|
||||
*/
|
||||
protected function checkDuplicateNonEbClaim(array $data): bool
|
||||
{
|
||||
$client_id = $data['client_id'] ?? null;
|
||||
$loss_date = $data['loss_date'] ?? null;
|
||||
$policy_no = $data['policy_no'] ?? null;
|
||||
|
||||
if (empty($client_id) || empty($loss_date)) return false;
|
||||
|
||||
$query = $this->nonEbTicketModel
|
||||
->where('client_id', $client_id)
|
||||
->where('loss_date', $loss_date)
|
||||
->where('is_active', 1);
|
||||
|
||||
if (!empty($policy_no)) {
|
||||
$query->where('policy_no', $policy_no);
|
||||
}
|
||||
|
||||
return !empty($query->first());
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the asset_file from the request. Returns filename or null.
|
||||
*/
|
||||
protected function handleAssetFileUpload(): ?string
|
||||
{
|
||||
$file = $this->request->getFile('asset_file');
|
||||
if ($file === null || !$file->isValid() || $file->hasMoved()) {
|
||||
return null;
|
||||
}
|
||||
$uploadPath = WRITEPATH . 'uploads/non_eb_asset_files/';
|
||||
$fileName = file_Upload($file, $uploadPath, UPLOAD_EXT_ASSET_FILES);
|
||||
return !empty($fileName) ? $fileName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save asset rows (asset_id_code[], serial_no[], etc.) linked to a claim.
|
||||
*/
|
||||
protected function saveAssets(int $claim_id, array $post_data): void
|
||||
{
|
||||
$this->assetModel->where('non_eb_ticket_id', $claim_id)->set(['is_active' => 0])->update();
|
||||
|
||||
$codes = $post_data['asset_id_code'] ?? [];
|
||||
$serials = $post_data['serial_no'] ?? [];
|
||||
$vehicles = $post_data['vehicle_no'] ?? [];
|
||||
$descriptions = $post_data['asset_description'] ?? [];
|
||||
|
||||
if (!is_array($codes)) return;
|
||||
|
||||
for ($i = 0; $i < count($codes); $i++) {
|
||||
$code = trim($codes[$i] ?? '');
|
||||
$serial = trim($serials[$i] ?? '');
|
||||
$vehicle = trim($vehicles[$i] ?? '');
|
||||
$desc = trim($descriptions[$i] ?? '');
|
||||
|
||||
if (empty($code) && empty($serial) && empty($vehicle) && empty($desc)) continue;
|
||||
|
||||
$this->assetModel->insert([
|
||||
'non_eb_ticket_id' => $claim_id,
|
||||
'asset_id_code' => $code,
|
||||
'serial_no' => $serial,
|
||||
'vehicle_no' => $vehicle,
|
||||
'asset_description' => $desc,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the initial history row after a new claim is created.
|
||||
*/
|
||||
protected function putHistoryAfterInsert(array $ticket_data, int $ticket_id): void
|
||||
{
|
||||
if (!empty($ticket_data)) {
|
||||
$this->ticketHistoryModel->insert([
|
||||
'ticket_id' => $ticket_id,
|
||||
'field_name' => 'claim_status_id',
|
||||
'display_name' => 'Claim Created',
|
||||
'old_value' => null,
|
||||
'new_value' => $ticket_data['claim_status_id'],
|
||||
'created_by' => self::API_SYSTEM_USER_ID,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== ENDPOINTS =====================
|
||||
|
||||
public function createClaim()
|
||||
{
|
||||
$authUser = $this->getAuthUser();
|
||||
if (!$authUser) {
|
||||
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$body = $this->request->getJSON(true) ?? $this->request->getPost();
|
||||
|
||||
// Validate minimal user-facing fields only
|
||||
$rules = [
|
||||
'client_policy_id' => ['rules' => 'required|is_natural_no_zero', 'errors' => ['required' => 'Client Policy is required']],
|
||||
'nature_of_loss' => ['rules' => 'required|min_length[3]', 'errors' => ['required' => 'Nature of Loss is required']],
|
||||
'loss_location' => ['rules' => 'required', 'errors' => ['required' => 'Loss Location is required']],
|
||||
'loss_date' => ['rules' => 'required', 'errors' => ['required' => 'Loss Date is required']],
|
||||
'loss_description' => ['rules' => 'permit_empty'],
|
||||
'loss_estimate' => ['rules' => 'permit_empty|numeric'],
|
||||
'claim_number' => ['rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/_-]+$/]'],
|
||||
];
|
||||
|
||||
if (!$this->validateData($body, $rules)) {
|
||||
return $this->respond([
|
||||
'status' => false, 'code' => 400,
|
||||
'message' => 'Input validation failed',
|
||||
'errors' => $this->validator->getErrors(),
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Fetch client_policy — derive all FK fields from it
|
||||
$cp = $this->clientPolicyModel
|
||||
->where('id', (int)$body['client_policy_id'])
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$cp) {
|
||||
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Client policy not found or inactive'], 404);
|
||||
}
|
||||
|
||||
// Validate policy type is Non-EB or Marine
|
||||
$policyType = $this->policyTypeModel
|
||||
->select('allocg')
|
||||
->where('id', $cp['policy_type_id'])
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$policyType || !in_array($policyType['allocg'], ['Non-EB', 'Marine'])) {
|
||||
return $this->respond(['status' => false, 'code' => 422, 'message' => 'Only Non-EB or Marine policy types are allowed'], 422);
|
||||
}
|
||||
|
||||
// Fetch ACM from client_rm (level 3)
|
||||
$acm = $this->clientRMModel
|
||||
->where('client_id', $cp['client_id'])
|
||||
->where('level', 3)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$acm) {
|
||||
$this->myLogger->logme('error', "[NON_EB_API] No ACM found for client_id: {$cp['client_id']}");
|
||||
}
|
||||
|
||||
// Auto-set first claim_status_id for this policy type
|
||||
$firstStatus = $this->claimStatusModel
|
||||
->select('id')
|
||||
->where('ticket_type', $cp['policy_type_id'])
|
||||
->orderBy('id', 'ASC')
|
||||
->first();
|
||||
|
||||
if (!$firstStatus) {
|
||||
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No claim status configured for this policy type'], 422);
|
||||
}
|
||||
|
||||
// Build ticket data — merge user input with server-derived values
|
||||
$ticket_data = [
|
||||
'client_policy_id' => (int)$cp['id'],
|
||||
'client_id' => (int)$cp['client_id'],
|
||||
'branch_id' => (int)$cp['client_branch_id'],
|
||||
'policy_type_id' => (int)$cp['policy_type_id'],
|
||||
'insurer_id' => (int)$cp['insurer_id'],
|
||||
'policy_no' => $cp['policy_no'] ?? null,
|
||||
'acm_id' => $acm ? (int)$acm['user_id'] : null,
|
||||
'claim_status_id' => (int)$firstStatus['id'],
|
||||
'insured_contact_name' => $authUser['name'],
|
||||
'insured_contact_number' => $authUser['mobile'],
|
||||
'insured_contact_email' => $authUser['email'],
|
||||
'nature_of_loss' => $body['nature_of_loss'],
|
||||
'loss_location' => $body['loss_location'],
|
||||
'loss_date' => $body['loss_date'],
|
||||
'loss_description' => $body['loss_description'] ?? null,
|
||||
'loss_estimate' => $body['loss_estimate'] ?? null,
|
||||
'claim_number' => $body['claim_number'] ?? null,
|
||||
'priority' => 1,
|
||||
'created_by' => self::API_SYSTEM_USER_ID,
|
||||
];
|
||||
|
||||
$ticket_data = $this->formatDatesForClaim($ticket_data);
|
||||
|
||||
// Handle optional asset file upload
|
||||
$assetFileName = null;
|
||||
$assetFile = $this->request->getFile('asset_file');
|
||||
|
||||
if ($assetFile !== null && $assetFile->isValid() && !$assetFile->hasMoved()) {
|
||||
$ext = strtolower($assetFile->getClientExtension());
|
||||
$allowed = UPLOAD_EXT_ASSET_FILES; // ['pdf', 'xls', 'xlsx', 'csv']
|
||||
|
||||
if (!in_array($ext, $allowed)) {
|
||||
return $this->respond([
|
||||
'status' => false, 'code' => 415,
|
||||
'message' => 'Unsupported file type: ' . $ext . '. Allowed: ' . implode(', ', $allowed),
|
||||
], 415);
|
||||
}
|
||||
|
||||
if (empty(trim($ticket_data['loss_description'] ?? ''))) {
|
||||
return $this->respond([
|
||||
'status' => false, 'code' => 400,
|
||||
'message' => 'Input validation failed',
|
||||
'errors' => ['loss_description' => 'Loss Description is required when uploading an asset file.'],
|
||||
], 400);
|
||||
}
|
||||
|
||||
$uploadPath = WRITEPATH . 'uploads/non_eb_asset_files/';
|
||||
$assetFileName = file_Upload($assetFile, $uploadPath, $allowed);
|
||||
|
||||
if (empty($assetFileName)) {
|
||||
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Asset file upload failed'], 500);
|
||||
}
|
||||
|
||||
$ticket_data['asset_file'] = $assetFileName;
|
||||
}
|
||||
|
||||
// Duplicate check (runs after derivation so client_id + policy_no are populated)
|
||||
if ($this->checkDuplicateNonEbClaim($ticket_data)) {
|
||||
return $this->respond([
|
||||
'status' => false, 'code' => 409,
|
||||
'message' => 'Duplicate claim found for Client + Loss Date + Policy No combination',
|
||||
], 409);
|
||||
}
|
||||
|
||||
$claim_id = $this->nonEbTicketModel->insert($ticket_data);
|
||||
|
||||
if (!$claim_id) {
|
||||
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Failed to create claim'], 500);
|
||||
}
|
||||
|
||||
$this->saveAssets($claim_id, $body);
|
||||
$this->putHistoryAfterInsert($ticket_data, $claim_id);
|
||||
|
||||
$this->myLogger->logme('error', "[NON_EB_API] Claim created. ID: $claim_id, user: {$authUser['id']}");
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'code' => 200,
|
||||
'claim_id' => $claim_id,
|
||||
'asset_file' => $assetFileName,
|
||||
'message' => 'Non-EB Claim created successfully',
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function listClaims()
|
||||
{
|
||||
$authUser = $this->getAuthUser();
|
||||
if (!$authUser) {
|
||||
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$body = $this->request->getJSON(true) ?? [];
|
||||
$page = max(1, (int)($body['page'] ?? 1));
|
||||
$per_page = min(100, max(1, (int)($body['per_page'] ?? 20)));
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
$db = db_connect();
|
||||
$builder = $db->table('non_eb_ticket_master tm');
|
||||
|
||||
$builder->select([
|
||||
'tm.id',
|
||||
'tm.claim_number',
|
||||
'tm.nhance_claim_ref_no',
|
||||
'tm.policy_no',
|
||||
'tm.policy_type_id',
|
||||
'tm.claim_status_id',
|
||||
'tcs.claim_status AS status',
|
||||
'tcs.display_name AS status_display',
|
||||
'pt.policy_type AS policy_type_name',
|
||||
'c.client_name',
|
||||
'i.name AS insurer_name',
|
||||
'tm.loss_date',
|
||||
'tm.loss_location',
|
||||
'tm.nature_of_loss',
|
||||
'tm.loss_estimate',
|
||||
'tm.insured_contact_name',
|
||||
'tm.insured_contact_number',
|
||||
'(SELECT first_name FROM user_profiles WHERE user_profiles.id = tm.acm_id) AS acm_name',
|
||||
'DATE_FORMAT(tm.created_at, "%d-%m-%Y") AS created_date',
|
||||
'DATE_FORMAT(tm.updated_at, "%d-%m-%Y") AS updated_date',
|
||||
]);
|
||||
|
||||
$builder->join('clients c', 'c.id = tm.client_id AND c.is_active = 1', 'left');
|
||||
$builder->join('insurers i', 'i.id = tm.insurer_id AND i.is_active = 1', 'left');
|
||||
$builder->join('ticket_claim_status tcs','tcs.id = tm.claim_status_id AND tcs.is_active = 1', 'left');
|
||||
$builder->join('policy_type pt', 'pt.id = tm.policy_type_id AND pt.is_active = 1', 'left');
|
||||
|
||||
$builder->where('tm.is_active', 1);
|
||||
|
||||
// Filters
|
||||
if (!empty($body['client_id'])) $builder->where('tm.client_id', (int)$body['client_id']);
|
||||
if (!empty($body['insurer_id'])) $builder->where('tm.insurer_id', (int)$body['insurer_id']);
|
||||
if (!empty($body['policy_type_id'])) $builder->where('tm.policy_type_id', (int)$body['policy_type_id']);
|
||||
if (!empty($body['claim_status_id'])) $builder->where('tm.claim_status_id', (int)$body['claim_status_id']);
|
||||
if (!empty($body['claim_number'])) $builder->like('tm.claim_number', $body['claim_number']);
|
||||
if (!empty($body['nhance_claim_ref_no'])) $builder->like('tm.nhance_claim_ref_no', $body['nhance_claim_ref_no']);
|
||||
|
||||
// Date range filter
|
||||
if (!empty($body['date_type']) && !empty($body['start_date']) && !empty($body['end_date'])) {
|
||||
$col = $body['date_type'] === 'updated_date' ? 'tm.updated_at' : 'tm.created_at';
|
||||
$start = date('Y-m-d 00:00:00', strtotime(str_replace('-', '/', $body['start_date'])));
|
||||
$end = date('Y-m-d 23:59:59', strtotime(str_replace('-', '/', $body['end_date'])));
|
||||
$builder->where("$col BETWEEN '$start' AND '$end'");
|
||||
}
|
||||
|
||||
// Exclude terminal statuses by default
|
||||
if (empty($body['show_closed'])) {
|
||||
$builder->whereNotIn('tcs.display_name', ['Claim Settled', 'Claim Closed', 'Claim Rejected', 'Claim Withdrawn']);
|
||||
}
|
||||
|
||||
// COUNT for pagination (clone before limit)
|
||||
$countBuilder = clone $builder;
|
||||
$total = $countBuilder->countAllResults(false);
|
||||
|
||||
$builder->orderBy('tm.id', 'DESC');
|
||||
$builder->limit($per_page, $offset);
|
||||
|
||||
$data = $builder->get()->getResultArray();
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'code' => 200,
|
||||
'total' => (int)$total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'data' => $data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function claimHistory(int $claim_id)
|
||||
{
|
||||
$authUser = $this->getAuthUser();
|
||||
if (!$authUser) {
|
||||
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// Verify claim exists
|
||||
$claim = $this->nonEbTicketModel
|
||||
->select('id, client_id, policy_type_id')
|
||||
->where('id', $claim_id)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$claim) {
|
||||
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
|
||||
}
|
||||
|
||||
// Fetch only claim_status_id history rows, oldest first
|
||||
$history_rows = $this->ticketHistoryModel
|
||||
->select('new_value, created_at')
|
||||
->where('ticket_id', $claim_id)
|
||||
->where('field_name', 'claim_status_id')
|
||||
->where('is_active', 1)
|
||||
->orderBy('created_at', 'ASC')
|
||||
->findAll();
|
||||
|
||||
if (empty($history_rows)) {
|
||||
return $this->respond(['status' => true, 'code' => 200, 'claim_id' => $claim_id, 'history' => []], 200);
|
||||
}
|
||||
|
||||
// Build display_name map for this policy type — only statuses with a display_name are user-visible
|
||||
$statuses = $this->claimStatusModel
|
||||
->select('id, claim_status, display_name')
|
||||
->where('ticket_type', $claim['policy_type_id'])
|
||||
->where('display_name IS NOT NULL')
|
||||
->where("display_name != ''")
|
||||
->where('is_active', 1)
|
||||
->findAll();
|
||||
|
||||
$display_map = array_column($statuses, 'display_name', 'id');
|
||||
|
||||
// Filter and format — skip statuses with no display_name
|
||||
$history = [];
|
||||
foreach ($history_rows as $row) {
|
||||
$status_id = (int)$row['new_value'];
|
||||
$display_name = $display_map[$status_id] ?? null;
|
||||
if (!$display_name) continue;
|
||||
|
||||
$history[] = [
|
||||
'status' => $display_name,
|
||||
'changed_at' => date('d-m-Y h:i A', strtotime($row['created_at'])),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'code' => 200,
|
||||
'claim_id' => $claim_id,
|
||||
'history' => $history,
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function uploadRequiredDoc(int $claim_id)
|
||||
{
|
||||
$authUser = $this->getAuthUser();
|
||||
if (!$authUser) {
|
||||
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// Verify claim exists
|
||||
$claim = $this->nonEbTicketModel
|
||||
->select('id, client_id, required_docs')
|
||||
->where('id', $claim_id)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$claim) {
|
||||
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
|
||||
}
|
||||
|
||||
// Validate required_docs checklist is configured
|
||||
$required_docs = json_decode($claim['required_docs'] ?? '{}', true);
|
||||
if (empty($required_docs) || empty($required_docs['docs'])) {
|
||||
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No required documents checklist configured for this claim'], 422);
|
||||
}
|
||||
|
||||
// Check if checklist is locked
|
||||
if (!empty($required_docs['is_action_freeze'])) {
|
||||
return $this->respond(['status' => false, 'code' => 423, 'message' => 'Document checklist is locked for this claim'], 423);
|
||||
}
|
||||
|
||||
// Validate inputs
|
||||
$document_name = trim($this->request->getPost('document_name') ?? '');
|
||||
if (empty($document_name)) {
|
||||
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['document_name' => 'document_name is required']], 400);
|
||||
}
|
||||
|
||||
$file = $this->request->getFile('file');
|
||||
if (!$file || !$file->isValid() || $file->hasMoved()) {
|
||||
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['file' => 'A valid file is required']], 400);
|
||||
}
|
||||
|
||||
// Validate file extension
|
||||
$allowed = defined('UPLOAD_EXT_CLAIM_DOCS') ? UPLOAD_EXT_CLAIM_DOCS : ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx'];
|
||||
$ext = strtolower($file->getClientExtension());
|
||||
if (is_array($allowed) && !in_array($ext, $allowed)) {
|
||||
return $this->respond(['status' => false, 'code' => 415, 'message' => 'Unsupported file type: ' . $ext], 415);
|
||||
}
|
||||
|
||||
// Find matching doc in checklist (exact case-sensitive match)
|
||||
$matched_index = null;
|
||||
foreach ($required_docs['docs'] as $i => $doc) {
|
||||
if (($doc['document_name'] ?? '') === $document_name) {
|
||||
$matched_index = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matched_index === null) {
|
||||
return $this->respond([
|
||||
'status' => false, 'code' => 404,
|
||||
'message' => "Document '{$document_name}' not found in required documents list",
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Upload file and update required_docs atomically
|
||||
$db = db_connect();
|
||||
$db->transStart();
|
||||
|
||||
$upload_path = WRITEPATH . 'uploads/claim_files/';
|
||||
$file_name = file_Upload($file, $upload_path, UPLOAD_EXT_CLAIM_DOCS);
|
||||
|
||||
if (empty($file_name)) {
|
||||
$db->transRollback();
|
||||
return $this->respond(['status' => false, 'code' => 500, 'message' => 'File upload failed'], 500);
|
||||
}
|
||||
|
||||
// Insert into claim_files
|
||||
$this->claimFilesModel->insert([
|
||||
'ticket_id' => $claim_id,
|
||||
'ticket_type' => 2,
|
||||
'doc_name' => $document_name,
|
||||
'file_name' => $file_name,
|
||||
'url' => $upload_path . $file_name,
|
||||
'file_type' => 2,
|
||||
'mime_type' => getMimeTypeByFileName($file_name),
|
||||
'is_active' => 1,
|
||||
'created_by' => self::API_SYSTEM_USER_ID,
|
||||
]);
|
||||
|
||||
// Mark document as received in required_docs JSON
|
||||
$required_docs['docs'][$matched_index]['document_received'] = true;
|
||||
$updated_json = json_encode($required_docs);
|
||||
|
||||
$db->query('UPDATE non_eb_ticket_master SET required_docs = ? WHERE id = ?', [$updated_json, $claim_id]);
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
if (!$db->transStatus()) {
|
||||
return $this->respond(['status' => false, 'code' => 500, 'message' => 'Failed to save document. Please try again.'], 500);
|
||||
}
|
||||
|
||||
$file_id = $this->claimFilesModel->insertID();
|
||||
$download_url = base_url('downloadClaimFile/') . $file_id;
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Document uploaded successfully',
|
||||
'claim_id' => $claim_id,
|
||||
'document_name' => $document_name,
|
||||
'download_url' => $download_url,
|
||||
'required_docs' => $required_docs,
|
||||
], 200);
|
||||
}
|
||||
}
|
||||
@ -5017,9 +5017,21 @@ class EmployeeRestController extends AdminController
|
||||
|
||||
public function hrFileUploadMasters()
|
||||
{
|
||||
$type = $this->request->getGet('type');
|
||||
if (empty($type)) {
|
||||
$type = 'EB';
|
||||
}
|
||||
|
||||
try {
|
||||
//for inception upload
|
||||
if($type == 'EB'){
|
||||
|
||||
$data['actions'] = ['addition' => 'Addition (Employee + Dependents)', 'missed_inception' => 'Missed Inception (Employee + Dependents)', 'dependent_addition' => 'Dependent Addition (Only Dependents)', 'deletion' => 'Deletion', 'correction' => 'Correction', 'si_enhancement' => 'SI Enhancement'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['actions'] = ['addition' => 'Adding New Assets', 'deletion' => 'Removing Assets', 'correction' => 'Correction of assets details'];
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
|
||||
1194
app/Controllers/NonEbClaimController.php
Normal file
1194
app/Controllers/NonEbClaimController.php
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,6 +11,7 @@ class ClaimFilesModel extends Model
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'ticket_id',
|
||||
'ticket_type',
|
||||
'ticket_message_id',
|
||||
'file_type',
|
||||
'doc_name',
|
||||
|
||||
36
app/Models/NonEbClaimAssetModel.php
Normal file
36
app/Models/NonEbClaimAssetModel.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class NonEbClaimAssetModel extends Model
|
||||
{
|
||||
protected $table = 'non_eb_claim_assets';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'id', 'non_eb_ticket_id', 'asset_id_code', 'serial_no',
|
||||
'vehicle_no', 'asset_description',
|
||||
'is_active', 'created_by', 'updated_by', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = ['checkAndADDCreatedByValue'];
|
||||
protected $beforeUpdate = ['checkAndUpdateUpdatedByValue'];
|
||||
|
||||
protected function checkAndADDCreatedByValue(array $data)
|
||||
{
|
||||
$data['data']['created_by'] = get_session_userid();
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function checkAndUpdateUpdatedByValue(array $data)
|
||||
{
|
||||
$data['data']['updated_by'] = get_session_userid();
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
87
app/Models/NonEbTicketMasterModel.php
Normal file
87
app/Models/NonEbTicketMasterModel.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class NonEbTicketMasterModel extends Model
|
||||
{
|
||||
protected $table = 'non_eb_ticket_master';
|
||||
protected $primaryKey = 'id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $returnType = 'array';
|
||||
protected $useSoftDeletes = false;
|
||||
protected $protectFields = true;
|
||||
protected $allowedFields = [
|
||||
'id', 'acm_id', 'client_id', 'branch_id', 'client_policy_id', 'policy_type_id',
|
||||
'policy_no', 'policy_start_date', 'policy_end_date', 'insurer_id',
|
||||
'nature_of_loss', 'loss_description', 'loss_location', 'loss_date', 'loss_estimate',
|
||||
'intimation_recd_date', 'intimated_to_insurer_date', 'nhance_claim_ref_no', 'claim_number',
|
||||
'claim_status_id', 'surveyor_file_ref_no',
|
||||
'surveyor_name', 'surveyor_contact_person', 'surveyor_contact_number', 'surveyor_email', 'lor', 'surveyor_remarks',
|
||||
'insured_contact_name', 'insured_contact_number', 'insured_contact_email',
|
||||
'google_drive_links', 'documents_required', 'documents_submitted', 'pending_documents', 'eta_for_documents',
|
||||
'loss_assessed_value', 'settled_amount', 'settlement_utr',
|
||||
'asset_file', 'closure_remark', 'required_docs',
|
||||
'priority', 'is_active', 'created_by', 'updated_by', 'last_updated_by', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
protected $allowCallbacks = true;
|
||||
protected $beforeInsert = ['checkAndADDCreatedByValue'];
|
||||
protected $beforeUpdate = ['checkAndUpdateUpdatedByValue'];
|
||||
|
||||
protected function checkAndADDCreatedByValue(array $data)
|
||||
{
|
||||
$data['data']['created_by'] = get_session_userid();
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function checkAndUpdateUpdatedByValue(array $data)
|
||||
{
|
||||
$data['data']['updated_by'] = get_session_userid();
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getTicketDataByTicketID($ticket_id)
|
||||
{
|
||||
return $this->select([
|
||||
'non_eb_ticket_master.*',
|
||||
'c.client_name', 'c.short_name',
|
||||
'i.name as insurer_name',
|
||||
'tcs.claim_status', 'tcs.display_name as status_display_name',
|
||||
'pt.policy_type as policy_type_name',
|
||||
'up.first_name as acm', 'up.mobile as acm_mobile',
|
||||
'c.common_mails',
|
||||
'cb.branch_name'
|
||||
])
|
||||
->join('clients c', 'c.id = non_eb_ticket_master.client_id AND c.is_active = 1', 'left')
|
||||
->join('client_branch cb', 'cb.id = non_eb_ticket_master.branch_id', 'left')
|
||||
->join('insurers i', 'i.id = non_eb_ticket_master.insurer_id AND i.is_active = 1', 'left')
|
||||
->join('ticket_claim_status tcs', 'tcs.id = non_eb_ticket_master.claim_status_id AND tcs.is_active = 1', 'left')
|
||||
->join('policy_type pt', 'pt.id = non_eb_ticket_master.policy_type_id AND pt.is_active = 1', 'left')
|
||||
->join('user_profiles up', 'up.id = non_eb_ticket_master.acm_id', 'left')
|
||||
->where('non_eb_ticket_master.id', $ticket_id)
|
||||
->where('non_eb_ticket_master.is_active', 1)
|
||||
->first();
|
||||
}
|
||||
|
||||
public function getTemplateDataByTicketID($ticket_id, $status_id = null)
|
||||
{
|
||||
$status_join = $status_id
|
||||
? 'tcs.id = ' . (int)$status_id . ' AND tcs.is_active = 1'
|
||||
: 'tcs.id = non_eb_ticket_master.claim_status_id AND tcs.is_active = 1';
|
||||
|
||||
return $this->select([
|
||||
'tmt.template_name', 'tmt.subject', 'tmt.mail_content', 'tmt.is_auto_mail', 'tmt.trigger_type',
|
||||
'non_eb_ticket_master.insured_contact_email',
|
||||
'non_eb_ticket_master.insured_contact_name',
|
||||
'non_eb_ticket_master.policy_type_id',
|
||||
'tcs.claim_status', 'tcs.trigger_type as status_trigger_type'
|
||||
])
|
||||
->join('ticket_claim_status tcs', $status_join, 'left')
|
||||
->join('ticket_mail_template tmt', 'tmt.ticket_type = non_eb_ticket_master.policy_type_id AND tmt.trigger_type = tcs.trigger_type AND tmt.is_active = 1', 'left')
|
||||
->where('non_eb_ticket_master.id', $ticket_id)
|
||||
->where('non_eb_ticket_master.is_active', 1)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@ -2021,10 +2021,16 @@ body[data-sidebar-size="condensed"] .footer {
|
||||
<div class="collapse" id="sidebarDashboardsTicket">
|
||||
<ul class="nav-second-level">
|
||||
<li>
|
||||
<a href="#" onclick="openTicketTypeAskModal()">New claim</a>
|
||||
<a href="#" onclick="openTicketTypeAskModal()">New EB Claim</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/ticket/list') ?>">Claim List</a>
|
||||
<a href="<?= base_url('/ticket/list') ?>">EB Claim List</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/non-eb-claim/new') ?>">New Non EB Claim</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/non-eb-claim/list') ?>">Non EB Claim List</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="<?= base_url('/ticket/mail_template') ?>">Mail Template</a>
|
||||
|
||||
1401
app/Views/non_eb_claim_edit.php
Normal file
1401
app/Views/non_eb_claim_edit.php
Normal file
File diff suppressed because it is too large
Load Diff
842
app/Views/non_eb_claim_form.php
Normal file
842
app/Views/non_eb_claim_form.php
Normal file
@ -0,0 +1,842 @@
|
||||
<style>
|
||||
.readonly-color {
|
||||
background-color: #e0e0e0;
|
||||
color: #666;
|
||||
}
|
||||
.readonly-select {
|
||||
pointer-events: none;
|
||||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
.label-font-size {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.is-invalid {
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
.invalid-feedback {
|
||||
display: none;
|
||||
color: #dc3545;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.is-invalid ~ .invalid-feedback,
|
||||
.is-invalid + .invalid-feedback {
|
||||
display: block;
|
||||
}
|
||||
.select2-container .select2-selection--single.is-invalid-select2 {
|
||||
border-color: #dc3545 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var base_url = '<?= base_url() ?>';
|
||||
var pageSubTitle = 'New Non-EB Claim';
|
||||
var pageBackButton = '<a href="'+base_url+'non-eb-claim/list"><i class="mdi mdi-arrow-left" style="font-size:17px;"></i></a>';
|
||||
</script>
|
||||
|
||||
<?php $sections = $visible_sections ?? []; ?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card" style="margin-right:23px;">
|
||||
<div class="card-body">
|
||||
<form id="nonEbClaimForm" onsubmit="return false;" enctype="multipart/form-data">
|
||||
<input type="hidden" name="policy_type_id" id="policy_type_id" value="<?= $policy_type_id ?>">
|
||||
<input type="hidden" name="client_id" id="client_id" value="" required>
|
||||
<input type="hidden" name="insurer_id" id="insurer_id" value="">
|
||||
<input type="hidden" name="client_policy_id" id="client_policy_id_hidden" value="">
|
||||
|
||||
<!-- Section 1: Policy & Account Details -->
|
||||
<div id="accordion1" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Policy & Account Details
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseOne" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseOne" class="collapse show" data-parent="#accordion1">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Client <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="client_select" required>
|
||||
<option value="">Select Client</option>
|
||||
<?php if(isset($clients)): foreach($clients as $c): ?>
|
||||
<option value="<?= $c['id'] ?>"><?= $c['client_name'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Branch <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="branch_id" name="branch_id" required>
|
||||
<option value="">Select Branch</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Policy</label>
|
||||
<select class="form-control" id="client_policy_id">
|
||||
<option value="">Select Policy</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Insurer</label>
|
||||
<input type="text" class="form-control readonly-color" id="insurer_name_display" readonly placeholder="Auto-filled from policy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Policy Type</label>
|
||||
<input type="text" class="form-control" name="policy_type_name" id="policy_type_display" value="<?= $policy_type_name ?? '' ?>">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Policy No</label>
|
||||
<input type="text" class="form-control readonly-color" name="policy_no" id="policy_no" readonly placeholder="Auto-filled from policy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Policy Start Date</label>
|
||||
<input type="text" class="form-control readonly-color" name="policy_start_date" id="policy_start_date" readonly placeholder="Auto-filled from policy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Policy Expiry Date</label>
|
||||
<input type="text" class="form-control readonly-color" name="policy_end_date" id="policy_end_date" readonly placeholder="Auto-filled from policy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Account Manager <span class="text-danger">*</span></label>
|
||||
<select class="form-control select2-init" name="acm_id" id="acm_id" required>
|
||||
<option value="">Select ACM</option>
|
||||
<?php if(isset($acms)): foreach($acms as $a): ?>
|
||||
<option value="<?= $a['id'] ?>"><?= $a['first_name'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2: Insured Contact Details -->
|
||||
<div id="accordion2" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Insured Contact Details
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseTwo" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseTwo" class="collapse show" data-parent="#accordion2">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Contact Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="insured_contact_name" id="insured_contact_name" minlength="3" pattern="^[a-zA-Z0-9\s_-]+$" required>
|
||||
<div class="invalid-feedback">Min 3 chars. Only letters, numbers, spaces, hyphens, underscores.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Contact Number <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="insured_contact_number" id="insured_contact_number" minlength="10" maxlength="15" pattern="^[0-9]+$" required>
|
||||
<div class="invalid-feedback">Required. 10-15 digits, numeric only.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Contact Email</label>
|
||||
<input type="email" class="form-control" name="insured_contact_email" id="insured_contact_email">
|
||||
<div class="invalid-feedback">Invalid email format.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3: Loss / Incident Details -->
|
||||
<div id="accordion3" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Loss / Incident Details
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseThree" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseThree" class="collapse show" data-parent="#accordion3">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Nature of Loss <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="nature_of_loss" id="nature_of_loss" minlength="3" required>
|
||||
<div class="invalid-feedback">Required. Min 3 characters.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Loss Location <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="loss_location" id="loss_location" required>
|
||||
<div class="invalid-feedback">Loss Location is required.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Loss Date <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control date-picker" name="loss_date" id="loss_date" required placeholder="dd-mm-yyyy">
|
||||
<div class="invalid-feedback">Loss Date is required.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Loss Estimate</label>
|
||||
<input type="number" class="form-control" name="loss_estimate" id="loss_estimate" step="0.01" min="0">
|
||||
<div class="invalid-feedback">Must be a valid number.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-8">
|
||||
<label class="label-font-size">Loss Description</label>
|
||||
<textarea class="form-control" name="loss_description" id="loss_description" rows="2"></textarea>
|
||||
<div class="invalid-feedback">Loss Description is required when uploading an asset file.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 4: Intimation & Claim Reference -->
|
||||
<div id="accordion4" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Intimation & Claim Reference
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseFour" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseFour" class="collapse show" data-parent="#accordion4">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Intimation Recd Date</label>
|
||||
<input type="text" class="form-control date-picker" name="intimation_recd_date" placeholder="dd-mm-yyyy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Intimated to Insurer Date</label>
|
||||
<input type="text" class="form-control date-picker" name="intimated_to_insurer_date" placeholder="dd-mm-yyyy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Nhance Claim Ref No</label>
|
||||
<input type="text" class="form-control" name="nhance_claim_ref_no" id="nhance_claim_ref_no" maxlength="100">
|
||||
<div class="invalid-feedback">Max 100 characters.</div>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label class="label-font-size">Claim Number (Insurer)</label>
|
||||
<input type="text" class="form-control" name="claim_number" id="claim_number" pattern="^[a-zA-Z0-9\/_-]+$">
|
||||
<div class="invalid-feedback">Only letters, numbers, /, - allowed.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 5: Status & Tracking -->
|
||||
<div id="accordion5" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Status & Tracking
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseFive" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseFive" class="collapse show" data-parent="#accordion5">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Status <span class="text-danger">*</span></label>
|
||||
<select class="form-control" name="claim_status_id" id="claim_status_id" required>
|
||||
<?php if(isset($claim_status)): foreach($claim_status as $cs): ?>
|
||||
<option value="<?= $cs['id'] ?>"><?= $cs['claim_status'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Surveyor File Ref No</label>
|
||||
<input type="text" class="form-control" name="surveyor_file_ref_no">
|
||||
</div>
|
||||
<div class="form-group col-md-4">
|
||||
<label class="label-font-size">Priority</label>
|
||||
<select class="form-control" name="priority">
|
||||
<option value="">Select Priority</option>
|
||||
<?php if(isset($priorityType)): foreach($priorityType as $k => $v): ?>
|
||||
<option value="<?= $k ?>"><?= $v ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Closure Remark: shown for Claim Closed / Rejected / Withdrawn -->
|
||||
<div id="closure_remark_row" class="form-row" style="display:none;">
|
||||
<div class="form-group col-md-12">
|
||||
<label class="label-font-size">Closure Remark</label>
|
||||
<textarea class="form-control" name="closure_remark" id="closure_remark" rows="2" placeholder="Enter closure remark..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 6: Asset Details -->
|
||||
<div id="accordion6" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Asset Details
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseSix" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseSix" class="collapse show" data-parent="#accordion6">
|
||||
<div class="card-body">
|
||||
<div id="asset_container">
|
||||
<div class="form-row asset-row" id="asset_row_1">
|
||||
<div class="form-group col-md-3"><label class="label-font-size">Asset ID/Code</label><input type="text" class="form-control" name="asset_id_code[]"></div>
|
||||
<div class="form-group col-md-2"><label class="label-font-size">Serial No</label><input type="text" class="form-control" name="serial_no[]"></div>
|
||||
<div class="form-group col-md-2"><label class="label-font-size">Vehicle No</label><input type="text" class="form-control" name="vehicle_no[]"></div>
|
||||
<div class="form-group col-md-4"><label class="label-font-size">Description</label><input type="text" class="form-control" name="asset_description[]"></div>
|
||||
<div class="form-group col-md-1 d-flex align-items-end"><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-info mt-2" onclick="addAssetRow()"><i class="mdi mdi-plus"></i> Add More Asset</button>
|
||||
|
||||
<!-- OR Divider -->
|
||||
<div class="d-flex align-items-center my-3">
|
||||
<hr class="flex-grow-1" style="border-top: 1px solid #aaa;">
|
||||
<span class="mx-3 font-weight-bold text-muted" style="font-size: 0.9rem;">OR</span>
|
||||
<hr class="flex-grow-1" style="border-top: 1px solid #aaa;">
|
||||
</div>
|
||||
|
||||
<!-- Asset File Upload -->
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label class="label-font-size">Upload Asset File <small class="text-muted">(Excel or PDF)</small></label>
|
||||
<input type="file" class="form-control-file" name="asset_file" id="asset_file" accept=".xlsx,.xls,.csv,.pdf">
|
||||
<div class="invalid-feedback">Only Excel (.xlsx, .xls, .csv) or PDF (.pdf) files allowed.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="asset_section_error" class="text-danger" style="display:none; font-size:0.85rem;">Please fill at least one asset row or upload an asset file.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 7: Surveyor Details -->
|
||||
<div id="section_surveyor" class="mb-3" style="<?= in_array('surveyor', $sections) ? '' : 'display:none' ?>">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Surveyor Details
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseSeven" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseSeven" class="collapse show" data-parent="#section_surveyor">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-3"><label class="label-font-size">Surveyor Name</label><input type="text" class="form-control" name="surveyor_name" maxlength="255"></div>
|
||||
<div class="form-group col-md-3"><label class="label-font-size">Contact Person</label><input type="text" class="form-control" name="surveyor_contact_person" maxlength="255"></div>
|
||||
<div class="form-group col-md-3"><label class="label-font-size">Contact Number</label><input type="text" class="form-control" name="surveyor_contact_number" minlength="10" maxlength="15" pattern="^[0-9]+$"><div class="invalid-feedback">10-15 digits, numeric only.</div></div>
|
||||
<div class="form-group col-md-3"><label class="label-font-size">Email</label><input type="email" class="form-control" name="surveyor_email"><div class="invalid-feedback">Invalid email format.</div></div>
|
||||
<div class="form-group col-md-6"><label class="label-font-size">LOR</label><textarea class="form-control" name="lor" rows="2"></textarea></div>
|
||||
<div class="form-group col-md-6"><label class="label-font-size">Surveyor Remarks</label><textarea class="form-control" name="surveyor_remarks" rows="2"></textarea></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /* Section 8: Documents & Attachments — commented out, moved to Claim Files tab on edit page
|
||||
<div id="accordion8" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Documents & Attachments
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseEight" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseEight" class="collapse show" data-parent="#accordion8">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12"><label class="label-font-size">Google Drive Links</label><textarea class="form-control" name="google_drive_links" rows="2"></textarea></div>
|
||||
<div class="form-group col-md-6"><label class="label-font-size">Documents Required</label><textarea class="form-control" name="documents_required" rows="2"></textarea></div>
|
||||
<div class="form-group col-md-6"><label class="label-font-size">Documents Submitted</label><textarea class="form-control" name="documents_submitted" rows="2"></textarea></div>
|
||||
<div class="form-group col-md-6"><label class="label-font-size">Pending Documents</label><textarea class="form-control" name="pending_documents" rows="2"></textarea></div>
|
||||
<div class="form-group col-md-3"><label class="label-font-size">ETA for Documents</label><input type="text" class="form-control date-picker" name="eta_for_documents" placeholder="dd-mm-yyyy"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
*/ ?>
|
||||
|
||||
<!-- Section 9: Settlement Details -->
|
||||
<div id="section_settlement" class="mb-3" style="<?= in_array('settlement', $sections) ? '' : 'display:none' ?>">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">
|
||||
Settlement Details
|
||||
<a class="text-dark float-right" data-toggle="collapse" href="#collapseNine" aria-expanded="true">
|
||||
<i class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
|
||||
</a>
|
||||
</h4>
|
||||
<div id="collapseNine" class="collapse show" data-parent="#section_settlement">
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4"><label class="label-font-size">Loss Assessed Value</label><input type="number" class="form-control" name="loss_assessed_value" step="0.01" min="0"><div class="invalid-feedback">Must be a valid number.</div></div>
|
||||
<div class="form-group col-md-4"><label class="label-font-size">Settled Amount</label><input type="number" class="form-control" name="settled_amount" step="0.01" min="0"><div class="invalid-feedback">Must be a valid number.</div></div>
|
||||
<div class="form-group col-md-4"><label class="label-font-size">Settlement UTR</label><input type="text" class="form-control" name="settlement_utr" maxlength="100"><div class="invalid-feedback">Max 100 characters.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right mt-3">
|
||||
<button type="button" class="btn btn-primary" onclick="submitNonEbClaim()">Create Claim</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Asset add/remove
|
||||
var assetIndex = 1;
|
||||
function addAssetRow() {
|
||||
assetIndex++;
|
||||
var html = '<div class="form-row asset-row" id="asset_row_'+assetIndex+'">' +
|
||||
'<div class="form-group col-md-3"><input type="text" class="form-control" name="asset_id_code[]" placeholder="Asset ID/Code"></div>' +
|
||||
'<div class="form-group col-md-2"><input type="text" class="form-control" name="serial_no[]" placeholder="Serial No"></div>' +
|
||||
'<div class="form-group col-md-2"><input type="text" class="form-control" name="vehicle_no[]" placeholder="Vehicle No"></div>' +
|
||||
'<div class="form-group col-md-4"><input type="text" class="form-control" name="asset_description[]" placeholder="Description"></div>' +
|
||||
'<div class="form-group col-md-1 d-flex align-items-end"><button type="button" class="btn btn-danger btn-sm" onclick="removeAssetRow('+assetIndex+')"><i class="mdi mdi-delete"></i></button></div>' +
|
||||
'</div>';
|
||||
$('#asset_container').append(html);
|
||||
}
|
||||
function removeAssetRow(idx) { $('#asset_row_' + idx).remove(); }
|
||||
|
||||
// Store branch & policy & contact data after client fetch
|
||||
var noneb_policy_list = {};
|
||||
var noneb_contact_list = {};
|
||||
|
||||
function appendBranches(data) {
|
||||
$('#branch_id').empty();
|
||||
$('#branch_id').append($('<option>', { value: '', text: 'Select Branch' }));
|
||||
$.each(data, function(index, item) {
|
||||
$('#branch_id').append($('<option>', { value: item.id, text: item.branch_name }));
|
||||
});
|
||||
}
|
||||
|
||||
function appendPolicies(data) {
|
||||
$('#client_policy_id').empty();
|
||||
$('#client_policy_id').append($('<option>', { value: '', text: 'Select Policy' }));
|
||||
$.each(data, function(index, p) {
|
||||
var option = $('<option>', {
|
||||
value: p.id,
|
||||
text: (p.policy_type || '') + ' - ' + (p.policy_no || '')
|
||||
});
|
||||
var insurerDisplay = (p.insurer_short_name || p.insurer_name || '');
|
||||
if (p.insurer_branch_code) { insurerDisplay += ' - ' + p.insurer_branch_code; }
|
||||
option.attr('data-insurer', p.insurer_id || '');
|
||||
option.attr('data-insurer-name', insurerDisplay);
|
||||
option.attr('data-policy-no', p.policy_no || '');
|
||||
option.attr('data-policy-type', p.policy_type || '');
|
||||
option.attr('data-start-date', p.policy_start_date || '');
|
||||
option.attr('data-end-date', p.policy_end_date || '');
|
||||
$('#client_policy_id').append(option);
|
||||
});
|
||||
}
|
||||
|
||||
function resetBranch() {
|
||||
$('#branch_id').empty().append($('<option>', { value: '', text: 'Select Branch' }));
|
||||
}
|
||||
|
||||
function resetPolicy() {
|
||||
$('#client_policy_id').empty().append($('<option>', { value: '', text: 'Select Policy' }));
|
||||
$('#insurer_id').val('');
|
||||
$('#insurer_name_display').val('');
|
||||
$('#policy_no').val('');
|
||||
$('#client_policy_id_hidden').val('');
|
||||
$('#policy_start_date').val('');
|
||||
$('#policy_end_date').val('');
|
||||
}
|
||||
|
||||
function resetContact() {
|
||||
$('input[name="insured_contact_name"]').val('');
|
||||
$('input[name="insured_contact_number"]').val('');
|
||||
$('input[name="insured_contact_email"]').val('');
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
var str = dateStr.toString();
|
||||
var months = {Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11',Dec:'12'};
|
||||
// Handle "12-Feb-2026 05:30 am" or "12-Feb-2026"
|
||||
var m = str.match(/^(\d{1,2})-([A-Za-z]{3})-(\d{4})/);
|
||||
if (m) {
|
||||
return ('0' + m[1]).slice(-2) + '-' + (months[m[2]] || '00') + '-' + m[3];
|
||||
}
|
||||
// Handle "2026-02-12" or "2026-02-12 00:00:00"
|
||||
var m2 = str.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (m2) {
|
||||
return m2[3] + '-' + m2[2] + '-' + m2[1];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// Clear all validation states
|
||||
function clearValidation() {
|
||||
$('#nonEbClaimForm .is-invalid').removeClass('is-invalid');
|
||||
$('#nonEbClaimForm .is-invalid-select2').removeClass('is-invalid-select2');
|
||||
}
|
||||
|
||||
// Mark field invalid
|
||||
function setInvalid(field, msg) {
|
||||
var $el = $(field);
|
||||
$el.addClass('is-invalid');
|
||||
if (msg) {
|
||||
var $fb = $el.siblings('.invalid-feedback');
|
||||
if ($fb.length) $fb.text(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate form - returns true if valid
|
||||
function validateNonEbForm() {
|
||||
clearValidation();
|
||||
var errors = [];
|
||||
var firstInvalid = null;
|
||||
|
||||
function markError(selector, msg) {
|
||||
var $el = $(selector);
|
||||
if ($el.length) {
|
||||
$el.addClass('is-invalid');
|
||||
var $fb = $el.siblings('.invalid-feedback');
|
||||
if ($fb.length) $fb.text(msg);
|
||||
}
|
||||
errors.push(msg);
|
||||
if (!firstInvalid) firstInvalid = $el;
|
||||
}
|
||||
|
||||
function markSelect2Error(selector, msg) {
|
||||
var $el = $(selector);
|
||||
$el.next('.select2-container').find('.select2-selection').addClass('is-invalid-select2');
|
||||
errors.push(msg);
|
||||
if (!firstInvalid) firstInvalid = $el;
|
||||
}
|
||||
|
||||
// --- Required fields ---
|
||||
// Client
|
||||
if (!$('#client_select').val()) {
|
||||
markSelect2Error('#client_select', 'Client is required.');
|
||||
}
|
||||
|
||||
// Branch
|
||||
if (!$('#branch_id').val()) {
|
||||
markSelect2Error('#branch_id', 'Branch is required.');
|
||||
}
|
||||
|
||||
// Account Manager
|
||||
if (!$('#acm_id').val()) {
|
||||
markSelect2Error('#acm_id', 'Account Manager is required.');
|
||||
}
|
||||
|
||||
// Status
|
||||
if (!$('#claim_status_id').val()) {
|
||||
markError('#claim_status_id', 'Status is required.');
|
||||
}
|
||||
|
||||
// Contact Name - required, min 3, alphanumeric + space _ -
|
||||
var contactName = $.trim($('input[name="insured_contact_name"]').val());
|
||||
if (!contactName) {
|
||||
markError('input[name="insured_contact_name"]', 'Contact Name is required.');
|
||||
} else if (contactName.length < 3) {
|
||||
markError('input[name="insured_contact_name"]', 'Min 3 characters.');
|
||||
} else if (!/^[a-zA-Z0-9\s_-]+$/.test(contactName)) {
|
||||
markError('input[name="insured_contact_name"]', 'Only letters, numbers, spaces, hyphens, underscores.');
|
||||
}
|
||||
|
||||
// Contact Number - required, numeric, 10-15 digits
|
||||
var contactNum = $.trim($('input[name="insured_contact_number"]').val());
|
||||
if (!contactNum) {
|
||||
markError('input[name="insured_contact_number"]', 'Contact Number is required.');
|
||||
} else if (!/^[0-9]+$/.test(contactNum)) {
|
||||
markError('input[name="insured_contact_number"]', 'Must be numeric only.');
|
||||
} else if (contactNum.length < 10 || contactNum.length > 15) {
|
||||
markError('input[name="insured_contact_number"]', 'Must be 10-15 digits.');
|
||||
}
|
||||
|
||||
// Contact Email - optional, validate format if provided
|
||||
var contactEmail = $.trim($('input[name="insured_contact_email"]').val());
|
||||
if (contactEmail && !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(contactEmail)) {
|
||||
markError('input[name="insured_contact_email"]', 'Invalid email format.');
|
||||
}
|
||||
|
||||
// Nature of Loss - required, min 3
|
||||
var natureOfLoss = $.trim($('input[name="nature_of_loss"]').val());
|
||||
if (!natureOfLoss) {
|
||||
markError('input[name="nature_of_loss"]', 'Nature of Loss is required.');
|
||||
} else if (natureOfLoss.length < 3) {
|
||||
markError('input[name="nature_of_loss"]', 'Min 3 characters.');
|
||||
}
|
||||
|
||||
// Loss Location - required
|
||||
if (!$.trim($('input[name="loss_location"]').val())) {
|
||||
markError('input[name="loss_location"]', 'Loss Location is required.');
|
||||
}
|
||||
|
||||
// Loss Date - required
|
||||
if (!$.trim($('input[name="loss_date"]').val())) {
|
||||
markError('input[name="loss_date"]', 'Loss Date is required.');
|
||||
}
|
||||
|
||||
// Loss Estimate - optional, numeric
|
||||
var lossEst = $.trim($('input[name="loss_estimate"]').val());
|
||||
if (lossEst && isNaN(lossEst)) {
|
||||
markError('input[name="loss_estimate"]', 'Must be a valid number.');
|
||||
}
|
||||
|
||||
// Claim Number - optional, pattern
|
||||
var claimNum = $.trim($('input[name="claim_number"]').val());
|
||||
if (claimNum && !/^[a-zA-Z0-9\/_-]+$/.test(claimNum)) {
|
||||
markError('input[name="claim_number"]', 'Only letters, numbers, /, - allowed.');
|
||||
}
|
||||
|
||||
// Nhance Ref No - optional, max 100
|
||||
var nhanceRef = $.trim($('input[name="nhance_claim_ref_no"]').val());
|
||||
if (nhanceRef && nhanceRef.length > 100) {
|
||||
markError('input[name="nhance_claim_ref_no"]', 'Max 100 characters.');
|
||||
}
|
||||
|
||||
// --- Surveyor fields (only if section visible) ---
|
||||
if ($('#section_surveyor').is(':visible')) {
|
||||
var survContactNum = $.trim($('input[name="surveyor_contact_number"]').val());
|
||||
if (survContactNum) {
|
||||
if (!/^[0-9]+$/.test(survContactNum)) {
|
||||
markError('input[name="surveyor_contact_number"]', 'Must be numeric only.');
|
||||
} else if (survContactNum.length < 10 || survContactNum.length > 15) {
|
||||
markError('input[name="surveyor_contact_number"]', 'Must be 10-15 digits.');
|
||||
}
|
||||
}
|
||||
var survEmail = $.trim($('input[name="surveyor_email"]').val());
|
||||
if (survEmail && !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(survEmail)) {
|
||||
markError('input[name="surveyor_email"]', 'Invalid email format.');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Settlement fields (only if section visible) ---
|
||||
if ($('#section_settlement').is(':visible')) {
|
||||
var lossAssessed = $.trim($('input[name="loss_assessed_value"]').val());
|
||||
if (lossAssessed && isNaN(lossAssessed)) {
|
||||
markError('input[name="loss_assessed_value"]', 'Must be a valid number.');
|
||||
}
|
||||
var settledAmt = $.trim($('input[name="settled_amount"]').val());
|
||||
if (settledAmt && isNaN(settledAmt)) {
|
||||
markError('input[name="settled_amount"]', 'Must be a valid number.');
|
||||
}
|
||||
var settlementUtr = $.trim($('input[name="settlement_utr"]').val());
|
||||
if (settlementUtr && settlementUtr.length > 100) {
|
||||
markError('input[name="settlement_utr"]', 'Max 100 characters.');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Asset: at least one filled row OR file uploaded ---
|
||||
var hasAssetRow = false;
|
||||
$('.asset-row').each(function() {
|
||||
var $inputs = $(this).find('input');
|
||||
$inputs.each(function() {
|
||||
if ($.trim($(this).val())) { hasAssetRow = true; return false; }
|
||||
});
|
||||
if (hasAssetRow) return false;
|
||||
});
|
||||
var assetFile = $('#asset_file')[0].files.length > 0;
|
||||
if (assetFile) {
|
||||
var fileName = $('#asset_file')[0].files[0].name.toLowerCase();
|
||||
var allowedExts = ['.xlsx', '.xls', '.csv', '.pdf'];
|
||||
var validExt = allowedExts.some(function(ext) { return fileName.endsWith(ext); });
|
||||
if (!validExt) {
|
||||
markError('#asset_file', 'Only Excel (.xlsx, .xls, .csv) or PDF (.pdf) files allowed.');
|
||||
assetFile = false;
|
||||
} else if (!$.trim($('textarea[name="loss_description"]').val())) {
|
||||
markError('textarea[name="loss_description"]', 'Loss Description is required when uploading an asset file.');
|
||||
}
|
||||
}
|
||||
if (!hasAssetRow && !assetFile) {
|
||||
$('#asset_section_error').show();
|
||||
errors.push('Please fill at least one asset row or upload an asset file.');
|
||||
if (!firstInvalid) firstInvalid = $('#asset_container');
|
||||
} else {
|
||||
$('#asset_section_error').hide();
|
||||
}
|
||||
|
||||
// Show errors & scroll to first invalid
|
||||
if (errors.length > 0) {
|
||||
errors.forEach(function(e) { toastr.error(e); });
|
||||
if (firstInvalid && firstInvalid.length) {
|
||||
// Expand collapsed section if needed
|
||||
var $collapse = firstInvalid.closest('.collapse');
|
||||
if ($collapse.length && !$collapse.hasClass('show')) {
|
||||
$collapse.collapse('show');
|
||||
}
|
||||
$('html, body').animate({ scrollTop: firstInvalid.offset().top - 100 }, 300);
|
||||
firstInvalid.focus();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Submit
|
||||
function submitNonEbClaim() {
|
||||
if (!validateNonEbForm()) return;
|
||||
|
||||
var formData = new FormData(document.getElementById('nonEbClaimForm'));
|
||||
$.ajax({
|
||||
url: base_url + 'non-eb-claim/create',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
success: function(response) {
|
||||
if (typeof response === 'string') response = JSON.parse(response);
|
||||
if (response.status == true) {
|
||||
toastr.success(response.message);
|
||||
window.location.href = base_url + 'non-eb-claim/list';
|
||||
} else {
|
||||
if (response.errors) { Object.values(response.errors).forEach(function(e) { toastr.error(e); }); }
|
||||
else { toastr.error(response.message); }
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status == 400) {
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.errors) { Object.values(resp.errors).forEach(function(e) { toastr.error(e); }); }
|
||||
} else {
|
||||
toastr.error('Something went wrong. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Init
|
||||
$(document).ready(function() {
|
||||
if (typeof flatpickr !== 'undefined') {
|
||||
$('.date-picker').flatpickr({ dateFormat: 'd-m-Y', maxDate: 'today' });
|
||||
}
|
||||
// Initialize Select2
|
||||
$('.select2-init').select2();
|
||||
$('#client_select').select2();
|
||||
$('#branch_id').select2();
|
||||
$('#client_policy_id').select2();
|
||||
|
||||
// Client change -> fetch branches, policies & contacts
|
||||
$('#client_select').on('change', function() {
|
||||
var client_id = $(this).val();
|
||||
resetBranch();
|
||||
resetPolicy();
|
||||
resetContact();
|
||||
|
||||
if (!client_id) {
|
||||
$('#client_id').val('');
|
||||
return;
|
||||
}
|
||||
$('#client_id').val(client_id);
|
||||
noneb_policy_list = {};
|
||||
noneb_contact_list = {};
|
||||
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
sendAjaxRequestForGlobal(base_url + 'non-eb-claim/getBranchAndPolicy', 'POST', {client_id: client_id}, function(response) {
|
||||
if (response.status) {
|
||||
noneb_policy_list = response.policy_data || {};
|
||||
noneb_contact_list = response.contact_data || {};
|
||||
appendBranches(response.branch_data || []);
|
||||
} else {
|
||||
toastr.error(response.message || 'Failed to load branches');
|
||||
}
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
}, function() {
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
});
|
||||
});
|
||||
|
||||
// Branch change -> populate policies & auto-fill contact from stored data
|
||||
$('#branch_id').on('change', function() {
|
||||
var branch_id = $(this).val();
|
||||
resetPolicy();
|
||||
resetContact();
|
||||
if (!branch_id) return;
|
||||
|
||||
var policies = noneb_policy_list[branch_id] || [];
|
||||
appendPolicies(policies);
|
||||
|
||||
// Auto-fill insured contact from level_contacts
|
||||
var contact = noneb_contact_list[branch_id];
|
||||
if (contact) {
|
||||
$('input[name="insured_contact_name"]').val(contact.name || '');
|
||||
$('input[name="insured_contact_number"]').val(contact.mobile || '');
|
||||
$('input[name="insured_contact_email"]').val(contact.email || '');
|
||||
}
|
||||
});
|
||||
|
||||
// Policy change -> auto-fill insurer, policy no, dates & section
|
||||
$('#client_policy_id').on('change', function() {
|
||||
var sel = $(this).find(':selected');
|
||||
$('#insurer_id').val(sel.data('insurer') || '');
|
||||
$('#insurer_name_display').val(sel.data('insurer-name') || '');
|
||||
$('#policy_no').val(sel.data('policy-no') || '');
|
||||
$('#client_policy_id_hidden').val($(this).val());
|
||||
$('#policy_type_display').val(sel.data('policy-type') || '');
|
||||
var startRaw = sel.attr('data-start-date') || '';
|
||||
var endRaw = sel.attr('data-end-date') || '';
|
||||
// API returns YYYY-MM-DD, convert to dd-mm-yyyy
|
||||
if (startRaw && startRaw.indexOf('-') > -1) {
|
||||
var sp = startRaw.split('-');
|
||||
if (sp[0].length === 4) startRaw = sp[2] + '-' + sp[1] + '-' + sp[0];
|
||||
}
|
||||
if (endRaw && endRaw.indexOf('-') > -1) {
|
||||
var ep = endRaw.split('-');
|
||||
if (ep[0].length === 4) endRaw = ep[2] + '-' + ep[1] + '-' + ep[0];
|
||||
}
|
||||
$('#policy_start_date').val(startRaw);
|
||||
$('#policy_end_date').val(endRaw);
|
||||
});
|
||||
|
||||
// Clear validation on input change
|
||||
$('#nonEbClaimForm').on('input change', '.form-control', function() {
|
||||
$(this).removeClass('is-invalid');
|
||||
});
|
||||
$('#nonEbClaimForm').on('change', 'select', function() {
|
||||
$(this).removeClass('is-invalid');
|
||||
$(this).next('.select2-container').find('.select2-selection').removeClass('is-invalid-select2');
|
||||
});
|
||||
// Clear asset section error on asset input or file change
|
||||
$('#nonEbClaimForm').on('input', '.asset-row input', function() {
|
||||
$('#asset_section_error').hide();
|
||||
});
|
||||
$('#asset_file').on('change', function() {
|
||||
$(this).removeClass('is-invalid');
|
||||
$('#asset_section_error').hide();
|
||||
});
|
||||
|
||||
var closureRemarkStatuses = ['Claim Closed', 'Claim Rejected', 'Claim Withdrawn'];
|
||||
function toggleClosureRemark() {
|
||||
var selectedText = $('#claim_status_id option:selected').text().trim();
|
||||
if (closureRemarkStatuses.indexOf(selectedText) !== -1) {
|
||||
$('#closure_remark_row').show();
|
||||
} else {
|
||||
$('#closure_remark_row').hide();
|
||||
$('#closure_remark').val('');
|
||||
}
|
||||
}
|
||||
|
||||
// Status change -> toggle sections + closure remark
|
||||
$('#claim_status_id').on('change', function() {
|
||||
var status_id = $(this).val();
|
||||
var policy_type_id = $('#policy_type_id').val();
|
||||
sendAjaxRequestForGlobal(base_url + 'non-eb-claim/getVisibleSections', 'POST', {claim_status_id: status_id, policy_type_id: policy_type_id}, function(response) {
|
||||
if (response.status) {
|
||||
var s = response.sections;
|
||||
['surveyor','settlement'].forEach(function(sec) {
|
||||
if (s.includes(sec)) { $('#section_'+sec).show(); } else { $('#section_'+sec).hide(); }
|
||||
});
|
||||
}
|
||||
});
|
||||
toggleClosureRemark();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
162
app/Views/non_eb_claim_list.php
Normal file
162
app/Views/non_eb_claim_list.php
Normal file
@ -0,0 +1,162 @@
|
||||
<style>
|
||||
.table th, .table td { padding: 8px; }
|
||||
table.dataTable tbody td { padding: 4px 4px !important; }
|
||||
.col-12 { max-width: 98% !important; }
|
||||
.dataTables_filter { position: absolute; }
|
||||
.dataTables_length label { height: 21px !important; }
|
||||
.text-custom-grey { font-size: 0.65em; color: #6c757d !important; }
|
||||
#non-eb-datatable tbody tr:hover { background-color: #e0e0e0; }
|
||||
</style>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="non-eb-datatable">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Status </th>
|
||||
<th>Policy Type </th>
|
||||
<th>Claim Number </th>
|
||||
<th>Nhance Ref </th>
|
||||
<th>Client </th>
|
||||
<th>Insurer </th>
|
||||
<th>Loss Date </th>
|
||||
<th>Nature of Loss </th>
|
||||
|
||||
<th style="display:none;">ACM</th>
|
||||
<th style="display:none;">Policy No</th>
|
||||
<th style="display:none;">Loss Location</th>
|
||||
<th style="display:none;">Loss Estimate</th>
|
||||
<th style="display:none;">Loss Description</th>
|
||||
<th style="display:none;">Intimation Recd</th>
|
||||
<th style="display:none;">Intimated to Insurer</th>
|
||||
<th style="display:none;">Contact Name</th>
|
||||
<th style="display:none;">Contact Number</th>
|
||||
<th style="display:none;">Contact Email</th>
|
||||
<th style="display:none;">Surveyor</th>
|
||||
<th style="display:none;">Loss Assessed</th>
|
||||
<th style="display:none;">Settled Amount</th>
|
||||
<th style="display:none;">Settlement UTR</th>
|
||||
<th style="display:none;">Priority</th>
|
||||
|
||||
<?php if(in_array(get_role_id(), [1,2,5])): ?>
|
||||
<th>ACTION</th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (isset($ticket_data)): foreach($ticket_data as $row): ?>
|
||||
<tr data-id="<?= $row['id']; ?>" style="cursor:pointer;">
|
||||
<td>
|
||||
<span><?= $row['status'] ?? 'N/A'; ?></span>
|
||||
<?php if (!empty($row['ticket_updated_date'])): ?>
|
||||
<span class="text-custom-grey"><br><?= "Updated " . $row['ticket_updated_date']; ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $row['policy_type_name'] ?? 'N/A'; ?></td>
|
||||
<td>
|
||||
<?= $row['claim_number'] ?: 'N/A'; ?>
|
||||
<?php if (!empty($row['ticket_created_date'])): ?>
|
||||
<span class="text-custom-grey"><br><?= "Created " . $row['ticket_created_date']; ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $row['nhance_claim_ref_no'] ?: 'N/A'; ?></td>
|
||||
<td><?= $row['short_name'] ?: ($row['client_name'] ?? 'N/A'); ?></td>
|
||||
<td><?= $row['insurer_short_name'] ?: ($row['insurer_name'] ?: 'N/A'); ?></td>
|
||||
<td><?= !empty($row['loss_date']) ? date('d-m-Y', strtotime($row['loss_date'])) : 'N/A'; ?></td>
|
||||
<td><?= $row['nature_of_loss'] ?: 'N/A'; ?></td>
|
||||
|
||||
<td style="display:none;"><?= $row['acm_name'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['policy_no'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['loss_location'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['loss_estimate'] ?? ''; ?></td>
|
||||
<td style="display:none;"></td>
|
||||
<td style="display:none;"><?= $row['intimation_recd_date'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['intimated_to_insurer_date'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['insured_contact_name'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['insured_contact_number'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['insured_contact_email'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['surveyor_name'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['loss_assessed_value'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['settled_amount'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= $row['settlement_utr'] ?? ''; ?></td>
|
||||
<td style="display:none;"><?= isset($priorityType[$row['priority'] ?? 0]) ? $priorityType[$row['priority']] : ''; ?></td>
|
||||
|
||||
<?php if(in_array(get_role_id(), [1,2,5])): ?>
|
||||
<td>
|
||||
<div class="btn-group dropdown">
|
||||
<a href="javascript:void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown"><i class="mdi mdi-dots-horizontal"></i></a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" onclick="removeClaim('<?= $row['id'] ?>')"><i class="mdi mdi-delete mr-2 text-muted font-18 vertical-middle"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var table = $('#non-eb-datatable');
|
||||
if (table.length) {
|
||||
table.DataTable({
|
||||
scrollX: true,
|
||||
dom: "<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
|
||||
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
|
||||
buttons: [
|
||||
{ text: '<i class="mdi mdi-filter"></i><span class="btn-custom"> Filter </span>', className: 'btn app-btn-primary mr-2', action: function() { openFilterNav(); } },
|
||||
{ text: '<i class="mdi mdi-plus"></i><span class="btn-custom"> Add </span>', className: 'btn app-btn-primary mr-2', action: function() { window.location.href = '<?= base_url("non-eb-claim/new/50") ?>'; } },
|
||||
{
|
||||
extend: 'collection', text: '<span class="btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>', className: 'btn app-btn-secondary',
|
||||
buttons: [
|
||||
{ extend: 'csv', text: '<i class="mdi mdi-file-delimited"></i> CSV', className: 'app-btn-primary', title: 'Non-EB-Claim-List' },
|
||||
{ extend: 'excel', text: '<i class="mdi mdi-file-excel"></i> EXCEL', title: 'Non-EB-Claim-List', sheetName: 'Claims', exportOptions: { orthogonal: 'sort' }, className: 'app-btn-primary' }
|
||||
]
|
||||
}
|
||||
],
|
||||
language: {
|
||||
search: '<div class="datatable-search-wrapper" style="position:relative;display:inline-block;">_INPUT_<i class="mdi mdi-magnify datatable-search-icon" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#666;"></i></div>',
|
||||
searchPlaceholder: "Search",
|
||||
emptyTable: '<div class="text-center text-muted">No Data found</div>'
|
||||
},
|
||||
paging: true, pageLength: 10, ordering: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// function openPolicyTypeModal() {
|
||||
// var modalEl = document.getElementById('policyTypeModal');
|
||||
// if (modalEl) {
|
||||
// var myModal = new bootstrap.Modal(modalEl);
|
||||
// myModal.show();
|
||||
// }
|
||||
// }
|
||||
|
||||
function viewClaim(id) { window.location.href = '<?= base_url("non-eb-claim/view/") ?>' + id; }
|
||||
|
||||
function removeClaim(ticket_id) {
|
||||
confirmActionSweertAlert("Do you want to delete this Claim?", "Yes, Proceed!", "No, Cancel").then((confirmed) => {
|
||||
if (confirmed) {
|
||||
sendAjaxRequestForGlobal('<?= base_url("non-eb-claim/remove") ?>', 'GET', { ticket_id: ticket_id }, function(response) {
|
||||
if (response.status === true) { toastr.success(response.message); window.location.reload(); }
|
||||
else { toastr.warning(response.message); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).on('click', '#non-eb-datatable tbody tr', function(e) {
|
||||
if ($(e.target).closest('td').index() !== $(this).children('td').length - 1) {
|
||||
viewClaim($(this).data('id'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
321
app/Views/non_eb_claim_mail_template.php
Normal file
321
app/Views/non_eb_claim_mail_template.php
Normal file
@ -0,0 +1,321 @@
|
||||
<style>
|
||||
.table th, .table td { padding: 8px; }
|
||||
table.dataTable tbody td { padding: 4px 4px !important; }
|
||||
.dataTables_filter { position: absolute; }
|
||||
.addbtnStyle { margin-left: 20px !important; color: #000; }
|
||||
.jodit-container .jodit-wysiwyg, .jodit-container .jodit-wysiwyg * { color: #000 !important; background-color: #fff !important; }
|
||||
.dataTables_length label { height: 21px !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var pageSubTitle = 'Non-EB Mail Templates';
|
||||
</script>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row" style="padding-bottom: 10px;">
|
||||
<div class="col-6" style="align-self: center;">
|
||||
<h4 style="position: relative;">Mail Template List</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table data-custom-table-css="table" id="noneb-mail-template-table" class="table w-100 nowrap">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>Template Name</th>
|
||||
<th>Policy Type</th>
|
||||
<th>Trigger Type</th>
|
||||
<th>Mail Send Type</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (isset($ticket_data)): foreach ($ticket_data as $row): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?= $row['template_name'] ?? '' ?>
|
||||
<i class="fe-alert-circle" style="color:#000;" data-toggle="tooltip" data-placement="top" title="Status: <?= $row['tool_tip'] ?? '' ?>"></i>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$ptName = '';
|
||||
if (isset($policy_types)) {
|
||||
foreach ($policy_types as $pt) {
|
||||
if ($pt['id'] == $row['ticket_type']) { $ptName = $pt['policy_type']; break; }
|
||||
}
|
||||
}
|
||||
echo $ptName;
|
||||
?>
|
||||
</td>
|
||||
<td><?= $trigger_type[$row['trigger_type']] ?? '' ?></td>
|
||||
<td><?= $row['is_auto_mail'] == 1 ? 'Auto' : 'Manual' ?></td>
|
||||
<td>
|
||||
<div class="btn-group dropdown">
|
||||
<a href="javascript:void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown"><i class="mdi mdi-dots-horizontal"></i></a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="#" onclick="getMailTemplateData(<?= $row['id'] ?>)"><i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit</a>
|
||||
<a class="dropdown-item" onclick="deleteTemplate(<?= $row['id'] ?>)"><i class="mdi mdi-delete mr-2 text-muted font-18 vertical-middle"></i>Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Template Modal -->
|
||||
<div class="modal fade" id="new_mail_template" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static">
|
||||
<div class="modal-dialog modal-full-width">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add New Template</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" method="post" id="ticket_mail_editor_modal" enctype="multipart/form-data" novalidate>
|
||||
<div class="form-group">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-4">
|
||||
<label>Template Name</label>
|
||||
<input type="text" id="template_name" name="template_name" class="form-control" placeholder="Template Name" required>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Policy Type</label>
|
||||
<select class="form-control" id="policy_type" name="ticket_type" required>
|
||||
<option value="">Select Policy Type</option>
|
||||
<?php if (isset($policy_types)): foreach ($policy_types as $pt): ?>
|
||||
<option value="<?= $pt['id'] ?>"><?= $pt['policy_type'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Trigger Type</label>
|
||||
<select class="form-control" id="trigger_type" name="trigger_type" required>
|
||||
<option value="">Select Trigger</option>
|
||||
<?php if (isset($trigger_type)): foreach ($trigger_type as $k => $v): ?>
|
||||
<option value="<?= $k ?>"><?= $v ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
<input id="primaryKey" type="hidden">
|
||||
</div>
|
||||
<div class="form-group col-md-2">
|
||||
<div id="statusSwitchWrapper" class="dt-switch-wrapper" style="padding-top:40px;padding-left:10px;">
|
||||
<div class="custom-control custom-switch" style="text-align:left;">
|
||||
<input type="checkbox" class="custom-control-input" id="statusSwitch">
|
||||
<label class="custom-control-label" for="statusSwitch">Auto Mail</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label>Claim Status</label>
|
||||
<input type="text" readonly id="claim_status_for_template" class="form-control">
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label>Subject</label>
|
||||
<input type="text" id="template_subject" name="subject" class="form-control" placeholder="Subject" required>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="form-row" id="rac_rate_dropdown">
|
||||
<div class="form-group col-md-12">
|
||||
<select id="ticket_mail_customButton" class="form-control" style="border:none;right:6px;width:auto;position:absolute;z-index:1;top:17px;height:32px;float:right;">
|
||||
<option value="">PlaceHolders</option>
|
||||
<?php foreach ($placeHolders as $key => $value): ?>
|
||||
<?php $valueChange = ucwords(str_replace('_', ' ', $value)); ?>
|
||||
<option value="<?= $key ?>"><?= $valueChange ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ticket_mail_editor" name="content" style="height: 300px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-right m-b-0">
|
||||
<button type="submit" class="btn btn-primary" id="btnGridSubmit_2">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
var ticketsTable = $('#noneb-mail-template-table');
|
||||
if (ticketsTable.length) {
|
||||
ticketsTable.DataTable({
|
||||
scrollX: true,
|
||||
dom: "<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
|
||||
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
|
||||
buttons: [{
|
||||
text: 'Add Template',
|
||||
className: 'buttons-html5 addbtnStyle',
|
||||
action: function() { openModal(); }
|
||||
}],
|
||||
language: {
|
||||
search: '<div class="datatable-search-wrapper" style="position:relative;display:inline-block;">_INPUT_<i class="mdi mdi-magnify datatable-search-icon" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#666;"></i></div>',
|
||||
searchPlaceholder: "Search",
|
||||
emptyTable: '<div class="text-center text-muted">No Data found</div>'
|
||||
},
|
||||
paging: true, pageLength: 10, ordering: false,
|
||||
});
|
||||
}
|
||||
|
||||
var toolbar = "bold,italic,strikethrough,|,superscript,subscript,|,align,";
|
||||
const editorConfig = {
|
||||
buttons: toolbar.concat(['fontsize']),
|
||||
fontsize: [8, 10, 12, 14, 16, 18, 20, 22, 24],
|
||||
showPlaceholder: false,
|
||||
toolbarButtonSize: 'small',
|
||||
toolbarAdaptive: false,
|
||||
saveHeightInStorage: true,
|
||||
minHeight: 400,
|
||||
defaultStyle: { color: '#000000' },
|
||||
};
|
||||
const editor = Jodit.make("#ticket_mail_editor", editorConfig);
|
||||
|
||||
let lastActiveField = null;
|
||||
$("#template_subject").on("focus", function() { lastActiveField = this; });
|
||||
editor.events.on("focus", function() { lastActiveField = editor; });
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
var target = event.target;
|
||||
if (target.matches('#ticket_mail_customButton')) {
|
||||
let placeholderValue = $(target).val();
|
||||
if (!placeholderValue) return;
|
||||
if (lastActiveField) {
|
||||
if (lastActiveField.id === "template_subject") {
|
||||
let input = lastActiveField;
|
||||
let startPos = input.selectionStart;
|
||||
let endPos = input.selectionEnd;
|
||||
input.value = input.value.substring(0, startPos) + placeholderValue + input.value.substring(endPos);
|
||||
input.selectionStart = input.selectionEnd = startPos + placeholderValue.length;
|
||||
} else {
|
||||
editor.selection.insertHTML(placeholderValue);
|
||||
}
|
||||
}
|
||||
$(target).val('');
|
||||
}
|
||||
});
|
||||
|
||||
$('.close').click(function() {
|
||||
$('#template_subject').val('');
|
||||
$('#template_name').val('');
|
||||
$('#primaryKey').val('');
|
||||
$('#policy_type').val('').change();
|
||||
$('#trigger_type').val('').change();
|
||||
$("#claim_status_for_template").val("");
|
||||
editor.setEditorValue('');
|
||||
$('#statusSwitch').prop('checked', false);
|
||||
});
|
||||
|
||||
// Submit template
|
||||
$('#ticket_mail_editor_modal').submit(function(event) {
|
||||
event.preventDefault();
|
||||
let isValid = true;
|
||||
$('#ticket_mail_editor_modal [required]').each(function() {
|
||||
if (!this.value.trim()) { isValid = false; $(this).addClass('is-invalid'); } else { $(this).removeClass('is-invalid'); }
|
||||
});
|
||||
if (!isValid) { toastr.error('Please fill all required fields.', 'Validation Error'); return; }
|
||||
|
||||
var joditEditor = editor;
|
||||
if (joditEditor) {
|
||||
var mailContent = $(joditEditor.currentPlace.container).find('.jodit-wysiwyg').html();
|
||||
var formData = $(this).serializeArray();
|
||||
formData.push({ name: 'mail_content', value: mailContent });
|
||||
var isAutoMailEnabled = $('#statusSwitch').is(':checked') ? 1 : 0;
|
||||
formData.push({ name: 'is_auto_mail', value: isAutoMailEnabled });
|
||||
var primary_key = $('#primaryKey').val();
|
||||
if (primary_key) { formData.push({ name: 'id', value: primary_key }); }
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
var form_action = '<?= base_url("non-eb-claim/crud_mail_template/1") ?>';
|
||||
$.ajax({
|
||||
url: form_action, type: "POST", data: formData, dataType: 'json',
|
||||
success: function(res) {
|
||||
if (res.status == true) {
|
||||
$('#new_mail_template').find('.close').click();
|
||||
toastr.success('Template saved successfully!', 'Success');
|
||||
location.reload();
|
||||
} else { toastr.error('Failed to Save Data', 'Error'); }
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
},
|
||||
error: function(xhr) {
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
if (xhr.status === 400) {
|
||||
let response = JSON.parse(xhr.responseText);
|
||||
if (response.errors) {
|
||||
let msgs = ""; $.each(response.errors, function(f, m) { msgs += '• ' + m + '<br>'; });
|
||||
toastr.error(msgs, 'Validation Error', { "allowHtml": true });
|
||||
} else { toastr.warning(response.message || 'Validation failed'); }
|
||||
} else { toastr.error('Something went wrong.', 'Error'); }
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openModal() {
|
||||
var myModal = new bootstrap.Modal(document.getElementById('new_mail_template'));
|
||||
myModal.show();
|
||||
}
|
||||
|
||||
function deleteTemplate(id) {
|
||||
Swal.fire({
|
||||
title: "Are you sure?", text: "You want to remove this template.", icon: "info",
|
||||
showCancelButton: true, confirmButtonColor: "#3085d6", confirmButtonText: "Yes",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
$.ajax({
|
||||
url: '<?= base_url("non-eb-claim/crud_mail_template/3") ?>', type: "POST", data: { id: id },
|
||||
success: function(res) {
|
||||
if (res.status === true) { location.reload(); toastr.success('Template Deleted!', 'Success'); }
|
||||
else { toastr.error('Failed to Delete Template', 'Error'); }
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
},
|
||||
error: function() {
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
toastr.error('Something went wrong!', 'Error');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getMailTemplateData(id) {
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
$.ajax({
|
||||
url: '<?= base_url("non-eb-claim/crud_mail_template/2") ?>', type: "POST", data: { id: id },
|
||||
success: function(res) {
|
||||
if (res && res.status == true) {
|
||||
openModal();
|
||||
$('#template_subject').val(res.data.subject);
|
||||
$('#template_name').val(res.data.template_name);
|
||||
$('#primaryKey').val(res.data.id);
|
||||
$('#policy_type').val(res.data.ticket_type).change();
|
||||
$('#trigger_type').val(res.data.trigger_type).change();
|
||||
$('#claim_status_for_template').val(res.data.tool_tip);
|
||||
$('.jodit-wysiwyg').html(res.data.mail_content);
|
||||
$('#statusSwitch').prop('checked', res.data.is_auto_mail == 1);
|
||||
} else { toastr.error('Data Not Found', 'Error'); }
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
},
|
||||
error: function() {
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
toastr.warning('Something Went Wrong!', 'Warning');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
159
app/Views/non_eb_claim_reports.php
Normal file
159
app/Views/non_eb_claim_reports.php
Normal file
@ -0,0 +1,159 @@
|
||||
<style>
|
||||
.table th, .table td { padding: 8px; }
|
||||
table.dataTable tbody td { padding: 4px 4px !important; }
|
||||
.col-12 { max-width: 98% !important; }
|
||||
.dataTables_filter { position: absolute; }
|
||||
.dataTables_length label { height: 21px !important; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var pageSubTitle = 'Non-EB Claim Reports';
|
||||
</script>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="form-group col-md-3">
|
||||
<label>Policy Type</label>
|
||||
<select class="form-control" id="report_policy_type">
|
||||
<option value="">All</option>
|
||||
<?php if (isset($policy_types)): foreach ($policy_types as $pt): ?>
|
||||
<option value="<?= $pt['id'] ?>"><?= $pt['policy_type'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Account Manager</label>
|
||||
<select class="form-control" id="report_acm">
|
||||
<option value="">All</option>
|
||||
<?php if (isset($account_manager)): foreach ($account_manager as $am): ?>
|
||||
<option value="<?= $am['first_name'] ?>"><?= $am['first_name'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>Start Date</label>
|
||||
<input type="text" class="form-control report-date" id="report_start_date" value="<?= $default_start ?? '' ?>" placeholder="dd-mm-yyyy">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label>End Date</label>
|
||||
<input type="text" class="form-control report-date" id="report_end_date" value="<?= $default_end ?? '' ?>" placeholder="dd-mm-yyyy">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12 text-right">
|
||||
<button class="btn btn-primary" onclick="generateReport()">Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" id="report_table_div">
|
||||
<table data-custom-table-css="table" id="report-datatable" class="table w-100 nowrap table-centered" cellspacing="0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th>S.No</th>
|
||||
<th>Status</th>
|
||||
<th>Policy Type</th>
|
||||
<th>Claim Number</th>
|
||||
<th>Nhance Ref</th>
|
||||
<th>Client</th>
|
||||
<th>Insurer</th>
|
||||
<th>Loss Date</th>
|
||||
<th>Nature of Loss</th>
|
||||
<th>Loss Location</th>
|
||||
<th>Loss Estimate</th>
|
||||
<th>Surveyor</th>
|
||||
<th>Loss Assessed</th>
|
||||
<th>Settled Amount</th>
|
||||
<th>ACM</th>
|
||||
<th>Created Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
if (typeof flatpickr !== 'undefined') {
|
||||
$('.report-date').flatpickr({ dateFormat: 'd-m-Y' });
|
||||
}
|
||||
|
||||
if ($('#report-datatable').length) {
|
||||
$('#report-datatable').DataTable({
|
||||
scrollX: true,
|
||||
dom: "<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
|
||||
"<'row'<'col-sm-12'tr>>" +
|
||||
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
|
||||
lengthMenu: [[10, 20, 50, 100, -1], [10, 20, 50, 100, 'All']],
|
||||
buttons: [
|
||||
{ extend: 'csv', text: '<i class="mdi mdi-file-delimited"></i> CSV', className: 'app-btn-primary', title: 'Non-EB-Claim-Report' },
|
||||
{ extend: 'excel', text: '<i class="mdi mdi-file-excel"></i> EXCEL', title: 'Non-EB-Claim-Report', sheetName: 'Claims Report', className: 'app-btn-primary' }
|
||||
],
|
||||
language: {
|
||||
search: '<div class="datatable-search-wrapper" style="position:relative;display:inline-block;">_INPUT_<i class="mdi mdi-magnify datatable-search-icon" style="position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#666;"></i></div>',
|
||||
searchPlaceholder: "Search",
|
||||
emptyTable: '<div class="text-center text-muted">No Data found. Please generate a report.</div>'
|
||||
},
|
||||
paging: true, pageLength: 50, ordering: true,
|
||||
});
|
||||
$('.dataTables_length label').css('height', '21px');
|
||||
}
|
||||
});
|
||||
|
||||
function generateReport() {
|
||||
var requestData = {
|
||||
policy_type_id: $('#report_policy_type').val(),
|
||||
acm_name: $('#report_acm').val(),
|
||||
start_date: $('#report_start_date').val(),
|
||||
end_date: $('#report_end_date').val(),
|
||||
};
|
||||
|
||||
if (!requestData.start_date || !requestData.end_date) {
|
||||
toastr.warning('Please select Start and End dates.');
|
||||
return false;
|
||||
}
|
||||
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
var url = '<?= base_url("/non-eb-claim/reports") ?>';
|
||||
|
||||
sendAjaxRequestForGlobal(url, 'POST', requestData, function(response) {
|
||||
if (response.status == true) {
|
||||
var table = $('#report-datatable').DataTable();
|
||||
table.clear();
|
||||
if (response.data && response.data.length > 0) {
|
||||
response.data.forEach(function(row, idx) {
|
||||
table.row.add([
|
||||
idx + 1,
|
||||
row.status || 'N/A',
|
||||
row.policy_type_name || 'N/A',
|
||||
row.claim_number || 'N/A',
|
||||
row.nhance_claim_ref_no || 'N/A',
|
||||
row.client_name || 'N/A',
|
||||
row.insurer_name || 'N/A',
|
||||
row.loss_date || 'N/A',
|
||||
row.nature_of_loss || 'N/A',
|
||||
row.loss_location || 'N/A',
|
||||
row.loss_estimate || '',
|
||||
row.surveyor_name || '',
|
||||
row.loss_assessed_value || '',
|
||||
row.settled_amount || '',
|
||||
row.acm_name || '',
|
||||
row.ticket_created_date || '',
|
||||
]);
|
||||
});
|
||||
}
|
||||
table.draw();
|
||||
} else {
|
||||
toastr.error(response.message || 'Error fetching report', 'Error');
|
||||
}
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
}, function() {
|
||||
toastr.error('An error occurred.', 'Error');
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
240
app/Views/non_eb_claim_search.php
Normal file
240
app/Views/non_eb_claim_search.php
Normal file
@ -0,0 +1,240 @@
|
||||
<style>
|
||||
.col-12 { max-width: 98% !important; }
|
||||
.filter-sidebar {
|
||||
height: 100%; width: 0; position: fixed; z-index: 1001; top: 0; right: 0;
|
||||
background-color: #f8f9fa; overflow-x: hidden; transition: 0.5s;
|
||||
box-shadow: -2px 0 5px rgba(0,0,0,0.1); display: flex; flex-direction: column;
|
||||
}
|
||||
.filter-sidebar-header { padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; flex-shrink: 0; }
|
||||
.filter-sidebar-header .closebtn { font-size: 28px; text-decoration: none; color: #6c757d; }
|
||||
.filter-sidebar .closebtn:hover { color: #000; }
|
||||
.filter-sidebar-content { padding: 20px; flex-grow: 1; overflow-y: auto; }
|
||||
.filter-sidebar-footer { padding: 15px 20px; display: flex; justify-content: flex-end; border-top: 1px solid #dee2e6; flex-shrink: 0; gap: 10px; }
|
||||
</style>
|
||||
|
||||
<div id="claim-filter-sidebar" class="filter-sidebar">
|
||||
<div class="filter-sidebar-header">
|
||||
<h4 class="m-0">Filter</h4>
|
||||
<a href="javascript:void(0)" class="closebtn" onclick="closeFilterNav()">×</a>
|
||||
</div>
|
||||
<div class="filter-sidebar-content">
|
||||
<div class="form-group">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="filter_policy_type">Policy Type</label>
|
||||
<select class="form-control" id="filter_policy_type" name="policy_type_id">
|
||||
<option value="0">Select Policy Type</option>
|
||||
<?php if (isset($policy_types)): foreach ($policy_types as $pt): ?>
|
||||
<option value="<?= $pt['id'] ?>"><?= $pt['policy_type'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12">
|
||||
<label for="filter_insurer">Insurer</label>
|
||||
<select class="form-control" id="filter_insurer" name="insurer_id">
|
||||
<option value="0">Select Insurer</option>
|
||||
<?php if (isset($insurer_list)): foreach ($insurer_list as $ins): ?>
|
||||
<option value="<?= $ins['id'] ?>"><?= $ins['name'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12">
|
||||
<label for="filter_claim_no">Claim No</label>
|
||||
<input type="text" class="form-control" id="filter_claim_no" name="claim_number">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12">
|
||||
<label for="filter_nhance_ref">Nhance Ref No</label>
|
||||
<input type="text" class="form-control" id="filter_nhance_ref" name="nhance_claim_ref_no">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12">
|
||||
<label for="filter_client">Client Name</label>
|
||||
<select class="form-control" id="filter_client" name="client_id">
|
||||
<option value="0">Select Client</option>
|
||||
<?php if (isset($client_list)): foreach ($client_list as $cl): ?>
|
||||
<option value="<?= $cl['id'] ?>"><?= $cl['client_name'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12">
|
||||
<label for="filter_claim_status">Status</label>
|
||||
<select class="form-control" id="filter_claim_status" name="claim_status_id">
|
||||
<option value="0">Select Status</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12">
|
||||
<label>Date Type</label>
|
||||
<select class="form-control" id="filter_date_type" name="date_type" onchange="toggleDateField()">
|
||||
<option value="0">Select Date Type</option>
|
||||
<?php if (isset($date_type)): foreach ($date_type as $k => $v): ?>
|
||||
<option value="<?= $k ?>"><?= $v ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-12" style="display:none;" id="date_div">
|
||||
<label>Date</label>
|
||||
<div class="input-icon">
|
||||
<input type="text" id="reportrange" class="form-control" readonly style="caret-color:transparent;">
|
||||
<i class="mdi mdi-calendar-blank-outline additional-icon"></i>
|
||||
</div>
|
||||
<input type="hidden" id="startDate">
|
||||
<input type="hidden" id="endDate">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-sidebar-footer">
|
||||
<a href="#" class="btn btn-secondary" id="clear-filters" onclick="clearFilters()">Clear</a>
|
||||
<a class="btn btn-primary" onclick="fetchClaimListData();">Submit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="claim_list_div">
|
||||
<?php include('non_eb_claim_list.php'); ?>
|
||||
</div>
|
||||
|
||||
<!-- Policy Type Selection Modal — commented out; Add button now goes directly to new claim page -->
|
||||
<!-- <div class="modal fade" id="policyTypeModal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Select Policy Type</h5>
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<select class="form-control" id="new_claim_policy_type">
|
||||
<option value="">Select Policy Type</option>
|
||||
<?php if(isset($policy_types)): foreach($policy_types as $pt): ?>
|
||||
<option value="<?= $pt['id'] ?>"><?= $pt['policy_type'] ?></option>
|
||||
<?php endforeach; endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="goToNewClaim()">Proceed</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<script>
|
||||
var pageSubTitle = 'Non-EB Claims';
|
||||
|
||||
// function goToNewClaim() {
|
||||
// var pt = $('#new_claim_policy_type').val();
|
||||
// if (!pt) { toastr.warning('Please select a Policy Type'); return; }
|
||||
// window.location.href = '<?= base_url("non-eb-claim/new/") ?>' + pt;
|
||||
// }
|
||||
function openFilterNav() { document.getElementById("claim-filter-sidebar").style.width = "350px"; }
|
||||
function closeFilterNav() { document.getElementById("claim-filter-sidebar").style.width = "0"; }
|
||||
function toggleDateField() {
|
||||
var dt = $('#filter_date_type').val();
|
||||
if (dt && dt != '0') { $('#date_div').show(); } else { $('#date_div').hide(); }
|
||||
}
|
||||
function clearFilters() {
|
||||
$('#filter_policy_type').val('0');
|
||||
$('#filter_insurer').val('0').trigger('change');
|
||||
$('#filter_claim_no').val('');
|
||||
$('#filter_nhance_ref').val('');
|
||||
$('#filter_client').val('0').trigger('change');
|
||||
$('#filter_claim_status').val('0').trigger('change');
|
||||
$('#filter_date_type').val('0');
|
||||
$('#date_div').hide();
|
||||
localStorage.removeItem('nonEbFilterData');
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('#filter_client').select2();
|
||||
$('#filter_claim_status').select2();
|
||||
$('#filter_insurer').select2();
|
||||
});
|
||||
|
||||
var allClaimStatuses = <?= isset($claim_status) ? json_encode($claim_status) : '[]' ?>;
|
||||
|
||||
$('#filter_policy_type').on('change', function() {
|
||||
var ptype = $(this).val();
|
||||
$('#filter_claim_status').empty().append('<option value="">Select Status</option>');
|
||||
for (var i = 0; i < allClaimStatuses.length; i++) {
|
||||
if (allClaimStatuses[i]['ticket_type'] == ptype) {
|
||||
$('#filter_claim_status').append($('<option>', {
|
||||
value: allClaimStatuses[i]['id'],
|
||||
text: allClaimStatuses[i]['claim_status']
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function fetchClaimListData() {
|
||||
closeFilterNav();
|
||||
|
||||
var requestData = {
|
||||
policy_type_id: $('#filter_policy_type').val(),
|
||||
claim_number: $('#filter_claim_no').val(),
|
||||
nhance_claim_ref_no: $('#filter_nhance_ref').val(),
|
||||
claim_status_id: $('#filter_claim_status').val(),
|
||||
client_id: $('#filter_client').val(),
|
||||
insurer_id: $('#filter_insurer').val(),
|
||||
start_date: $('#startDate').val(),
|
||||
end_date: $('#endDate').val(),
|
||||
date_type: $('#filter_date_type').val(),
|
||||
};
|
||||
|
||||
function isEmpty(v) { return v === null || v === undefined || v.toString().trim() === '' || v == 0; }
|
||||
|
||||
if (isEmpty(requestData.policy_type_id) && isEmpty(requestData.claim_number) && isEmpty(requestData.nhance_claim_ref_no) && isEmpty(requestData.claim_status_id) && isEmpty(requestData.client_id) && isEmpty(requestData.insurer_id) && isEmpty(requestData.date_type)) {
|
||||
toastr.warning('Please enter at least one search criteria.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requestData.date_type && requestData.date_type != '0') {
|
||||
if (!requestData.start_date || !requestData.end_date) { toastr.warning('Please select both dates.'); return false; }
|
||||
}
|
||||
|
||||
localStorage.setItem('nonEbFilterData', JSON.stringify(requestData));
|
||||
|
||||
var url = '<?= base_url('/non-eb-claim/list') ?>';
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
|
||||
sendAjaxRequestForGlobal(url, 'POST', requestData, function(response) {
|
||||
if (response.status == true) {
|
||||
$('#claim_list_div').empty().html(response.html);
|
||||
} else {
|
||||
toastr.error(response.message || 'Error', 'Error');
|
||||
}
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
}, function(xhr, status, error) {
|
||||
console.error('Error:', error);
|
||||
toastr.error('An error occurred.', 'Error');
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let filterData = localStorage.getItem('nonEbFilterData');
|
||||
filterData = filterData ? JSON.parse(filterData) : null;
|
||||
if (filterData) {
|
||||
var url = '<?= base_url('/non-eb-claim/list') ?>';
|
||||
$('.loader').fadeIn(); $('.loader-mask').fadeIn();
|
||||
sendAjaxRequestForGlobal(url, 'POST', filterData, function(response) {
|
||||
if (response.status == true) { $('#claim_list_div').empty().html(response.html); }
|
||||
$('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow');
|
||||
}, function() { $('.loader').fadeOut(); $('.loader-mask').delay(350).fadeOut('slow'); });
|
||||
|
||||
$('#filter_policy_type').val(filterData.policy_type_id);
|
||||
$('#filter_claim_no').val(filterData.claim_number);
|
||||
$('#filter_nhance_ref').val(filterData.nhance_claim_ref_no);
|
||||
$('#filter_client').val(filterData.client_id);
|
||||
$('#filter_insurer').val(filterData.insurer_id);
|
||||
$('#filter_date_type').val(filterData.date_type);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
341
db.md
Normal file
341
db.md
Normal file
@ -0,0 +1,341 @@
|
||||
# Non-EB Claims Module - Database Changes
|
||||
|
||||
## 1. New Table: `non_eb_ticket_master`
|
||||
|
||||
```sql
|
||||
CREATE TABLE `non_eb_ticket_master` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
-- Section 1: Policy & Account
|
||||
`acm_id` INT UNSIGNED DEFAULT NULL,
|
||||
`client_id` INT UNSIGNED DEFAULT NULL,
|
||||
`branch_id` INT UNSIGNED DEFAULT NULL,
|
||||
`client_policy_id` INT UNSIGNED DEFAULT NULL,
|
||||
`policy_type_id` INT UNSIGNED DEFAULT NULL,
|
||||
`policy_no` VARCHAR(100) DEFAULT NULL,
|
||||
`policy_section` VARCHAR(255) DEFAULT NULL,
|
||||
`policy_period` VARCHAR(50) DEFAULT NULL,
|
||||
`insurer_id` INT UNSIGNED DEFAULT NULL,
|
||||
-- Section 2: Loss / Incident
|
||||
`nature_of_loss` VARCHAR(255) DEFAULT NULL,
|
||||
`loss_description` TEXT DEFAULT NULL,
|
||||
`loss_location` VARCHAR(255) DEFAULT NULL,
|
||||
`loss_date` DATE DEFAULT NULL,
|
||||
`loss_estimate` DECIMAL(15,2) DEFAULT NULL,
|
||||
-- Section 3: Intimation & Ref
|
||||
`intimation_recd_date` DATE DEFAULT NULL,
|
||||
`intimated_to_insurer_date` DATE DEFAULT NULL,
|
||||
`nhance_claim_ref_no` VARCHAR(100) DEFAULT NULL,
|
||||
`claim_number` VARCHAR(100) DEFAULT NULL,
|
||||
-- Section 4: Status
|
||||
`claim_status_id` INT UNSIGNED DEFAULT NULL,
|
||||
`surveyor_file_ref_no` VARCHAR(100) DEFAULT NULL,
|
||||
-- Section 5: Surveyor
|
||||
`surveyor_name` VARCHAR(255) DEFAULT NULL,
|
||||
`surveyor_contact_person` VARCHAR(255) DEFAULT NULL,
|
||||
`surveyor_contact_number` VARCHAR(20) DEFAULT NULL,
|
||||
`surveyor_email` VARCHAR(255) DEFAULT NULL,
|
||||
`lor` TEXT DEFAULT NULL,
|
||||
`surveyor_remarks` TEXT DEFAULT NULL,
|
||||
-- Section 6: Insured Contact
|
||||
`insured_contact_name` VARCHAR(255) DEFAULT NULL,
|
||||
`insured_contact_number` VARCHAR(20) DEFAULT NULL,
|
||||
`insured_contact_email` VARCHAR(255) DEFAULT NULL,
|
||||
-- Section 7: Documents
|
||||
`google_drive_links` TEXT DEFAULT NULL,
|
||||
`documents_required` TEXT DEFAULT NULL,
|
||||
`documents_submitted` TEXT DEFAULT NULL,
|
||||
`pending_documents` TEXT DEFAULT NULL,
|
||||
`eta_for_documents` DATE DEFAULT NULL,
|
||||
-- Section 8: Settlement
|
||||
`loss_assessed_value` DECIMAL(15,2) DEFAULT NULL,
|
||||
`settled_amount` DECIMAL(15,2) DEFAULT NULL,
|
||||
`settlement_utr` VARCHAR(100) DEFAULT NULL,
|
||||
-- Asset File (uploaded alternative to manual asset rows)
|
||||
`asset_file` VARCHAR(255) DEFAULT NULL,
|
||||
-- System
|
||||
`priority` TINYINT UNSIGNED DEFAULT NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_by` INT UNSIGNED DEFAULT NULL,
|
||||
`updated_by` INT UNSIGNED DEFAULT NULL,
|
||||
`last_updated_by` INT UNSIGNED DEFAULT NULL,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_claim_status` (`claim_status_id`),
|
||||
KEY `idx_client` (`client_id`),
|
||||
KEY `idx_branch` (`branch_id`),
|
||||
KEY `idx_acm` (`acm_id`),
|
||||
KEY `idx_insurer` (`insurer_id`),
|
||||
KEY `idx_policy_type` (`policy_type_id`),
|
||||
KEY `idx_is_active` (`is_active`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## 2. New Table: `non_eb_claim_assets`
|
||||
|
||||
```sql
|
||||
CREATE TABLE `non_eb_claim_assets` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`non_eb_ticket_id` INT UNSIGNED NOT NULL,
|
||||
`asset_id_code` VARCHAR(100) DEFAULT NULL,
|
||||
`serial_no` VARCHAR(100) DEFAULT NULL,
|
||||
`vehicle_no` VARCHAR(50) DEFAULT NULL,
|
||||
`asset_description` VARCHAR(255) DEFAULT NULL,
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`created_by` INT UNSIGNED DEFAULT NULL,
|
||||
`updated_by` INT UNSIGNED DEFAULT NULL,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_ticket` (`non_eb_ticket_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
## 3. Reused Tables (No Changes Needed)
|
||||
|
||||
- `ticket_claim_status` - Add Non-EB sub-statuses with `ticket_type` = `policy_type.id`
|
||||
- `ticket_history` - Reuse for field change tracking
|
||||
- `ticket_notes` - Reuse for notes
|
||||
- `ticket_messages` - Reuse for conversation/replies
|
||||
- `ticket_mail_template` - Reuse for email templates
|
||||
- `ticket_check_list` - Reuse for document checklists
|
||||
|
||||
## 4. Status Seed Data (per policy type)
|
||||
|
||||
For each Non-EB policy type (from `policy_type` table where `allocg IN ('Non-EB', 'Marine')`), insert these sub-statuses into `ticket_claim_status`:
|
||||
|
||||
| Sub Status (claim_status) | Display Name (Status for user) | allowed_status |
|
||||
|---|---|---|
|
||||
| Claim Intimation - Insured | Claim Intimation Received | [next_id] |
|
||||
| Claim Intimation - Insurer | Claim Intimated | [next_id] |
|
||||
| Survey Appointment - Awaited | Insurer Pending | [next_id] |
|
||||
| Surveyor Appointed | Surveyor Pending | [next_id, lor_awaited_id] |
|
||||
| LOR Awaited - Surveyor | Surveyor Pending | [next_id] |
|
||||
| Documents Awaited - Insured | Insured Pending | [next_id, partial_docs_id] |
|
||||
| Partial Documents Awaited - Insured | Insured Pending | [next_id] |
|
||||
| Documents Submitted - Awaiting Loss Assessment | Insurer / Surveyor Pending | [next_id, discrepancy_id] |
|
||||
| Loss Assessment - Discrepancy | Insurer / Surveyor Pending | [next_id] |
|
||||
| Loss Assessment - Consent Awaited - Insured | Insured Pending | [next_id] |
|
||||
| Consent Agreed | Insurer / Surveyor Pending | [approved_id] |
|
||||
| Claim Approved | Claim Approved | [settled_id] |
|
||||
| Claim Settled | Claim Settled | [] |
|
||||
| Claim Closed | Claim Closed | [] |
|
||||
| Claim Rejected | Claim Rejected | [] |
|
||||
| Claim Withdrawn | Claim Withdrawn | [] |
|
||||
| Workshop Pending | Workshop Pending | [next_id] |
|
||||
|
||||
> Note: `allowed_status` JSON should contain the IDs of the next valid statuses after insert.
|
||||
|
||||
---
|
||||
|
||||
## 5. ALTER TABLE Queries (for existing databases)
|
||||
|
||||
Run these if the tables already exist and need the new columns:
|
||||
|
||||
```sql
|
||||
-- Add branch_id column
|
||||
ALTER TABLE `non_eb_ticket_master` ADD COLUMN `branch_id` INT UNSIGNED DEFAULT NULL AFTER `client_id`;
|
||||
ALTER TABLE `non_eb_ticket_master` ADD KEY `idx_branch` (`branch_id`);
|
||||
|
||||
-- Add asset_file column
|
||||
ALTER TABLE `non_eb_ticket_master` ADD COLUMN `asset_file` VARCHAR(255) DEFAULT NULL AFTER `settlement_utr`;
|
||||
|
||||
-- Add closure_remark column
|
||||
ALTER TABLE `non_eb_ticket_master` ADD COLUMN `closure_remark` TEXT DEFAULT NULL AFTER `asset_file`;
|
||||
|
||||
-- Add policy date columns
|
||||
ALTER TABLE `non_eb_ticket_master` ADD COLUMN `policy_start_date` VARCHAR(20) DEFAULT NULL AFTER `policy_period`;
|
||||
ALTER TABLE `non_eb_ticket_master` ADD COLUMN `policy_end_date` VARCHAR(20) DEFAULT NULL AFTER `policy_start_date`;
|
||||
|
||||
-- Optional: Drop legacy columns (only after confirming no existing data needed)
|
||||
-- ALTER TABLE `non_eb_ticket_master` DROP COLUMN `policy_period`;
|
||||
-- ALTER TABLE `non_eb_ticket_master` DROP COLUMN `policy_section`;
|
||||
|
||||
-- Add required_docs column for IR Documents checklist (Claim Files tab)
|
||||
ALTER TABLE `non_eb_ticket_master` ADD COLUMN `required_docs` TEXT DEFAULT NULL AFTER `closure_remark`;
|
||||
|
||||
-- Add ticket_type flag to claim_files to distinguish EB (1) vs Non-EB (2) records
|
||||
ALTER TABLE `claim_files` ADD COLUMN `ticket_type` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '1=EB, 2=Non-EB' AFTER `ticket_id`;
|
||||
ALTER TABLE `claim_files` ADD KEY `idx_ticket_type` (`ticket_type`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Trigger: `non_eb_ticket_master_after_update`
|
||||
|
||||
Tracks field-level changes into `ticket_history` on every UPDATE.
|
||||
|
||||
```sql
|
||||
DELIMITER $$
|
||||
|
||||
CREATE TRIGGER `non_eb_ticket_master_after_update`
|
||||
AFTER UPDATE ON `non_eb_ticket_master`
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
|
||||
-- Claim Status
|
||||
IF (OLD.claim_status_id IS NULL OR OLD.claim_status_id != NEW.claim_status_id) AND NEW.claim_status_id IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'claim_status_id', 'Claim Status', OLD.claim_status_id, NEW.claim_status_id, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Priority
|
||||
IF (OLD.priority IS NULL OR OLD.priority != NEW.priority) AND NEW.priority IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'priority', 'Priority', OLD.priority, NEW.priority, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Account Manager
|
||||
IF (OLD.acm_id IS NULL OR OLD.acm_id != NEW.acm_id) AND NEW.acm_id IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'acm_id', 'Account Manager', OLD.acm_id, NEW.acm_id, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Nature of Loss
|
||||
IF (OLD.nature_of_loss IS NULL OR OLD.nature_of_loss != NEW.nature_of_loss) AND NEW.nature_of_loss IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'nature_of_loss', 'Nature of Loss', OLD.nature_of_loss, NEW.nature_of_loss, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Loss Location
|
||||
IF (OLD.loss_location IS NULL OR OLD.loss_location != NEW.loss_location) AND NEW.loss_location IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'loss_location', 'Loss Location', OLD.loss_location, NEW.loss_location, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Loss Date
|
||||
IF (OLD.loss_date IS NULL OR OLD.loss_date != NEW.loss_date) AND NEW.loss_date IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'loss_date', 'Loss Date', OLD.loss_date, NEW.loss_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Loss Estimate
|
||||
IF (OLD.loss_estimate IS NULL OR OLD.loss_estimate != NEW.loss_estimate) AND NEW.loss_estimate IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'loss_estimate', 'Loss Estimate', OLD.loss_estimate, NEW.loss_estimate, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Loss Description (truncated to 500 chars)
|
||||
IF (OLD.loss_description IS NULL OR OLD.loss_description != NEW.loss_description) AND NEW.loss_description IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'loss_description', 'Loss Description', LEFT(OLD.loss_description, 500), LEFT(NEW.loss_description, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Intimation Recd Date
|
||||
IF (OLD.intimation_recd_date IS NULL OR OLD.intimation_recd_date != NEW.intimation_recd_date) AND NEW.intimation_recd_date IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'intimation_recd_date', 'Intimation Recd Date', OLD.intimation_recd_date, NEW.intimation_recd_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Intimated to Insurer Date
|
||||
IF (OLD.intimated_to_insurer_date IS NULL OR OLD.intimated_to_insurer_date != NEW.intimated_to_insurer_date) AND NEW.intimated_to_insurer_date IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'intimated_to_insurer_date', 'Intimated to Insurer Date', OLD.intimated_to_insurer_date, NEW.intimated_to_insurer_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Nhance Claim Ref No
|
||||
IF (OLD.nhance_claim_ref_no IS NULL OR OLD.nhance_claim_ref_no != NEW.nhance_claim_ref_no) AND NEW.nhance_claim_ref_no IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'nhance_claim_ref_no', 'Nhance Claim Ref No', OLD.nhance_claim_ref_no, NEW.nhance_claim_ref_no, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Claim Number
|
||||
IF (OLD.claim_number IS NULL OR OLD.claim_number != NEW.claim_number) AND NEW.claim_number IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'claim_number', 'Claim Number', OLD.claim_number, NEW.claim_number, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Surveyor File Ref No
|
||||
IF (OLD.surveyor_file_ref_no IS NULL OR OLD.surveyor_file_ref_no != NEW.surveyor_file_ref_no) AND NEW.surveyor_file_ref_no IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'surveyor_file_ref_no', 'Surveyor File Ref No', OLD.surveyor_file_ref_no, NEW.surveyor_file_ref_no, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Surveyor Name
|
||||
IF (OLD.surveyor_name IS NULL OR OLD.surveyor_name != NEW.surveyor_name) AND NEW.surveyor_name IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'surveyor_name', 'Surveyor Name', OLD.surveyor_name, NEW.surveyor_name, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Surveyor Contact Person
|
||||
IF (OLD.surveyor_contact_person IS NULL OR OLD.surveyor_contact_person != NEW.surveyor_contact_person) AND NEW.surveyor_contact_person IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'surveyor_contact_person', 'Surveyor Contact Person', OLD.surveyor_contact_person, NEW.surveyor_contact_person, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Surveyor Contact Number
|
||||
IF (OLD.surveyor_contact_number IS NULL OR OLD.surveyor_contact_number != NEW.surveyor_contact_number) AND NEW.surveyor_contact_number IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'surveyor_contact_number', 'Surveyor Contact Number', OLD.surveyor_contact_number, NEW.surveyor_contact_number, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Surveyor Email
|
||||
IF (OLD.surveyor_email IS NULL OR OLD.surveyor_email != NEW.surveyor_email) AND NEW.surveyor_email IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'surveyor_email', 'Surveyor Email', OLD.surveyor_email, NEW.surveyor_email, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- LOR (truncated to 500 chars)
|
||||
IF (OLD.lor IS NULL OR OLD.lor != NEW.lor) AND NEW.lor IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'lor', 'LOR', LEFT(OLD.lor, 500), LEFT(NEW.lor, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Surveyor Remarks (truncated to 500 chars)
|
||||
IF (OLD.surveyor_remarks IS NULL OR OLD.surveyor_remarks != NEW.surveyor_remarks) AND NEW.surveyor_remarks IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'surveyor_remarks', 'Surveyor Remarks', LEFT(OLD.surveyor_remarks, 500), LEFT(NEW.surveyor_remarks, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Loss Assessed Value
|
||||
IF (OLD.loss_assessed_value IS NULL OR OLD.loss_assessed_value != NEW.loss_assessed_value) AND NEW.loss_assessed_value IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'loss_assessed_value', 'Loss Assessed Value', OLD.loss_assessed_value, NEW.loss_assessed_value, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Settled Amount
|
||||
IF (OLD.settled_amount IS NULL OR OLD.settled_amount != NEW.settled_amount) AND NEW.settled_amount IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'settled_amount', 'Settled Amount', OLD.settled_amount, NEW.settled_amount, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Settlement UTR
|
||||
IF (OLD.settlement_utr IS NULL OR OLD.settlement_utr != NEW.settlement_utr) AND NEW.settlement_utr IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'settlement_utr', 'Settlement UTR', OLD.settlement_utr, NEW.settlement_utr, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Closure Remark (truncated to 500 chars)
|
||||
IF (OLD.closure_remark IS NULL OR OLD.closure_remark != NEW.closure_remark) AND NEW.closure_remark IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'closure_remark', 'Closure Remark', LEFT(OLD.closure_remark, 500), LEFT(NEW.closure_remark, 500), NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Policy No
|
||||
IF (OLD.policy_no IS NULL OR OLD.policy_no != NEW.policy_no) AND NEW.policy_no IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'policy_no', 'Policy No', OLD.policy_no, NEW.policy_no, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Policy Start Date
|
||||
IF (OLD.policy_start_date IS NULL OR OLD.policy_start_date != NEW.policy_start_date) AND NEW.policy_start_date IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'policy_start_date', 'Policy Start Date', OLD.policy_start_date, NEW.policy_start_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- Policy Expiry Date
|
||||
IF (OLD.policy_end_date IS NULL OR OLD.policy_end_date != NEW.policy_end_date) AND NEW.policy_end_date IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'policy_end_date', 'Policy Expiry Date', OLD.policy_end_date, NEW.policy_end_date, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
-- ETA for Documents
|
||||
IF (OLD.eta_for_documents IS NULL OR OLD.eta_for_documents != NEW.eta_for_documents) AND NEW.eta_for_documents IS NOT NULL THEN
|
||||
INSERT INTO ticket_history (ticket_id, field_name, display_name, old_value, new_value, created_at, created_by, updated_by)
|
||||
VALUES (OLD.id, 'eta_for_documents', 'ETA for Documents', OLD.eta_for_documents, NEW.eta_for_documents, NEW.updated_at, NEW.updated_by, IF(NEW.last_updated_by = 'API', 0, NULL));
|
||||
END IF;
|
||||
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
```
|
||||
257
noneb.md
Normal file
257
noneb.md
Normal file
@ -0,0 +1,257 @@
|
||||
# Non-EB Claims Module — Development Summary
|
||||
|
||||
## Overview
|
||||
Non-EB (Non-Employee Benefits) Claims module for nhance. Handles commercial/property/asset insurance claims (All Risk, Marine, Aviation, Cyber, etc. — 120+ policy types). Mirrors the existing EB Claims (TicketController) architecture.
|
||||
|
||||
Built across 4 sessions: 2026-03-24 (prerequisites), 2026-03-26 (form UI + edit sync sessions 2–3), 2026-03-27 (edit read-only + Claim Files tab, session 4).
|
||||
|
||||
---
|
||||
|
||||
## Files Created
|
||||
|
||||
### Controller
|
||||
- **`app/Controllers/NonEbClaimController.php`**
|
||||
- Extends BaseController, uses ResponseTrait
|
||||
- Methods: `claimList`, `claimForm`, `createClaim`, `view_claim`, `updateClaim`, `removeClaim`, `mailTemplate`, `crudTemplate`, `crudNote`, `saveReply`, `claimReports`, `getBranchAndPolicyByClientID`, `getVisibleSectionsAjax`, `getMoreInfo`, `uploadFile`, `getClaimFiles`, `removeFile`, `saveIRDocs`
|
||||
- `getBranchAndPolicyByClientID()` returns `branch_data[]` + `policy_data` keyed by `branch_id`, filtered by `pt.allocg IN ('Non-EB','Marine')`
|
||||
|
||||
### Models
|
||||
- **`app/Models/NonEbClaimAssetModel.php`** — Asset details per claim
|
||||
- **`app/Models/NonEbTicketMasterModel.php`** — Main ticket/claim master
|
||||
- `getTicketDataByTicketID()` JOINs `client_branch cb` to expose `cb.branch_name` for read-only display in edit view
|
||||
|
||||
### Views
|
||||
- **`app/Views/non_eb_claim_search.php`** — Search page with filter sidebar (Policy Type, Insurer, Claim No, Nhance Ref, Client, Status, Date Range). Contains the Policy Type selection modal for "Add New Claim" (survives AJAX list reload). Uses `localStorage` to persist filters.
|
||||
- **`app/Views/non_eb_claim_list.php`** — DataTable list with CSV/Excel export, action buttons. Modal HTML removed from here (lives in search view).
|
||||
- **`app/Views/non_eb_claim_form.php`** — New claim form with 9 accordion sections. 3-level cascade: Client → Branch → Policy. Status-based section visibility via AJAX. Section 8 (Documents & Attachments) commented out (code preserved).
|
||||
- **`app/Views/non_eb_claim_edit.php`** — Edit/view claim. Sections 1 & 2 are fully read-only. 5 tabs: General, Claim Files, Reply, Notes, History. Section 8 commented out.
|
||||
- **`app/Views/non_eb_claim_mail_template.php`** — Mail template CRUD with Jodit editor, placeholder insertion, auto-mail toggle.
|
||||
- **`app/Views/non_eb_claim_reports.php`** — Reports with filters (Policy Type, ACM, Date Range), AJAX DataTable with export.
|
||||
|
||||
### Config Changes
|
||||
- **`app/Config/Routes.php`** — Added `/non-eb-claim` route group (18 routes) under `authMVC` filter.
|
||||
- **`app/Config/Acl.php`** — Added ACL entry `#^/non-eb-claim#` for HEAD, ADMIN, MANAGER, ACCOUNT_MANAGER roles + CLAIMS team.
|
||||
- **`app/Config/Constants.php`** — Added `UPLOAD_EXT_ASSET_FILES` constant.
|
||||
|
||||
---
|
||||
|
||||
## Routes Reference
|
||||
|
||||
```
|
||||
/non-eb-claim/list GET|POST claimList
|
||||
/non-eb-claim/remove GET removeClaim
|
||||
/non-eb-claim/new/(:any) GET claimForm/$1
|
||||
/non-eb-claim/create POST createClaim
|
||||
/non-eb-claim/update POST updateClaim
|
||||
/non-eb-claim/view/(:any) GET view_claim/$1
|
||||
/non-eb-claim/mail_template GET mailTemplate
|
||||
/non-eb-claim/crud_mail_template/(:any) POST crudTemplate/$1
|
||||
/non-eb-claim/note/(:any) POST crudNote/$1
|
||||
/non-eb-claim/reply POST saveReply
|
||||
/non-eb-claim/reports GET|POST claimReports
|
||||
/non-eb-claim/getBranchAndPolicy POST getBranchAndPolicyByClientID
|
||||
/non-eb-claim/getVisibleSections POST getVisibleSectionsAjax
|
||||
/non-eb-claim/getMoreInfo POST getMoreInfo
|
||||
/non-eb-claim/uploadFile POST uploadFile
|
||||
/non-eb-claim/getClaimFiles POST getClaimFiles
|
||||
/non-eb-claim/removeFile GET removeFile
|
||||
/non-eb-claim/saveIRDocs POST saveIRDocs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section Order (Form & Edit Views)
|
||||
|
||||
| # | Section | Notes |
|
||||
|---|---|---|
|
||||
| 1 | Policy & Account Details | Read-only in edit view (Client, Branch, Policy, Insurer, Policy No, Policy Start/End Date, ACM) |
|
||||
| 2 | Insured Contact Details | Read-only in edit view |
|
||||
| 3 | Loss / Incident Details | Editable |
|
||||
| 4 | Intimation & Claim Reference | Editable |
|
||||
| 5 | Status & Tracking | Editable |
|
||||
| 6 | Asset Details | Dynamic rows + file upload option |
|
||||
| 7 | Surveyor Details | Conditional (status-based) |
|
||||
| 8 | Documents & Attachments | Commented out in both form and edit (code preserved) |
|
||||
| 9 | Settlement Details | Conditional (status-based) |
|
||||
|
||||
**Edit view additionally has:** Claim Files tab (IR Documents checklist + file upload + file list)
|
||||
|
||||
---
|
||||
|
||||
## DB Schema — Key Columns
|
||||
|
||||
### `non_eb_ticket_master`
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `branch_id` | INT | Added session 2 |
|
||||
| `asset_file` | VARCHAR | Path to uploaded asset file |
|
||||
| `policy_start_date` | VARCHAR(20) | Stored as YYYY-MM-DD, displayed as DD-MM-YYYY |
|
||||
| `policy_end_date` | VARCHAR(20) | Same format |
|
||||
| `required_docs` | TEXT | JSON array for IR Documents checklist |
|
||||
| `policy_section` | — | Kept in DB but removed from all UI/model code |
|
||||
| `policy_period` | — | Kept in DB but removed from all UI/model code |
|
||||
|
||||
### `claim_files`
|
||||
| Column | Type | Notes |
|
||||
|---|---|---|
|
||||
| `ticket_type` | TINYINT(1) DEFAULT 1 | 1=EB, 2=Non-EB. Added to share table without breaking EB records |
|
||||
|
||||
### DB Trigger
|
||||
`non_eb_ticket_master_after_update` — tracks 22 fields into `ticket_history` on every UPDATE. TEXT fields (loss_description, lor, surveyor_remarks, closure_remark) truncated to 500 chars. Skipped: set-once/bulk fields (client_id, branch_id, insured_contact_*, google_drive_links, documents_*, asset_file, required_docs).
|
||||
|
||||
Full DDL in `db.md`.
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns & Decisions
|
||||
|
||||
### UI Style — Bootstrap Card/Collapse Accordion
|
||||
All sections use Bootstrap card/collapse pattern (matching `ticket_form_gmc.php`):
|
||||
```html
|
||||
<div id="accordionX" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">Title <a data-toggle="collapse" href="#collapseX"><i class="mdi mdi-chevron-down"></i></a></h4>
|
||||
<div id="collapseX" class="collapse show" data-parent="#accordionX">
|
||||
<div class="card-body"> ... </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
Custom `toggleAccordion()` and `.accordion`/`.accordion-content` CSS fully removed.
|
||||
|
||||
### CSS Classes
|
||||
```css
|
||||
.readonly-color { background-color: #e0e0e0; color: #666; }
|
||||
.readonly-select { pointer-events: none; background-color: #f0f0f0; color: #666; }
|
||||
.label-font-size { font-size: 0.875rem; }
|
||||
.is-invalid { border-color: #dc3545 !important; }
|
||||
.invalid-feedback { display: none; color: #dc3545; font-size: 0.8rem; }
|
||||
```
|
||||
|
||||
### 3-Level Dropdown Cascade (Create Form Only)
|
||||
- Client change → AJAX `non-eb-claim/getBranchAndPolicy` → returns `branch_data[]` + `policy_data{branch_id: [...]}`
|
||||
- Branch change → populates policies from stored `noneb_policy_list[branch_id]`, auto-fills insured contact
|
||||
- Policy change → auto-fills insurer, policy_no, policy start/end dates (YYYY-MM-DD → dd-mm-yyyy)
|
||||
- **Select2 fix**: All append/reset functions do `select2('destroy')` before DOM changes, then `select2()` re-init after. Guard: `.hasClass('select2-hidden-accessible')`
|
||||
|
||||
### Edit View — Read-Only Section 1 & 2 (Session 4)
|
||||
Sections 1 and 2 are non-editable on the edit page. Strategy:
|
||||
- All dropdowns replaced with readonly text inputs showing stored values
|
||||
- Hidden inputs for `branch_id` and `acm_id` carry values on form submit
|
||||
- No AJAX cascade JS — all client/branch/policy change handlers removed from edit page
|
||||
- Policy display: `policy_type_name + policy_no` combined in one text input
|
||||
- PHP date formatting at top of edit view: detects YYYY-MM-DD, converts to DD-MM-YYYY for display
|
||||
- `validateNonEbForm()` in edit page: removed client/branch/ACM/contact validation (those sections are read-only)
|
||||
|
||||
### Form Submission — FormData
|
||||
Both create and edit forms use `new FormData()` + `$.ajax({ processData: false, contentType: false })` to support file uploads. `X-Requested-With: XMLHttpRequest` header added for CI4 `isAJAX()` detection.
|
||||
|
||||
### Status-Based Section Visibility
|
||||
`$statusSectionVisibility` mapping in controller. AJAX endpoint `getVisibleSections` returns which sections show/hide. Surveyor and Settlement sections are conditionally visible.
|
||||
|
||||
### Modal Fix (List Page)
|
||||
Modal HTML in `non_eb_claim_search.php` (parent), not inside the AJAX-replaced `#claim_list_div`. Reason: `$('#claim_list_div').empty().html(response.html)` destroys inner modals on filter. Uses `new bootstrap.Modal()` for AJAX-safe init.
|
||||
|
||||
### URL Construction
|
||||
- `base_url` JS var = `'http://localhost/nhance/'` (trailing slash from App.php)
|
||||
- All AJAX URLs: `base_url + 'non-eb-claim/...'` (no leading slash — avoids double-slash)
|
||||
|
||||
---
|
||||
|
||||
## Claim Files Tab (Edit Page — Session 4)
|
||||
|
||||
Reuses `claim_files` table with `ticket_type=2` flag. Four controller methods handle Non-EB file operations:
|
||||
|
||||
| Method | Route | Purpose |
|
||||
|---|---|---|
|
||||
| `uploadFile()` | POST `/uploadFile` | URL or physical file upload, inserts with `ticket_type=2` |
|
||||
| `getClaimFiles()` | POST `/getClaimFiles` | Fetch files for ticket filtered by `ticket_type=2` |
|
||||
| `removeFile()` | GET `/removeFile` | Soft delete (`is_active=0`) by file id |
|
||||
| `saveIRDocs()` | POST `/saveIRDocs` | Save IR Documents JSON to `non_eb_ticket_master.required_docs` |
|
||||
|
||||
**Tab content:** IR Documents checklist (JSON stored in `required_docs`, with freeze toggle + save button) + file upload form (URL/file toggle) + file list table + edit URL modal.
|
||||
|
||||
**JS functions (all prefixed `noneb` to avoid collision with EB claims JS):**
|
||||
`nonebLoadClaimFiles`, `nonebCreateFileList`, `nonebAddHTMLInput`, `nonebAddFileUploadHtml`, `nonebRemoveHTMLInput`, `nonebToggleUploadType`, `nonebLoadConfiguration`, `nonebRenderDocumentList`, `nonebCreateDocumentRow`, `nonebAddDocument`, `nonebRemoveDocument`, `nonebUpdateDocName`, `nonebUpdateDocReceived`, `nonebToggleActionFreeze`, `nonebSaveIRDocs`
|
||||
|
||||
File downloads reuse the global `downloadClaimFile/(:any)` route (no `ticket_type` filtering needed for download).
|
||||
|
||||
**`saveIRDocs` is a separate endpoint** (`non-eb-claim/saveIRDocs`) from the EB equivalent (`ticket/saveIRDocsJson`) — EB updates `ticket_master`, Non-EB updates `non_eb_ticket_master`.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Validation (`validateNonEbForm()`)
|
||||
|
||||
Full JS validation runs before AJAX submit on both create and edit forms.
|
||||
|
||||
| Field | Rules |
|
||||
|---|---|
|
||||
| `client_select` | Required (create only) |
|
||||
| `branch_id` | Required (create only) |
|
||||
| `acm_id` | Required (create only) |
|
||||
| `claim_status_id` | Required |
|
||||
| `insured_contact_name` | Required, min 3 chars, `^[a-zA-Z0-9\s_-]+$` (create only) |
|
||||
| `insured_contact_number` | Required, numeric, 10–15 digits (create only) |
|
||||
| `insured_contact_email` | Optional, email format (create only) |
|
||||
| `nature_of_loss` | Required, min 3 chars |
|
||||
| `loss_location` | Required |
|
||||
| `loss_date` | Required |
|
||||
| `loss_estimate` | Optional, numeric |
|
||||
| `claim_number` | Optional, `^[a-zA-Z0-9\/_-]+$` |
|
||||
| `nhance_claim_ref_no` | Optional, max 100 chars |
|
||||
| `surveyor_contact_number` | Optional if section visible, numeric 10–15 digits |
|
||||
| `surveyor_email` | Optional if section visible, email format |
|
||||
| `loss_assessed_value` | Optional if section visible, numeric |
|
||||
| `settled_amount` | Optional if section visible, numeric |
|
||||
| `settlement_utr` | Optional if section visible, max 100 chars |
|
||||
| Asset section | At least one asset row with data OR valid file upload |
|
||||
|
||||
On failure: toastr error, expands collapsed section containing first invalid field, scrolls + focuses it.
|
||||
|
||||
---
|
||||
|
||||
## Post-Update Redirect (Edit Page)
|
||||
After successful update AJAX, 800ms delay then redirect to list:
|
||||
```js
|
||||
setTimeout(function() { window.location.href = base_url + 'non-eb-claim/list'; }, 800);
|
||||
```
|
||||
Toast remains visible during the delay.
|
||||
|
||||
---
|
||||
|
||||
## Bugs Fixed
|
||||
|
||||
| Bug | Fix |
|
||||
|---|---|
|
||||
| Modal not showing on Add click | Moved modal outside AJAX-replaced div, used `new bootstrap.Modal()` |
|
||||
| Policies not loading on client select | Fixed double-slash in AJAX URLs (`base_url` already has trailing `/`) |
|
||||
| Select2 dropdowns not updating | Added `select2('destroy')` before DOM manipulation, `select2()` re-init after |
|
||||
| Policy dates showing as `2-Dec-2026 12:00 am` | Replaced `formatDate()` with inline string split; checks `sp[0].length === 4` to detect YYYY |
|
||||
| Branch name missing in edit view `$td` | Added `JOIN client_branch cb` + `cb.branch_name` select to `getTicketDataByTicketID()` |
|
||||
| Edit Section 1 dropdowns not pre-selecting | Replaced dropdowns with readonly text inputs — simpler and reliable |
|
||||
|
||||
---
|
||||
|
||||
## Removed Fields (policy_section & policy_period)
|
||||
|
||||
Removed from all UI and code but kept in DB (for historical data).
|
||||
|
||||
| Location | Change |
|
||||
|---|---|
|
||||
| `NonEbTicketMasterModel` allowedFields | Removed both |
|
||||
| `claimSearch()` select in controller | Removed `tm.policy_section`, `tm.policy_period` |
|
||||
| `getValidationRules()` in controller | Removed both validation rules |
|
||||
| `non_eb_claim_edit.php` Section 1 | Removed from view |
|
||||
| `non_eb_claim_list.php` | Removed hidden `<th>` and `<td>` columns |
|
||||
| `db.md` | Commented-out DROP COLUMN statements added for future cleanup |
|
||||
|
||||
---
|
||||
|
||||
## Pending / TODO
|
||||
|
||||
- Run DB ALTER TABLE statements (policy_start_date, policy_end_date, required_docs on `non_eb_ticket_master`; ticket_type on `claim_files`)
|
||||
- Run `non_eb_ticket_master_after_update` trigger in DB
|
||||
- End-to-end testing: create claim → edit claim → Claim Files tab → IR docs → History tab trigger verification
|
||||
- Verify status-based section visibility for all policy types
|
||||
- Test filter persistence (localStorage) on search page
|
||||
- Review `db.md` for any missing DDL
|
||||
465
nonebapi.md
Normal file
465
nonebapi.md
Normal file
@ -0,0 +1,465 @@
|
||||
# Non-EB Claims — External API Dev Doc & TODO
|
||||
|
||||
**Purpose:** Expose four read/write endpoints to external applications (client portals, mobile apps, integrations) for Non-EB Claims.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints to Build
|
||||
|
||||
| # | Method | URL | Purpose |
|
||||
|---|---|---|---|
|
||||
| 1 | POST | `/api/v1/non-eb-claim/create` | Create a new Non-EB claim |
|
||||
| 2 | POST | `/api/v1/non-eb-claim/list` | List claims with optional filter params |
|
||||
| 3 | GET | `/api/v1/non-eb-claim/history/{claim_id}` | Get field change history of a claim |
|
||||
| 4 | POST | `/api/v1/non-eb-claim/{claim_id}/upload-required-doc` | Upload a file against a required document checklist item |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decision — New Controller
|
||||
|
||||
Create a **separate API controller**: `app/Controllers/Api/NonEbClaimApiController.php`
|
||||
|
||||
**Why separate, not reusing `NonEbClaimController`:**
|
||||
- Existing controller uses session-based auth (`get_session_userid()`, `set_session_context()`) — not safe for API
|
||||
- API needs token/key auth middleware (no session)
|
||||
- API responses must be pure JSON always — no HTML rendering
|
||||
- `createClaim()` currently calls `get_session_userid()` via model callbacks (`checkAndADDCreatedByValue`) — needs to be overridden for API context
|
||||
- Existing `claimSearch()` reads `$this->request->getPost()` directly — API version should accept clean JSON body
|
||||
|
||||
**Reuse:**
|
||||
- `NonEbTicketMasterModel`, `NonEbClaimAssetModel`, `TicketHistoryModel`, `ClaimFilesModel` — reuse as-is
|
||||
- `claimHistory()` logic — copy and adapt (remove priority label mapping that uses internal arrays)
|
||||
- `getValidationRules()` — reuse for create, with relaxed `insured_contact_*` rules since those come from external context
|
||||
- `formatDatesForClaim()` — reuse as-is
|
||||
- `checkDuplicateNonEbClaim()` — reuse as-is
|
||||
- `handleAssetFileUpload()` — reuse as-is
|
||||
- `saveAssets()` — reuse as-is
|
||||
|
||||
---
|
||||
|
||||
## TODO Checklist
|
||||
|
||||
### Phase 1 — Route Registration
|
||||
|
||||
- [ ] Add `/api/v1` route group in `app/Config/Routes.php` (use existing auth filter — no `authMVC`):
|
||||
```php
|
||||
$routes->group('api/v1', ['filter' => 'your-existing-api-filter'], function($routes) {
|
||||
$routes->group('non-eb-claim', function($routes) {
|
||||
$routes->post('create', 'Api\NonEbClaimApiController::createClaim');
|
||||
$routes->post('list', 'Api\NonEbClaimApiController::listClaims');
|
||||
$routes->get('history/(:num)', 'Api\NonEbClaimApiController::claimHistory/$1');
|
||||
$routes->post('(:num)/upload-required-doc', 'Api\NonEbClaimApiController::uploadRequiredDoc/$1');
|
||||
});
|
||||
});
|
||||
```
|
||||
- [ ] Confirm CI4 namespace resolution for `App\Controllers\Api\NonEbClaimApiController`
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — Controller Skeleton
|
||||
|
||||
- [ ] Create `app/Controllers/Api/NonEbClaimApiController.php`
|
||||
- Namespace: `App\Controllers\Api`
|
||||
- Extends `BaseController`, uses `ResponseTrait`
|
||||
- Import: `NonEbTicketMasterModel`, `NonEbClaimAssetModel`, `TicketHistoryModel`, `TicketClaimStatusModel`, `ClaimFilesModel`
|
||||
- No session calls — `created_by` should be the resolved `api_client_id` (or a fixed system user ID for API)
|
||||
- All responses: `Content-Type: application/json`
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — API 1: Create Claim
|
||||
|
||||
**POST** `/api/v1/non-eb-claim/create`
|
||||
|
||||
**Context:** The user is already logged in and has selected a client policy from the interface. They are triggering a claim with only the loss details — what happened, where, and when. This is a **one-shot create with no edit step**. The controller is responsible for fetching all other mandatory data from the DB and assembling the full record before insert. The caller sends the minimum possible.
|
||||
|
||||
---
|
||||
|
||||
**What the caller sends (user-facing fields only):**
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|---|---|---|---|
|
||||
| `client_policy_id` | Yes | int | Selected in UI before opening the claim form; all other FK fields derived from this |
|
||||
| `nature_of_loss` | Yes | string | min 3 chars — what happened |
|
||||
| `loss_location` | Yes | string | where it happened |
|
||||
| `loss_date` | Yes | string | `DD-MM-YYYY` — when it happened |
|
||||
| `loss_description` | No | string | Additional details; required if `asset_file` is sent |
|
||||
| `loss_estimate` | No | numeric | Approximate loss value |
|
||||
| `claim_number` | No | string | `^[a-zA-Z0-9\/_-]+$` — if already known |
|
||||
| `asset_file` | No | file | xlsx/xls/csv/pdf |
|
||||
| `asset_id_code[]` | No | array | asset rows |
|
||||
| `serial_no[]` | No | array | |
|
||||
| `vehicle_no[]` | No | array | |
|
||||
| `asset_description[]` | No | array | |
|
||||
|
||||
> Surveyor fields, settlement fields, `nhance_claim_ref_no`, `priority` are **not** part of this create flow — they are filled by staff later via the internal web interface.
|
||||
|
||||
---
|
||||
|
||||
**What the controller fetches and inserts (caller does NOT send these):**
|
||||
|
||||
| Field | Source |
|
||||
|---|---|
|
||||
| `client_id` | `client_policy.client_id` |
|
||||
| `branch_id` | `client_policy.client_branch_id` |
|
||||
| `policy_type_id` | `client_policy.policy_type_id` |
|
||||
| `insurer_id` | `client_policy.insurer_id` |
|
||||
| `policy_no` | `client_policy.policy_no` |
|
||||
| `acm_id` | `client_rm` where `client_id = cp.client_id AND level = 3 AND is_active = 1` — first result |
|
||||
| `claim_status_id` | First row of `ticket_claim_status` where `ticket_type = policy_type_id ORDER BY id ASC` |
|
||||
| `insured_contact_name` | Logged-in user's name from auth (resolved via auth middleware) |
|
||||
| `insured_contact_number` | Logged-in user's mobile from auth |
|
||||
| `insured_contact_email` | Logged-in user's email from auth |
|
||||
| `priority` | Hard-coded default: `1` (Low) |
|
||||
| `created_by` | API system user ID (explicit — not from session) |
|
||||
|
||||
---
|
||||
|
||||
**Success Response `200`:**
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"claim_id": 123,
|
||||
"message": "Non-EB Claim created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response `400` — validation failure:**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "Input validation failed",
|
||||
"errors": {
|
||||
"nature_of_loss": "Nature of Loss is required",
|
||||
"loss_date": "Loss Date is required"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response `404` — policy not found:**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "Client policy not found or inactive"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response `422` — policy type not allowed:**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "Only Non-EB or Marine policy types are allowed"
|
||||
}
|
||||
```
|
||||
|
||||
**Conflict Response `409`:**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "Duplicate claim found for Client + Loss Date + Policy No combination"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**TODOs:**
|
||||
- [ ] Validate only the user-facing fields — write a trimmed validation rule set for API (not reusing `getValidationRules()` directly since that includes staff-side fields); required: `client_policy_id`, `nature_of_loss`, `loss_location`, `loss_date`
|
||||
- [ ] Fetch `client_policy` row — return `404` if not found or `is_active != 1`
|
||||
- [ ] Validate `policy_type.allocg IN ('Non-EB', 'Marine')` — return `422` if EB type sent
|
||||
- [ ] Fetch `acm_id` from `client_rm` (level=3, is_active=1) for the derived `client_id` — set null and log warning if none found
|
||||
- [ ] Auto-set `claim_status_id` — first `ticket_claim_status` row for derived `policy_type_id ORDER BY id ASC` (same pattern as `initiateClaim` in `EmployeeRestController`)
|
||||
- [ ] Resolve insured contact fields from the authenticated user (name, mobile, email) via auth middleware — these are mandatory DB fields but the user does not type them
|
||||
- [ ] Handle `created_by` explicitly in insert data — model's `beforeInsert` callback calls `get_session_userid()` which returns null for API; CI4 uses the explicitly passed value
|
||||
- [ ] Duplicate check via `checkDuplicateNonEbClaim()` — runs after derivation so `client_id` and `policy_no` are already populated
|
||||
- [ ] Date format: `DD-MM-YYYY` → `formatDatesForClaim()` converts to `Y-m-d` for DB
|
||||
- [ ] Call `saveAssets()` after insert if asset fields present
|
||||
- [ ] Call `putHistoryAfterInsert()` after insert
|
||||
- [ ] Skip `sendAutoMailTrigger()` for this initial create — claim is at first status, mail trigger is for status transitions; document this
|
||||
- [ ] Handle `multipart/form-data` when `asset_file` is included
|
||||
- [ ] Return `claim_id` in response so the caller can reference the created claim
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — API 2: List Claims
|
||||
|
||||
**POST** `/api/v1/non-eb-claim/list`
|
||||
|
||||
**Request Body (JSON):**
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|---|---|---|---|
|
||||
| `page` | No | int | Default 1 |
|
||||
| `per_page` | No | int | Default 20, max 100 |
|
||||
| `policy_type_id` | No | int | Filter by policy type |
|
||||
| `claim_status_id` | No | int | Filter by status |
|
||||
| `client_id` | No | int | Filter by client |
|
||||
| `insurer_id` | No | int | Filter by insurer |
|
||||
| `claim_number` | No | string | LIKE search |
|
||||
| `nhance_claim_ref_no` | No | string | LIKE search |
|
||||
| `date_type` | No | string | `created_date` or `updated_date` |
|
||||
| `start_date` | No | string | `DD-MM-YYYY`, used with `date_type` |
|
||||
| `end_date` | No | string | `DD-MM-YYYY`, used with `date_type` |
|
||||
| `show_closed` | No | bool | Default `false` — if false, excludes Settled/Closed/Rejected/Withdrawn |
|
||||
|
||||
**Success Response `200`:**
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"total": 87,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"data": [
|
||||
{
|
||||
"id": 123,
|
||||
"claim_number": "CLM/2026/001",
|
||||
"nhance_claim_ref_no": "NEB/2026/00123",
|
||||
"client_name": "ABC Corp",
|
||||
"insurer_name": "New India Assurance",
|
||||
"policy_type_name": "All Risk",
|
||||
"policy_no": "POL/1234/2026",
|
||||
"status": "Claim Intimation - Insured",
|
||||
"status_display": "Claim Intimation - Insured",
|
||||
"loss_date": "15-03-2026",
|
||||
"loss_location": "Chennai",
|
||||
"nature_of_loss": "Fire damage",
|
||||
"loss_estimate": "500000",
|
||||
"insured_contact_name": "John Doe",
|
||||
"insured_contact_number": "9876543210",
|
||||
"acm_name": "Ravi Kumar",
|
||||
"created_date": "01-03-2026",
|
||||
"updated_date": "20-03-2026"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**TODOs:**
|
||||
- [ ] Reuse the `claimSearch()` query from existing controller — extract it into a shared private method or duplicate in API controller
|
||||
- [ ] Add **pagination**: `LIMIT` + `OFFSET` based on `page` and `per_page`; also run a `COUNT(*)` variant of the same query for `total`
|
||||
- [ ] Accept JSON body (`$this->request->getJSON(true)`) not `getPost()` — existing controller uses `getPost()`
|
||||
- [ ] Cap `per_page` at 100 to prevent abuse
|
||||
- [ ] Strip HTML from `nature_of_loss`, `loss_description` before returning (use existing `convertHtmlToText()` or `esc()`)
|
||||
- [ ] Decide which fields to expose — **do not return**: `google_drive_links`, `documents_*`, `required_docs`, `is_active`, `created_by`, `updated_by`
|
||||
- [ ] `show_closed: false` (default) mirrors the existing default list behaviour (`whereNotIn` on terminal statuses)
|
||||
- [ ] Add `sort_by` param later (optional/phase 2): `loss_date`, `created_at`, `updated_at` with `sort_dir: asc|desc`
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 — API 3: Claim History
|
||||
|
||||
**GET** `/api/v1/non-eb-claim/history/{claim_id}`
|
||||
|
||||
**URL Param:** `claim_id` — integer, required
|
||||
|
||||
**What is shown vs hidden:**
|
||||
|
||||
This endpoint does **not** expose the full raw `ticket_history` table. It follows the same pattern as the EB `claimView` API — only **status progression is shown to the user**, and only statuses that have a `display_name` in `ticket_claim_status` are included. Internal status names, field-level changes (surveyor, ACM, priority etc.), and who made the change are all hidden.
|
||||
|
||||
The response is a **chronological status timeline** — oldest to newest — showing when the claim moved through each user-visible stage.
|
||||
|
||||
**Filtering logic (mirrors EB `claimView`):**
|
||||
1. Fetch all `ticket_history` rows for `ticket_id`
|
||||
2. Keep only rows where `field_name = 'claim_status_id'`
|
||||
3. For each row, look up `new_value` against `ticket_claim_status` — only include it if that status has a non-null `display_name`
|
||||
4. Map to `display_name` for output — never expose raw internal `claim_status` string
|
||||
5. Reverse to chronological order (oldest first for timeline display)
|
||||
6. Do not expose `modified_by` — who changed it is internal
|
||||
|
||||
**URL Param:** `claim_id` — integer, required
|
||||
|
||||
**Success Response `200`:**
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"claim_id": 123,
|
||||
"history": [
|
||||
{
|
||||
"status": "Claim Intimation - Insured",
|
||||
"changed_at": "01-03-2026 10:15 AM"
|
||||
},
|
||||
{
|
||||
"status": "Under Process",
|
||||
"changed_at": "05-03-2026 02:30 PM"
|
||||
},
|
||||
{
|
||||
"status": "Claim Settled",
|
||||
"changed_at": "20-03-2026 04:45 PM"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> If a status does not have a `display_name` in `ticket_claim_status` it is silently skipped — it is an internal-only status not meant for user visibility.
|
||||
|
||||
**Error Response `404`:**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"message": "Claim not found"
|
||||
}
|
||||
```
|
||||
|
||||
**TODOs:**
|
||||
- [ ] Do NOT reuse `claimHistory()` from `NonEbClaimController` as-is — that returns all field changes for internal staff view. Write a separate method that fetches only `claim_status_id` history rows
|
||||
- [ ] Fetch all `ticket_history` where `ticket_id = claim_id AND field_name = 'claim_status_id' AND is_active = 1 ORDER BY created_at ASC`
|
||||
- [ ] For each row join or look up `ticket_claim_status` on `new_value = id` — filter out rows where `display_name IS NULL`
|
||||
- [ ] Verify claim exists in `non_eb_ticket_master` (`is_active = 1`) before querying history — return `404` if not
|
||||
- [ ] Format `created_at` as `DD-MM-YYYY HH:MM AM/PM` (matches EB pattern: `date('d-m-Y h:i A', strtotime(...))`)
|
||||
- [ ] Output only `status` (display_name) and `changed_at` per row — no `field_name`, no `old_value`, no `modified_by`
|
||||
- [ ] Scope check: verify the claim's `client_id` matches the authenticated user's client before returning — prevents users from fetching other clients' claim history
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 — API 4: Upload Required Document
|
||||
|
||||
**POST** `/api/v1/non-eb-claim/{claim_id}/upload-required-doc`
|
||||
|
||||
**URL Param:** `claim_id` — integer, required
|
||||
|
||||
**Request Body (`multipart/form-data`):**
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|---|---|---|---|
|
||||
| `document_name` | Yes | string | Must exactly match one of the `document_name` values in `required_docs.docs[]` |
|
||||
| `file` | Yes | file | Allowed types: same as `UPLOAD_EXT_CLAIM_DOCS` |
|
||||
|
||||
**`required_docs` JSON structure (stored in `non_eb_ticket_master.required_docs`):**
|
||||
```json
|
||||
{
|
||||
"is_action_freeze": false,
|
||||
"docs": [
|
||||
{ "document_name": "Invoice Copy", "document_received": false },
|
||||
{ "document_name": "Survey Report", "document_received": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
1. Validate claim exists (`non_eb_ticket_master.id = claim_id`, `is_active = 1`) — `404` if not
|
||||
2. Fetch `required_docs` JSON from the ticket
|
||||
3. If `required_docs` is empty or null — return `422` (no checklist configured for this claim)
|
||||
4. If `is_action_freeze = true` — return `423` (checklist is locked, uploads not allowed)
|
||||
5. Find the doc entry where `document_name` matches exactly — `404` if not found in list
|
||||
6. Upload file → insert row into `claim_files` (`ticket_type = 2`, `file_type = 2`, `ticket_id = claim_id`, `doc_name = document_name`)
|
||||
7. Update `required_docs`: set `document_received = true` for the matched doc entry, write back to `non_eb_ticket_master.required_docs`
|
||||
8. Return success with the full updated `required_docs` object
|
||||
|
||||
**Success Response `200`:**
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"message": "Document uploaded successfully",
|
||||
"claim_id": 123,
|
||||
"document_name": "Invoice Copy",
|
||||
"required_docs": {
|
||||
"is_action_freeze": false,
|
||||
"docs": [
|
||||
{ "document_name": "Invoice Copy", "document_received": true },
|
||||
{ "document_name": "Survey Report", "document_received": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response `404` — claim not found:**
|
||||
```json
|
||||
{ "status": false, "message": "Claim not found" }
|
||||
```
|
||||
|
||||
**Error Response `422` — no checklist configured:**
|
||||
```json
|
||||
{ "status": false, "message": "No required documents checklist configured for this claim" }
|
||||
```
|
||||
|
||||
**Error Response `423` — checklist locked:**
|
||||
```json
|
||||
{ "status": false, "message": "Document checklist is locked for this claim" }
|
||||
```
|
||||
|
||||
**Error Response `404` — document_name not in list:**
|
||||
```json
|
||||
{ "status": false, "message": "Document 'Invoice Copy' not found in required documents list" }
|
||||
```
|
||||
|
||||
**TODOs:**
|
||||
- [ ] Use `claim_files` model for file insert — same structure as existing `uploadFile()` in `NonEbClaimController` (`ticket_type = 2`, `file_type = 2`)
|
||||
- [ ] File upload path: `WRITEPATH . 'uploads/claim_files/'` — same as web controller
|
||||
- [ ] `created_by`: use API system user ID (same pattern as create endpoint)
|
||||
- [ ] `document_name` match is **case-sensitive exact match** — document this for API consumers
|
||||
- [ ] Allow re-upload if `document_received` is already `true` — overwrite the previous `claim_files` entry (soft-delete old, insert new) OR just insert new and keep both; decide and document
|
||||
- [ ] Do NOT expose the `claim_files.url` file path directly — return download URL via `base_url('downloadClaimFile/') . $file_id` so internal paths are not leaked
|
||||
- [ ] Validate file extension against `UPLOAD_EXT_CLAIM_DOCS` constant — reject unsupported types with `415`
|
||||
- [ ] The `required_docs` update must be atomic — update the JSON and `claim_files` insert in a DB transaction; roll back file insert if JSON update fails
|
||||
|
||||
---
|
||||
|
||||
### Phase 7 — Cross-cutting Concerns
|
||||
|
||||
- [ ] **Rate limiting**: add a simple request counter per `api_key` per minute in a cache table or Redis. Block at 60 req/min.
|
||||
- [ ] **Request logging**: log every API request (api_client_id, endpoint, status_code, ip, timestamp) into an `api_request_log` table for audit
|
||||
```sql
|
||||
CREATE TABLE api_request_log (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
api_client_id INT,
|
||||
endpoint VARCHAR(100),
|
||||
method VARCHAR(10),
|
||||
status_code SMALLINT,
|
||||
ip_address VARCHAR(45),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
- [ ] **CORS headers**: if external app is browser-based, add `Access-Control-Allow-Origin` + preflight `OPTIONS` handling in a filter
|
||||
- [ ] **API versioning**: prefix as `/api/v1/non-eb-claim/...` from day one — easier to version-bump later without breaking consumers
|
||||
- [ ] **Error envelope**: all errors must follow a consistent shape:
|
||||
```json
|
||||
{ "status": false, "code": 400, "message": "...", "errors": {} }
|
||||
```
|
||||
- [ ] **Postman collection**: create and share a Postman collection with all 4 endpoints, sample request bodies, and expected responses
|
||||
- [ ] **DB changes** — add to `db.md`:
|
||||
- `api_request_log` table
|
||||
|
||||
---
|
||||
|
||||
## Field Reference — What the External App Must Know
|
||||
|
||||
### Required IDs (need lookup endpoints or a handshake step)
|
||||
These are FK IDs that the external app must send — they need to either be pre-agreed or fetched from lookup endpoints (future phase):
|
||||
|
||||
| ID Field | Lookup Source |
|
||||
|---|---|
|
||||
| `client_id` | `clients` table |
|
||||
| `branch_id` | `client_branch` table, filtered by `client_id` |
|
||||
| `policy_type_id` | `policy_type` table, `allocg IN ('Non-EB','Marine')` |
|
||||
| `client_policy_id` | `client_policy` table, filtered by `branch_id` |
|
||||
| `insurer_id` | `insurers` table |
|
||||
| `acm_id` | `user_profiles` table, `role = 3` |
|
||||
| `claim_status_id` | `ticket_claim_status` table, filtered by `ticket_type = policy_type_id` |
|
||||
|
||||
**TODO (future phase):** Build read-only lookup endpoints (`/api/v1/lookup/clients`, `/api/v1/lookup/policy-types`, etc.) so the external app can populate dropdowns without hardcoding IDs.
|
||||
|
||||
---
|
||||
|
||||
### Claim Status Flow (for external app awareness)
|
||||
The trigger-based DB trigger (`non_eb_ticket_master_after_update`) auto-logs status changes into `ticket_history`. History API reflects these changes. External app should not attempt to set arbitrary statuses — only statuses listed in `allowed_status` of the current status are valid transitions.
|
||||
|
||||
**TODO:** Document valid status transitions in a separate handshake endpoint response or static doc section.
|
||||
|
||||
---
|
||||
|
||||
## Files to Create / Modify
|
||||
|
||||
| File | Action |
|
||||
|---|---|
|
||||
| `app/Controllers/Api/NonEbClaimApiController.php` | **Create** |
|
||||
| `app/Config/Routes.php` | **Modify** — add `/api/v1` route group |
|
||||
| `db.md` | **Modify** — add `api_request_log` DDL |
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (this phase)
|
||||
|
||||
- Update claim via API — not requested; skip for now
|
||||
- Delete/remove claim via API — internal operation only
|
||||
- Claim file download via API — separate phase
|
||||
- IR Documents (required_docs) CRUD via API — separate phase (upload-required-doc only marks received + stores file; full checklist management is out of scope)
|
||||
- Webhook callbacks to external app on status change — separate phase
|
||||
447
nonebapidocs.md
Normal file
447
nonebapidocs.md
Normal file
@ -0,0 +1,447 @@
|
||||
# Non-EB Claims API — Developer Reference
|
||||
|
||||
**Version:** v1
|
||||
**Base URL:** `{{base_url}}/api/v1/non-eb-claim`
|
||||
**Content-Type:** `application/json` (except file upload endpoints — see individual notes)
|
||||
**All responses are JSON.**
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Every request must include a valid JWT token in the `Authorization` header.
|
||||
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
- The token is issued at login and identifies the current user.
|
||||
- The API reads the user's **name, mobile, and email** from the token automatically — you do not need to send contact details separately.
|
||||
- If the token is missing or expired, all endpoints return `401`.
|
||||
|
||||
---
|
||||
|
||||
## Response Envelope
|
||||
|
||||
All responses follow this consistent shape:
|
||||
|
||||
**Success:**
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"code": 200,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Error:**
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"code": 400,
|
||||
"message": "Human-readable error message",
|
||||
"errors": { "field": "Specific field error" }
|
||||
}
|
||||
```
|
||||
|
||||
> `errors` is only present on `400` validation failures.
|
||||
|
||||
---
|
||||
|
||||
## Date Format
|
||||
|
||||
- **Input:** always send dates as `DD-MM-YYYY` (e.g. `"25-03-2026"`)
|
||||
- **Output:** dates in list/history responses are returned as `DD-MM-YYYY`
|
||||
- **Timestamps** in history are returned as `DD-MM-YYYY hh:mm AM/PM`
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
| # | Method | URL | Description |
|
||||
|---|---|---|---|
|
||||
| 1 | `POST` | `/api/v1/non-eb-claim/create` | Raise a new Non-EB claim |
|
||||
| 2 | `POST` | `/api/v1/non-eb-claim/list` | List / search claims |
|
||||
| 3 | `GET` | `/api/v1/non-eb-claim/history/{claim_id}` | Status timeline of a claim |
|
||||
| 4 | `POST` | `/api/v1/non-eb-claim/{claim_id}/upload-required-doc` | Upload a required document |
|
||||
|
||||
---
|
||||
|
||||
## 1. Create Claim
|
||||
|
||||
**POST** `/api/v1/non-eb-claim/create`
|
||||
|
||||
### How it works
|
||||
|
||||
The user has already selected a **client policy** on the previous screen. You send only that policy ID plus the loss details. The server derives everything else (client, branch, insurer, policy number, account manager, initial status) automatically from the policy.
|
||||
|
||||
The logged-in user's name, mobile, and email are pulled from their JWT token and stored as the insured contact — you do not send them.
|
||||
|
||||
### Request
|
||||
|
||||
**Content-Type:** `application/json`
|
||||
If you are also uploading an asset file, switch to `multipart/form-data` and include all fields as form fields.
|
||||
|
||||
| Field | Required | Type | Validation | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `client_policy_id` | Yes | integer | Must exist and be active | Selected policy from the previous screen |
|
||||
| `nature_of_loss` | Yes | string | min 3 chars | What happened |
|
||||
| `loss_location` | Yes | string | non-empty | Where it happened |
|
||||
| `loss_date` | Yes | string | `DD-MM-YYYY` | When it happened |
|
||||
| `loss_description` | No | string | — | Required only if `asset_file` is included |
|
||||
| `loss_estimate` | No | numeric | — | Approximate value of loss |
|
||||
| `claim_number` | No | string | `[a-zA-Z0-9/_-]` only | If already assigned by insurer |
|
||||
| `asset_file` | No | file | xlsx/xls/csv/pdf | Asset register file |
|
||||
| `asset_id_code[]` | No | array | — | Asset ID codes (one per row) |
|
||||
| `serial_no[]` | No | array | — | Serial numbers (parallel to asset_id_code[]) |
|
||||
| `vehicle_no[]` | No | array | — | Vehicle numbers |
|
||||
| `asset_description[]` | No | array | — | Asset descriptions |
|
||||
|
||||
> **Do not send:** `client_id`, `branch_id`, `insurer_id`, `policy_no`, `acm_id`, `claim_status_id`, `insured_contact_name`, `insured_contact_number`, `insured_contact_email` — these are all derived server-side.
|
||||
|
||||
### Example Request (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"client_policy_id": 42,
|
||||
"nature_of_loss": "Fire damage to warehouse",
|
||||
"loss_location": "Chennai",
|
||||
"loss_date": "22-03-2026",
|
||||
"loss_description": "Warehouse section B caught fire due to electrical fault",
|
||||
"loss_estimate": 500000
|
||||
}
|
||||
```
|
||||
|
||||
### Example Request (multipart — with asset file)
|
||||
|
||||
```
|
||||
POST /api/v1/non-eb-claim/create
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
client_policy_id = 42
|
||||
nature_of_loss = Fire damage to warehouse
|
||||
loss_location = Chennai
|
||||
loss_date = 22-03-2026
|
||||
loss_description = Warehouse section B caught fire
|
||||
asset_file = <file>
|
||||
asset_id_code[] = AST-001
|
||||
serial_no[] = SN-12345
|
||||
asset_description[] = Industrial generator
|
||||
```
|
||||
|
||||
### Success Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"code": 200,
|
||||
"claim_id": 123,
|
||||
"message": "Non-EB Claim created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
> Save `claim_id` — you'll need it for history and document upload.
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Code | Scenario | Sample Message |
|
||||
|---|---|---|
|
||||
| `400` | Validation failed | `"Input validation failed"` + `errors` object |
|
||||
| `400` | Asset file sent without `loss_description` | `"Loss Description is required when uploading an asset file."` |
|
||||
| `401` | Missing / expired token | `"Unauthorized"` |
|
||||
| `404` | `client_policy_id` not found or inactive | `"Client policy not found or inactive"` |
|
||||
| `409` | Duplicate claim (same client + loss date + policy no) | `"Duplicate claim found for Client + Loss Date + Policy No combination"` |
|
||||
| `422` | Policy type is EB (not Non-EB / Marine) | `"Only Non-EB or Marine policy types are allowed"` |
|
||||
| `422` | No claim status configured for policy type | `"No claim status configured for this policy type"` |
|
||||
| `500` | DB insert failed | `"Failed to create claim"` |
|
||||
|
||||
### 400 Validation Error Example
|
||||
|
||||
```json
|
||||
{
|
||||
"status": false,
|
||||
"code": 400,
|
||||
"message": "Input validation failed",
|
||||
"errors": {
|
||||
"nature_of_loss": "Nature of Loss is required",
|
||||
"loss_date": "Loss Date is required"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. List Claims
|
||||
|
||||
**POST** `/api/v1/non-eb-claim/list`
|
||||
|
||||
### How it works
|
||||
|
||||
Send a JSON body with optional filters and pagination params. By default, **closed/settled/rejected/withdrawn claims are excluded**. Pass `"show_closed": true` to include them.
|
||||
|
||||
Results are sorted newest first.
|
||||
|
||||
### Request
|
||||
|
||||
**Content-Type:** `application/json`
|
||||
|
||||
| Field | Required | Type | Default | Notes |
|
||||
|---|---|---|---|---|
|
||||
| `page` | No | integer | `1` | Page number |
|
||||
| `per_page` | No | integer | `20` | Max `100` |
|
||||
| `client_id` | No | integer | — | Filter by client |
|
||||
| `insurer_id` | No | integer | — | Filter by insurer |
|
||||
| `policy_type_id` | No | integer | — | Filter by policy type |
|
||||
| `claim_status_id` | No | integer | — | Filter by exact status |
|
||||
| `claim_number` | No | string | — | Partial match (LIKE) |
|
||||
| `nhance_claim_ref_no` | No | string | — | Partial match (LIKE) |
|
||||
| `date_type` | No | string | — | `"created_date"` or `"updated_date"` |
|
||||
| `start_date` | No | string | — | `DD-MM-YYYY` — used with `date_type` |
|
||||
| `end_date` | No | string | — | `DD-MM-YYYY` — used with `date_type` |
|
||||
| `show_closed` | No | boolean | `false` | `true` to include settled/closed/rejected/withdrawn |
|
||||
|
||||
### Example Request
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"client_id": 5,
|
||||
"date_type": "created_date",
|
||||
"start_date": "01-03-2026",
|
||||
"end_date": "31-03-2026"
|
||||
}
|
||||
```
|
||||
|
||||
### Success Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"code": 200,
|
||||
"total": 87,
|
||||
"page": 1,
|
||||
"per_page": 20,
|
||||
"data": [
|
||||
{
|
||||
"id": 123,
|
||||
"claim_number": "CLM/2026/001",
|
||||
"nhance_claim_ref_no": "NEB/2026/00123",
|
||||
"policy_no": "POL/1234/2026",
|
||||
"policy_type_id": 10,
|
||||
"claim_status_id": 3,
|
||||
"status": "Claim Intimation - Insured",
|
||||
"status_display": "Claim Intimation - Insured",
|
||||
"policy_type_name": "All Risk",
|
||||
"client_name": "ABC Corp",
|
||||
"insurer_name": "New India Assurance",
|
||||
"loss_date": "2026-03-22",
|
||||
"loss_location": "Chennai",
|
||||
"nature_of_loss": "Fire damage to warehouse",
|
||||
"loss_estimate": "500000",
|
||||
"insured_contact_name": "Ravi Kumar",
|
||||
"insured_contact_number": "9876543210",
|
||||
"acm_name": "Anand",
|
||||
"created_date": "22-03-2026",
|
||||
"updated_date": "25-03-2026"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> When no results match, `total` is `0` and `data` is `[]`. The response is still `200`.
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Code | Scenario |
|
||||
|---|---|
|
||||
| `401` | Missing / expired token |
|
||||
|
||||
---
|
||||
|
||||
## 3. Claim History
|
||||
|
||||
**GET** `/api/v1/non-eb-claim/history/{claim_id}`
|
||||
|
||||
### How it works
|
||||
|
||||
Returns the **status progression timeline** of a claim — oldest stage first. Only statuses that are configured as user-visible are included. Internal/intermediate statuses used by staff are automatically filtered out.
|
||||
|
||||
Each entry shows the status name and when it was reached. Who changed it is not exposed.
|
||||
|
||||
### URL Parameter
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `claim_id` | integer | Yes | The `id` returned from the create endpoint |
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
GET /api/v1/non-eb-claim/history/123
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### Success Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"code": 200,
|
||||
"claim_id": 123,
|
||||
"history": [
|
||||
{
|
||||
"status": "Claim Intimation - Insured",
|
||||
"changed_at": "22-03-2026 10:15 AM"
|
||||
},
|
||||
{
|
||||
"status": "Under Process",
|
||||
"changed_at": "24-03-2026 02:30 PM"
|
||||
},
|
||||
{
|
||||
"status": "Claim Settled",
|
||||
"changed_at": "28-03-2026 04:45 PM"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> If no history exists yet, `history` is an empty array `[]`.
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Code | Scenario |
|
||||
|---|---|
|
||||
| `401` | Missing / expired token |
|
||||
| `404` | Claim not found or inactive |
|
||||
|
||||
---
|
||||
|
||||
## 4. Upload Required Document
|
||||
|
||||
**POST** `/api/v1/non-eb-claim/{claim_id}/upload-required-doc`
|
||||
|
||||
### How it works
|
||||
|
||||
Each claim has a **required documents checklist** configured by the staff (e.g. "Invoice Copy", "Survey Report"). This endpoint lets the user upload a file against one of those checklist items.
|
||||
|
||||
When a document is uploaded successfully, its `document_received` flag in the checklist is set to `true` and the updated checklist is returned so you can refresh the UI.
|
||||
|
||||
The `document_name` you send must **exactly match** (case-sensitive) one of the `document_name` values in the checklist. Use the checklist data to drive your UI — display the exact names as options so the user cannot type the wrong value.
|
||||
|
||||
### URL Parameter
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `claim_id` | integer | Yes | The claim to upload against |
|
||||
|
||||
### Request
|
||||
|
||||
**Content-Type:** `multipart/form-data` (always — this is a file upload)
|
||||
|
||||
| Field | Required | Type | Notes |
|
||||
|---|---|---|---|
|
||||
| `document_name` | Yes | string | Must exactly match a `document_name` in the claim's checklist |
|
||||
| `file` | Yes | file | Allowed types: pdf, jpg, jpeg, png, doc, docx, xls, xlsx |
|
||||
|
||||
### How to get the checklist
|
||||
|
||||
The required documents list for a claim is returned when you fetch claim details (or can be shown after claim creation). The structure is:
|
||||
|
||||
```json
|
||||
{
|
||||
"is_action_freeze": false,
|
||||
"docs": [
|
||||
{ "document_name": "Invoice Copy", "document_received": false },
|
||||
{ "document_name": "Survey Report", "document_received": true },
|
||||
{ "document_name": "Police FIR Copy","document_received": false }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `is_action_freeze: true` means the checklist is locked — the upload endpoint will reject new uploads with `423`.
|
||||
- `document_received: true` means the staff has already received this document. You may still re-upload if needed (previous upload is replaced).
|
||||
- Show only the document names as upload targets — do not allow freetext entry.
|
||||
|
||||
### Example Request
|
||||
|
||||
```
|
||||
POST /api/v1/non-eb-claim/123/upload-required-doc
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token>
|
||||
|
||||
document_name = Invoice Copy
|
||||
file = <file>
|
||||
```
|
||||
|
||||
### Success Response `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"status": true,
|
||||
"code": 200,
|
||||
"message": "Document uploaded successfully",
|
||||
"claim_id": 123,
|
||||
"document_name": "Invoice Copy",
|
||||
"download_url": "https://yourdomain.com/downloadClaimFile/456",
|
||||
"required_docs": {
|
||||
"is_action_freeze": false,
|
||||
"docs": [
|
||||
{ "document_name": "Invoice Copy", "document_received": true },
|
||||
{ "document_name": "Survey Report", "document_received": true },
|
||||
{ "document_name": "Police FIR Copy", "document_received": false }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Use the returned `required_docs` to update the checklist UI immediately without a separate fetch.
|
||||
|
||||
### Error Responses
|
||||
|
||||
| Code | Scenario | Message |
|
||||
|---|---|---|
|
||||
| `400` | `document_name` not sent | `"document_name is required"` |
|
||||
| `400` | No file sent or invalid file | `"A valid file is required"` |
|
||||
| `401` | Missing / expired token | `"Unauthorized"` |
|
||||
| `404` | Claim not found | `"Claim not found"` |
|
||||
| `404` | `document_name` not in checklist | `"Document 'Invoice Copy' not found in required documents list"` |
|
||||
| `415` | Unsupported file type | `"Unsupported file type: bmp"` |
|
||||
| `422` | Claim has no checklist configured | `"No required documents checklist configured for this claim"` |
|
||||
| `423` | Checklist is locked | `"Document checklist is locked for this claim"` |
|
||||
| `500` | Upload or DB failure | `"File upload failed"` / `"Failed to save document. Please try again."` |
|
||||
|
||||
---
|
||||
|
||||
## Common Error Reference
|
||||
|
||||
| HTTP Code | Meaning | When it happens |
|
||||
|---|---|---|
|
||||
| `200` | OK | Request succeeded |
|
||||
| `400` | Bad Request | Validation failed — check `errors` object |
|
||||
| `401` | Unauthorized | Token missing, invalid, or expired |
|
||||
| `404` | Not Found | Resource doesn't exist or is inactive |
|
||||
| `409` | Conflict | Duplicate claim detected |
|
||||
| `415` | Unsupported Media Type | File type not allowed |
|
||||
| `422` | Unprocessable | Valid request but business rule blocks it |
|
||||
| `423` | Locked | Checklist is frozen — no uploads allowed |
|
||||
| `500` | Server Error | Something failed on the backend |
|
||||
|
||||
---
|
||||
|
||||
## Tips for Integration
|
||||
|
||||
**Checking `status` field:**
|
||||
Always check `response.status === true` before proceeding — do not rely solely on the HTTP status code.
|
||||
|
||||
**Pagination:**
|
||||
Use `total`, `page`, and `per_page` from the list response to build pagination controls. Total number of pages = `Math.ceil(total / per_page)`.
|
||||
|
||||
**Asset rows (create claim):**
|
||||
Send parallel arrays. Row 0 of `asset_id_code[]`, `serial_no[]`, `vehicle_no[]`, `asset_description[]` form one asset entry. Leave a value empty string if not applicable for that row.
|
||||
|
||||
**Document name matching (upload):**
|
||||
Always populate the `document_name` field from the checklist data returned by the server — never let users type it freehand. The match is case-sensitive exact.
|
||||
|
||||
**Re-upload:**
|
||||
Uploading a document that already has `document_received: true` is allowed. The new file is saved alongside the previous one and `document_received` stays `true`.
|
||||
79
public/dev_logs/2026-03-24.md
Normal file
79
public/dev_logs/2026-03-24.md
Normal file
@ -0,0 +1,79 @@
|
||||
# Dev Log — 2026-03-24
|
||||
|
||||
## Feature: Non EB Terms & Non EB Rack Rate in Client Policy List
|
||||
|
||||
### Objective
|
||||
Introduce "Non EB Terms" and "Non EB Rack Rate" menu items in the client policy table hamburger dropdown. These menus replace the existing "Terms" and "Rack Rate" menus when the policy type's `allocg` field is `Non-EB` or `Marine`.
|
||||
|
||||
---
|
||||
|
||||
### Files Changed
|
||||
|
||||
#### 1. `app/Models/ClientPolicyModel.php`
|
||||
- **`getClientPolicyByClientId()`** — Added two new selects:
|
||||
- `policy_type.allocg as allocg` (policy_type table was already joined)
|
||||
- `leads.misc as lead_misc` (leads table was already joined via `client_policy.is_from_lead = leads.id`)
|
||||
- **`$allowedFields`** — Added `non_eb_rack_rate_files` to allow updating the new JSON column.
|
||||
|
||||
#### 2. `app/Controllers/ClientController.php`
|
||||
- **`editClientOnboarding()`** — Added `allocg` and `lead_misc` to the policy data mapping array passed to the view.
|
||||
- **Added 3 new methods:**
|
||||
- `getNonEbRackRateFiles()` — GET endpoint, reads `non_eb_rack_rate_files` JSON from `client_policy` table and returns parsed file list.
|
||||
- `uploadNonEbRackRateFile()` — POST endpoint, uploads file to `writable/uploads/non_eb_rack_rate/`, appends file entry to `non_eb_rack_rate_files` JSON column.
|
||||
- `removeNonEbRackRateFile()` — POST endpoint, removes file entry from JSON column and deletes physical file from disk.
|
||||
|
||||
#### 3. `app/Config/Routes.php`
|
||||
- Added 3 new routes under `client/policy` group:
|
||||
- `GET getNonEbRackRateFiles`
|
||||
- `POST uploadNonEbRackRateFile`
|
||||
- `POST removeNonEbRackRateFile`
|
||||
|
||||
#### 4. `app/Config/Constants.php`
|
||||
- Added `UPLOAD_EXT_NON_EB_RACK_RATE` constant: `['pdf', 'xls', 'xlsx']`
|
||||
|
||||
#### 5. `app/Views/client_policy.php`
|
||||
- **Dropdown menus (4 locations: ~lines 530, 808, 2596, 2642):**
|
||||
- Added `allocg` check: `item.allocg === 'Non-EB' || item.allocg === 'Marine'`
|
||||
- If true → shows "Non EB Terms" + "Non EB Rack Rate", hides "Terms" + "Rack Rate"
|
||||
- If false → shows "Terms" + "Rack Rate" (original behavior)
|
||||
- **Non EB Terms click handler (`btnNonEbTerms`):**
|
||||
- Parses `lead_misc` JSON from data attribute
|
||||
- If `placement_sheet_id` key exists → opens Google Sheet (`https://docs.google.com/spreadsheets/d/{id}`) in new tab
|
||||
- Otherwise → `alert('No terms found')`
|
||||
- **Non EB Rack Rate click handler (`btnNonEbRackRateModal`):**
|
||||
- Opens `#nonEbRackRateModal` using `new bootstrap.Modal()` (BS5 API, stored on `window._nonEbModal`)
|
||||
- Loads existing files via AJAX `getNonEbRackRateFiles`
|
||||
- **Upload handler (`#btnUploadNonEbFile`):**
|
||||
- Validates file extension (pdf/xlsx/xls) client-side
|
||||
- Uploads via AJAX `uploadNonEbRackRateFile` with FormData
|
||||
- Appends uploaded file to list on success
|
||||
- **Delete handler (`btnRemoveNonEbFile`):**
|
||||
- SweetAlert confirmation
|
||||
- Calls `removeNonEbRackRateFile` via AJAX
|
||||
- Removes file row from DOM on success
|
||||
|
||||
#### 6. `app/Views/client_onboarding.php`
|
||||
- **Added modal HTML** (`#nonEbRackRateModal`) at line ~457, placed outside tab-pane containers (same level as `autoFetchBranchModal`) to avoid Bootstrap stacking issues.
|
||||
- **Scoped CSS** for compact modal: forced `max-width: 420px`, `min-height: auto` on modal-body, reduced padding on header/body/footer.
|
||||
- **Close buttons** use `onclick="if(window._nonEbModal) window._nonEbModal.hide();"` — inline JS referencing the global BS5 Modal instance to avoid jQuery binding/timing issues.
|
||||
|
||||
#### 7. `app/Controllers/EmployeeRestController.php`
|
||||
- **Line ~2257 query** — Added `LEFT JOIN` with `policy_type` table and a `CASE WHEN` select:
|
||||
```sql
|
||||
CASE WHEN policy_type.allocg IN ('Non-EB', 'Marine') THEN 'Non-EB' ELSE 'EB' END as allocg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### DDL Required (Manual)
|
||||
```sql
|
||||
ALTER TABLE `client_policy` ADD COLUMN `non_eb_rack_rate_files` JSON NULL DEFAULT NULL AFTER `wellness_vendor_id`;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Key Technical Decisions
|
||||
- **Bootstrap version conflict**: Project loads BS5 via `vendor.min.js` and BS3.4.1 via footer CDN. `$.fn.modal()` uses BS3 (broken), so all modal operations use `new bootstrap.Modal()` (BS5 native API). Modal instance stored on `window._nonEbModal` for close button access.
|
||||
- **Modal placement**: Modal HTML must be outside tab-pane containers in `client_onboarding.php` to avoid visibility/stacking issues.
|
||||
- **File storage**: Rack rate files stored physically in `writable/uploads/non_eb_rack_rate/` and tracked as JSON array in `client_policy.non_eb_rack_rate_files` column. Each entry: `{name, original_name, type, uploaded_at}`.
|
||||
- **Menu visibility**: Driven by `policy_type.allocg` field — `Non-EB`/`Marine` shows Non EB menus, everything else shows standard Terms/Rack Rate menus.
|
||||
341
public/dev_logs/2026-03-26.md
Normal file
341
public/dev_logs/2026-03-26.md
Normal file
@ -0,0 +1,341 @@
|
||||
# Dev Log — 2026-03-26
|
||||
|
||||
## Feature: Non-EB Claim Form — UI Overhaul, Validation & Asset File Upload
|
||||
|
||||
### Objective
|
||||
Continued development of the Non-EB Claims creation form (`non_eb_claim_form.php`). This session covered three major areas: (1) UI restyling to match the GMC ticket form, (2) comprehensive frontend validation with JS-only form submission, and (3) asset section enhancement with file upload support.
|
||||
|
||||
---
|
||||
|
||||
## 1. UI Restyling — Match `ticket_form_gmc.php` Accordion & Card Style
|
||||
|
||||
### What Changed
|
||||
Replaced the custom accordion implementation with Bootstrap card/collapse pattern used in the existing GMC ticket form.
|
||||
|
||||
### Before
|
||||
- Custom CSS classes: `.accordion`, `.accordion-content`, `.arrow`, `.rotate`
|
||||
- Custom JS `toggleAccordion(el)` function using `classList.toggle('show')`
|
||||
- Section headers: `<h4 onclick="toggleAccordion(this)">Title <span class="arrow">▶</span></h4>`
|
||||
- Section body: `<div class="accordion-content show">`
|
||||
- Labels had no consistent font-size class
|
||||
|
||||
### After
|
||||
- Bootstrap card/collapse pattern: `<div class="card mb-1">` → `<div class="collapse show">`
|
||||
- Section headers: `<h4 class="m-1">Title <a data-toggle="collapse" href="#collapseX"><i class="mdi mdi-chevron-down"></i></a></h4>`
|
||||
- Section body: `<div id="collapseX" class="collapse show" data-parent="#accordionX"><div class="card-body">`
|
||||
- Removed `toggleAccordion()` JS function entirely
|
||||
- All labels now use `class="label-font-size"` (0.875rem) matching GMC form
|
||||
|
||||
### CSS Classes Added (matching `ticket_form_gmc.php`)
|
||||
```css
|
||||
.readonly-color { background-color: #e0e0e0; color: #666; }
|
||||
.readonly-select { pointer-events: none; background-color: #f0f0f0; color: #666; }
|
||||
.label-font-size { font-size: 0.875rem; }
|
||||
```
|
||||
|
||||
### Readonly Fields Styled
|
||||
- Insurer, Policy No, Policy Start Date, Policy Expiry Date inputs — added `readonly-color` class for darker background on auto-filled fields
|
||||
|
||||
### Section ID Mapping
|
||||
| Section | Accordion ID | Collapse ID |
|
||||
|---|---|---|
|
||||
| Policy & Account Details | accordion1 | collapseOne |
|
||||
| Insured Contact Details | accordion2 | collapseTwo |
|
||||
| Loss / Incident Details | accordion3 | collapseThree |
|
||||
| Intimation & Claim Reference | accordion4 | collapseFour |
|
||||
| Status & Tracking | accordion5 | collapseFive |
|
||||
| Asset Details | accordion6 | collapseSix |
|
||||
| Surveyor Details | section_surveyor | collapseSeven |
|
||||
| Documents & Attachments | accordion8 | collapseEight |
|
||||
| Settlement Details | section_settlement | collapseNine |
|
||||
|
||||
- Surveyor and Settlement sections use `section_surveyor` / `section_settlement` as parent IDs (for JS show/hide toggling based on status)
|
||||
|
||||
---
|
||||
|
||||
## 2. Frontend Validation & JS-Only Form Submit
|
||||
|
||||
### What Changed
|
||||
Removed reliance on native HTML5 form submission. Implemented full JavaScript validation matching backend rules in `NonEbClaimController::getValidationRules()`.
|
||||
|
||||
### Form Changes
|
||||
- Added `onsubmit="return false;"` to `<form>` to prevent native submit
|
||||
- Submit button remains `type="button"` with `onclick="submitNonEbClaim()"`
|
||||
- `submitNonEbClaim()` now calls `validateNonEbForm()` first — only proceeds to AJAX if validation passes
|
||||
|
||||
### Validation CSS Added
|
||||
```css
|
||||
.is-invalid { border-color: #dc3545 !important; }
|
||||
.invalid-feedback { display: none; color: #dc3545; font-size: 0.8rem; }
|
||||
.is-invalid ~ .invalid-feedback, .is-invalid + .invalid-feedback { display: block; }
|
||||
.select2-container .select2-selection--single.is-invalid-select2 { border-color: #dc3545 !important; }
|
||||
```
|
||||
|
||||
### Validation Rules (matching backend)
|
||||
|
||||
| Field | Type | Rules |
|
||||
|---|---|---|
|
||||
| `client_select` | Select2 | Required |
|
||||
| `branch_id` | Select2 | Required |
|
||||
| `acm_id` | Select2 | Required |
|
||||
| `claim_status_id` | Select | Required |
|
||||
| `insured_contact_name` | Text | Required, min 3 chars, pattern `^[a-zA-Z0-9\s_-]+$` |
|
||||
| `insured_contact_number` | Text | Required, numeric only, 10-15 digits |
|
||||
| `insured_contact_email` | Email | Optional, email format regex |
|
||||
| `nature_of_loss` | Text | Required, min 3 chars |
|
||||
| `loss_location` | Text | Required |
|
||||
| `loss_date` | Date | Required |
|
||||
| `loss_estimate` | Number | Optional, must be numeric |
|
||||
| `claim_number` | Text | Optional, pattern `^[a-zA-Z0-9\/_-]+$` |
|
||||
| `nhance_claim_ref_no` | Text | Optional, max 100 chars |
|
||||
| `surveyor_contact_number` | Text | Optional (only if section visible), numeric, 10-15 digits |
|
||||
| `surveyor_email` | Email | Optional (only if section visible), email format |
|
||||
| `loss_assessed_value` | Number | Optional (only if section visible), numeric |
|
||||
| `settled_amount` | Number | Optional (only if section visible), numeric |
|
||||
| `settlement_utr` | Text | Optional (only if section visible), max 100 chars |
|
||||
|
||||
### HTML Validation Attributes Added
|
||||
- `minlength`, `maxlength`, `pattern`, `min`, `required` attributes on relevant input fields
|
||||
- `<div class="invalid-feedback">` added after each validated field with descriptive error message
|
||||
|
||||
### JS Validation Functions Added
|
||||
- **`clearValidation()`** — Removes `.is-invalid` and `.is-invalid-select2` from all form elements
|
||||
- **`setInvalid(field, msg)`** — Marks a field invalid with custom message
|
||||
- **`validateNonEbForm()`** — Main validation function:
|
||||
- Validates all required and optional fields per rules above
|
||||
- Select2 dropdowns: highlights the `.select2-selection` border red via `.is-invalid-select2`
|
||||
- Surveyor/Settlement fields: only validated if their parent section is visible
|
||||
- On failure: shows all errors via `toastr.error()`, expands collapsed section if first invalid field is hidden, scrolls to first invalid field and focuses it
|
||||
- Returns `true`/`false`
|
||||
|
||||
### Real-time Validation Clearing
|
||||
- `input`/`change` events on `.form-control` → removes `.is-invalid`
|
||||
- `change` event on `select` → removes both `.is-invalid` and `.is-invalid-select2` from Select2 container
|
||||
|
||||
---
|
||||
|
||||
## 3. Asset Section — File Upload with OR Divider
|
||||
|
||||
### What Changed
|
||||
Added a file upload option in the Asset Details section as an alternative to manually filling asset rows.
|
||||
|
||||
### UI Structure
|
||||
```
|
||||
[Asset Row 1: ID/Code | Serial No | Vehicle No | Description]
|
||||
[+ Add More Asset] button
|
||||
|
||||
─────────────────── OR ───────────────────
|
||||
|
||||
[Upload Asset File (Excel or PDF)] [Browse...]
|
||||
```
|
||||
|
||||
### OR Divider
|
||||
- Flexbox layout with two `<hr>` elements and centered "OR" text
|
||||
- Styled with `font-weight-bold text-muted`, `0.9rem` font size
|
||||
|
||||
### File Upload Field
|
||||
- `<input type="file" name="asset_file" id="asset_file" accept=".xlsx,.xls,.csv,.pdf">`
|
||||
- Accepts: `.xlsx`, `.xls`, `.csv`, `.pdf`
|
||||
- Error message div: `#asset_section_error` — shown when neither asset row nor file is provided
|
||||
|
||||
### Validation Logic
|
||||
On form submit, checks:
|
||||
1. **Has any asset row with data?** — Iterates all `.asset-row` elements, checks if any input has a non-empty value
|
||||
2. **Has a valid file?** — Checks if file is selected and extension is one of `.xlsx`, `.xls`, `.csv`, `.pdf`
|
||||
3. If **neither** condition met → shows error "Please fill at least one asset row or upload an asset file."
|
||||
4. If file has **invalid extension** → marks file input invalid with specific message
|
||||
|
||||
### Form Submit Changed to FormData
|
||||
- Form tag: added `enctype="multipart/form-data"`
|
||||
- `submitNonEbClaim()`: replaced `$('#nonEbClaimForm').serialize()` with `new FormData(document.getElementById('nonEbClaimForm'))`
|
||||
- AJAX call: switched from `sendAjaxRequestForGlobal()` to `$.ajax()` with `processData: false, contentType: false` to support file upload
|
||||
- Added `X-Requested-With: XMLHttpRequest` header for CI4 `$this->request->isAJAX()` detection
|
||||
|
||||
### Real-time Error Clearing
|
||||
- Asset row input change → hides `#asset_section_error`
|
||||
- File input change → removes `.is-invalid` and hides `#asset_section_error`
|
||||
|
||||
---
|
||||
|
||||
## 4. Policy Date Format Fix (Continued from Previous Session)
|
||||
|
||||
### Issue
|
||||
Policy start/end dates displayed as `2-Dec-2026 12:00 am` instead of `12-02-2026` when selecting a policy.
|
||||
|
||||
### Root Cause
|
||||
API returns dates in MySQL format `YYYY-MM-DD` (e.g., `2026-02-12`). The JS `formatDate()` function was not being applied correctly.
|
||||
|
||||
### Fix Applied
|
||||
Replaced `formatDate()` call with inline string split conversion in policy change handler:
|
||||
```js
|
||||
var startRaw = sel.attr('data-start-date') || '';
|
||||
if (startRaw && startRaw.indexOf('-') > -1) {
|
||||
var sp = startRaw.split('-');
|
||||
if (sp[0].length === 4) startRaw = sp[2] + '-' + sp[1] + '-' + sp[0];
|
||||
}
|
||||
```
|
||||
- Splits `2026-02-12` → `['2026','02','12']`
|
||||
- Checks first part is 4 digits (year)
|
||||
- Rearranges to `12-02-2026`
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `app/Views/non_eb_claim_form.php` | Full UI restyle, validation, asset file upload |
|
||||
| `app/Controllers/NonEbClaimController.php` | Minor — reverted PHP date formatting (not needed, API already returns MySQL format) |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
- Backend controller `asset_file` upload handling — implemented in both `createClaim()` and `updateClaim()`
|
||||
- The `sendAjaxRequestForGlobal()` utility was replaced with raw `$.ajax()` in submit because it doesn't support `FormData` with `processData: false`
|
||||
|
||||
---
|
||||
|
||||
## 5. Edit View (`non_eb_claim_edit.php`) — Sync with Create Form
|
||||
|
||||
### Objective
|
||||
Bring the edit view in line with the create form's UI, validation, and scripting changes from sections 1–4 above.
|
||||
|
||||
### A. CSS Changes
|
||||
|
||||
| Change | Before (Edit) | After (Edit) |
|
||||
|---|---|---|
|
||||
| Accordion CSS | Custom `.accordion`, `.accordion-content`, `.arrow`, `.rotate` | Removed — replaced by Bootstrap card/collapse |
|
||||
| `toggleAccordion()` JS | Present | Removed |
|
||||
| Validation CSS | Missing | Added `.is-invalid`, `.invalid-feedback`, `.is-invalid-select2` |
|
||||
| Readonly CSS | Missing | Added `.readonly-color`, `.readonly-select` |
|
||||
|
||||
### B. HTML Section Changes — All 9 Sections Converted to Bootstrap Card/Collapse
|
||||
|
||||
Each section changed from:
|
||||
```html
|
||||
<div class="accordion">
|
||||
<h4 onclick="toggleAccordion(this)">Title <span class="arrow rotate">▶</span></h4>
|
||||
<div class="accordion-content show">
|
||||
```
|
||||
To:
|
||||
```html
|
||||
<div id="accordionX" class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<h4 class="m-1">Title <a data-toggle="collapse" href="#collapseX"><i class="mdi mdi-chevron-down"></i></a></h4>
|
||||
<div id="collapseX" class="collapse show" data-parent="#accordionX">
|
||||
<div class="card-body">
|
||||
```
|
||||
|
||||
### Section-Specific HTML Changes
|
||||
|
||||
| Section | ID Mapping | Key Field Changes |
|
||||
|---|---|---|
|
||||
| 1. Policy & Account | accordion1 / collapseOne | Added Policy Start Date & Expiry Date fields (readonly, `readonly-color`). Insurer field gets `readonly-color`. Policy No gets `readonly-color`. All labels get `label-font-size`. |
|
||||
| 2. Insured Contact | accordion2 / collapseTwo | Added `id` attrs on inputs. Added `minlength="3"`, `pattern` on contact name. Added `minlength="10" maxlength="15" pattern="^[0-9]+$"` on contact number. Added `invalid-feedback` divs. |
|
||||
| 3. Loss / Incident | accordion3 / collapseThree | Added `id` attrs, `minlength="3"` on nature_of_loss, `min="0"` on loss_estimate. Added `invalid-feedback` divs. Date placeholder changed to `dd-mm-yyyy`. |
|
||||
| 4. Intimation & Ref | accordion4 / collapseFour | Added `id` attrs, `maxlength="100"` on nhance_claim_ref_no, `pattern` on claim_number. Added `invalid-feedback` divs. Date placeholder `dd-mm-yyyy`. |
|
||||
| 5. Status & Tracking | accordion5 / collapseFive | Labels get `label-font-size`. |
|
||||
| 6. Asset Details | accordion6 / collapseSix | Labels get `label-font-size`. |
|
||||
| 7. Surveyor Details | section_surveyor / collapseSeven | Added `minlength="10" maxlength="15" pattern="^[0-9]+$"` on contact number. Added `invalid-feedback` divs on contact number and email. `maxlength="255"` on name/contact person. |
|
||||
| 8. Documents | accordion8 / collapseEight | Labels get `label-font-size`. Date placeholder `dd-mm-yyyy`. |
|
||||
| 9. Settlement | section_settlement / collapseNine | Added `min="0"` on numeric fields, `maxlength="100"` on UTR. Added `invalid-feedback` divs. |
|
||||
|
||||
### Form Tag Changes
|
||||
- Added `onsubmit="return false;"` to prevent native submit
|
||||
|
||||
### C. JavaScript Changes
|
||||
|
||||
| Change | Details |
|
||||
|---|---|
|
||||
| Removed `toggleAccordion()` | No longer needed — Bootstrap collapse handles it |
|
||||
| Added `validateNonEbForm()` | Full JS validation matching create form — all 16+ field rules |
|
||||
| Added `clearValidation()` | Removes `.is-invalid` / `.is-invalid-select2` |
|
||||
| Added `setInvalid()` | Marks field invalid with message |
|
||||
| Added `formatDate()` | Handles MySQL YYYY-MM-DD to dd-mm-yyyy conversion |
|
||||
| Added `resetContact()` | Clears insured contact fields |
|
||||
| Added `noneb_contact_list` | Stores contact data from AJAX |
|
||||
| Branch change handler | Added contact auto-fill from `noneb_contact_list` |
|
||||
| `appendPolicies()` | Added `data-policy-type`, `data-start-date`, `data-end-date` attrs |
|
||||
| Policy change handler | Added date auto-fill (policy start/end date) with YYYY-MM-DD → dd-mm-yyyy |
|
||||
| `resetPolicy()` | Added clearing of policy start/end date fields |
|
||||
| `submitNonEbClaim()` | Now calls `validateNonEbForm()` before AJAX |
|
||||
| Real-time validation clearing | `input`/`change` on `.form-control` removes `.is-invalid`. `change` on `select` removes `.is-invalid-select2`. Asset file change clears error. |
|
||||
| flatpickr dateFormat | Changed from `d/m/Y` to `d-m-Y` to match create form |
|
||||
|
||||
---
|
||||
|
||||
## Files Modified (Updated)
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `app/Views/non_eb_claim_form.php` | Full UI restyle, validation, asset file upload (session 1) |
|
||||
| `app/Views/non_eb_claim_edit.php` | Full UI restyle sync, validation, contact auto-fill, date fields (session 2) |
|
||||
| `app/Controllers/NonEbClaimController.php` | `asset_file` upload handling in create/update |
|
||||
| `app/Models/NonEbTicketMasterModel.php` | Added `asset_file`, `branch_id` to allowedFields |
|
||||
| `app/Config/Constants.php` | Added `UPLOAD_EXT_ASSET_FILES` |
|
||||
| `db.md` | Added `branch_id`, `asset_file` columns + ALTER TABLE queries |
|
||||
|
||||
---
|
||||
|
||||
## 6. Edit View (`non_eb_claim_edit.php`) — Full Sync with Create Form (Session 3)
|
||||
|
||||
### Objective
|
||||
Complete remaining sections (3–9) and all JS that were still using the old accordion pattern. This session fully brings `non_eb_claim_edit.php` in parity with `non_eb_claim_form.php`.
|
||||
|
||||
---
|
||||
|
||||
### A. HTML — Sections 3–9 Converted to Bootstrap Card/Collapse
|
||||
|
||||
| Section | Old | New |
|
||||
|---|---|---|
|
||||
| 3. Loss / Incident Details | `<div class="accordion">` + `onclick="toggleAccordion(this)"` | `<div id="accordion3" class="mb-3">` + Bootstrap card/collapse (`collapseThree`) |
|
||||
| 4. Intimation & Claim Reference | Same old pattern | `<div id="accordion4" class="mb-3">` (`collapseFour`) |
|
||||
| 5. Status & Tracking | Same old pattern | `<div id="accordion5" class="mb-3">` (`collapseFive`) |
|
||||
| 6. Asset Details | Same old pattern | `<div id="accordion6" class="mb-3">` (`collapseSix`) |
|
||||
| 7. Surveyor Details | `<div class="accordion" id="section_surveyor">` | `<div id="section_surveyor" class="mb-3">` + inner Bootstrap card/collapse (`collapseSeven`) |
|
||||
| 8. Documents & Attachments | Same old pattern | `<div id="accordion8" class="mb-3">` (`collapseEight`) |
|
||||
| 9. Settlement Details | `<div class="accordion" id="section_settlement">` | `<div id="section_settlement" class="mb-3">` + inner Bootstrap card/collapse (`collapseNine`) |
|
||||
|
||||
### Section-Specific Field Changes
|
||||
|
||||
| Section | Field Changes |
|
||||
|---|---|
|
||||
| 3. Loss / Incident | Added `id` attrs on all inputs. Added `minlength="3"` on `nature_of_loss`. Added `min="0"` on `loss_estimate`. Added `invalid-feedback` divs. Date placeholder `dd/mm/yyyy` → `dd-mm-yyyy`. All labels get `label-font-size`. |
|
||||
| 4. Intimation | Added `id` attrs. Added `maxlength="100"` on `nhance_claim_ref_no`. Added `pattern="^[a-zA-Z0-9\/_-]+$"` on `claim_number`. Added `invalid-feedback` divs. Date placeholders `dd/mm/yyyy` → `dd-mm-yyyy`. Labels get `label-font-size`. |
|
||||
| 5. Status & Tracking | Labels get `label-font-size`. |
|
||||
| 6. Asset Details | Asset row labels get `label-font-size`. Asset file upload label gets `label-font-size`. |
|
||||
| 7. Surveyor Details | Added `minlength="10" maxlength="15" pattern="^[0-9]+$"` on contact number. Added `maxlength="255"` on name and contact person. Added `invalid-feedback` on contact number and email. Labels get `label-font-size`. |
|
||||
| 8. Documents | Labels get `label-font-size`. Date placeholder `dd/mm/yyyy` → `dd-mm-yyyy`. |
|
||||
| 9. Settlement | Added `min="0"` on `loss_assessed_value` and `settled_amount`. Added `maxlength="100"` on `settlement_utr`. Added `invalid-feedback` divs on all three. Labels get `label-font-size`. |
|
||||
|
||||
---
|
||||
|
||||
### B. JavaScript Changes
|
||||
|
||||
| Change | Details |
|
||||
|---|---|
|
||||
| Removed `toggleAccordion()` | No longer needed — Bootstrap collapse handles it |
|
||||
| `appendPolicies()` | Added `data-policy-type`, `data-start-date`, `data-end-date` attrs; also added `insurer_short_name`/`insurer_branch_code` display logic matching create form |
|
||||
| `resetPolicy()` | Added `$('#policy_start_date').val('')` and `$('#policy_end_date').val('')` |
|
||||
| Added `noneb_contact_list` | Variable to store contact data keyed by branch |
|
||||
| Removed `noneb_branch_list` | Was unused; removed |
|
||||
| Added `resetContact()` | Clears `insured_contact_name`, `insured_contact_number`, `insured_contact_email` |
|
||||
| Client change handler | Added `resetContact()` call; populates `noneb_contact_list` from `response.contact_data`; added error toastr on failure |
|
||||
| Branch change handler | Added contact auto-fill from `noneb_contact_list[branch_id]` (name, number, email) |
|
||||
| Policy change handler | Changed `.data()` to `.attr()` for custom data attributes. Added date auto-fill: reads `data-start-date`/`data-end-date`, converts YYYY-MM-DD → dd-mm-yyyy via inline split, writes to `#policy_start_date`/`#policy_end_date` |
|
||||
| Added `formatDate()` | Handles both `dd-Mon-yyyy hh:mm` and `YYYY-MM-DD` formats → `dd-mm-yyyy` |
|
||||
| Added `clearValidation()` | Removes `.is-invalid` and `.is-invalid-select2` from all form elements |
|
||||
| Added `setInvalid()` | Marks a field invalid with optional custom message |
|
||||
| Added `validateNonEbForm()` | Full validation matching backend rules — 16+ fields including conditional surveyor/settlement sections and asset row/file check |
|
||||
| `submitNonEbClaim()` | Added `if (!validateNonEbForm()) return;` before AJAX |
|
||||
| `flatpickr` dateFormat | Changed from `d/m/Y` to `d-m-Y` |
|
||||
| Select2 init | Added `$('#client_select').select2()`, `$('#branch_id').select2()`, `$('#client_policy_id').select2()` alongside existing `.select2-init` |
|
||||
| Real-time validation clearing | `input`/`change` on `.form-control` removes `.is-invalid`; `change` on `select` removes `.is-invalid` and `.is-invalid-select2`; `#asset_file` change hides `#asset_section_error`; asset row input hides `#asset_section_error` |
|
||||
|
||||
---
|
||||
|
||||
## Files Modified (Session 3)
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `app/Views/non_eb_claim_edit.php` | Sections 3–9 HTML converted to Bootstrap card/collapse; full JS sync — validation, contact auto-fill, policy date auto-fill, real-time clearing |
|
||||
100
public/dev_logs/2026-03-27.md
Normal file
100
public/dev_logs/2026-03-27.md
Normal file
@ -0,0 +1,100 @@
|
||||
# Dev Log — 2026-03-27
|
||||
|
||||
## Non-EB Claims Module — Session 4
|
||||
|
||||
---
|
||||
|
||||
### 1. Edit Page: Policy & Account Details — Read-Only (Section 1 & 2)
|
||||
|
||||
- Replaced all dropdowns (Client, Branch, Policy, ACM) in Section 1 with readonly text inputs showing stored values
|
||||
- Added hidden form inputs for `branch_id` and `acm_id` to ensure values are submitted on update
|
||||
- Removed all AJAX-based client/branch/policy population JS (appendBranches, appendPolicies, resetBranch, resetPolicy, resetContact, formatDate, existingBranchId, existingPolicyId, client_select/branch_id/client_policy_id change handlers)
|
||||
- Removed client/branch/ACM/contact validation from `validateNonEbForm()` since sections are non-editable
|
||||
- Made Section 2 (Insured Contact Details) fully readonly — all 3 inputs get `readonly` + `readonly-color` class
|
||||
|
||||
**Model:** Added `branch_name` to `getTicketDataByTicketID` select + `JOIN client_branch cb` so branch name is available in `$td`
|
||||
|
||||
---
|
||||
|
||||
### 2. Remove policy_section & policy_period
|
||||
|
||||
- Removed from `non_eb_claim_edit.php` Section 1 (already done previous session)
|
||||
- Removed `tm.policy_section`, `tm.policy_period` from `claimSearch()` select in controller
|
||||
- Removed both validation rules from `getValidationRules()`
|
||||
- Removed from `NonEbTicketMasterModel` allowedFields
|
||||
- Removed hidden `<th>` and `<td>` columns from `non_eb_claim_list.php`
|
||||
- Added commented-out DROP COLUMN statements to `db.md` for future cleanup
|
||||
|
||||
---
|
||||
|
||||
### 3. Policy Start Date & Policy Expiry Date
|
||||
|
||||
- Added `policy_start_date VARCHAR(20)` and `policy_end_date VARCHAR(20)` to `non_eb_ticket_master` (ALTER TABLE in `db.md`)
|
||||
- Added both fields to `NonEbTicketMasterModel` allowedFields
|
||||
- Edit page: populated readonly inputs with `$td['policy_start_date']` and `$td['policy_end_date']`
|
||||
- Added PHP formatting at top of edit view — if stored value is `YYYY-MM-DD`, converts to `DD-MM-YYYY` for display
|
||||
|
||||
---
|
||||
|
||||
### 4. After Successful Update — Redirect to List
|
||||
|
||||
- Changed `location.reload()` in `submitNonEbClaim()` AJAX success handler to `setTimeout(() => window.location.href = base_url + 'non-eb-claim/list', 800)` — 800ms delay so toast is visible
|
||||
|
||||
---
|
||||
|
||||
### 5. Claim Files Tab (new) — Edit Page
|
||||
|
||||
**DB changes:**
|
||||
- `claim_files`: added `ticket_type TINYINT(1) DEFAULT 1` (1=EB, 2=Non-EB) — existing EB records unaffected
|
||||
- `non_eb_ticket_master`: added `required_docs TEXT` for IR Documents checklist JSON
|
||||
|
||||
**Model changes:**
|
||||
- `ClaimFilesModel`: added `ticket_type` to allowedFields
|
||||
- `NonEbTicketMasterModel`: added `required_docs` to allowedFields
|
||||
|
||||
**Controller — 4 new methods in `NonEbClaimController`:**
|
||||
- `uploadFile()` — URL or file upload into `claim_files` with `ticket_type=2`
|
||||
- `getClaimFiles()` — fetch files for ticket filtered by `ticket_type=2`
|
||||
- `removeFile()` — soft delete (`is_active=0`) by file id
|
||||
- `saveIRDocs()` — save IR Documents JSON to `non_eb_ticket_master.required_docs`
|
||||
|
||||
**Routes — 4 new routes added to `/non-eb-claim` group:**
|
||||
```
|
||||
POST non-eb-claim/uploadFile
|
||||
POST non-eb-claim/getClaimFiles
|
||||
GET non-eb-claim/removeFile
|
||||
POST non-eb-claim/saveIRDocs
|
||||
```
|
||||
|
||||
**Edit view:**
|
||||
- Added "Claim Files" tab to nav tabs
|
||||
- Tab content includes: IR Documents checklist (with freeze toggle + save), file upload form (URL/file toggle), file list table, edit URL modal
|
||||
- Full JS: `nonebLoadClaimFiles`, `nonebCreateFileList`, `nonebAddHTMLInput`, `nonebAddFileUploadHtml`, `nonebToggleUploadType`, IR docs CRUD (`nonebLoadConfiguration`, `nonebRenderDocumentList`, `nonebCreateDocumentRow`, `nonebAddDocument`, `nonebRemoveDocument`, `nonebSaveIRDocs`, `nonebToggleActionFreeze`)
|
||||
- Reuses `downloadClaimFile` global route for file downloads
|
||||
|
||||
**Section 8 (Documents & Attachments):**
|
||||
- Commented out in both `non_eb_claim_form.php` and `non_eb_claim_edit.php` (code preserved, not deleted)
|
||||
|
||||
---
|
||||
|
||||
### 6. Trigger — `non_eb_ticket_master_after_update`
|
||||
|
||||
- Written and added to `db.md` Section 6
|
||||
- Tracks 22 fields into `ticket_history` on every UPDATE
|
||||
- TEXT fields (`loss_description`, `lor`, `surveyor_remarks`, `closure_remark`) truncated to 500 chars in history
|
||||
- Skipped set-once/bulk fields: `client_id`, `branch_id`, `insurer_id`, `client_policy_id`, `insured_contact_*`, `google_drive_links`, `documents_*`, `asset_file`, `required_docs`
|
||||
|
||||
---
|
||||
|
||||
### Files Modified Today
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `app/Models/NonEbTicketMasterModel.php` | Added branch_name join, policy_start/end_date, required_docs to allowedFields; removed policy_section/period |
|
||||
| `app/Models/ClaimFilesModel.php` | Added ticket_type to allowedFields |
|
||||
| `app/Controllers/NonEbClaimController.php` | Removed policy_section/period from query & validation; added ClaimFilesModel; added 4 new Claim Files methods |
|
||||
| `app/Config/Routes.php` | Added 4 non-eb-claim routes |
|
||||
| `app/Views/non_eb_claim_edit.php` | Section 1 & 2 read-only; date formatting; Section 8 commented out; Claim Files tab added; redirect on update |
|
||||
| `app/Views/non_eb_claim_form.php` | Section 8 commented out |
|
||||
| `app/Views/non_eb_claim_list.php` | Removed policy_section/period hidden columns |
|
||||
| `db.md` | ALTER TABLEs for policy dates, required_docs, ticket_type; trigger added as Section 6 |
|
||||
58
public/dev_logs/2026-03-30.md
Normal file
58
public/dev_logs/2026-03-30.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Dev Log — 2026-03-30
|
||||
|
||||
## Non-EB Claims API — Unit Tests
|
||||
|
||||
---
|
||||
|
||||
### 1. Added `print_r` Debug Output to All API Unit Tests
|
||||
|
||||
Added `print_r($body)` after every response body decode across all 4 test files so developers can see the actual JSON output directly in the terminal when running PHPUnit.
|
||||
|
||||
**Files modified:**
|
||||
|
||||
| File | Tests updated | Notes |
|
||||
|---|---|---|
|
||||
| `tests/unit/Api/ListClaimsTest.php` | 5 tests | Pagination tests that didn't capture `$resp` were updated to capture it and print decoded body |
|
||||
| `tests/unit/Api/ClaimHistoryTest.php` | 6 tests | `print_r($body)` added after every `$body = $this->body($resp)` |
|
||||
| `tests/unit/Api/UploadRequiredDocTest.php` | 8 tests | `print_r($body)` added after every `$body = $this->body($resp)` |
|
||||
| `tests/unit/Api/CreateClaimTest.php` | 5 remaining tests | Already had it in 2 tests; added to the remaining 5 |
|
||||
|
||||
**Pattern applied:**
|
||||
```php
|
||||
$body = $this->body($resp);
|
||||
print_r($body); // ← added
|
||||
$this->assertFalse($body['status']);
|
||||
```
|
||||
|
||||
For pagination tests (`testPerPageIsCappedAt100`, `testPerPageMinimumIsOne`, `testPageDefaultsToOne`) that previously discarded the return value:
|
||||
```php
|
||||
// before
|
||||
$ctrl->listClaims();
|
||||
|
||||
// after
|
||||
$resp = $ctrl->listClaims();
|
||||
print_r(json_decode($resp->getBody(), true));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `testReturns409OnDuplicateClaim` — Explained How It Works
|
||||
|
||||
Investigated why changing `nature_of_loss`, `loss_location`, and `client_id` in `$_POST` still always produces 409. Key findings:
|
||||
|
||||
- `nature_of_loss` / `loss_location` — not part of `checkDuplicateNonEbClaim()`, only used for validation (min length check)
|
||||
- `client_id` in `$_POST` — **never read from POST**; controller derives it from `clientPolicyModel` stub (`client_id = 35`)
|
||||
- Duplicate check only queries on: `client_id` + `loss_date` + `is_active` (+ `policy_no`)
|
||||
- `nonEbTicketModel` stub (`makeFluentStub(['id' => 55])`) ignores all `.where()` chain calls and always returns `['id' => 55]` on `->first()` — meaning "duplicate found" unconditionally
|
||||
- The only way to change the 409 outcome is to make the `nonEbTicketModel` stub return `null` on `->first()`
|
||||
|
||||
---
|
||||
|
||||
### Files Modified
|
||||
|
||||
| File | Changes |
|
||||
|---|---|
|
||||
| `tests/unit/Api/ListClaimsTest.php` | `print_r` added to all 5 test methods |
|
||||
| `tests/unit/Api/ClaimHistoryTest.php` | `print_r` added to all 6 test methods |
|
||||
| `tests/unit/Api/UploadRequiredDocTest.php` | `print_r` added to all 8 test methods |
|
||||
| `tests/unit/Api/CreateClaimTest.php` | `print_r` added to remaining 5 test methods |
|
||||
283
tests/unit/Api/ClaimHistoryTest.php
Normal file
283
tests/unit/Api/ClaimHistoryTest.php
Normal file
@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\unit\Api;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Controllers\Api\NonEbClaimApiController;
|
||||
|
||||
/**
|
||||
* Unit tests for NonEbClaimApiController::claimHistory()
|
||||
*/
|
||||
class ClaimHistoryTest extends CIUnitTestCase
|
||||
{
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
protected function makeController(?array $user = null): NonEbClaimApiController
|
||||
{
|
||||
$mockUser = $user;
|
||||
|
||||
$controller = new class($mockUser) extends NonEbClaimApiController {
|
||||
private ?array $_mockUser;
|
||||
|
||||
public function __construct(?array $u) { $this->_mockUser = $u; }
|
||||
|
||||
protected function getAuthUser(): ?array { return $this->_mockUser; }
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$controller->initController($request, $response, $logger);
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a fluent stub model.
|
||||
* $firstReturn → returned by ->first()
|
||||
* $allReturn → returned by ->findAll()
|
||||
*/
|
||||
protected function makeFluentStub(mixed $firstReturn, array $allReturn = []): object
|
||||
{
|
||||
return new class($firstReturn, $allReturn) {
|
||||
private mixed $fr;
|
||||
private array $ar;
|
||||
|
||||
public function __construct(mixed $fr, array $ar) { $this->fr = $fr; $this->ar = $ar; }
|
||||
|
||||
public function __call(string $name, array $args): mixed
|
||||
{
|
||||
if ($name === 'first') return $this->fr;
|
||||
if ($name === 'findAll') return $this->ar;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected function inject(NonEbClaimApiController $ctrl, string $property, object $stub): void
|
||||
{
|
||||
$ref = new \ReflectionClass($ctrl);
|
||||
$prop = $ref->getProperty($property);
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($ctrl, $stub);
|
||||
}
|
||||
|
||||
protected function body(\CodeIgniter\HTTP\ResponseInterface $resp): array
|
||||
{
|
||||
return json_decode($resp->getBody(), true);
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testReturns401WhenNoAuth(): void
|
||||
{
|
||||
$ctrl = $this->makeController(null);
|
||||
$resp = $ctrl->claimHistory(1);
|
||||
|
||||
$this->assertSame(401, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(401, $body['code']);
|
||||
}
|
||||
|
||||
public function testReturns404WhenClaimNotFound(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
// nonEbTicketModel->first() returns null — claim not found
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub(null));
|
||||
|
||||
$resp = $ctrl->claimHistory(999);
|
||||
|
||||
$this->assertSame(404, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(404, $body['code']);
|
||||
$this->assertStringContainsString('not found', strtolower($body['message']));
|
||||
}
|
||||
|
||||
public function testReturnsEmptyHistoryWhenNoHistoryRows(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'policy_type_id' => 10,
|
||||
]));
|
||||
|
||||
// ticketHistoryModel returns no rows
|
||||
$this->inject($ctrl, 'ticketHistoryModel', $this->makeFluentStub(null, []));
|
||||
|
||||
$resp = $ctrl->claimHistory(10);
|
||||
|
||||
$this->assertSame(200, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertTrue($body['status']);
|
||||
$this->assertSame(10, $body['claim_id']);
|
||||
$this->assertIsArray($body['history']);
|
||||
$this->assertEmpty($body['history']);
|
||||
}
|
||||
|
||||
public function testFiltersOutStatusesWithNoDisplayName(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'policy_type_id' => 10,
|
||||
]));
|
||||
|
||||
// History rows: status IDs 1, 2, 3
|
||||
$historyRows = [
|
||||
['new_value' => '1', 'created_at' => '2026-03-22 10:00:00'],
|
||||
['new_value' => '2', 'created_at' => '2026-03-23 11:00:00'],
|
||||
['new_value' => '3', 'created_at' => '2026-03-24 12:00:00'],
|
||||
];
|
||||
|
||||
// display_map: only status 1 and 3 have display_name; status 2 is internal
|
||||
$statusRows = [
|
||||
['id' => 1, 'claim_status' => 'Internal-A', 'display_name' => 'Claim Intimation'],
|
||||
['id' => 3, 'claim_status' => 'Internal-C', 'display_name' => 'Under Process'],
|
||||
];
|
||||
|
||||
// ticketHistoryModel findAll() returns history rows
|
||||
$histStub = new class($historyRows) {
|
||||
private array $rows;
|
||||
public function __construct(array $rows) { $this->rows = $rows; }
|
||||
public function __call(string $n, array $a): mixed
|
||||
{
|
||||
if ($n === 'findAll') return $this->rows;
|
||||
if ($n === 'first') return $this->rows[0] ?? null;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
|
||||
// claimStatusModel findAll() returns status rows
|
||||
$statusStub = new class($statusRows) {
|
||||
private array $rows;
|
||||
public function __construct(array $rows) { $this->rows = $rows; }
|
||||
public function __call(string $n, array $a): mixed
|
||||
{
|
||||
if ($n === 'findAll') return $this->rows;
|
||||
if ($n === 'first') return $this->rows[0] ?? null;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
|
||||
$this->inject($ctrl, 'ticketHistoryModel', $histStub);
|
||||
$this->inject($ctrl, 'claimStatusModel', $statusStub);
|
||||
|
||||
$resp = $ctrl->claimHistory(10);
|
||||
|
||||
$this->assertSame(200, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertTrue($body['status']);
|
||||
$this->assertCount(2, $body['history']);
|
||||
|
||||
// Status 2 (no display_name) must not appear
|
||||
$statuses = array_column($body['history'], 'status');
|
||||
$this->assertContains('Claim Intimation', $statuses);
|
||||
$this->assertContains('Under Process', $statuses);
|
||||
$this->assertNotContains('Internal-B', $statuses);
|
||||
}
|
||||
|
||||
public function testHistoryIsReturnedOldestFirst(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'policy_type_id' => 10,
|
||||
]));
|
||||
|
||||
$historyRows = [
|
||||
['new_value' => '1', 'created_at' => '2026-03-22 10:00:00'],
|
||||
['new_value' => '2', 'created_at' => '2026-03-25 14:00:00'],
|
||||
];
|
||||
|
||||
$statusRows = [
|
||||
['id' => 1, 'claim_status' => 'A', 'display_name' => 'Claim Intimation'],
|
||||
['id' => 2, 'claim_status' => 'B', 'display_name' => 'Under Process'],
|
||||
];
|
||||
|
||||
$histStub = new class($historyRows) {
|
||||
private array $rows;
|
||||
public function __construct(array $rows) { $this->rows = $rows; }
|
||||
public function __call(string $n, array $a): mixed
|
||||
{
|
||||
if ($n === 'findAll') return $this->rows;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
|
||||
$statusStub = new class($statusRows) {
|
||||
private array $rows;
|
||||
public function __construct(array $rows) { $this->rows = $rows; }
|
||||
public function __call(string $n, array $a): mixed
|
||||
{
|
||||
if ($n === 'findAll') return $this->rows;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
|
||||
$this->inject($ctrl, 'ticketHistoryModel', $histStub);
|
||||
$this->inject($ctrl, 'claimStatusModel', $statusStub);
|
||||
|
||||
$resp = $ctrl->claimHistory(10);
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
|
||||
$this->assertCount(2, $body['history']);
|
||||
$this->assertSame('Claim Intimation', $body['history'][0]['status']);
|
||||
$this->assertSame('Under Process', $body['history'][1]['status']);
|
||||
}
|
||||
|
||||
public function testHistoryItemHasRequiredKeys(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'policy_type_id' => 10,
|
||||
]));
|
||||
|
||||
$historyRows = [
|
||||
['new_value' => '1', 'created_at' => '2026-03-22 10:15:00'],
|
||||
];
|
||||
|
||||
$statusRows = [
|
||||
['id' => 1, 'claim_status' => 'A', 'display_name' => 'Claim Settled'],
|
||||
];
|
||||
|
||||
$histStub = new class($historyRows) {
|
||||
private array $rows;
|
||||
public function __construct(array $rows) { $this->rows = $rows; }
|
||||
public function __call(string $n, array $a): mixed { if ($n === 'findAll') return $this->rows; return $this; }
|
||||
};
|
||||
$statusStub = new class($statusRows) {
|
||||
private array $rows;
|
||||
public function __construct(array $rows) { $this->rows = $rows; }
|
||||
public function __call(string $n, array $a): mixed { if ($n === 'findAll') return $this->rows; return $this; }
|
||||
};
|
||||
|
||||
$this->inject($ctrl, 'ticketHistoryModel', $histStub);
|
||||
$this->inject($ctrl, 'claimStatusModel', $statusStub);
|
||||
|
||||
$resp = $ctrl->claimHistory(10);
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
|
||||
$item = $body['history'][0];
|
||||
$this->assertArrayHasKey('status', $item);
|
||||
$this->assertArrayHasKey('changed_at', $item);
|
||||
$this->assertArrayNotHasKey('modified_by', $item); // must not expose who changed it
|
||||
}
|
||||
}
|
||||
297
tests/unit/Api/CreateClaimTest.php
Normal file
297
tests/unit/Api/CreateClaimTest.php
Normal file
@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\unit\Api;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Controllers\Api\NonEbClaimApiController;
|
||||
|
||||
/**
|
||||
* Unit tests for NonEbClaimApiController::createClaim()
|
||||
*
|
||||
* Auth is controlled by overriding getAuthUser() in an anonymous subclass.
|
||||
* Model dependencies are stubbed via ReflectionClass property injection.
|
||||
*/
|
||||
class CreateClaimTest extends CIUnitTestCase
|
||||
{
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Build a controller subclass where getAuthUser() returns $user (or null).
|
||||
*/
|
||||
protected function makeController(?array $user = null): NonEbClaimApiController
|
||||
{
|
||||
$mockUser = $user;
|
||||
|
||||
$controller = new class($mockUser) extends NonEbClaimApiController {
|
||||
private ?array $_mockUser;
|
||||
|
||||
public function __construct(?array $mockUser)
|
||||
{
|
||||
$this->_mockUser = $mockUser;
|
||||
}
|
||||
|
||||
protected function getAuthUser(): ?array
|
||||
{
|
||||
return $this->_mockUser;
|
||||
}
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$controller->initController($request, $response, $logger);
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal fluent stub model that returns $returnValue on ->first().
|
||||
* Unknown chained method calls return $this (fluent builder pattern).
|
||||
*/
|
||||
protected function makeFluentStub(mixed $returnValue): object
|
||||
{
|
||||
return new class($returnValue) {
|
||||
private mixed $rv;
|
||||
private array $inserts = [];
|
||||
|
||||
public function __construct(mixed $rv) { $this->rv = $rv; }
|
||||
|
||||
public function __call(string $name, array $args): mixed
|
||||
{
|
||||
if ($name === 'first') return $this->rv;
|
||||
if ($name === 'findAll') return is_array($this->rv) ? $this->rv : [$this->rv];
|
||||
if ($name === 'insert') { $this->inserts[] = $args[0] ?? []; return 99; }
|
||||
if ($name === 'insertID') return 99;
|
||||
return $this; // fluent chain
|
||||
}
|
||||
|
||||
public function getInserts(): array { return $this->inserts; }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject a stub into a named property of the controller.
|
||||
*/
|
||||
protected function inject(NonEbClaimApiController $ctrl, string $property, object $stub): void
|
||||
{
|
||||
$ref = new \ReflectionClass($ctrl);
|
||||
$prop = $ref->getProperty($property);
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($ctrl, $stub);
|
||||
}
|
||||
|
||||
/** Decode response body to array. */
|
||||
protected function body(\CodeIgniter\HTTP\ResponseInterface $resp): array
|
||||
{
|
||||
return json_decode($resp->getBody(), true);
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testReturns401WhenNoAuth(): void
|
||||
{
|
||||
$ctrl = $this->makeController(null);
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(401, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(401, $body['code']);
|
||||
}
|
||||
|
||||
public function testReturns400WhenRequiredFieldsMissing(): void
|
||||
{
|
||||
$_POST = []; // empty body — all required fields missing
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'Test', 'email' => 't@t.com', 'mobile' => '9999']);
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(400, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(400, $body['code']);
|
||||
$this->assertArrayHasKey('errors', $body);
|
||||
$this->assertArrayHasKey('nature_of_loss', $body['errors']);
|
||||
}
|
||||
|
||||
public function testReturns404WhenClientPolicyNotFound(): void
|
||||
{
|
||||
$_POST = [
|
||||
'client_policy_id' => 8101,
|
||||
'nature_of_loss' => 'Fire damage',
|
||||
'loss_location' => 'Chennai',
|
||||
'loss_date' => '22-03-2026',
|
||||
];
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'User', 'email' => 'u@t.com', 'mobile' => '999']);
|
||||
|
||||
// clientPolicyModel returns null — policy not found
|
||||
$this->inject($ctrl, 'clientPolicyModel', $this->makeFluentStub(null));
|
||||
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(404, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(404, $body['code']);
|
||||
$this->assertStringContainsString('policy', strtolower($body['message']));
|
||||
}
|
||||
|
||||
public function testReturns422WhenPolicyTypeIsEB(): void
|
||||
{
|
||||
$_POST = [
|
||||
'client_policy_id' => 10,
|
||||
'nature_of_loss' => 'Theft',
|
||||
'loss_location' => 'Mumbai',
|
||||
'loss_date' => '01-03-2026',
|
||||
];
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'User', 'email' => 'u@t.com', 'mobile' => '999']);
|
||||
|
||||
$this->inject($ctrl, 'clientPolicyModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'client_branch_id' => 2,
|
||||
'policy_type_id' => 3,
|
||||
'insurer_id' => 7,
|
||||
'policy_no' => 'POL/2026/001',
|
||||
]));
|
||||
|
||||
// policyTypeModel returns EB — should be rejected
|
||||
$this->inject($ctrl, 'policyTypeModel', $this->makeFluentStub(['allocg' => 'EB']));
|
||||
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(422, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(422, $body['code']);
|
||||
}
|
||||
|
||||
public function testReturns422WhenNoClaimStatusConfigured(): void
|
||||
{
|
||||
$_POST = [
|
||||
'client_policy_id' => 10,
|
||||
'nature_of_loss' => 'Flood',
|
||||
'loss_location' => 'Kochi',
|
||||
'loss_date' => '05-03-2026',
|
||||
];
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'User', 'email' => 'u@t.com', 'mobile' => '999']);
|
||||
|
||||
$this->inject($ctrl, 'clientPolicyModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'client_branch_id' => 2,
|
||||
'policy_type_id' => 10,
|
||||
'insurer_id' => 7,
|
||||
'policy_no' => 'POL/2026/002',
|
||||
]));
|
||||
$this->inject($ctrl, 'policyTypeModel', $this->makeFluentStub(['allocg' => 'Non-EB']));
|
||||
$this->inject($ctrl, 'clientRMModel', $this->makeFluentStub(['user_id' => 4]));
|
||||
// claimStatusModel returns null — no status configured
|
||||
$this->inject($ctrl, 'claimStatusModel', $this->makeFluentStub(null));
|
||||
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(422, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertStringContainsString('status', strtolower($body['message']));
|
||||
}
|
||||
|
||||
public function testReturns409OnDuplicateClaim(): void
|
||||
{
|
||||
$_POST = [
|
||||
'client_policy_id' => 8101,
|
||||
'nature_of_loss' => 'Fire accident one edited two',
|
||||
'loss_location' => 'bjdbkkjb',
|
||||
'loss_date' => '01-03-2026',
|
||||
];
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'User', 'email' => 'u@t.com', 'mobile' => '999']);
|
||||
|
||||
$this->inject($ctrl, 'clientPolicyModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 12,
|
||||
'client_branch_id' => 2,
|
||||
'policy_type_id' => 10,
|
||||
'insurer_id' => 7,
|
||||
'policy_no' => 'POL/2026/DUPJHBVJH',
|
||||
]));
|
||||
$this->inject($ctrl, 'policyTypeModel', $this->makeFluentStub(['allocg' => 'Non-EB']));
|
||||
$this->inject($ctrl, 'clientRMModel', $this->makeFluentStub(['user_id' => 4]));
|
||||
$this->inject($ctrl, 'claimStatusModel', $this->makeFluentStub(['id' => 1]));
|
||||
|
||||
// nonEbTicketModel->first() returns existing row => duplicate detected
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub(['id' => 550]));
|
||||
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(409, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(409, $body['code']);
|
||||
$this->assertStringContainsString('Duplicate', $body['message']);
|
||||
}
|
||||
|
||||
public function testReturns200AndClaimIdOnSuccess(): void
|
||||
{
|
||||
$_POST = [
|
||||
'client_policy_id' => 10,
|
||||
'nature_of_loss' => 'Storm damage',
|
||||
'loss_location' => 'Hyderabad',
|
||||
'loss_date' => '15-03-2026',
|
||||
];
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'Test User', 'email' => 'u@t.com', 'mobile' => '9876543210']);
|
||||
|
||||
$this->inject($ctrl, 'clientPolicyModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'client_branch_id' => 2,
|
||||
'policy_type_id' => 10,
|
||||
'insurer_id' => 7,
|
||||
'policy_no' => 'POL/2026/STR',
|
||||
]));
|
||||
$this->inject($ctrl, 'policyTypeModel', $this->makeFluentStub(['allocg' => 'Marine']));
|
||||
$this->inject($ctrl, 'clientRMModel', $this->makeFluentStub(['user_id' => 4]));
|
||||
$this->inject($ctrl, 'claimStatusModel', $this->makeFluentStub(['id' => 1]));
|
||||
|
||||
// nonEbTicketModel: first() returns null (no duplicate), insert() returns 99
|
||||
$ticketStub = new class {
|
||||
private int $callCount = 0;
|
||||
public function __call(string $name, array $args): mixed
|
||||
{
|
||||
if ($name === 'first') return null; // no duplicate
|
||||
if ($name === 'insert') return 99;
|
||||
if ($name === 'insertID') return 99;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $ticketStub);
|
||||
|
||||
// Provide pass-through stubs for asset and history models
|
||||
$noopStub = new class {
|
||||
public function __call(string $n, array $a): mixed { return $this; }
|
||||
};
|
||||
$this->inject($ctrl, 'assetModel', $noopStub);
|
||||
$this->inject($ctrl, 'ticketHistoryModel', $noopStub);
|
||||
|
||||
$resp = $ctrl->createClaim();
|
||||
|
||||
$this->assertSame(200, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertTrue($body['status']);
|
||||
$this->assertSame(200, $body['code']);
|
||||
$this->assertSame(99, $body['claim_id']);
|
||||
$this->assertStringContainsString('created', strtolower($body['message']));
|
||||
}
|
||||
}
|
||||
200
tests/unit/Api/ListClaimsTest.php
Normal file
200
tests/unit/Api/ListClaimsTest.php
Normal file
@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\unit\Api;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Controllers\Api\NonEbClaimApiController;
|
||||
|
||||
/**
|
||||
* Unit tests for NonEbClaimApiController::listClaims()
|
||||
*
|
||||
* listClaims() relies on db_connect() internally, so only the pre-DB validation
|
||||
* paths (auth guard, pagination clamping, response envelope) are unit-testable
|
||||
* without a live database. The DB-dependent data path is noted separately.
|
||||
*/
|
||||
class ListClaimsTest extends CIUnitTestCase
|
||||
{
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
protected function makeController(?array $user = null): NonEbClaimApiController
|
||||
{
|
||||
$mockUser = $user;
|
||||
|
||||
$controller = new class($mockUser) extends NonEbClaimApiController {
|
||||
private ?array $_mockUser;
|
||||
|
||||
public function __construct(?array $mockUser)
|
||||
{
|
||||
$this->_mockUser = $mockUser;
|
||||
}
|
||||
|
||||
protected function getAuthUser(): ?array
|
||||
{
|
||||
return $this->_mockUser;
|
||||
}
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$controller->initController($request, $response, $logger);
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
protected function body(\CodeIgniter\HTTP\ResponseInterface $resp): array
|
||||
{
|
||||
return json_decode($resp->getBody(), true);
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testReturns401WhenNoAuth(): void
|
||||
{
|
||||
$ctrl = $this->makeController(null);
|
||||
$resp = $ctrl->listClaims();
|
||||
|
||||
$this->assertSame(401, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(401, $body['code']);
|
||||
$this->assertSame('Unauthorized', $body['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify pagination defaults: page=1, per_page=20 when body is empty.
|
||||
* We intercept just before the DB query by checking the response structure.
|
||||
* (The DB call itself will fail gracefully in unit context — we catch the
|
||||
* 500 or exception and only assert the pre-DB path works.)
|
||||
*/
|
||||
public function testDefaultPaginationValuesAreApplied(): void
|
||||
{
|
||||
$_POST = [];
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
// We only care that the auth guard passed and the method runs.
|
||||
// In a unit (no-DB) environment the db_connect() builder will throw or
|
||||
// return empty — catch both outcomes and assert auth was not the blocker.
|
||||
try {
|
||||
$resp = $ctrl->listClaims();
|
||||
$code = $resp->getStatusCode();
|
||||
// 200 with empty data is also acceptable
|
||||
$this->assertContains($code, [200, 500]);
|
||||
if ($code === 200) {
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertTrue($body['status']);
|
||||
$this->assertArrayHasKey('page', $body);
|
||||
$this->assertArrayHasKey('per_page', $body);
|
||||
$this->assertArrayHasKey('data', $body);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// DB not available in unit test — that is expected
|
||||
$this->addToAssertionCount(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* per_page is capped at 100 regardless of what the caller sends.
|
||||
*/
|
||||
public function testPerPageIsCappedAt100(): void
|
||||
{
|
||||
// We verify the cap by subclassing and exposing the computed per_page
|
||||
// without touching the DB.
|
||||
$ctrl = new class(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']) extends NonEbClaimApiController {
|
||||
private ?array $_mockUser;
|
||||
public ?int $capturedPerPage = null;
|
||||
|
||||
public function __construct(?array $u) { $this->_mockUser = $u; }
|
||||
|
||||
protected function getAuthUser(): ?array { return $this->_mockUser; }
|
||||
|
||||
public function listClaims()
|
||||
{
|
||||
// replicate per_page computation from the real method
|
||||
$body = ['per_page' => 999];
|
||||
$this->capturedPerPage = min(100, max(1, (int)($body['per_page'] ?? 20)));
|
||||
// skip DB work
|
||||
return \Config\Services::response()
|
||||
->setStatusCode(200)
|
||||
->setJSON(['status' => true, 'code' => 200, 'per_page' => $this->capturedPerPage]);
|
||||
}
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$ctrl->initController($request, $response, $logger);
|
||||
|
||||
$resp = $ctrl->listClaims();
|
||||
print_r(json_decode($resp->getBody(), true));
|
||||
|
||||
$this->assertSame(100, $ctrl->capturedPerPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* per_page minimum is 1 — a value of 0 or negative is clamped up.
|
||||
*/
|
||||
public function testPerPageMinimumIsOne(): void
|
||||
{
|
||||
$ctrl = new class(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']) extends NonEbClaimApiController {
|
||||
public ?int $capturedPerPage = null;
|
||||
|
||||
public function __construct(?array $u) {}
|
||||
|
||||
protected function getAuthUser(): ?array { return ['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']; }
|
||||
|
||||
public function listClaims()
|
||||
{
|
||||
$body = ['per_page' => -5];
|
||||
$this->capturedPerPage = min(100, max(1, (int)($body['per_page'] ?? 20)));
|
||||
return \Config\Services::response()
|
||||
->setStatusCode(200)
|
||||
->setJSON(['status' => true, 'code' => 200, 'per_page' => $this->capturedPerPage]);
|
||||
}
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$ctrl->initController($request, $response, $logger);
|
||||
$resp = $ctrl->listClaims();
|
||||
print_r(json_decode($resp->getBody(), true));
|
||||
|
||||
$this->assertSame(1, $ctrl->capturedPerPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* page defaults to 1 when not provided or <= 0.
|
||||
*/
|
||||
public function testPageDefaultsToOne(): void
|
||||
{
|
||||
$ctrl = new class extends NonEbClaimApiController {
|
||||
public ?int $capturedPage = null;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
protected function getAuthUser(): ?array { return ['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']; }
|
||||
|
||||
public function listClaims()
|
||||
{
|
||||
$body = []; // page not provided
|
||||
$this->capturedPage = max(1, (int)($body['page'] ?? 1));
|
||||
return \Config\Services::response()
|
||||
->setStatusCode(200)
|
||||
->setJSON(['status' => true, 'code' => 200, 'page' => $this->capturedPage]);
|
||||
}
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$ctrl->initController($request, $response, $logger);
|
||||
$resp = $ctrl->listClaims();
|
||||
print_r(json_decode($resp->getBody(), true));
|
||||
|
||||
$this->assertSame(1, $ctrl->capturedPage);
|
||||
}
|
||||
}
|
||||
312
tests/unit/Api/UploadRequiredDocTest.php
Normal file
312
tests/unit/Api/UploadRequiredDocTest.php
Normal file
@ -0,0 +1,312 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\unit\Api;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Controllers\Api\NonEbClaimApiController;
|
||||
|
||||
/**
|
||||
* Unit tests for NonEbClaimApiController::uploadRequiredDoc()
|
||||
*
|
||||
* File-upload and DB-transaction paths require integration tests.
|
||||
* These tests cover all validation guards that execute before those paths.
|
||||
*/
|
||||
class UploadRequiredDocTest extends CIUnitTestCase
|
||||
{
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
protected function makeController(?array $user = null): NonEbClaimApiController
|
||||
{
|
||||
$mockUser = $user;
|
||||
|
||||
$controller = new class($mockUser) extends NonEbClaimApiController {
|
||||
private ?array $_mockUser;
|
||||
|
||||
public function __construct(?array $u) { $this->_mockUser = $u; }
|
||||
|
||||
protected function getAuthUser(): ?array { return $this->_mockUser; }
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$controller->initController($request, $response, $logger);
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fluent stub: always returns $returnValue on ->first(); ignores other calls.
|
||||
*/
|
||||
protected function makeFluentStub(mixed $returnValue): object
|
||||
{
|
||||
return new class($returnValue) {
|
||||
private mixed $rv;
|
||||
public function __construct(mixed $rv) { $this->rv = $rv; }
|
||||
public function __call(string $n, array $a): mixed
|
||||
{
|
||||
if ($n === 'first') return $this->rv;
|
||||
return $this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected function inject(NonEbClaimApiController $ctrl, string $property, object $stub): void
|
||||
{
|
||||
$ref = new \ReflectionClass($ctrl);
|
||||
$prop = $ref->getProperty($property);
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($ctrl, $stub);
|
||||
}
|
||||
|
||||
protected function body(\CodeIgniter\HTTP\ResponseInterface $resp): array
|
||||
{
|
||||
return json_decode($resp->getBody(), true);
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function testReturns401WhenNoAuth(): void
|
||||
{
|
||||
$ctrl = $this->makeController(null);
|
||||
$resp = $ctrl->uploadRequiredDoc(1);
|
||||
|
||||
$this->assertSame(401, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame(401, $body['code']);
|
||||
}
|
||||
|
||||
public function testReturns404WhenClaimNotFound(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub(null));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(999);
|
||||
|
||||
$this->assertSame(404, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertSame('Claim not found', $body['message']);
|
||||
}
|
||||
|
||||
public function testReturns422WhenNoChecklistConfigured(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'required_docs' => null, // no checklist
|
||||
]));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(10);
|
||||
|
||||
$this->assertSame(422, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertStringContainsString('checklist', strtolower($body['message']));
|
||||
}
|
||||
|
||||
public function testReturns422WhenChecklistHasEmptyDocs(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'required_docs' => json_encode(['is_action_freeze' => false, 'docs' => []]),
|
||||
]));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(10);
|
||||
|
||||
$this->assertSame(422, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
}
|
||||
|
||||
public function testReturns423WhenChecklistIsFrozen(): void
|
||||
{
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'required_docs' => json_encode([
|
||||
'is_action_freeze' => true,
|
||||
'docs' => [
|
||||
['document_name' => 'Invoice Copy', 'document_received' => false],
|
||||
],
|
||||
]),
|
||||
]));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(10);
|
||||
|
||||
$this->assertSame(423, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertStringContainsString('locked', strtolower($body['message']));
|
||||
}
|
||||
|
||||
public function testReturns400WhenDocumentNameMissing(): void
|
||||
{
|
||||
$_POST = []; // document_name not set
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'required_docs' => json_encode([
|
||||
'is_action_freeze' => false,
|
||||
'docs' => [
|
||||
['document_name' => 'Invoice Copy', 'document_received' => false],
|
||||
],
|
||||
]),
|
||||
]));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(10);
|
||||
|
||||
$this->assertSame(400, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertArrayHasKey('document_name', $body['errors']);
|
||||
}
|
||||
|
||||
public function testReturns400WhenNoFileUploaded(): void
|
||||
{
|
||||
$_POST = ['document_name' => 'Invoice Copy'];
|
||||
// No file in $_FILES — getFile() will return null/invalid
|
||||
|
||||
$ctrl = $this->makeController(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']);
|
||||
|
||||
$this->inject($ctrl, 'nonEbTicketModel', $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'required_docs' => json_encode([
|
||||
'is_action_freeze' => false,
|
||||
'docs' => [
|
||||
['document_name' => 'Invoice Copy', 'document_received' => false],
|
||||
],
|
||||
]),
|
||||
]));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(10);
|
||||
|
||||
$this->assertSame(400, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertArrayHasKey('file', $body['errors']);
|
||||
}
|
||||
|
||||
public function testReturns404WhenDocumentNameNotInChecklist(): void
|
||||
{
|
||||
$_POST = ['document_name' => 'Wrong Document'];
|
||||
// Still no real file — but the doc-name check fires first
|
||||
// We need to make the file check pass; we do this via a subclass
|
||||
// that overrides the file validation.
|
||||
|
||||
$ctrl = new class(['id' => 1, 'name' => 'U', 'email' => 'u@t.com', 'mobile' => '9']) extends NonEbClaimApiController {
|
||||
private ?array $_mockUser;
|
||||
|
||||
public function __construct(?array $u) { $this->_mockUser = $u; }
|
||||
|
||||
protected function getAuthUser(): ?array { return $this->_mockUser; }
|
||||
|
||||
/**
|
||||
* Override to return a fake valid file stub so the file check passes.
|
||||
*/
|
||||
public function uploadRequiredDoc(int $claim_id)
|
||||
{
|
||||
$authUser = $this->getAuthUser();
|
||||
if (!$authUser) {
|
||||
return $this->respond(['status' => false, 'code' => 401, 'message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$claim = $this->nonEbTicketModel
|
||||
->select('id, client_id, required_docs')
|
||||
->where('id', $claim_id)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (!$claim) {
|
||||
return $this->respond(['status' => false, 'code' => 404, 'message' => 'Claim not found'], 404);
|
||||
}
|
||||
|
||||
$required_docs = json_decode($claim['required_docs'] ?? '{}', true);
|
||||
if (empty($required_docs) || empty($required_docs['docs'])) {
|
||||
return $this->respond(['status' => false, 'code' => 422, 'message' => 'No required documents checklist configured for this claim'], 422);
|
||||
}
|
||||
|
||||
if (!empty($required_docs['is_action_freeze'])) {
|
||||
return $this->respond(['status' => false, 'code' => 423, 'message' => 'Document checklist is locked for this claim'], 423);
|
||||
}
|
||||
|
||||
$document_name = trim($_POST['document_name'] ?? '');
|
||||
if (empty($document_name)) {
|
||||
return $this->respond(['status' => false, 'code' => 400, 'message' => 'Input validation failed', 'errors' => ['document_name' => 'document_name is required']], 400);
|
||||
}
|
||||
|
||||
// Simulate valid file (bypass getFile())
|
||||
$ext = 'pdf';
|
||||
$allowed = ['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx', 'xls', 'xlsx'];
|
||||
if (!in_array($ext, $allowed)) {
|
||||
return $this->respond(['status' => false, 'code' => 415, 'message' => 'Unsupported file type: ' . $ext], 415);
|
||||
}
|
||||
|
||||
// Doc name lookup
|
||||
$matched_index = null;
|
||||
foreach ($required_docs['docs'] as $i => $doc) {
|
||||
if (($doc['document_name'] ?? '') === $document_name) {
|
||||
$matched_index = $i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matched_index === null) {
|
||||
return $this->respond([
|
||||
'status' => false, 'code' => 404,
|
||||
'message' => "Document '{$document_name}' not found in required documents list",
|
||||
], 404);
|
||||
}
|
||||
|
||||
return $this->respond(['status' => true, 'code' => 200, 'message' => 'Document uploaded successfully'], 200);
|
||||
}
|
||||
};
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
$ctrl->initController($request, $response, $logger);
|
||||
|
||||
$ref = new \ReflectionClass($ctrl);
|
||||
$prop = $ref->getProperty('nonEbTicketModel');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($ctrl, $this->makeFluentStub([
|
||||
'id' => 10,
|
||||
'client_id' => 5,
|
||||
'required_docs' => json_encode([
|
||||
'is_action_freeze' => false,
|
||||
'docs' => [
|
||||
['document_name' => 'Invoice Copy', 'document_received' => false],
|
||||
],
|
||||
]),
|
||||
]));
|
||||
|
||||
$resp = $ctrl->uploadRequiredDoc(10);
|
||||
|
||||
$this->assertSame(404, $resp->getStatusCode());
|
||||
$body = $this->body($resp);
|
||||
print_r($body);
|
||||
$this->assertFalse($body['status']);
|
||||
$this->assertStringContainsString('Wrong Document', $body['message']);
|
||||
$this->assertStringContainsString('not found', strtolower($body['message']));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user