diff --git a/internal/handlers/registrasi/registrasih.go b/internal/handlers/registrasi/registrasih.go index 28ae6f55..cc493bd9 100644 --- a/internal/handlers/registrasi/registrasih.go +++ b/internal/handlers/registrasi/registrasih.go @@ -1 +1,849 @@ -package handlers \ No newline at end of file +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 +} \ No newline at end of file diff --git a/internal/models/references/references.go b/internal/models/references/references.go new file mode 100644 index 00000000..6aae0788 --- /dev/null +++ b/internal/models/references/references.go @@ -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 +} \ No newline at end of file diff --git a/internal/models/registrasi/registrasi.go b/internal/models/registrasi/registrasi.go index aadd24e3..0f069263 100644 --- a/internal/models/registrasi/registrasi.go +++ b/internal/models/registrasi/registrasi.go @@ -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 { diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 019ff9ca..51e317ba 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -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 }