1424 lines
41 KiB
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}
|
|
} |