diff --git a/Modules/Internal/Http/Controllers/Api/MemberController.php b/Modules/Internal/Http/Controllers/Api/MemberController.php new file mode 100644 index 00000000..119bf1e2 --- /dev/null +++ b/Modules/Internal/Http/Controllers/Api/MemberController.php @@ -0,0 +1,188 @@ +memberEnrollmentService = $memberEnrollmentService; + } + /** + * Display a listing of the resource. + * @return Renderable + */ + public function index(Request $request, $corporate_id) + { + $benefits = Member::query() + ->filter($request->all()) + // ->where('corporate_id', $corporate_id) + ->paginate(20) + ->appends($request->all()); + + return $benefits; + } + + /** + * Show the form for creating a new resource. + * @return Renderable + */ + public function create() + { + return view('internal::create'); + } + + /** + * Store a newly created resource in storage. + * @param Request $request + * @return Renderable + */ + public function store(Request $request) + { + // + } + + /** + * Show the specified resource. + * @param int $id + * @return Renderable + */ + public function show($id) + { + return view('internal::show'); + } + + /** + * Show the form for editing the specified resource. + * @param int $id + * @return Renderable + */ + public function edit($id) + { + return view('internal::edit'); + } + + /** + * Update the specified resource in storage. + * @param Request $request + * @param int $id + * @return Renderable + */ + public function update(Request $request, $id) + { + // + } + + /** + * Remove the specified resource from storage. + * @param int $id + * @return Renderable + */ + public function destroy($id) + { + // + } + + + public function import(Request $request, $corporate_id) + { + $request->validate([ + 'file' => 'required|file|mimes:xls,xlsx,csv,txt', + ]); + $corporate = Corporate::findOrFail($corporate_id); + + $file_name = now()->getPreciseTimestamp(3).'-'.$request->file('file')->getClientOriginalName(); + $file = $request->file('file')->storeAs('temp', $file_name); + + $reader = ReaderEntityFactory::createReaderFromFile(Storage::path('temp/'.$file_name)); + $reader->open(Storage::path('temp/'.$file_name)); + + $writer = WriterEntityFactory::createXLSXWriter(); + $writer->openToFile(Storage::disk('public')->path('temp/result-'.$file_name)); + + $headers_map_to_table_fields = Member::$doc_headers_to_field_map; + + // Write Header to File + $result_headers = array_keys($headers_map_to_table_fields); + array_push($result_headers, 'Ingestion Status'); + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRow($result_headers)); + $writer->addRow($singleRow); + + $imported_member_data = 0; + $failed_member_data = []; + foreach ($reader->getSheetIterator() as $sheet) { + $doc_headers_indexes = []; + foreach ($sheet->getRowIterator() as $index => $row) { + if ($index == 1) { // First Row Must be Header + foreach ($row->getCells() as $index => $cell) { + // Clear up the string and remove all spaces + $title = $cell->getValue(); + $title = preg_replace( "/\r|\n/", " ", $title ); + $title = preg_replace('/\xc2\xa0/', " ", $title ); + $title = rtrim($title); + $title = ltrim($title); + $doc_headers_indexes[$index] = $title; + } + } else { // Next Row Should be Data + $new_member_data = []; + foreach ($row->getCells() as $header_index => $cell) { + if (isset($headers_map_to_table_fields[$doc_headers_indexes[$header_index]])) { + $new_member_data[$headers_map_to_table_fields[$doc_headers_indexes[$header_index]]] = $cell->getValue(); + } + } + + try { + $rowResponse = $this->memberEnrollmentService->handleImportRow($corporate, $new_member_data); + + // Write Success Result to File + array_push($new_member_data, 'SUCCESS'); + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRow($new_member_data)); + $writer->addRow($singleRow); + $imported_member_data++; + } catch (ImportRowException $e) { + // Write Data Validation Error to File + array_push($new_member_data, $e->getMessage()); + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRow($new_member_data)); + $writer->addRow($singleRow); + $failed_member_data[] = ['row_number' => $index, 'error' => $e->getMessage()]; + } catch (\Exception $e) { + // Write Server Error to File + array_push($new_member_data, "Server Error"); + $singleRow = WriterEntityFactory::createRow($this->memberEnrollmentService->makeResultRow($new_member_data)); + $writer->addRow($singleRow); + $failed_member_data[] = ['row_number' => $index, 'error' => $e->getMessage()]; + } + } + + } + + break; //only read first sheet + } + $reader->close(); + $writer->close(); + Storage::delete('temp/'.$file_name); + // throw(404); + + return [ + 'total_success_row' => $imported_member_data, + 'total_failed_row' => count($failed_member_data), + 'failed_row' => $failed_member_data, + 'result_file' => [ + 'url' => Storage::disk('public')->url('temp/result-'.$file_name), + 'name' => 'result-'.$file_name, + ] + ]; + } +} diff --git a/Modules/Internal/Routes/api.php b/Modules/Internal/Routes/api.php index e9ef230f..258ac329 100644 --- a/Modules/Internal/Routes/api.php +++ b/Modules/Internal/Routes/api.php @@ -7,6 +7,7 @@ use Modules\Internal\Http\Controllers\Api\CorporateBenefitController; use Modules\Internal\Http\Controllers\Api\CorporateController; use Modules\Internal\Http\Controllers\Api\CorporatePlanController; use Modules\Internal\Http\Controllers\Api\DivisionController; +use Modules\Internal\Http\Controllers\Api\MemberController; use Modules\Internal\Http\Controllers\Api\PlanController; /* @@ -57,5 +58,9 @@ Route::prefix('internal')->group(function () { Route::post('corporates/{corporate_id}/divisions', [DivisionController::class, 'store']); Route::get('corporates/{corporate_id}/divisions/{id}/edit', [DivisionController::class, 'edit']); Route::put('corporates/{corporate_id}/divisions/{id}', [DivisionController::class, 'update']); + + Route::get('corporates/{corporate_id}/members', [MemberController::class, 'index']); + Route::post('corporates/{corporate_id}/members/import', [MemberController::class, 'import']); + }); }); diff --git a/Modules/Internal/Services/MemberEnrollmentService.php b/Modules/Internal/Services/MemberEnrollmentService.php new file mode 100644 index 00000000..04df67f4 --- /dev/null +++ b/Modules/Internal/Services/MemberEnrollmentService.php @@ -0,0 +1,537 @@ +member = $member; + } + + protected function validateRow($row) + { + if (empty($row['record_type'])) { + throw new ImportRowException(__('enrollment.RECORD_TYPE_REQUIRED'), 0, null, $row); + } + + if (empty($row['payor_id'])) { + throw new ImportRowException(__('enrollment.PAYOR_ID_REQUIRED'), 0, null, $row); + } + + if (empty($row['member_id'])) { + throw new ImportRowException(__('enrollment.MEMBER_ID_REQUIRED'), 0, null, $row); + } + + if ($row['record_type'] == 'P') { + if (!empty($row['principal_id'])) { + throw new ImportRowException(__('enrollment.PRINCIPAL_ID_NOT_REQUIRED'), 0, null, $row); + } + + if (empty($row['corporate_id'])) { + throw new ImportRowException(__('enrollment.CORPORATE_ID_REQUIRED'), 0, null, $row); + } + } + + if ($row['record_type'] == 'D') { + if (empty($row['principal_id'])) { + throw new ImportRowException(__('enrollment.PRINCIPAL_ID_REQUIRED'), 0, null, $row); + } + } + + // TOOD RECORD BCA ONLY + if ($row['record_type'] == 'D' && !empty($row['branch_code'])) { + throw new ImportRowException(__('enrollment.BRANCH_CODE_NOT_REQUIRED'), 0, null, $row); + } + + // TODO BANK VALIDATION + // if ($row['record_type'] == 'D' && !empty($row['branch_code'])) { + // throw new ImportRowException(__('enrollment.BRANCH_CODE_NOT_REQUIRED'), 0, null, $row); + // } + + if (!empty($row['language']) && !in_array($row['language'], ['M', 'E', 'C', 'I', 'O'])) { + throw new ImportRowException(__('enrollment.INVALID_LANGUAGE'), 0, null, $row); + } + + if (!empty($row['type_of_work']) && !in_array($row['type_of_work'], ['0', '1', '2', '3'])) { + throw new ImportRowException(__('enrollment.INVALID_TYPE_OF_WORK'), 0, null, $row); + } + + if (!empty($row['race']) && !in_array($row['race'], ['M', 'C', 'I', 'O'])) { + throw new ImportRowException(__('enrollment.INVALID_RACE'), 0, null, $row); + } + + if (empty($row['policy_number'])) { + throw new ImportRowException(__('enrollment.POLICY_NUMBER_REQUIRED'), 0, null, $row); + } + + if (!empty($row['marital_status']) && !in_array($row['marital_status'], ['S', 'M', 'D'])) { + throw new ImportRowException(__('enrollment.INVALID_MARITAL_STATUS'), 0, null, $row); + } + + if (empty($row['member_effective_date']) ) { + throw new ImportRowException(__('enrollment.MEMBER_EFFECTIVE_REQUIRED'), 0, null, $row); + } + // TODO EFFECTIVE DATE VALIDATION + + if (empty($row['member_expiry_date']) ) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_REQUIRED'), 0, null, $row); + } + // TODO EFFECTIVE DATE VALIDATION + + // TODO FKTP VALIDATION + // TODO FKRTL VALIDATION + + if (!empty($row['marital_status']) && !in_array($row['marital_status'], ['S', 'M', 'D'])) { + throw new ImportRowException(__('enrollment.INVALID_MARITAL_STATUS'), 0, null, $row); + } + + if (empty($row['name'])) { + throw new ImportRowException(__('enrollment.NAME_REQUIRED'), 0, null, $row); + } + + if (!empty($row['telephone_mobile']) + && substr($row['telephone_mobile'], 0, 4) != '+628') { + throw new ImportRowException(__('enrollment.PHONE_INVALID'), 0, null, $row); + } + + if (!empty($row['email']) + && !filter_var($row['email'], FILTER_VALIDATE_EMAIL)) { + throw new ImportRowException(__('enrollment.EMAIL_INVALID'), 0, null, $row); + } + + if (empty($row['date_of_birth'])) { + throw new ImportRowException(__('enrollment.DATE_OF_BIRTH_REQUIRED'), 0, null, $row); + } + // TODO DOB FORMAT VALIDATION + + if (empty($row['sex'])) { + throw new ImportRowException(__('enrollment.SEX_REQUIRED'), 0, null, $row); + } + + if (empty($row['sex'])) { + throw new ImportRowException(__('enrollment.SEX_REQUIRED'), 0, null, $row); + } + } + + public function handleImportRow(Corporate $corporate, $row) + { + try { + $member_data = [ + "name" => $row['name'], + "member_id" => $row['member_id'], + "payor_id" => $row['payor_id'], + "nik" => $row['nik'], + "birth_date" => Carbon::parse($row['date_of_birth']), + "gender" => Helper::genderNormalization($row['sex']), + "language" => $row['language'], + "race" => $row['race'], + "marital_status" => $row['marital_status'], + "record_type" => $row['record_type'], + "principal_id" => $row['principal_id'], + "relation_with_principal" => $row['relationship_with_principal'], + "bpjs_class" => $row['bpjs_class'], + ]; + + switch ($row['record_mode']) { + case "1": // New Member + $member = Member::query() + ->where('member_id', $row['member_id']) + ->first(); + + if ($member) { + throw new ImportRowException(__('enrollment.MEMBER_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } else { + $member = new Member(); + } + + $memberPolicy = $member->policies() + ->where('policy_id', $row['policy_number']) + ->first(); + + if ($memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + $this->validateRow($row); + + try { + DB::beginTransaction(); + $member->fill($member_data); + if ($member->save()) { + $memberPolicy = new MemberPolicy(); + $memberPolicy->fill([ + 'member_id' => $member->id, + 'policy_id' => $row['policy_number'], + 'start' => Carbon::parse($row['member_effective_date']), + 'end' => Carbon::parse($row['member_expiry_date']), + 'status' => 'active' + ]); + $memberPolicy->save(); + + if (!empty($row['division'])) { + $division_id = CorporateDivision::where('code', $row['division_code'])->where('')->pluck('id'); + } + + $member->employeds()->create([ + 'corporate_id' => $corporate->id, + 'branch_code' => $row['branch_code'], + 'division_id' => $division_id ?? null, + 'nik' => $row['nik'] + ]); + } + DB::commit(); + } catch (\Exception $e) { + DB::rollback(); + throw new ImportRowException($e->getMessage(), $e->getCode(), $e, $row); + } + break; + case "2": // Member Information Update (Without Replacement Card) + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->with('member') + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + $memberPolicy->member->fill($member_data); + if (!$memberPolicy->member->isDirty()) { + throw new ImportRowException(__('enrollment.MEMBER_NO_CHANGE'), 0, null, $row); + } + + return $memberPolicy->member->save(); + + break; + case "3": // Member Deletion + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + return $memberPolicy->delete(); + break; + case "4": // Member Update Start and End Date + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + $memberPolicy->fill([ + 'start' => $row['start_date'], + 'end' => $row['end_date'] + ]); + + if (!$memberPolicy->isDirty()) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_NO_CHANGE'), 0, null, $row); + } + + if (Carbon::parse($row['member_effective_date']) > Carbon::parse($row['end_date'])) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_INVALID'), 0, null, $row); + } + + return $memberPolicy->save(); + + break; + case "5": // Member Renewal Policy (without card) + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if (Carbon::parse($row['member_effective_date']) > Carbon::parse($row['member_expiry_date'])) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_INVALID'), 0, null, $row); + } + + if (Carbon::parse($memberPolicy->end) > Carbon::parse($row['member_expiry_date'] + || $memberPolicy->end > Carbon::parse($row['member_expiry_date']))) { + throw new ImportRowException(__('enrollment.MEMBER_RENEWAL_STILL_ACTIVE'), 0, null, $row); + } + + $memberPolicy->fill([ + 'start' => $row['member_expiry_date'], + 'end' => $row['member_expiry_date'] + ]); + + if (!$memberPolicy->isDirty()) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_NO_CHANGE'), 0, null, $row); + } + + return $memberPolicy->save(); + break; + case "6": // Member Renewal Policy (with card) + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if (Carbon::parse($row['member_effective_date']) > Carbon::parse($row['member_expiry_date'])) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_INVALID'), 0, null, $row); + } + + if (Carbon::parse($memberPolicy->end) > Carbon::parse($row['member_expiry_date'] + || $memberPolicy->end > Carbon::parse($row['member_expiry_date']))) { + throw new ImportRowException(__('enrollment.MEMBER_RENEWAL_STILL_ACTIVE'), 0, null, $row); + } + + $memberPolicy->fill([ + 'start' => $row['member_expiry_date'], + 'end' => $row['member_expiry_date'] + ]); + + if (!$memberPolicy->isDirty()) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_NO_CHANGE'), 0, null, $row); + } + + return $memberPolicy->save(); + break; + case "7": // Requesting to Change the Unique Data of the Member + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->with('member') + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + // TODO OPTION MODE FORMAT VALIDATION + if (empty($row["option_mode"])) { + throw new ImportRowException(__('enrollment.OPTION_MODE_INVALID_FORMAT', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ( + $row['record_type'] == 'P' && + $memberPolicy->member->corporate->code == $row["corporate_id"] && + $memberPolicy->policy_id == $row['policy_number'] && + $memberPolicy->member_id == $row['member_id'] && + $memberPolicy->member->record_type == $row['record_type'] + ) { + throw new ImportRowException(__('enrollment.UNIQUE_CHANGE_PRINCIPAL_INVALID', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ( + $row['record_type'] == 'D' && + $memberPolicy->member->corporate->code == $row["corporate_id"] && + $memberPolicy->policy_id == $row['policy_number'] && + $memberPolicy->member->record_type == $row['record_type'] && + $memberPolicy->member->payor_id == $row['payor_id'] + ) { + throw new ImportRowException(__('enrollment.UNIQUE_CHANGE_DEPENDANT_INVALID', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + throw new ImportRowException(__('MODE 7 NOT HANDLED PROPERLY'), 0, null, $row); + break; + case "8": // Member Information Update (With Replacement Card) + + break; + case "9": // Member Reactivation and Personal information update (Without replacement Card) + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if (Carbon::parse($row['member_effective_date']) < now() || Carbon::parse($row['member_expiry_date']) < now()) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_MUST_BE_AFTER_TODAY'), 0, null, $row); + } + + if (Carbon::parse($row['member_effective_date']) > Carbon::parse($row['member_expiry_date'])) { + throw new ImportRowException(__('enrollment.MEMBER_EXPIRY_DATE_INVALID'), 0, null, $row); + } + + $memberPolicy->fill([ + 'start' => $row['member_effective_date'], + 'end' => $row['member_expiry_date'] + ]); + + return $memberPolicy->save(); + break; + // case "10": // No Information Available + + // break; + case "11": // Advance Renewal with OLD Card No. (PRINT) + + throw new ImportRowException(__('MODE 11 NOT HANDLED PROPERLY'), 0, null, $row); + break; + case "12": // Advance Renewal iwh NEW Card No. (PRINT) + + throw new ImportRowException(__('MODE 12 NOT HANDLED PROPERLY'), 0, null, $row); + break; + case "13": // Advance Renewal with OLD Card No. (NO PRINT) + + throw new ImportRowException(__('MODE 13 NOT HANDLED PROPERLY'), 0, null, $row); + break; + // case "14": // No Information Available + + // break; + case "15": // Lost Card / Change Card with new card number (Print) (Rarely Used) + + throw new ImportRowException(__('MODE 15 NOT HANDLED PROPERLY'), 0, null, $row); + break; + case "16": // Endorsement Plan OLD Card No. (NO PRINT) + $plan = CorporatePlan::query() + ->where('corporate_id', $corporate->id) + ->where('code', $row['plan_id']) + ->where('active', true) + ->first(); + if (!$plan) { + throw new ImportRowException(__('enrollment.PLAN_NOT_FOUND'), 0, null, $row); + } + + $memberPolicy = MemberPolicy::query() + ->where('policy_id', $row['policy_number']) + ->where('member_id', $row['member_id']) + ->first(); + + if (!$memberPolicy) { + throw new ImportRowException(__('enrollment.MEMBER_NOT_EXISTS', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + if ($memberPolicy->status != 'active') { + throw new ImportRowException(__('enrollment.MEMBER_INACTIVE', [ + 'member_id' => $row['member_id'], + 'policy_id' => $row['policy_number'] + ]), 0, null, $row); + } + + throw new ImportRowException(__('MODE 16 NOT HANDLED PROPERLY'), 0, null, $row); + break; + case "17": // Endorsement Plan OLD Card No. (PRINT) + + throw new ImportRowException(__('MODE 17 NOT HANDLED PROPERLY'), 0, null, $row); + break; + default: + throw new ImportRowException(__("enrollment.MODE_UNAVAILABLE"), 0, null, $row); + } + } catch (\Exception $e) { + throw new ImportRowException($e->getMessage(), (int) $e->getCode(), $e, $row); + } + } + + public function makeResultRow($row_data) + { + $cells = []; + foreach ($row_data as $cellValue) { + $cells[] = WriterEntityFactory::createCell($cellValue); + } + + return $cells; + } +} diff --git a/app/Exceptions/ImportRowException.php b/app/Exceptions/ImportRowException.php new file mode 100644 index 00000000..a6167832 --- /dev/null +++ b/app/Exceptions/ImportRowException.php @@ -0,0 +1,22 @@ +data = $data; + } + + public function getData() + { + return $this->data; + } +} diff --git a/app/Helpers/Helper.php b/app/Helpers/Helper.php new file mode 100644 index 00000000..a256791c --- /dev/null +++ b/app/Helpers/Helper.php @@ -0,0 +1,20 @@ +where('active', true) ->latestOfMany(); } + + public function corporatePlans() + { + return $this->hasMany(CorporatePlan::class, 'corporate_id'); + } + + public function corporateBenefits() + { + return $this->hasMany(CorporateBenefit::class, 'corporate_id'); + } + + public function corporateDivisions() + { + return $this->hasMany(CorporateDivision::class, 'corporate_id'); + } } diff --git a/app/Models/CorporateEmployee.php b/app/Models/CorporateEmployee.php new file mode 100644 index 00000000..925b6a12 --- /dev/null +++ b/app/Models/CorporateEmployee.php @@ -0,0 +1,20 @@ + "record_mode", + "Record Type" => "record_type", + "Payor ID" => "payor_id", + "Member ID" => "member_id", + "Mapping ID" => "principal_id", + "Halodoc Member ID" => "halodoc_member_id", + "Corporate ID" => "corporate_id", + "NIK" => "nik", + "Division" => "division_code", + "Branch Code" => "branch_code", + "Bank Info" => "banks_info", + "Language" => "language", + "Type of work" => "type_of_work", + "Race" => "race", + "Policy Number" => "policy_number", + "Marital Status" => "marital_status", + "Relationship" => "relationship_with_principal", + "Member's Effective Date" => "member_effective_date", + "Member's Expiry Date" => "member_expiry_date", + "Faskes FKTP (First Level Provider) or Individual preferred provider" => "faskes_fktp", + "Faskes FKRTL (Next Level Provider) or Individual group preferred provider" => "faskes_fkrtl", + "The Right Classes Room of BPJS Participants" => "bpjs_class", + "Name of Faskes" => "faskes_name", + "Rule_BPJSK ('Y' or 'N')" => "bpjsk", + "Agent Code / intermediary code" => "agent_code", + "Member Name" => "name", + "Address1" => "address1", + "Address2" => "address2", + "Address3" => "address3", + "Address4" => "address4", + "City" => "city", + "State" => "state", + "Post Code" => "post_code", + "Telephone – Mobile" => "telephone_mobile", + "Telephone – Res" => "telephone_res", + "Telephone – Office" => "telephone_office", + "NRIC" => "nric", + "Passport No" => "passport_number", + "Passport Country" => "passport_country", + "Email" => "email", + "Identification Code" => "identification_code", + "Date of Birth" => "date_of_birth", + "Sex" => "sex", + "Internal Use" => "internal_use", + "Plan-ID" => "plan_id", + "Employment-Status" => "employment_status", + "Internal Use" => "internal_use_1", + "Internal Use" => "internal_use_2", + "Internal Use" => "internal_use_3", + "Date Terminated" => "date_terminated", + "Pre Existing" => "pre_existing", + "BPJS ID" => "bpjs_id", + "Endorsement Date" => "endorsement_date", + "Remarks" => "remarks", + "Internal Use" => "internal_use_4", + "Member Since" => "member_since", + "Internal Use" => "internal_use_5", + "Policy In Force" => "policy_in_force", + "Member Suspended" => "member_suspended", + "Activation Date" => "activation_date", + "Internal Use" => "internal_use_6", + "StartNoClaim" => "start_no_claim", + "EndNoClaim" => "end_no_claim", + "Option Mode" => "option_mode", + ]; + + public function employeds() + { + return $this->hasMany(CorporateEmployee::class, 'member_id'); + } + + public function policies() + { + return $this->hasMany(MemberPolicy::class, 'member_id'); + } + + public function scopeFilter($query, array $filters) + { + $query->when($filters['search'] ?? false, function ($query, $search) { + return $query + ->where('member_id', 'like', "%" . $search . "%") + ->orWhere('payor_id', 'like', "%" . $search . "%") + ->orWhere('name', 'like', "%" . $search . "%") + ; + // ->orWhereHas('corporatePlan', function ($query) use ($search) { + // $query->where('code', 'like', "%" . $search . "%"); + // }); + }); + } } diff --git a/app/Models/MemberPolicy.php b/app/Models/MemberPolicy.php new file mode 100644 index 00000000..c4561dd1 --- /dev/null +++ b/app/Models/MemberPolicy.php @@ -0,0 +1,31 @@ +belongsTo(Policy::class, 'policy_id', 'policy_id'); + } + + public function member() + { + return $this->belongsTo(Member::class, 'member_id', 'member_id'); + } +} diff --git a/database/migrations/2022_05_23_073350_create_members_table.php b/database/migrations/2022_05_23_073350_create_members_table.php index 0224a4e6..0fbaa089 100644 --- a/database/migrations/2022_05_23_073350_create_members_table.php +++ b/database/migrations/2022_05_23_073350_create_members_table.php @@ -16,6 +16,8 @@ return new class extends Migration Schema::create('members', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->nullable()->index(); + $table->string('member_id')->nullable()->unique(); + $table->string('payor_id')->nullable(); $table->string('name_prefix')->nullable(); $table->string('name'); $table->string('name_suffix')->nullable(); @@ -25,9 +27,10 @@ return new class extends Migration $table->string('language')->nullable(); $table->string('race')->nullable(); $table->string('marital_status')->nullable(); - $table->foreignId('principle_id')->nullable()->index(); - $table->string('relation_with_principle')->nullable(); - $table->date('bpjs_class')->nullable(); + $table->string('record_type', 1)->nullable(); + $table->string('principal_id')->nullable()->index(); + $table->string('relation_with_principal')->nullable(); + $table->string('bpjs_class')->nullable(); $table->boolean('active')->default(true); $table->timestamps(); diff --git a/database/migrations/2022_06_17_024432_create_corporate_employees_table.php b/database/migrations/2022_06_17_024432_create_corporate_employees_table.php index 7ce9cf18..aa6b43a8 100644 --- a/database/migrations/2022_06_17_024432_create_corporate_employees_table.php +++ b/database/migrations/2022_06_17_024432_create_corporate_employees_table.php @@ -15,7 +15,19 @@ return new class extends Migration { Schema::create('corporate_employees', function (Blueprint $table) { $table->id(); + $table->foreignId('corporate_id')->index(); + $table->foreignId('member_id')->index(); + $table->string('branch_code')->nullable()->index(); + $table->foreignId('division_id')->nullable()->index(); + $table->string('nik')->nullable()->index(); + $table->string('status')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->unsignedBigInteger('created_by')->nullable(); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->unsignedBigInteger('deleted_by')->nullable(); }); } diff --git a/database/migrations/2022_06_21_042321_create_corporate_policies_table.php b/database/migrations/2022_06_21_042321_create_corporate_policies_table.php index de8d2aae..af7a9d23 100644 --- a/database/migrations/2022_06_21_042321_create_corporate_policies_table.php +++ b/database/migrations/2022_06_21_042321_create_corporate_policies_table.php @@ -16,7 +16,7 @@ return new class extends Migration Schema::create('corporate_policies', function (Blueprint $table) { $table->id(); $table->string('corporate_id'); - $table->string('code')->unique(); + $table->string('policy_id')->unique(); $table->decimal('total_premi', 15, 2)->nullable(); $table->decimal('minimal_deposit_percentage', 15, 2)->nullable(); $table->decimal('minimal_deposit_net', 15, 2)->nullable(); diff --git a/database/migrations/2022_07_21_121346_create_member_policies_table.php b/database/migrations/2022_07_21_121346_create_member_policies_table.php new file mode 100644 index 00000000..c2ef0b69 --- /dev/null +++ b/database/migrations/2022_07_21_121346_create_member_policies_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('policy_id'); + $table->string('member_id'); + $table->string('status')->default('active'); + $table->dateTime('start')->nullable(); + $table->dateTime('end')->nullalbe(); + + $table->timestamps(); + $table->softDeletes(); + + $table->unsignedBigInteger('created_by')->nullable(); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->unsignedBigInteger('deleted_by')->nullable(); + + $table->index(['policy_id', 'member_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('member_policies'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 71f673f0..a1df7280 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -15,5 +15,10 @@ class DatabaseSeeder extends Seeder public function run() { // \App\Models\User::factory(10)->create(); + + $this->call([ + DummyMemberSeeder::class, + DummyCorprateSeeder::class + ]); } } diff --git a/database/seeders/DummyCorporateSeeder.php b/database/seeders/DummyCorporateSeeder.php new file mode 100644 index 00000000..70ecbbac --- /dev/null +++ b/database/seeders/DummyCorporateSeeder.php @@ -0,0 +1,49 @@ + 'UNTR0001', + 'name' => 'United Tractor', + 'welcome_message' => 'Welcome to United Tractor', + 'help_text' => 'You can blah blah blah blah', + 'active' => true + ]); + + $corporate->corporatePlans()->create([ + 'code' => 'PLAN001', + 'name' => 'PLAN Name', + 'description' => 'Description of PLAN Name', + 'active' => true, + ]); + + $corporate->corporateBenefits()->create([ + 'code' => 'BENEFIT001', + 'name' => 'BENEFIT Name', + 'description' => 'Description of BENEFIT Name', + 'active' => true, + ]); + + $corporate->corporateDivisions()->create([ + 'code' => 'DIVISION001', + 'name' => 'DIVISION Name', + 'description' => 'Description of DIVISION Name', + 'active' => true, + ]); + } +} diff --git a/frontend/dashboard/src/@types/member.ts b/frontend/dashboard/src/@types/member.ts index a141bb56..c38b0640 100644 --- a/frontend/dashboard/src/@types/member.ts +++ b/frontend/dashboard/src/@types/member.ts @@ -1,5 +1,21 @@ // ---------------------------------------------------------------------- export type Member = { - + id: string, + member_id: string, + record_type: string, + payor_id: string, + user_id: string, + name_prefix: string, + name: string, + name_suffix: string, + birth_date: string, + gender: string, + language: string, + race: string, + marital_status: string, + principal_id: string, + relation_with_principal: string, + bpjs_class: string, + active: string, }; diff --git a/frontend/dashboard/src/pages/Corporates/Member/Create.tsx b/frontend/dashboard/src/pages/Corporates/Member/Create.tsx index c6a6250a..80c9b007 100644 --- a/frontend/dashboard/src/pages/Corporates/Member/Create.tsx +++ b/frontend/dashboard/src/pages/Corporates/Member/Create.tsx @@ -151,15 +151,15 @@ export default function PlanCreate() { }, { name: 'Corporate Name', - href: '/corporates/'+id, + href: '/corporates/'+corporate_id, }, { name: 'Plans', - href: '/corporates/'+id+'/plans', + href: '/corporates/'+corporate_id+'/plans', }, { name: 'Create', - href: '/corporates/'+id+'/plans/create', + href: '/corporates/'+corporate_id+'/plans/create', }, ]} /> diff --git a/frontend/dashboard/src/pages/Corporates/Member/Index.tsx b/frontend/dashboard/src/pages/Corporates/Member/Index.tsx index bd264631..1f3412ef 100644 --- a/frontend/dashboard/src/pages/Corporates/Member/Index.tsx +++ b/frontend/dashboard/src/pages/Corporates/Member/Index.tsx @@ -34,20 +34,10 @@ export default function Divisions() { ]} /> - - - - - - - - - - Corporate Detail Goes Here -   - - - + + + + ); } diff --git a/frontend/dashboard/src/pages/Corporates/Member/List.tsx b/frontend/dashboard/src/pages/Corporates/Member/List.tsx index 223f9e31..1a8e8cd4 100644 --- a/frontend/dashboard/src/pages/Corporates/Member/List.tsx +++ b/frontend/dashboard/src/pages/Corporates/Member/List.tsx @@ -13,11 +13,13 @@ import { useParams, useSearchParams } from 'react-router-dom'; import axios from '../../../utils/axios'; import { Plan } from '../../../@types/corporates'; import { LaravelPaginatedData } from '../../../@types/paginated-data'; +import { Member } from '../../../@types/member'; export default function CorporatePlanList() { const { themeStretch } = useSettings(); const { corporate_id } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); + const [importResult, setImportResult] = useState(null) function SearchInput(props: any) { // SEARCH @@ -90,13 +92,16 @@ export default function CorporatePlanList() { formData.append("file", importPlan.current?.files[0]) axios.post(`corporates/${corporate_id}/members/import`, formData ) .then(response => { + setImportResult(response.data) handleCancelImportButton(); loadDataTableData(); - alert('Succesfully read '+ response.data.total_successed_row + ' with ' + response.data.total_failed_row + ' failed rows'); }) .catch(response => { alert('Looks like something went wrong. Please check your data and try again. ' + response.message) }) + .then(x => { + console.log('motherfucker', importResult) + }) } else { alert('No File Selected') } @@ -150,6 +155,11 @@ export default function CorporatePlanList() { )} + {( importResult && + + Last Import Result : { importResult.total_success_row ?? 0 } Row Processed, { importResult.total_failed_row } Failed, Report : {importResult.result_file?.name ?? "-"} + + )} ); } @@ -161,6 +171,20 @@ export default function CorporatePlanList() { } } + const [columns, setColumns] = React.useState([ + { id: 'member_id', label: 'MemberID', minWidth: 100, align: "left" }, + { id: 'principal_id', label: 'Mapping ID', minWidth: 100, align: "left" }, + { id: 'nik', label: 'NIK', minWidth: 100, align: "left" }, + { id: 'policy_number', label: 'Policy Number', minWidth: 100, align: "left" }, + { id: 'effective_date', label: 'Effective Date', minWidth: 100, align: "left" }, + { id: 'name', label: 'Name', minWidth: 100, align: "left" }, + { id: 'nric', label: 'NRIC', minWidth: 100, align: "left" }, + { id: 'email', label: 'E-mail', minWidth: 100, align: "left" }, + { id: 'plan_id', label: 'PlanID', minWidth: 100, align: "left" }, + { id: 'termination_date', label: 'Termination Date', minWidth: 100, align: 'right' }, + { id: 'activation_date', label: 'Activation Date', minWidth: 100, align: "right" }, + ]); + // Generate the every row of the table function Row(props: { row: ReturnType }) { const { row } = props; @@ -178,54 +202,9 @@ export default function CorporatePlanList() { {open ? : } - {row.service_code} - {row.corporate_plan?.code} - {row.code} - {row.type} - {row.start} - {row.end} - {row.require_referral} - {row.referral_source} - {row.referral_duration} - {row.family_plan} - {row.family_plan_share_rules} - {row.limit_rules} - {row.layer} - {row.layer_conditions} - {row.budget_type} - {row.budget_code} - {row.budget_conditions} - {row.surgery_limit} - {row.non_surgery_limit} - {row.max_claim_limit} - {row.max_claim_count} - {row.area_limit} - {row.limit_shared_plans} - {row.limit_shared_plan_type} - {row.cashless_percentage} - {row.reimbursement_percentage} - {row.digital_percentage} - {row.co_share_m_percentage} - {row.co_share_s_percentage} - {row.co_share_c_percentage} - {row.cashless_deductible} - {row.reimbursement_deductible} - {row.digital_deductible} - {row.co_share_m_deductible} - {row.co_share_s_deductible} - {row.co_share_c_deductible} - {row.co_share_deductible_condition} - {row.msc} - {row.genders} - {row.min_age} - {row.max_age} - {row.rule_of_excess} - {row.max_excess_covered} - {row.prorate_type} - {row.prorate_lookup} - {row.currency} - {row.max_surgery_reinstatement_days} - {row.max_surgery_periode_days} + { columns.map((column, index) => + { row[column.id] ?? '-' } + ) } @@ -238,37 +217,7 @@ export default function CorporatePlanList() { No Extra Data - {false && - - Rules - - - - - Date - Customer - Amount - Total price ($) - - - - {/* {row.history ? row.history.map((historyRow) => ( */} - - {row.start} - {row.end} - {row.start} - {row.start} - {row.start} - - {/* )) - : ( - - No Data - - ) - } */} - -
-
} + {false && } @@ -296,7 +245,7 @@ export default function CorporatePlanList() { const loadDataTableData = async (appliedFilter = null) => { setDataTableLoading(true); const filter = appliedFilter ? appliedFilter : Object.fromEntries([...searchParams.entries()]); - const response = await axios.get('/corporates/'+corporate_id+'/plans', { params: filter }); + const response = await axios.get('/corporates/'+corporate_id+'/members', { params: filter }); // console.log(response.data); setDataTableLoading(false); @@ -327,54 +276,9 @@ export default function CorporatePlanList() { - Service - Plan - Code - Type - Start - End - Referral - Referral Source - Referral Duration - Family Plan - Family Sharing Overflow - Plan Limit - Layer ID - Layer Condition - Budget Type - Budget Code - Budget Condition - Surgery - Non Surgery - Max/Claim - Max Count of Claim - Area - Shared Plan - Limit Shared Type - Cashless(%) - Reimbursement(%) - Digital(%) - CoShare M(%) - CoShare S(%) - CoShare C(%) - Cashless Deductible - Reimbursement Deductible - Digital Deductible - DeductibleM - DeductibleS - DeductibleC - CoShare & Deductible Condition - MSC - Gender - Min Age - Max Age - Rule of Excess - Max Excess Covered - Prorate Type - Prorate Lookup - Currency - Reinstatement Surgery - Period of Surgery + { columns.map((column, index) => ( + {column.label} + )) } Status Action @@ -396,8 +300,8 @@ export default function CorporatePlanList() { ) : ( - {dataTableData.data.map(row => ( - + {dataTableData.data.map((row, index) => ( + ))} ) diff --git a/lang/en/enrollment.php b/lang/en/enrollment.php new file mode 100644 index 00000000..21fbc63e --- /dev/null +++ b/lang/en/enrollment.php @@ -0,0 +1,56 @@ + "Record mode must be filled", + "MODE_UNAVAILABLE" => "Record mode for member is not available", + "RECORD_TYPE_REQUIRED" => "Record Type must be filled for member (Member ID)", + "MEMBER_EXISTS" => "Member (:member_id) for policy (:policy_id) already exist in database", + "MEMBER_NOT_EXISTS" => "Member (Member ID) for policy (Policy No) not found", + "MEMBER_INACTIVE" => "Member (Member ID) for policy (Policy No) is inactive", + "MEMBER_NO_CHANGE" => "No changes in plan/ personal info found", + "MEMBER_EXPIRY_DATE_NO_CHANGE" => "No changes in member effective/ expiry date found", + "MEMBER_EXPIRY_DATE_INVALID" => "Member Effective Date must be before or equal to Member Expiry Date", + "MEMBER_RENEWAL_STILL_ACTIVE" => "Policy period is still active, please use mode 11/12/13", + "OPTION_MODE_INVALID_FORMAT" => "Option Mode must follow delimited format", + "UNIQUE_CHANGE_PRINCIPAL_INVALID" => "No changes in the following info found: + > Corporate Code (Field 7) + > Policy No (Field 15) + > Member ID (Field 4) + > Record Type (Field 2)", + "UNIQUE_CHANGE_DEPENDANT_INVALID" => "No changes in the following info found: + > Corporate Code (Field 7) + > Policy No (Field 15) + > Record Type (Field 2) + > Payor ID (Field 3)", + "MEMBER_EXPIRY_MUST_BE_AFTER_TODAY" => "Valid if Activation Date is later than member effective date, not empty and in YYYYMMDD format", + "PLAN_NOT_FOUND" => "Plan ID inactive / not found in the system", + + "PAYOR_ID_REQUIRED" => "Payor ID must be filled for member (Member ID)", + "MEMBER_ID_REQUIRED" => "Member ID must be filled", + "PRINCIPAL_ID_NOT_REQUIRED" => "Mapping ID should only be filled for dependents", + "CORPORATE_ID_REQUIRED" => "Corporate ID must be filled", + "PRINCIPAL_ID_REQUIRED" => "Mapping ID must be filled", + "BRANCH_CODE_NOT_REQUIRED" => "Dependents don't need to fill in Branch Code ", + "INVALID_LANGUAGE" => "Language (field 12) is invalid", + "INVALID_TYPE_OF_WORK" => "Type of work (field 13) is invalid", + "INVALID_RACE" => "Race (field 14) is invalid", + "POLICY_NUMBER_REQUIRED" => "Policy Number must be filled for member (Member ID)", + "MEMBER_EFFECTIVE_REQUIRED" => "Member's Effective Date must be filled for member (Member ID)", + "MEMBER_EXPIRY_REQUIRED" => "Member's Expiry Date must be filled for member (Member ID)", + "INVALID_MARITAL_STATUS" => "Marital Status (field 16) is invalid", + "NAME_REQUIRED" => "Member Name must be filled", + "PHONE_INVALID" => "Telephone - Mobile must follow +628 format", + "EMAIL_INVALID" => "Email must folllow email format e.g. xx@gmail.com", + "DATE_OF_BIRTH_REQUIRED" => "Date of Birth must be filled", + "SEX_REQUIRED" => "Sex must be filled", + +];