diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 14d86ad..0000000 --- a/.serena/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cache diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index 02bc3e8..0000000 --- a/.serena/project.yml +++ /dev/null @@ -1,110 +0,0 @@ -# list of languages for which language servers are started; choose from: -# al bash clojure cpp csharp -# csharp_omnisharp dart elixir elm erlang -# fortran fsharp go groovy haskell -# java julia kotlin lua markdown -# matlab nix pascal perl php -# powershell python python_jedi r rego -# ruby ruby_solargraph rust scala swift -# terraform toml typescript typescript_vts vue -# yaml zig -# (This list may be outdated. For the current list, see values of Language enum here: -# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py -# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) -# Note: -# - For C, use cpp -# - For JavaScript, use typescript -# - For Free Pascal/Lazarus, use pascal -# Special requirements: -# - csharp: Requires the presence of a .sln file in the project folder. -# - pascal: Requires Free Pascal Compiler (fpc) and optionally Lazarus. -# When using multiple languages, the first language server that supports a given file will be used for that file. -# The first language is the default language and the respective language server will be used as a fallback. -# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. -languages: -- php - -# the encoding used by text files in the project -# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings -encoding: "utf-8" - -# whether to use project's .gitignore files to ignore files -ignore_all_files_in_gitignore: true - -# list of additional paths to ignore in all projects -# same syntax as gitignore, so you can use * and ** -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" -# the name by which the project can be referenced within Serena -project_name: "clqms01" - -# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default) -included_optional_tools: [] - -# list of mode names to that are always to be included in the set of active modes -# The full set of modes to be activated is base_modes + default_modes. -# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. -# Otherwise, this setting overrides the global configuration. -# Set this to [] to disable base modes for this project. -# Set this to a list of mode names to always include the respective modes for this project. -base_modes: - -# list of mode names that are to be activated by default. -# The full set of modes to be activated is base_modes + default_modes. -# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. -# Otherwise, this overrides the setting from the global configuration (serena_config.yml). -# This setting can, in turn, be overridden by CLI parameters (--mode). -default_modes: - -# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. -# This cannot be combined with non-empty excluded_tools or included_optional_tools. -fixed_tools: [] diff --git a/app/Controllers/PatVisitController.php b/app/Controllers/PatVisitController.php index bea238f..7fc7591 100644 --- a/app/Controllers/PatVisitController.php +++ b/app/Controllers/PatVisitController.php @@ -5,6 +5,7 @@ use CodeIgniter\API\ResponseTrait; use App\Controllers\BaseController; use App\Models\PatVisit\PatVisitModel; use App\Models\PatVisit\PatVisitADTModel; +use App\Models\Patient\PatientModel; class PatVisitController extends BaseController { use ResponseTrait; @@ -15,11 +16,38 @@ class PatVisitController extends BaseController { $this->model = new PatVisitModel(); } + public function index() { + try { + $InternalPID = $this->request->getVar('InternalPID'); + $PVID = $this->request->getVar('PVID'); + + $builder = $this->model->select('patvisit.*, patient.NameFirst, patient.NameLast, patient.PatientID') + ->join('patient', 'patient.InternalPID=patvisit.InternalPID', 'left'); + + if ($InternalPID) { + $builder->where('patvisit.InternalPID', $InternalPID); + } + if ($PVID) { + $builder->where('patvisit.PVID', $PVID); + } + + $rows = $builder->orderBy('patvisit.CreateDate', 'DESC')->findAll(); + + if (empty($rows)) { + return $this->respond(['status' => 'success', 'message' => 'data not found', 'data' => []], 200); + } + + return $this->respond(['status' => 'success', 'message' => 'data found', 'data' => $rows], 200); + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + public function show($PVID = null) { try { $row = $this->model->show($PVID); if (empty($row)) { - return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => null ], 200); + return $this->respond([ 'status' => 'success', 'message'=> "data not found", 'data' => [] ], 200); } return $this->respond([ 'status' => 'success', 'message'=> "data found", 'data' => $row ], 200); } catch (\Exception $e) { @@ -41,9 +69,18 @@ class PatVisitController extends BaseController { public function update() { $input = $this->request->getJSON(true); try { - if (!$input["InternalPVID"] || !is_numeric($input["InternalPVID"])) { return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); } + if (!isset($input["InternalPVID"]) || !is_numeric($input["InternalPVID"])) { + return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); + } + + // Check if visit exists + $visit = $this->model->find($input["InternalPVID"]); + if (!$visit) { + return $this->respond(['status' => 'error', 'message' => 'Visit not found'], 404); + } + $data = $this->model->updatePatVisit($input); - return $this->respond(['status' => 'success', 'message' => 'Data updated successfully', 'data' => $data], 201); + return $this->respond(['status' => 'success', 'message' => 'Data updated successfully', 'data' => $data], 200); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } @@ -52,6 +89,18 @@ class PatVisitController extends BaseController { public function create() { $input = $this->request->getJSON(true); try { + // Validate required fields + if (!isset($input['InternalPID']) || !is_numeric($input['InternalPID'])) { + return $this->respond(['status' => 'error', 'message' => 'InternalPID is required and must be numeric'], 400); + } + + // Check if patient exists + $patientModel = new PatientModel(); + $patient = $patientModel->find($input['InternalPID']); + if (!$patient) { + return $this->respond(['status' => 'error', 'message' => 'Patient not found'], 404); + } + $data = $this->model->createPatVisit($input); return $this->respond(['status' => 'success', 'message' => 'Data created successfully', 'data' => $data], 201); } catch (\Exception $e) { @@ -59,6 +108,32 @@ class PatVisitController extends BaseController { } } + public function delete() { + $input = $this->request->getJSON(true); + try { + if (!isset($input["InternalPVID"]) || !is_numeric($input["InternalPVID"])) { + return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); + } + + // Check if visit exists + $visit = $this->model->find($input["InternalPVID"]); + if (!$visit) { + return $this->respond(['status' => 'error', 'message' => 'Visit not found'], 404); + } + + // Soft delete using EndDate (configured in model) + $result = $this->model->delete($input["InternalPVID"]); + + if ($result) { + return $this->respond(['status' => 'success', 'message' => 'Data deleted successfully'], 200); + } else { + return $this->respond(['status' => 'error', 'message' => 'Failed to delete data'], 500); + } + } catch (\Exception $e) { + return $this->failServerError('Something went wrong: ' . $e->getMessage()); + } + } + public function createADT() { $input = $this->request->getJSON(true); if (!$input["InternalPVID"] || !is_numeric($input["InternalPVID"])) { return $this->respond(['status' => 'error', 'message' => 'Invalid or missing ID'], 400); } @@ -77,7 +152,7 @@ class PatVisitController extends BaseController { $modelPVA = new PatVisitADTModel(); try { $data = $modelPVA->update($input['PVADTID'], $input); - return $this->respond(['status' => 'success', 'message' => 'Data updated successfully', 'data' => $data], 201); + return $this->respond(['status' => 'success', 'message' => 'Data updated successfully', 'data' => $data], 200); } catch (\Exception $e) { return $this->failServerError('Something went wrong: ' . $e->getMessage()); } diff --git a/app/Models/PatVisit/PatVisitModel.php b/app/Models/PatVisit/PatVisitModel.php index dfb462b..48a3484 100644 --- a/app/Models/PatVisit/PatVisitModel.php +++ b/app/Models/PatVisit/PatVisitModel.php @@ -23,7 +23,9 @@ class PatVisitModel extends BaseModel { $row = $this->select("*, patvisit.InternalPID, patvisit.CreateDate as PVCreateDate, patdiag.CreateDate as PDCreateDate, patvisitadt.CreateDate as PVACreateDate") ->join('patdiag', 'patdiag.InternalPVID=patvisit.InternalPVID and patdiag.DelDate is null', 'left') ->join('patvisitadt', 'patvisitadt.InternalPVID=patvisit.InternalPVID', 'left') - ->where('patvisit.PVID',$PVID)->first(); + ->where('patvisit.PVID',$PVID) + ->where('patvisit.EndDate IS NULL') // Exclude soft deleted + ->first(); return $row; } @@ -41,7 +43,9 @@ class PatVisitModel extends BaseModel { 'patvisitadt.InternalPVID = patvisit.InternalPVID', 'left') ->join('location', 'location.LocationID=patvisitadt.LocationID', 'left') - ->where('patvisit.InternalPID',$InternalPID)->findAll(); + ->where('patvisit.InternalPID',$InternalPID) + ->where('patvisit.EndDate IS NULL') // Exclude soft deleted + ->findAll(); return $rows; } @@ -94,6 +98,15 @@ class PatVisitModel extends BaseModel { $db = $this->db; $db->transBegin(); try{ + // Check if visit is not soft deleted before updating + $visit = $this->where('InternalPVID', $InternalPVID) + ->where('EndDate IS NULL') + ->first(); + + if (!$visit) { + throw new \Exception("Visit not found or has been deleted."); + } + $this->where('InternalPVID',$InternalPVID)->set($input)->update(); // patdiag @@ -109,24 +122,21 @@ class PatVisitModel extends BaseModel { } } if (isset($tmp) && $tmp === false) { + $error = $db->error(); + throw new \Exception("Failed to update PatDiag record. ". $error['message']); + } + + if(!empty($input['PatVisitADT'])) { + $adt = $input['PatVisitADT']; + $adt['InternalPVID'] = $InternalPVID; + $tmp = $modelPVA->insert($adt); + if ($tmp === false) { $error = $db->error(); - throw new \Exception("Failed to update PatDiag record. ". $error['message']); + throw new \Exception("Failed to update PatVisitADT record. ". $error['message']); } + } - if(!empty($input['PatVisitADT'])) { - $adtList = $input['PatVisitADT']; - usort($adtList, fn($a, $b) => $a['sequence'] <=> $b['sequence']); - foreach ($adtList as $adt) { - $adt['InternalPVID'] = $InternalPVID; - $tmp = $modelPVA->insert($adt); - if ($tmp === false) { - $error = $db->error(); - throw new \Exception("Failed to update PatVisitADT record. ". $error['message']); - } - } - } - - if ($db->transStatus() === FALSE) { + if ($db->transStatus() === FALSE) { $db->transRollback(); return false; } else { diff --git a/tests/feature/PatVisit/PatVisitCreateTest.php b/tests/feature/PatVisit/PatVisitCreateTest.php index f4366eb..200587e 100644 --- a/tests/feature/PatVisit/PatVisitCreateTest.php +++ b/tests/feature/PatVisit/PatVisitCreateTest.php @@ -54,7 +54,7 @@ class PatVisitCreateTest extends CIUnitTestCase } /** - * Test: Create patient visit with invalid (empty) data + * Test: Create patient visit with invalid (empty) data - missing InternalPID */ public function testCreatePatientVisitInvalidInput() { @@ -62,26 +62,49 @@ class PatVisitCreateTest extends CIUnitTestCase // Kirim data kosong $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(500); + $response->assertStatus(400); $response->assertJSONFragment([ - 'status' => 500, + 'status' => 'error', + 'message' => 'InternalPID is required and must be numeric' ]); } - // /** - // * Test: Simulate internal server error (trigger exception manually) - // */ - public function testCreatePatientVisitThrowsException() + /** + * Test: Create patient visit with non-existent patient + */ + public function testCreatePatientVisitPatientNotFound() { - // Gunakan input yang memicu exception, misalnya EpisodeID panjang tak wajar $payload = [ - 'InternalPID' => 1, - 'EpisodeID' => str_repeat('X', 300) // melebihi batas kolom (jika <255) + 'InternalPID' => 999999, // Non-existent patient + 'EpisodeID' => 'EPI001' ]; - $response = $this->post($this->endpoint , $payload); + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); - $response->assertStatus(500); + $response->assertStatus(404); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Patient not found' + ]); + } + + /** + * Test: Create patient visit with invalid InternalPID (non-numeric) + */ + public function testCreatePatientVisitInvalidInternalPID() + { + $payload = [ + 'InternalPID' => 'invalid', + 'EpisodeID' => 'EPI001' + ]; + + $response = $this->withBodyFormat('json')->call('post', $this->endpoint, $payload); + + $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'InternalPID is required and must be numeric' + ]); } } diff --git a/tests/feature/PatVisit/PatVisitDeleteTest.php b/tests/feature/PatVisit/PatVisitDeleteTest.php new file mode 100644 index 0000000..ab25b82 --- /dev/null +++ b/tests/feature/PatVisit/PatVisitDeleteTest.php @@ -0,0 +1,98 @@ + "1", + "EpisodeID"=> "TEST001", + "PatVisitADT"=> [ + "ADTCode"=> "A01", + "LocationID"=> "1" + ] + ]; + + $createResponse = $this->withBodyFormat('json')->call('post', $this->endpoint, $createPayload); + $createResponse->assertStatus(201); + + $json = json_decode($createResponse->getJSON(), true); + $internalPVID = $json['data']['InternalPVID']; + + // Now delete it + $deletePayload = [ + 'InternalPVID' => $internalPVID + ]; + + $response = $this->withBodyFormat('json')->call('delete', $this->endpoint, $deletePayload); + + $response->assertStatus(200); + $response->assertJSONFragment([ + 'status' => 'success', + 'message' => 'Data deleted successfully' + ]); + } + + /** + * Test: Delete patient visit with missing ID + */ + public function testDeletePatientVisitMissingId() + { + $payload = []; + + $response = $this->withBodyFormat('json')->call('delete', $this->endpoint, $payload); + $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); + } + + /** + * Test: Delete patient visit with invalid ID (non-numeric) + */ + public function testDeletePatientVisitInvalidId() + { + $payload = [ + 'InternalPVID' => 'invalid' + ]; + + $response = $this->withBodyFormat('json')->call('delete', $this->endpoint, $payload); + $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); + } + + /** + * Test: Delete patient visit with non-existent ID + */ + public function testDeletePatientVisitNotFound() + { + $payload = [ + 'InternalPVID' => 999999 // Non-existent visit + ]; + + $response = $this->withBodyFormat('json')->call('delete', $this->endpoint, $payload); + $response->assertStatus(404); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Visit not found' + ]); + } + +} diff --git a/tests/feature/PatVisit/PatVisitUpdateTest.php b/tests/feature/PatVisit/PatVisitUpdateTest.php index cccefb7..7cd6507 100644 --- a/tests/feature/PatVisit/PatVisitUpdateTest.php +++ b/tests/feature/PatVisit/PatVisitUpdateTest.php @@ -15,10 +15,27 @@ class PatVisitUpdateTest extends CIUnitTestCase */ public function testUpdatePatientVisitSuccess() { - // Sesuaikan nilai ID dengan data di database test kamu + // First create a visit to update + $createPayload = [ + "InternalPID"=> "1", + "EpisodeID"=> "TEST001", + "PatVisitADT"=> [ + "ADTCode"=> "A01", + "LocationID"=> "1" + ] + ]; + + $createResponse = $this->withBodyFormat('json')->call('post', 'api/patvisit', $createPayload); + $createResponse->assertStatus(201); + + $createJson = json_decode($createResponse->getJSON(), true); + $internalPVID = $createJson['data']['InternalPVID']; + $pvid = $createJson['data']['PVID']; + + // Now update it $payload = [ - 'InternalPVID' => 1, - 'PVID' => 'DV0001', + 'InternalPVID' => $internalPVID, + 'PVID' => $pvid, 'EpisodeID' => 'EPI001', 'PatDiag' => [ 'DiagCode' => 'A02', @@ -32,8 +49,8 @@ class PatVisitUpdateTest extends CIUnitTestCase $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - // Pastikan response sukses - $response->assertStatus(201); + // Pastikan response sukses (200 OK untuk update) + $response->assertStatus(200); // Periksa fragment JSON $response->assertJSONFragment([ @@ -44,13 +61,13 @@ class PatVisitUpdateTest extends CIUnitTestCase // Pastikan response mengandung data yang sesuai $json = json_decode($response->getJSON(), true); $this->assertArrayHasKey('data', $json); - $this->assertEquals('DV0001', $json['data']); + $this->assertEquals($pvid, $json['data']['PVID']); } /** - * Test: Update patient visit with invalid or missing ID + * Test: Update patient visit with missing ID */ - public function testUpdatePatientVisitInvalidId() + public function testUpdatePatientVisitMissingId() { // InternalPVID tidak ada $payload = [ @@ -59,21 +76,44 @@ class PatVisitUpdateTest extends CIUnitTestCase ]; $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - // Karena ID tidak ada → 500 Bad Request - $response->assertStatus(500); + // Karena ID tidak ada → 400 Bad Request + $response->assertStatus(400); $response->assertJSONFragment([ - 'status' => '500' + 'status' => 'error', + 'message' => 'Invalid or missing ID' ]); } /** - * Test: Update patient visit throws exception + * Test: Update patient visit with non-existent ID */ - public function testUpdatePatientVisitThrowsException() + public function testUpdatePatientVisitNotFound() { $payload = [ - 'InternalPVID' => 'zehahahaha', + 'InternalPVID' => 999999, // Non-existent visit + 'EpisodeID' => 'EPI001', + 'PatDiag' => [ + 'DiagCode' => 'A02', + 'DiagName' => 'Dysentery' + ] + ]; + + $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); + $response->assertStatus(404); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Visit not found' + ]); + } + + /** + * Test: Update patient visit with invalid ID (non-numeric) + */ + public function testUpdatePatientVisitInvalidId() + { + $payload = [ + 'InternalPVID' => 'invalid', 'PVID' => 'DV0001', 'EpisodeID' => 'EPI001', 'PatDiag' => [ @@ -88,17 +128,25 @@ class PatVisitUpdateTest extends CIUnitTestCase $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); } /** - * Test: Update patient visit with [] + * Test: Update patient visit with empty payload */ public function testUpdatePatientVisitInvalidInput() { $payload = []; $response = $this->withBodyFormat('json')->call('patch', $this->endpoint, $payload); - $response->assertStatus(500); + $response->assertStatus(400); + $response->assertJSONFragment([ + 'status' => 'error', + 'message' => 'Invalid or missing ID' + ]); } }