From 2878d784a02b466b4866a0d924979e6eb36e2e2f Mon Sep 17 00:00:00 2001 From: Annisa Rachmadiyanti Date: Mon, 5 Jan 2026 11:56:19 +0700 Subject: [PATCH] Intial commit --- cmd/api/main.go | 2 +- docs/docs.go | 2575 ++++++++++++++++- docs/swagger.json | 2575 ++++++++++++++++- docs/swagger.yaml | 1729 ++++++++++- internal/config/config.go | 8 +- internal/handlers/component/rol_component.go | 520 ++++ internal/handlers/pages/rol_pages.go | 1580 ++++++++++ internal/handlers/patient/ms_patient.go | 1286 ++++++++ .../handlers/permission/rol_permission.go | 487 ++++ internal/models/component/rol_component.go | 107 + internal/models/pages/rol_pages.go | 274 ++ internal/models/patient/ms_patient.go | 280 ++ internal/models/permission/rol_permission.go | 135 + internal/routes/v1/routes.go | 45 +- internal/utils/query/builder.go | 151 +- tools/general/services-config.yaml | 389 +-- 16 files changed, 11883 insertions(+), 260 deletions(-) create mode 100644 internal/handlers/component/rol_component.go create mode 100644 internal/handlers/pages/rol_pages.go create mode 100644 internal/handlers/patient/ms_patient.go create mode 100644 internal/handlers/permission/rol_permission.go create mode 100644 internal/models/component/rol_component.go create mode 100644 internal/models/pages/rol_pages.go create mode 100644 internal/models/patient/ms_patient.go create mode 100644 internal/models/permission/rol_permission.go diff --git a/cmd/api/main.go b/cmd/api/main.go index 8f4e5c3..d7de342 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -28,7 +28,7 @@ import ( // @license.name Apache 2.0 // @license.url http://www.apache.org/licenses/LICENSE-2.0.html -// @host localhost:8080 +// @host localhost:8084 // @BasePath /api/v1 // @schemes http https diff --git a/docs/docs.go b/docs/docs.go index 6060163..25973eb 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -214,6 +214,492 @@ const docTemplate = `{ } } }, + "/api/v1/pasien/": { + "get": { + "description": "Get list of pasien with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new pasien", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Create Pasien", + "parameters": [ + { + "description": "Pasien creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Pasien created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/:id": { + "get": { + "description": "Get pasien by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by ID", + "parameters": [ + { + "type": "string", + "description": "Pasien ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetByIDResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Pasien not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/:nomr": { + "put": { + "description": "Update an existing pasien", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Update Pasien", + "parameters": [ + { + "type": "string", + "description": "Pasien ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pasien update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Pasien updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Pasien not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a pasien", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Delete Pasien", + "parameters": [ + { + "type": "string", + "description": "Pasien ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Pasien deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Pasien not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/by-age": { + "get": { + "description": "Get pasien statistics by age group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by Age Group", + "parameters": [ + { + "type": "string", + "description": "Age group (child, teen, adult, senior)", + "name": "age_group", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Statistics data", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienAgeStatsResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/by-location": { + "get": { + "description": "Get pasien by location (provinsi, kota, kecamatan, kelurahan)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by Location", + "parameters": [ + { + "type": "integer", + "description": "Filter by kelurahan ID", + "name": "kelurahan", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by kdkecamatan ID", + "name": "kdkecamatan", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by kota ID", + "name": "kota", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by kdprovinsi ID", + "name": "kdprovinsi", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/dynamic": { + "get": { + "description": "Get pasien with dynamic filtering", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien Dynamic", + "parameters": [ + { + "type": "string", + "description": "Fields to select (e.g., fields=*.*)", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "Dynamic filters (e.g., filter[name][_eq]=value)", + "name": "filter[column][operator]", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields (e.g., sort=date_created,-name)", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/nomr/:nomr": { + "get": { + "description": "Get pasien by Nomr", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by Nomr", + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetByNomrResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, "/api/v1/retribusi/{id}": { "get": { "description": "Returns a single retribusi by ID", @@ -601,6 +1087,32 @@ const docTemplate = `{ } } }, + "/api/v1/retribusis/welcome": { + "get": { + "description": "Returns a welcome message and logs the request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get welcome message", + "responses": { + "200": { + "description": "Welcome message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/token/generate": { "post": { "description": "Generate a JWT token for testing purposes", @@ -688,6 +1200,869 @@ const docTemplate = `{ } } } + }, + "/component": { + "get": { + "description": "Get list of components with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Get Components List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by parent page ID", + "name": "page_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentsGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new component", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Create Component", + "parameters": [ + { + "description": "Component creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Component created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/component/{id}": { + "put": { + "description": "Update an existing component", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Update Component", + "parameters": [ + { + "type": "string", + "description": "Component ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Component update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Component updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Component not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a component", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Delete Component", + "parameters": [ + { + "type": "string", + "description": "Component ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Component deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Component not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/pages": { + "get": { + "description": "Get list of pages with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Get Pages List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Create Page", + "parameters": [ + { + "description": "Rol_pages creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Rol_pages created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/pages/{id}": { + "put": { + "description": "Update an existing page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Update Page", + "parameters": [ + { + "type": "string", + "description": "Rol_pages ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Rol_pages update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Rol_pages updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Rol_pages not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Delete Page", + "parameters": [ + { + "type": "string", + "description": "Rol_pages ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Rol_pages deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Rol_pages not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/patient": { + "get": { + "description": "Returns a paginated list of patients with optional summary statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Get patient with pagination and optional aggregation", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Include aggregation summary", + "name": "include_summary", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a new patient record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Create patient", + "parameters": [ + { + "description": "Patient creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Patient created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/patient/{id}": { + "put": { + "description": "Updates an existing patient record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Update patient", + "parameters": [ + { + "type": "string", + "description": "Medical Record Number", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Patient update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Patient updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Patient not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Soft deletes a patient by setting status to 'deleted'", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Delete patient", + "parameters": [ + { + "type": "string", + "description": "Medical Record Number", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Patient deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Patient not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/permission": { + "get": { + "description": "Get list of permissions with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Get Permissions List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by parent page ID", + "name": "page_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionsGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new permission", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Create Permission", + "parameters": [ + { + "description": "Permission creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Permission created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/permission/{id}": { + "put": { + "description": "Update an existing permission", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Update Permission", + "parameters": [ + { + "type": "string", + "description": "Permission ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Permission update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Permission updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Permission not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a permission", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Delete Permission", + "parameters": [ + { + "type": "string", + "description": "Permission ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Permission deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Permission not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -859,6 +2234,1142 @@ const docTemplate = `{ } } }, + "api-service_internal_models_component.ComponentCreateRequest": { + "type": "object", + "required": [ + "directory", + "fk_rol_pages_id", + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "type": "string", + "maxLength": 255 + }, + "directory": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "fk_rol_pages_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_component.ComponentCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_component.Rol_component" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_component.ComponentDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_component.ComponentUpdateRequest": { + "type": "object", + "required": [ + "directory", + "fk_rol_pages_id", + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "type": "string", + "maxLength": 255 + }, + "directory": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "fk_rol_pages_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_component.ComponentUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_component.Rol_component" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_component.ComponentsGetResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_component.Rol_component" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + } + } + }, + "api-service_internal_models_component.Rol_component": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "$ref": "#/definitions/sql.NullString" + }, + "directory": { + "type": "string" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_pages.PagesCreateRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "icon": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "id": { + "description": "Status string ` + "`" + `json:\"status\" validate:\"required,oneof=draft active inactive\"` + "`" + `", + "type": "integer" + }, + "level": { + "type": "integer" + }, + "name": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "parent": { + "type": "integer", + "minimum": 1 + }, + "sort": { + "type": "integer" + }, + "url": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + } + }, + "api-service_internal_models_pages.PagesCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_pages" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pages.PagesDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pages.PagesGetResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_pages" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_pages.PagesUpdateRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "icon": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "level": { + "type": "integer" + }, + "name": { + "description": "Status string ` + "`" + `json:\"status\" validate:\"required,oneof=draft active inactive\"` + "`" + `", + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "parent": { + "type": "integer", + "minimum": 1 + }, + "sort": { + "type": "integer" + }, + "url": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + } + }, + "api-service_internal_models_pages.PagesUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_pages" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pages.Rol_component": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "$ref": "#/definitions/sql.NullString" + }, + "directory": { + "type": "string" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_pages.Rol_pages": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "icon": { + "$ref": "#/definitions/sql.NullString" + }, + "id": { + "type": "integer" + }, + "level": { + "type": "integer" + }, + "list_component": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_component" + } + }, + "list_permission": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_permission" + } + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "sort": { + "type": "integer" + }, + "url": { + "$ref": "#/definitions/sql.NullString" + } + } + }, + "api-service_internal_models_pages.Rol_permission": { + "type": "object", + "properties": { + "active": { + "$ref": "#/definitions/sql.NullBool" + }, + "create": { + "$ref": "#/definitions/sql.NullBool" + }, + "delete": { + "$ref": "#/definitions/sql.NullBool" + }, + "disable": { + "description": "Note: \"disable\" is a Go keyword, so \"Disable\" is used for the field name.", + "allOf": [ + { + "$ref": "#/definitions/sql.NullBool" + } + ] + }, + "fk_rol_pages_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "read": { + "$ref": "#/definitions/sql.NullBool" + }, + "role_keycloak": { + "description": "Use NullString for optional text fields", + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "$ref": "#/definitions/sql.NullBool" + } + } + }, + "api-service_internal_models_pasien.Pasien": { + "type": "object", + "properties": { + "agama": { + "$ref": "#/definitions/sql.NullInt32" + }, + "alamat": { + "$ref": "#/definitions/sql.NullString" + }, + "createdAt": { + "$ref": "#/definitions/sql.NullTime" + }, + "id": { + "type": "integer" + }, + "idkecamatan": { + "type": "integer" + }, + "idkelurahan": { + "type": "integer" + }, + "idkota": { + "type": "integer" + }, + "idprovinsi": { + "type": "integer" + }, + "jeniskelamin": { + "$ref": "#/definitions/sql.NullString" + }, + "kdkecamatan": { + "$ref": "#/definitions/sql.NullInt32" + }, + "kdprovinsi": { + "$ref": "#/definitions/sql.NullInt32" + }, + "kelurahan": { + "$ref": "#/definitions/sql.NullInt64" + }, + "kota": { + "$ref": "#/definitions/sql.NullInt32" + }, + "nama": { + "$ref": "#/definitions/sql.NullString" + }, + "namakecamatan": { + "$ref": "#/definitions/sql.NullString" + }, + "namakelurahan": { + "$ref": "#/definitions/sql.NullString" + }, + "namakota": { + "$ref": "#/definitions/sql.NullString" + }, + "namaprovinsi": { + "$ref": "#/definitions/sql.NullString" + }, + "noKartu": { + "$ref": "#/definitions/sql.NullString" + }, + "noktpBaru": { + "$ref": "#/definitions/sql.NullString" + }, + "nomr": { + "$ref": "#/definitions/sql.NullString" + }, + "status": { + "$ref": "#/definitions/sql.NullString" + }, + "tempat": { + "$ref": "#/definitions/sql.NullString" + }, + "tgllahir": { + "$ref": "#/definitions/sql.NullTime" + }, + "title": { + "$ref": "#/definitions/sql.NullString" + }, + "updatedAt": { + "$ref": "#/definitions/sql.NullTime" + } + } + }, + "api-service_internal_models_pasien.PasienAgeStatsResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienCreateRequest": { + "type": "object", + "required": [ + "nama", + "status", + "title" + ], + "properties": { + "agama": { + "type": "integer" + }, + "alamat": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "jeniskelamin": { + "type": "string", + "enum": [ + "L", + "P" + ] + }, + "kdkecamatan": { + "type": "integer" + }, + "kdprovinsi": { + "type": "integer" + }, + "kelurahan": { + "type": "integer" + }, + "kota": { + "type": "integer" + }, + "nama": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "noKartu": { + "type": "string" + }, + "noktpBaru": { + "type": "string" + }, + "nomr": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tempat": { + "type": "string" + }, + "tgllahir": { + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + } + }, + "api-service_internal_models_pasien.PasienCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienGetByIDResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienGetByNomrResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_pasien.PasienGetResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_pasien.PasienUpdateRequest": { + "type": "object", + "required": [ + "nama", + "status" + ], + "properties": { + "agama": { + "type": "integer" + }, + "alamat": { + "type": "string" + }, + "jeniskelamin": { + "type": "string", + "enum": [ + "L", + "P" + ] + }, + "kdkecamatan": { + "type": "integer" + }, + "kdprovinsi": { + "type": "integer" + }, + "kelurahan": { + "type": "integer" + }, + "kota": { + "type": "integer" + }, + "nama": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "noKartu": { + "type": "string" + }, + "noktpBaru": { + "type": "string" + }, + "nomr": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tempat": { + "type": "string" + }, + "tgllahir": { + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 255, + "minLength": 1 + } + } + }, + "api-service_internal_models_pasien.PasienUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_patient.Patient": { + "type": "object", + "properties": { + "active": { + "$ref": "#/definitions/sql.NullBool" + }, + "address": { + "$ref": "#/definitions/sql.NullString" + }, + "birth_date": { + "$ref": "#/definitions/sql.NullTime" + }, + "ds_sd_kabupaten_kota": { + "$ref": "#/definitions/sql.NullString" + }, + "ds_sd_kecamatan": { + "$ref": "#/definitions/sql.NullString" + }, + "ds_sd_kelurahan": { + "$ref": "#/definitions/sql.NullString" + }, + "ds_sd_provinsi": { + "$ref": "#/definitions/sql.NullString" + }, + "fk_sd_kabupaten_kota_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "fk_sd_kecamatan_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "fk_sd_kelurahan_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "fk_sd_provinsi_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "gender": { + "$ref": "#/definitions/sql.NullString" + }, + "id": { + "type": "integer" + }, + "medical_record_number": { + "$ref": "#/definitions/sql.NullString" + }, + "name": { + "$ref": "#/definitions/sql.NullString" + }, + "phone_number": { + "$ref": "#/definitions/sql.NullString" + } + } + }, + "api-service_internal_models_patient.PatientCreateRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "address": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "birth_date": { + "type": "string" + }, + "ds_sd_kabupaten_kota": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "ds_sd_kecamatan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "ds_sd_kelurahan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "ds_sd_provinsi": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "fk_sd_kabupaten_kota_id": { + "type": "integer" + }, + "fk_sd_kecamatan_id": { + "type": "integer" + }, + "fk_sd_kelurahan_id": { + "type": "integer" + }, + "fk_sd_provinsi_id": { + "type": "integer" + }, + "gender": { + "type": "string", + "maxLength": 1 + }, + "id": { + "type": "integer" + }, + "medical_record_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "phone_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + } + } + }, + "api-service_internal_models_patient.PatientCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_patient.Patient" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_patient.PatientDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "medical_record_number": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_patient.PatientGetResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_patient.Patient" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_patient.PatientUpdateRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "active": { + "type": "boolean" + }, + "address": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "birth_date": { + "type": "string" + }, + "ds_sd_kabupaten_kota": { + "type": "string", + "maxLength": 255 + }, + "ds_sd_kecamatan": { + "type": "string", + "maxLength": 255 + }, + "ds_sd_kelurahan": { + "type": "string", + "maxLength": 255 + }, + "ds_sd_provinsi": { + "type": "string", + "maxLength": 255 + }, + "fk_sd_kabupaten_kota_id": { + "type": "integer" + }, + "fk_sd_kecamatan_id": { + "type": "integer" + }, + "fk_sd_kelurahan_id": { + "type": "integer" + }, + "fk_sd_provinsi_id": { + "type": "integer" + }, + "gender": { + "type": "string", + "maxLength": 1 + }, + "id": { + "type": "integer", + "minimum": 1 + }, + "medical_record_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "phone_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + } + } + }, + "api-service_internal_models_patient.PatientUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_patient.Patient" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionCreateRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "delete": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "read": { + "type": "boolean" + }, + "role_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "type": "boolean" + } + } + }, + "api-service_internal_models_permission.PermissionCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_permission.Rol_permission" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionUpdateRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "delete": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "read": { + "type": "boolean" + }, + "role_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "type": "boolean" + } + } + }, + "api-service_internal_models_permission.PermissionUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_permission.Rol_permission" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionsGetResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_permission.Rol_permission" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + } + } + }, + "api-service_internal_models_permission.Rol_permission": { + "type": "object", + "properties": { + "active": { + "$ref": "#/definitions/sql.NullBool" + }, + "create": { + "$ref": "#/definitions/sql.NullBool" + }, + "delete": { + "$ref": "#/definitions/sql.NullBool" + }, + "disable": { + "description": "\"disable\" is a Go keyword, so \"Disable\" is used for the field name.", + "allOf": [ + { + "$ref": "#/definitions/sql.NullBool" + } + ] + }, + "fk_rol_pages_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "read": { + "$ref": "#/definitions/sql.NullBool" + }, + "role_keycloak": { + "description": "Use NullString for optional text fields", + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "$ref": "#/definitions/sql.NullBool" + } + } + }, "api-service_internal_models_retribusi.Retribusi": { "type": "object", "properties": { @@ -1142,6 +3653,68 @@ const docTemplate = `{ "type": "string" } } + }, + "sql.NullBool": { + "type": "object", + "properties": { + "bool": { + "type": "boolean" + }, + "valid": { + "description": "Valid is true if Bool is not NULL", + "type": "boolean" + } + } + }, + "sql.NullInt32": { + "type": "object", + "properties": { + "int32": { + "type": "integer", + "format": "int32" + }, + "valid": { + "description": "Valid is true if Int32 is not NULL", + "type": "boolean" + } + } + }, + "sql.NullInt64": { + "type": "object", + "properties": { + "int64": { + "type": "integer", + "format": "int64" + }, + "valid": { + "description": "Valid is true if Int64 is not NULL", + "type": "boolean" + } + } + }, + "sql.NullString": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "valid": { + "description": "Valid is true if String is not NULL", + "type": "boolean" + } + } + }, + "sql.NullTime": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } } } }` @@ -1149,7 +3722,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ Version: "1.0.0", - Host: "localhost:8080", + Host: "localhost:8084", BasePath: "/api/v1", Schemes: []string{"http", "https"}, Title: "API Service", diff --git a/docs/swagger.json b/docs/swagger.json index 87661e0..c62739c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -19,7 +19,7 @@ }, "version": "1.0.0" }, - "host": "localhost:8080", + "host": "localhost:8084", "basePath": "/api/v1", "paths": { "/api/v1/auth/login": { @@ -212,6 +212,492 @@ } } }, + "/api/v1/pasien/": { + "get": { + "description": "Get list of pasien with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new pasien", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Create Pasien", + "parameters": [ + { + "description": "Pasien creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Pasien created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/:id": { + "get": { + "description": "Get pasien by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by ID", + "parameters": [ + { + "type": "string", + "description": "Pasien ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetByIDResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Pasien not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/:nomr": { + "put": { + "description": "Update an existing pasien", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Update Pasien", + "parameters": [ + { + "type": "string", + "description": "Pasien ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Pasien update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Pasien updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Pasien not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a pasien", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Delete Pasien", + "parameters": [ + { + "type": "string", + "description": "Pasien ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Pasien deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Pasien not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/by-age": { + "get": { + "description": "Get pasien statistics by age group", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by Age Group", + "parameters": [ + { + "type": "string", + "description": "Age group (child, teen, adult, senior)", + "name": "age_group", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Statistics data", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienAgeStatsResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/by-location": { + "get": { + "description": "Get pasien by location (provinsi, kota, kecamatan, kelurahan)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by Location", + "parameters": [ + { + "type": "integer", + "description": "Filter by kelurahan ID", + "name": "kelurahan", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by kdkecamatan ID", + "name": "kdkecamatan", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by kota ID", + "name": "kota", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by kdprovinsi ID", + "name": "kdprovinsi", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/dynamic": { + "get": { + "description": "Get pasien with dynamic filtering", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien Dynamic", + "parameters": [ + { + "type": "string", + "description": "Fields to select (e.g., fields=*.*)", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "Dynamic filters (e.g., filter[name][_eq]=value)", + "name": "filter[column][operator]", + "in": "query" + }, + { + "type": "string", + "description": "Sort fields (e.g., sort=date_created,-name)", + "name": "sort", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/api/v1/pasien/nomr/:nomr": { + "get": { + "description": "Get pasien by Nomr", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pasien" + ], + "summary": "Get Pasien by Nomr", + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pasien.PasienGetByNomrResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, "/api/v1/retribusi/{id}": { "get": { "description": "Returns a single retribusi by ID", @@ -599,6 +1085,32 @@ } } }, + "/api/v1/retribusis/welcome": { + "get": { + "description": "Returns a welcome message and logs the request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Retribusi" + ], + "summary": "Get welcome message", + "responses": { + "200": { + "description": "Welcome message", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/v1/token/generate": { "post": { "description": "Generate a JWT token for testing purposes", @@ -686,6 +1198,869 @@ } } } + }, + "/component": { + "get": { + "description": "Get list of components with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Get Components List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by parent page ID", + "name": "page_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentsGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new component", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Create Component", + "parameters": [ + { + "description": "Component creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Component created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/component/{id}": { + "put": { + "description": "Update an existing component", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Update Component", + "parameters": [ + { + "type": "string", + "description": "Component ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Component update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Component updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Component not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a component", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Components" + ], + "summary": "Delete Component", + "parameters": [ + { + "type": "string", + "description": "Component ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Component deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_component.ComponentDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Component not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/pages": { + "get": { + "description": "Get list of pages with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Get Pages List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Create Page", + "parameters": [ + { + "description": "Rol_pages creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Rol_pages created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/pages/{id}": { + "put": { + "description": "Update an existing page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Update Page", + "parameters": [ + { + "type": "string", + "description": "Rol_pages ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Rol_pages update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Rol_pages updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Rol_pages not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a page", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Pages" + ], + "summary": "Delete Page", + "parameters": [ + { + "type": "string", + "description": "Rol_pages ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Rol_pages deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_pages.PagesDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Rol_pages not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/patient": { + "get": { + "description": "Returns a paginated list of patients with optional summary statistics", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Get patient with pagination and optional aggregation", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "boolean", + "default": false, + "description": "Include aggregation summary", + "name": "include_summary", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Search in multiple fields", + "name": "search", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a new patient record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Create patient", + "parameters": [ + { + "description": "Patient creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Patient created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/patient/{id}": { + "put": { + "description": "Updates an existing patient record", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Update patient", + "parameters": [ + { + "type": "string", + "description": "Medical Record Number", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Patient update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Patient updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Patient not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Soft deletes a patient by setting status to 'deleted'", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Patient" + ], + "summary": "Delete patient", + "parameters": [ + { + "type": "string", + "description": "Medical Record Number", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Patient deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_patient.PatientDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Patient not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/permission": { + "get": { + "description": "Get list of permissions with pagination and filters", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Get Permissions List", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit (max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "Filter by parent page ID", + "name": "page_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by status", + "name": "active", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success response", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionsGetResponse" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "post": { + "description": "Create a new permission", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Create Permission", + "parameters": [ + { + "description": "Permission creation request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Permission created successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionCreateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } + }, + "/permission/{id}": { + "put": { + "description": "Update an existing permission", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Update Permission", + "parameters": [ + { + "type": "string", + "description": "Permission ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Permission update request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "Permission updated successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionUpdateResponse" + } + }, + "400": { + "description": "Bad request or validation error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Permission not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Delete a permission", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Permissions" + ], + "summary": "Delete Permission", + "parameters": [ + { + "type": "string", + "description": "Permission ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Permission deleted successfully", + "schema": { + "$ref": "#/definitions/api-service_internal_models_permission.PermissionDeleteResponse" + } + }, + "400": { + "description": "Invalid ID format", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "404": { + "description": "Permission not found", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/api-service_internal_models.ErrorResponse" + } + } + } + } } }, "definitions": { @@ -857,6 +2232,1142 @@ } } }, + "api-service_internal_models_component.ComponentCreateRequest": { + "type": "object", + "required": [ + "directory", + "fk_rol_pages_id", + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "type": "string", + "maxLength": 255 + }, + "directory": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "fk_rol_pages_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_component.ComponentCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_component.Rol_component" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_component.ComponentDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_component.ComponentUpdateRequest": { + "type": "object", + "required": [ + "directory", + "fk_rol_pages_id", + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "type": "string", + "maxLength": 255 + }, + "directory": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "fk_rol_pages_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_component.ComponentUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_component.Rol_component" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_component.ComponentsGetResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_component.Rol_component" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + } + } + }, + "api-service_internal_models_component.Rol_component": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "$ref": "#/definitions/sql.NullString" + }, + "directory": { + "type": "string" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_pages.PagesCreateRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "icon": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "id": { + "description": "Status string `json:\"status\" validate:\"required,oneof=draft active inactive\"`", + "type": "integer" + }, + "level": { + "type": "integer" + }, + "name": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "parent": { + "type": "integer", + "minimum": 1 + }, + "sort": { + "type": "integer" + }, + "url": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + } + }, + "api-service_internal_models_pages.PagesCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_pages" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pages.PagesDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pages.PagesGetResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_pages" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_pages.PagesUpdateRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "active": { + "type": "boolean" + }, + "icon": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "level": { + "type": "integer" + }, + "name": { + "description": "Status string `json:\"status\" validate:\"required,oneof=draft active inactive\"`", + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "parent": { + "type": "integer", + "minimum": 1 + }, + "sort": { + "type": "integer" + }, + "url": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + } + }, + "api-service_internal_models_pages.PagesUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_pages" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pages.Rol_component": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "description": { + "$ref": "#/definitions/sql.NullString" + }, + "directory": { + "type": "string" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "api-service_internal_models_pages.Rol_pages": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "icon": { + "$ref": "#/definitions/sql.NullString" + }, + "id": { + "type": "integer" + }, + "level": { + "type": "integer" + }, + "list_component": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_component" + } + }, + "list_permission": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pages.Rol_permission" + } + }, + "name": { + "type": "string" + }, + "parent": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "sort": { + "type": "integer" + }, + "url": { + "$ref": "#/definitions/sql.NullString" + } + } + }, + "api-service_internal_models_pages.Rol_permission": { + "type": "object", + "properties": { + "active": { + "$ref": "#/definitions/sql.NullBool" + }, + "create": { + "$ref": "#/definitions/sql.NullBool" + }, + "delete": { + "$ref": "#/definitions/sql.NullBool" + }, + "disable": { + "description": "Note: \"disable\" is a Go keyword, so \"Disable\" is used for the field name.", + "allOf": [ + { + "$ref": "#/definitions/sql.NullBool" + } + ] + }, + "fk_rol_pages_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "read": { + "$ref": "#/definitions/sql.NullBool" + }, + "role_keycloak": { + "description": "Use NullString for optional text fields", + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "$ref": "#/definitions/sql.NullBool" + } + } + }, + "api-service_internal_models_pasien.Pasien": { + "type": "object", + "properties": { + "agama": { + "$ref": "#/definitions/sql.NullInt32" + }, + "alamat": { + "$ref": "#/definitions/sql.NullString" + }, + "createdAt": { + "$ref": "#/definitions/sql.NullTime" + }, + "id": { + "type": "integer" + }, + "idkecamatan": { + "type": "integer" + }, + "idkelurahan": { + "type": "integer" + }, + "idkota": { + "type": "integer" + }, + "idprovinsi": { + "type": "integer" + }, + "jeniskelamin": { + "$ref": "#/definitions/sql.NullString" + }, + "kdkecamatan": { + "$ref": "#/definitions/sql.NullInt32" + }, + "kdprovinsi": { + "$ref": "#/definitions/sql.NullInt32" + }, + "kelurahan": { + "$ref": "#/definitions/sql.NullInt64" + }, + "kota": { + "$ref": "#/definitions/sql.NullInt32" + }, + "nama": { + "$ref": "#/definitions/sql.NullString" + }, + "namakecamatan": { + "$ref": "#/definitions/sql.NullString" + }, + "namakelurahan": { + "$ref": "#/definitions/sql.NullString" + }, + "namakota": { + "$ref": "#/definitions/sql.NullString" + }, + "namaprovinsi": { + "$ref": "#/definitions/sql.NullString" + }, + "noKartu": { + "$ref": "#/definitions/sql.NullString" + }, + "noktpBaru": { + "$ref": "#/definitions/sql.NullString" + }, + "nomr": { + "$ref": "#/definitions/sql.NullString" + }, + "status": { + "$ref": "#/definitions/sql.NullString" + }, + "tempat": { + "$ref": "#/definitions/sql.NullString" + }, + "tgllahir": { + "$ref": "#/definitions/sql.NullTime" + }, + "title": { + "$ref": "#/definitions/sql.NullString" + }, + "updatedAt": { + "$ref": "#/definitions/sql.NullTime" + } + } + }, + "api-service_internal_models_pasien.PasienAgeStatsResponse": { + "type": "object", + "properties": { + "data": { + "type": "object", + "additionalProperties": true + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienCreateRequest": { + "type": "object", + "required": [ + "nama", + "status", + "title" + ], + "properties": { + "agama": { + "type": "integer" + }, + "alamat": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "jeniskelamin": { + "type": "string", + "enum": [ + "L", + "P" + ] + }, + "kdkecamatan": { + "type": "integer" + }, + "kdprovinsi": { + "type": "integer" + }, + "kelurahan": { + "type": "integer" + }, + "kota": { + "type": "integer" + }, + "nama": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "noKartu": { + "type": "string" + }, + "noktpBaru": { + "type": "string" + }, + "nomr": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tempat": { + "type": "string" + }, + "tgllahir": { + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 100, + "minLength": 1 + } + } + }, + "api-service_internal_models_pasien.PasienCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienGetByIDResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_pasien.PasienGetByNomrResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_pasien.PasienGetResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_pasien.PasienUpdateRequest": { + "type": "object", + "required": [ + "nama", + "status" + ], + "properties": { + "agama": { + "type": "integer" + }, + "alamat": { + "type": "string" + }, + "jeniskelamin": { + "type": "string", + "enum": [ + "L", + "P" + ] + }, + "kdkecamatan": { + "type": "integer" + }, + "kdprovinsi": { + "type": "integer" + }, + "kelurahan": { + "type": "integer" + }, + "kota": { + "type": "integer" + }, + "nama": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "noKartu": { + "type": "string" + }, + "noktpBaru": { + "type": "string" + }, + "nomr": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "inactive" + ] + }, + "tempat": { + "type": "string" + }, + "tgllahir": { + "type": "string" + }, + "title": { + "type": "string", + "maxLength": 255, + "minLength": 1 + } + } + }, + "api-service_internal_models_pasien.PasienUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_pasien.Pasien" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_patient.Patient": { + "type": "object", + "properties": { + "active": { + "$ref": "#/definitions/sql.NullBool" + }, + "address": { + "$ref": "#/definitions/sql.NullString" + }, + "birth_date": { + "$ref": "#/definitions/sql.NullTime" + }, + "ds_sd_kabupaten_kota": { + "$ref": "#/definitions/sql.NullString" + }, + "ds_sd_kecamatan": { + "$ref": "#/definitions/sql.NullString" + }, + "ds_sd_kelurahan": { + "$ref": "#/definitions/sql.NullString" + }, + "ds_sd_provinsi": { + "$ref": "#/definitions/sql.NullString" + }, + "fk_sd_kabupaten_kota_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "fk_sd_kecamatan_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "fk_sd_kelurahan_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "fk_sd_provinsi_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "gender": { + "$ref": "#/definitions/sql.NullString" + }, + "id": { + "type": "integer" + }, + "medical_record_number": { + "$ref": "#/definitions/sql.NullString" + }, + "name": { + "$ref": "#/definitions/sql.NullString" + }, + "phone_number": { + "$ref": "#/definitions/sql.NullString" + } + } + }, + "api-service_internal_models_patient.PatientCreateRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "address": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "birth_date": { + "type": "string" + }, + "ds_sd_kabupaten_kota": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "ds_sd_kecamatan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "ds_sd_kelurahan": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "ds_sd_provinsi": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "fk_sd_kabupaten_kota_id": { + "type": "integer" + }, + "fk_sd_kecamatan_id": { + "type": "integer" + }, + "fk_sd_kelurahan_id": { + "type": "integer" + }, + "fk_sd_provinsi_id": { + "type": "integer" + }, + "gender": { + "type": "string", + "maxLength": 1 + }, + "id": { + "type": "integer" + }, + "medical_record_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "phone_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + } + } + }, + "api-service_internal_models_patient.PatientCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_patient.Patient" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_patient.PatientDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "medical_record_number": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_patient.PatientGetResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_patient.Patient" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + }, + "summary": { + "$ref": "#/definitions/api-service_internal_models.AggregateData" + } + } + }, + "api-service_internal_models_patient.PatientUpdateRequest": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "active": { + "type": "boolean" + }, + "address": { + "type": "string", + "maxLength": 255, + "minLength": 1 + }, + "birth_date": { + "type": "string" + }, + "ds_sd_kabupaten_kota": { + "type": "string", + "maxLength": 255 + }, + "ds_sd_kecamatan": { + "type": "string", + "maxLength": 255 + }, + "ds_sd_kelurahan": { + "type": "string", + "maxLength": 255 + }, + "ds_sd_provinsi": { + "type": "string", + "maxLength": 255 + }, + "fk_sd_kabupaten_kota_id": { + "type": "integer" + }, + "fk_sd_kecamatan_id": { + "type": "integer" + }, + "fk_sd_kelurahan_id": { + "type": "integer" + }, + "fk_sd_provinsi_id": { + "type": "integer" + }, + "gender": { + "type": "string", + "maxLength": 1 + }, + "id": { + "type": "integer", + "minimum": 1 + }, + "medical_record_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + }, + "name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "phone_number": { + "type": "string", + "maxLength": 20, + "minLength": 1 + } + } + }, + "api-service_internal_models_patient.PatientUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_patient.Patient" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionCreateRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "delete": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "read": { + "type": "boolean" + }, + "role_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "type": "boolean" + } + } + }, + "api-service_internal_models_permission.PermissionCreateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_permission.Rol_permission" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionDeleteResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionUpdateRequest": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "create": { + "type": "boolean" + }, + "delete": { + "type": "boolean" + }, + "disable": { + "type": "boolean" + }, + "fk_rol_pages_id": { + "type": "integer" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "read": { + "type": "boolean" + }, + "role_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "type": "boolean" + } + } + }, + "api-service_internal_models_permission.PermissionUpdateResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/api-service_internal_models_permission.Rol_permission" + }, + "message": { + "type": "string" + } + } + }, + "api-service_internal_models_permission.PermissionsGetResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/api-service_internal_models_permission.Rol_permission" + } + }, + "message": { + "type": "string" + }, + "meta": { + "$ref": "#/definitions/api-service_internal_models.MetaResponse" + } + } + }, + "api-service_internal_models_permission.Rol_permission": { + "type": "object", + "properties": { + "active": { + "$ref": "#/definitions/sql.NullBool" + }, + "create": { + "$ref": "#/definitions/sql.NullBool" + }, + "delete": { + "$ref": "#/definitions/sql.NullBool" + }, + "disable": { + "description": "\"disable\" is a Go keyword, so \"Disable\" is used for the field name.", + "allOf": [ + { + "$ref": "#/definitions/sql.NullBool" + } + ] + }, + "fk_rol_pages_id": { + "$ref": "#/definitions/api-service_internal_models.NullableInt32" + }, + "group_keycloak": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "read": { + "$ref": "#/definitions/sql.NullBool" + }, + "role_keycloak": { + "description": "Use NullString for optional text fields", + "type": "array", + "items": { + "type": "string" + } + }, + "update": { + "$ref": "#/definitions/sql.NullBool" + } + } + }, "api-service_internal_models_retribusi.Retribusi": { "type": "object", "properties": { @@ -1140,6 +3651,68 @@ "type": "string" } } + }, + "sql.NullBool": { + "type": "object", + "properties": { + "bool": { + "type": "boolean" + }, + "valid": { + "description": "Valid is true if Bool is not NULL", + "type": "boolean" + } + } + }, + "sql.NullInt32": { + "type": "object", + "properties": { + "int32": { + "type": "integer", + "format": "int32" + }, + "valid": { + "description": "Valid is true if Int32 is not NULL", + "type": "boolean" + } + } + }, + "sql.NullInt64": { + "type": "object", + "properties": { + "int64": { + "type": "integer", + "format": "int64" + }, + "valid": { + "description": "Valid is true if Int64 is not NULL", + "type": "boolean" + } + } + }, + "sql.NullString": { + "type": "object", + "properties": { + "string": { + "type": "string" + }, + "valid": { + "description": "Valid is true if String is not NULL", + "type": "boolean" + } + } + }, + "sql.NullTime": { + "type": "object", + "properties": { + "time": { + "type": "string" + }, + "valid": { + "description": "Valid is true if Time is not NULL", + "type": "boolean" + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 654bca0..58b8dba 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -110,6 +110,779 @@ definitions: username: type: string type: object + api-service_internal_models_component.ComponentCreateRequest: + properties: + active: + type: boolean + description: + maxLength: 255 + type: string + directory: + maxLength: 255 + minLength: 1 + type: string + fk_rol_pages_id: + minimum: 1 + type: integer + name: + maxLength: 100 + minLength: 1 + type: string + sort: + type: integer + required: + - directory + - fk_rol_pages_id + - name + type: object + api-service_internal_models_component.ComponentCreateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_component.Rol_component' + message: + type: string + type: object + api-service_internal_models_component.ComponentDeleteResponse: + properties: + id: + type: string + message: + type: string + type: object + api-service_internal_models_component.ComponentUpdateRequest: + properties: + active: + type: boolean + description: + maxLength: 255 + type: string + directory: + maxLength: 255 + minLength: 1 + type: string + fk_rol_pages_id: + minimum: 1 + type: integer + name: + maxLength: 100 + minLength: 1 + type: string + sort: + type: integer + required: + - directory + - fk_rol_pages_id + - name + type: object + api-service_internal_models_component.ComponentUpdateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_component.Rol_component' + message: + type: string + type: object + api-service_internal_models_component.ComponentsGetResponse: + properties: + code: + type: integer + data: + items: + $ref: '#/definitions/api-service_internal_models_component.Rol_component' + type: array + message: + type: string + meta: + $ref: '#/definitions/api-service_internal_models.MetaResponse' + type: object + api-service_internal_models_component.Rol_component: + properties: + active: + type: boolean + description: + $ref: '#/definitions/sql.NullString' + directory: + type: string + fk_rol_pages_id: + type: integer + id: + type: integer + name: + type: string + sort: + type: integer + type: object + api-service_internal_models_pages.PagesCreateRequest: + properties: + active: + type: boolean + icon: + maxLength: 20 + minLength: 1 + type: string + id: + description: Status string `json:"status" validate:"required,oneof=draft + active inactive"` + type: integer + level: + type: integer + name: + maxLength: 20 + minLength: 1 + type: string + parent: + minimum: 1 + type: integer + sort: + type: integer + url: + maxLength: 100 + minLength: 1 + type: string + required: + - name + type: object + api-service_internal_models_pages.PagesCreateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_pages.Rol_pages' + message: + type: string + type: object + api-service_internal_models_pages.PagesDeleteResponse: + properties: + id: + type: string + message: + type: string + type: object + api-service_internal_models_pages.PagesGetResponse: + properties: + code: + type: integer + data: + items: + $ref: '#/definitions/api-service_internal_models_pages.Rol_pages' + type: array + message: + type: string + meta: + $ref: '#/definitions/api-service_internal_models.MetaResponse' + summary: + $ref: '#/definitions/api-service_internal_models.AggregateData' + type: object + api-service_internal_models_pages.PagesUpdateRequest: + properties: + active: + type: boolean + icon: + maxLength: 20 + minLength: 1 + type: string + level: + type: integer + name: + description: Status string `json:"status" validate:"required,oneof=draft + active inactive"` + maxLength: 20 + minLength: 1 + type: string + parent: + minimum: 1 + type: integer + sort: + type: integer + url: + maxLength: 100 + minLength: 1 + type: string + required: + - name + type: object + api-service_internal_models_pages.PagesUpdateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_pages.Rol_pages' + message: + type: string + type: object + api-service_internal_models_pages.Rol_component: + properties: + active: + type: boolean + description: + $ref: '#/definitions/sql.NullString' + directory: + type: string + fk_rol_pages_id: + type: integer + id: + type: integer + name: + type: string + sort: + type: integer + type: object + api-service_internal_models_pages.Rol_pages: + properties: + active: + type: boolean + icon: + $ref: '#/definitions/sql.NullString' + id: + type: integer + level: + type: integer + list_component: + items: + $ref: '#/definitions/api-service_internal_models_pages.Rol_component' + type: array + list_permission: + items: + $ref: '#/definitions/api-service_internal_models_pages.Rol_permission' + type: array + name: + type: string + parent: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + sort: + type: integer + url: + $ref: '#/definitions/sql.NullString' + type: object + api-service_internal_models_pages.Rol_permission: + properties: + active: + $ref: '#/definitions/sql.NullBool' + create: + $ref: '#/definitions/sql.NullBool' + delete: + $ref: '#/definitions/sql.NullBool' + disable: + allOf: + - $ref: '#/definitions/sql.NullBool' + description: 'Note: "disable" is a Go keyword, so "Disable" is used for the + field name.' + fk_rol_pages_id: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + group_keycloak: + items: + type: string + type: array + id: + type: integer + read: + $ref: '#/definitions/sql.NullBool' + role_keycloak: + description: Use NullString for optional text fields + items: + type: string + type: array + update: + $ref: '#/definitions/sql.NullBool' + type: object + api-service_internal_models_pasien.Pasien: + properties: + agama: + $ref: '#/definitions/sql.NullInt32' + alamat: + $ref: '#/definitions/sql.NullString' + createdAt: + $ref: '#/definitions/sql.NullTime' + id: + type: integer + idkecamatan: + type: integer + idkelurahan: + type: integer + idkota: + type: integer + idprovinsi: + type: integer + jeniskelamin: + $ref: '#/definitions/sql.NullString' + kdkecamatan: + $ref: '#/definitions/sql.NullInt32' + kdprovinsi: + $ref: '#/definitions/sql.NullInt32' + kelurahan: + $ref: '#/definitions/sql.NullInt64' + kota: + $ref: '#/definitions/sql.NullInt32' + nama: + $ref: '#/definitions/sql.NullString' + namakecamatan: + $ref: '#/definitions/sql.NullString' + namakelurahan: + $ref: '#/definitions/sql.NullString' + namakota: + $ref: '#/definitions/sql.NullString' + namaprovinsi: + $ref: '#/definitions/sql.NullString' + noKartu: + $ref: '#/definitions/sql.NullString' + noktpBaru: + $ref: '#/definitions/sql.NullString' + nomr: + $ref: '#/definitions/sql.NullString' + status: + $ref: '#/definitions/sql.NullString' + tempat: + $ref: '#/definitions/sql.NullString' + tgllahir: + $ref: '#/definitions/sql.NullTime' + title: + $ref: '#/definitions/sql.NullString' + updatedAt: + $ref: '#/definitions/sql.NullTime' + type: object + api-service_internal_models_pasien.PasienAgeStatsResponse: + properties: + data: + additionalProperties: true + type: object + message: + type: string + type: object + api-service_internal_models_pasien.PasienCreateRequest: + properties: + agama: + type: integer + alamat: + type: string + id: + type: integer + jeniskelamin: + enum: + - L + - P + type: string + kdkecamatan: + type: integer + kdprovinsi: + type: integer + kelurahan: + type: integer + kota: + type: integer + nama: + maxLength: 100 + minLength: 1 + type: string + noKartu: + type: string + noktpBaru: + type: string + nomr: + type: string + status: + enum: + - draft + - active + - inactive + type: string + tempat: + type: string + tgllahir: + type: string + title: + maxLength: 100 + minLength: 1 + type: string + required: + - nama + - status + - title + type: object + api-service_internal_models_pasien.PasienCreateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_pasien.Pasien' + message: + type: string + type: object + api-service_internal_models_pasien.PasienDeleteResponse: + properties: + id: + type: string + message: + type: string + type: object + api-service_internal_models_pasien.PasienGetByIDResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_pasien.Pasien' + message: + type: string + type: object + api-service_internal_models_pasien.PasienGetByNomrResponse: + properties: + data: + items: + $ref: '#/definitions/api-service_internal_models_pasien.Pasien' + type: array + message: + type: string + meta: + $ref: '#/definitions/api-service_internal_models.MetaResponse' + summary: + $ref: '#/definitions/api-service_internal_models.AggregateData' + type: object + api-service_internal_models_pasien.PasienGetResponse: + properties: + data: + items: + $ref: '#/definitions/api-service_internal_models_pasien.Pasien' + type: array + message: + type: string + meta: + $ref: '#/definitions/api-service_internal_models.MetaResponse' + summary: + $ref: '#/definitions/api-service_internal_models.AggregateData' + type: object + api-service_internal_models_pasien.PasienUpdateRequest: + properties: + agama: + type: integer + alamat: + type: string + jeniskelamin: + enum: + - L + - P + type: string + kdkecamatan: + type: integer + kdprovinsi: + type: integer + kelurahan: + type: integer + kota: + type: integer + nama: + maxLength: 100 + minLength: 1 + type: string + noKartu: + type: string + noktpBaru: + type: string + nomr: + type: string + status: + enum: + - draft + - active + - inactive + type: string + tempat: + type: string + tgllahir: + type: string + title: + maxLength: 255 + minLength: 1 + type: string + required: + - nama + - status + type: object + api-service_internal_models_pasien.PasienUpdateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_pasien.Pasien' + message: + type: string + type: object + api-service_internal_models_patient.Patient: + properties: + active: + $ref: '#/definitions/sql.NullBool' + address: + $ref: '#/definitions/sql.NullString' + birth_date: + $ref: '#/definitions/sql.NullTime' + ds_sd_kabupaten_kota: + $ref: '#/definitions/sql.NullString' + ds_sd_kecamatan: + $ref: '#/definitions/sql.NullString' + ds_sd_kelurahan: + $ref: '#/definitions/sql.NullString' + ds_sd_provinsi: + $ref: '#/definitions/sql.NullString' + fk_sd_kabupaten_kota_id: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + fk_sd_kecamatan_id: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + fk_sd_kelurahan_id: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + fk_sd_provinsi_id: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + gender: + $ref: '#/definitions/sql.NullString' + id: + type: integer + medical_record_number: + $ref: '#/definitions/sql.NullString' + name: + $ref: '#/definitions/sql.NullString' + phone_number: + $ref: '#/definitions/sql.NullString' + type: object + api-service_internal_models_patient.PatientCreateRequest: + properties: + active: + type: boolean + address: + maxLength: 255 + minLength: 1 + type: string + birth_date: + type: string + ds_sd_kabupaten_kota: + maxLength: 255 + minLength: 1 + type: string + ds_sd_kecamatan: + maxLength: 255 + minLength: 1 + type: string + ds_sd_kelurahan: + maxLength: 255 + minLength: 1 + type: string + ds_sd_provinsi: + maxLength: 255 + minLength: 1 + type: string + fk_sd_kabupaten_kota_id: + type: integer + fk_sd_kecamatan_id: + type: integer + fk_sd_kelurahan_id: + type: integer + fk_sd_provinsi_id: + type: integer + gender: + maxLength: 1 + type: string + id: + type: integer + medical_record_number: + maxLength: 20 + minLength: 1 + type: string + name: + maxLength: 100 + minLength: 1 + type: string + phone_number: + maxLength: 20 + minLength: 1 + type: string + type: object + api-service_internal_models_patient.PatientCreateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_patient.Patient' + message: + type: string + type: object + api-service_internal_models_patient.PatientDeleteResponse: + properties: + id: + type: string + medical_record_number: + type: string + message: + type: string + type: object + api-service_internal_models_patient.PatientGetResponse: + properties: + data: + items: + $ref: '#/definitions/api-service_internal_models_patient.Patient' + type: array + message: + type: string + meta: + $ref: '#/definitions/api-service_internal_models.MetaResponse' + summary: + $ref: '#/definitions/api-service_internal_models.AggregateData' + type: object + api-service_internal_models_patient.PatientUpdateRequest: + properties: + active: + type: boolean + address: + maxLength: 255 + minLength: 1 + type: string + birth_date: + type: string + ds_sd_kabupaten_kota: + maxLength: 255 + type: string + ds_sd_kecamatan: + maxLength: 255 + type: string + ds_sd_kelurahan: + maxLength: 255 + type: string + ds_sd_provinsi: + maxLength: 255 + type: string + fk_sd_kabupaten_kota_id: + type: integer + fk_sd_kecamatan_id: + type: integer + fk_sd_kelurahan_id: + type: integer + fk_sd_provinsi_id: + type: integer + gender: + maxLength: 1 + type: string + id: + minimum: 1 + type: integer + medical_record_number: + maxLength: 20 + minLength: 1 + type: string + name: + maxLength: 100 + minLength: 1 + type: string + phone_number: + maxLength: 20 + minLength: 1 + type: string + required: + - id + type: object + api-service_internal_models_patient.PatientUpdateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_patient.Patient' + message: + type: string + type: object + api-service_internal_models_permission.PermissionCreateRequest: + properties: + active: + type: boolean + create: + type: boolean + delete: + type: boolean + disable: + type: boolean + fk_rol_pages_id: + type: integer + group_keycloak: + items: + type: string + type: array + read: + type: boolean + role_keycloak: + items: + type: string + type: array + update: + type: boolean + type: object + api-service_internal_models_permission.PermissionCreateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_permission.Rol_permission' + message: + type: string + type: object + api-service_internal_models_permission.PermissionDeleteResponse: + properties: + id: + type: string + message: + type: string + type: object + api-service_internal_models_permission.PermissionUpdateRequest: + properties: + active: + type: boolean + create: + type: boolean + delete: + type: boolean + disable: + type: boolean + fk_rol_pages_id: + type: integer + group_keycloak: + items: + type: string + type: array + read: + type: boolean + role_keycloak: + items: + type: string + type: array + update: + type: boolean + type: object + api-service_internal_models_permission.PermissionUpdateResponse: + properties: + data: + $ref: '#/definitions/api-service_internal_models_permission.Rol_permission' + message: + type: string + type: object + api-service_internal_models_permission.PermissionsGetResponse: + properties: + code: + type: integer + data: + items: + $ref: '#/definitions/api-service_internal_models_permission.Rol_permission' + type: array + message: + type: string + meta: + $ref: '#/definitions/api-service_internal_models.MetaResponse' + type: object + api-service_internal_models_permission.Rol_permission: + properties: + active: + $ref: '#/definitions/sql.NullBool' + create: + $ref: '#/definitions/sql.NullBool' + delete: + $ref: '#/definitions/sql.NullBool' + disable: + allOf: + - $ref: '#/definitions/sql.NullBool' + description: '"disable" is a Go keyword, so "Disable" is used for the field + name.' + fk_rol_pages_id: + $ref: '#/definitions/api-service_internal_models.NullableInt32' + group_keycloak: + items: + type: string + type: array + id: + type: integer + read: + $ref: '#/definitions/sql.NullBool' + role_keycloak: + description: Use NullString for optional text fields + items: + type: string + type: array + update: + $ref: '#/definitions/sql.NullBool' + type: object api-service_internal_models_retribusi.Retribusi: properties: date_created: @@ -310,7 +1083,49 @@ definitions: message: type: string type: object -host: localhost:8080 + sql.NullBool: + properties: + bool: + type: boolean + valid: + description: Valid is true if Bool is not NULL + type: boolean + type: object + sql.NullInt32: + properties: + int32: + format: int32 + type: integer + valid: + description: Valid is true if Int32 is not NULL + type: boolean + type: object + sql.NullInt64: + properties: + int64: + format: int64 + type: integer + valid: + description: Valid is true if Int64 is not NULL + type: boolean + type: object + sql.NullString: + properties: + string: + type: string + valid: + description: Valid is true if String is not NULL + type: boolean + type: object + sql.NullTime: + properties: + time: + type: string + valid: + description: Valid is true if Time is not NULL + type: boolean + type: object +host: localhost:8084 info: contact: email: support@swagger.io @@ -447,6 +1262,328 @@ paths: summary: Register new user tags: - Authentication + /api/v1/pasien/: + get: + consumes: + - application/json + description: Get list of pasien with pagination and filters + parameters: + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - description: Filter by status + in: query + name: status + type: string + - description: Search in multiple fields + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pasien List + tags: + - Pasien + post: + consumes: + - application/json + description: Create a new pasien + parameters: + - description: Pasien creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienCreateRequest' + produces: + - application/json + responses: + "201": + description: Pasien created successfully + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienCreateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Create Pasien + tags: + - Pasien + /api/v1/pasien/:id: + get: + consumes: + - application/json + description: Get pasien by ID + parameters: + - description: Pasien ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienGetByIDResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Pasien not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pasien by ID + tags: + - Pasien + /api/v1/pasien/:nomr: + delete: + consumes: + - application/json + description: Delete a pasien + parameters: + - description: Pasien ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Pasien deleted successfully + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienDeleteResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Pasien not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Delete Pasien + tags: + - Pasien + put: + consumes: + - application/json + description: Update an existing pasien + parameters: + - description: Pasien ID + in: path + name: id + required: true + type: string + - description: Pasien update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienUpdateRequest' + produces: + - application/json + responses: + "200": + description: Pasien updated successfully + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienUpdateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Pasien not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Update Pasien + tags: + - Pasien + /api/v1/pasien/by-age: + get: + consumes: + - application/json + description: Get pasien statistics by age group + parameters: + - description: Age group (child, teen, adult, senior) + in: query + name: age_group + type: string + produces: + - application/json + responses: + "200": + description: Statistics data + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienAgeStatsResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pasien by Age Group + tags: + - Pasien + /api/v1/pasien/by-location: + get: + consumes: + - application/json + description: Get pasien by location (provinsi, kota, kecamatan, kelurahan) + parameters: + - description: Filter by kelurahan ID + in: query + name: kelurahan + type: integer + - description: Filter by kdkecamatan ID + in: query + name: kdkecamatan + type: integer + - description: Filter by kota ID + in: query + name: kota + type: integer + - description: Filter by kdprovinsi ID + in: query + name: kdprovinsi + type: integer + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pasien by Location + tags: + - Pasien + /api/v1/pasien/dynamic: + get: + consumes: + - application/json + description: Get pasien with dynamic filtering + parameters: + - description: Fields to select (e.g., fields=*.*) + in: query + name: fields + type: string + - description: Dynamic filters (e.g., filter[name][_eq]=value) + in: query + name: filter[column][operator] + type: string + - description: Sort fields (e.g., sort=date_created,-name) + in: query + name: sort + type: string + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pasien Dynamic + tags: + - Pasien + /api/v1/pasien/nomr/:nomr: + get: + consumes: + - application/json + description: Get pasien by Nomr + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_pasien.PasienGetByNomrResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pasien by Nomr + tags: + - Pasien /api/v1/retribusi/{id}: delete: consumes: @@ -704,6 +1841,23 @@ paths: summary: Get retribusi statistics tags: - Retribusi + /api/v1/retribusis/welcome: + get: + consumes: + - application/json + description: Returns a welcome message and logs the request + produces: + - application/json + responses: + "200": + description: Welcome message + schema: + additionalProperties: + type: string + type: object + summary: Get welcome message + tags: + - Retribusi /api/v1/token/generate: post: consumes: @@ -762,6 +1916,579 @@ paths: summary: Generate JWT token directly tags: - Token + /component: + get: + consumes: + - application/json + description: Get list of components with pagination and filters + parameters: + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - description: Filter by parent page ID + in: query + name: page_id + type: integer + - description: Filter by status + in: query + name: active + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_component.ComponentsGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Components List + tags: + - Components + post: + consumes: + - application/json + description: Create a new component + parameters: + - description: Component creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_component.ComponentCreateRequest' + produces: + - application/json + responses: + "201": + description: Component created successfully + schema: + $ref: '#/definitions/api-service_internal_models_component.ComponentCreateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Create Component + tags: + - Components + /component/{id}: + delete: + consumes: + - application/json + description: Delete a component + parameters: + - description: Component ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Component deleted successfully + schema: + $ref: '#/definitions/api-service_internal_models_component.ComponentDeleteResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Component not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Delete Component + tags: + - Components + put: + consumes: + - application/json + description: Update an existing component + parameters: + - description: Component ID + in: path + name: id + required: true + type: string + - description: Component update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_component.ComponentUpdateRequest' + produces: + - application/json + responses: + "200": + description: Component updated successfully + schema: + $ref: '#/definitions/api-service_internal_models_component.ComponentUpdateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Component not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Update Component + tags: + - Components + /pages: + get: + consumes: + - application/json + description: Get list of pages with pagination and filters + parameters: + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - description: Filter by status + in: query + name: status + type: string + - description: Search in multiple fields + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_pages.PagesGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Pages List + tags: + - Pages + post: + consumes: + - application/json + description: Create a new page + parameters: + - description: Rol_pages creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_pages.PagesCreateRequest' + produces: + - application/json + responses: + "201": + description: Rol_pages created successfully + schema: + $ref: '#/definitions/api-service_internal_models_pages.PagesCreateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Create Page + tags: + - Pages + /pages/{id}: + delete: + consumes: + - application/json + description: Delete a page + parameters: + - description: Rol_pages ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Rol_pages deleted successfully + schema: + $ref: '#/definitions/api-service_internal_models_pages.PagesDeleteResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Rol_pages not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Delete Page + tags: + - Pages + put: + consumes: + - application/json + description: Update an existing page + parameters: + - description: Rol_pages ID + in: path + name: id + required: true + type: string + - description: Rol_pages update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_pages.PagesUpdateRequest' + produces: + - application/json + responses: + "200": + description: Rol_pages updated successfully + schema: + $ref: '#/definitions/api-service_internal_models_pages.PagesUpdateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Rol_pages not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Update Page + tags: + - Pages + /patient: + get: + consumes: + - application/json + description: Returns a paginated list of patients with optional summary statistics + parameters: + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - default: false + description: Include aggregation summary + in: query + name: include_summary + type: boolean + - description: Filter by status + in: query + name: status + type: string + - description: Search in multiple fields + in: query + name: search + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_patient.PatientGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get patient with pagination and optional aggregation + tags: + - Patient + post: + consumes: + - application/json + description: Creates a new patient record + parameters: + - description: Patient creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_patient.PatientCreateRequest' + produces: + - application/json + responses: + "201": + description: Patient created successfully + schema: + $ref: '#/definitions/api-service_internal_models_patient.PatientCreateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Create patient + tags: + - Patient + /patient/{id}: + delete: + consumes: + - application/json + description: Soft deletes a patient by setting status to 'deleted' + parameters: + - description: Medical Record Number + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Patient deleted successfully + schema: + $ref: '#/definitions/api-service_internal_models_patient.PatientDeleteResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Patient not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Delete patient + tags: + - Patient + put: + consumes: + - application/json + description: Updates an existing patient record + parameters: + - description: Medical Record Number + in: path + name: id + required: true + type: string + - description: Patient update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_patient.PatientUpdateRequest' + produces: + - application/json + responses: + "200": + description: Patient updated successfully + schema: + $ref: '#/definitions/api-service_internal_models_patient.PatientUpdateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Patient not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Update patient + tags: + - Patient + /permission: + get: + consumes: + - application/json + description: Get list of permissions with pagination and filters + parameters: + - default: 10 + description: Limit (max 100) + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer + - description: Filter by parent page ID + in: query + name: page_id + type: integer + - description: Filter by status + in: query + name: active + type: string + produces: + - application/json + responses: + "200": + description: Success response + schema: + $ref: '#/definitions/api-service_internal_models_permission.PermissionsGetResponse' + "400": + description: Bad request + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Get Permissions List + tags: + - Permissions + post: + consumes: + - application/json + description: Create a new permission + parameters: + - description: Permission creation request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_permission.PermissionCreateRequest' + produces: + - application/json + responses: + "201": + description: Permission created successfully + schema: + $ref: '#/definitions/api-service_internal_models_permission.PermissionCreateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Create Permission + tags: + - Permissions + /permission/{id}: + delete: + consumes: + - application/json + description: Delete a permission + parameters: + - description: Permission ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: Permission deleted successfully + schema: + $ref: '#/definitions/api-service_internal_models_permission.PermissionDeleteResponse' + "400": + description: Invalid ID format + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Permission not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Delete Permission + tags: + - Permissions + put: + consumes: + - application/json + description: Update an existing permission + parameters: + - description: Permission ID + in: path + name: id + required: true + type: string + - description: Permission update request + in: body + name: request + required: true + schema: + $ref: '#/definitions/api-service_internal_models_permission.PermissionUpdateRequest' + produces: + - application/json + responses: + "200": + description: Permission updated successfully + schema: + $ref: '#/definitions/api-service_internal_models_permission.PermissionUpdateResponse' + "400": + description: Bad request or validation error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "404": + description: Permission not found + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/api-service_internal_models.ErrorResponse' + summary: Update Permission + tags: + - Permissions schemes: - http - https diff --git a/internal/config/config.go b/internal/config/config.go index 740e634..0c6c7c5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -197,7 +197,7 @@ func LoadConfig() *Config { log.Printf("DEBUG: Raw ENV for SECURITY_MAX_INPUT_LENGTH is: '%s'", os.Getenv("SECURITY_MAX_INPUT_LENGTH")) config := &Config{ Server: ServerConfig{ - Port: getEnvAsInt("PORT", 8080), + Port: getEnvAsInt("PORT", 8084), Mode: getEnv("GIN_MODE", "debug"), }, Databases: make(map[string]DatabaseConfig), @@ -232,12 +232,12 @@ func LoadConfig() *Config { ContactEmail: getEnv("SWAGGER_CONTACT_EMAIL", "support@swagger.io"), LicenseName: getEnv("SWAGGER_LICENSE_NAME", "Apache 2.0"), LicenseURL: getEnv("SWAGGER_LICENSE_URL", "http://www.apache.org/licenses/LICENSE-2.0.html"), - Host: getEnv("SWAGGER_HOST", "localhost:8080"), + Host: getEnv("SWAGGER_HOST", "localhost:8084"), BasePath: getEnv("SWAGGER_BASE_PATH", "/api/v1"), Schemes: parseSchemes(getEnv("SWAGGER_SCHEMES", "http,https")), }, Security: SecurityConfig{ - TrustedOrigins: parseOrigins(getEnv("SECURITY_TRUSTED_ORIGINS", "http://localhost:3000,http://localhost:8080")), + TrustedOrigins: parseOrigins(getEnv("SECURITY_TRUSTED_ORIGINS", "http://localhost:3000,http://localhost:8084")), MaxInputLength: getEnvAsInt("SECURITY_MAX_INPUT_LENGTH", 500), RateLimit: RateLimitConfig{ RequestsPerMinute: getEnvAsInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 60), @@ -1195,7 +1195,7 @@ func parseStaticTokens(tokensStr string) []string { func parseOrigins(originsStr string) []string { if originsStr == "" { - return []string{"http://localhost:8080"} // Default untuk pengembangan + return []string{"http://localhost:8084"} // Default untuk pengembangan } origins := strings.Split(originsStr, ",") for i, origin := range origins { diff --git a/internal/handlers/component/rol_component.go b/internal/handlers/component/rol_component.go new file mode 100644 index 0000000..bdd9a34 --- /dev/null +++ b/internal/handlers/component/rol_component.go @@ -0,0 +1,520 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models" + componentModels "api-service/internal/models/component" + queryUtils "api-service/internal/utils/query" + "api-service/internal/utils/validation" + "api-service/pkg/logger" + "context" + "database/sql" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/jmoiron/sqlx" +) + +// ============================================================================= +// GLOBAL INITIALIZATION & VALIDATION +// ============================================================================= + +var ( + db database.Service + once sync.Once + validate *validator.Validate +) + +// Initialize the database connection and validator once +func init() { + once.Do(func() { + db = database.New(config.LoadConfig()) + validate = validator.New() + validate.RegisterValidation("rol_pages_status", validateRol_pagesStatus) + if db == nil { + logger.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for rol_pages status +func validateRol_pagesStatus(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// ============================================================================= +// CACHE IMPLEMENTATION +// ============================================================================= + +// CacheEntry represents an entry in the cache +type CacheEntry struct { + Data interface{} + ExpiresAt time.Time +} + +// IsExpired checks if the cache entry has expired +func (e *CacheEntry) IsExpired() bool { + return time.Now().After(e.ExpiresAt) +} + +// InMemoryCache implements a simple in-memory cache with TTL +type InMemoryCache struct { + items sync.Map +} + +// NewInMemoryCache creates a new in-memory cache +func NewInMemoryCache() *InMemoryCache { + return &InMemoryCache{} +} + +// Get retrieves an item from the cache +func (c *InMemoryCache) Get(key string) (interface{}, bool) { + val, ok := c.items.Load(key) + if !ok { + return nil, false + } + + entry, ok := val.(*CacheEntry) + if !ok || entry.IsExpired() { + c.items.Delete(key) + return nil, false + } + + return entry.Data, true +} + +// Set stores an item in the cache with a TTL +func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) { + entry := &CacheEntry{ + Data: value, + ExpiresAt: time.Now().Add(ttl), + } + c.items.Store(key, entry) +} + +// Delete removes an item from the cache +func (c *InMemoryCache) Delete(key string) { + c.items.Delete(key) +} + +// DeleteByPrefix removes all items with a specific prefix +func (c *InMemoryCache) DeleteByPrefix(prefix string) { + c.items.Range(func(key, value interface{}) bool { + if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix { + c.items.Delete(key) + } + return true + }) +} + +// ============================================================================= +// ROL_COMPONENT HANDLER STRUCT +// ============================================================================= + +// Rol_componentHandler handles rol_component services +type Rol_componentHandler struct { + db database.Service + queryBuilder *queryUtils.QueryBuilder + validator *validation.DynamicValidator + cache *InMemoryCache // Re-using the same cache type +} + +// NewRol_componentHandler creates a new Rol_componentHandler +func NewRol_componentHandler() *Rol_componentHandler { + // Initialize QueryBuilder with allowed columns for security. + queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL). + SetAllowedColumns([]string{ + "id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id", + }) + + return &Rol_componentHandler{ + // We reuse the global 'db' instance from the init() function in rol_pages_handler.go + db: db, + queryBuilder: queryBuilder, + validator: validation.NewDynamicValidator(queryBuilder), + cache: NewInMemoryCache(), // Each handler gets its own cache instance + } +} + +// ============================================================================= +// HANDLER ENDPOINTS +// ============================================================================= + +// GetRol_components godoc +// @Summary Get Components List +// @Description Get list of components with pagination and filters +// @Tags Components +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param page_id query int false "Filter by parent page ID" +// @Param active query string false "Filter by status" +// @Success 200 {object} componentModels.ComponentsGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /component [get] +func (h *Rol_componentHandler) GetRol_components(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + query := queryUtils.DynamicQuery{ + From: "role_access.rol_component", + Aliases: "rc", + Fields: []queryUtils.SelectField{ + {Expression: "rc.id", Alias: "id"}, + {Expression: "rc.name", Alias: "name"}, + {Expression: "rc.description", Alias: "description"}, + {Expression: "rc.directory", Alias: "directory"}, + {Expression: "rc.active", Alias: "active"}, + {Expression: "rc.sort", Alias: "sort"}, + {Expression: "rc.fk_rol_pages_id", Alias: "fk_rol_pages_id"}, + }, + Sort: []queryUtils.SortField{ + {Column: "rc.fk_rol_pages_id", Order: "ASC"}, + {Column: "rc.sort", Order: "ASC"}, + }, + } + + // Parse pagination + if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 { + query.Limit = limit + } + if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 { + query.Offset = offset + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Parse filters + var filters []queryUtils.DynamicFilter + if pageID := c.Query("page_id"); pageID != "" { + if id, err := strconv.ParseInt(pageID, 10, 64); err == nil { + filters = append(filters, queryUtils.DynamicFilter{Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}) + } + } + if active := c.Query("active"); active != "" && models.IsValidStatus(active) { + filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: active}) + } + + if len(filters) > 0 { + query.Filters = []queryUtils.FilterGroup{{Filters: filters, LogicOp: "AND"}} + } + + // Execute query + var components []componentModels.Rol_component + err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &components) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Get total count for pagination + total, err := h.getTotalCount(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := componentModels.ComponentsGetResponse{ + Message: "Data rol_component berhasil diambil", + Data: components, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +} + +// CreateRol_component godoc +// @Summary Create Component +// @Description Create a new component +// @Tags Components +// @Accept json +// @Produce json +// @Param request body componentModels.ComponentCreateRequest true "Component creation request" +// @Success 201 {object} componentModels.ComponentCreateResponse "Component created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /component [post] +func (h *Rol_componentHandler) CreateRol_component(c *gin.Context) { + var req componentModels.ComponentCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + data := queryUtils.InsertData{ + Columns: []string{"name", "description", "directory", "active", "sort", "fk_rol_pages_id"}, + Values: []interface{}{req.Name, req.Description, req.Directory, req.Active, req.Sort, req.FkRolPagesID}, + } + returningCols := []string{"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id"} + + sql, args, err := h.queryBuilder.BuildInsertQuery("role_access.rol_component", data, returningCols...) + if err != nil { + h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError) + return + } + + var dataComponent componentModels.Rol_component + err = dbConn.GetContext(ctx, &dataComponent, sql, args...) + if err != nil { + h.logAndRespondError(c, "Failed to create rol_component", err, http.StatusInternalServerError) + return + } + + h.invalidateRelatedCache() + response := componentModels.ComponentCreateResponse{Message: "Rol_component berhasil dibuat", Data: &dataComponent} + c.JSON(http.StatusCreated, response) +} + +// UpdateRol_component godoc +// @Summary Update Component +// @Description Update an existing component +// @Tags Components +// @Accept json +// @Produce json +// @Param id path string true "Component ID" +// @Param request body componentModels.ComponentUpdateRequest true "Component update request" +// @Success 200 {object} componentModels.ComponentUpdateResponse "Component updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Component not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /component/{id} [put] +func (h *Rol_componentHandler) UpdateRol_component(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) + return + } + + var req componentModels.ComponentUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + idInt, err := strconv.Atoi(id) + if err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + req.ID = &idInt + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + columns := make([]string, 0, 6) + values := make([]interface{}, 0, 6) + + if req.Name != nil { + columns = append(columns, "name") + values = append(values, *req.Name) + } + if req.Description != nil { + columns = append(columns, "description") + values = append(values, *req.Description) // keep as string; if you want SQL NULL for empty use sql.NullString + } + if req.Directory != nil { + columns = append(columns, "directory") + values = append(values, *req.Directory) + } + if req.Active != nil { + columns = append(columns, "active") + values = append(values, *req.Active) + } + if req.Sort != nil { + columns = append(columns, "sort") + values = append(values, *req.Sort) + } + if req.FkRolPagesID != nil { + columns = append(columns, "fk_rol_pages_id") + values = append(values, *req.FkRolPagesID) + } + + if len(columns) == 0 { + h.respondError(c, "No fields to update", fmt.Errorf("empty update payload"), http.StatusBadRequest) + return + } + + updateData := queryUtils.UpdateData{ + Columns: columns, + Values: values, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: req.ID}, + }, + LogicOp: "AND", + }} + returningCols := []string{"id", "name", "description", "directory", "active", "sort", "fk_rol_pages_id"} + + sql, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", updateData, filters, returningCols...) + if err != nil { + h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError) + return + } + + var dataComponent componentModels.Rol_component + err = dbConn.GetContext(ctx, &dataComponent, sql, args...) + if err != nil { + if err.Error() == "sql: no rows in result set" { + h.respondError(c, "Rol_component not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update rol_component", err, http.StatusInternalServerError) + } + return + } + + h.invalidateRelatedCache() + response := componentModels.ComponentUpdateResponse{ + Message: "Rol_component berhasil diperbarui", + Data: &dataComponent, + } + c.JSON(http.StatusOK, response) +} + +// DeleteRol_component godoc +// @Summary Delete Component +// @Description Delete a component +// @Tags Components +// @Accept json +// @Produce json +// @Param id path string true "Component ID" +// @Success 200 {object} componentModels.ComponentDeleteResponse "Component deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Component not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /component/{id} [delete] +func (h *Rol_componentHandler) DeleteRol_component(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Soft delete by changing status + updateData := queryUtils.UpdateData{ + Columns: []string{"active"}, + Values: []interface{}{false}, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + + result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "role_access.rol_component", updateData, filters) + if err != nil { + h.logAndRespondError(c, "Failed to delete rol_component", err, http.StatusInternalServerError) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + h.logAndRespondError(c, "Failed to get affected rows", err, http.StatusInternalServerError) + return + } + if rowsAffected == 0 { + h.respondError(c, "Rol_component not found", sql.ErrNoRows, http.StatusNotFound) + return + } + + h.invalidateRelatedCache() + response := componentModels.ComponentDeleteResponse{Message: "Rol_component berhasil dihapus", ID: id} + c.JSON(http.StatusOK, response) +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +// invalidateRelatedCache clears cache entries that might be affected by component changes. +// This includes the pages cache, since pages contain components. +func (h *Rol_componentHandler) invalidateRelatedCache() { + h.cache.DeleteByPrefix("rol_component:") + h.cache.DeleteByPrefix("rol_pages:") // Invalidate pages cache as well +} + +func (h *Rol_componentHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) { + countQuery := queryUtils.DynamicQuery{ + From: query.From, + Aliases: query.Aliases, + Filters: query.Filters, + } + + count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery) + if err != nil { + return 0, fmt.Errorf("failed to execute count query: %w", err) + } + + return int(count), nil +} + +func (h *Rol_componentHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode}) + h.respondError(c, message, err, statusCode) +} + +func (h *Rol_componentHandler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()}) +} + +func (h *Rol_componentHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages, currentPage := 0, 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit + currentPage = (offset / limit) + 1 + } + return models.MetaResponse{ + Limit: limit, Offset: offset, Total: total, TotalPages: totalPages, + CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, + } +} diff --git a/internal/handlers/pages/rol_pages.go b/internal/handlers/pages/rol_pages.go new file mode 100644 index 0000000..056f8f2 --- /dev/null +++ b/internal/handlers/pages/rol_pages.go @@ -0,0 +1,1580 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models" + pagesModels "api-service/internal/models/pages" + queryUtils "api-service/internal/utils/query" + "api-service/internal/utils/validation" + "api-service/pkg/logger" + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/jmoiron/sqlx" + "github.com/lib/pq" +) + +// ============================================================================= +// GLOBAL INITIALIZATION & VALIDATION +// ============================================================================= + +var ( + db database.Service + once sync.Once + validate *validator.Validate +) + +// Initialize the database connection and validator once +func init() { + once.Do(func() { + db = database.New(config.LoadConfig()) + validate = validator.New() + validate.RegisterValidation("rol_pages_status", validateRol_pagesStatus) + if db == nil { + logger.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for rol_pages status +func validateRol_pagesStatus(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// ============================================================================= +// CACHE IMPLEMENTATION +// ============================================================================= + +// CacheEntry represents an entry in the cache +type CacheEntry struct { + Data interface{} + ExpiresAt time.Time +} + +// IsExpired checks if the cache entry has expired +func (e *CacheEntry) IsExpired() bool { + return time.Now().After(e.ExpiresAt) +} + +// InMemoryCache implements a simple in-memory cache with TTL +type InMemoryCache struct { + items sync.Map +} + +// NewInMemoryCache creates a new in-memory cache +func NewInMemoryCache() *InMemoryCache { + return &InMemoryCache{} +} + +// Get retrieves an item from the cache +func (c *InMemoryCache) Get(key string) (interface{}, bool) { + val, ok := c.items.Load(key) + if !ok { + return nil, false + } + + entry, ok := val.(*CacheEntry) + if !ok || entry.IsExpired() { + c.items.Delete(key) + return nil, false + } + + return entry.Data, true +} + +// Set stores an item in the cache with a TTL +func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) { + entry := &CacheEntry{ + Data: value, + ExpiresAt: time.Now().Add(ttl), + } + c.items.Store(key, entry) +} + +// Delete removes an item from the cache +func (c *InMemoryCache) Delete(key string) { + c.items.Delete(key) +} + +// DeleteByPrefix removes all items with a specific prefix +func (c *InMemoryCache) DeleteByPrefix(prefix string) { + c.items.Range(func(key, value interface{}) bool { + if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix { + c.items.Delete(key) + } + return true + }) +} + +// ============================================================================= +// Rol_pages HANDLER STRUCT +// ============================================================================= + +// Rol_pagesHandler handles rol_pages services +type Rol_pagesHandler struct { + db database.Service + queryBuilder *queryUtils.QueryBuilder + validator *validation.DynamicValidator + cache *InMemoryCache +} + +// NewRol_pagesHandler creates a new Rol_pagesHandler with a pre-configured QueryBuilder +func NewRol_pagesHandler() *Rol_pagesHandler { + // Initialize QueryBuilder with allowed columns list for security. + queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL). + SetAllowedColumns([]string{ + "id", "name", "icon", "url", "level", "sort", "parent", "active", + // Component fields + "fk_rol_pages_id", "name", "description", "directory", "active", "sort", + // Permission fields + `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, + `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, + }) + + return &Rol_pagesHandler{ + db: db, + queryBuilder: queryBuilder, + validator: validation.NewDynamicValidator(queryBuilder), + cache: NewInMemoryCache(), + } +} + +// ============================================================================= +// HANDLER ENDPOINTS +// ============================================================================= + +// GetRol_pages godoc +// @Summary Get Pages List +// @Description Get list of pages with pagination and filters +// @Tags Pages +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param status query string false "Filter by status" +// @Param search query string false "Search in multiple fields" +// @Success 200 {object} pagesModels.PagesGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /pages [get] +func (h *Rol_pagesHandler) GetRol_pages(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) + defer cancel() + + query := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{ + {Expression: "rp.id", Alias: "id"}, + {Expression: "rp.name", Alias: "name"}, + {Expression: "rp.icon", Alias: "icon"}, + {Expression: "rp.url", Alias: "url"}, + {Expression: "rp.level", Alias: "level"}, + {Expression: "rp.sort", Alias: "sort"}, + {Expression: "rp.parent", Alias: "parent"}, + {Expression: "rp.active", Alias: "active"}, + // Component fields + {Expression: "rc.id", Alias: "component_id"}, + {Expression: "rc.fk_rol_pages_id", Alias: "component_fk"}, + {Expression: "rc.name", Alias: "component_name"}, + {Expression: "rc.description", Alias: "component_description"}, + {Expression: "rc.directory", Alias: "component_directory"}, + {Expression: "rc.active", Alias: "component_active"}, + {Expression: "rc.sort", Alias: "component_sort"}, + // Permission fields + {Expression: "rper.id", Alias: "permission_id"}, + {Expression: "rper.create", Alias: "permission_create"}, + {Expression: "rper.read", Alias: "permission_read"}, + {Expression: "rper.update", Alias: "permission_update"}, + {Expression: "rper.delete", Alias: "permission_delete"}, + {Expression: "rper.disable", Alias: "permission_disable"}, + {Expression: "rper.active", Alias: "permission_active"}, + {Expression: "rper.fk_rol_pages_id", Alias: "permission_fk"}, + {Expression: "rper.role_keycloak", Alias: "permission_role_keycloak"}, + {Expression: "rper.group_keycloak", Alias: "permission_group_keycloak"}, + }, + Sort: []queryUtils.SortField{ + {Column: "rp.sort", Order: "ASC"}, + // {Column: "rp.id", Order: "ASC"}, + }, + Joins: []queryUtils.Join{ + { + Type: "LEFT", + Table: "role_access.rol_component", + Alias: "rc", + OnConditions: queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rc.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rp.id"}, + }, + }, + }, + { + Type: "LEFT", + Table: "role_access.rol_permission", + Alias: "rper", + OnConditions: queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rper.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rp.id"}, + }, + }, + }, + }, + } + + // Parse pagination + if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 { + query.Limit = limit + } + if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 { + query.Offset = offset + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Parse filters + var filters []queryUtils.DynamicFilter + // accept active from path param or query param; only boolean true/false allowed + activeVal := "" + if p := c.Param("active"); p != "" { + activeVal = p + } else if q := c.Query("active"); q != "" { + activeVal = q + } + if activeVal != "" { + if b, err := strconv.ParseBool(activeVal); err == nil { + filters = append(filters, queryUtils.DynamicFilter{Column: "rp.active", Operator: queryUtils.OpEqual, Value: b}) + } else { + h.respondError(c, "Invalid 'active' value; must be true or false", fmt.Errorf("invalid active value: %s", activeVal), http.StatusBadRequest) + return + } + } + + // Parse roles and groups params (comma separated). They filter on permission fields (rper). + if rolesParam := c.Query("roles"); rolesParam != "" { + roles := parseCSVParam(rolesParam) + if len(roles) > 0 { + // Use array-overlap operator so record's role_keycloak array overlaps provided roles + filters = append(filters, queryUtils.DynamicFilter{ + Column: "rper.role_keycloak", + Operator: queryUtils.OpArrayOverlap, + Value: roles, + }) + } + } + if groupsParam := c.Query("groups"); groupsParam != "" { + groups := parseCSVParam(groupsParam) + if len(groups) > 0 { + filters = append(filters, queryUtils.DynamicFilter{ + Column: "rper.group_keycloak", + Operator: queryUtils.OpArrayOverlap, + Value: groups, + }) + } + } + + // Handle search with caching + search := c.Query("search") + var searchFilters []queryUtils.DynamicFilter + var cacheKey string + var useCache bool + + if search != "" { + if len(search) > 50 { + search = search[:50] + } + + cacheKey = fmt.Sprintf("rol_pages:search:%s:%d:%d", search, query.Limit, query.Offset) + searchFilters = []queryUtils.DynamicFilter{ + {Column: "rp.id", Operator: queryUtils.OpEqual, Value: "" + search + ""}, + // {Column: "rp.name", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, + // {Column: "rp.url", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, + // {Column: "rp.level", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, + } + + if cachedData, found := h.cache.Get(cacheKey); found { + logger.Info("Cache hit for search", map[string]interface{}{"search": search}) + + if rol_pages, ok := cachedData.([]pagesModels.Rol_pages); ok { + var aggregateData *models.AggregateData + if c.Query("include_summary") == "true" { + fullFilterGroups := []queryUtils.FilterGroup{ + {Filters: searchFilters, LogicOp: "OR"}, + } + if len(filters) > 0 { + fullFilterGroups = append(fullFilterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) + } + aggregateData, err = h.getAggregateData(ctx, dbConn, fullFilterGroups) + if err != nil { + h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) + return + } + } + + meta := h.calculateMeta(query.Limit, query.Offset, len(rol_pages)) + response := pagesModels.PagesGetResponse{ + Message: "Data rol_pages berhasil diambil (dari cache)", + Data: rol_pages, + Meta: meta, + } + + if aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) + return + } + } + + useCache = true + query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"}) + } + + if len(filters) > 0 { + query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) + } + + // Execute query + var results []map[string]interface{} + err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Process results + rol_pages := h.processQueryResults(results) + + // Get total count + total, err := h.getTotalCount(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) + return + } + + // Cache results if search + if useCache && len(rol_pages) > 0 { + h.cache.Set(cacheKey, rol_pages, 15*time.Minute) + logger.Info("Cached search results", map[string]interface{}{"search": search, "count": len(rol_pages)}) + } + + // Get aggregate data if requested + var aggregateData *models.AggregateData + if c.Query("include_summary") == "true" { + aggregateData, err = h.getAggregateData(ctx, dbConn, query.Filters) + if err != nil { + h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) + return + } + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := pagesModels.PagesGetResponse{ + Message: "Data rol_pages berhasil diambil", + Data: rol_pages, + Meta: meta, + } + + if aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) +} + +// CreateRol_pages godoc +// @Summary Create Page +// @Description Create a new page +// @Tags Pages +// @Accept json +// @Produce json +// @Param request body pagesModels.PagesCreateRequest true "Rol_pages creation request" +// @Success 201 {object} pagesModels.PagesCreateResponse "Rol_pages created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /pages [post] +func (h *Rol_pagesHandler) CreateRol_pages(c *gin.Context) { + // Read raw body to support either single object or array + body, err := io.ReadAll(c.Request.Body) + if err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + trimmed := bytes.TrimSpace(body) + if len(trimmed) == 0 { + h.respondError(c, "Empty request body", fmt.Errorf("empty body"), http.StatusBadRequest) + return + } + + var reqs []pagesModels.PagesCreateRequest + if trimmed[0] == '[' { + if err := json.Unmarshal(trimmed, &reqs); err != nil { + h.respondError(c, "Invalid request body (array)", err, http.StatusBadRequest) + return + } + } else { + var single pagesModels.PagesCreateRequest + if err := json.Unmarshal(trimmed, &single); err != nil { + h.respondError(c, "Invalid request body (object)", err, http.StatusBadRequest) + return + } + reqs = append(reqs, single) + } + + // Validate each request + for i := range reqs { + if err := validate.Struct(&reqs[i]); err != nil { + h.respondError(c, fmt.Sprintf("Validation failed at index %d", i), err, http.StatusBadRequest) + return + } + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 60*time.Second) + defer cancel() + + // Pre-validate unique IDs (if provided) + for i, req := range reqs { + if req.ID != nil { + rule := validation.NewUniqueFieldRule( + "role_access.rol_pages", + "id", + queryUtils.DynamicFilter{ + Column: "active", + Operator: queryUtils.OpNotEqual, + Value: false, + }, + ) + dataToValidate := map[string]interface{}{"id": *req.ID} + isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) + if err != nil { + h.logAndRespondError(c, fmt.Sprintf("Failed to validate id at index %d", i), err, http.StatusInternalServerError) + return + } + if isDuplicate { + h.respondError(c, fmt.Sprintf("id already exists at index %d", i), fmt.Errorf("duplicate id: %d", *req.ID), http.StatusConflict) + return + } + } + } + + // Begin single transaction for all inserts (atomic) + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + h.logAndRespondError(c, "Failed to begin transaction", err, http.StatusInternalServerError) + return + } + rollback := func(errMsg string, err error, code int) { + _ = tx.Rollback() + h.logAndRespondError(c, errMsg, err, code) + } + + createdPages := make([]pagesModels.Rol_pages, 0, len(reqs)) + + for idx, req := range reqs { + // Insert page + pageInsert := queryUtils.InsertData{ + Columns: []string{"name", "icon", "url", "level", "sort", "parent", "active"}, + Values: []interface{}{req.Name, req.Icon, req.Url, req.Level, req.Sort, req.Parent, req.Active}, + } + pageReturning := []string{"id", "name", "icon", "url", "level", "sort", "parent", "active"} + pageSQL, pageArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_pages", pageInsert, pageReturning...) + if err != nil { + rollback("Failed to build insert query for page", err, http.StatusInternalServerError) + return + } + + var createdPage pagesModels.Rol_pages + if err := tx.GetContext(ctx, &createdPage, pageSQL, pageArgs...); err != nil { + rollback(fmt.Sprintf("Failed to insert rol_pages at index %d", idx), err, http.StatusInternalServerError) + return + } + + // Insert components + createdComponents := make([]pagesModels.Rol_component, 0) + if req.Components != nil { + for _, compReq := range req.Components { + compInsert := queryUtils.InsertData{ + Columns: []string{"name", "description", "directory", "sort", "active", "fk_rol_pages_id"}, + Values: []interface{}{compReq.Name, compReq.Description, compReq.Directory, compReq.Sort, compReq.Active, createdPage.ID}, + } + compReturning := []string{"id", "name", "description", "directory", "sort", "active", "fk_rol_pages_id"} + compSQL, compArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_component", compInsert, compReturning...) + if err != nil { + rollback("Failed to build insert query for component", err, http.StatusInternalServerError) + return + } + var createdComp pagesModels.Rol_component + if err := tx.GetContext(ctx, &createdComp, compSQL, compArgs...); err != nil { + rollback("Failed to insert rol_component", err, http.StatusInternalServerError) + return + } + createdComponents = append(createdComponents, createdComp) + } + } + + // Insert permissions + createdPermissions := make([]pagesModels.Rol_permission, 0) + if req.Permissions != nil { + for _, permReq := range req.Permissions { + createNB := toNullBool(permReq.Create) + readNB := toNullBool(permReq.Read) + updateNB := toNullBool(permReq.Update) + deleteNB := toNullBool(permReq.Delete) + disableNB := toNullBool(permReq.Disable) + activeNB := toNullBool(permReq.Active) + + roles := toPQStringArray(permReq.RoleKeycloak) + groups := toPQStringArray(permReq.GroupKeycloak) + + permInsert := queryUtils.InsertData{ + Columns: []string{ + `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, + `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, + }, + Values: []interface{}{ + createNB, readNB, updateNB, deleteNB, disableNB, activeNB, + createdPage.ID, roles, groups, + }, + } + permReturning := []string{ + `"id"`, `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, + `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`, + } + permSQL, permArgs, err := h.queryBuilder.BuildInsertQuery("role_access.rol_permission", permInsert, permReturning...) + if err != nil { + rollback("Failed to build insert query for permission", err, http.StatusInternalServerError) + return + } + var createdPerm pagesModels.Rol_permission + if err := tx.GetContext(ctx, &createdPerm, permSQL, permArgs...); err != nil { + rollback("Failed to insert rol_permission", err, http.StatusInternalServerError) + return + } + createdPermissions = append(createdPermissions, createdPerm) + } + } + + createdPage.Components = createdComponents + createdPage.Permissions = createdPermissions + createdPages = append(createdPages, createdPage) + } + + // Commit + if err := tx.Commit(); err != nil { + _ = tx.Rollback() + h.logAndRespondError(c, "Failed to commit transaction", err, http.StatusInternalServerError) + return + } + + h.invalidateRelatedCache() + c.JSON(http.StatusCreated, gin.H{ + "message": "Rol_pages berhasil dibuat", + "data": createdPages, + }) +} + +// UpdateRol_pages godoc +// @Summary Update Page +// @Description Update an existing page +// @Tags Pages +// @Accept json +// @Produce json +// @Param id path string true "Rol_pages ID" +// @Param request body pagesModels.PagesUpdateRequest true "Rol_pages update request" +// @Success 200 {object} pagesModels.PagesUpdateResponse "Rol_pages updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Rol_pages not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /pages/{id} [put] +func (h *Rol_pagesHandler) UpdateRol_pages(c *gin.Context) { + id := c.Param("id") + // if id == "" { + // h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) + // return + // } + + // Validate ID is integer + if _, err := strconv.Atoi(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + var req pagesModels.PagesUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Set ID from path parameter + idInt, _ := strconv.Atoi(id) + req.ID = &idInt + + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + // Get old data for cache invalidation + var oldData pagesModels.Rol_pages + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err == nil { + ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) + defer cancel() + + dynamicQuery := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{{Expression: "*"}}, + Filters: []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rp.id", Operator: queryUtils.OpEqual, Value: id}, + }, + LogicOp: "AND", + }}, + Limit: 1, + Joins: []queryUtils.Join{ + { + Type: "LEFT", + Table: "role_access.rol_component", + Alias: "rc", + OnConditions: queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rc.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rp.id"}, + }, + }, + }, + }, + } + + err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &oldData) + if err != nil { + logger.Error("Failed to fetch old data for cache invalidation", map[string]interface{}{"error": err.Error(), "id": id}) + } + } + + dbConn, err = h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Validate id must be unique, except for record with this id + if req.ID != nil { + rule := validation.ValidationRule{ + TableName: "role_access.rol_pages", + UniqueColumns: []string{"id"}, + Conditions: []queryUtils.DynamicFilter{ + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + ExcludeIDColumn: "id", + ExcludeIDValue: id, + } + + dataToValidate := map[string]interface{}{"id": *req.ID} + isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) + if err != nil { + h.logAndRespondError(c, "Failed to validate id", err, http.StatusInternalServerError) + return + } + + if isDuplicate { + h.respondError(c, "id already exists", fmt.Errorf("duplicate id: %d", *req.ID), http.StatusConflict) + return + } + } + + columns := make([]string, 0, 7) + values := make([]interface{}, 0, 7) + + if req.Name != nil { + columns = append(columns, "name") + values = append(values, *req.Name) + } + if req.Icon != nil { + columns = append(columns, "icon") + values = append(values, *req.Icon) + } + if req.Url != nil { + columns = append(columns, "url") + values = append(values, *req.Url) + } + if req.Level != nil { + columns = append(columns, "level") + values = append(values, *req.Level) + } + if req.Sort != nil { + columns = append(columns, "sort") + values = append(values, *req.Sort) + } + + updateData := queryUtils.UpdateData{ + Columns: columns, + Values: values, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: req.ID}, + }, + LogicOp: "AND", + }} + returningCols := []string{"id", "name", "icon", "url", "level", "sort", "parent", "active"} + + // Start transaction for update including components and permissions + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + h.logAndRespondError(c, "Failed to begin transaction", err, http.StatusInternalServerError) + return + } + rollback := func(errMsg string, err error, code int) { + _ = tx.Rollback() + h.logAndRespondError(c, errMsg, err, code) + } + + var updatedPage pagesModels.Rol_pages + + // If there are no page-level columns to update, skip UPDATE and SELECT existing row + if len(updateData.Columns) == 0 { + if err := tx.GetContext(ctx, &updatedPage, "SELECT id, name, icon, url, level, sort, parent, active FROM role_access.rol_pages WHERE id=$1", *req.ID); err != nil { + _ = tx.Rollback() + if err == sql.ErrNoRows { + h.respondError(c, "Rol_pages not found", err, http.StatusNotFound) + return + } + h.logAndRespondError(c, "Failed to fetch existing rol_pages", err, http.StatusInternalServerError) + return + } + } else { + // Update page row and get returning + sqlQuery, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_pages", updateData, filters, returningCols...) + if err != nil { + rollback("Failed to build update query", err, http.StatusInternalServerError) + return + } + + if err := tx.GetContext(ctx, &updatedPage, sqlQuery, args...); err != nil { + _ = tx.Rollback() + if err == sql.ErrNoRows || err.Error() == "sql: no rows in result set" { + h.respondError(c, "Rol_pages not found", err, http.StatusNotFound) + return + } + h.logAndRespondError(c, "Failed to update rol_pages", err, http.StatusInternalServerError) + return + } + } + + // Handle components (insert or update) if provided + updatedComponents := make([]pagesModels.Rol_component, 0) + if req.Components != nil { + for _, compReq := range req.Components { + // If ID present => update, else insert + if compReq.ID != nil && *compReq.ID > 0 { + compCols := make([]string, 0) + compVals := make([]interface{}, 0) + if compReq.Name != nil { + compCols = append(compCols, "name") + compVals = append(compVals, *compReq.Name) + } + if compReq.Description != nil { + compCols = append(compCols, "description") + compVals = append(compVals, *compReq.Description) + } + if compReq.Directory != nil { + compCols = append(compCols, "directory") + compVals = append(compVals, *compReq.Directory) + } + if compReq.Sort != nil { + compCols = append(compCols, "sort") + compVals = append(compVals, *compReq.Sort) + } + if compReq.Active != nil { + compCols = append(compCols, "active") + compVals = append(compVals, *compReq.Active) + } + if len(compCols) > 0 { + compUpdate := queryUtils.UpdateData{Columns: compCols, Values: compVals} + compFilters := []queryUtils.FilterGroup{{Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: *compReq.ID}, + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: updatedPage.ID}, + }, + + LogicOp: "AND", + }} + compReturning := []string{"id", "name", "description", "directory", "sort", "active", "fk_rol_pages_id"} + compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters, compReturning...) + if err != nil { + rollback("Failed to build component update query", err, http.StatusInternalServerError) + return + } + var updatedComp pagesModels.Rol_component + if err := tx.GetContext(ctx, &updatedComp, compSQL, compArgs...); err != nil { + rollback("Failed to update rol_component", err, http.StatusInternalServerError) + return + } + updatedComponents = append(updatedComponents, updatedComp) + } + } else { + // Do NOT insert new component on update. + // If client wants to add a new component, use Create endpoint. + rollback("Component not found or missing id", fmt.Errorf("component must include id to be updated; insert not allowed in UpdateRol_pages"), http.StatusNotFound) + return + } + } + } + + // Handle permissions (insert or update) if provided + updatedPermissions := make([]pagesModels.Rol_permission, 0) + if req.Permissions != nil { + for _, permReq := range req.Permissions { + createNB := toNullBool(permReq.Create) + readNB := toNullBool(permReq.Read) + updateNB := toNullBool(permReq.Update) + deleteNB := toNullBool(permReq.Delete) + disableNB := toNullBool(permReq.Disable) + activeNB := toNullBool(permReq.Active) + roles := toPQStringArray(permReq.RoleKeycloak) + groups := toPQStringArray(permReq.GroupKeycloak) + + if permReq.ID != nil && *permReq.ID > 0 { + permCols := make([]string, 0) + permVals := make([]interface{}, 0) + + if permReq.Create != nil { + permCols = append(permCols, `"create"`) + permVals = append(permVals, createNB) + } + if permReq.Read != nil { + permCols = append(permCols, `"read"`) + permVals = append(permVals, readNB) + } + if permReq.Update != nil { + permCols = append(permCols, `"update"`) + permVals = append(permVals, updateNB) + } + if permReq.Delete != nil { + permCols = append(permCols, `"delete"`) + permVals = append(permVals, deleteNB) + } + if permReq.Disable != nil { + permCols = append(permCols, `"disable"`) + permVals = append(permVals, disableNB) + } + if permReq.Active != nil { + permCols = append(permCols, `"active"`) + permVals = append(permVals, activeNB) + } + if permReq.RoleKeycloak != nil { + permCols = append(permCols, `"role_keycloak"`) + permVals = append(permVals, roles) + } + if permReq.GroupKeycloak != nil { + permCols = append(permCols, `"group_keycloak"`) + permVals = append(permVals, groups) + } + if len(permCols) > 0 { + permUpdate := queryUtils.UpdateData{Columns: permCols, Values: permVals} + permFilters := []queryUtils.FilterGroup{{Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: *permReq.ID}, + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: updatedPage.ID}, + }, + LogicOp: "AND", + }} + permReturning := []string{`"id"`, `"create"`, `"read"`, `"update"`, `"delete"`, `"disable"`, `"active"`, `"fk_rol_pages_id"`, `"role_keycloak"`, `"group_keycloak"`} + permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters, permReturning...) + if err != nil { + rollback("Failed to build permission update query", err, http.StatusInternalServerError) + return + } + var updatedPerm pagesModels.Rol_permission + if err := tx.GetContext(ctx, &updatedPerm, permSQL, permArgs...); err != nil { + rollback("Failed to update rol_permission", err, http.StatusInternalServerError) + return + } + updatedPermissions = append(updatedPermissions, updatedPerm) + } else { + // Do NOT insert new permission on update. + // If client wants to add a new permission, use Create endpoint. + rollback("Permission not found or missing id", fmt.Errorf("permission must include id to be updated; insert not allowed in Update endpoint"), http.StatusNotFound) + return + } + } + } + } + + // Commit transaction + if err := tx.Commit(); err != nil { + _ = tx.Rollback() + h.logAndRespondError(c, "Failed to commit transaction", err, http.StatusInternalServerError) + return + } + + // Invalidate cache + cacheKey := fmt.Sprintf("rol_pages:id:%s", id) + h.cache.Delete(cacheKey) + if oldData.ID != 0 { + h.invalidateRelatedCache() + } + + // Attach related rows + updatedPage.Components = updatedComponents + updatedPage.Permissions = updatedPermissions + + response := pagesModels.PagesUpdateResponse{Message: "Rol_pages berhasil diperbarui", Data: &updatedPage} + c.JSON(http.StatusOK, response) +} + +// DeleteRol_pages godoc +// @Summary Delete Page +// @Description Delete a page +// @Tags Pages +// @Accept json +// @Produce json +// @Param id path string true "Rol_pages ID" +// @Success 200 {object} pagesModels.PagesDeleteResponse "Rol_pages deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Rol_pages not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /pages/{id} [delete] +func (h *Rol_pagesHandler) DeleteRol_pages(c *gin.Context) { + id := c.Param("id") + // if id == "" { + // h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) + // return + // } + + // Validate ID is integer + if _, err := strconv.Atoi(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + // Optional DELETE body: if provided, will limit deletion to these child IDs + var delReq struct { + ComponentIDs []int64 `json:"component_ids"` + PermissionIDs []int64 `json:"permission_ids"` + } + // ignore error so empty body is allowed + _ = c.ShouldBindJSON(&delReq) + + // Get data for cache invalidation + var dataToDelete pagesModels.Rol_pages + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err == nil { + ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) + defer cancel() + + dynamicQuery := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{{Expression: "*"}}, + Filters: []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: id}, + }, + LogicOp: "AND", + }}, + Limit: 1, + Joins: []queryUtils.Join{ + { + Type: "LEFT", + Table: "role_access.rol_component", + Alias: "rc", + OnConditions: queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "rc.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rp.id"}, + }, + }, + }, + }, + } + + err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &dataToDelete) + if err != nil { + logger.Error("Failed to fetch data for cache invalidation", map[string]interface{}{"error": err.Error(), "id": id}) + } + } + + dbConn, err = h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Start transaction + tx, err := dbConn.BeginTxx(ctx, nil) + if err != nil { + h.logAndRespondError(c, "Failed to begin transaction", err, http.StatusInternalServerError) + return + } + rollback := func(msg string, err error, code int) { + _ = tx.Rollback() + h.logAndRespondError(c, msg, err, code) + } + + // Soft-delete page (active = false) only if currently active != false + pageUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + pageFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + pageSQL, pageArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_pages", pageUpdate, pageFilters) + if err != nil { + rollback("Failed to build delete query for page", err, http.StatusInternalServerError) + return + } + res, err := tx.ExecContext(ctx, pageSQL, pageArgs...) + if err != nil { + rollback("Failed to execute delete for page", err, http.StatusInternalServerError) + return + } + ra, err := res.RowsAffected() + if err != nil { + rollback("Failed to get affected rows for page delete", err, http.StatusInternalServerError) + return + } + if ra == 0 { + _ = tx.Rollback() + h.respondError(c, "Rol_pages not found", sql.ErrNoRows, http.StatusNotFound) + return + } + + // Soft-delete related components + // If client provided specific component IDs -> delete only those; otherwise delete all components for the page. + if len(delReq.ComponentIDs) > 0 { + // Use query builder to update only specified component IDs that belong to this page + compUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + compFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "id", Operator: queryUtils.OpIn, Value: delReq.ComponentIDs}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters) + if err != nil { + rollback("Failed to build component delete query", err, http.StatusInternalServerError) + return + } + if _, err := tx.ExecContext(ctx, compSQL, compArgs...); err != nil { + rollback("Failed to execute delete for components", err, http.StatusInternalServerError) + return + } + } else { + compUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + compFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + compSQL, compArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_component", compUpdate, compFilters) + if err != nil { + rollback("Failed to build delete query for components", err, http.StatusInternalServerError) + return + } + if _, err := tx.ExecContext(ctx, compSQL, compArgs...); err != nil { + rollback("Failed to execute delete for components", err, http.StatusInternalServerError) + return + } + } + + // Soft-delete related permissions + // If client provided specific permission IDs -> delete only those; otherwise delete all permissions for the page. + if len(delReq.PermissionIDs) > 0 { + permUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + permFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "id", Operator: queryUtils.OpIn, Value: delReq.PermissionIDs}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters) + if err != nil { + rollback("Failed to build permission delete query", err, http.StatusInternalServerError) + return + } + if _, err := tx.ExecContext(ctx, permSQL, permArgs...); err != nil { + rollback("Failed to execute delete for permissions", err, http.StatusInternalServerError) + return + } + } else { + permUpdate := queryUtils.UpdateData{Columns: []string{"active"}, Values: []interface{}{false}} + permFilters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + permSQL, permArgs, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", permUpdate, permFilters) + if err != nil { + rollback("Failed to build delete query for permissions", err, http.StatusInternalServerError) + return + } + if _, err := tx.ExecContext(ctx, permSQL, permArgs...); err != nil { + rollback("Failed to execute delete for permissions", err, http.StatusInternalServerError) + return + } + } + + // Commit + if err := tx.Commit(); err != nil { + _ = tx.Rollback() + h.logAndRespondError(c, "Failed to commit transaction", err, http.StatusInternalServerError) + return + } + + // Invalidate cache + cacheKey := fmt.Sprintf("rol_pages:id:%s", id) + h.cache.Delete(cacheKey) + if dataToDelete.ID != 0 { + h.invalidateRelatedCache() + } + + response := pagesModels.PagesDeleteResponse{Message: "Rol_pages berhasil dihapus", ID: id} + c.JSON(http.StatusOK, response) +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +func (h *Rol_pagesHandler) processQueryResults(results []map[string]interface{}) []pagesModels.Rol_pages { + pagesMap := make(map[int64]*pagesModels.Rol_pages) + order := make([]int64, 0, len(results)) // preserve encounter order + + for _, result := range results { + // === ADD THIS DEBUG LOG === + // logger.Info("Processing result row", map[string]interface{}{ + // "row_index": i, + // "data": result, // This will print the entire map for the first few rows + // }) + pageID := getInt64(result, "id") + + page, exists := pagesMap[pageID] + if !exists { + page = &pagesModels.Rol_pages{ + ID: pageID, + Name: getString(result, "name"), + Icon: getNullString(result, "icon"), + Url: getNullString(result, "url"), + Level: getInt64(result, "level"), + Sort: getInt64(result, "sort"), + Parent: getNullableInt32(result, "parent"), + Active: getBool(result, "active"), + Components: []pagesModels.Rol_component{}, + Permissions: []pagesModels.Rol_permission{}, + } + pagesMap[pageID] = page + order = append(order, pageID) + } + + componentID := getInt64(result, "component_id") + if componentID != 0 { + // Check if this component has already been added to this page's list + // A map is much faster for this check than iterating over a slice. + // We create a temporary map just for this check. + seenComponents := make(map[int64]struct{}) + for _, comp := range page.Components { + seenComponents[comp.ID] = struct{}{} + } + + if _, seen := seenComponents[componentID]; !seen { + component := pagesModels.Rol_component{ + ID: componentID, + FkRolPagesID: getInt64(result, "component_fk"), + Name: getString(result, "component_name"), + Description: getNullString(result, "component_description"), + Directory: getString(result, "component_directory"), + Active: getBool(result, "component_active"), + Sort: getInt64(result, "component_sort"), + } + page.Components = append(page.Components, component) + } + } + + permissionID := getInt64(result, "permission_id") + if permissionID != 0 { + // Check if this permission has already been added to this page's list + seenPermissions := make(map[int64]struct{}) + for _, perm := range page.Permissions { + seenPermissions[perm.ID] = struct{}{} + } + + if _, seen := seenPermissions[permissionID]; !seen { + permission := pagesModels.Rol_permission{ + ID: permissionID, + Create: getNullBool(result, "permission_create"), + Read: getNullBool(result, "permission_read"), + Update: getNullBool(result, "permission_update"), + Disable: getNullBool(result, "permission_disable"), + Delete: getNullBool(result, "permission_delete"), + Active: getNullBool(result, "permission_active"), + FkRolPagesID: getNullableInt32(result, "permission_fk"), + RoleKeycloak: getStringArray(result, "permission_role_keycloak"), + GroupKeycloak: getStringArray(result, "permission_group_keycloak"), + } + page.Permissions = append(page.Permissions, permission) + } + } + + } + + rol_pages := make([]pagesModels.Rol_pages, 0, len(pagesMap)) + for _, id := range order { // append in DB result order + if p, ok := pagesMap[id]; ok { + rol_pages = append(rol_pages, *p) + } + } + + return rol_pages +} + +// Helper functions +func getInt64(m map[string]interface{}, key string) int64 { + if val, ok := m[key]; ok { + if v, ok := val.(int64); ok { + return v + } + } + return 0 +} + +func getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if v, ok := val.(string); ok { + return v + } + } + return "" +} + +// getNullString safely extracts a models.NullString from a map[string]interface{} +func getNullString(m map[string]interface{}, key string) sql.NullString { + if val, ok := m[key]; ok { + if ns, ok := val.(sql.NullString); ok { // The DB driver still returns sql.NullString + return sql.NullString(ns) // Convert it to your custom type + } + if s, ok := val.(string); ok { + return sql.NullString{String: s, Valid: true} + } + } + return sql.NullString{Valid: false} +} + +func getStringArray(m map[string]interface{}, key string) pq.StringArray { + if val, ok := m[key]; ok { + // Case 1: The DB driver correctly returns pq.StringArray (ideal case) + if arr, ok := val.(pq.StringArray); ok { + // === CLEAN THE ARRAY ELEMENTS HERE === + cleanedArray := make([]string, 0, len(arr)) + for _, element := range arr { + // Trim whitespace, then remove surrounding braces and quotes + cleaned := strings.TrimSpace(element) + cleaned = strings.TrimPrefix(cleaned, "{") + cleaned = strings.TrimSuffix(cleaned, "}") + cleaned = strings.Trim(cleaned, `"`) + cleanedArray = append(cleanedArray, cleaned) + } + return pq.StringArray(cleanedArray) + } + + // Case 2: The DB driver returns a string because the column is TEXT + if str, ok := val.(string); ok { + // This case is less likely now, but we handle it. + parts := strings.Split(str, ",") + var stringSlice []string + for _, part := range parts { + if t := strings.TrimSpace(part); t != "" { + // Apply same cleaning logic + cleaned := strings.TrimPrefix(t, "{") + cleaned = strings.TrimSuffix(cleaned, "}") + cleaned = strings.Trim(cleaned, `"`) + stringSlice = append(stringSlice, cleaned) + } + } + return pq.StringArray(stringSlice) + } + } + // Return an empty, valid pq.StringArray if the key doesn't exist. + return pq.StringArray{} +} + +func getNullableInt32(m map[string]interface{}, key string) models.NullableInt32 { + if val, ok := m[key]; ok { + if v, ok := val.(models.NullableInt32); ok { + return v + } + } + return models.NullableInt32{} +} + +func getBool(m map[string]interface{}, key string) bool { + if val, ok := m[key]; ok { + if v, ok := val.(bool); ok { + return v + } + } + return false +} + +// getNullBool safely extracts a sql.NullBool from a map[string]interface{} +func getNullBool(m map[string]interface{}, key string) sql.NullBool { + if val, ok := m[key]; ok { + // Check if the value is already of type sql.NullBool + if nb, ok := val.(sql.NullBool); ok { + return nb + } + // Also handle the case where it's a plain bool, for flexibility + if b, ok := val.(bool); ok { + return sql.NullBool{Bool: b, Valid: true} + } + } + // Return an invalid NullBool if the key doesn't exist or the type is wrong + return sql.NullBool{Valid: false} +} + +// helper to split comma-separated query params and trim spaces +func parseCSVParam(s string) []string { + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + if t := strings.TrimSpace(p); t != "" { + out = append(out, t) + } + } + return out +} + +func toNullBool(p *bool) sql.NullBool { + if p == nil { + return sql.NullBool{Valid: false} + } + return sql.NullBool{Bool: *p, Valid: true} +} + +func toPQStringArray(p *[]string) pq.StringArray { + if p == nil || len(*p) == 0 { + return pq.StringArray{} + } + return pq.StringArray(*p) +} + +func (h *Rol_pagesHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) { + countQuery := queryUtils.DynamicQuery{ + From: query.From, + Aliases: query.Aliases, + Filters: query.Filters, + Joins: query.Joins, + } + + count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery) + if err != nil { + return 0, fmt.Errorf("failed to execute count query: %w", err) + } + + return int(count), nil +} + +func (h *Rol_pagesHandler) invalidateRelatedCache() { + h.cache.DeleteByPrefix("rol_pages:search:") + h.cache.DeleteByPrefix("rol_pages:dynamic:") + h.cache.DeleteByPrefix("rol_pages:stats:") + h.cache.DeleteByPrefix("rol_pages:id:") +} + +func (h *Rol_pagesHandler) getAggregateData(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + } + + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 2) + + // Count by status + wg.Add(1) + go func() { + defer wg.Done() + queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) + defer queryCancel() + + query := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{ + {Expression: "active"}, + {Expression: "COUNT(*)", Alias: "count"}, + }, + Filters: filterGroups, + GroupBy: []string{"active"}, + } + var results []struct { + Status string `db:"active"` + Count int `db:"count"` + } + err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + mu.Lock() + for _, result := range results { + aggregate.ByStatus[result.Status] = result.Count + switch result.Status { + case "active": + aggregate.TotalActive = result.Count + case "draft": + aggregate.TotalDraft = result.Count + case "inactive": + aggregate.TotalInactive = result.Count + } + } + mu.Unlock() + }() + + // Get last updated and today's stats + wg.Add(1) + go func() { + defer wg.Done() + queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) + defer queryCancel() + + // Last updated + query1 := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{{Expression: "MAX(date_updated)"}}, + Filters: filterGroups, + } + var lastUpdated sql.NullTime + err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated) + if err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + today := time.Now().Format("2006-01-02") + + // Query for created_today + createdTodayQuery := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{ + {Expression: "COUNT(*)", Alias: "count"}, + }, + Filters: append(filterGroups, queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "DATE(date_created)", Operator: queryUtils.OpEqual, Value: today}, + }, + LogicOp: "AND", + }), + } + + var createdToday int + err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday) + if err != nil { + errChan <- fmt.Errorf("created today query failed: %w", err) + return + } + + // Query for updated_today + updatedTodayQuery := queryUtils.DynamicQuery{ + From: "role_access.rol_pages", + Aliases: "rp", + Fields: []queryUtils.SelectField{ + {Expression: "COUNT(*)", Alias: "count"}, + }, + Filters: append(filterGroups, queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "DATE(date_updated)", Operator: queryUtils.OpEqual, Value: today}, + {Column: "DATE(date_created)", Operator: queryUtils.OpNotEqual, Value: today}, + }, + LogicOp: "AND", + }), + } + + var updatedToday int + err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday) + if err != nil { + errChan <- fmt.Errorf("updated today query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + wg.Wait() + close(errChan) + + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} + +func (h *Rol_pagesHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode}) + h.respondError(c, message, err, statusCode) +} + +func (h *Rol_pagesHandler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()}) +} + +func (h *Rol_pagesHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages, currentPage := 0, 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit + currentPage = (offset / limit) + 1 + } + return models.MetaResponse{ + Limit: limit, Offset: offset, Total: total, TotalPages: totalPages, + CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, + } +} diff --git a/internal/handlers/patient/ms_patient.go b/internal/handlers/patient/ms_patient.go new file mode 100644 index 0000000..e60093e --- /dev/null +++ b/internal/handlers/patient/ms_patient.go @@ -0,0 +1,1286 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models" + patientModels "api-service/internal/models/patient" + queryUtils "api-service/internal/utils/query" + "api-service/internal/utils/validation" + "api-service/pkg/logger" + "context" + "database/sql" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/jmoiron/sqlx" +) + +// ============================================================================= +// GLOBAL INITIALIZATION & VALIDATION +// ============================================================================= + +var ( + patientdb database.Service + patientonce sync.Once + patientvalidate *validator.Validate +) + +// Initialize the database connection and validator once +func init() { + patientonce.Do(func() { + patientdb = database.New(config.LoadConfig()) + patientvalidate = validator.New() + patientvalidate.RegisterValidation("ms_patient_status", validatePatientStatus) + if patientdb == nil { + logger.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for patient status +func validatePatientStatus(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// ============================================================================= +// CACHE IMPLEMENTATION +// ============================================================================= + +// CacheEntry represents an entry in the cache +type CacheEntry struct { + Data interface{} + ExpiresAt time.Time +} + +// IsExpired checks if the cache entry has expired +func (e *CacheEntry) IsExpired() bool { + return time.Now().After(e.ExpiresAt) +} + +// PatientInMemoryCache implements a simple in-memory cache with TTL +type InMemoryCache struct { + items sync.Map +} + +// NewPatientInMemoryCache creates a new in-memory cache +func NewInMemoryCache() *InMemoryCache { + return &InMemoryCache{} +} + +// Get retrieves an item from the cache +func (c *InMemoryCache) Get(key string) (interface{}, bool) { + val, ok := c.items.Load(key) + if !ok { + return nil, false + } + + entry, ok := val.(*CacheEntry) + if !ok || entry.IsExpired() { + c.items.Delete(key) + return nil, false + } + + return entry.Data, true +} + +// Set stores an item in the cache with a TTL +func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) { + entry := &CacheEntry{ + Data: value, + ExpiresAt: time.Now().Add(ttl), + } + c.items.Store(key, entry) +} + +// Delete removes an item from the cache +func (c *InMemoryCache) Delete(key string) { + c.items.Delete(key) +} + +// DeleteByPrefix removes all items with a specific prefix +func (c *InMemoryCache) DeleteByPrefix(prefix string) { + c.items.Range(func(key, value interface{}) bool { + if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix { + c.items.Delete(key) + } + return true + }) +} + +// ============================================================================= +// PATIENT HANDLER STRUCT +// ============================================================================= + +// PatientHandler handles patient services +type PatientHandler struct { + db database.Service + queryBuilder *queryUtils.QueryBuilder + validator *validation.DynamicValidator + cache *InMemoryCache +} + +// NewPatientHandler creates a new PatientHandler with a pre-configured QueryBuilder +func NewPatientHandler() *PatientHandler { + // Initialize QueryBuilder with allowed columns list for security. + queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL). + SetAllowedColumns([]string{ + "id", "name", "medical_record_number", "phone_number", "gender", + "birth_date", "address", "active", "fk_sd_provinsi_id", + "fk_sd_kabupaten_kota_id", "fk_sd_kecamatan_id", "fk_sd_kelurahan_id", + "ds_sd_provinsi", "ds_sd_kabupaten_kota", "ds_sd_kecamatan", "ds_sd_kelurahan", + }) + + return &PatientHandler{ + db: patientdb, + queryBuilder: queryBuilder, + validator: validation.NewDynamicValidator(queryBuilder), + cache: NewInMemoryCache(), + } +} + +// ============================================================================= +// HANDLER ENDPOINTS +// ============================================================================= + +// GetPatient godoc +// @Summary Get patient with pagination and optional aggregation +// @Description Returns a paginated list of patients with optional summary statistics +// @Tags Patient +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param include_summary query bool false "Include aggregation summary" default(false) +// @Param status query string false "Filter by status" +// @Param search query string false "Search in multiple fields" +// @Success 200 {object} patientModels.PatientGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient [get] +func (h *PatientHandler) GetMs_patient(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) + defer cancel() + + query := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{ + {Expression: "mp.id", Alias: "id"}, + {Expression: "mp.name", Alias: "name"}, + {Expression: "mp.medical_record_number", Alias: "medical_record_number"}, + {Expression: "mp.phone_number", Alias: "phone_number"}, + {Expression: "mp.gender", Alias: "gender"}, + {Expression: "mp.birth_date", Alias: "birth_date"}, + {Expression: "mp.address", Alias: "address"}, + {Expression: "mp.active", Alias: "active"}, + {Expression: "mp.fk_sd_provinsi_id", Alias: "fk_sd_provinsi_id"}, + {Expression: "mp.fk_sd_kabupaten_kota_id", Alias: "fk_sd_kabupaten_kota_id"}, + {Expression: "mp.fk_sd_kecamatan_id", Alias: "fk_sd_kecamatan_id"}, + {Expression: "mp.fk_sd_kelurahan_id", Alias: "fk_sd_kelurahan_id"}, + {Expression: "mp.ds_sd_provinsi", Alias: "ds_sd_provinsi"}, + {Expression: "mp.ds_sd_kabupaten_kota", Alias: "ds_sd_kabupaten_kota"}, + {Expression: "mp.ds_sd_kecamatan", Alias: "ds_sd_kecamatan"}, + {Expression: "mp.ds_sd_kelurahan", Alias: "ds_sd_kelurahan"}, + }, + Sort: []queryUtils.SortField{ + {Column: "mp.id", Order: "ASC"}, + }, + } + + // Parse pagination + if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 { + query.Limit = limit + } + if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 { + query.Offset = offset + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Parse filters + var filters []queryUtils.DynamicFilter + if active := c.Query("active"); active != "" && models.IsValidStatus(active) { + filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: active}) + } + + // Handle search with caching + search := c.Query("search") + var searchFilters []queryUtils.DynamicFilter + var cacheKey string + var useCache bool + + if search != "" { + if len(search) > 50 { + search = search[:50] + } + + cacheKey = fmt.Sprintf("ms_patient:search:%s:%d:%d", search, query.Limit, query.Offset) + searchFilters = []queryUtils.DynamicFilter{ + {Column: "mp.medical_record_number", Operator: queryUtils.OpEqual, Value: "" + search + ""}, + {Column: "mp.name", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, + {Column: "mp.phone_number", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, + } + + if cachedData, found := h.cache.Get(cacheKey); found { + logger.Info("Cache hit for search", map[string]interface{}{"search": search}) + + if patients, ok := cachedData.([]patientModels.Patient); ok { + var aggregateData *models.AggregateData + if c.Query("include_summary") == "true" { + fullFilterGroups := []queryUtils.FilterGroup{ + {Filters: searchFilters, LogicOp: "OR"}, + } + if len(filters) > 0 { + fullFilterGroups = append(fullFilterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) + } + aggregateData, err = h.getAggregateData(ctx, dbConn, fullFilterGroups) + if err != nil { + h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) + return + } + } + + meta := h.calculateMeta(query.Limit, query.Offset, len(patients)) + response := patientModels.PatientGetResponse{ + Message: "Data ms_patient berhasil diambil (dari cache)", + Data: patients, + Meta: meta, + } + + if aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) + return + } + } + + useCache = true + query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"}) + } + + if len(filters) > 0 { + query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) + } + + // Execute query + var results []map[string]interface{} + err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Process results + ms_patient := h.processQueryResults(results) + + // Get total count + total, err := h.getTotalCount(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) + return + } + + // Cache results if search + if useCache && len(ms_patient) > 0 { + h.cache.Set(cacheKey, ms_patient, 15*time.Minute) + logger.Info("Cached search results", map[string]interface{}{"search": search, "count": len(ms_patient)}) + } + + // Get aggregate data if requested + var aggregateData *models.AggregateData + if c.Query("include_summary") == "true" { + aggregateData, err = h.getAggregateData(ctx, dbConn, query.Filters) + if err != nil { + h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) + return + } + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := patientModels.PatientGetResponse{ + Message: "Data ms_patient berhasil diambil", + Data: ms_patient, + Meta: meta, + } + + if aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) +} + +// GetPatientByIDPost godoc +// @Summary Get patient by ID using POST request +// @Description Retrieves a patient record by ID using POST request +// @Tags Patient +// @Accept json +// @Produce json +// @Param request body patientModels.PatientPostRequest true "Patient post request" +// @Success 200 {object} patientModels.PatientCreateResponse "Patient details retrieved successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Patient not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient/detail [post] +// func (h *PatientHandler) GetPatientByIDPost(c *gin.Context) { +// var req patientModels.PatientPostRequest +// if err := c.ShouldBindJSON(&req); err != nil { +// h.respondError(c, "Invalid request body", err, http.StatusBadRequest) +// return +// } + +// if err := patientvalidate.Struct(&req); err != nil { +// h.respondError(c, "Validation failed", err, http.StatusBadRequest) +// return +// } + +// dbConn, err := h.db.GetSQLXDB("db_antrean") +// if err != nil { +// h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) +// return +// } + +// ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) +// defer cancel() + +// // Check cache first +// cacheKey := fmt.Sprintf("patient:id:%d", req.ID) +// if cachedData, found := h.cache.Get(cacheKey); found { +// if patient, ok := cachedData.(*patientModels.Patient); ok { +// response := patientModels.PatientCreateResponse{ +// Message: "patient details retrieved successfully (dari cache)", +// Data: patient, +// } +// c.JSON(http.StatusOK, response) +// return +// } +// } + +// query := queryUtils.DynamicQuery{ +// From: "master.ms_patient", +// Aliases: "mp", +// Fields: []queryUtils.SelectField{ +// {Expression: "mp.id", Alias: "id"}, +// {Expression: "mp.name", Alias: "name"}, +// {Expression: "mp.medical_record_number", Alias: "medical_record_number"}, +// {Expression: "mp.phone_number", Alias: "phone_number"}, +// {Expression: "mp.gender", Alias: "gender"}, +// {Expression: "mp.birth_date", Alias: "birth_date"}, +// {Expression: "mp.address", Alias: "address"}, +// {Expression: "mp.active", Alias: "active"}, +// {Expression: "mp.fk_sd_provinsi_id", Alias: "fk_sd_provinsi_id"}, +// {Expression: "mp.fk_sd_kabupaten_kota_id", Alias: "fk_sd_kabupaten_kota_id"}, +// {Expression: "mp.fk_sd_kecamatan_id", Alias: "fk_sd_kecamatan_id"}, +// {Expression: "mp.fk_sd_kelurahan_id", Alias: "fk_sd_kelurahan_id"}, +// {Expression: "mp.ds_sd_provinsi", Alias: "ds_sd_provinsi"}, +// {Expression: "mp.ds_sd_kabupaten_kota", Alias: "ds_sd_kabupaten_kota"}, +// {Expression: "mp.ds_sd_kecamatan", Alias: "ds_sd_kecamatan"}, +// {Expression: "mp.ds_sd_kelurahan", Alias: "ds_sd_kelurahan"}, +// }, +// Filters: []queryUtils.FilterGroup{{ +// Filters: []queryUtils.DynamicFilter{ +// {Column: "mp.id", Operator: queryUtils.OpEqual, Value: req.ID}, +// }, +// LogicOp: "AND", +// }}, +// Limit: 1, +// } + +// var patient patientModels.Patient +// err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, query, &patient) +// if err != nil { +// if err.Error() == "sql: no rows in result set" { +// h.respondError(c, "Patient not found", err, http.StatusNotFound) +// } else { +// h.logAndRespondError(c, "Failed to get patient", err, http.StatusInternalServerError) +// } +// return +// } + +// // Cache the result +// h.cache.Set(cacheKey, &patient, 30*time.Minute) + +// response := patientModels.PatientCreateResponse{ +// Message: "patient details retrieved successfully", +// Data: &patient, +// } + +// c.JSON(http.StatusOK, response) +// } + +// CreatePatient godoc +// @Summary Create patient +// @Description Creates a new patient record +// @Tags Patient +// @Accept json +// @Produce json +// @Param request body patientModels.PatientCreateRequest true "Patient creation request" +// @Success 201 {object} patientModels.PatientCreateResponse "Patient created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient [post] +func (h *PatientHandler) CreateMs_patient(c *gin.Context) { + var req patientModels.PatientCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Validate request + if err := patientvalidate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Validate id must be unique + if req.ID != nil { + rule := validation.NewUniqueFieldRule( + "master.ms_patient", + "id", + queryUtils.DynamicFilter{ + Column: "active", + Operator: queryUtils.OpNotEqual, + Value: false, + }, + ) + + dataToValidate := map[string]interface{}{"id": *req.ID} + isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) + if err != nil { + h.logAndRespondError(c, "Failed to validate id", err, http.StatusInternalServerError) + return + } + + if isDuplicate { + h.respondError(c, "id already exists", fmt.Errorf("duplicate id: %d", *req.ID), http.StatusConflict) + return + } + } + + // Validate medical_record_number must be unique + if req.MedicalRecordNumber != nil { + rule := validation.NewUniqueFieldRule( + "master.ms_patient", + "medical_record_number", + queryUtils.DynamicFilter{ + Column: "active", + Operator: queryUtils.OpNotEqual, + Value: false, + }, + ) + + dataToValidate := map[string]interface{}{"medical_record_number": req.MedicalRecordNumber} + isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) + if err != nil { + h.logAndRespondError(c, "Failed to validate medical_record_number", err, http.StatusInternalServerError) + return + } + + if isDuplicate { + h.respondError(c, "medical_record_number already exists", fmt.Errorf("duplicate medical_record_number: %s", *req.MedicalRecordNumber), http.StatusConflict) + return + } + } + + data := queryUtils.InsertData{ + Columns: []string{ + "name", "medical_record_number", "phone_number", "gender", + "birth_date", "address", "active", "fk_sd_provinsi_id", + "fk_sd_kabupaten_kota_id", "fk_sd_kecamatan_id", "fk_sd_kelurahan_id", + "ds_sd_provinsi", "ds_sd_kabupaten_kota", "ds_sd_kecamatan", "ds_sd_kelurahan", + }, + Values: []interface{}{ + req.Name, req.MedicalRecordNumber, req.PhoneNumber, req.Gender, + req.BirthDate, req.Address, req.Active, req.FKSdProvinsiID, + req.FKSdKabupatenKotaID, req.FKSdKecamatanID, req.FKSdKelurahanID, + req.DsSdProvinsi, req.DsSdKabupatenKota, req.DsSdKecamatan, req.DsSdKelurahan, + }, + } + returningCols := []string{ + "id", "name", "medical_record_number", "phone_number", "gender", + "birth_date", "address", "active", "fk_sd_provinsi_id", + "fk_sd_kabupaten_kota_id", "fk_sd_kecamatan_id", "fk_sd_kelurahan_id", + "ds_sd_provinsi", "ds_sd_kabupaten_kota", "ds_sd_kecamatan", "ds_sd_kelurahan", + } + + sql, args, err := h.queryBuilder.BuildInsertQuery("master.ms_patient", data, returningCols...) + if err != nil { + h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError) + return + } + + var dataMs_patient patientModels.Patient + err = dbConn.GetContext(ctx, &dataMs_patient, sql, args...) + if err != nil { + h.logAndRespondError(c, "Failed to create patient", err, http.StatusInternalServerError) + return + } + + h.invalidateRelatedCache() + response := patientModels.PatientCreateResponse{ + Message: "Patient berhasil dibuat", + Data: &dataMs_patient, + } + + c.JSON(http.StatusCreated, response) +} + +// UpdatePatient godoc +// @Summary Update patient +// @Description Updates an existing patient record +// @Tags Patient +// @Accept json +// @Produce json +// @Param id path string true "Medical Record Number" +// @Param request body patientModels.PatientUpdateRequest true "Patient update request" +// @Success 200 {object} patientModels.PatientUpdateResponse "Patient updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Patient not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient/{id} [put] +func (h *PatientHandler) UpdateMs_patient(c *gin.Context) { + medical_record_number := c.Param("medical_record_number") + if medical_record_number == "" { + h.respondError(c, "Invalid medical_record_number format", fmt.Errorf("medical_record_number cannot be empty"), http.StatusBadRequest) + return + } + + var req patientModels.PatientUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Set medical_record_number from path parameter (string) + req.MedicalRecordNumber = &medical_record_number + + // Validate request + if err := patientvalidate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + // Get old data for cache invalidation + var oldData patientModels.Patient + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err == nil { + ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) + defer cancel() + + query := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{{Expression: "*"}}, + Filters: []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "mp.medical_record_number", Operator: queryUtils.OpEqual, Value: medical_record_number}, + }, + LogicOp: "AND", + }}, + Limit: 1, + } + + err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, query, &oldData) + if err != nil { + logger.Error("Failed to fetch old data for cache invalidation", map[string]interface{}{"error": err.Error(), "medical_record_number": medical_record_number}) + } + } + + dbConn, err = h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Validate medical_record_number must be unique + if req.MedicalRecordNumber != nil { + rule := validation.ValidationRule{ + TableName: "role_access.rol_pages", + UniqueColumns: []string{"medical_record_number"}, + Conditions: []queryUtils.DynamicFilter{ + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + ExcludeIDColumn: "medical_record_number", + ExcludeIDValue: medical_record_number, + } + + dataToValidate := map[string]interface{}{"medical_record_number": req.MedicalRecordNumber} + isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate) + if err != nil { + h.logAndRespondError(c, "Failed to validate medical_record_number", err, http.StatusInternalServerError) + return + } + + if isDuplicate { + h.respondError(c, "medical_record_number already exists", fmt.Errorf("duplicate medical_record_number: %s", *req.MedicalRecordNumber), http.StatusConflict) + return + } + } + + updateData := queryUtils.UpdateData{ + Columns: []string{ + "name", "medical_record_number", "phone_number", "gender", + "birth_date", "address", "active", "fk_sd_provinsi_id", + "fk_sd_kabupaten_kota_id", "fk_sd_kecamatan_id", "fk_sd_kelurahan_id", + "ds_sd_provinsi", "ds_sd_kabupaten_kota", "ds_sd_kecamatan", "ds_sd_kelurahan", + }, + Values: []interface{}{ + req.Name, req.MedicalRecordNumber, req.PhoneNumber, req.Gender, + req.BirthDate, req.Address, req.Active, req.FKSdProvinsiID, + req.FKSdKabupatenKotaID, req.FKSdKecamatanID, req.FKSdKelurahanID, + req.DsSdProvinsi, req.DsSdKabupatenKota, req.DsSdKecamatan, req.DsSdKelurahan, + }, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "medical_record_number", Operator: queryUtils.OpEqual, Value: req.MedicalRecordNumber}, + }, + LogicOp: "AND", + }} + returningCols := []string{ + "id", "name", "medical_record_number", "phone_number", "gender", + "birth_date", "address", "active", "fk_sd_provinsi_id", + "fk_sd_kabupaten_kota_id", "fk_sd_kecamatan_id", "fk_sd_kelurahan_id", + "ds_sd_provinsi", "ds_sd_kabupaten_kota", "ds_sd_kecamatan", "ds_sd_kelurahan", + } + + sql, args, err := h.queryBuilder.BuildUpdateQuery("master.ms_patient", updateData, filters, returningCols...) + if err != nil { + h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError) + return + } + + var dataMs_patient patientModels.Patient + err = dbConn.GetContext(ctx, &dataMs_patient, sql, args...) + if err != nil { + if err.Error() == "sql: no rows in result set" { + h.respondError(c, "Patient not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update patient", err, http.StatusInternalServerError) + } + return + } + + // Invalidate cache + cacheKey := fmt.Sprintf("patient:medical_record_number:%s", medical_record_number) + h.cache.Delete(cacheKey) + + if oldData.ID != 0 { + h.invalidateRelatedCache() + } + + response := patientModels.PatientUpdateResponse{ + Message: "Patient berhasil diperbarui", + Data: &dataMs_patient, + } + + c.JSON(http.StatusOK, response) +} + +// DeletePatient godoc +// @Summary Delete patient +// @Description Soft deletes a patient by setting status to 'deleted' +// @Tags Patient +// @Accept json +// @Produce json +// @Param id path string true "Medical Record Number" +// @Success 200 {object} patientModels.PatientDeleteResponse "Patient deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Patient not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient/{id} [delete] +func (h *PatientHandler) DeleteMs_patient(c *gin.Context) { + medical_record_number := c.Param("medical_record_number") + if medical_record_number == "" { + h.respondError(c, "Invalid medical_record_number format", fmt.Errorf("medical_record_number cannot be empty"), http.StatusBadRequest) + return + } + + // Get data for cache invalidation + var dataToDelete patientModels.Patient + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err == nil { + ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) + defer cancel() + + query := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{{Expression: "*"}}, + Filters: []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "mp.medical_record_number", Operator: queryUtils.OpEqual, Value: medical_record_number}, + }, + LogicOp: "AND", + }}, + Limit: 1, + } + + err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, query, &dataToDelete) + if err != nil { + logger.Error("Failed to fetch data for cache invalidation", map[string]interface{}{"error": err.Error(), "medical_record_number": medical_record_number}) + } + } + + dbConn, err = h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Soft delete by changing active status + updateData := queryUtils.UpdateData{ + Columns: []string{"active"}, + Values: []interface{}{false}, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "medical_record_number", Operator: queryUtils.OpEqual, Value: medical_record_number}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + + result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "master.ms_patient", updateData, filters) + if err != nil { + h.logAndRespondError(c, "Failed to delete patient", err, http.StatusInternalServerError) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + h.logAndRespondError(c, "Failed to get affected rows", err, http.StatusInternalServerError) + return + } + if rowsAffected == 0 { + h.respondError(c, "Patient not found", sql.ErrNoRows, http.StatusNotFound) + return + } + + // Invalidate cache + cacheKey := fmt.Sprintf("patient:medical_record_number:%s", medical_record_number) + h.cache.Delete(cacheKey) + + if dataToDelete.ID != 0 { + h.invalidateRelatedCache() + } + + response := patientModels.PatientDeleteResponse{ + Message: "Patient berhasil dihapus", + MedicalRecordNumber: medical_record_number, + } + + c.JSON(http.StatusOK, response) +} + +// GetPatientStats godoc +// @Summary Get patient statistics +// @Description Returns comprehensive statistics about patient data +// @Tags Patient +// @Accept json +// @Produce json +// @Param status query string false "Filter statistics by status" +// @Success 200 {object} models.AggregateData "Statistics data" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/patients/stats [get] +// func (h *PatientHandler) GetPatientStats(c *gin.Context) { +// dbConn, err := h.db.GetSQLXDB("db_antrean") +// if err != nil { +// h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) +// return +// } + +// ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) +// defer cancel() + +// // Parse filters +// var filters []queryUtils.DynamicFilter +// if status := c.Query("status"); status != "" && models.IsValidStatus(status) { +// filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: status}) +// } + +// var filterGroups []queryUtils.FilterGroup +// if len(filters) > 0 { +// filterGroups = append(filterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) +// } + +// aggregateData, err := h.getAggregateData(ctx, dbConn, filterGroups) +// if err != nil { +// h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) +// return +// } + +// c.JSON(http.StatusOK, gin.H{ +// "message": "Statistik patient berhasil diambil", +// "data": aggregateData, +// }) +// } + +// SearchPatientAdvanced provides advanced search capabilities +// func (h *PatientHandler) SearchPatientAdvanced(c *gin.Context) { +// ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) +// defer cancel() + +// // Parse complex search parameters +// searchQuery := c.Query("norm") +// if searchQuery == "" { +// // If no search query provided, return all records with default sorting +// query := queryUtils.DynamicQuery{ +// From: "master.ms_patient", +// Aliases: "mp", +// Fields: []queryUtils.SelectField{ +// {Expression: "mp.id", Alias: "id"}, +// {Expression: "mp.name", Alias: "name"}, +// {Expression: "mp.medical_record_number", Alias: "medical_record_number"}, +// {Expression: "mp.phone_number", Alias: "phone_number"}, +// {Expression: "mp.gender", Alias: "gender"}, +// {Expression: "mp.birth_date", Alias: "birth_date"}, +// {Expression: "mp.address", Alias: "address"}, +// {Expression: "mp.active", Alias: "active"}, +// }, +// Filters: []queryUtils.FilterGroup{{ +// Filters: []queryUtils.DynamicFilter{ +// {Column: "mp.active", Operator: queryUtils.OpEqual, Value: true}, +// }, +// LogicOp: "AND", +// }}, +// Sort: []queryUtils.SortField{{ +// Column: "mp.id", +// Order: "ASC", +// }}, +// Limit: 20, +// Offset: 0, +// } + +// // Parse pagination if provided +// if limit := c.Query("limit"); limit != "" { +// if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { +// query.Limit = l +// } +// } + +// if offset := c.Query("offset"); offset != "" { +// if o, err := strconv.Atoi(offset); err == nil && o >= 0 { +// query.Offset = o +// } +// } + +// // Get database connection +// dbConn, err := h.db.GetSQLXDB("db_antrean") +// if err != nil { +// h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) +// return +// } + +// // Execute query to get all records +// var results []map[string]interface{} +// err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) +// if err != nil { +// h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) +// return +// } + +// patients := h.processQueryResults(results) +// total, err := h.getTotalCount(ctx, dbConn, query) +// if err != nil { +// h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) +// return +// } + +// // Build response +// meta := h.calculateMeta(query.Limit, query.Offset, total) +// response := patientModels.PatientGetResponse{ +// Message: "All records retrieved (no search query provided)", +// Data: patients, +// Meta: meta, +// } + +// c.JSON(http.StatusOK, response) +// return +// } + +// // Build dynamic query for search +// filters := []queryUtils.DynamicFilter{ +// {Column: "mp.medical_record_number", Operator: queryUtils.OpContains, Value: searchQuery}, +// {Column: "mp.active", Operator: queryUtils.OpEqual, Value: true}, +// } + +// query := queryUtils.DynamicQuery{ +// From: "master.ms_patient", +// Aliases: "mp", +// Fields: []queryUtils.SelectField{ +// {Expression: "mp.id", Alias: "id"}, +// {Expression: "mp.name", Alias: "name"}, +// {Expression: "mp.medical_record_number", Alias: "medical_record_number"}, +// {Expression: "mp.phone_number", Alias: "phone_number"}, +// {Expression: "mp.gender", Alias: "gender"}, +// {Expression: "mp.birth_date", Alias: "birth_date"}, +// {Expression: "mp.address", Alias: "address"}, +// {Expression: "mp.active", Alias: "active"}, +// }, +// Filters: []queryUtils.FilterGroup{{ +// Filters: filters, +// LogicOp: "AND", +// }}, +// Sort: []queryUtils.SortField{{ +// Column: "mp.id", +// Order: "ASC", +// }}, +// Limit: 20, +// Offset: 0, +// } + +// // Parse pagination if provided +// if limit := c.Query("limit"); limit != "" { +// if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 { +// query.Limit = l +// } +// } +// if offset := c.Query("offset"); offset != "" { +// if o, err := strconv.Atoi(offset); err == nil && o >= 0 { +// query.Offset = o +// } +// } + +// // Get database connection +// dbConn, err := h.db.GetSQLXDB("db_antrean") +// if err != nil { +// h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) +// return +// } + +// // Execute search +// var results []map[string]interface{} +// err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) +// if err != nil { +// h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError) +// return +// } + +// patients := h.processQueryResults(results) +// total, err := h.getTotalCount(ctx, dbConn, query) +// if err != nil { +// h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) +// return +// } + +// // Build response +// meta := h.calculateMeta(query.Limit, query.Offset, total) +// response := patientModels.PatientGetResponse{ +// Message: fmt.Sprintf("Search results for '%s'", searchQuery), +// Data: patients, +// Meta: meta, +// } + +// c.JSON(http.StatusOK, response) +// } + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +func (h *PatientHandler) processQueryResults(results []map[string]interface{}) []patientModels.Patient { + patients := make([]patientModels.Patient, 0, len(results)) + + for _, result := range results { + patient := patientModels.Patient{ + ID: getInt64(result, "id"), + Name: getNullString(result, "name"), + MedicalRecordNumber: getNullString(result, "medical_record_number"), + PhoneNumber: getNullString(result, "phone_number"), + Gender: getNullString(result, "gender"), + BirthDate: getNullTime(result, "birth_date"), + Address: getNullString(result, "address"), + Active: getNullBool(result, "active"), + FKSdProvinsiID: getNullableInt32(result, "fk_sd_provinsi_id"), + FKSdKabupatenKotaID: getNullableInt32(result, "fk_sd_kabupaten_kota_id"), + FKSdKecamatanID: getNullableInt32(result, "fk_sd_kecamatan_id"), + FKSdKelurahanID: getNullableInt32(result, "fk_sd_kelurahan_id"), + DsSdProvinsi: getNullString(result, "ds_sd_provinsi"), + DsSdKabupatenKota: getNullString(result, "ds_sd_kabupaten_kota"), + DsSdKecamatan: getNullString(result, "ds_sd_kecamatan"), + DsSdKelurahan: getNullString(result, "ds_sd_kelurahan"), + } + patients = append(patients, patient) + } + + return patients +} + +// Helper functions for extracting values from map[string]interface{} +func getInt64(m map[string]interface{}, key string) int64 { + if val, ok := m[key]; ok { + switch v := val.(type) { + case int64: + return v + case int: + return int64(v) + case float64: + return int64(v) + } + } + return 0 +} + +func getString(m map[string]interface{}, key string) string { + if val, ok := m[key]; ok { + if v, ok := val.(string); ok { + return v + } + } + return "" +} + +func getNullString(m map[string]interface{}, key string) sql.NullString { + if val, ok := m[key]; ok { + if ns, ok := val.(sql.NullString); ok { + return ns + } + if s, ok := val.(string); ok { + return sql.NullString{String: s, Valid: true} + } + } + return sql.NullString{Valid: false} +} + +func getNullableInt32(m map[string]interface{}, key string) models.NullableInt32 { + if val, ok := m[key]; ok { + if v, ok := val.(models.NullableInt32); ok { + return v + } + } + return models.NullableInt32{} +} + +func getNullBool(m map[string]interface{}, key string) sql.NullBool { + if val, ok := m[key]; ok { + if v, ok := val.(sql.NullBool); ok { + return v + } + } + return sql.NullBool{} +} + +func getNullTime(m map[string]interface{}, key string) sql.NullTime { + if val, ok := m[key]; ok { + if v, ok := val.(sql.NullTime); ok { + return v + } + } + return sql.NullTime{} +} + +func (h *PatientHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) { + countQuery := queryUtils.DynamicQuery{ + From: query.From, + Aliases: query.Aliases, + Filters: query.Filters, + Joins: query.Joins, + } + + count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery) + if err != nil { + return 0, fmt.Errorf("failed to execute count query: %w", err) + } + + return int(count), nil +} + +func (h *PatientHandler) invalidateRelatedCache() { + h.cache.DeleteByPrefix("patient:search:") + h.cache.DeleteByPrefix("patient:dynamic:") + h.cache.DeleteByPrefix("patient:stats:") + h.cache.DeleteByPrefix("patient:id:") +} + +func (h *PatientHandler) getAggregateData(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + } + + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 2) + + // Count by status + wg.Add(1) + go func() { + defer wg.Done() + queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) + defer queryCancel() + + query := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{ + {Expression: "active"}, + {Expression: "COUNT(*)", Alias: "count"}, + }, + Filters: filterGroups, + GroupBy: []string{"active"}, + } + var results []struct { + Status string `db:"active"` + Count int `db:"count"` + } + err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + mu.Lock() + for _, result := range results { + aggregate.ByStatus[result.Status] = result.Count + switch result.Status { + case "active": + aggregate.TotalActive = result.Count + case "draft": + aggregate.TotalDraft = result.Count + case "inactive": + aggregate.TotalInactive = result.Count + } + } + mu.Unlock() + }() + + // Get last updated and today's stats + wg.Add(1) + go func() { + defer wg.Done() + queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) + defer queryCancel() + + // Last updated + query1 := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{{Expression: "MAX(date_updated)"}}, + Filters: filterGroups, + } + var lastUpdated sql.NullTime + err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated) + if err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + today := time.Now().Format("2006-01-02") + + // Query for created_today + createdTodayQuery := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{ + {Expression: "COUNT(*)", Alias: "count"}, + }, + Filters: append(filterGroups, queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "DATE(date_created)", Operator: queryUtils.OpEqual, Value: today}, + }, + LogicOp: "AND", + }), + } + + var createdToday int + err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday) + if err != nil { + errChan <- fmt.Errorf("created today query failed: %w", err) + return + } + + // Query for updated_today + updatedTodayQuery := queryUtils.DynamicQuery{ + From: "master.ms_patient", + Aliases: "mp", + Fields: []queryUtils.SelectField{ + {Expression: "COUNT(*)", Alias: "count"}, + }, + Filters: append(filterGroups, queryUtils.FilterGroup{ + Filters: []queryUtils.DynamicFilter{ + {Column: "DATE(date_updated)", Operator: queryUtils.OpEqual, Value: today}, + {Column: "DATE(date_created)", Operator: queryUtils.OpNotEqual, Value: today}, + }, + LogicOp: "AND", + }), + } + + var updatedToday int + err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday) + if err != nil { + errChan <- fmt.Errorf("updated today query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + wg.Wait() + close(errChan) + + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} + +func (h *PatientHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode}) + h.respondError(c, message, err, statusCode) +} + +func (h *PatientHandler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()}) +} + +func (h *PatientHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages, currentPage := 0, 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit + currentPage = (offset / limit) + 1 + } + return models.MetaResponse{ + Limit: limit, Offset: offset, Total: total, TotalPages: totalPages, + CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, + } +} diff --git a/internal/handlers/permission/rol_permission.go b/internal/handlers/permission/rol_permission.go new file mode 100644 index 0000000..a0c5960 --- /dev/null +++ b/internal/handlers/permission/rol_permission.go @@ -0,0 +1,487 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models" + permissionModels "api-service/internal/models/permission" + queryUtils "api-service/internal/utils/query" + "api-service/internal/utils/validation" + "api-service/pkg/logger" + "context" + "database/sql" + "fmt" + "net/http" + "strconv" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/jmoiron/sqlx" +) + +// ============================================================================= +// GLOBAL INITIALIZATION & VALIDATION +// ============================================================================= + +var ( + db database.Service + once sync.Once + validate *validator.Validate +) + +// Initialize the database connection and validator once +func init() { + once.Do(func() { + db = database.New(config.LoadConfig()) + validate = validator.New() + validate.RegisterValidation("rol_pages_status", validateRol_pagesStatus) + if db == nil { + logger.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for rol_pages status +func validateRol_pagesStatus(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// ============================================================================= +// CACHE IMPLEMENTATION +// ============================================================================= + +// CacheEntry represents an entry in the cache +type CacheEntry struct { + Data interface{} + ExpiresAt time.Time +} + +// IsExpired checks if the cache entry has expired +func (e *CacheEntry) IsExpired() bool { + return time.Now().After(e.ExpiresAt) +} + +// InMemoryCache implements a simple in-memory cache with TTL +type InMemoryCache struct { + items sync.Map +} + +// NewInMemoryCache creates a new in-memory cache +func NewInMemoryCache() *InMemoryCache { + return &InMemoryCache{} +} + +// Get retrieves an item from the cache +func (c *InMemoryCache) Get(key string) (interface{}, bool) { + val, ok := c.items.Load(key) + if !ok { + return nil, false + } + + entry, ok := val.(*CacheEntry) + if !ok || entry.IsExpired() { + c.items.Delete(key) + return nil, false + } + + return entry.Data, true +} + +// Set stores an item in the cache with a TTL +func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) { + entry := &CacheEntry{ + Data: value, + ExpiresAt: time.Now().Add(ttl), + } + c.items.Store(key, entry) +} + +// Delete removes an item from the cache +func (c *InMemoryCache) Delete(key string) { + c.items.Delete(key) +} + +// DeleteByPrefix removes all items with a specific prefix +func (c *InMemoryCache) DeleteByPrefix(prefix string) { + c.items.Range(func(key, value interface{}) bool { + if keyStr, ok := key.(string); ok && len(keyStr) >= len(prefix) && keyStr[:len(prefix)] == prefix { + c.items.Delete(key) + } + return true + }) +} + +// ============================================================================= +// ROL_PERMISSION HANDLER STRUCT +// ============================================================================= + +// Rol_permissionHandler handles rol_permission services +type Rol_permissionHandler struct { + db database.Service + queryBuilder *queryUtils.QueryBuilder + validator *validation.DynamicValidator + cache *InMemoryCache +} + +// NewRol_permissionHandler creates a new Rol_permissionHandler +func NewRol_permissionHandler() *Rol_permissionHandler { + // Initialize QueryBuilder with allowed columns for security. + queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL). + SetAllowedColumns([]string{ + "id", "create", "read", "update", "disable", "delete", "active", + "fk_rol_pages_id", "role_keycloak", "group_keycloak", + }) + + return &Rol_permissionHandler{ + db: db, + queryBuilder: queryBuilder, + validator: validation.NewDynamicValidator(queryBuilder), + cache: NewInMemoryCache(), + } +} + +// ============================================================================= +// HANDLER ENDPOINTS +// ============================================================================= + +// GetRol_permissions godoc +// @Summary Get Permissions List +// @Description Get list of permissions with pagination and filters +// @Tags Permissions +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param page_id query int false "Filter by parent page ID" +// @Param active query string false "Filter by status" +// @Success 200 {object} permissionModels.PermissionsGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /permission [get] +func (h *Rol_permissionHandler) GetRol_permissions(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + query := queryUtils.DynamicQuery{ + From: "role_access.rol_permission", + Aliases: "rper", + Fields: []queryUtils.SelectField{ + {Expression: "rper.id", Alias: "id"}, + {Expression: "rper.create", Alias: "create"}, + {Expression: "rper.read", Alias: "read"}, + {Expression: "rper.update", Alias: "update"}, + {Expression: "rper.disable", Alias: "disable"}, + {Expression: "rper.delete", Alias: "delete"}, + {Expression: "rper.active", Alias: "active"}, + {Expression: "rper.fk_rol_pages_id", Alias: "fk_rol_pages_id"}, + {Expression: "rper.role_keycloak", Alias: "role_keycloak"}, + {Expression: "rper.group_keycloak", Alias: "group_keycloak"}, + }, + Sort: []queryUtils.SortField{ + {Column: "rper.fk_rol_pages_id", Order: "ASC"}, + {Column: "rper.id", Order: "ASC"}, + }, + } + + // Parse pagination + if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 { + query.Limit = limit + } + if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 { + query.Offset = offset + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Parse filters + var filters []queryUtils.DynamicFilter + if pageID := c.Query("page_id"); pageID != "" { + if id, err := strconv.ParseInt(pageID, 10, 64); err == nil { + filters = append(filters, queryUtils.DynamicFilter{Column: "fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: id}) + } + } + if active := c.Query("active"); active != "" && models.IsValidStatus(active) { + filters = append(filters, queryUtils.DynamicFilter{Column: "active", Operator: queryUtils.OpEqual, Value: active}) + } + + if len(filters) > 0 { + query.Filters = []queryUtils.FilterGroup{{Filters: filters, LogicOp: "AND"}} + } + + // Execute query + var permissions []permissionModels.Rol_permission + err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &permissions) + if err != nil { + h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) + return + } + + // Get total count for pagination + total, err := h.getTotalCount(ctx, dbConn, query) + if err != nil { + h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) + return + } + + // Build response + meta := h.calculateMeta(query.Limit, query.Offset, total) + response := permissionModels.PermissionsGetResponse{ + Message: "Data rol_permission berhasil diambil", + Data: permissions, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +} + +// CreateRol_permission godoc +// @Summary Create Permission +// @Description Create a new permission +// @Tags Permissions +// @Accept json +// @Produce json +// @Param request body permissionModels.PermissionCreateRequest true "Permission creation request" +// @Success 201 {object} permissionModels.PermissionCreateResponse "Permission created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /permission [post] +func (h *Rol_permissionHandler) CreateRol_permission(c *gin.Context) { + var req permissionModels.PermissionCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + data := queryUtils.InsertData{ + Columns: []string{"create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"}, + Values: []interface{}{req.Create, req.Read, req.Update, req.Disable, req.Delete, req.Active, req.FkRolPagesID, req.RoleKeycloak, req.GroupKeycloak}, + } + returningCols := []string{"id", "create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"} + + sql, args, err := h.queryBuilder.BuildInsertQuery("role_access.rol_permission", data, returningCols...) + if err != nil { + h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError) + return + } + + var dataPermission permissionModels.Rol_permission + err = dbConn.GetContext(ctx, &dataPermission, sql, args...) + if err != nil { + h.logAndRespondError(c, "Failed to create rol_permission", err, http.StatusInternalServerError) + return + } + + h.invalidateRelatedCache() + response := permissionModels.PermissionCreateResponse{Message: "Rol_permission berhasil dibuat", Data: &dataPermission} + c.JSON(http.StatusCreated, response) +} + +// UpdateRol_permission godoc +// @Summary Update Permission +// @Description Update an existing permission +// @Tags Permissions +// @Accept json +// @Produce json +// @Param id path string true "Permission ID" +// @Param request body permissionModels.PermissionUpdateRequest true "Permission update request" +// @Success 200 {object} permissionModels.PermissionUpdateResponse "Permission updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Permission not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /permission/{id} [put] +func (h *Rol_permissionHandler) UpdateRol_permission(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) + return + } + + var req permissionModels.PermissionUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + idInt, err := strconv.Atoi(id) + if err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + req.ID = &idInt + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + updateData := queryUtils.UpdateData{ + Columns: []string{"create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"}, + Values: []interface{}{req.Create, req.Read, req.Update, req.Disable, req.Delete, req.Active, req.FkRolPagesID, req.RoleKeycloak, req.GroupKeycloak}, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: req.ID}, + }, + LogicOp: "AND", + }} + returningCols := []string{"id", "create", "read", "update", "disable", "delete", "active", "fk_rol_pages_id", "role_keycloak", "group_keycloak"} + + sql, args, err := h.queryBuilder.BuildUpdateQuery("role_access.rol_permission", updateData, filters, returningCols...) + if err != nil { + h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError) + return + } + + var dataPermission permissionModels.Rol_permission + err = dbConn.GetContext(ctx, &dataPermission, sql, args...) + if err != nil { + if err.Error() == "sql: no rows in result set" { + h.respondError(c, "Rol_permission not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update rol_permission", err, http.StatusInternalServerError) + } + return + } + + h.invalidateRelatedCache() + response := permissionModels.PermissionUpdateResponse{Message: "Rol_permission berhasil diperbarui", Data: &dataPermission} + c.JSON(http.StatusOK, response) +} + +// DeleteRol_permission godoc +// @Summary Delete Permission +// @Description Delete a permission +// @Tags Permissions +// @Accept json +// @Produce json +// @Param id path string true "Permission ID" +// @Success 200 {object} permissionModels.PermissionDeleteResponse "Permission deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Permission not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /permission/{id} [delete] +func (h *Rol_permissionHandler) DeleteRol_permission(c *gin.Context) { + id := c.Param("id") + if id == "" { + h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetSQLXDB("db_antrean") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + // Soft delete by changing status + updateData := queryUtils.UpdateData{ + Columns: []string{"active"}, + Values: []interface{}{false}, + } + filters := []queryUtils.FilterGroup{{ + Filters: []queryUtils.DynamicFilter{ + {Column: "id", Operator: queryUtils.OpEqual, Value: id}, + {Column: "active", Operator: queryUtils.OpNotEqual, Value: false}, + }, + LogicOp: "AND", + }} + + result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "role_access.rol_permission", updateData, filters) + if err != nil { + h.logAndRespondError(c, "Failed to delete rol_permission", err, http.StatusInternalServerError) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + h.logAndRespondError(c, "Failed to get affected rows", err, http.StatusInternalServerError) + return + } + if rowsAffected == 0 { + h.respondError(c, "Rol_permission not found", sql.ErrNoRows, http.StatusNotFound) + return + } + + h.invalidateRelatedCache() + response := permissionModels.PermissionDeleteResponse{Message: "Rol_permission berhasil dihapus", ID: id} + c.JSON(http.StatusOK, response) +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +// invalidateRelatedCache clears cache entries that might be affected by permission changes. +// This includes pages cache, since pages contain permissions. +func (h *Rol_permissionHandler) invalidateRelatedCache() { + h.cache.DeleteByPrefix("rol_permission:") + h.cache.DeleteByPrefix("rol_pages:") // Invalidate pages cache as well +} + +func (h *Rol_permissionHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) { + countQuery := queryUtils.DynamicQuery{ + From: query.From, + Aliases: query.Aliases, + Filters: query.Filters, + } + + count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery) + if err != nil { + return 0, fmt.Errorf("failed to execute count query: %w", err) + } + + return int(count), nil +} + +func (h *Rol_permissionHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode}) + h.respondError(c, message, err, statusCode) +} + +func (h *Rol_permissionHandler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()}) +} + +func (h *Rol_permissionHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages, currentPage := 0, 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit + currentPage = (offset / limit) + 1 + } + return models.MetaResponse{ + Limit: limit, Offset: offset, Total: total, TotalPages: totalPages, + CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, + } +} diff --git a/internal/models/component/rol_component.go b/internal/models/component/rol_component.go new file mode 100644 index 0000000..e55b7c0 --- /dev/null +++ b/internal/models/component/rol_component.go @@ -0,0 +1,107 @@ +package component + +import ( + "api-service/internal/models" + "database/sql" + "encoding/json" + "time" +) + +// Rol_component represents the data structure for the role_access.rol_component table +// with proper null handling and optimized JSON marshaling. +type Rol_component struct { + ID int64 `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description sql.NullString `json:"description,omitempty" db:"description"` + Directory string `json:"directory" db:"directory"` + Active bool `json:"active" db:"active"` + FkRolPagesID int64 `json:"fk_rol_pages_id" db:"fk_rol_pages_id"` + Sort int64 `json:"sort" db:"sort"` +} + +// MarshalJSON for Rol_component handles the sql.NullString field 'Description' +// to ensure it appears as null, an empty string, or the string value in JSON. +func (r Rol_component) MarshalJSON() ([]byte, error) { + type Alias Rol_component + aux := &struct { + *Alias + Description *string `json:"description,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Description.Valid { + aux.Description = &r.Description.String + } + return json.Marshal(aux) +} + +// Helper method to safely get Description +func (r *Rol_component) GetDescription() string { + if r.Description.Valid { + return r.Description.String + } + return "" +} + +// ============================================================================= +// REQUEST & RESPONSE STRUCTS FOR ROL_COMPONENT +// ============================================================================= + +// ComponentCreateRequest defines the structure for creating a new component. +type ComponentCreateRequest struct { + Name string `json:"name" validate:"required,min=1,max=100"` + Description *string `json:"description" validate:"omitempty,max=255"` + Directory string `json:"directory" validate:"required,min=1,max=255"` + Active bool `json:"active"` + Sort int64 `json:"sort"` + FkRolPagesID int64 `json:"fk_rol_pages_id" validate:"required,min=1"` +} + +// ComponentUpdateRequest defines the structure for updating an existing component. +// ID is handled via URL parameter, so it's omitted from JSON with `json:"-"`. +type ComponentUpdateRequest struct { + ID *int `json:"-" validate:"required"` // ID is from URL param + Name *string `json:"name" validate:"omitempty,min=1,max=100"` + Description *string `json:"description" validate:"omitempty,max=255"` + Directory *string `json:"directory" validate:"omitempty,min=1,max=255"` + Active *bool `json:"active" validate:"omitempty"` + Sort *int `json:"sort" validate:"omitempty,min=1"` + FkRolPagesID *int `json:"fk_rol_pages_id" validate:"omitempty,min=1"` +} + +// ComponentsGetResponse defines the response structure for getting a list of components. +type ComponentsGetResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data []Rol_component `json:"data"` + Meta models.MetaResponse `json:"meta"` +} + +// ComponentCreateResponse defines the response structure for creating a component. +type ComponentCreateResponse struct { + Message string `json:"message"` + Data *Rol_component `json:"data"` +} + +// ComponentUpdateResponse defines the response structure for updating a component. +type ComponentUpdateResponse struct { + Message string `json:"message"` + Data *Rol_component `json:"data"` +} + +// ComponentDeleteResponse defines the response structure for deleting a component. +type ComponentDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +// Rol_componentFilter defines the structure for query parameters when filtering components. +type Rol_componentFilter struct { + PageID *int64 `json:"page_id,omitempty" form:"page_id"` + Active *bool `json:"active,omitempty" form:"active"` + Search *string `json:"search,omitempty" form:"search"` + DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` + DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` + Status *string `json:"status,omitempty" form:"status"` +} diff --git a/internal/models/pages/rol_pages.go b/internal/models/pages/rol_pages.go new file mode 100644 index 0000000..6924301 --- /dev/null +++ b/internal/models/pages/rol_pages.go @@ -0,0 +1,274 @@ +package pages + +import ( + "api-service/internal/models" + "database/sql" + "encoding/json" + "time" + + "github.com/lib/pq" +) + +// Rol_pages represents the data structure for the role_access.rol_pages table +// with proper null handling and optimized JSON marshaling +type Rol_pages struct { + ID int64 `json:"id" db:"id"` + Name string `json:"name," db:"name"` + Icon sql.NullString `json:"icon,omitempty" db:"icon"` + Url sql.NullString `json:"url,omitempty" db:"url"` + Level int64 `json:"level" db:"level"` + Sort int64 `json:"sort" db:"sort"` + Parent models.NullableInt32 `json:"parent,omitempty" db:"parent"` + Active bool `json:"active" db:"active"` + Components []Rol_component `json:"list_component,omitempty" db:"list_component"` + Permissions []Rol_permission `json:"list_permission,omitempty" db:"permission"` +} + +type Rol_component struct { + ID int64 `json:"id,omitempty" db:"id"` + Name string `json:"name,omitempty" db:"name"` + Description sql.NullString `json:"description,omitempty" db:"description"` + Directory string `json:"directory,omitempty" db:"directory"` + Active bool `json:"active,omitempty" db:"active"` + FkRolPagesID int64 `json:"fk_rol_pages_id,omitempty" db:"fk_rol_pages_id"` + Sort int64 `json:"sort,omitempty" db:"sort"` +} + +type Rol_permission struct { + ID int64 `json:"id,omitempty" db:"id"` + Create sql.NullBool `json:"create,omitempty" db:"create"` + Read sql.NullBool `json:"read,omitempty" db:"read"` + Update sql.NullBool `json:"update,omitempty" db:"update"` + Disable sql.NullBool `json:"disable,omitempty" db:"disable"` // Note: "disable" is a Go keyword, so "Disable" is used for the field name. + Delete sql.NullBool `json:"delete,omitempty" db:"delete"` + Active sql.NullBool `json:"active,omitempty" db:"active"` + FkRolPagesID models.NullableInt32 `json:"fk_rol_pages_id,omitempty" db:"fk_rol_pages_id"` + RoleKeycloak pq.StringArray `json:"role_keycloak,omitempty" db:"role_keycloak"` // Use NullString for optional text fields + GroupKeycloak pq.StringArray `json:"group_keycloak,omitempty" db:"group_keycloak"` +} + +// Custom JSON marshaling for Rol_pages so NULL values don't appear in response +func (r Rol_pages) MarshalJSON() ([]byte, error) { + type Alias Rol_pages + aux := &struct { + *Alias + Icon *string `json:"icon,omitempty"` + Url *string `json:"url,omitempty"` + Parent *int `json:"parent,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Icon.Valid { + aux.Icon = &r.Icon.String + } + if r.Url.Valid { + aux.Url = &r.Url.String + } + if r.Parent.Valid { + parent := int(r.Parent.Int32) + aux.Parent = &parent + } + return json.Marshal(aux) +} + +func (r Rol_component) MarshalJSON() ([]byte, error) { + type Alias Rol_component + aux := &struct { + *Alias + Description *string `json:"description,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Description.Valid { + aux.Description = &r.Description.String + } + return json.Marshal(aux) +} + +func (r Rol_permission) MarshalJSON() ([]byte, error) { + type Alias Rol_permission + aux := &struct { + *Alias + Create *bool `json:"create,omitempty"` + Read *bool `json:"read,omitempty"` + Update *bool `json:"update,omitempty"` + Disable *bool `json:"disable,omitempty"` + Delete *bool `json:"delete,omitempty"` + Active *bool `json:"active,omitempty"` + FkRolPagesID *int `json:"fk_rol_pages_id,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Create.Valid { + aux.Create = &r.Create.Bool + } + if r.Read.Valid { + aux.Read = &r.Read.Bool + } + if r.Update.Valid { + aux.Update = &r.Update.Bool + } + if r.Disable.Valid { + aux.Disable = &r.Disable.Bool + } + if r.Delete.Valid { + aux.Delete = &r.Delete.Bool + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + if r.FkRolPagesID.Valid { + fkRolPagesID := int(r.FkRolPagesID.Int32) + aux.FkRolPagesID = &fkRolPagesID + } + return json.Marshal(aux) +} + +// Helper method to safely get Icon +func (r *Rol_pages) GetIcon() string { + if r.Icon.Valid { + return r.Icon.String + } + return "" +} + +// Helper method to safely get Url +func (r *Rol_pages) GetUrl() string { + if r.Url.Valid { + return r.Url.String + } + return "" +} + +// Helper method to safely get Parent +func (r *Rol_pages) GetParent() int { + if r.Parent.Valid { + return int(r.Parent.Int32) + } + return 0 +} + +// Helper method to safely get Description +func (r *Rol_component) GetDescription() string { + if r.Description.Valid { + return r.Description.String + } + return "" +} + +// Helper method to safely get Sort +func (r *Rol_pages) GetSort() int64 { + return r.Sort +} + +// Response struct for GET list +type PagesGetResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data []Rol_pages `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +// Response struct for create +type PagesCreateResponse struct { + Message string `json:"message"` + Data *Rol_pages `json:"data"` +} + +// Request struct for create +type PagesCreateRequest struct { + //Status string `json:"status" validate:"required,oneof=draft active inactive"` + ID *int `json:"id"` + Name string `json:"name" validate:"required,min=1,max=20"` + Icon *string `json:"icon" validate:"omitempty,min=1,max=20"` + Url *string `json:"url" validate:"omitempty,min=1,max=100"` + Level int `json:"level"` + Sort int `json:"sort"` + Parent *int `json:"parent" validate:"omitempty,min=1"` + Active bool `json:"active"` + Components []ComponentCreateRequest `json:"components,omitempty" validate:"omitempty,dive"` + Permissions []PermissionCreateRequest `json:"permissions,omitempty" validate:"omitempty,dive"` +} + +type ComponentCreateRequest struct { + Name string `json:"name" validate:"required,min=1,max=100"` + Description *string `json:"description" validate:"omitempty,max=255"` + Directory string `json:"directory" validate:"required,min=1,max=255"` + Active bool `json:"active"` + Sort int64 `json:"sort"` + // FkRolPagesID int64 `json:"fk_rol_pages_id" validate:"required,min=1"` +} + +type PermissionCreateRequest struct { + Create *bool `json:"create" validate:"omitempty"` + Read *bool `json:"read" validate:"omitempty"` + Update *bool `json:"update" validate:"omitempty"` + Disable *bool `json:"disable" validate:"omitempty"` + Delete *bool `json:"delete" validate:"omitempty"` + Active *bool `json:"active" validate:"omitempty"` + // FkRolPagesID *int64 `json:"fk_rol_pages_id" validate:"omitempty,"` + RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"` + GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"` +} + +type ComponentUpdateRequest struct { + ID *int `json:"id" validate:"required"` + Name *string `json:"name" validate:"omitempty,min=1,max=100"` + Description *string `json:"description" validate:"omitempty,max=255"` + Directory *string `json:"directory" validate:"omitempty,min=1,max=255"` + Active *bool `json:"active" validate:"omitempty"` + Sort *int `json:"sort" validate:"omitempty,min=1"` + FkRolPagesID *int `json:"fk_rol_pages_id" validate:"omitempty,min=1"` +} + +type PermissionUpdateRequest struct { + ID *int `json:"id" validate:"required"` + Create *bool `json:"create" validate:"omitempty"` + Read *bool `json:"read" validate:"omitempty"` + Update *bool `json:"update" validate:"omitempty"` + Disable *bool `json:"disable" validate:"omitempty"` + Delete *bool `json:"delete" validate:"omitempty"` + Active *bool `json:"active" validate:"omitempty"` + FkRolPagesID *int `json:"fk_rol_pages_id" validate:"omitempty"` + RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"` + GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"` +} + +// Response struct for update +type PagesUpdateResponse struct { + Message string `json:"message"` + Data *Rol_pages `json:"data"` +} + +// Update request +type PagesUpdateRequest struct { + ID *int `json:"-" validate:"required"` + // Status string `json:"status" validate:"required,oneof=draft active inactive"` + Name *string `json:"name" validate:"omitempty,min=1,max=20"` + Icon *string `json:"icon" validate:"omitempty,min=1,max=20"` + Url *string `json:"url" validate:"omitempty,min=1,max=100"` + Level *int `json:"level" validate:"omitempty,min=1"` + Sort *int `json:"sort" validate:"omitempty,min=1"` + Parent *int `json:"parent" validate:"omitempty,min=1"` + Active *bool `json:"active" validate:"omitempty"` + Components []ComponentUpdateRequest `json:"components,omitempty" validate:"omitempty,dive"` + Permissions []PermissionUpdateRequest `json:"permissions,omitempty" validate:"omitempty,dive"` +} + +// Response struct for delete +type PagesDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +// Filter struct for query parameters +type Rol_pagesFilter struct { + Search *string `json:"search,omitempty" form:"search"` + DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` + DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` + Status *string `json:"status,omitempty" form:"status"` +} diff --git a/internal/models/patient/ms_patient.go b/internal/models/patient/ms_patient.go new file mode 100644 index 0000000..9001241 --- /dev/null +++ b/internal/models/patient/ms_patient.go @@ -0,0 +1,280 @@ +package patient + +import ( + "api-service/internal/models" + "database/sql" + "encoding/json" + "time" +) + +// type Date time.Time + +// func (d *Date) UnmarshalJSON(b []byte) error { +// s := strings.Trim(string(b), "\"") +// if s == "" || s == "null" { +// *d = Date(time.Time{}) +// return nil +// } +// t, err := time.Parse("2006-01-02", s) +// if err != nil { +// return fmt.Errorf("invalid date format, expected YYYY-MM-DD: %w", err) +// } +// *d = Date(t) +// return nil +// } + +// // new: MarshalJSON so responses use "YYYY-MM-DD" +// func (d Date) MarshalJSON() ([]byte, error) { +// t := time.Time(d) +// if t.IsZero() { +// return []byte("null"), nil +// } +// return []byte("\"" + t.Format("2006-01-02") + "\""), nil +// } + +// // new: database/sql/driver.Valuer implementation so DB driver can encode Date +// func (d Date) Value() (driver.Value, error) { +// t := time.Time(d) +// if t.IsZero() { +// return nil, nil +// } +// // return time.Time so pq/pgx has an encode plan for date/timestamp types +// return t, nil +// } + +// // new: sql.Scanner implementation so DB rows decode into Date +// func (d *Date) Scan(src interface{}) error { +// if src == nil { +// *d = Date(time.Time{}) +// return nil +// } +// switch v := src.(type) { +// case time.Time: +// *d = Date(v) +// return nil +// case []byte: +// s := string(v) +// if t, err := time.Parse("2006-01-02", s); err == nil { +// *d = Date(t) +// return nil +// } +// if t, err := time.Parse(time.RFC3339, s); err == nil { +// *d = Date(t) +// return nil +// } +// return fmt.Errorf("cannot parse date from []byte: %s", s) +// case string: +// s := v +// if s == "" { +// *d = Date(time.Time{}) +// return nil +// } +// if t, err := time.Parse("2006-01-02", s); err == nil { +// *d = Date(t) +// return nil +// } +// if t, err := time.Parse(time.RFC3339, s); err == nil { +// *d = Date(t) +// return nil +// } +// return fmt.Errorf("cannot parse date from string: %s", s) +// default: +// return fmt.Errorf("unsupported scan type for Date: %T", src) +// } +// } + +// func (d Date) ToTime() time.Time { +// return time.Time(d) +// } + +// Patient represents the data structure for the patient table +// with proper null handling and optimized JSON marshaling +type Patient struct { + ID int64 `json:"id" db:"id"` + Name sql.NullString `json:"name,omitempty" db:"name"` + MedicalRecordNumber sql.NullString `json:"medical_record_number,omitempty" db:"medical_record_number"` + PhoneNumber sql.NullString `json:"phone_number,omitempty" db:"phone_number"` + Gender sql.NullString `json:"gender,omitempty" db:"gender"` + BirthDate sql.NullTime `json:"birth_date,omitempty" db:"birth_date"` + Address sql.NullString `json:"address,omitempty" db:"address"` + Active sql.NullBool `json:"active,omitempty" db:"active"` + FKSdProvinsiID models.NullableInt32 `json:"fk_sd_provinsi_id,omitempty" db:"fk_sd_provinsi_id"` + FKSdKabupatenKotaID models.NullableInt32 `json:"fk_sd_kabupaten_kota_id,omitempty" db:"fk_sd_kabupaten_kota_id"` + FKSdKecamatanID models.NullableInt32 `json:"fk_sd_kecamatan_id,omitempty" db:"fk_sd_kecamatan_id"` + FKSdKelurahanID models.NullableInt32 `json:"fk_sd_kelurahan_id,omitempty" db:"fk_sd_kelurahan_id"` + DsSdProvinsi sql.NullString `json:"ds_sd_provinsi,omitempty" db:"ds_sd_provinsi"` + DsSdKabupatenKota sql.NullString `json:"ds_sd_kabupaten_kota,omitempty" db:"ds_sd_kabupaten_kota"` + DsSdKecamatan sql.NullString `json:"ds_sd_kecamatan,omitempty" db:"ds_sd_kecamatan"` + DsSdKelurahan sql.NullString `json:"ds_sd_kelurahan,omitempty" db:"ds_sd_kelurahan"` +} + +// Custom JSON marshaling untuk Patient agar NULL values tidak muncul di response +func (r Patient) MarshalJSON() ([]byte, error) { + type Alias Patient + aux := &struct { + Name *string `json:"name,omitempty"` + MedicalRecordNumber *string `json:"medical_record_number,omitempty"` + PhoneNumber *string `json:"phone_number,omitempty"` + Gender *string `json:"gender,omitempty"` + BirthDate *string `json:"birth_date,omitempty"` + Address *string `json:"address,omitempty"` + Active *bool `json:"active,omitempty"` + FKSdProvinsiID *int32 `json:"fk_sd_provinsi_id,omitempty"` + FKSdKabupatenKotaID *int32 `json:"fk_sd_kabupaten_kota_id,omitempty"` + FKSdKecamatanID *int32 `json:"fk_sd_kecamatan_id,omitempty"` + FKSdKelurahanID *int32 `json:"fk_sd_kelurahan_id,omitempty"` + DsSdProvinsi *string `json:"ds_sd_provinsi,omitempty"` + DsSdKabupatenKota *string `json:"ds_sd_kabupaten_kota,omitempty"` + DsSdKecamatan *string `json:"ds_sd_kecamatan,omitempty"` + DsSdKelurahan *string `json:"ds_sd_kelurahan,omitempty"` + *Alias + }{ + Alias: (*Alias)(&r), + } + + if r.Name.Valid { + aux.Name = &r.Name.String + } + if r.MedicalRecordNumber.Valid { + aux.MedicalRecordNumber = &r.MedicalRecordNumber.String + } + if r.PhoneNumber.Valid { + aux.PhoneNumber = &r.PhoneNumber.String + } + if r.Gender.Valid { + aux.Gender = &r.Gender.String + } + if r.BirthDate.Valid { + birthDateStr := r.BirthDate.Time.Format("2006-01-02") + aux.BirthDate = &birthDateStr + } + if r.Address.Valid { + aux.Address = &r.Address.String + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + if r.FKSdProvinsiID.Valid { + fksp := int32(r.FKSdProvinsiID.Int32) + aux.FKSdProvinsiID = &fksp + } + if r.FKSdKabupatenKotaID.Valid { + fksk := int32(r.FKSdKabupatenKotaID.Int32) + aux.FKSdKabupatenKotaID = &fksk + } + if r.FKSdKecamatanID.Valid { + fksc := int32(r.FKSdKecamatanID.Int32) + aux.FKSdKecamatanID = &fksc + } + if r.FKSdKelurahanID.Valid { + fksl := int32(r.FKSdKelurahanID.Int32) + aux.FKSdKelurahanID = &fksl + } + if r.DsSdProvinsi.Valid { + aux.DsSdProvinsi = &r.DsSdProvinsi.String + } + if r.DsSdKabupatenKota.Valid { + aux.DsSdKabupatenKota = &r.DsSdKabupatenKota.String + } + if r.DsSdKecamatan.Valid { + aux.DsSdKecamatan = &r.DsSdKecamatan.String + } + if r.DsSdKelurahan.Valid { + aux.DsSdKelurahan = &r.DsSdKelurahan.String + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *Patient) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +} + +// Response struct untuk GET by ID +type PatientGetByIDResponse struct { + Message string `json:"message"` + Data *Patient `json:"data"` +} + +// Enhanced GET response dengan pagination dan aggregation +type PatientGetResponse struct { + Message string `json:"message"` + Data []Patient `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +// Request struct untuk create +type PatientCreateRequest struct { + ID *int `json:"id"` + Name *string `json:"name" validate:"min=1,max=100"` + MedicalRecordNumber *string `json:"medical_record_number" validate:"min=1,max=20"` + PhoneNumber *string `json:"phone_number" validate:"min=1,max=20"` + Gender *string `json:"gender" validate:"max=1"` + BirthDate *time.Time `json:"birth_date"` + Address *string `json:"address" validate:"min=1,max=255"` + Active *bool `json:"active"` + FKSdProvinsiID *int32 `json:"fk_sd_provinsi_id"` + FKSdKabupatenKotaID *int32 `json:"fk_sd_kabupaten_kota_id"` + FKSdKecamatanID *int32 `json:"fk_sd_kecamatan_id"` + FKSdKelurahanID *int32 `json:"fk_sd_kelurahan_id"` + DsSdProvinsi *string `json:"ds_sd_provinsi" validate:"min=1,max=255"` + DsSdKabupatenKota *string `json:"ds_sd_kabupaten_kota" validate:"min=1,max=255"` + DsSdKecamatan *string `json:"ds_sd_kecamatan" validate:"min=1,max=255"` + DsSdKelurahan *string `json:"ds_sd_kelurahan" validate:"min=1,max=255"` +} + +type PatientPostRequest struct { + ID int64 `json:"id" validate:"required,min=1"` +} + +// Response struct untuk create +type PatientCreateResponse struct { + Message string `json:"message"` + Data *Patient `json:"data"` +} + +// Update request +type PatientUpdateRequest struct { + ID *int `json:"id" validate:"required,min=1"` + Name *string `json:"name" validate:"min=1,max=100"` + MedicalRecordNumber *string `json:"medical_record_number" validate:"min=1,max=20"` + PhoneNumber *string `json:"phone_number" validate:"min=1,max=20"` + Gender *string `json:"gender" validate:"max=1"` + BirthDate *time.Time `json:"birth_date"` + Address *string `json:"address" validate:"min=1,max=255"` + Active *bool `json:"active"` + FKSdProvinsiID *int32 `json:"fk_sd_provinsi_id"` + FKSdKabupatenKotaID *int32 `json:"fk_sd_kabupaten_kota_id"` + FKSdKecamatanID *int32 `json:"fk_sd_kecamatan_id"` + FKSdKelurahanID *int32 `json:"fk_sd_kelurahan_id"` + DsSdProvinsi *string `json:"ds_sd_provinsi" validate:"max=255"` + DsSdKabupatenKota *string `json:"ds_sd_kabupaten_kota" validate:"max=255"` + DsSdKecamatan *string `json:"ds_sd_kecamatan" validate:"max=255"` + DsSdKelurahan *string `json:"ds_sd_kelurahan" validate:"max=255"` +} + +// Response struct untuk update +type PatientUpdateResponse struct { + Message string `json:"message"` + Data *Patient `json:"data"` +} + +// Response struct untuk delete +type PatientDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` + MedicalRecordNumber string `json:"medical_record_number"` +} + +// Filter struct untuk query parameters +type PatientFilter struct { + Status *string `json:"status,omitempty" form:"status"` + Search *string `json:"search,omitempty" form:"search"` + DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` + DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` +} diff --git a/internal/models/permission/rol_permission.go b/internal/models/permission/rol_permission.go new file mode 100644 index 0000000..303ca9c --- /dev/null +++ b/internal/models/permission/rol_permission.go @@ -0,0 +1,135 @@ +package pages + +import ( + "api-service/internal/models" + "database/sql" + "encoding/json" + "time" + + "github.com/lib/pq" +) + +// Rol_permission represents the data structure for the role_access.rol_permission table +// with proper null handling and optimized JSON marshaling. +type Rol_permission struct { + ID int64 `json:"id" db:"id"` + Create sql.NullBool `json:"create,omitempty" db:"create"` + Read sql.NullBool `json:"read,omitempty" db:"read"` + Update sql.NullBool `json:"update,omitempty" db:"update"` + Disable sql.NullBool `json:"disable,omitempty" db:"disable"` // "disable" is a Go keyword, so "Disable" is used for the field name. + Delete sql.NullBool `json:"delete,omitempty" db:"delete"` + Active sql.NullBool `json:"active,omitempty" db:"active"` + FkRolPagesID models.NullableInt32 `json:"fk_rol_pages_id,omitempty" db:"fk_rol_pages_id"` + RoleKeycloak pq.StringArray `json:"role_keycloak,omitempty" db:"role_keycloak"` // Use NullString for optional text fields + GroupKeycloak pq.StringArray `json:"group_keycloak,omitempty" db:"group_keycloak"` +} + +// MarshalJSON for Rol_permission handles all sql.Null* fields +// to ensure they appear as null, a Go value, or a string value in JSON. +func (r Rol_permission) MarshalJSON() ([]byte, error) { + type Alias Rol_permission + aux := &struct { + *Alias + Create *bool `json:"create,omitempty"` + Read *bool `json:"read,omitempty"` + Update *bool `json:"update,omitempty"` + Disable *bool `json:"disable,omitempty"` + Delete *bool `json:"delete,omitempty"` + Active *bool `json:"active,omitempty"` + FkRolPagesID *int `json:"fk_rol_pages_id,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Create.Valid { + aux.Create = &r.Create.Bool + } + if r.Read.Valid { + aux.Read = &r.Read.Bool + } + if r.Update.Valid { + aux.Update = &r.Update.Bool + } + if r.Disable.Valid { + aux.Disable = &r.Disable.Bool + } + if r.Delete.Valid { + aux.Delete = &r.Delete.Bool + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + if r.FkRolPagesID.Valid { + fkRolPagesID := int(r.FkRolPagesID.Int32) + aux.FkRolPagesID = &fkRolPagesID + } + return json.Marshal(aux) +} + +// ============================================================================= +// REQUEST & RESPONSE STRUCTS FOR ROL_PERMISSION +// ============================================================================= + +// PermissionCreateRequest defines the structure for creating a new permission. +type PermissionCreateRequest struct { + Create *bool `json:"create" validate:"omitempty"` + Read *bool `json:"read" validate:"omitempty"` + Update *bool `json:"update" validate:"omitempty"` + Disable *bool `json:"disable" validate:"omitempty"` + Delete *bool `json:"delete" validate:"omitempty"` + Active *bool `json:"active" validate:"omitempty"` + FkRolPagesID *int64 `json:"fk_rol_pages_id" validate:"omitempty,"` + RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"` + GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"` +} + +// PermissionUpdateRequest defines the structure for updating an existing permission. +// ID is handled via URL parameter, so it's omitted from JSON with `json:"-"`. +type PermissionUpdateRequest struct { + ID *int `json:"-" validate:"required"` // ID is from URL param + Create *bool `json:"create" validate:"omitempty"` + Read *bool `json:"read" validate:"omitempty"` + Update *bool `json:"update" validate:"omitempty"` + Disable *bool `json:"disable" validate:"omitempty"` + Delete *bool `json:"delete" validate:"omitempty"` + Active *bool `json:"active" validate:"omitempty"` + FkRolPagesID int64 `json:"fk_rol_pages_id" validate:"omitempty"` + RoleKeycloak *[]string `json:"role_keycloak" validate:"omitempty"` + GroupKeycloak *[]string `json:"group_keycloak" validate:"omitempty"` +} + +// PermissionsGetResponse defines the response structure for getting a list of permissions. +type PermissionsGetResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data []Rol_permission `json:"data"` + Meta models.MetaResponse `json:"meta"` +} + +// PermissionCreateResponse defines the response structure for creating a permission. +type PermissionCreateResponse struct { + Message string `json:"message"` + Data *Rol_permission `json:"data"` +} + +// PermissionUpdateResponse defines the response structure for updating a permission. +type PermissionUpdateResponse struct { + Message string `json:"message"` + Data *Rol_permission `json:"data"` +} + +// PermissionDeleteResponse defines the response structure for deleting a permission. +type PermissionDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +// Rol_permissionFilter defines the structure for query parameters when filtering permissions. +type Rol_permissionFilter struct { + PageID *int64 `json:"page_id,omitempty" form:"page_id"` + Active *bool `json:"active,omitempty" form:"active"` + Search *string `json:"search,omitempty" form:"search"` + DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` + DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` + Status *string `json:"status,omitempty" form:"status"` +} diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 9dab660..1ce9c41 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -4,9 +4,12 @@ import ( "api-service/internal/config" "api-service/internal/database" authHandlers "api-service/internal/handlers/auth" + componentRolcomponentHandlers "api-service/internal/handlers/component" healthcheckHandlers "api-service/internal/handlers/healthcheck" - + pagesRolpagesHandlers "api-service/internal/handlers/pages" pasienPasienHandlers "api-service/internal/handlers/pasien" + patientMspatientHandlers "api-service/internal/handlers/patient" + permissionRolpermissionHandlers "api-service/internal/handlers/permission" retribusiHandlers "api-service/internal/handlers/retribusi" "api-service/internal/middleware" services "api-service/internal/services/auth" @@ -137,6 +140,46 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { pasienPasienGroup.GET("/by-location", pasienPasienHandler.GetPasienByLocation) } + // Rol_pages endpoints + pagesRolpagesHandler := pagesRolpagesHandlers.NewRol_pagesHandler() + pagesRolpagesGroup := v1.Group("/pages") + { + pagesRolpagesGroup.GET("/", pagesRolpagesHandler.GetRol_pages) + pagesRolpagesGroup.POST("/", pagesRolpagesHandler.CreateRol_pages) + pagesRolpagesGroup.PUT("/:id", pagesRolpagesHandler.UpdateRol_pages) + pagesRolpagesGroup.DELETE("/:id", pagesRolpagesHandler.DeleteRol_pages) + } + + // Rol_component endpoints + componentRolcomponentHandler := componentRolcomponentHandlers.NewRol_componentHandler() + componentRolcomponentGroup := v1.Group("/component") + { + componentRolcomponentGroup.GET("/", componentRolcomponentHandler.GetRol_components) + componentRolcomponentGroup.POST("/", componentRolcomponentHandler.CreateRol_component) + componentRolcomponentGroup.PUT("/:id", componentRolcomponentHandler.UpdateRol_component) + componentRolcomponentGroup.DELETE("/:id", componentRolcomponentHandler.DeleteRol_component) + } + + // Rol_permission endpoints + permissionRolpermissionHandler := permissionRolpermissionHandlers.NewRol_permissionHandler() + permissionRolpermissionGroup := v1.Group("/permission") + { + permissionRolpermissionGroup.GET("/", permissionRolpermissionHandler.GetRol_permissions) + permissionRolpermissionGroup.POST("/", permissionRolpermissionHandler.CreateRol_permission) + permissionRolpermissionGroup.PUT("/:id", permissionRolpermissionHandler.UpdateRol_permission) + permissionRolpermissionGroup.DELETE("/:id", permissionRolpermissionHandler.DeleteRol_permission) + } + + // Ms_patient endpoints + patientMspatientHandler := patientMspatientHandlers.NewPatientHandler() + patientMspatientGroup := v1.Group("/patient") + { + patientMspatientGroup.GET("/", patientMspatientHandler.GetMs_patient) + patientMspatientGroup.POST("/", patientMspatientHandler.CreateMs_patient) + patientMspatientGroup.PUT("/:medical_record_number", patientMspatientHandler.UpdateMs_patient) + patientMspatientGroup.DELETE("/:medical_record_number", patientMspatientHandler.DeleteMs_patient) + } + // ============================================================================= // PROTECTED ROUTES (Authentication Required) // ============================================================================= diff --git a/internal/utils/query/builder.go b/internal/utils/query/builder.go index 95360b2..e71aa17 100644 --- a/internal/utils/query/builder.go +++ b/internal/utils/query/builder.go @@ -61,6 +61,8 @@ const ( OpArrayContains FilterOperator = "_array_contains" OpArrayNotContains FilterOperator = "_array_ncontains" OpArrayLength FilterOperator = "_array_length" + OpArrayOverlap FilterOperator = "_array_overlap" + OpArrayContainedBy FilterOperator = "_array_contained_by" ) // DynamicFilter represents a single filter condition @@ -627,6 +629,19 @@ func (qb *QueryBuilder) buildCTEClause(ctes []CTE) (string, []interface{}, error // buildFromClause builds the FROM clause with optional alias func (qb *QueryBuilder) buildFromClause(table, alias string) string { + // Check if the table name contains a dot (schema.table) + if strings.Contains(table, ".") { + parts := strings.Split(table, ".") + if len(parts) == 2 { + // Quote schema and table separately + fromClause := fmt.Sprintf("%s.%s", qb.escapeIdentifier(parts[0]), qb.escapeIdentifier(parts[1])) + if alias != "" { + fromClause += " " + qb.escapeIdentifier(alias) + } + return fromClause + } + } + fromClause := qb.escapeIdentifier(table) if alias != "" { fromClause += " " + qb.escapeIdentifier(alias) @@ -641,7 +656,18 @@ func (qb *QueryBuilder) buildSingleJoinClause(join Join) (string, string, string joinType = "INNER" } - table := qb.escapeIdentifier(join.Table) + var table string + if strings.Contains(join.Table, ".") { + parts := strings.Split(join.Table, ".") + if len(parts) == 2 { + // Quote schema and table separately + table = fmt.Sprintf("%s.%s", qb.escapeIdentifier(parts[0]), qb.escapeIdentifier(parts[1])) + } else { + table = qb.escapeIdentifier(join.Table) + } + } else { + table = qb.escapeIdentifier(join.Table) + } if join.Alias != "" { table += " " + qb.escapeIdentifier(join.Alias) } @@ -803,7 +829,7 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []in switch filter.Operator { case OpJsonContains, OpJsonNotContains, OpJsonExists, OpJsonNotExists, OpJsonEqual, OpJsonNotEqual: return qb.buildJsonFilterCondition(filter) - case OpArrayContains, OpArrayNotContains, OpArrayLength: + case OpArrayContains, OpArrayNotContains, OpArrayLength, OpArrayOverlap, OpArrayContainedBy: return qb.buildArrayFilterCondition(filter) } @@ -1051,6 +1077,45 @@ func (qb *QueryBuilder) buildArrayFilterCondition(filter DynamicFilter) (string, default: return "", nil, fmt.Errorf("Array operations not supported for database type: %s", qb.dbType) } + case OpArrayOverlap: + // TAMBAHKAN INI + switch qb.dbType { + case DBTypePostgreSQL: + expr = fmt.Sprintf("%s && ?", column) + args = append(args, filter.Value) + case DBTypeMySQL: + // MySQL doesn't have native array overlap, use JSON_OVERLAPS if available + expr = fmt.Sprintf("JSON_OVERLAPS(%s, ?)", column) + args = append(args, filter.Value) + case DBTypeSQLServer: + // SQL Server workaround using EXISTS and OPENJSON + expr = fmt.Sprintf("EXISTS (SELECT 1 FROM OPENJSON(%s) o1 CROSS JOIN OPENJSON(?) o2 WHERE o1.value = o2.value)", column) + args = append(args, filter.Value) + case DBTypeSQLite: + // SQLite workaround using json_each + expr = fmt.Sprintf("EXISTS (SELECT 1 FROM json_each(%s) j1 CROSS JOIN json_each(?) j2 WHERE j1.value = j2.value)", column) + args = append(args, filter.Value) + default: + return "", nil, fmt.Errorf("Array overlap operations not supported for database type: %s", qb.dbType) + } + case OpArrayContainedBy: + // TAMBAHKAN INI + switch qb.dbType { + case DBTypePostgreSQL: + expr = fmt.Sprintf("%s <@ ?", column) + args = append(args, filter.Value) + case DBTypeMySQL: + expr = fmt.Sprintf("JSON_CONTAINS(?, %s)", column) + args = append(args, filter.Value) + case DBTypeSQLServer: + expr = fmt.Sprintf("NOT EXISTS (SELECT 1 FROM OPENJSON(%s) WHERE value NOT IN (SELECT value FROM OPENJSON(?)))", column) + args = append(args, filter.Value) + case DBTypeSQLite: + expr = fmt.Sprintf("NOT EXISTS (SELECT 1 FROM json_each(%s) j1 WHERE j1.value NOT IN (SELECT j2.value FROM json_each(?) j2))", column) + args = append(args, filter.Value) + default: + return "", nil, fmt.Errorf("Array contained_by operations not supported for database type: %s", qb.dbType) + } case OpArrayNotContains: switch qb.dbType { case DBTypePostgreSQL: @@ -1114,11 +1179,54 @@ func (qb *QueryBuilder) buildArrayFilterCondition(filter DynamicFilter) (string, // ============================================================================= func (qb *QueryBuilder) ExecuteQuery(ctx context.Context, db *sqlx.DB, query DynamicQuery, dest interface{}) error { + // sql, args, err := qb.BuildQuery(query) + // if err != nil { + // return err + // } + // start := time.Now() + // err = db.SelectContext(ctx, dest, sql, args...) + // fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start)) + // return err sql, args, err := qb.BuildQuery(query) if err != nil { return err } start := time.Now() + + // Check if dest is a pointer to a slice of maps + destValue := reflect.ValueOf(dest) + if destValue.Kind() != reflect.Ptr || destValue.IsNil() { + return fmt.Errorf("dest must be a non-nil pointer") + } + + destElem := destValue.Elem() + if destElem.Kind() == reflect.Slice { + sliceType := destElem.Type().Elem() + if sliceType.Kind() == reflect.Map && + sliceType.Key().Kind() == reflect.String && + sliceType.Elem().Kind() == reflect.Interface { + + // Handle slice of map[string]interface{} + rows, err := db.QueryxContext(ctx, sql, args...) + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + row := make(map[string]interface{}) + if err := rows.MapScan(row); err != nil { + return err + } + destElem.Set(reflect.Append(destElem, reflect.ValueOf(row))) + } + + fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start)) + return nil + } + } + + // Default case: use SelectContext err = db.SelectContext(ctx, dest, sql, args...) fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start)) return err @@ -2345,3 +2453,42 @@ func (mqb *MongoQueryBuilder) ExecuteDelete(ctx context.Context, collection *mon fmt.Printf("[DEBUG] MongoDB Delete executed in %v\n", time.Since(start)) return result, err } + +func (qb *QueryBuilder) getOperatorSQL(op FilterOperator) (string, error) { + switch op { + case OpEqual: + return "=", nil + case OpNotEqual: + return "!=", nil + case OpLike: + return "LIKE", nil + case OpILike: + return "ILIKE", nil + case OpIn: + return "IN", nil + case OpNotIn: + return "NOT IN", nil + case OpGreaterThan: + return ">", nil + case OpGreaterThanEqual: + return ">=", nil + case OpLessThan: + return "<", nil + case OpLessThanEqual: + return "<=", nil + case OpNull: + return "IS NULL", nil + case OpNotNull: + return "IS NOT NULL", nil + case OpArrayContains: + return "@>", nil + case OpArrayNotContains: + return "NOT @>", nil + case OpArrayOverlap: // TAMBAHKAN INI + return "&&", nil + case OpArrayContainedBy: // BONUS + return "<@", nil + default: + return "", fmt.Errorf("unsupported operator: %s", op) + } +} diff --git a/tools/general/services-config.yaml b/tools/general/services-config.yaml index b7ad69f..12cdc56 100644 --- a/tools/general/services-config.yaml +++ b/tools/general/services-config.yaml @@ -1,297 +1,213 @@ global: - module_name: "api-service" + module_name: "service-antrean" output_dir: "internal/handlers" enable_swagger: true enable_logging: true database: - default_connection: "postgres_satudata" + default_connection: "db_antrean" timeout_seconds: 30 +# services: +# pasien: +# name: "Manajemen Data Pasien" +# category: "pasien" +# package: "pasien" +# description: "API untuk mengelola data pasien dengan informasi lokasi lengkap" +# base_url: "" +# timeout: 30 +# retry_count: 3 +# table_name: "m_pasien" + services: - pasien: - name: "Manajemen Data Pasien" - category: "pasien" - package: "pasien" - description: "API untuk mengelola data pasien dengan informasi lokasi lengkap" + rol_pages: + name: "Manajemen Pages Role" + category: "pages" + package: "pages" + description: "Pages service for role management" base_url: "" timeout: 30 retry_count: 3 - table_name: "m_pasien" + table_name: "role_access.rol_pages" # Define all columns once for reuse schema: columns: - name: "id" - type: "serial4" + type: "int4" nullable: false - go_type: "int32" + go_type: "int" primary_key: true unique: true - description: "Primary key for schedule" - - name: "nomr" + description: "Primary key for pages" + - name: "name" type: "varchar" - nullable: true + nullable: false go_type: "string" + validation: "required,min=1,max=20" searchable: true - unique: true - description: "Nomor Rekam Medis" - - name: "status" + description: "Name of the page" + - name: "icon" type: "varchar" nullable: true go_type: "string" - description: "Status pasien (A = Aktif, I = Inaktif)" - - name: "title" - type: "varchar" + validation: "omitempty,min=1,max=20" + description: "Icon for the page" + - name: "url" + type: "text" nullable: true go_type: "string" - description: "Gelar pasien (Tn, Ny, Sdr, dll)" - - name: "nama" - type: "varchar" - nullable: true - go_type: "string" - validation: "required,min=1,max=100" + validation: "omitempty,min=1,max=100" searchable: true - description: "Nama lengkap pasien" - - name: "tempat" - type: "varchar" - nullable: true - go_type: "string" - description: "Tempat lahir pasien" - - name: "tgllahir" - type: "date" - nullable: true - go_type: "time.Time" - description: "Tanggal lahir pasien" - - name: "jeniskelamin" - type: "varchar" - nullable: true - go_type: "string" - validation: "oneof=L P" - description: "Jenis kelamin (L/P)" - - name: "alamat" - type: "varchar" - nullable: true - go_type: "string" - description: "Alamat lengkap pasien" - - name: "kelurahan" - type: "int8" - nullable: true - go_type: "int64" - description: "ID Kelurahan" - - name: "kdkecamatan" - type: "int4" - nullable: true - go_type: "int32" - description: "ID Kecamatan" - - name: "kota" - type: "int4" - nullable: true - go_type: "int32" - description: "ID Kota" - - name: "kdprovinsi" - type: "int4" - nullable: true - go_type: "int32" - description: "ID Provinsi" - - name: "agama" - type: "int4" - nullable: true - go_type: "int32" - description: "ID Agama" - - name: "no_kartu" - type: "varchar" - nullable: true - go_type: "string" + description: "URL of the page" + - name: "level" + type: "int2" + nullable: false + go_type: "int" searchable: true - unique: true - description: "Nomor kartu identitas" - - name: "noktp_baru" - type: "varchar" + description: "Level of the page in hierarchy" + - name: "sort" + type: "int2" + nullable: false + go_type: "int" + description: "Sort order of the page" + - name: "parent" + type: "int4" nullable: true - go_type: "string" - description: "Nomor KTP baru" - - name: "created_at" - type: "timestamp" - nullable: true - go_type: "time.Time" - system_field: true - description: "Tanggal pembuatan record" - - name: "updated_at" - type: "timestamp" - nullable: true - go_type: "time.Time" - system_field: true - description: "Tanggal update record" + go_type: "int" + validation: "omitempty,min=1" + description: "Parent page ID" + - name: "active" + type: "bool" + nullable: false + go_type: "bool" + description: "Active status of the page" # Define relationships with other tables relationships: - - name: "provinsi" - table: "m_provinsi" - foreign_key: "kdprovinsi" - local_key: "idprovinsi" + - name: "component" + table: "role_access.rol_component" + foreign_key: "fk_rol_pages_id" + local_key: "id" columns: - - name: "idprovinsi" + - name: "id" type: "int4" nullable: false - go_type: "int32" + go_type: "int" primary_key: true - - name: "namaprovinsi" + unique: true + description: "Primary key for component" + - name: "name" type: "varchar" + nullable: false + go_type: "string" + validation: "required,min=1,max=100" + searchable: true + description: "Name of the component" + - name: "description" + type: "text" nullable: true go_type: "string" - description: "Nama provinsi" - - name: "kota" - table: "m_kota" - foreign_key: "kota" - local_key: "idkota" - columns: - - name: "idkota" + validation: "omitempty,min=1,max=100" + description: "Description of the component" + - name: "directory" + type: "text" + nullable: false + go_type: "string" + validation: "required,min=1,max=100" + searchable: true + description: "Directory path of the component" + - name: "active" + type: "bool" + nullable: false + go_type: "bool" + description: "Active status of the component" + - name: "fk_rol_pages_id" type: "int4" nullable: false - go_type: "int32" - primary_key: true - - name: "namakota" - type: "varchar" + go_type: "int" + searchable: true + description: "Foreign key to rol_pages" + - name: "sort" + type: "int2" nullable: true - go_type: "string" - description: "Nama kota" - - name: "kecamatan" - table: "m_kecamatan" - foreign_key: "kdkecamatan" - local_key: "idkecamatan" - columns: - - name: "idkecamatan" - type: "int8" - nullable: false - go_type: "int64" - primary_key: true - - name: "namakecamatan" - type: "varchar" - nullable: true - go_type: "string" - description: "Nama kecamatan" - - name: "kelurahan" - table: "m_kelurahan" - foreign_key: "kelurahan" - local_key: "idkelurahan" - columns: - - name: "idkelurahan" - type: "int8" - nullable: false - go_type: "int64" - primary_key: true - - name: "namakelurahan" - type: "varchar" - nullable: true - go_type: "string" - description: "Nama kelurahan" + go_type: "int" + validation: "omitempty,min=1" + description: "Sort order of the component" # Define reusable field groups field_groups: - base_fields: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin"] - location_fields: ["alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi"] - identity_fields: ["agama", "no_kartu", "noktp_baru"] - all_fields: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin", "alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi", "agama", "no_kartu", "noktp_baru"] - with_location_names: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin", "alamat", "kelurahan", "namakelurahan", "kdkecamatan", "namakecamatan", "kota", "namakota", "kdprovinsi", "namaprovinsi", "agama", "no_kartu", "noktp_baru"] + base_fields: ["id", "name", "icon", "url", "level", "sort", "parent", "active"] + # location_fields: ["alamat", "kelurahan", "kdkecamatan", "kota", "kdprovinsi"] + # identity_fields: ["agama", "no_kartu", "noktp_baru"] + all_fields: ["id", "name", "icon", "url", "level", "sort", "parent", "active"] + # with_location_names: ["nomr", "title", "nama", "tempat", "tgllahir", "jeniskelamin", "alamat", "kelurahan", "namakelurahan", "kdkecamatan", "namakecamatan", "kota", "namakota", "kdprovinsi", "namaprovinsi", "agama", "no_kartu", "noktp_baru"] # Define endpoints with reusable configurations endpoints: list: - handler_folder: "pasien" - handler_file: "pasien.go" + handler_folder: "pages" + handler_file: "pages.go" methods: ["GET"] path: "/" - description: "Get list of pasien with pagination and filters" - summary: "Get Pasien List" - tags: ["Pasien"] - require_auth: true + description: "Get list of pages with pagination and filters" + summary: "Get Pages List" + tags: ["Pages"] cache_enabled: true cache_ttl: 300 has_pagination: true has_filter: true has_search: true has_stats: true - fields: "with_location_names" - response_model: "PasienGetResponse" - - get_by_id: - handler_folder: "pasien" - handler_file: "pasien.go" - methods: ["GET"] - path: "/:id" - description: "Get pasien by ID" - summary: "Get Pasien by ID" - tags: ["Pasien"] - require_auth: true - cache_enabled: true - cache_ttl: 300 - fields: "with_location_names" - response_model: "PasienGetByIDResponse" - - get_by_nomr: - handler_folder: "pasien" - handler_file: "pasien.go" - methods: ["GET"] - path: "/nomr/:nomr" - description: "Get pasien by Nomr" - summary: "Get Pasien by Nomr" - tags: ["Pasien"] - require_auth: true - cache_enabled: true - cache_ttl: 300 - fields: "with_location_names" - response_model: "PasienGetByNomrResponse" + fields: "base_fields" + response_model: "PagesGetResponse" create: - handler_folder: "pasien" - handler_file: "pasien.go" + handler_folder: "pages" + handler_file: "pages.go" methods: ["POST"] path: "/" - description: "Create a new pasien" - summary: "Create Pasien" - tags: ["Pasien"] - require_auth: true - fields: "all_fields" - request_model: "PasienCreateRequest" - response_model: "PasienCreateResponse" + description: "Create a new page" + summary: "Create Page" + tags: ["Pages"] + fields: "base_fields" + request_model: "PagesCreateRequest" + response_model: "PagesCreateResponse" update: - handler_folder: "pasien" - handler_file: "pasien.go" + handler_folder: "pages" + handler_file: "pages.go" methods: ["PUT"] - path: "/:nomr" - description: "Update an existing pasien" - summary: "Update Pasien" - tags: ["Pasien"] - require_auth: true - fields: "all_fields" - request_model: "PasienUpdateRequest" - response_model: "PasienUpdateResponse" + path: "/:id" + description: "Update an existing page" + summary: "Update Page" + tags: ["Pages"] + fields: "base_fields" + request_model: "PagesUpdateRequest" + response_model: "PagesUpdateResponse" delete: - handler_folder: "pasien" - handler_file: "pasien.go" + handler_folder: "pages" + handler_file: "pages.go" methods: ["DELETE"] - path: "/:nomr" - description: "Delete a pasien" - summary: "Delete Pasien" - tags: ["Pasien"] - require_auth: true + path: "/:id" + description: "Delete a page" + summary: "Delete Page" + tags: ["Pages"] soft_delete: true - response_model: "PasienDeleteResponse" + response_model: "PagesDeleteResponse" - dynamic: - handler_folder: "pasien" - handler_file: "pasien.go" - methods: ["GET"] - path: "/dynamic" - description: "Get pasien with dynamic filtering" - summary: "Get Pasien Dynamic" - tags: ["Pasien"] - require_auth: true - has_dynamic: true - fields: "with_location_names" - response_model: "PasienGetResponse" + # dynamic: + # handler_folder: "pages" + # handler_file: "pages.go" + # methods: ["GET"] + # path: "/pages/dynamic" + # description: "Get pages with dynamic filtering" + # summary: "Get Pages Dynamic" + # tags: ["Pages"] + # require_auth: true + # has_dynamic: true + # fields: "base_fields" + # response_model: "PagesGetResponse" # search: # handler_folder: "pasien" @@ -317,31 +233,6 @@ services: # require_auth: true # has_stats: true # response_model: "AggregateData" - - by_location: - handler_folder: "pasien" - handler_file: "pasien.go" - methods: ["GET"] - path: "/by-location" - description: "Get pasien by location (provinsi, kota, kecamatan, kelurahan)" - summary: "Get Pasien by Location" - tags: ["Pasien"] - require_auth: true - has_filter: true - fields: "with_location_names" - response_model: "PasienGetResponse" - - by_age: - handler_folder: "pasien" - handler_file: "pasien.go" - methods: ["GET"] - path: "/by-age" - description: "Get pasien statistics by age group" - summary: "Get Pasien by Age Group" - tags: ["Pasien"] - require_auth: true - has_stats: true - response_model: "PasienAgeStatsResponse" # schedule: # name: "Jadwal Dokter"