bikin fitur crud registrasi counter
Some checks failed
Go-test / build (push) Has been cancelled

This commit is contained in:
2025-12-01 10:43:56 +07:00
parent 61af0740ee
commit 6a985c837f
4 changed files with 1021 additions and 2 deletions

View File

@@ -1 +1,849 @@
package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models"
registrasi "api-service/internal/models/registrasi"
"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 (
registrationCounterDB database.Service
registrationCounterOnce sync.Once
registrationCounterValidate *validator.Validate
)
// Initialize the database connection and validator
func init() {
registrationCounterOnce.Do(func() {
registrationCounterDB = database.New(config.LoadConfig())
registrationCounterValidate = validator.New()
// Jika ada validasi khusus untuk status, daftarkan di sini
// registrationCounterValidate.RegisterValidation("registration_counter_status", validateRegistrationCounterStatus)
if registrationCounterDB == nil {
log.Fatal("Failed to initialize database connection for registration counter")
}
})
}
// RegistrationCounterHandler handles registration counter services
type RegistrationCounterHandler struct {
db database.Service
}
// NewRegistrationCounterHandler creates a new RegistrationCounterHandler
func NewRegistrationCounterHandler() *RegistrationCounterHandler {
return &RegistrationCounterHandler{
db: registrationCounterDB,
}
}
// GetRegistrationCounters godoc
// @Summary Get registration counters with pagination and optional aggregation
// @Description Returns a paginated list of registration counters with optional summary statistics
// @Tags RegistrationCounter
// @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 active query bool false "Filter by active status"
// @Param search query string false "Search in name or code"
// @Success 200 {object} registrasi.MsRegistrationCounterGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /registration-counter [get]
func (h *RegistrationCounterHandler) GetRegistrationCounters(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("default")
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 []registrasi.MsRegistrationCounter
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.fetchRegistrationCounters(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 := registrasi.MsRegistrationCounterGetResponse{
Message: "Data registration counter berhasil diambil",
Data: items,
Meta: meta,
}
if includeAggregation && aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// GetRegistrationCounterByID godoc
// @Summary Get Registration Counter by ID
// @Description Returns a single registration counter by its ID
// @Tags RegistrationCounter
// @Accept json
// @Produce json
// @Param id path int true "Registration Counter ID"
// @Success 200 {object} registrasi.MsRegistrationCounterGetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Registration Counter not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /registration-counter/{id} [get]
func (h *RegistrationCounterHandler) GetRegistrationCounterByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("default")
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.getRegistrationCounterByID(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Registration Counter not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get registration counter", err, http.StatusInternalServerError)
}
return
}
response := registrasi.MsRegistrationCounterGetByIDResponse{
Message: "Detail registration counter berhasil diambil",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// CreateRegistrationCounter godoc
// @Summary Create a new registration counter
// @Description Creates a new registration counter record
// @Tags RegistrationCounter
// @Accept json
// @Produce json
// @Param request body registrasi.MsRegistrationCounterCreateRequest true "Registration Counter creation request"
// @Success 201 {object} registrasi.MsRegistrationCounterCreateResponse "Registration Counter created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /registration-counter [post]
func (h *RegistrationCounterHandler) CreateRegistrationCounter(c *gin.Context) {
var req registrasi.MsRegistrationCounterCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Validate request
if err := registrationCounterValidate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("default")
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 entries
if err := h.validateRegistrationCounterSubmission(ctx, dbConn, &req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
item, err := h.createRegistrationCounter(ctx, dbConn, &req)
if err != nil {
h.logAndRespondError(c, "Failed to create registration counter", err, http.StatusInternalServerError)
return
}
response := registrasi.MsRegistrationCounterCreateResponse{
Message: "Registration counter berhasil dibuat",
Data: item,
}
c.JSON(http.StatusCreated, response)
}
// UpdateRegistrationCounter godoc
// @Summary Update an existing registration counter
// @Description Updates an existing registration counter record
// @Tags RegistrationCounter
// @Accept json
// @Produce json
// @Param id path int true "Registration Counter ID"
// @Param request body registrasi.MsRegistrationCounterUpdateRequest true "Registration Counter update request"
// @Success 200 {object} registrasi.MsRegistrationCounterUpdateResponse "Registration Counter updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "Registration Counter not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /registration-counter/{id} [put]
func (h *RegistrationCounterHandler) UpdateRegistrationCounter(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
}
var req registrasi.MsRegistrationCounterUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Set ID from path parameter
idInt, _ := strconv.Atoi(id)
req.ID = idInt
// Validate request
if err := registrationCounterValidate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("default")
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.updateRegistrationCounter(ctx, dbConn, &req)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Registration Counter not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update registration counter", err, http.StatusInternalServerError)
}
return
}
response := registrasi.MsRegistrationCounterUpdateResponse{
Message: "Registration counter berhasil diperbarui",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// DeleteRegistrationCounter godoc
// @Summary Soft delete a registration counter
// @Description Soft deletes a registration counter by setting active to false
// @Tags RegistrationCounter
// @Accept json
// @Produce json
// @Param id path int true "Registration Counter ID"
// @Success 200 {object} registrasi.MsRegistrationCounterDeleteResponse "Registration Counter deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Registration Counter not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /registration-counter/{id} [delete]
func (h *RegistrationCounterHandler) DeleteRegistrationCounter(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("default")
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.deleteRegistrationCounter(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Registration Counter not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to delete registration counter", err, http.StatusInternalServerError)
}
return
}
response := registrasi.MsRegistrationCounterDeleteResponse{
Message: "Registration counter berhasil dihapus (diset tidak aktif)",
ID: idStr,
}
c.JSON(http.StatusOK, response)
}
// GetRegistrationCounterStats godoc
// @Summary Get registration counter statistics
// @Description Returns comprehensive statistics about registration counter data
// @Tags RegistrationCounter
// @Accept json
// @Produce json
// @Param active query bool false "Filter statistics by active status"
// @Success 200 {object} models.AggregateData "Statistics data"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /registration-counter/stats [get]
func (h *RegistrationCounterHandler) GetRegistrationCounterStats(c *gin.Context) {
dbConn, err := h.db.GetDB("default")
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 registration counter berhasil diambil",
"data": aggregateData,
})
}
// Database operations
// func (h *RegistrationCounterHandler) getRegistrationCounterByID(ctx context.Context, dbConn *sql.DB, id int64) (*registrasi.MsRegistrationCounter, error) {
// query := `SELECT id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location
// FROM master.ms_registration_counter WHERE id = $1`
// row := dbConn.QueryRowContext(ctx, query, id)
// var item registrasi.MsRegistrationCounter
// err := row.Scan(
// &item.ID,
// &item.Name,
// &item.Code,
// &item.Icon,
// &item.Quota,
// &item.Active,
// &item.FKRefHealtcareTypeID,
// &item.FKRefServiceTypeID,
// &item.FKSdLocationID,
// &item.DsSdLocation,
// )
// if err != nil {
// return nil, err
// }
// return &item, nil
// }
func (h *RegistrationCounterHandler) getRegistrationCounterByID(ctx context.Context, dbConn *sql.DB, id int64) (*registrasi.MsRegistrationCounterDetail, error) {
query := `
SELECT rc.id, rc.name, rc.code, rc.icon, rc.quota, rc.active,
ht.name as healthcaretype,
st.name as servicetype
FROM master.ms_registration_counter rc
LEFT JOIN reference.ref_healthcare_type ht ON rc.fk_ref_healtcare_type_id = ht.id
LEFT JOIN reference.ref_service_type st ON rc.fk_ref_service_type_id = st.id
WHERE rc.id = $1`
row := dbConn.QueryRowContext(ctx, query, id)
var item registrasi.MsRegistrationCounterDetail
err := row.Scan(
&item.ID,
&item.Name,
&item.Code,
&item.Icon,
&item.Quota,
&item.Active,
&item.HealthCareType,
&item.ServiceType,
)
if err != nil {
return nil, err
}
return &item, nil
}
func (h *RegistrationCounterHandler) createRegistrationCounter(ctx context.Context, dbConn *sql.DB, req *registrasi.MsRegistrationCounterCreateRequest) (*registrasi.MsRegistrationCounter, error) {
query := `INSERT INTO master.ms_registration_counter
(name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
row := dbConn.QueryRowContext(
ctx,
query,
req.Name,
req.Code,
req.Icon,
req.Quota,
req.Active,
req.FKRefHealtcareTypeID,
req.FKRefServiceTypeID,
req.FKSdLocationID,
req.DsSdLocation,
)
var item registrasi.MsRegistrationCounter
err := row.Scan(
&item.ID,
&item.Name,
&item.Code,
&item.Icon,
&item.Quota,
&item.Active,
&item.FKRefHealtcareTypeID,
&item.FKRefServiceTypeID,
&item.FKSdLocationID,
&item.DsSdLocation,
)
if err != nil {
return nil, fmt.Errorf("failed to create registration counter: %w", err)
}
return &item, nil
}
func (h *RegistrationCounterHandler) updateRegistrationCounter(ctx context.Context, dbConn *sql.DB, req *registrasi.MsRegistrationCounterUpdateRequest) (*registrasi.MsRegistrationCounter, error) {
query := `UPDATE master.ms_registration_counter
SET name = $2, code = $3, icon = $4, quota = $5, active = $6, fk_ref_healtcare_type_id = $7, fk_ref_service_type_id = $8, fk_sd_location_id = $9, ds_sd_location = $10
WHERE id = $1
RETURNING id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
row := dbConn.QueryRowContext(
ctx,
query,
req.ID,
req.Name,
req.Code,
req.Icon,
req.Quota,
req.Active,
req.FKRefHealtcareTypeID,
req.FKRefServiceTypeID,
req.FKSdLocationID,
req.DsSdLocation,
)
var item registrasi.MsRegistrationCounter
err := row.Scan(
&item.ID,
&item.Name,
&item.Code,
&item.Icon,
&item.Quota,
&item.Active,
&item.FKRefHealtcareTypeID,
&item.FKRefServiceTypeID,
&item.FKSdLocationID,
&item.DsSdLocation,
)
if err != nil {
return nil, fmt.Errorf("failed to update registration counter: %w", err)
}
return &item, nil
}
func (h *RegistrationCounterHandler) deleteRegistrationCounter(ctx context.Context, dbConn *sql.DB, id int64) error {
query := `UPDATE master.ms_registration_counter SET active = false WHERE id = $1`
result, err := dbConn.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete registration counter: %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 *RegistrationCounterHandler) fetchRegistrationCounters(ctx context.Context, dbConn *sql.DB, filter registrasi.MsRegistrationCounterFilter, limit, offset int) ([]registrasi.MsRegistrationCounter, error) {
whereClause, args := h.buildWhereClause(filter)
query := fmt.Sprintf(`SELECT id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location
FROM master.ms_registration_counter
WHERE %s ORDER BY id 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 registration counters query failed: %w", err)
}
defer rows.Close()
items := make([]registrasi.MsRegistrationCounter, 0, limit)
for rows.Next() {
var item registrasi.MsRegistrationCounter
err := rows.Scan(
&item.ID,
&item.Name,
&item.Code,
&item.Icon,
&item.Quota,
&item.Active,
&item.FKRefHealtcareTypeID,
&item.FKRefServiceTypeID,
&item.FKSdLocationID,
&item.DsSdLocation,
)
if err != nil {
return nil, fmt.Errorf("scan registration counter 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 registration counters with filters applied", len(items))
return items, nil
}
func (h *RegistrationCounterHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter registrasi.MsRegistrationCounterFilter, total *int) error {
whereClause, args := h.buildWhereClause(filter)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM master.ms_registration_counter 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
}
func (h *RegistrationCounterHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter registrasi.MsRegistrationCounterFilter) (*models.AggregateData, error) {
aggregate := &models.AggregateData{
ByStatus: make(map[string]int),
}
whereClause, args := h.buildWhereClause(filter)
var wg sync.WaitGroup
var mu sync.Mutex
errChan := make(chan error, 2)
// 1. Count by active status
wg.Add(1)
go func() {
defer wg.Done()
statusQuery := fmt.Sprintf("SELECT active, COUNT(*) FROM master.ms_registration_counter WHERE %s GROUP BY active ORDER BY active", 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 active bool
var count int
if err := rows.Scan(&active, &count); err != nil {
mu.Unlock()
errChan <- fmt.Errorf("status scan failed: %w", err)
return
}
if active {
aggregate.ByStatus["active"] = count
aggregate.TotalActive = count
} else {
aggregate.ByStatus["inactive"] = count
aggregate.TotalInactive = count
}
}
mu.Unlock()
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("status iteration error: %w", err)
}
}()
// 2. Get total count
wg.Add(1)
go func() {
defer wg.Done()
// Last updated
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM master.ms_registration_counter 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_registration_counter
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
}
// Helper methods
func (h *RegistrationCounterHandler) 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 *RegistrationCounterHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{
Error: errorMessage,
Code: statusCode,
Message: err.Error(),
Timestamp: time.Now(),
})
}
func (h *RegistrationCounterHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
limit := 10
offset := 0
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 || parsedLimit > 100 {
return 0, 0, fmt.Errorf("limit must be between 1 and 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
}
return limit, offset, nil
}
func (h *RegistrationCounterHandler) parseFilterParams(c *gin.Context) registrasi.MsRegistrationCounterFilter {
filter := registrasi.MsRegistrationCounterFilter{}
if status := c.Query("active"); status != "" {
if models.IsValidStatus(status) {
filter.Status = &status
}
}
if search := c.Query("search"); search != "" {
filter.Search = &search
}
return filter
}
func (h *RegistrationCounterHandler) buildWhereClause(filter registrasi.MsRegistrationCounterFilter) (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 OR code ILIKE $%d)", paramCount, paramCount)
conditions = append(conditions, searchCondition)
searchTerm := "%" + *filter.Search + "%"
args = append(args, searchTerm)
paramCount++
}
return strings.Join(conditions, " AND "), args
}
func (h *RegistrationCounterHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
totalPages := 0
currentPage := 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 *RegistrationCounterHandler) validateRegistrationCounterSubmission(ctx context.Context, dbConn *sql.DB, req *registrasi.MsRegistrationCounterCreateRequest) error {
validator := validation.NewDuplicateValidator(dbConn)
config := validation.ValidationConfig{
TableName: "master.ms_registration_counter",
IDColumn: "id",
AdditionalFields: map[string]interface{}{
"name": req.Name,
"code": req.Code,
},
}
fields := map[string]interface{}{
"name": req.Name,
"code": req.Code,
}
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
return nil
}

View File

@@ -0,0 +1,95 @@
package models
import (
"api-service/internal/models"
"database/sql"
"encoding/json"
)
type RefHealthcareType struct {
ID int32 `json:"id" db:"id"`
Name sql.NullString `json:"name,omitempty" db:"name"`
Active sql.NullBool `json:"active,omitempty" db:"active"`
}
func (r RefHealthcareType) MarshalJSON() ([]byte, error) {
// Buat alias untuk menghindari rekursi tak terbatas saat pemanggilan json.Marshal
type Alias RefHealthcareType
aux := &struct {
Name *string `json:"name,omitempty"`
Active *bool `json:"active,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
// Jika field Name valid, ambil nilainya
if r.Name.Valid {
aux.Name = &r.Name.String
}
// Jika field Active valid, ambil nilainya
if r.Active.Valid {
aux.Active = &r.Active.Bool
}
return json.Marshal(aux)
}
type RefHealthcareTypeGetResponse struct {
Message string `json:"message"`
Data *RefHealthcareType `json:"data"`
}
type RefHealthcareTypeGetListResponse struct {
Message string `json:"message"`
Data []RefHealthcareType `json:"data"`
Meta models.MetaResponse `json:"meta"`
}
type RefServiceType struct {
ID int32 `json:"id" db:"id"`
Name sql.NullString `json:"name,omitempty" db:"name"`
Active sql.NullBool `json:"active,omitempty" db:"active"`
}
func (r RefServiceType) MarshalJSON() ([]byte, error) {
type Alias RefServiceType
aux := &struct {
Name *string `json:"name,omitempty"`
Active *bool `json:"active,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
if r.Name.Valid {
aux.Name = &r.Name.String
}
if r.Active.Valid {
aux.Active = &r.Active.Bool
}
return json.Marshal(aux)
}
type RefServiceTypeGetResponse struct {
Message string `json:"message"`
Data *RefServiceType `json:"data"`
}
type RefServiceTypeGetListResponse struct {
Message string `json:"message"`
Data []RefServiceType `json:"data"`
Meta models.MetaResponse `json:"meta"`
}
type RefServiceTypeFilter struct {
Active *bool `json:"active,omitempty" form:"active"` // Digunakan untuk query parameter ?active=true
Search *string `json:"search,omitempty" form:"search"` // Digunakan untuk query parameter ?search=lab
}

View File

@@ -21,6 +21,63 @@ type MsRegistrationCounter struct {
DsSdLocation sql.NullString `json:"ds_sd_location,omitempty" db:"ds_sd_location"`
}
type MsRegistrationCounterDetail struct {
ID int64 `json:"id" db:"id"`
Name sql.NullString `json:"name,omitempty" db:"name"`
Code sql.NullString `json:"code,omitempty" db:"code"`
Icon sql.NullString `json:"icon,omitempty" db:"icon"`
Quota sql.NullInt16 `json:"quota,omitempty" db:"quota"`
Active sql.NullBool `json:"active,omitempty" db:"active"`
HealthCareType sql.NullString `json:"healthcaretype,omitempty" db:"healthcaretype"`
ServiceType sql.NullString `json:"servicetype,omitempty" db:"fk_ref_service_type_id"`
}
func (m MsRegistrationCounterDetail) MarshalJSON() ([]byte, error) {
// Buat alias untuk menghindari rekursi tak terbatas saat pemanggilan json.Marshal
type Alias MsRegistrationCounterDetail
// Buat struct anonim dengan field pointer untuk menangani nilai NULL
aux := &struct {
Name *string `json:"name,omitempty"`
Code *string `json:"code,omitempty"`
Icon *string `json:"icon,omitempty"`
Quota *int16 `json:"quota,omitempty"`
Active *bool `json:"active,omitempty"`
HealthCareType *string `json:"healthcaretype,omitempty"`
ServiceType *string `json:"servicetype,omitempty"`
// Embed alias untuk menyertakan field non-nullable seperti 'id'
*Alias
}{
Alias: (*Alias)(&m),
}
// Jika field asli valid, isi pointer di struct anonim
if m.Name.Valid {
aux.Name = &m.Name.String
}
if m.Code.Valid {
aux.Code = &m.Code.String
}
if m.Icon.Valid {
aux.Icon = &m.Icon.String
}
if m.Quota.Valid {
aux.Quota = &m.Quota.Int16
}
if m.Active.Valid {
aux.Active = &m.Active.Bool
}
if m.HealthCareType.Valid {
aux.HealthCareType = &m.HealthCareType.String
}
if m.ServiceType.Valid {
aux.ServiceType = &m.ServiceType.String
}
// Marshal struct anonim yang sudah "bersih"
return json.Marshal(aux)
}
func (r MsRegistrationCounter) MarshalJSON() ([]byte, error) {
type Alias MsRegistrationCounter
@@ -72,6 +129,7 @@ func (r MsRegistrationCounter) MarshalJSON() ([]byte, error) {
return json.Marshal(aux)
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *MsRegistrationCounter) GetName() string {
if r.Name.Valid {
@@ -108,9 +166,14 @@ func (r *MsRegistrationCounter) GetActive() bool {
return false
}
// type MsRegistrationCounterGetByIDResponse struct {
// Message string `json:"message"`
// Data *MsRegistrationCounter `json:"data"`
// }
type MsRegistrationCounterGetByIDResponse struct {
Message string `json:"message"`
Data *MsRegistrationCounter `json:"data"`
Data *MsRegistrationCounterDetail `json:"data"`
}
type MsRegistrationCounterGetResponse struct {

View File

@@ -7,6 +7,7 @@ import (
healthcheckHandlers "api-service/internal/handlers/healthcheck"
kioskListkioskHandlers "api-service/internal/handlers/kiosk"
pesertaHandlers "api-service/internal/handlers/peserta"
registerCounterkHandlers "api-service/internal/handlers/registrasi"
retribusiHandlers "api-service/internal/handlers/retribusi"
"api-service/internal/handlers/websocket"
websocketHandlers "api-service/internal/handlers/websocket"
@@ -795,5 +796,17 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
kioskListkioskGroup.GET("/stats", kioskListkioskHandler.GetKioskStats)
}
registerCounterHandler := registerCounterkHandlers.NewRegistrationCounterHandler()
regitsterCounterGroup := v1.Group("/registercounter")
{
regitsterCounterGroup.GET("/list", registerCounterHandler.GetRegistrationCounters)
regitsterCounterGroup.GET("/:id", registerCounterHandler.GetRegistrationCounterByID)
regitsterCounterGroup.POST("", registerCounterHandler.CreateRegistrationCounter)
regitsterCounterGroup.PUT("/:id", registerCounterHandler.UpdateRegistrationCounter)
regitsterCounterGroup.DELETE("/:id", registerCounterHandler.DeleteRegistrationCounter)
regitsterCounterGroup.GET("/stats", registerCounterHandler.GetRegistrationCounterStats)
}
return router
}