Files
service_antrean/internal/handlers/reference/references.go

1424 lines
41 KiB
Go

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}
}