package handlers import ( "api-service/internal/config" "api-service/internal/database" models "api-service/internal/models" referenceModels "api-service/internal/models/reference" queryUtils "api-service/internal/utils/query" "api-service/pkg/logger" "context" "database/sql" "fmt" "net/http" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/jmoiron/sqlx" ) var ( referencedb database.Service referenceonce sync.Once referencevalidate *validator.Validate ) // Initialize the database connection and validator once func init() { referenceonce.Do(func() { referencedb = database.New(config.LoadConfig()) referencevalidate = validator.New() referencevalidate.RegisterValidation("reference_status", validateReferenceStatus) if referencedb == nil { logger.Fatal("Failed to initialize database connection") } }) } // Custom validation for reference status func validateReferenceStatus(fl validator.FieldLevel) bool { return models.IsValidStatus(fl.Field().String()) } // ============================================================================= // CACHE IMPLEMENTATION // ============================================================================= // CacheEntry represents an entry in the cache type CacheEntry struct { Data interface{} ExpiresAt time.Time } // IsExpired checks if the cache entry has expired func (e *CacheEntry) IsExpired() bool { return time.Now().After(e.ExpiresAt) } // InMemoryCache implements a simple in-memory cache with TTL type InMemoryCache struct { items sync.Map } // NewInMemoryCache creates a new in-memory cache func NewInMemoryCache() *InMemoryCache { return &InMemoryCache{} } // Get retrieves an item from the cache func (c *InMemoryCache) Get(key string) (interface{}, bool) { val, ok := c.items.Load(key) if !ok { return nil, false } entry, ok := val.(*CacheEntry) if !ok || entry.IsExpired() { c.items.Delete(key) return nil, false } return entry.Data, true } // Set stores an item in the cache with a TTL func (c *InMemoryCache) Set(key string, value interface{}, ttl time.Duration) { c.items.Store(key, &CacheEntry{ Data: value, ExpiresAt: time.Now().Add(ttl), }) } // Delete removes an item from the cache func (c *InMemoryCache) Delete(key string) { c.items.Delete(key) } // DeleteByPrefix removes all items with a specific prefix func (c *InMemoryCache) DeleteByPrefix(prefix string) { c.items.Range(func(key, value interface{}) bool { if keyStr, ok := key.(string); ok && strings.HasPrefix(keyStr, prefix) { c.items.Delete(key) } return true }) } // ============================================================================= // REFERENCE HANDLER STRUCT // ============================================================================= // ReferenceHandler handles reference services type ReferenceHandler struct { db database.Service queryBuilder *queryUtils.QueryBuilder cache *InMemoryCache } // ============================================================================= // HANDLER ENDPOINTS (READ-ONLY) // ============================================================================= // GetRefServiceType godoc // @Summary Get RefServiceType List // @Description Get list of RefServiceType with pagination and filters, including their attachments and payment types. // @Tags Reference // @Accept json // @Produce json // @Param limit query int false "Limit (max 100)" default(10) // @Param offset query int false "Offset" default(0) // @Param active query string false "Filter by active status (true/false)" // @Param search query string false "Search in medical_record_number, name, or phone_number" // @Param include_summary query bool false "Include aggregation summary" default(false) // @Success 200 {object} referenceModels.RefServiceTypeGetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /reference [get] func (h *ReferenceHandler) GetRefServiceType(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() // Build base query with LEFT JOINs to fetch related data in a single trip query := queryUtils.DynamicQuery{ From: "reference.ref_service_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "rst.id", Alias: "id"}, {Expression: "rst.name", Alias: "name"}, {Expression: "rst.active", Alias: "medical_record_number"}, }, Sort: []queryUtils.SortField{ {Column: "rst.name", Order: "ASC"}, }, } // Parse pagination h.parsePagination(c, &query) // Get database connection dbConn, err := h.db.GetSQLXDB("db_antrean") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } // Parse filters var filters []queryUtils.DynamicFilter if active := c.Query("active"); active != "" { if b, err := strconv.ParseBool(active); err == nil { filters = append(filters, queryUtils.DynamicFilter{Column: "rst.active", Operator: queryUtils.OpEqual, Value: b}) } else { h.respondError(c, "Invalid 'active' value; must be true or false", fmt.Errorf("invalid active value: %s", active), http.StatusBadRequest) return } } // Handle search with caching search := c.Query("search") var searchFilters []queryUtils.DynamicFilter var cacheKey string var useCache bool if search != "" { if len(search) > 50 { search = search[:50] } cacheKey = fmt.Sprintf("ref_service_type:search:%s:%d:%d", search, query.Limit, query.Offset) searchFilters = []queryUtils.DynamicFilter{ {Column: "rst.id", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, {Column: "rst.name", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, } if cachedData, found := h.cache.Get(cacheKey); found { logger.Info("Cache hit for search", map[string]interface{}{"search": search}) if patients, ok := cachedData.([]referenceModels.RefServiceType); ok { var aggregateData *models.AggregateData if c.Query("include_summary") == "true" { fullFilterGroups := []queryUtils.FilterGroup{ {Filters: searchFilters, LogicOp: "OR"}, } if len(filters) > 0 { fullFilterGroups = append(fullFilterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) } aggregateData, err = h.getAggregateDataRST(ctx, dbConn, fullFilterGroups) if err != nil { h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) return } } meta := h.calculateMeta(query.Limit, query.Offset, len(patients)) response := referenceModels.RefServiceTypeGetResponse{ Message: "Data patient berhasil diambil (dari cache)", Data: patients, Meta: meta, } if aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) return } } useCache = true query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"}) } if len(filters) > 0 { query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) } // Execute query var results []map[string]interface{} err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) if err != nil { h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) return } // Process results into structured format patients := h.processQueryResultsRST(results) // Get total count for pagination metadata total, err := h.getTotalCount(ctx, dbConn, query) if err != nil { h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) return } // Cache results if this was a search query if useCache && len(patients) > 0 { h.cache.Set(cacheKey, patients, 15*time.Minute) logger.Info("Cached search results", map[string]interface{}{"search": search, "count": len(patients)}) } // Get aggregate data if requested var aggregateData *models.AggregateData if c.Query("include_summary") == "true" { aggregateData, err = h.getAggregateDataRST(ctx, dbConn, query.Filters) if err != nil { h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) return } } // Build and send response meta := h.calculateMeta(query.Limit, query.Offset, total) response := referenceModels.RefServiceTypeGetResponse{ Message: "Data patient berhasil diambil", Data: patients, Meta: meta, } if aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) } // GetRefPaymentType godoc // @Summary Get RefPaymentType List // @Description Get list of RefPaymentType with pagination and filters, including their attachments and payment types. // @Tags Reference // @Accept json // @Produce json // @Param limit query int false "Limit (max 100)" default(10) // @Param offset query int false "Offset" default(0) // @Param active query string false "Filter by active status (true/false)" // @Param search query string false "Search in medical_record_number, name, or phone_number" // @Param include_summary query bool false "Include aggregation summary" default(false) // @Success 200 {object} referenceModels.RefPaymentTypeGetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /reference [get] func (h *ReferenceHandler) GetRefPaymentType(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() // Build base query with LEFT JOINs to fetch related data in a single trip query := queryUtils.DynamicQuery{ From: "reference.ref_payment_type", Aliases: "rpt", Fields: []queryUtils.SelectField{ {Expression: "rpt.id", Alias: "id"}, {Expression: "rpt.name", Alias: "name"}, {Expression: "rpt.active", Alias: "medical_record_number"}, }, Sort: []queryUtils.SortField{ {Column: "rpt.name", Order: "ASC"}, }, } // Parse pagination h.parsePagination(c, &query) // Get database connection dbConn, err := h.db.GetSQLXDB("db_antrean") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } // Parse filters var filters []queryUtils.DynamicFilter if active := c.Query("active"); active != "" { if b, err := strconv.ParseBool(active); err == nil { filters = append(filters, queryUtils.DynamicFilter{Column: "rpt.active", Operator: queryUtils.OpEqual, Value: b}) } else { h.respondError(c, "Invalid 'active' value; must be true or false", fmt.Errorf("invalid active value: %s", active), http.StatusBadRequest) return } } // Handle search with caching search := c.Query("search") var searchFilters []queryUtils.DynamicFilter var cacheKey string var useCache bool if search != "" { if len(search) > 50 { search = search[:50] } cacheKey = fmt.Sprintf("ref_payment_type:search:%s:%d:%d", search, query.Limit, query.Offset) searchFilters = []queryUtils.DynamicFilter{ {Column: "rpt.id", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, {Column: "rpt.name", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, } if cachedData, found := h.cache.Get(cacheKey); found { logger.Info("Cache hit for search", map[string]interface{}{"search": search}) if patients, ok := cachedData.([]referenceModels.RefPaymentType); ok { var aggregateData *models.AggregateData if c.Query("include_summary") == "true" { fullFilterGroups := []queryUtils.FilterGroup{ {Filters: searchFilters, LogicOp: "OR"}, } if len(filters) > 0 { fullFilterGroups = append(fullFilterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) } aggregateData, err = h.getAggregateDataRPT(ctx, dbConn, fullFilterGroups) if err != nil { h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) return } } meta := h.calculateMeta(query.Limit, query.Offset, len(patients)) response := referenceModels.RefPaymentTypeGetResponse{ Message: "Data patient berhasil diambil (dari cache)", Data: patients, Meta: meta, } if aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) return } } useCache = true query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"}) } if len(filters) > 0 { query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) } // Execute query var results []map[string]interface{} err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) if err != nil { h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) return } // Process results into structured format patients := h.processQueryResultsRST(results) // Get total count for pagination metadata total, err := h.getTotalCount(ctx, dbConn, query) if err != nil { h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) return } // Cache results if this was a search query if useCache && len(patients) > 0 { h.cache.Set(cacheKey, patients, 15*time.Minute) logger.Info("Cached search results", map[string]interface{}{"search": search, "count": len(patients)}) } // Get aggregate data if requested var aggregateData *models.AggregateData if c.Query("include_summary") == "true" { aggregateData, err = h.getAggregateDataRST(ctx, dbConn, query.Filters) if err != nil { h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) return } } // Build and send response meta := h.calculateMeta(query.Limit, query.Offset, total) response := referenceModels.RefServiceTypeGetResponse{ Message: "Data patient berhasil diambil", Data: patients, Meta: meta, } if aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) } // GetRefVisitType godoc // @Summary Get RefVisitType List // @Description Get list of RefVisitType with pagination and filters, including their attachments and payment types. // @Tags Reference // @Accept json // @Produce json // @Param limit query int false "Limit (max 100)" default(10) // @Param offset query int false "Offset" default(0) // @Param active query string false "Filter by active status (true/false)" // @Param search query string false "Search in medical_record_number, name, or phone_number" // @Param include_summary query bool false "Include aggregation summary" default(false) // @Success 200 {object} referenceModels.RefVisitTypeGetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /reference [get] func (h *ReferenceHandler) GetRefVisitType(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() // Build base query with LEFT JOINs to fetch related data in a single trip query := queryUtils.DynamicQuery{ From: "reference.ref_visit_type", Aliases: "rvt", Fields: []queryUtils.SelectField{ {Expression: "rvt.id", Alias: "id"}, {Expression: "rvt.name", Alias: "name"}, {Expression: "rvt.active", Alias: "medical_record_number"}, }, Sort: []queryUtils.SortField{ {Column: "rvt.name", Order: "ASC"}, }, } // Parse pagination h.parsePagination(c, &query) // Get database connection dbConn, err := h.db.GetSQLXDB("db_antrean") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } // Parse filters var filters []queryUtils.DynamicFilter if active := c.Query("active"); active != "" { if b, err := strconv.ParseBool(active); err == nil { filters = append(filters, queryUtils.DynamicFilter{Column: "rvt.active", Operator: queryUtils.OpEqual, Value: b}) } else { h.respondError(c, "Invalid 'active' value; must be true or false", fmt.Errorf("invalid active value: %s", active), http.StatusBadRequest) return } } // Handle search with caching search := c.Query("search") var searchFilters []queryUtils.DynamicFilter var cacheKey string var useCache bool if search != "" { if len(search) > 50 { search = search[:50] } cacheKey = fmt.Sprintf("ref_visit_type:search:%s:%d:%d", search, query.Limit, query.Offset) searchFilters = []queryUtils.DynamicFilter{ {Column: "rvt.id", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, {Column: "rvt.name", Operator: queryUtils.OpILike, Value: "%" + search + "%"}, } if cachedData, found := h.cache.Get(cacheKey); found { logger.Info("Cache hit for search", map[string]interface{}{"search": search}) if patients, ok := cachedData.([]referenceModels.RefVisitType); ok { var aggregateData *models.AggregateData if c.Query("include_summary") == "true" { fullFilterGroups := []queryUtils.FilterGroup{ {Filters: searchFilters, LogicOp: "OR"}, } if len(filters) > 0 { fullFilterGroups = append(fullFilterGroups, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) } aggregateData, err = h.getAggregateDataRVT(ctx, dbConn, fullFilterGroups) if err != nil { h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) return } } meta := h.calculateMeta(query.Limit, query.Offset, len(patients)) response := referenceModels.RefVisitTypeGetResponse{ Message: "Data ref visit type berhasil diambil (dari cache)", Data: patients, Meta: meta, } if aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) return } } useCache = true query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: searchFilters, LogicOp: "OR"}) } if len(filters) > 0 { query.Filters = append(query.Filters, queryUtils.FilterGroup{Filters: filters, LogicOp: "AND"}) } // Execute query var results []map[string]interface{} err = h.queryBuilder.ExecuteQuery(ctx, dbConn, query, &results) if err != nil { h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError) return } // Process results into structured format patients := h.processQueryResultsRVT(results) // Get total count for pagination metadata total, err := h.getTotalCount(ctx, dbConn, query) if err != nil { h.logAndRespondError(c, "Failed to get total count", err, http.StatusInternalServerError) return } // Cache results if this was a search query if useCache && len(patients) > 0 { h.cache.Set(cacheKey, patients, 15*time.Minute) logger.Info("Cached search results", map[string]interface{}{"search": search, "count": len(patients)}) } // Get aggregate data if requested var aggregateData *models.AggregateData if c.Query("include_summary") == "true" { aggregateData, err = h.getAggregateDataRVT(ctx, dbConn, query.Filters) if err != nil { h.logAndRespondError(c, "Failed to get aggregate data", err, http.StatusInternalServerError) return } } // Build and send response meta := h.calculateMeta(query.Limit, query.Offset, total) response := referenceModels.RefVisitTypeGetResponse{ Message: "Data Refence Visit Type berhasil diambil", Data: patients, Meta: meta, } if aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) } // ============================================================================= // HELPER FUNCTIONS (READ-ONLY) // ============================================================================= // parsePagination parses pagination parameters from the request func (h *ReferenceHandler) parsePagination(c *gin.Context, query *queryUtils.DynamicQuery) { if limit, err := strconv.Atoi(c.DefaultQuery("limit", "10")); err == nil && limit > 0 && limit <= 100 { query.Limit = limit } if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil && offset >= 0 { query.Offset = offset } } // processQueryResults processes the flat query results into a structured nested format func (h *ReferenceHandler) processQueryResultsRST(results []map[string]interface{}) []referenceModels.RefServiceType { rstMap := make(map[int64]*referenceModels.RefServiceType) order := make([]int64, 0, len(results)) for _, result := range results { referenceID := getInt64(result, "id") rst, exists := rstMap[referenceID] if !exists { rst = &referenceModels.RefServiceType{ ID: referenceID, Name: getNullString(result, "name"), Active: getNullBool(result,"active"), } rstMap[referenceID] = rst order = append(order, referenceID) } } // Convert map to slice while preserving order rsts := make([]referenceModels.RefServiceType, 0, len(rstMap)) for _, id := range order { if p, ok := rstMap[id]; ok { rsts = append(rsts, *p) } } return rsts } // processQueryResults processes the flat query results into a structured nested format func (h *ReferenceHandler) processQueryResultsRPT(results []map[string]interface{}) []referenceModels.RefPaymentType { rstMap := make(map[int64]*referenceModels.RefPaymentType) order := make([]int64, 0, len(results)) for _, result := range results { referenceID := getInt64(result, "id") rst, exists := rstMap[referenceID] if !exists { rst = &referenceModels.RefPaymentType{ ID: referenceID, Name: getNullString(result, "name"), Active: getNullBool(result,"active"), } rstMap[referenceID] = rst order = append(order, referenceID) } } // Convert map to slice while preserving order rsts := make([]referenceModels.RefPaymentType, 0, len(rstMap)) for _, id := range order { if p, ok := rstMap[id]; ok { rsts = append(rsts, *p) } } return rsts } func (h *ReferenceHandler) processQueryResultsRVT(results []map[string]interface{}) []referenceModels.RefVisitType { rstMap := make(map[int64]*referenceModels.RefVisitType) order := make([]int64, 0, len(results)) for _, result := range results { referenceID := getInt64(result, "id") rst, exists := rstMap[referenceID] if !exists { rst = &referenceModels.RefVisitType{ ID: referenceID, Name: getNullString(result, "name"), Active: getNullBool(result,"active"), } rstMap[referenceID] = rst order = append(order, referenceID) } } // Convert map to slice while preserving order rsts := make([]referenceModels.RefVisitType, 0, len(rstMap)) for _, id := range order { if p, ok := rstMap[id]; ok { rsts = append(rsts, *p) } } return rsts } func (h *ReferenceHandler) processQueryResultsRHT(results []map[string]interface{}) []referenceModels.RefHealthcareType { rstMap := make(map[int64]*referenceModels.RefHealthcareType) order := make([]int64, 0, len(results)) for _, result := range results { referenceID := getInt64(result, "id") rst, exists := rstMap[referenceID] if !exists { rst = &referenceModels.RefHealthcareType{ ID: referenceID, Name: getNullString(result, "name"), Active: getNullBool(result,"active"), } rstMap[referenceID] = rst order = append(order, referenceID) } } // Convert map to slice while preserving order rsts := make([]referenceModels.RefHealthcareType, 0, len(rstMap)) for _, id := range order { if p, ok := rstMap[id]; ok { rsts = append(rsts, *p) } } return rsts } func (h *ReferenceHandler) getTotalCount(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) (int, error) { countQuery := queryUtils.DynamicQuery{ From: query.From, Aliases: query.Aliases, Filters: query.Filters, Joins: query.Joins, } count, err := h.queryBuilder.ExecuteCount(ctx, dbConn, countQuery) if err != nil { return 0, fmt.Errorf("failed to execute count query: %w", err) } return int(count), nil } // getAggregateData gets aggregate data for the references service type func (h *ReferenceHandler) getAggregateDataRST(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } var wg sync.WaitGroup var mu sync.Mutex errChan := make(chan error, 2) // Count by status wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() query := queryUtils.DynamicQuery{ From: "reference.ref_service_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "active"}, {Expression: "COUNT(*)", Alias: "count"}, }, Filters: filterGroups, GroupBy: []string{"active"}, } var results []struct { Status string `db:"active"` Count int `db:"count"` } err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results) if err != nil { errChan <- fmt.Errorf("status query failed: %w", err) return } mu.Lock() for _, result := range results { aggregate.ByStatus[result.Status] = result.Count switch result.Status { case "active": aggregate.TotalActive = result.Count case "draft": aggregate.TotalDraft = result.Count case "inactive": aggregate.TotalInactive = result.Count } } mu.Unlock() }() // Get last updated and today's stats wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() // Last updated query1 := queryUtils.DynamicQuery{ From: "reference.ref_service_type", Aliases: "rst", Fields: []queryUtils.SelectField{{Expression: "MAX(date_updated)"}}, Filters: filterGroups, } var lastUpdated sql.NullTime err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated) if err != nil { errChan <- fmt.Errorf("last updated query failed: %w", err) return } today := time.Now().Format("2006-01-02") // Query for created_today createdTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_service_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_created)", Operator: queryUtils.OpEqual, Value: today}, }, LogicOp: "AND", }), } var createdToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday) if err != nil { errChan <- fmt.Errorf("created today query failed: %w", err) return } // Query for updated_today updatedTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_service_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_updated)", Operator: queryUtils.OpEqual, Value: today}, {Column: "DATE(date_created)", Operator: queryUtils.OpNotEqual, Value: today}, }, LogicOp: "AND", }), } var updatedToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday) if err != nil { errChan <- fmt.Errorf("updated today query failed: %w", err) return } mu.Lock() if lastUpdated.Valid { aggregate.LastUpdated = &lastUpdated.Time } aggregate.CreatedToday = createdToday aggregate.UpdatedToday = updatedToday mu.Unlock() }() wg.Wait() close(errChan) for err := range errChan { if err != nil { return nil, err } } return aggregate, nil } func (h *ReferenceHandler) getAggregateDataRPT(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } var wg sync.WaitGroup var mu sync.Mutex errChan := make(chan error, 2) // Count by status wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() query := queryUtils.DynamicQuery{ From: "reference.ref_payment_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "active"}, {Expression: "COUNT(*)", Alias: "count"}, }, Filters: filterGroups, GroupBy: []string{"active"}, } var results []struct { Status string `db:"active"` Count int `db:"count"` } err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results) if err != nil { errChan <- fmt.Errorf("status query failed: %w", err) return } mu.Lock() for _, result := range results { aggregate.ByStatus[result.Status] = result.Count switch result.Status { case "active": aggregate.TotalActive = result.Count case "draft": aggregate.TotalDraft = result.Count case "inactive": aggregate.TotalInactive = result.Count } } mu.Unlock() }() // Get last updated and today's stats wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() // Last updated query1 := queryUtils.DynamicQuery{ From: "reference.ref_payment_type", Aliases: "rst", Fields: []queryUtils.SelectField{{Expression: "MAX(date_updated)"}}, Filters: filterGroups, } var lastUpdated sql.NullTime err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated) if err != nil { errChan <- fmt.Errorf("last updated query failed: %w", err) return } today := time.Now().Format("2006-01-02") // Query for created_today createdTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_payment_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_created)", Operator: queryUtils.OpEqual, Value: today}, }, LogicOp: "AND", }), } var createdToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday) if err != nil { errChan <- fmt.Errorf("created today query failed: %w", err) return } // Query for updated_today updatedTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_payment_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_updated)", Operator: queryUtils.OpEqual, Value: today}, {Column: "DATE(date_created)", Operator: queryUtils.OpNotEqual, Value: today}, }, LogicOp: "AND", }), } var updatedToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday) if err != nil { errChan <- fmt.Errorf("updated today query failed: %w", err) return } mu.Lock() if lastUpdated.Valid { aggregate.LastUpdated = &lastUpdated.Time } aggregate.CreatedToday = createdToday aggregate.UpdatedToday = updatedToday mu.Unlock() }() wg.Wait() close(errChan) for err := range errChan { if err != nil { return nil, err } } return aggregate, nil } func (h *ReferenceHandler) getAggregateDataRVT(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } var wg sync.WaitGroup var mu sync.Mutex errChan := make(chan error, 2) // Count by status wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() query := queryUtils.DynamicQuery{ From: "reference.ref_visit_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "active"}, {Expression: "COUNT(*)", Alias: "count"}, }, Filters: filterGroups, GroupBy: []string{"active"}, } var results []struct { Status string `db:"active"` Count int `db:"count"` } err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results) if err != nil { errChan <- fmt.Errorf("status query failed: %w", err) return } mu.Lock() for _, result := range results { aggregate.ByStatus[result.Status] = result.Count switch result.Status { case "active": aggregate.TotalActive = result.Count case "draft": aggregate.TotalDraft = result.Count case "inactive": aggregate.TotalInactive = result.Count } } mu.Unlock() }() // Get last updated and today's stats wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() // Last updated query1 := queryUtils.DynamicQuery{ From: "reference.ref_visit_type", Aliases: "rst", Fields: []queryUtils.SelectField{{Expression: "MAX(date_updated)"}}, Filters: filterGroups, } var lastUpdated sql.NullTime err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated) if err != nil { errChan <- fmt.Errorf("last updated query failed: %w", err) return } today := time.Now().Format("2006-01-02") // Query for created_today createdTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_visit_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_created)", Operator: queryUtils.OpEqual, Value: today}, }, LogicOp: "AND", }), } var createdToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday) if err != nil { errChan <- fmt.Errorf("created today query failed: %w", err) return } // Query for updated_today updatedTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_visit_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_updated)", Operator: queryUtils.OpEqual, Value: today}, {Column: "DATE(date_created)", Operator: queryUtils.OpNotEqual, Value: today}, }, LogicOp: "AND", }), } var updatedToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday) if err != nil { errChan <- fmt.Errorf("updated today query failed: %w", err) return } mu.Lock() if lastUpdated.Valid { aggregate.LastUpdated = &lastUpdated.Time } aggregate.CreatedToday = createdToday aggregate.UpdatedToday = updatedToday mu.Unlock() }() wg.Wait() close(errChan) for err := range errChan { if err != nil { return nil, err } } return aggregate, nil } func (h *ReferenceHandler) getAggregateDataRHT(ctx context.Context, dbConn *sqlx.DB, filterGroups []queryUtils.FilterGroup) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } var wg sync.WaitGroup var mu sync.Mutex errChan := make(chan error, 2) // Count by status wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() query := queryUtils.DynamicQuery{ From: "reference.ref_healthcare_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "active"}, {Expression: "COUNT(*)", Alias: "count"}, }, Filters: filterGroups, GroupBy: []string{"active"}, } var results []struct { Status string `db:"active"` Count int `db:"count"` } err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &results) if err != nil { errChan <- fmt.Errorf("status query failed: %w", err) return } mu.Lock() for _, result := range results { aggregate.ByStatus[result.Status] = result.Count switch result.Status { case "active": aggregate.TotalActive = result.Count case "draft": aggregate.TotalDraft = result.Count case "inactive": aggregate.TotalInactive = result.Count } } mu.Unlock() }() // Get last updated and today's stats wg.Add(1) go func() { defer wg.Done() queryCtx, queryCancel := context.WithTimeout(ctx, 20*time.Second) defer queryCancel() // Last updated query1 := queryUtils.DynamicQuery{ From: "reference.ref_healthcare_type", Aliases: "rst", Fields: []queryUtils.SelectField{{Expression: "MAX(date_updated)"}}, Filters: filterGroups, } var lastUpdated sql.NullTime err := h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, query1, &lastUpdated) if err != nil { errChan <- fmt.Errorf("last updated query failed: %w", err) return } today := time.Now().Format("2006-01-02") // Query for created_today createdTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_healthcare_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_created)", Operator: queryUtils.OpEqual, Value: today}, }, LogicOp: "AND", }), } var createdToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, createdTodayQuery, &createdToday) if err != nil { errChan <- fmt.Errorf("created today query failed: %w", err) return } // Query for updated_today updatedTodayQuery := queryUtils.DynamicQuery{ From: "reference.ref_healthcare_type", Aliases: "rst", Fields: []queryUtils.SelectField{ {Expression: "COUNT(*)", Alias: "count"}, }, Filters: append(filterGroups, queryUtils.FilterGroup{ Filters: []queryUtils.DynamicFilter{ {Column: "DATE(date_updated)", Operator: queryUtils.OpEqual, Value: today}, {Column: "DATE(date_created)", Operator: queryUtils.OpNotEqual, Value: today}, }, LogicOp: "AND", }), } var updatedToday int err = h.queryBuilder.ExecuteQueryRow(queryCtx, dbConn, updatedTodayQuery, &updatedToday) if err != nil { errChan <- fmt.Errorf("updated today query failed: %w", err) return } mu.Lock() if lastUpdated.Valid { aggregate.LastUpdated = &lastUpdated.Time } aggregate.CreatedToday = createdToday aggregate.UpdatedToday = updatedToday mu.Unlock() }() wg.Wait() close(errChan) for err := range errChan { if err != nil { return nil, err } } return aggregate, nil } // logAndRespondError logs an error and sends an error response func (h *ReferenceHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { logger.Error(message, map[string]interface{}{"error": err.Error(), "status_code": statusCode}) h.respondError(c, message, err, statusCode) } // respondError sends an error response func (h *ReferenceHandler) respondError(c *gin.Context, message string, err error, statusCode int) { errorMessage := message if gin.Mode() == gin.ReleaseMode { errorMessage = "Internal server error" } c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()}) } // calculateMeta calculates pagination metadata func (h *ReferenceHandler) calculateMeta(limit, offset, total int) models.MetaResponse { totalPages, currentPage := 0, 1 if limit > 0 { totalPages = (total + limit - 1) / limit currentPage = (offset / limit) + 1 } return models.MetaResponse{ Limit: limit, Offset: offset, Total: total, TotalPages: totalPages, CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, } } // ============================================================================= // UTILITY FUNCTIONS (DATA EXTRACTION) // ============================================================================= // getInt64 safely extracts an int64 from a map[string]interface{} func getInt64(m map[string]interface{}, key string) int64 { if val, ok := m[key]; ok { switch v := val.(type) { case int64: return v case int: return int64(v) case float64: return int64(v) } } return 0 } // getNullString safely extracts a sql.NullString from a map[string]interface{} func getNullString(m map[string]interface{}, key string) sql.NullString { if val, ok := m[key]; ok { if ns, ok := val.(sql.NullString); ok { return ns } if s, ok := val.(string); ok { return sql.NullString{String: s, Valid: true} } } return sql.NullString{Valid: false} } // getNullableInt32 safely extracts a models.NullableInt32 from a map[string]interface{} func getNullableInt32(m map[string]interface{}, key string) models.NullableInt32 { if val, ok := m[key]; ok { if v, ok := val.(models.NullableInt32); ok { return v } } return models.NullableInt32{} } // getNullBool safely extracts a sql.NullBool from a map[string]interface{} func getNullBool(m map[string]interface{}, key string) sql.NullBool { if val, ok := m[key]; ok { if nb, ok := val.(sql.NullBool); ok { return nb } if b, ok := val.(bool); ok { return sql.NullBool{Bool: b, Valid: true} } } return sql.NullBool{Valid: false} } // getNullTime safely extracts a sql.NullTime from a map[string]interface{} func getNullTime(m map[string]interface{}, key string) sql.NullTime { if val, ok := m[key]; ok { if nt, ok := val.(sql.NullTime); ok { return nt } if t, ok := val.(time.Time); ok { return sql.NullTime{Time: t, Valid: true} } } return sql.NullTime{Valid: false} }