2
go.mod
2
go.mod
@@ -29,7 +29,6 @@ require (
|
|||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.30.0
|
gorm.io/gorm v1.30.0
|
||||||
)
|
)
|
||||||
@@ -37,6 +36,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|||||||
@@ -309,7 +309,6 @@ func (h *ListkioskHandler) UpdateKiosk(c *gin.Context) {
|
|||||||
// Set ID from path parameter
|
// Set ID from path parameter
|
||||||
// req.ID = id
|
// req.ID = id
|
||||||
|
|
||||||
// Set ID from path parameter
|
|
||||||
idInt, _ := strconv.Atoi(id)
|
idInt, _ := strconv.Atoi(id)
|
||||||
req.ID = idInt
|
req.ID = idInt
|
||||||
|
|
||||||
@@ -450,7 +449,15 @@ func (h *ListkioskHandler) createKiosk(ctx context.Context, dbConn *sql.DB, req
|
|||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
|
RETURNING id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
|
||||||
|
|
||||||
row := dbConn.QueryRowContext(ctx, query, req.Name, req.Icon, req.Url, req.Active, req.FKRefHealthcareTypeID, req.FKRefServiceTypeID, req.FKSdLocationID, req.DsSdLocation)
|
// ensure optional UUID-like string is passed as NULL when empty
|
||||||
|
var fkLocation interface{}
|
||||||
|
if strings.TrimSpace(req.FKSdLocationID) == "" {
|
||||||
|
fkLocation = nil
|
||||||
|
} else {
|
||||||
|
fkLocation = req.FKSdLocationID
|
||||||
|
}
|
||||||
|
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, req.Name, req.Icon, req.Url, req.Active, req.FKRefHealthcareTypeID, req.FKRefServiceTypeID, fkLocation, req.DsSdLocation)
|
||||||
|
|
||||||
var item kiosk.Listkiosk
|
var item kiosk.Listkiosk
|
||||||
err := row.Scan(&item.ID, &item.Name, &item.Icon, &item.Url, &item.Active, &item.FKRefHealthcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation)
|
err := row.Scan(&item.ID, &item.Name, &item.Icon, &item.Url, &item.Active, &item.FKRefHealthcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation)
|
||||||
@@ -469,7 +476,15 @@ func (h *ListkioskHandler) updateKiosk(ctx context.Context, dbConn *sql.DB, req
|
|||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
|
RETURNING id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
|
||||||
|
|
||||||
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Name, req.Icon, req.Url, req.Active, req.FKRefHealthcareTypeID, req.FKRefServiceTypeID, req.FKSdLocationID, req.DsSdLocation)
|
// ensure optional UUID-like string is passed as NULL when empty
|
||||||
|
var fkLocation interface{}
|
||||||
|
if strings.TrimSpace(req.FKSdLocationID) == "" {
|
||||||
|
fkLocation = nil
|
||||||
|
} else {
|
||||||
|
fkLocation = req.FKSdLocationID
|
||||||
|
}
|
||||||
|
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Name, req.Icon, req.Url, req.Active, req.FKRefHealthcareTypeID, req.FKRefServiceTypeID, fkLocation, req.DsSdLocation)
|
||||||
|
|
||||||
var item kiosk.Listkiosk
|
var item kiosk.Listkiosk
|
||||||
err := row.Scan(&item.ID, &item.Name, &item.Icon, &item.Url, &item.Active, &item.FKRefHealthcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation)
|
err := row.Scan(&item.ID, &item.Name, &item.Icon, &item.Url, &item.Active, &item.FKRefHealthcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation)
|
||||||
|
|||||||
898
internal/handlers/patient/patient.go
Normal file
898
internal/handlers/patient/patient.go
Normal file
@@ -0,0 +1,898 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"api-service/internal/config"
|
||||||
|
"api-service/internal/database"
|
||||||
|
models "api-service/internal/models"
|
||||||
|
"api-service/internal/models/patient"
|
||||||
|
"api-service/internal/utils/validation"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
patientdb database.Service
|
||||||
|
patientonce sync.Once
|
||||||
|
patientvalidate *validator.Validate
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize the database connection and validator
|
||||||
|
func init() {
|
||||||
|
patientonce.Do(func() {
|
||||||
|
patientdb = database.New(config.LoadConfig())
|
||||||
|
patientvalidate = validator.New()
|
||||||
|
patientvalidate.RegisterValidation("patient_status", validatePatientStatus)
|
||||||
|
if patientdb == nil {
|
||||||
|
log.Fatal("Failed to initialize database connection")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation for patient status
|
||||||
|
func validatePatientStatus(fl validator.FieldLevel) bool {
|
||||||
|
return models.IsValidStatus(fl.Field().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// PatientHandler handles patient services
|
||||||
|
type PatientHandler struct {
|
||||||
|
db database.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPatientHandler creates a new PatientHandler
|
||||||
|
func NewPatientHandler() *PatientHandler {
|
||||||
|
return &PatientHandler{
|
||||||
|
db: patientdb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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} patient.PatientGetResponse "Success response"
|
||||||
|
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||||
|
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||||
|
// @Router /patient/listpatient [get]
|
||||||
|
func (h *PatientHandler) GetPatient(c *gin.Context) {
|
||||||
|
// Parse pagination parameters
|
||||||
|
limit, offset, err := h.parsePaginationParams(c)
|
||||||
|
if err != nil {
|
||||||
|
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse filter parameters
|
||||||
|
filter := h.parseFilterParams(c)
|
||||||
|
includeAggregation := c.Query("include_summary") == "true"
|
||||||
|
|
||||||
|
// Get database connection
|
||||||
|
dbConn, err := h.db.GetDB("db_antrean")
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Execute concurrent operations
|
||||||
|
var (
|
||||||
|
items []patient.Patient
|
||||||
|
total int
|
||||||
|
aggregateData *models.AggregateData
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errChan = make(chan error, 3)
|
||||||
|
mu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch total count
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errChan <- fmt.Errorf("failed to get total count: %w", err)
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Fetch main data
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
result, err := h.fetchPatients(ctx, dbConn, filter, limit, offset)
|
||||||
|
mu.Lock()
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("failed to fetch data: %w", err)
|
||||||
|
} else {
|
||||||
|
items = result
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Fetch aggregation data if requested
|
||||||
|
if includeAggregation {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
result, err := h.getAggregateData(ctx, dbConn, filter)
|
||||||
|
mu.Lock()
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
|
||||||
|
} else {
|
||||||
|
aggregateData = result
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all goroutines
|
||||||
|
wg.Wait()
|
||||||
|
close(errChan)
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
for err := range errChan {
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response
|
||||||
|
meta := h.calculateMeta(limit, offset, total)
|
||||||
|
response := patient.PatientGetResponse{
|
||||||
|
Message: "Data patient berhasil diambil",
|
||||||
|
Data: items,
|
||||||
|
Meta: meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
if includeAggregation && 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 patient.PatientPostRequest true "Patient post request"
|
||||||
|
// @Success 200 {object} patient.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/{id} [post]
|
||||||
|
func (h *PatientHandler) GetPatientByIDPost(c *gin.Context) {
|
||||||
|
// id := c.Param("id")
|
||||||
|
|
||||||
|
// Validate UUID format
|
||||||
|
// if _, err := uuid.Parse(id); err != nil {
|
||||||
|
// h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
var req patient.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.GetDB("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()
|
||||||
|
|
||||||
|
item, err := h.getPatientByIDPost(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Patient not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to get patient", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := patient.PatientCreateResponse{
|
||||||
|
Message: "patient details retrieved successfully",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 patient.PatientCreateRequest true "Patient creation request"
|
||||||
|
// @Success 201 {object} patient.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) CreatePatient(c *gin.Context) {
|
||||||
|
var req patient.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.GetDB("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 duplicate and daily submission
|
||||||
|
if err := h.validatePatientSubmission(ctx, dbConn, &req); err != nil {
|
||||||
|
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := h.createPatient(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndRespondError(c, "Failed to create patient", err, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := patient.PatientCreateResponse{
|
||||||
|
Message: "Patient berhasil dibuat",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 integer true "Patient ID"
|
||||||
|
// @Param request body patient.PatientUpdateRequest true "Patient update request"
|
||||||
|
// @Success 200 {object} patient.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) UpdatePatient(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
// Validate UUID format
|
||||||
|
// if _, err := uuid.Parse(id); err != nil {
|
||||||
|
// h.respondError(c, "Invalid ID format", err, 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 patient.PatientUpdateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ID from path parameter
|
||||||
|
// req.ID = id
|
||||||
|
idInt, _ := strconv.Atoi(id)
|
||||||
|
req.ID = idInt
|
||||||
|
|
||||||
|
// Validate request
|
||||||
|
if err := patientvalidate.Struct(&req); err != nil {
|
||||||
|
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := h.db.GetDB("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()
|
||||||
|
|
||||||
|
item, err := h.updatePatient(ctx, dbConn, &req)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Patient not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to update patient", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := patient.PatientUpdateResponse{
|
||||||
|
Message: "Patient berhasil diperbarui",
|
||||||
|
Data: item,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 integer true "Patient ID"
|
||||||
|
// @Success 200 {object} patient.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) DeletePatient(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
|
||||||
|
// Validate ID is integer
|
||||||
|
if _, err := strconv.Atoi(id); err != nil {
|
||||||
|
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dbConn, err := h.db.GetDB("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()
|
||||||
|
|
||||||
|
err = h.deletePatient(ctx, dbConn, id)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
h.respondError(c, "Patient not found", err, http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
h.logAndRespondError(c, "Failed to delete patient", err, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := patient.PatientDeleteResponse{
|
||||||
|
Message: "Patient berhasil dihapus",
|
||||||
|
ID: id,
|
||||||
|
}
|
||||||
|
|
||||||
|
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.GetDB("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()
|
||||||
|
|
||||||
|
filter := h.parseFilterParams(c)
|
||||||
|
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database operations
|
||||||
|
func (h *PatientHandler) getPatientByIDPost(ctx context.Context, dbConn *sql.DB, req *patient.PatientPostRequest) (*patient.Patient, error) {
|
||||||
|
query := `SELECT 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
|
||||||
|
FROM master.ms_patient WHERE id = $1`
|
||||||
|
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, req.ID)
|
||||||
|
|
||||||
|
var item patient.Patient
|
||||||
|
err := row.Scan(&item.ID, &item.Name, &item.MedicalRecordNumber, &item.PhoneNumber, &item.Gender, &item.BirthDate, &item.Address, &item.Active, &item.FKSdProvinsiID, &item.FKSdKabupatenKotaID, &item.FKSdKecamatanID, &item.FKSdKelurahanID, &item.DsSdProvinsi, &item.DsSdKabupatenKota, &item.DsSdKecamatan, &item.DsSdKelurahan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) createPatient(ctx context.Context, dbConn *sql.DB, req *patient.PatientCreateRequest) (*patient.Patient, error) {
|
||||||
|
// id := uuid.New().String()
|
||||||
|
// now := time.Now()
|
||||||
|
|
||||||
|
query := `INSERT INTO master.ms_patient
|
||||||
|
(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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
|
RETURNING 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`
|
||||||
|
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, 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)
|
||||||
|
|
||||||
|
var item patient.Patient
|
||||||
|
err := row.Scan(&item.ID, &item.Name, &item.MedicalRecordNumber, &item.PhoneNumber, &item.Gender, &item.BirthDate, &item.Address, &item.Active, &item.FKSdProvinsiID, &item.FKSdKabupatenKotaID, &item.FKSdKecamatanID, &item.FKSdKelurahanID, &item.DsSdProvinsi, &item.DsSdKabupatenKota, &item.DsSdKecamatan, &item.DsSdKelurahan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create patient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) updatePatient(ctx context.Context, dbConn *sql.DB, req *patient.PatientUpdateRequest) (*patient.Patient, error) {
|
||||||
|
// now := time.Now()
|
||||||
|
|
||||||
|
query := `UPDATE master.ms_patient
|
||||||
|
SET name = $2, medical_record_number = $3, phone_number = $4, gender = $5, birth_date = $6, address = $7, active = $8, fk_sd_provinsi_id = $9, fk_sd_kabupaten_kota_id = $10, fk_sd_kecamatan_id = $11, fk_sd_kelurahan_id = $12, ds_sd_provinsi = $13, ds_sd_kabupaten_kota = $14, ds_sd_kecamatan = $15, ds_sd_kelurahan = $16
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING 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`
|
||||||
|
|
||||||
|
row := dbConn.QueryRowContext(ctx, query, req.ID, 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)
|
||||||
|
|
||||||
|
var item patient.Patient
|
||||||
|
err := row.Scan(&item.ID, &item.Name, &item.MedicalRecordNumber, &item.PhoneNumber, &item.Gender, &item.BirthDate, &item.Address, &item.Active, &item.FKSdProvinsiID, &item.FKSdKabupatenKotaID, &item.FKSdKecamatanID, &item.FKSdKelurahanID, &item.DsSdProvinsi, &item.DsSdKabupatenKota, &item.DsSdKecamatan, &item.DsSdKelurahan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update patient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) deletePatient(ctx context.Context, dbConn *sql.DB, id string) error {
|
||||||
|
// now := time.Now()
|
||||||
|
query := `UPDATE master.ms_patient
|
||||||
|
SET active = false
|
||||||
|
WHERE id = $1`
|
||||||
|
|
||||||
|
result, err := dbConn.ExecContext(ctx, query, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete patient: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) fetchPatients(ctx context.Context, dbConn *sql.DB, filter patient.PatientFilter, limit, offset int) ([]patient.Patient, error) {
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
query := fmt.Sprintf(`SELECT 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
|
||||||
|
FROM master.ms_patient
|
||||||
|
WHERE %s LIMIT $%d OFFSET $%d`, whereClause, len(args)+1, len(args)+2)
|
||||||
|
|
||||||
|
args = append(args, limit, offset)
|
||||||
|
|
||||||
|
rows, err := dbConn.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetch patients query failed: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]patient.Patient, 0, limit)
|
||||||
|
for rows.Next() {
|
||||||
|
item, err := h.scanPatient(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scan Patient failed: %w", err)
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Successfully fetched %d patients with filters applied", len(items))
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized scanning function
|
||||||
|
func (h *PatientHandler) scanPatient(rows *sql.Rows) (patient.Patient, error) {
|
||||||
|
var item patient.Patient
|
||||||
|
|
||||||
|
// Scan into individual fields to handle nullable types properly
|
||||||
|
err := rows.Scan(
|
||||||
|
&item.ID,
|
||||||
|
&item.Name,
|
||||||
|
&item.MedicalRecordNumber,
|
||||||
|
&item.PhoneNumber,
|
||||||
|
&item.Gender,
|
||||||
|
&item.BirthDate,
|
||||||
|
&item.Address,
|
||||||
|
&item.Active,
|
||||||
|
&item.FKSdProvinsiID,
|
||||||
|
&item.FKSdKabupatenKotaID,
|
||||||
|
&item.FKSdKecamatanID,
|
||||||
|
&item.FKSdKelurahanID,
|
||||||
|
&item.DsSdProvinsi,
|
||||||
|
&item.DsSdKabupatenKota,
|
||||||
|
&item.DsSdKecamatan,
|
||||||
|
&item.DsSdKelurahan,
|
||||||
|
)
|
||||||
|
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter patient.PatientFilter, total *int) error {
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM master.ms_patient WHERE %s", whereClause)
|
||||||
|
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
|
||||||
|
return fmt.Errorf("total count query failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get comprehensive aggregate data dengan filter support
|
||||||
|
func (h *PatientHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter patient.PatientFilter) (*models.AggregateData, error) {
|
||||||
|
aggregate := &models.AggregateData{
|
||||||
|
ByStatus: make(map[string]int),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build where clause untuk filter
|
||||||
|
whereClause, args := h.buildWhereClause(filter)
|
||||||
|
|
||||||
|
// Use concurrent execution untuk performance
|
||||||
|
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()
|
||||||
|
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM master.ms_patient WHERE %s GROUP BY status ORDER BY status", whereClause)
|
||||||
|
|
||||||
|
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- fmt.Errorf("status query failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
for rows.Next() {
|
||||||
|
var status string
|
||||||
|
var count int
|
||||||
|
if err := rows.Scan(&status, &count); err != nil {
|
||||||
|
mu.Unlock()
|
||||||
|
errChan <- fmt.Errorf("status scan failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
aggregate.ByStatus[status] = count
|
||||||
|
switch status {
|
||||||
|
case "active":
|
||||||
|
aggregate.TotalActive = count
|
||||||
|
case "draft":
|
||||||
|
aggregate.TotalDraft = count
|
||||||
|
case "inactive":
|
||||||
|
aggregate.TotalInactive = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
errChan <- fmt.Errorf("status iteration error: %w", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 2. Get last updated time dan today statistics
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// Last updated
|
||||||
|
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM master.ms_patient WHERE %s AND date_updated IS NOT NULL", whereClause)
|
||||||
|
var lastUpdated sql.NullTime
|
||||||
|
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
|
||||||
|
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Today statistics
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
todayStatsQuery := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today,
|
||||||
|
SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today
|
||||||
|
FROM master.ms_patient
|
||||||
|
WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause)
|
||||||
|
|
||||||
|
todayArgs := append(args, today)
|
||||||
|
var createdToday, updatedToday int
|
||||||
|
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
|
||||||
|
errChan <- fmt.Errorf("today stats query failed: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
if lastUpdated.Valid {
|
||||||
|
aggregate.LastUpdated = &lastUpdated.Time
|
||||||
|
}
|
||||||
|
aggregate.CreatedToday = createdToday
|
||||||
|
aggregate.UpdatedToday = updatedToday
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for all goroutines
|
||||||
|
wg.Wait()
|
||||||
|
close(errChan)
|
||||||
|
|
||||||
|
// Check for errors
|
||||||
|
for err := range errChan {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced error handling
|
||||||
|
func (h *PatientHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
||||||
|
log.Printf("[ERROR] %s: %v", message, err)
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pagination parameters dengan validation yang lebih ketat
|
||||||
|
func (h *PatientHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
|
||||||
|
limit := 10 // Default limit
|
||||||
|
offset := 0 // Default offset
|
||||||
|
|
||||||
|
if limitStr := c.Query("limit"); limitStr != "" {
|
||||||
|
parsedLimit, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
|
||||||
|
}
|
||||||
|
if parsedLimit <= 0 {
|
||||||
|
return 0, 0, fmt.Errorf("limit must be greater than 0")
|
||||||
|
}
|
||||||
|
if parsedLimit > 100 {
|
||||||
|
return 0, 0, fmt.Errorf("limit cannot exceed 100")
|
||||||
|
}
|
||||||
|
limit = parsedLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||||
|
parsedOffset, err := strconv.Atoi(offsetStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
|
||||||
|
}
|
||||||
|
if parsedOffset < 0 {
|
||||||
|
return 0, 0, fmt.Errorf("offset cannot be negative")
|
||||||
|
}
|
||||||
|
offset = parsedOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
|
||||||
|
return limit, offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) parseFilterParams(c *gin.Context) patient.PatientFilter {
|
||||||
|
filter := patient.PatientFilter{}
|
||||||
|
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
if models.IsValidStatus(status) {
|
||||||
|
filter.Status = &status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
filter.Search = &search
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse date filters
|
||||||
|
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
|
||||||
|
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
|
||||||
|
filter.DateFrom = &dateFrom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateToStr := c.Query("date_to"); dateToStr != "" {
|
||||||
|
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
|
||||||
|
filter.DateTo = &dateTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build WHERE clause dengan filter parameters
|
||||||
|
func (h *PatientHandler) buildWhereClause(filter patient.PatientFilter) (string, []interface{}) {
|
||||||
|
conditions := []string{"1=1"}
|
||||||
|
args := []interface{}{}
|
||||||
|
paramCount := 1
|
||||||
|
|
||||||
|
if filter.Status != nil {
|
||||||
|
conditions = append(conditions, fmt.Sprintf("active = $%d", paramCount))
|
||||||
|
args = append(args, *filter.Status)
|
||||||
|
paramCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Search != nil {
|
||||||
|
searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount)
|
||||||
|
conditions = append(conditions, searchCondition)
|
||||||
|
searchTerm := "%" + *filter.Search + "%"
|
||||||
|
args = append(args, searchTerm)
|
||||||
|
paramCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// if filter.DateFrom != nil {
|
||||||
|
// conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
|
||||||
|
// args = append(args, *filter.DateFrom)
|
||||||
|
// paramCount++
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if filter.DateTo != nil {
|
||||||
|
// conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
|
||||||
|
// args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
|
||||||
|
// paramCount++
|
||||||
|
// }
|
||||||
|
|
||||||
|
return strings.Join(conditions, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PatientHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
||||||
|
totalPages := 0
|
||||||
|
currentPage := 1
|
||||||
|
if limit > 0 {
|
||||||
|
totalPages = (total + limit - 1) / limit // Ceiling division
|
||||||
|
currentPage = (offset / limit) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.MetaResponse{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
Total: total,
|
||||||
|
TotalPages: totalPages,
|
||||||
|
CurrentPage: currentPage,
|
||||||
|
HasNext: offset+limit < total,
|
||||||
|
HasPrev: offset > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePatientSubmission performs validation for duplicate entries and daily submission limits
|
||||||
|
func (h *PatientHandler) validatePatientSubmission(ctx context.Context, dbConn *sql.DB, req *patient.PatientCreateRequest) error {
|
||||||
|
// Import the validation utility
|
||||||
|
validator := validation.NewDuplicateValidator(dbConn)
|
||||||
|
|
||||||
|
// Use default configuration
|
||||||
|
config := validation.ValidationConfig{
|
||||||
|
TableName: "master.ms_patient",
|
||||||
|
IDColumn: "id",
|
||||||
|
// StatusColumn: "active",
|
||||||
|
// DateColumn: "date_created",
|
||||||
|
ActiveStatuses: []string{"active"},
|
||||||
|
AdditionalFields: map[string]interface{}{
|
||||||
|
"name": req.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare fields for validation
|
||||||
|
fields := map[string]interface{}{
|
||||||
|
"name": req.Name,
|
||||||
|
"active": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate duplicate entries with active status for today
|
||||||
|
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate once per day submission
|
||||||
|
// err = validator.ValidateOncePerDay(ctx, "master.ms_patient", "id", "date_created", "daily_limit")
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("daily submission limit exceeded: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage of the validation utility with custom configuration
|
||||||
|
// func (h *PatientHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *patient.PatientCreateRequest) error {
|
||||||
|
// // Create validator instance
|
||||||
|
// validator := validation.NewDuplicateValidator(dbConn)
|
||||||
|
|
||||||
|
// // Use custom configuration
|
||||||
|
// config := validation.ValidationConfig{
|
||||||
|
// TableName: "master.ms_patient",
|
||||||
|
// IDColumn: "id",
|
||||||
|
// StatusColumn: "status",
|
||||||
|
// DateColumn: "date_created",
|
||||||
|
// ActiveStatuses: []string{"active", "draft"},
|
||||||
|
// AdditionalFields: map[string]interface{}{
|
||||||
|
// "name": req.Name,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Validate with custom fields
|
||||||
|
// fields := map[string]interface{}{
|
||||||
|
// "name": *req.Name,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
||||||
|
// if err != nil {
|
||||||
|
// return fmt.Errorf("custom validation failed: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// GetLastSubmissionTime example
|
||||||
|
func (h *PatientHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
|
||||||
|
validator := validation.NewDuplicateValidator(dbConn)
|
||||||
|
return validator.GetLastSubmissionTime(ctx, "master.ms_patient", "id", "date_created", identifier)
|
||||||
|
}
|
||||||
@@ -100,7 +100,7 @@ type ListkioskCreateRequest struct {
|
|||||||
FKRefHealthcareTypeID int32 `json:"fk_ref_healthcare_type_id" validate:"min=1"`
|
FKRefHealthcareTypeID int32 `json:"fk_ref_healthcare_type_id" validate:"min=1"`
|
||||||
FKRefServiceTypeID int32 `json:"fk_ref_service_type_id" validate:"min=1"`
|
FKRefServiceTypeID int32 `json:"fk_ref_service_type_id" validate:"min=1"`
|
||||||
FKSdLocationID string `json:"fk_sd_location_id"`
|
FKSdLocationID string `json:"fk_sd_location_id"`
|
||||||
DsSdLocation string `json:"ds_sd_location" validate:"min=1,max=255"`
|
DsSdLocation string `json:"ds_sd_location"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response struct untuk create
|
// Response struct untuk create
|
||||||
@@ -118,8 +118,8 @@ type ListkioskUpdateRequest struct {
|
|||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
FKRefHealthcareTypeID int32 `json:"fk_ref_healthcare_type_id" validate:"min=1"`
|
FKRefHealthcareTypeID int32 `json:"fk_ref_healthcare_type_id" validate:"min=1"`
|
||||||
FKRefServiceTypeID int32 `json:"fk_ref_service_type_id" validate:"min=1"`
|
FKRefServiceTypeID int32 `json:"fk_ref_service_type_id" validate:"min=1"`
|
||||||
FKSdLocationID string `json:"fk_sd_location_id" validate:"min=1"`
|
FKSdLocationID string `json:"fk_sd_location_id"`
|
||||||
DsSdLocation string `json:"ds_sd_location" validate:"min=1,max=255"`
|
DsSdLocation string `json:"ds_sd_location" validate:"max=255"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response struct untuk update
|
// Response struct untuk update
|
||||||
|
|||||||
197
internal/models/patient/patient.go
Normal file
197
internal/models/patient/patient.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package patient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"api-service/internal/models"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 *time.Time `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 {
|
||||||
|
aux.BirthDate = &r.BirthDate.Time
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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:"min=1,max=20"`
|
||||||
|
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:"min=1,max=20"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
authHandlers "api-service/internal/handlers/auth"
|
authHandlers "api-service/internal/handlers/auth"
|
||||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||||
kioskListkioskHandlers "api-service/internal/handlers/kiosk"
|
kioskListkioskHandlers "api-service/internal/handlers/kiosk"
|
||||||
|
patientPatientHandlers "api-service/internal/handlers/patient"
|
||||||
pesertaHandlers "api-service/internal/handlers/peserta"
|
pesertaHandlers "api-service/internal/handlers/peserta"
|
||||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||||
"api-service/internal/handlers/websocket"
|
"api-service/internal/handlers/websocket"
|
||||||
@@ -795,5 +796,17 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
kioskListkioskGroup.GET("/stats", kioskListkioskHandler.GetKioskStats)
|
kioskListkioskGroup.GET("/stats", kioskListkioskHandler.GetKioskStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patient endpoints
|
||||||
|
patientPatientHandler := patientPatientHandlers.NewPatientHandler()
|
||||||
|
patientPatientGroup := v1.Group("/patient")
|
||||||
|
{
|
||||||
|
patientPatientGroup.GET("/listpatient", patientPatientHandler.GetPatient)
|
||||||
|
patientPatientGroup.POST("/:id", patientPatientHandler.GetPatientByIDPost)
|
||||||
|
patientPatientGroup.POST("", patientPatientHandler.CreatePatient)
|
||||||
|
patientPatientGroup.PUT("/:id", patientPatientHandler.UpdatePatient)
|
||||||
|
patientPatientGroup.DELETE("/:id", patientPatientHandler.DeletePatient)
|
||||||
|
patientPatientGroup.GET("/stats", patientPatientHandler.GetPatientStats)
|
||||||
|
}
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user