Files
api_antrean/internal/handlers/pasien/pasien.go
T
2026-02-13 15:57:28 +07:00

2422 lines
70 KiB
Go

package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models"
pasienModels "api-service/internal/models/pasien"
queryUtils "api-service/internal/utils/query"
"api-service/internal/utils/validation"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"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("pasien_status", validatePasienStatus)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for pasien status
func validatePasienStatus(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
mu sync.RWMutex
}
// 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 && strings.HasPrefix(keyStr, prefix) {
c.items.Delete(key)
}
return true
})
}
// =============================================================================
// Pasien HANDLER STRUCT
// =============================================================================
// PasienHandler handles pasien services
type PasienHandler struct {
db database.Service
queryBuilder *queryUtils.QueryBuilder
validator *validation.DynamicValidator
cache *InMemoryCache
}
// NewPasienHandler creates a new PasienHandler with a pre-configured QueryBuilder
func NewPasienHandler() *PasienHandler {
// Initialize QueryBuilder with allowed columns list for security.
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
SetAllowedColumns([]string{
"id",
"nomr",
"status",
"title",
"nama",
"tempat",
"tgllahir",
"jeniskelamin",
"alamat",
"kelurahan",
"kdkecamatan",
"kota",
"kdprovinsi",
"agama",
"no_kartu",
"noktp_baru",
"created_at",
"updated_at",
"idprovinsi",
"namaprovinsi",
"idkota",
"namakota",
"idkecamatan",
"namakecamatan",
"idkelurahan",
"namakelurahan",
})
return &PasienHandler{
db: db,
queryBuilder: queryBuilder,
validator: validation.NewDynamicValidator(queryBuilder),
cache: NewInMemoryCache(),
}
}
// =============================================================================
// HANDLER ENDPOINTS
// =============================================================================
// GetPasienByAge godoc
// @Summary Get Pasien by Age Group
// @Description Get pasien statistics by age group
// @Tags Pasien
// @Accept json
// @Produce json
// @Param age_group query string false "Age group (child, teen, adult, senior)"
// @Success 200 {object} pasienModels.PasienAgeStatsResponse "Statistics data"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/by-age [get]
func (h *PasienHandler) GetPasienByAge(c *gin.Context) {
// Parse age group
ageGroup := c.Query("age_group")
validAgeGroups := map[string]bool{
"child": true, // 0-12 years
"teen": true, // 13-17 years
"adult": true, // 18-59 years
"senior": true, // 60+ years
}
if ageGroup == "" || !validAgeGroups[ageGroup] {
h.respondError(c, "Invalid age group", fmt.Errorf("age group must be one of: child, teen, adult, senior"), http.StatusBadRequest)
return
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Build query
query := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{
{Expression: "COUNT(*)", Alias: "count"},
},
Filters: []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
LogicOp: "AND",
}},
}
// Execute query
var result struct {
Count int `db:"count"`
}
err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, query, &result)
if err != nil {
h.logAndRespondError(c, "Failed to get age statistics", err, http.StatusInternalServerError)
return
}
// Build response
response := pasienModels.PasienAgeStatsResponse{
Message: fmt.Sprintf("Age group '%s' statistics retrieved successfully", ageGroup),
Data: map[string]interface{}{
"age_group": ageGroup,
"count": result.Count,
},
}
c.JSON(http.StatusOK, response)
}
// GetPasienDynamic godoc
// @Summary Get Pasien Dynamic
// @Description Get pasien with dynamic filtering
// @Tags Pasien
// @Accept json
// @Produce json
// @Param fields query string false "Fields to select (e.g., fields=*.*)"
// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[name][_eq]=value)"
// @Param sort query string false "Sort fields (e.g., sort=date_created,-name)"
// @Param limit query int false "Limit" default(10)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} pasienModels.PasienGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/dynamic [get]
func (h *PasienHandler) GetPasienDynamic(c *gin.Context) {
parser := queryUtils.NewQueryParser().SetLimits(10, 100)
dynamicQuery, err := parser.ParseQueryWithDefaultFields(c.Request.URL.Query(), "m_pasien", []string{
"id",
"nomr",
"status",
"title",
"nama",
"tempat",
"tgllahir",
"jeniskelamin",
"alamat",
"kelurahan",
"kdkecamatan",
"kota",
"kdprovinsi",
"agama",
"no_kartu",
"noktp_baru",
"namakelurahan",
"namakecamatan",
"namakota",
"namaprovinsi",
})
if err != nil {
h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest)
return
}
// Add joins for relationships using the correct structure
dynamicQuery.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
// Add default filter to exclude deleted records
dynamicQuery.Filters = append([]queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"}},
LogicOp: "AND",
}}, dynamicQuery.Filters...)
// Try to get from cache first
// Create cache key from query string
cacheKey := fmt.Sprintf("pasien:dynamic:%s", c.Request.URL.RawQuery)
if cachedData, found := h.cache.Get(cacheKey); found {
logger.Info("Cache hit for dynamic query", map[string]interface{}{"cache_key": cacheKey})
// Convert from interface{} to expected type
if pasiens, ok := cachedData.([]pasienModels.Pasien); ok {
meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, len(pasiens))
response := pasienModels.PasienGetResponse{
Message: "Data pasien berhasil diambil (dari cache)",
Data: pasiens,
Meta: meta,
}
c.JSON(http.StatusOK, response)
return
}
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
pasiens, total, err := h.fetchPasiensDynamic(ctx, dbConn, dynamicQuery)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Save to cache
h.cache.Set(cacheKey, pasiens, 10*time.Minute) // Cache for 10 minutes
meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total)
response := pasienModels.PasienGetResponse{
Message: "Data pasien berhasil diambil",
Data: pasiens,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}
// GetPasienByLocation godoc
// @Summary Get Pasien by Location
// @Description Get pasien by location (provinsi, kota, kecamatan, kelurahan)
// @Tags Pasien
// @Accept json
// @Produce json
// @Param kelurahan query int false "Filter by kelurahan ID"
// @Param kdkecamatan query int false "Filter by kdkecamatan ID"
// @Param kota query int false "Filter by kota ID"
// @Param kdprovinsi query int false "Filter by kdprovinsi ID"
// @Param limit query int false "Limit (max 100)" default(10)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} pasienModels.PasienGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/by-location [get]
func (h *PasienHandler) GetPasienByLocation(c *gin.Context) {
// Parse location filters
var filters []queryUtils.DynamicFilter
if kelurahan := c.Query("kelurahan"); kelurahan != "" {
if kelurahanID, err := strconv.Atoi(kelurahan); err == nil {
filters = append(filters, queryUtils.DynamicFilter{Column: "kelurahan", Operator: queryUtils.OpEqual, Value: kelurahanID})
}
}
if kdkecamatan := c.Query("kdkecamatan"); kdkecamatan != "" {
if kdkecamatanID, err := strconv.Atoi(kdkecamatan); err == nil {
filters = append(filters, queryUtils.DynamicFilter{Column: "kdkecamatan", Operator: queryUtils.OpEqual, Value: kdkecamatanID})
}
}
if kota := c.Query("kota"); kota != "" {
if kotaID, err := strconv.Atoi(kota); err == nil {
filters = append(filters, queryUtils.DynamicFilter{Column: "kota", Operator: queryUtils.OpEqual, Value: kotaID})
}
}
if kdprovinsi := c.Query("kdprovinsi"); kdprovinsi != "" {
if kdprovinsiID, err := strconv.Atoi(kdprovinsi); err == nil {
filters = append(filters, queryUtils.DynamicFilter{Column: "kdprovinsi", Operator: queryUtils.OpEqual, Value: kdprovinsiID})
}
}
// Parse pagination
limit, offset := 10, 0
if limitStr := c.Query("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
offset = o
}
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Build query
query := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{
{Expression: "id"},
{Expression: "nomr"},
{Expression: "status"},
{Expression: "title"},
{Expression: "nama"},
{Expression: "tempat"},
{Expression: "tgllahir"},
{Expression: "jeniskelamin"},
{Expression: "alamat"},
{Expression: "kelurahan"},
{Expression: "kdkecamatan"},
{Expression: "kota"},
{Expression: "kdprovinsi"},
{Expression: "agama"},
{Expression: "no_kartu"},
{Expression: "noktp_baru"},
{Expression: "namakelurahan"},
{Expression: "namakecamatan"},
{Expression: "namakota"},
{Expression: "namaprovinsi"},
},
Sort: []queryUtils.SortField{{Column: "date_created", Order: "DESC"}},
Limit: limit,
Offset: offset,
}
// Add joins for relationships using the correct structure
query.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
// Add filters if any
if len(filters) > 0 {
query.Filters = append(query.Filters, queryUtils.FilterGroup{
Filters: filters,
LogicOp: "AND",
})
}
// Execute query
pasiens, total, err := h.fetchPasiensDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(limit, offset, total)
response := pasienModels.PasienGetResponse{
Message: "Data pasien by location retrieved successfully",
Data: pasiens,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}
// CreatePasien godoc
// @Summary Create Pasien
// @Description Create a new pasien
// @Tags Pasien
// @Accept json
// @Produce json
// @Param request body pasienModels.PasienCreateRequest true "Pasien creation request"
// @Success 201 {object} pasienModels.PasienCreateResponse "Pasien created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/ [post]
func (h *PasienHandler) CreatePasien(c *gin.Context) {
var req pasienModels.PasienCreateRequest
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
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
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(
"m_pasien", // Table name
"id", // Column that must be unique
queryUtils.DynamicFilter{ // Additional condition
Column: "status",
Operator: queryUtils.OpNotEqual,
Value: "deleted",
},
)
// Prepare data from request for validation
dataToValidate := map[string]interface{}{
"id": *req.ID,
}
// Execute validation
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 nomr must be unique
if req.Nomr != nil && *req.Nomr != "" {
rule := validation.NewUniqueFieldRule(
"m_pasien", // Table name
"nomr", // Column that must be unique
queryUtils.DynamicFilter{ // Additional condition
Column: "status",
Operator: queryUtils.OpNotEqual,
Value: "deleted",
},
)
// Prepare data from request for validation
dataToValidate := map[string]interface{}{
"nomr": *req.Nomr,
}
// Execute validation
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
if err != nil {
h.logAndRespondError(c, "Failed to validate nomr", err, http.StatusInternalServerError)
return
}
if isDuplicate {
h.respondError(c, "nomr already exists", fmt.Errorf("duplicate nomr: %s", *req.Nomr), http.StatusConflict)
return
}
}
// Validate no_kartu must be unique
if req.NoKartu != nil && *req.NoKartu != "" {
rule := validation.NewUniqueFieldRule(
"m_pasien", // Table name
"no_kartu", // Column that must be unique
queryUtils.DynamicFilter{ // Additional condition
Column: "status",
Operator: queryUtils.OpNotEqual,
Value: "deleted",
},
)
// Prepare data from request for validation
dataToValidate := map[string]interface{}{
"no_kartu": *req.NoKartu,
}
// Execute validation
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
if err != nil {
h.logAndRespondError(c, "Failed to validate no_kartu", err, http.StatusInternalServerError)
return
}
if isDuplicate {
h.respondError(c, "no_kartu already exists", fmt.Errorf("duplicate no_kartu: %s", *req.NoKartu), http.StatusConflict)
return
}
}
data := queryUtils.InsertData{
Columns: []string{
"status",
"date_created", "date_updated",
"id",
"nomr",
"status",
"title",
"nama",
"tempat",
"tgllahir",
"jeniskelamin",
"alamat",
"kelurahan",
"kdkecamatan",
"kota",
"kdprovinsi",
"agama",
"no_kartu",
"noktp_baru",
},
Values: []interface{}{
req.Status,
time.Now(), time.Now(),
req.ID,
req.Nomr,
req.Status,
req.Title,
req.Nama,
req.Tempat,
req.Tgllahir,
req.Jeniskelamin,
req.Alamat,
req.Kelurahan,
req.Kdkecamatan,
req.Kota,
req.Kdprovinsi,
req.Agama,
req.NoKartu,
req.NoktpBaru,
},
}
returningCols := []string{
"status",
"sort", "user_created", "date_created", "user_updated", "date_updated",
"id",
"nomr",
"status",
"title",
"nama",
"tempat",
"tgllahir",
"jeniskelamin",
"alamat",
"kelurahan",
"kdkecamatan",
"kota",
"kdprovinsi",
"agama",
"no_kartu",
"noktp_baru",
}
sql, args, err := h.queryBuilder.BuildInsertQuery("m_pasien", data, returningCols...)
if err != nil {
h.logAndRespondError(c, "Failed to build insert query", err, http.StatusInternalServerError)
return
}
var dataPasien pasienModels.Pasien
err = dbConn.GetContext(ctx, &dataPasien, sql, args...)
if err != nil {
h.logAndRespondError(c, "Failed to create pasien", err, http.StatusInternalServerError)
return
}
// Invalidate cache that might be affected
h.invalidateRelatedCache()
response := pasienModels.PasienCreateResponse{Message: "Pasien berhasil dibuat", Data: &dataPasien}
c.JSON(http.StatusCreated, response)
}
// GetPasien godoc
// @Summary Get Pasien List
// @Description Get list of pasien with pagination and filters
// @Tags Pasien
// @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} pasienModels.PasienGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/ [get]
func (h *PasienHandler) GetPasien(c *gin.Context) {
// Increase timeout for complex queries
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
// Use the core fetchPasiensDynamic function for all data retrieval logic.
query := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{
{Expression: "id"},
{Expression: "nomr"},
{Expression: "status"},
{Expression: "title"},
{Expression: "nama"},
{Expression: "tempat"},
{Expression: "tgllahir"},
{Expression: "jeniskelamin"},
{Expression: "alamat"},
{Expression: "kelurahan"},
{Expression: "kdkecamatan"},
{Expression: "kota"},
{Expression: "kdprovinsi"},
{Expression: "agama"},
{Expression: "no_kartu"},
{Expression: "noktp_baru"},
{Expression: "namakelurahan"},
{Expression: "namakecamatan"},
{Expression: "namakota"},
{Expression: "namaprovinsi"},
},
Sort: []queryUtils.SortField{{Column: "date_created", Order: "DESC"}},
}
// Add joins for relationships using the correct structure
query.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
// 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
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Parse simple filters
var filters []queryUtils.DynamicFilter
if status := c.Query("status"); status != "" && models.IsValidStatus(status) {
filters = append(filters, queryUtils.DynamicFilter{Column: "status", Operator: queryUtils.OpEqual, Value: status})
}
// Optimize query search with caching
search := c.Query("search")
var searchFilters []queryUtils.DynamicFilter
var cacheKey string
var useCache bool
// Initialize searchFilters before using it in the cache hit section
if search != "" {
// Limit search length to prevent slow queries
if len(search) > 50 {
search = search[:50]
}
// Generate cache key for search
cacheKey = fmt.Sprintf("pasien:search:%s:%d:%d", search, query.Limit, query.Offset)
// Initialize searchFilters here
searchFilters = []queryUtils.DynamicFilter{
{Column: "nomr", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
{Column: "title", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
{Column: "nama", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
{Column: "no_kartu", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
}
// Try to get from cache first
if cachedData, found := h.cache.Get(cacheKey); found {
logger.Info("Cache hit for search", map[string]interface{}{"search": search, "cache_key": cacheKey})
// Convert from interface{} to expected type
pasiens, ok := cachedData.([]pasienModels.Pasien)
if !ok {
logger.Error("Failed to convert cached data", map[string]interface{}{"cache_key": cacheKey})
} else {
// If requested, get aggregation data
var aggregateData *models.AggregateData
if c.Query("include_summary") == "true" {
// Build full filter groups for aggregate data (including search filters)
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
}
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, len(pasiens))
response := pasienModels.PasienGetResponse{
Message: "Data pasien berhasil diambil (dari cache)",
Data: pasiens,
Meta: meta,
}
if aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
return
}
}
// If not in cache, mark for saving after query
useCache = true
// If there's search, create OR filter group
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"})
}
// Add other filters (if any) as AND group
if len(filters) > 0 {
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"})
}
pasiens, total, err := h.fetchPasiensDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Save search results to cache if there's a search parameter
if useCache && len(pasiens) > 0 {
h.cache.Set(cacheKey, pasiens, 15*time.Minute) // Cache for 15 minutes
logger.Info("Cached search results", map[string]interface{}{"search": search, "cache_key": cacheKey, "count": len(pasiens)})
}
// If requested, get aggregation data
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 := pasienModels.PasienGetResponse{
Message: "Data pasien berhasil diambil",
Data: pasiens,
Meta: meta,
}
if aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// GetPasienByID godoc
// @Summary Get Pasien by ID
// @Description Get pasien by ID
// @Tags Pasien
// @Accept json
// @Produce json
// @Param id path string true "Pasien ID"
// @Success 200 {object} pasienModels.PasienGetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Pasien not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/:id [get]
func (h *PasienHandler) GetPasienByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
return
}
// Try to get from cache first
cacheKey := fmt.Sprintf("pasien:id:%s", id)
if cachedData, found := h.cache.Get(cacheKey); found {
logger.Info("Cache hit for id", map[string]interface{}{"id": id, "cache_key": cacheKey})
// Convert from interface{} to expected type
if cachedPasien, ok := cachedData.(pasienModels.Pasien); ok {
response := pasienModels.PasienGetByIDResponse{
Message: "Pasien details retrieved successfully (dari cache)",
Data: &cachedPasien,
}
c.JSON(http.StatusOK, response)
return
}
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
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()
dynamicQuery := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{
{Expression: "id"},
{Expression: "nomr"},
{Expression: "status"},
{Expression: "title"},
{Expression: "nama"},
{Expression: "tempat"},
{Expression: "tgllahir"},
{Expression: "jeniskelamin"},
{Expression: "alamat"},
{Expression: "kelurahan"},
{Expression: "kdkecamatan"},
{Expression: "kota"},
{Expression: "kdprovinsi"},
{Expression: "agama"},
{Expression: "no_kartu"},
{Expression: "noktp_baru"},
{Expression: "namakelurahan"},
{Expression: "namakecamatan"},
{Expression: "namakota"},
{Expression: "namaprovinsi"},
},
Filters: []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
LogicOp: "AND",
}},
Limit: 1,
}
// Add joins for relationships using the correct structure
dynamicQuery.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
var dataPasien pasienModels.Pasien
err = h.queryBuilder.ExecuteQueryRow(ctx, dbConn, dynamicQuery, &dataPasien)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Pasien not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get pasien", err, http.StatusInternalServerError)
}
return
}
// Save to cache
h.cache.Set(cacheKey, dataPasien, 30*time.Minute) // Cache for 30 minutes
response := pasienModels.PasienGetByIDResponse{
Message: "Pasien details retrieved successfully",
Data: &dataPasien,
}
c.JSON(http.StatusOK, response)
}
// GetPasienByNomr godoc
// @Summary Get Pasien by Nomr
// @Description Get pasien by Nomr
// @Tags Pasien
// @Accept json
// @Produce json
// @Success 200 {object} pasienModels.PasienGetByNomrResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/nomr/:nomr [get]
func (h *PasienHandler) GetPasienByNomr(c *gin.Context) {
// Increase timeout for complex queries
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
// Use the core fetchPasiensDynamic function for all data retrieval logic.
query := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{
{Expression: "id"},
{Expression: "nomr"},
{Expression: "status"},
{Expression: "title"},
{Expression: "nama"},
{Expression: "tempat"},
{Expression: "tgllahir"},
{Expression: "jeniskelamin"},
{Expression: "alamat"},
{Expression: "kelurahan"},
{Expression: "kdkecamatan"},
{Expression: "kota"},
{Expression: "kdprovinsi"},
{Expression: "agama"},
{Expression: "no_kartu"},
{Expression: "noktp_baru"},
{Expression: "namakelurahan"},
{Expression: "namakecamatan"},
{Expression: "namakota"},
{Expression: "namaprovinsi"},
},
Sort: []queryUtils.SortField{{Column: "date_created", Order: "DESC"}},
}
// Add joins for relationships using the correct structure
query.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
// 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
}
// Use GetSQLXDB to get database connection
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Parse simple filters
var filters []queryUtils.DynamicFilter
if status := c.Query("status"); status != "" && models.IsValidStatus(status) {
filters = append(filters, queryUtils.DynamicFilter{Column: "status", Operator: queryUtils.OpEqual, Value: status})
}
// Optimize query search with caching
search := c.Query("search")
var searchFilters []queryUtils.DynamicFilter
var cacheKey string
var useCache bool
// Initialize searchFilters before using it in the cache hit section
if search != "" {
// Limit search length to prevent slow queries
if len(search) > 50 {
search = search[:50]
}
// Generate cache key for search
cacheKey = fmt.Sprintf("pasien:search:%s:%d:%d", search, query.Limit, query.Offset)
// Initialize searchFilters here
searchFilters = []queryUtils.DynamicFilter{
{Column: "nomr", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
{Column: "title", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
{Column: "nama", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
{Column: "no_kartu", Operator: queryUtils.OpILike, Value: "%" + search + "%"},
}
// Try to get from cache first
if cachedData, found := h.cache.Get(cacheKey); found {
logger.Info("Cache hit for search", map[string]interface{}{"search": search, "cache_key": cacheKey})
// Convert from interface{} to expected type
pasiens, ok := cachedData.([]pasienModels.Pasien)
if !ok {
logger.Error("Failed to convert cached data", map[string]interface{}{"cache_key": cacheKey})
} else {
// If requested, get aggregation data
var aggregateData *models.AggregateData
if c.Query("include_summary") == "true" {
// Build full filter groups for aggregate data (including search filters)
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
}
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, len(pasiens))
response := pasienModels.PasienGetByNomrResponse{
Message: "Data pasien berhasil diambil (dari cache)",
Data: pasiens,
Meta: meta,
}
if aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
return
}
}
// If not in cache, mark for saving after query
useCache = true
// If there's search, create OR filter group
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"})
}
// Add other filters (if any) as AND group
if len(filters) > 0 {
query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"})
}
pasiens, total, err := h.fetchPasiensDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Save search results to cache if there's a search parameter
if useCache && len(pasiens) > 0 {
h.cache.Set(cacheKey, pasiens, 15*time.Minute) // Cache for 15 minutes
logger.Info("Cached search results", map[string]interface{}{"search": search, "cache_key": cacheKey, "count": len(pasiens)})
}
// If requested, get aggregation data
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 := pasienModels.PasienGetByNomrResponse{
Message: "Data pasien berhasil diambil",
Data: pasiens,
Meta: meta,
}
if aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// UpdatePasien godoc
// @Summary Update Pasien
// @Description Update an existing pasien
// @Tags Pasien
// @Accept json
// @Produce json
// @Param id path string true "Pasien ID"
// @Param request body pasienModels.PasienUpdateRequest true "Pasien update request"
// @Success 200 {object} pasienModels.PasienUpdateResponse "Pasien updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "Pasien not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/:nomr [put]
func (h *PasienHandler) UpdatePasien(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 pasienModels.PasienUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Set the ID from path parameter to request
idInt, err := strconv.Atoi(id)
if err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
idInt32 := int32(idInt)
req.ID = &idInt32
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
// Try to get old data for cache invalidation
var oldData pasienModels.Pasien
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err == nil {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
dynamicQuery := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{{Expression: "*"}},
Filters: []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
},
LogicOp: "AND",
}},
Limit: 1,
}
// Add joins for relationships using the correct structure
dynamicQuery.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
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})
}
}
// Use GetSQLXDB to get database connection
dbConn, err = h.db.GetSQLXDB("postgres_simrs")
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: "m_pasien",
UniqueColumns: []string{"id"},
Conditions: []queryUtils.DynamicFilter{
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
ExcludeIDColumn: "id", // Exclude based on 'id' column
ExcludeIDValue: id, // ...with id value from parameter
}
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 nomr must be unique, except for record with this id
if req.Nomr != nil && *req.Nomr != "" {
rule := validation.ValidationRule{
TableName: "m_pasien",
UniqueColumns: []string{"nomr"},
Conditions: []queryUtils.DynamicFilter{
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
ExcludeIDColumn: "id", // Exclude based on 'id' column
ExcludeIDValue: id, // ...with id value from parameter
}
dataToValidate := map[string]interface{}{
"nomr": *req.Nomr,
}
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
if err != nil {
h.logAndRespondError(c, "Failed to validate nomr", err, http.StatusInternalServerError)
return
}
if isDuplicate {
h.respondError(c, "nomr already exists", fmt.Errorf("duplicate nomr: %s", *req.Nomr), http.StatusConflict)
return
}
}
// Validate no_kartu must be unique, except for record with this id
if req.NoKartu != nil && *req.NoKartu != "" {
rule := validation.ValidationRule{
TableName: "m_pasien",
UniqueColumns: []string{"no_kartu"},
Conditions: []queryUtils.DynamicFilter{
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
ExcludeIDColumn: "id", // Exclude based on 'id' column
ExcludeIDValue: id, // ...with id value from parameter
}
dataToValidate := map[string]interface{}{
"no_kartu": *req.NoKartu,
}
isDuplicate, err := h.validator.Validate(ctx, dbConn, rule, dataToValidate)
if err != nil {
h.logAndRespondError(c, "Failed to validate no_kartu", err, http.StatusInternalServerError)
return
}
if isDuplicate {
h.respondError(c, "no_kartu already exists", fmt.Errorf("duplicate no_kartu: %s", *req.NoKartu), http.StatusConflict)
return
}
}
updateData := queryUtils.UpdateData{
Columns: []string{
"status",
"date_updated",
"id",
"nomr",
"status",
"title",
"nama",
"tempat",
"tgllahir",
"jeniskelamin",
"alamat",
"kelurahan",
"kdkecamatan",
"kota",
"kdprovinsi",
"agama",
"no_kartu",
"noktp_baru",
},
Values: []interface{}{
req.Status,
time.Now(),
req.ID,
req.Nomr,
req.Status,
req.Title,
req.Nama,
req.Tempat,
req.Tgllahir,
req.Jeniskelamin,
req.Alamat,
req.Kelurahan,
req.Kdkecamatan,
req.Kota,
req.Kdprovinsi,
req.Agama,
req.NoKartu,
req.NoktpBaru,
},
}
filters := []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: req.ID},
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
LogicOp: "AND",
}}
returningCols := []string{
"status",
"sort", "user_created", "date_created", "user_updated", "date_updated",
"id",
"nomr",
"status",
"title",
"nama",
"tempat",
"tgllahir",
"jeniskelamin",
"alamat",
"kelurahan",
"kdkecamatan",
"kota",
"kdprovinsi",
"agama",
"no_kartu",
"noktp_baru",
}
sql, args, err := h.queryBuilder.BuildUpdateQuery("m_pasien", updateData, filters, returningCols...)
if err != nil {
h.logAndRespondError(c, "Failed to build update query", err, http.StatusInternalServerError)
return
}
var dataPasien pasienModels.Pasien
err = dbConn.GetContext(ctx, &dataPasien, sql, args...)
if err != nil {
if err.Error() == "sql: no rows in result set" {
h.respondError(c, "Pasien not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update pasien", err, http.StatusInternalServerError)
}
return
}
// Invalidate cache that might be affected
// Invalidate cache for id that was updated
cacheKey := fmt.Sprintf("pasien:id:%s", id)
h.cache.Delete(cacheKey)
// Invalidate cache for old and new data
if oldData.ID != 0 {
h.invalidateRelatedCache()
}
h.invalidateRelatedCache()
response := pasienModels.PasienUpdateResponse{Message: "Pasien berhasil diperbarui", Data: &dataPasien}
c.JSON(http.StatusOK, response)
}
// DeletePasien godoc
// @Summary Delete Pasien
// @Description Delete a pasien
// @Tags Pasien
// @Accept json
// @Produce json
// @Param id path string true "Pasien ID"
// @Success 200 {object} pasienModels.PasienDeleteResponse "Pasien deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Pasien not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/pasien/:nomr [delete]
func (h *PasienHandler) DeletePasien(c *gin.Context) {
id := c.Param("id")
if id == "" {
h.respondError(c, "Invalid ID format", fmt.Errorf("id cannot be empty"), http.StatusBadRequest)
return
}
// Try to get data for cache invalidation
var dataToDelete pasienModels.Pasien
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err == nil {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
dynamicQuery := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{{Expression: "*"}},
Filters: []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
},
LogicOp: "AND",
}},
Limit: 1,
}
// Add joins for relationships using the correct structure
dynamicQuery.Joins = []queryUtils.Join{
{
Type: "LEFT",
Table: "m_provinsi",
Alias: "m_provinsi",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdprovinsi", Operator: queryUtils.OpEqual, Value: "m_provinsi.idprovinsi"},
},
},
},
{
Type: "LEFT",
Table: "m_kota",
Alias: "m_kota",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kota", Operator: queryUtils.OpEqual, Value: "m_kota.idkota"},
},
},
},
{
Type: "LEFT",
Table: "m_kecamatan",
Alias: "m_kecamatan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kdkecamatan", Operator: queryUtils.OpEqual, Value: "m_kecamatan.idkecamatan"},
},
},
},
{
Type: "LEFT",
Table: "m_kelurahan",
Alias: "m_kelurahan",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "m_pasien.kelurahan", Operator: queryUtils.OpEqual, Value: "m_kelurahan.idkelurahan"},
},
},
},
}
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})
}
}
// Use GetSQLXDB to get database connection
dbConn, err = h.db.GetSQLXDB("postgres_simrs")
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()
// Use ExecuteUpdate for soft delete by changing status
updateData := queryUtils.UpdateData{
Columns: []string{"status", "date_updated"},
Values: []interface{}{"deleted", time.Now()},
}
filters := []queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: "id", Operator: queryUtils.OpEqual, Value: id},
{Column: "status", Operator: queryUtils.OpNotEqual, Value: "deleted"},
},
LogicOp: "AND",
}}
// Use ExecuteUpdate instead of ExecuteDelete
result, err := h.queryBuilder.ExecuteUpdate(ctx, dbConn, "m_pasien", updateData, filters)
if err != nil {
h.logAndRespondError(c, "Failed to delete pasien", 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, "Pasien not found", sql.ErrNoRows, http.StatusNotFound)
return
}
// Invalidate cache that might be affected
// Invalidate cache for id that was deleted
cacheKey := fmt.Sprintf("pasien:id:%s", id)
h.cache.Delete(cacheKey)
// Invalidate cache for data that was deleted
if dataToDelete.ID != 0 {
h.invalidateRelatedCache()
}
response := pasienModels.PasienDeleteResponse{Message: "Pasien berhasil dihapus", ID: id}
c.JSON(http.StatusOK, response)
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
// invalidateRelatedCache invalidates cache that might be affected by data changes
func (h *PasienHandler) invalidateRelatedCache() {
// Invalidate cache for search that might be affected
h.cache.DeleteByPrefix("pasien:search:")
h.cache.DeleteByPrefix("pasien:dynamic:")
h.cache.DeleteByPrefix("pasien:stats:")
h.cache.DeleteByPrefix("pasien:id:")
}
// fetchPasiensDynamic executes dynamic query with timeout handling
func (h *PasienHandler) fetchPasiensDynamic(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) ([]pasienModels.Pasien, int, error) {
logger.Info("Starting fetchPasiensDynamic", map[string]interface{}{
"limit": query.Limit,
"offset": query.Offset,
"from": query.From,
})
var total int
var pasiens []pasienModels.Pasien
// Check if query has search
hasSearch := false
for _, filterGroup := range query.Filters {
for _, filter := range filterGroup.Filters {
if filter.Operator == queryUtils.OpILike {
hasSearch = true
break
}
}
if hasSearch {
break
}
}
logger.Info("Query analysis", map[string]interface{}{
"hasSearch": hasSearch,
"totalFilters": len(query.Filters),
})
// Optimize to prevent timeout on search queries
// Use shorter context for search and count queries
queryCtx, queryCancel := context.WithTimeout(ctx, 30*time.Second)
defer queryCancel()
// For search queries, limit maximum to prevent timeout
if hasSearch {
search := getSearchTerm(query)
logger.Info("Executing search query with timeout context", map[string]interface{}{"search_term": search})
// Limit maximum search limit to prevent timeout
maxSearchLimit := 50
if query.Limit > maxSearchLimit {
query.Limit = maxSearchLimit
logger.Info("Reduced search limit to prevent timeout", map[string]interface{}{
"original_limit": query.Limit,
"new_limit": maxSearchLimit,
})
}
// Execute search query
err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &pasiens)
if err != nil {
// Check if it's a PostgreSQL statement timeout error
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "57014" {
logger.Warn("Search query timed out, trying fallback strategy", map[string]interface{}{
"search_term": search,
})
// Fallback: Search only in the most relevant column
// We need to rebuild the filters for the fallback
var fallbackFilters []queryUtils.FilterGroup
// Add other non-search filters back (e.g., status)
for _, fg := range query.Filters {
if fg.LogicOp == "AND" {
fallbackFilters = append(fallbackFilters, fg)
}
}
// Add the single, more specific search filter
searchableColumns := []string{
"nomr",
"title",
"nama",
"no_kartu",
}
if len(searchableColumns) > 0 {
fallbackFilters = append([]queryUtils.FilterGroup{{
Filters: []queryUtils.DynamicFilter{
{Column: searchableColumns[0], Operator: queryUtils.OpILike, Value: "%" + search + "%"},
},
LogicOp: "AND",
}}, fallbackFilters...)
fallbackQuery := query
fallbackQuery.Filters = fallbackFilters
// Execute the fallback query with a shorter timeout
fallbackCtx, fallbackCancel := context.WithTimeout(ctx, 10*time.Second)
defer fallbackCancel()
err = h.queryBuilder.ExecuteQuery(fallbackCtx, dbConn, fallbackQuery, &pasiens)
if err != nil {
logger.Error("Fallback search query also failed", map[string]interface{}{
"error": err.Error(),
"query": fallbackQuery,
})
// Return a more user-friendly error
return nil, 0, fmt.Errorf("search timed out. The search term '%s' is too general. Please try a more specific term", search)
}
logger.Info("Fallback search query successful", map[string]interface{}{
"recordsFetched": len(pasiens),
})
}
} else {
// It's a different error, handle it as before
logger.Error("Failed to execute search query", map[string]interface{}{
"error": err.Error(),
"query": query,
})
return nil, 0, fmt.Errorf("failed to execute search query: %w", err)
}
}
// Estimate total for search query (don't count exact for performance)
total = len(pasiens)
if len(pasiens) == query.Limit {
// If reached limit, estimate there are more data
total = query.Offset + query.Limit + 100
} else {
total = query.Offset + len(pasiens)
}
} else {
logger.Info("Executing regular query without search")
// For queries without search, count total with shorter timeout
countCtx, countCancel := context.WithTimeout(ctx, 15*time.Second)
defer countCancel()
count, err := h.queryBuilder.ExecuteCount(countCtx, dbConn, query)
if err != nil {
// If count failed, fallback to estimation or return error
logger.Warn("Failed to get exact count, using estimation", map[string]interface{}{"error": err.Error()})
// For queries without search, we can estimate based on limit
total = query.Offset + query.Limit + 100 // Conservative estimation
} else {
total = int(count)
}
logger.Info("Count query successful", map[string]interface{}{
"count": total,
})
// Execute main data query
err = h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &pasiens)
if err != nil {
logger.Error("Failed to execute main query", map[string]interface{}{
"error": err.Error(),
"query": query,
})
return nil, 0, fmt.Errorf("failed to execute main query: %w", err)
}
logger.Info("Data query successful", map[string]interface{}{
"recordsFetched": len(pasiens),
})
}
logger.Info("Query execution completed", map[string]interface{}{
"totalRecords": total,
"returnedRecords": len(pasiens),
"hasSearch": hasSearch,
})
return pasiens, total, nil
}
// getSearchTerm extracts the search term from a DynamicQuery object.
// It assumes the search is the first filter group with an "OR" logic operator.
func getSearchTerm(query queryUtils.DynamicQuery) string {
for _, filterGroup := range query.Filters {
if filterGroup.LogicOp == "OR" && len(filterGroup.Filters) > 0 {
if valueStr, ok := filterGroup.Filters[0].Value.(string); ok {
return strings.Trim(valueStr, "%")
}
}
}
return ""
}
// getAggregateData gets comprehensive statistics about pasien data
func (h *PasienHandler) 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, 4)
// 1. Count by status
wg.Add(1)
go func() {
defer wg.Done()
// Use context with shorter timeout
queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second)
defer queryCancel()
query := queryUtils.DynamicQuery{
From: "m_pasien",
Fields: []queryUtils.SelectField{
{Expression: "status"},
{Expression: "COUNT(*)", Alias: "count"},
},
Filters: filterGroups,
GroupBy: []string{"status"},
}
var results []struct {
Status string `db:"status"`
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()
}()
// 4. Get last updated and today's stats
wg.Add(1)
go func() {
defer wg.Done()
// Use context with shorter timeout
queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second)
defer queryCancel()
// Last updated
query1 := queryUtils.DynamicQuery{
From: "m_pasien",
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
}
// Using QueryBuilder for today's statistics
today := time.Now().Format("2006-01-02")
// Query for created_today
createdTodayQuery := queryUtils.DynamicQuery{
From: "m_pasien",
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 (updated today but not created today)
updatedTodayQuery := queryUtils.DynamicQuery{
From: "m_pasien",
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
}
// logAndRespondError logs an error and sends a JSON response
func (h *PasienHandler) 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)
}
// respondError sends a standardized JSON error response
func (h *PasienHandler) 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()})
}
// calculateMeta creates pagination metadata
func (h *PasienHandler) 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,
}
}
func (h *PasienHandler) GetPasienByNOMR(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Nomr := c.Param("nomr")
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
Pasien, err := h.Getpasien(ctx, dbConn, Nomr)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{
"error": "Pasien tidak ditemukan",
"nomr": Nomr,
})
return
}
logger.Error("Failed to get pasien", map[string]interface{}{
"error": err.Error(),
"nomr": Nomr,
})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve pasien data"})
return
}
logger.Info("Pasien data retrieved successfully", map[string]interface{}{
"nomr": Nomr,
"nama": Pasien.NamaLengkap,
})
c.JSON(http.StatusOK, gin.H{
"message": "Data pasien berhasil diambil",
"data": Pasien,
})
}
func (h *PasienHandler) Getpasien(ctx context.Context, dbConn *sqlx.DB, nomr string) (*pasienModels.PasienData, error) {
query := `
SELECT
mp.nomr,
mp.nama,
mp.notelp,
mp.jeniskelamin,
mp.tgllahir,
mp.alamat,
COALESCE(mp2.idprovinsi, 0) as idprovinsi,
COALESCE(mp2.namaprovinsi, '') as namaprovinsi,
COALESCE(mk2.idkota, 0) as idkota,
COALESCE(mk2.namakota, '') as namakota,
COALESCE(mk.idkecamatan, 0) as idkecamatan,
COALESCE(mk.namakecamatan, '') as namakecamatan,
COALESCE(mk3.idkelurahan, 0) as idkelurahan,
COALESCE(mk3.namakelurahan, '') as namakelurahan
FROM m_pasien mp
LEFT JOIN m_provinsi mp2 ON mp.kdprovinsi = mp2.idprovinsi
LEFT JOIN m_kecamatan mk ON mp.kdkecamatan = mk.idkecamatan
LEFT JOIN m_kota mk2 ON mk.idkota = mk2.idkota
LEFT JOIN m_kelurahan mk3 ON mp.kelurahan = mk3.idkelurahan
WHERE mp.nomr = $1
`
var pasien pasienModels.PasienData
err := dbConn.QueryRowContext(ctx, query, nomr).Scan(
&pasien.Nomr,
&pasien.NamaLengkap,
&pasien.NoTelp,
&pasien.JenisKelamin,
&pasien.TTL,
&pasien.Alamat,
&pasien.IdProvinsi,
&pasien.NamaProvinsi,
&pasien.IdKota,
&pasien.NamaKota,
&pasien.IdKecamatan,
&pasien.NamaKecamatan,
&pasien.IdKelurahan,
&pasien.NamaKelurahan,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, fmt.Errorf("failed to query pasien: %w", err)
}
logger.Info("Pasien query executed successfully", map[string]interface{}{
"nomr": nomr,
"nama": pasien.NamaLengkap,
})
return &pasien, nil
}
func (h *PasienHandler) SearchPasienByNama(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Get query parameter
nama := c.Query("nama")
// Validasi minimal 2 karakter untuk search
if len(nama) < 2 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Minimal 2 karakter untuk pencarian",
"message": "Silahkan ketik minimal 2 huruf",
})
return
}
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
// Search pasien by nama
pasienList, err := h.GetPasienByNamaPattern(ctx, dbConn, nama)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, gin.H{
"message": "Tidak ada pasien ditemukan",
"data": []pasienModels.PasienData{},
"meta": map[string]interface{}{
"total": 0,
"search_query": nama,
},
})
return
}
logger.Error("Failed to search pasien", map[string]interface{}{
"error": err.Error(),
"nama": nama,
})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search pasien"})
return
}
logger.Info("Pasien search completed", map[string]interface{}{
"search_query": nama,
"total_found": len(pasienList),
})
c.JSON(http.StatusOK, gin.H{
"message": "Data pasien berhasil ditemukan",
"data": pasienList,
"meta": map[string]interface{}{
"total": len(pasienList),
"search_query": nama,
},
})
}
func (h *PasienHandler) GetPasienByNamaPattern(ctx context.Context, dbConn *sqlx.DB, namaPattern string) ([]*pasienModels.PasienData, error) {
searchPattern := "%" + namaPattern + "%"
query := `
SELECT
mp.nomr,
mp.nama,
mp.notelp,
mp.jeniskelamin,
mp.tgllahir,
mp.alamat,
mp2.idprovinsi,
mp2.namaprovinsi,
mk2.idkota,
mk2.namakota,
mk.idkecamatan,
mk.namakecamatan,
mk3.idkelurahan,
mk3.namakelurahan
FROM m_pasien mp
LEFT JOIN m_provinsi mp2 ON mp.kdprovinsi = mp2.idprovinsi
LEFT JOIN m_kecamatan mk ON mp.kdkecamatan = mk.idkecamatan
LEFT JOIN m_kota mk2 ON mk.idkota = mk2.idkota
LEFT JOIN m_kelurahan mk3 ON mp.kelurahan = mk3.idkelurahan
WHERE mp.nama ILIKE $1
ORDER BY mp.nama ASC
LIMIT 1
`
rows, err := dbConn.QueryContext(ctx, query, searchPattern)
if err != nil {
return nil, fmt.Errorf("failed to execute search query: %w", err)
}
defer rows.Close()
var pasienList []*pasienModels.PasienData
for rows.Next() {
var pasien pasienModels.PasienData
err := rows.Scan(
&pasien.Nomr,
&pasien.NamaLengkap,
&pasien.NoTelp,
&pasien.JenisKelamin,
&pasien.TTL,
&pasien.Alamat,
&pasien.IdProvinsi,
&pasien.NamaProvinsi,
&pasien.IdKota,
&pasien.NamaKota,
&pasien.IdKecamatan,
&pasien.NamaKecamatan,
&pasien.IdKelurahan,
&pasien.NamaKelurahan,
)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
// Clean strings
pasien.Nomr = cleanString(pasien.Nomr)
pasien.NamaLengkap = cleanString(pasien.NamaLengkap)
pasien.NoTelp = cleanString(pasien.NoTelp)
pasien.JenisKelamin = cleanString(pasien.JenisKelamin)
pasien.Alamat = cleanString(pasien.Alamat)
pasien.NamaProvinsi = cleanString(pasien.NamaProvinsi)
pasien.NamaKota = cleanString(pasien.NamaKota)
pasien.NamaKecamatan = cleanString(pasien.NamaKecamatan)
pasien.NamaKelurahan = cleanString(pasien.NamaKelurahan)
pasienList = append(pasienList, &pasien)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
if len(pasienList) == 0 {
return nil, sql.ErrNoRows
}
logger.Info("Search query executed successfully", map[string]interface{}{
"pattern": namaPattern,
"found": len(pasienList),
})
return pasienList, nil
}
func cleanString(s string) string {
// Trim leading dan trailing whitespace
s = strings.TrimSpace(s)
// Replace newline, carriage return, dan tab di tengah string dengan single space
s = strings.ReplaceAll(s, "\n", " ")
s = strings.ReplaceAll(s, "\r", " ")
s = strings.ReplaceAll(s, "\t", " ")
// Replace multiple spaces dengan single space
s = strings.Join(strings.Fields(s), " ")
return s
}