From 93d07e85ce8228e5f57ab686cd721cba672c3da3 Mon Sep 17 00:00:00 2001 From: "achmad.nauval.0510" Date: Thu, 8 Jan 2026 10:40:29 +0700 Subject: [PATCH] pembuatan servis reference untuk payment, health, visit, Service type --- internal/handlers/reference/references.go | 1424 +++++++++++++++++++++ internal/models/reference/reference.go | 355 +++++ 2 files changed, 1779 insertions(+) create mode 100644 internal/handlers/reference/references.go create mode 100644 internal/models/reference/reference.go diff --git a/internal/handlers/reference/references.go b/internal/handlers/reference/references.go new file mode 100644 index 0000000..d20a02d --- /dev/null +++ b/internal/handlers/reference/references.go @@ -0,0 +1,1424 @@ +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} +} \ No newline at end of file diff --git a/internal/models/reference/reference.go b/internal/models/reference/reference.go new file mode 100644 index 0000000..51ce540 --- /dev/null +++ b/internal/models/reference/reference.go @@ -0,0 +1,355 @@ +package referecnce + +import ( + "api-service/internal/models" + "database/sql" + "encoding/json" +) + +// table ref_service_type table +type RefServiceType struct { + ID int64 `json:"id" db:"id"` + Name sql.NullString `json:"name,omitempty" db:"name"` + Active sql.NullBool `json:"active,omitempty" db:"active"` +} + +// Custom JSON marshaling untuk RefServiceType agar NULL values tidak muncul di response +func (r RefServiceType) MarshalJSON() ([]byte, error) { + type Alias RefServiceType + aux := &struct { + *Alias + Name *string `json:"name,omitempty"` + Active *bool `json:"active,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Name.Valid { + aux.Name = &r.Name.String + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *RefServiceType) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +} + +func (r *RefServiceType) GetActive() bool { + if r.Active.Valid { + return r.Active.Bool + } + return false +} + +// table ref_payment_type table +type RefPaymentType struct { + ID int64 `json:"id" db:"id"` + Name sql.NullString `json:"name,omitempty" db:"name"` + Active sql.NullBool `json:"active,omitempty" db:"active"` +} + +// Custom JSON marshaling untuk RefPaymentType agar NULL values tidak muncul di response +func (r RefPaymentType) MarshalJSON() ([]byte, error) { + type Alias RefPaymentType + aux := &struct { + *Alias + Name *string `json:"name,omitempty"` + Active *bool `json:"active,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Name.Valid { + aux.Name = &r.Name.String + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *RefPaymentType) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +} + +func (r *RefPaymentType) GetActive() bool { + if r.Active.Valid { + return r.Active.Bool + } + return false +} + +// table ref_visit_type table +type RefVisitType struct { + ID int64 `json:"id" db:"id"` + Name sql.NullString `json:"name,omitempty" db:"name"` + Active sql.NullBool `json:"active,omitempty" db:"active"` +} + +// Custom JSON marshaling untuk RefVisitType agar NULL values tidak muncul di response +func (r RefVisitType) MarshalJSON() ([]byte, error) { + type Alias RefVisitType + aux := &struct { + *Alias + Name *string `json:"name,omitempty"` + Active *bool `json:"active,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Name.Valid { + aux.Name = &r.Name.String + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *RefVisitType) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +} + +func (r *RefVisitType) GetActive() bool { + if r.Active.Valid { + return r.Active.Bool + } + return false +} + +// table ref_healthcare_type table +type RefHealthcareType struct { + ID int64 `json:"id" db:"id"` + Name sql.NullString `json:"name,omitempty" db:"name"` + Active sql.NullBool `json:"active,omitempty" db:"active"` +} + +// Custom JSON marshaling untuk RefHealthcareType agar NULL values tidak muncul di response +func (r RefHealthcareType) MarshalJSON() ([]byte, error) { + type Alias RefHealthcareType + aux := &struct { + *Alias + Name *string `json:"name,omitempty"` + Active *bool `json:"active,omitempty"` + }{ + Alias: (*Alias)(&r), + } + + if r.Name.Valid { + aux.Name = &r.Name.String + } + if r.Active.Valid { + aux.Active = &r.Active.Bool + } + + return json.Marshal(aux) +} + +// Helper methods untuk mendapatkan nilai yang aman +func (r *RefHealthcareType) GetName() string { + if r.Name.Valid { + return r.Name.String + } + return "" +} + +func (r *RefHealthcareType) GetActive() bool { + if r.Active.Valid { + return r.Active.Bool + } + return false +} + +// Response structs for RefServiceType +type RefServiceTypeGetByIDResponse struct { + Message string `json:"message"` + Data *RefServiceType `json:"data"` +} + +type RefServiceTypeGetResponse struct { + Message string `json:"message"` + Data []RefServiceType `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +type RefServiceTypeCreateRequest struct { + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefServiceTypeCreateResponse struct { + Message string `json:"message"` + Data *RefServiceType `json:"data"` +} + +type RefServiceTypeUpdateRequest struct { + ID *int `json:"-" validate:"required,min=1"` + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefServiceTypeUpdateResponse struct { + Message string `json:"message"` + Data *RefServiceType `json:"data"` +} + +type RefServiceTypeDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +type RefServiceTypeFilter struct { + Status *string `json:"status,omitempty" form:"status"` + Search *string `json:"search,omitempty" form:"search"` +} + +// Response structs for RefPaymentType +type RefPaymentTypeGetByIDResponse struct { + Message string `json:"message"` + Data *RefPaymentType `json:"data"` +} + +type RefPaymentTypeGetResponse struct { + Message string `json:"message"` + Data []RefPaymentType `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +type RefPaymentTypeCreateRequest struct { + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefPaymentTypeCreateResponse struct { + Message string `json:"message"` + Data *RefPaymentType `json:"data"` +} + +type RefPaymentTypeUpdateRequest struct { + ID *int `json:"-" validate:"required,min=1"` + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefPaymentTypeUpdateResponse struct { + Message string `json:"message"` + Data *RefPaymentType `json:"data"` +} + +type RefPaymentTypeDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +type RefPaymentTypeFilter struct { + Status *string `json:"status,omitempty" form:"status"` + Search *string `json:"search,omitempty" form:"search"` +} + +// Response structs for RefVisitType +type RefVisitTypeGetByIDResponse struct { + Message string `json:"message"` + Data *RefVisitType `json:"data"` +} + +type RefVisitTypeGetResponse struct { + Message string `json:"message"` + Data []RefVisitType `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +type RefVisitTypeCreateRequest struct { + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefVisitTypeCreateResponse struct { + Message string `json:"message"` + Data *RefVisitType `json:"data"` +} + +type RefVisitTypeUpdateRequest struct { + ID *int `json:"-" validate:"required,min=1"` + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefVisitTypeUpdateResponse struct { + Message string `json:"message"` + Data *RefVisitType `json:"data"` +} + +type RefVisitTypeDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +type RefVisitTypeFilter struct { + Status *string `json:"status,omitempty" form:"status"` + Search *string `json:"search,omitempty" form:"search"` +} + +// Response structs for RefHealthcareType +type RefHealthcareTypeGetByIDResponse struct { + Message string `json:"message"` + Data *RefHealthcareType `json:"data"` +} + +type RefHealthcareTypeGetResponse struct { + Message string `json:"message"` + Data []RefHealthcareType `json:"data"` + Meta models.MetaResponse `json:"meta"` + Summary *models.AggregateData `json:"summary,omitempty"` +} + +type RefHealthcareTypeCreateRequest struct { + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefHealthcareTypeCreateResponse struct { + Message string `json:"message"` + Data *RefHealthcareType `json:"data"` +} + +type RefHealthcareTypeUpdateRequest struct { + ID *int `json:"-" validate:"required,min=1"` + Name *string `json:"name" validate:"required,min=1,max=20"` + Active *bool `json:"active"` +} + +type RefHealthcareTypeUpdateResponse struct { + Message string `json:"message"` + Data *RefHealthcareType `json:"data"` +} + +type RefHealthcareTypeDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +type RefHealthcareTypeFilter struct { + Status *string `json:"status,omitempty" form:"status"` + Search *string `json:"search,omitempty" form:"search"` +} \ No newline at end of file