first commit

This commit is contained in:
2026-02-13 15:57:28 +07:00
parent d4248b62e1
commit 7ae8c76a20
26 changed files with 5040 additions and 32 deletions

No files matched your search

+919
View File
@@ -0,0 +1,919 @@
package antrean
import (
"api-service/internal/config"
"api-service/internal/database"
"api-service/internal/models"
modelsAntrean "api-service/internal/models/antrean"
queryUtils "api-service/internal/utils/query"
"api-service/internal/utils/validation"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
validate.RegisterValidation("antrean_status", validated)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
func validated(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
func NewAntreanHandler() *AntreanHandler {
// Initialize QueryBuilder with allowed columns list for security.
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
SetAllowedColumns([]string{
// Columns from permission_rol table (rpr)
"rpr.id",
"rpr.create",
"rpr.read",
"rpr.update",
"rpr.disable",
"rpr.delete",
"rpr.active",
"rpr.role_keycloak",
"rpr.group_keycloak",
"rpr.fk_rol_pages_id",
"create", "\"create\"",
"read", "\"read\"",
"update", "\"update\"",
"disable", "\"disable\"",
"delete", "\"delete\"",
"active", "fk_rol_pages_id", "role_keycloak", "group_keycloak", "id",
// Columns from pages_rol table (rpf)
"rpf.id",
"rpf.name",
// Alias columns
"perid",
"pecreate",
"peread",
"peupdate",
"pedisable",
"pedelete",
"pgid",
"pagename",
})
return &AntreanHandler{
db: db,
queryBuilder: queryBuilder,
validator: validation.NewDynamicValidator(queryBuilder),
}
}
type AntreanHandler struct {
db database.Service
queryBuilder *queryUtils.QueryBuilder
validator *validation.DynamicValidator
}
func (h *AntreanHandler) GetData(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
rolesStr := c.Query("roles")
groupsStr := c.Query("groups")
if rolesStr == "" || groupsStr == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Missing required parameters: roles (comma-separated, e.g., superadmin,admin) and groups (comma-separated, e.g., STIM,OTHERGROUP)",
})
return
}
roles := strings.Split(rolesStr, ",")
for i := range roles {
roles[i] = strings.TrimSpace(roles[i])
}
var filteredRoles []string
for _, role := range roles {
if role != "" {
filteredRoles = append(filteredRoles, role)
}
}
if len(filteredRoles) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid roles provided"})
return
}
roles = filteredRoles
groups := strings.Split(groupsStr, ",")
for i := range groups {
groups[i] = strings.TrimSpace(groups[i])
}
var filteredGroups []string
for _, group := range groups {
if group != "" {
filteredGroups = append(filteredGroups, group)
}
}
if len(filteredGroups) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid groups provided"})
return
}
groups = filteredGroups
query := queryUtils.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rpr",
Fields: []queryUtils.SelectField{
{Expression: "rpr.id", Alias: "perid"},
{Expression: "rpr.create", Alias: "pecreate"},
{Expression: "rpr.read", Alias: "peread"},
{Expression: "rpr.update", Alias: "peupdate"},
{Expression: "rpr.disable", Alias: "pedisable"},
{Expression: "rpr.delete", Alias: "pedelete"},
{Expression: "rpr.active", Alias: "active"},
{Expression: "rpf.id", Alias: "pgid"},
{Expression: "rpf.name", Alias: "pagename"},
},
Joins: []queryUtils.Join{
{
Type: "LEFT",
Table: "role_access.rol_pages",
Alias: "rpf",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "rpr.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rpf.id"},
},
},
},
},
Sort: []queryUtils.SortField{
{Column: "rpf.id", Order: "ASC"},
},
}
var filters []queryUtils.DynamicFilter
filters = append(filters, queryUtils.DynamicFilter{
Column: "rpr.role_keycloak",
Operator: queryUtils.OpArrayOverlap,
Value: pq.Array(roles),
})
filters = append(filters, queryUtils.DynamicFilter{
Column: "rpr.group_keycloak",
Operator: queryUtils.OpArrayOverlap,
Value: pq.Array(groups),
})
query.Filters = []queryUtils.FilterGroup{
{Filters: filters, LogicOp: "AND"},
}
fmt.Println("INI QUERY NYA :", query)
permissions, total, err := h.fetchPermissionsDynamic(ctx, dbConn, query)
if err != nil {
logger.Error("Failed to fetch permission data", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch permission: " + err.Error()})
return
}
if len(permissions) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "No permission found for the given roles and groups"})
return
}
response := modelsAntrean.PermissionGetResponse{
Message: "Data permission berhasil diambil",
Data: permissions,
Meta: map[string]interface{}{
"total": total,
"count": len(permissions),
},
}
c.JSON(http.StatusOK, response)
}
func (h *AntreanHandler) CreatePermission(c *gin.Context) {
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error(err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get DB connection"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
var req modelsAntrean.CreatePermissionRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON body: " + err.Error()})
return
}
// Validasi input
if len(req.Roles) == 0 || len(req.Groups) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Role dan Group Tidak Boleh Kosong!!"})
return
}
// Build InsertData dengan query builder
insertData := queryUtils.InsertData{
Columns: []string{
"\"create\"",
"\"read\"",
"\"update\"",
"\"disable\"",
"\"delete\"",
"active",
"fk_rol_pages_id",
"role_keycloak",
"group_keycloak",
},
Values: []interface{}{
req.CreatePermission,
req.ReadPermission,
req.UpdatePermission,
req.DisablePermission,
req.DeletePermission,
req.Active,
req.PagesID,
pq.Array(req.Roles),
pq.Array(req.Groups),
},
}
// Build insert query dengan RETURNING
sql, args, err := h.queryBuilder.BuildInsertQuery(
"role_access.rol_permission",
insertData,
"id", // returning column
)
if err != nil {
logger.Error("Failed to build insert query", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build query: " + err.Error()})
return
}
logger.Info("Executing insert query", map[string]interface{}{
"sql": sql,
"args": args,
})
// Execute query
var newID int
err = dbConn.QueryRowxContext(ctx, sql, args...).Scan(&newID)
if err != nil {
// Handle PostgreSQL errors
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code {
case "23505": // unique_violation
logger.Error(pqErr.Error())
c.JSON(http.StatusConflict, gin.H{"error": "Permission already exists for this pagesID, roles, and groups"})
return
case "23503": // foreign_key_violation
logger.Error(pqErr.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid pagesID: foreign key does not exist"})
return
default:
logger.Error(pqErr.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error: " + pqErr.Error()})
return
}
} else {
logger.Error("Failed to execute insert query", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create permission: " + err.Error()})
return
}
}
logger.Info("Permission created successfully", map[string]interface{}{
"id": newID,
})
// Build response
permission := &modelsAntrean.PermisssionAntrean{
IDpermission: newID,
CreatePermission: req.CreatePermission,
ReadPermission: req.ReadPermission,
UpdatePermission: req.UpdatePermission,
DisablePermission: req.DisablePermission,
DeletePermission: req.DeletePermission,
PagesID: req.PagesID,
}
c.JSON(http.StatusCreated, gin.H{
"message": "Data Permission Berhasil",
"permission": permission,
})
}
func (h *AntreanHandler) fetchPermissionsDynamic(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) ([]*modelsAntrean.PermisssionAntrean, int, error) {
logger.Info("Starting fetchPermissionsDynamic", map[string]interface{}{
"from": query.From,
})
var total int
var permissions []*modelsAntrean.PermisssionAntrean
queryCtx, queryCancel := context.WithTimeout(ctx, 30*time.Second)
defer queryCancel()
logger.Info("Executing permission query")
err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &permissions)
if err != nil {
logger.Error("Failed to execute permission query", map[string]interface{}{
"error": err.Error(),
"query": query,
})
return nil, 0, fmt.Errorf("failed to execute permission query: %w", err)
}
logger.Info("Permission query successful", map[string]interface{}{
"recordsFetched": len(permissions),
})
countCtx, countCancel := context.WithTimeout(ctx, 15*time.Second)
defer countCancel()
count, err := h.queryBuilder.ExecuteCount(countCtx, dbConn, query)
if err != nil {
logger.Warn("Failed to get exact count, using returned records", map[string]interface{}{"error": err.Error()})
total = len(permissions)
} else {
total = int(count)
}
logger.Info("Query execution completed", map[string]interface{}{
"totalRecords": total,
"returnedRecords": len(permissions),
})
// Check if no data found
if len(permissions) == 0 {
return nil, 0, sql.ErrNoRows
}
return permissions, total, nil
}
func (h *AntreanHandler) UpdatePermission(c *gin.Context) {
// Get SQLX DB connection
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error(err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get DB connection"})
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
// Get ID from query parameter
idStr := c.Query("id")
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or missing ID in query (must be positive integer)"})
return
}
// Bind request body
var updateReq modelsAntrean.UpdatePermission
if err := c.ShouldBindJSON(&updateReq); err != nil {
logger.Error(err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON body: " + err.Error()})
return
}
// Validate PagesID if provided
if updateReq.PagesID != nil && *updateReq.PagesID <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid PagesID (must be positive integer)"})
return
}
// Filter and validate Roles
if len(updateReq.Roles) > 0 {
var filteredRoles []string
for _, role := range updateReq.Roles {
trimmed := strings.TrimSpace(role)
if trimmed != "" {
filteredRoles = append(filteredRoles, trimmed)
}
}
if len(filteredRoles) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid roles provided"})
return
}
updateReq.Roles = filteredRoles
}
// Filter and validate Groups
if len(updateReq.Groups) > 0 {
var filteredGroups []string
for _, group := range updateReq.Groups {
trimmed := strings.TrimSpace(group)
if trimmed != "" {
filteredGroups = append(filteredGroups, trimmed)
}
}
if len(filteredGroups) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid groups provided"})
return
}
updateReq.Groups = filteredGroups
}
// Build UpdateData untuk query builder
var columns []string
var values []interface{}
// Add fields yang akan di-update (hanya yang provided)
if updateReq.CreatePermission != nil {
columns = append(columns, `"create"`)
values = append(values, *updateReq.CreatePermission)
}
if updateReq.ReadPermission != nil {
columns = append(columns, `"read"`)
values = append(values, *updateReq.ReadPermission)
}
if updateReq.UpdatePermission != nil {
columns = append(columns, `"update"`)
values = append(values, *updateReq.UpdatePermission)
}
if updateReq.DisablePermission != nil {
columns = append(columns, `"disable"`)
values = append(values, *updateReq.DisablePermission)
}
if updateReq.DeletePermission != nil {
columns = append(columns, `"delete"`)
values = append(values, *updateReq.DeletePermission)
}
if updateReq.Active != nil {
columns = append(columns, "active")
values = append(values, *updateReq.Active)
}
if updateReq.PagesID != nil {
columns = append(columns, "fk_rol_pages_id")
values = append(values, *updateReq.PagesID)
}
if len(updateReq.Roles) > 0 {
columns = append(columns, "role_keycloak")
values = append(values, pq.Array(updateReq.Roles))
}
if len(updateReq.Groups) > 0 {
columns = append(columns, "group_keycloak")
values = append(values, pq.Array(updateReq.Groups))
}
// Check if there's anything to update
if len(columns) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No fields to update"})
return
}
updateData := queryUtils.UpdateData{
Columns: columns,
Values: values,
}
// Build WHERE filter untuk UPDATE
filters := []queryUtils.FilterGroup{
{
Filters: []queryUtils.DynamicFilter{
{
Column: "id",
Operator: queryUtils.OpEqual,
Value: id,
},
},
},
}
// Build UPDATE query dengan RETURNING
sql, args, err := h.queryBuilder.BuildUpdateQuery(
"role_access.rol_permission",
updateData,
filters,
"id", // returning column
)
if err != nil {
logger.Error("Failed to build update query", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build query: " + err.Error()})
return
}
logger.Info("Executing update query", map[string]interface{}{
"sql": sql,
"args": args,
})
// Execute UPDATE query
var updatedID int
err = dbConn.QueryRowxContext(ctx, sql, args...).Scan(&updatedID)
if err != nil {
if strings.Contains(err.Error(), "no rows") {
logger.Error("Permission not found", map[string]interface{}{"id": id})
c.JSON(http.StatusNotFound, gin.H{"error": "Permission not found with ID: " + strconv.Itoa(id)})
return
}
// Handle PostgreSQL errors
if pqErr, ok := err.(*pq.Error); ok {
switch pqErr.Code {
case "23503": // foreign_key_violation
logger.Error(pqErr.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid pagesID: foreign key does not exist"})
return
default:
logger.Error(pqErr.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error: " + pqErr.Error()})
return
}
}
logger.Error("Failed to execute update query", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update permission: " + err.Error()})
return
}
logger.Info("Permission updated successfully", map[string]interface{}{"id": updatedID})
// Fetch updated data menggunakan function terpisah
updatedPermission, err := h.fetchPermissionByID(ctx, dbConn, updatedID)
if err != nil {
logger.Error("Failed to fetch updated permission", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission updated but failed to fetch: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Permission updated successfully",
"permission": updatedPermission,
})
}
func (h *AntreanHandler) fetchPermissionByID(ctx context.Context, dbConn *sqlx.DB, id int) (*modelsAntrean.PermisssionAntrean, error) {
logger.Info("Fetching permission by ID", map[string]interface{}{"id": id})
// Build query menggunakan query builder (sama seperti GetData)
query := queryUtils.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rpr",
Fields: []queryUtils.SelectField{
{Expression: "rpr.id", Alias: "perid"},
{Expression: "rpr.\"create\"", Alias: "pecreate"},
{Expression: "rpr.\"read\"", Alias: "peread"},
{Expression: "rpr.\"update\"", Alias: "peupdate"},
{Expression: "rpr.\"disable\"", Alias: "pedisable"},
{Expression: "rpr.\"delete\"", Alias: "pedelete"},
{Expression: "rpr.active", Alias: "active"},
{Expression: "rpr.fk_rol_pages_id", Alias: "pgid"},
{Expression: "rpr.role_keycloak", Alias: "role_keycloak"},
{Expression: "rpr.group_keycloak", Alias: "group_keycloak"},
{Expression: "rpf.id", Alias: "pgid"},
{Expression: "rpf.\"name\"", Alias: "pagename"},
},
Joins: []queryUtils.Join{
{
Type: "LEFT",
Table: "role_access.rol_pages",
Alias: "rpf",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{
Column: "rpr.fk_rol_pages_id",
Operator: queryUtils.OpEqual,
Value: "rpf.id",
},
},
},
},
},
Filters: []queryUtils.FilterGroup{
{
Filters: []queryUtils.DynamicFilter{
{
Column: "rpr.id",
Operator: queryUtils.OpEqual,
Value: id,
},
{
Column: "rpr.active",
Operator: queryUtils.OpEqual,
Value: true,
},
},
LogicOp: "AND",
},
},
Limit: 1,
}
// Execute query
var permissions []modelsAntrean.PermisssionAntrean
queryCtx, queryCancel := context.WithTimeout(ctx, 10*time.Second)
defer queryCancel()
err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &permissions)
if err != nil {
return nil, fmt.Errorf("failed to execute fetch query: %w", err)
}
if len(permissions) == 0 {
return nil, sql.ErrNoRows
}
logger.Info("Permission fetched successfully", map[string]interface{}{
"id": permissions[0].IDpermission,
})
return &permissions[0], nil
}
func (h *AntreanHandler) GetDataPermission(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
rolesStr := c.Query("roles")
groupsStr := c.Query("groups")
if rolesStr == "" || groupsStr == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Missing required parameters: roles (comma-separated, e.g., superadmin,admin) and groups (comma-separated, e.g., STIM,OTHERGROUP)",
})
return
}
roles := strings.Split(rolesStr, ",")
for i := range roles {
roles[i] = strings.TrimSpace(roles[i])
}
var filteredRoles []string
for _, role := range roles {
if role != "" {
filteredRoles = append(filteredRoles, role)
}
}
if len(filteredRoles) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid roles provided"})
return
}
groups := strings.Split(groupsStr, ",")
for i := range groups {
groups[i] = strings.TrimSpace(groups[i])
}
var filteredGroups []string
for _, group := range groups {
if group != "" {
filteredGroups = append(filteredGroups, group)
}
}
if len(filteredGroups) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid groups provided"})
return
}
query := queryUtils.DynamicQuery{
From: "role_access.rol_permission",
Aliases: "rpr",
Fields: []queryUtils.SelectField{
{Expression: "rpr.id", Alias: "perid"},
{Expression: "rpr.create", Alias: "pecreate"},
{Expression: "rpr.read", Alias: "peread"},
{Expression: "rpr.update", Alias: "peupdate"},
{Expression: "rpr.disable", Alias: "pedisable"},
{Expression: "rpr.delete", Alias: "pedelete"},
{Expression: "rpr.active", Alias: "active"},
{Expression: "rpf.id", Alias: "pgid"},
{Expression: "rpf.name", Alias: "pagename"},
{Expression: "rpf.level", Alias: "level"},
{Expression: "rpf.sort", Alias: "sort"},
{Expression: "rpf.parent", Alias: "parent"},
},
Joins: []queryUtils.Join{
{
Type: "LEFT",
Table: "role_access.rol_pages",
Alias: "rpf",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "rpr.fk_rol_pages_id", Operator: queryUtils.OpEqual, Value: "rpf.id"},
},
},
},
},
Sort: []queryUtils.SortField{
{Column: "rpf.level", Order: "ASC"},
{Column: "rpf.sort", Order: "ASC"},
{Column: "rpf.id", Order: "ASC"},
},
}
var filters []queryUtils.DynamicFilter
filters = append(filters, queryUtils.DynamicFilter{
Column: "rpr.role_keycloak",
Operator: queryUtils.OpArrayOverlap,
Value: pq.Array(filteredRoles),
})
filters = append(filters, queryUtils.DynamicFilter{
Column: "rpr.group_keycloak",
Operator: queryUtils.OpArrayOverlap,
Value: pq.Array(filteredGroups),
})
query.Filters = []queryUtils.FilterGroup{
{Filters: filters, LogicOp: "AND"},
}
fmt.Println("INI QUERY NYA :", query)
permissions, total, err := h.fetchPermissionsDynamicnew(ctx, dbConn, query)
if err != nil {
logger.Error("Failed to fetch permission data", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch permission: " + err.Error()})
return
}
if len(permissions) == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "No permission found for the given roles and groups"})
return
}
sortedPermissions := h.sortHierarchical(permissions)
response := modelsAntrean.PermissionGetResponse{
Message: "Data permission berhasil diambil",
Data: sortedPermissions,
Meta: map[string]interface{}{
"total": total,
"count": len(sortedPermissions),
},
}
c.JSON(http.StatusOK, response)
}
// FUNGSI PENTING: Manual Hierarchical Sorting
func (h *AntreanHandler) sortHierarchical(permissions []*modelsAntrean.PermisssionAntrean) []*modelsAntrean.PermisssionAntrean {
if len(permissions) == 0 {
return permissions
}
// Grup berdasarkan parent (0/NIL = root)
parentMap := make(map[int][]*modelsAntrean.PermisssionAntrean)
rootItems := make([]*modelsAntrean.PermisssionAntrean, 0)
for _, perm := range permissions {
parentID := 0 // default root
if perm.Parent != nil && *perm.Parent > 0 {
parentID = *perm.Parent
}
if parentID == 0 {
rootItems = append(rootItems, perm)
} else {
parentMap[parentID] = append(parentMap[parentID], perm)
}
}
// Sort root items berdasarkan sort/level
sort.Slice(rootItems, func(i, j int) bool {
sortI := 999 // default jika nil
sortJ := 999
if rootItems[i].Sort != nil {
sortI = *rootItems[i].Sort
}
if rootItems[j].Sort != nil {
sortJ = *rootItems[j].Sort
}
if sortI == sortJ {
return rootItems[i].PagesID < rootItems[j].PagesID
}
return sortI < sortJ
})
result := make([]*modelsAntrean.PermisssionAntrean, 0)
// Build hierarchy secara rekursif
var buildTree func(parentID int)
buildTree = func(parentID int) {
children, exists := parentMap[parentID]
if !exists {
return
}
// Sort children berdasarkan sort
sort.Slice(children, func(i, j int) bool {
sortI := 999
sortJ := 999
if children[i].Sort != nil {
sortI = *children[i].Sort
}
if children[j].Sort != nil {
sortJ = *children[j].Sort
}
if sortI == sortJ {
return children[i].PagesID < children[j].PagesID
}
return sortI < sortJ
})
for _, child := range children {
result = append(result, child)
buildTree(child.PagesID) // rekursi ke grandchildren
}
}
// Mulai dari root items
for _, root := range rootItems {
result = append(result, root)
buildTree(root.PagesID)
}
return result
}
func (h *AntreanHandler) fetchPermissionsDynamicnew(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) ([]*modelsAntrean.PermisssionAntrean, int, error) {
logger.Info("Starting fetchPermissionsDynamic", map[string]interface{}{
"from": query.From,
})
var total int
var permissions []*modelsAntrean.PermisssionAntrean
queryCtx, queryCancel := context.WithTimeout(ctx, 30*time.Second)
defer queryCancel()
logger.Info("Executing permission query")
err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &permissions)
if err != nil {
logger.Error("Failed to execute permission query", map[string]interface{}{
"error": err.Error(),
"query": query,
})
return nil, 0, fmt.Errorf("failed to execute permission query: %w", err)
}
logger.Info("Permission query successful", map[string]interface{}{
"recordsFetched": len(permissions),
})
countCtx, countCancel := context.WithTimeout(ctx, 15*time.Second)
defer countCancel()
count, err := h.queryBuilder.ExecuteCount(countCtx, dbConn, query)
if err != nil {
logger.Warn("Failed to get exact count, using returned records", map[string]interface{}{"error": err.Error()})
total = len(permissions)
} else {
total = int(count)
}
logger.Info("Query execution completed", map[string]interface{}{
"totalRecords": total,
"returnedRecords": len(permissions),
})
if len(permissions) == 0 {
return nil, 0, sql.ErrNoRows
}
return permissions, total, nil
}
+228
View File
@@ -0,0 +1,228 @@
package master
import (
"api-service/internal/config"
"api-service/internal/database"
"api-service/internal/models"
modelsMaster "api-service/internal/models/master"
"api-service/pkg/logger"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
"net/http"
"strconv"
"strings"
"time"
)
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("retribusi_status", validateRetribusiStatus)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
func validateRetribusiStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
type DokterHandler struct {
db database.Service
}
func NewDokterHandler() *DokterHandler {
return &DokterHandler{
db: db,
}
}
func (h *DokterHandler) GetDokterbyspesialis(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
klinikIDStr := c.Param("idklinik")
klinikID, err := strconv.Atoi(klinikIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid klinik ID"})
return
}
dbAntrean, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Failed to connect postgres_antrean", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
spesialisIDs, err := h.GetSpesialisIDsByKlinik(ctx, dbAntrean, klinikID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
if len(spesialisIDs) == 0 {
c.JSON(http.StatusOK, modelsMaster.DokterResponse{
Message: "Tidak ada dokter untuk klinik ini",
Data: []*modelsMaster.DokterFormatted{},
Meta: map[string]interface{}{"total": 0},
})
return
}
logger.Info("Spesialis IDs found", map[string]interface{}{
"klinikID": klinikID,
"spesialisIDs": spesialisIDs,
"count": len(spesialisIDs),
})
dbConn, err := h.db.GetSQLXDB("postgres_satudata")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
dokterList, err := h.GetDokterBySpesialisIDs(ctx, dbConn, spesialisIDs)
if err != nil {
logger.Error("Failed to get dokter data", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get dokter data"})
return
}
formattedData := h.FormatDokterData(dokterList)
c.JSON(http.StatusOK, modelsMaster.DokterResponse{
Message: "Data dokter berhasil diambil",
Data: formattedData,
Meta: map[string]interface{}{
"total": len(formattedData),
"klinikID": klinikID,
"jumlahSpesialis": len(spesialisIDs),
},
})
}
func (h *DokterHandler) GetSpesialisIDsByKlinik(ctx context.Context, dbConn *sqlx.DB, klinikID int) ([]int, error) {
query := `
SELECT fk_sd_spesialis_id
FROM master.ms_healthcare_service_spesialis
WHERE fk_ms_healthcare_service_id = $1
`
var spesialisIDs []int
err := dbConn.SelectContext(ctx, &spesialisIDs, query, klinikID)
if err != nil {
return nil, fmt.Errorf("failed to get spesialis IDs: %w", err)
}
return spesialisIDs, nil
}
func (h *DokterHandler) FormatDokterData(data []*modelsMaster.ListDokter) []*modelsMaster.DokterFormatted {
result := make([]*modelsMaster.DokterFormatted, 0, len(data))
for _, dokter := range data {
namaLengkap := ""
if dokter.GelarDepan != "" {
gelarDepanList := strings.Split(dokter.GelarDepan, "|")
var formattedGelar []string
for _, gelar := range gelarDepanList {
gelar = strings.TrimSpace(gelar)
if gelar != "" && !strings.HasSuffix(gelar, ".") {
gelar += "."
}
if gelar != "" {
formattedGelar = append(formattedGelar, gelar)
}
}
if len(formattedGelar) > 0 {
namaLengkap = strings.Join(formattedGelar, " ") + " "
}
}
namaLengkap += strings.TrimSpace(dokter.NamaLengkap)
if dokter.GelarBelakang != "" {
gelarBelakangList := strings.Split(dokter.GelarBelakang, "|")
var formattedGelar []string
for _, gelar := range gelarBelakangList {
gelar = strings.TrimSpace(gelar)
if gelar != "" {
formattedGelar = append(formattedGelar, gelar)
}
}
if len(formattedGelar) > 0 {
namaLengkap += ", " + strings.Join(formattedGelar, ", ")
}
}
formatted := &modelsMaster.DokterFormatted{
NamaLengkapFormatted: strings.TrimSpace(namaLengkap),
Spesialis: strings.TrimSpace(dokter.Spesialis),
Subspesialis: strings.TrimSpace(dokter.Subspesialis),
}
result = append(result, formatted)
}
return result
}
func (h *DokterHandler) GetDokterBySpesialisIDs(ctx context.Context, dbConn *sqlx.DB, spesialisIDs []int) ([]*modelsMaster.ListDokter, error) {
query, args, err := sqlx.In(`
SELECT
ARRAY_TO_STRING(
ARRAY(
SELECT dgd."Gelar_depan"
FROM data_pegawai_daftar_gelar_depan dpdgd
LEFT JOIN daftar_gelar_depan dgd ON dpdgd.daftar_gelar_depan_id = dgd.id
WHERE dpdgd.data_pegawai_id = dp.id
), '|'
) AS gelardepan,
COALESCE(dk."Nama_lengkap", '') AS namalengkap,
ARRAY_TO_STRING(
ARRAY(
SELECT dgb."Gelar_belakang"
FROM data_pegawai_daftar_gelar_belakang dpdgb
LEFT JOIN daftar_gelar_belakang dgb ON dpdgb.daftar_gelar_belakang_id = dgb.id
WHERE dpdgb.data_pegawai_id = dp.id
), '|'
) AS gelarbelakang,
ds."Subspesialis",
dss."Spesialis"
FROM "data_pegawai" dp
LEFT JOIN data_ktp dk ON dp."KTP" = dk.id
LEFT JOIN daftar_subspesialis ds ON dp."Subspesialis" = ds.id
LEFT JOIN daftar_spesialis dss ON ds."FK_daftar_spesialis_ID" = dss.id
WHERE dss.id IN (?)
ORDER BY dk."Nama_lengkap" ASC
`, spesialisIDs)
if err != nil {
return nil, fmt.Errorf("failed to build query: %w", err)
}
query = dbConn.Rebind(query)
var dokterList []*modelsMaster.ListDokter
err = dbConn.SelectContext(ctx, &dokterList, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err)
}
return dokterList, nil
}
+403
View File
@@ -0,0 +1,403 @@
package master
import (
"api-service/internal/config"
"api-service/internal/database"
"api-service/internal/models"
"api-service/internal/models/master"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
"net/http"
"time"
)
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("retribusi_status", validateloketstatus)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
func validateloketstatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
type LoketHandler struct {
db database.Service
}
func NewLoketGHandler() *LoketHandler {
return &LoketHandler{
db: db,
}
}
func (h *LoketHandler) GetLoket(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
IdLoket := c.Param("loket")
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
remainingQuota, err := h.GetRemainingQuota(ctx, dbConn, IdLoket)
if err != nil {
logger.Warn("Failed to get remaining quota, defaulting to 0", map[string]interface{}{
"error": err.Error(),
})
remainingQuota = 0
}
ticketQueue, err := h.GetTicketQueueData(ctx, dbConn, IdLoket)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, master.TicketQueueResponse{
Message: "Tidak ada antrian ticket",
Data: []*master.LoketTiketResponse{},
Meta: map[string]interface{}{
"total": 0,
"counter_id": IdLoket,
},
})
return
}
logger.Error("Failed to get ticket queue", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve ticket queue"})
return
}
mappedData := h.MapToLoketTiketResponse(ticketQueue)
c.JSON(http.StatusOK, master.TicketQueueResponse{
Message: "Data antrian ticket berhasil diambil",
KuotaLoket: remainingQuota,
Data: mappedData,
Meta: map[string]interface{}{
"total": len(ticketQueue),
"counter_id": IdLoket,
},
})
}
func (h *LoketHandler) GetTicketQueueData(ctx context.Context, dbConn *sqlx.DB, counterID string) ([]*master.TicketQueue, error) {
query := `
select tpvhs.id ,tpvhs.healtcare_service_code ,tpvhs.ticket ,tpvhs.datetime_start,
tpv.visit_code , mhs."name" , mhs.code ,mhss.shift_number, rpt."name" as "payment",
rvs."name" as "posisi", rvs.description as "deskripsi", tpvhsvs.datetime as "waktuposisi"
from "transaction".tr_patient_visit_healthcare_service tpvhs
left join master.ms_healthcare_service mhs on tpvhs.fk_ms_healthcare_service_id = mhs.id
left join master.ms_registration_counter mrc on tpvhs.fk_ms_registration_counter_id = mrc.id
left join "transaction".tr_patient_visit tpv on tpvhs.fk_tr_patient_visit_id = tpv.id
left join reference.ref_healthcare_type rht on tpvhs.fk_ref_healthcare_type_id = rht.id
left join reference.ref_service_type rst on tpvhs.fk_ref_service_type_id = rst.id
left join master.ms_sub_healthcare_service mshs on tpvhs.fk_ms_sub_healthcare_service_id = mshs.id
left join reference.ref_visit_type rvt on tpvhs.fk_ref_visit_type_id = rvt.id
left join reference.ref_payment_type rpt on tpvhs.fk_ref_payment_type_id = rpt.id
left join master.ms_healthcare_service_shift mhss on mhss.fk_ms_healthcare_service_id = mhs.id
left join "transaction".tr_patient_visit_healthcare_service_visit_status tpvhsvs
on tpvhsvs.fk_tr_patient_visit_healthcare_service_id =tpvhs.id
left join reference.ref_visit_status rvs on tpvhsvs.fk_ref_visit_status_id = rvs.id
where mrc.id = $1
and tpvhs.active = true
and tpv.active = true
ORDER BY tpvhs.datetime_start ASC
`
rows, err := dbConn.QueryContext(ctx, query, counterID)
if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err)
}
defer rows.Close()
var ticketQueue []*master.TicketQueue
for rows.Next() {
var ticket master.TicketQueue
err := rows.Scan(
&ticket.ID,
&ticket.HealthcareServiceCode,
&ticket.Ticket,
&ticket.DatetimeStart,
&ticket.VisitCode,
&ticket.HealthcareServiceName,
&ticket.HealthcareServiceCodeMS,
&ticket.ShiftNumber,
&ticket.PaymentTypeName,
&ticket.Posisi,
&ticket.Deskripsi,
&ticket.WaktuPosisi,
)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
ticketQueue = append(ticketQueue, &ticket)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
// Check if no data found
if len(ticketQueue) == 0 {
return nil, sql.ErrNoRows
}
logger.Info("Ticket queue retrieved successfully", map[string]interface{}{
"counterID": counterID,
"total": len(ticketQueue),
})
return ticketQueue, nil
}
func (h *LoketHandler) MapToLoketTiketResponse(tickets []*master.TicketQueue) []*master.LoketTiketResponse {
ticketMap := make(map[int64]*master.LoketTiketResponse)
for _, ticket := range tickets {
// Jika ticket ID belum ada di map, buat entry baru
if _, exists := ticketMap[ticket.ID]; !exists {
ticketNumber := ticket.Ticket
if ticket.HealthcareServiceCode.Valid && ticket.HealthcareServiceCode.String != "" {
ticketNumber = ticket.HealthcareServiceCode.String + ticket.Ticket
}
klinikName := ""
if ticket.HealthcareServiceName.Valid {
klinikName = ticket.HealthcareServiceName.String
}
shift := 0
if ticket.ShiftNumber.Valid {
shift = int(ticket.ShiftNumber.Int64)
}
payment := ""
if ticket.PaymentTypeName.Valid {
payment = ticket.PaymentTypeName.String
}
ticketMap[ticket.ID] = &master.LoketTiketResponse{
IdLoketTiket: fmt.Sprintf("%d", ticket.ID),
TiketLoket: ticketNumber,
KlinikTiketLoket: klinikName,
BarcodeTiketLoket: ticket.VisitCode,
TanggalTiketLoket: ticket.DatetimeStart.Format("2006-01-02"),
WaktuTiketLoket: ticket.DatetimeStart.Format("15:04:05"),
ShiftTiketLoket: shift,
Pembayaran: payment,
Posisi: []master.Posisi{},
}
}
// Tambahkan posisi ke array (jika ada data posisi)
if (ticket.Posisi.Valid && ticket.Posisi.String != "") ||
(ticket.Deskripsi.Valid && ticket.Deskripsi.String != "") ||
(ticket.WaktuPosisi.Valid) {
posisiData := master.Posisi{}
if ticket.Posisi.Valid {
posisiData.Posisi = ticket.Posisi.String
}
if ticket.Deskripsi.Valid {
posisiData.Deskripsi = ticket.Deskripsi.String
}
if ticket.WaktuPosisi.Valid {
posisiData.TanggalPosisi = ticket.WaktuPosisi.Time.Format("2006-01-02")
posisiData.WaktuPosisi = ticket.WaktuPosisi.Time.Format("15:04:05")
}
ticketMap[ticket.ID].Posisi = append(ticketMap[ticket.ID].Posisi, posisiData)
}
}
// Convert map ke slice
result := make([]*master.LoketTiketResponse, 0, len(ticketMap))
for _, ticket := range ticketMap {
result = append(result, ticket)
}
return result
}
func (h *LoketHandler) GetRemainingQuota(ctx context.Context, dbConn *sqlx.DB, counterID string) (int, error) {
currentDate := time.Now().Format("2006-01-02")
query := `
SELECT
mrc.quota,
COALESCE(tmrcsq.seat_quota, 0) as used_quota
FROM master.ms_registration_counter mrc
LEFT JOIN temporary.tm_registration_counter_seat_quota tmrcsq
ON tmrcsq.fk_ms_registration_counter_id = mrc.id
AND tmrcsq.date = $1
WHERE mrc.id = $2
`
var maxQuota, usedQuota int
err := dbConn.QueryRowContext(ctx, query, currentDate, counterID).Scan(&maxQuota, &usedQuota)
if err != nil {
if err == sql.ErrNoRows {
return 0, fmt.Errorf("loket not found")
}
return 0, fmt.Errorf("failed to get quota: %w", err)
}
remainingQuota := maxQuota - usedQuota
logger.Info("Remaining quota retrieved", map[string]interface{}{
"counter_id": counterID,
"max_quota": maxQuota,
"used_quota": usedQuota,
"remaining_quota": remainingQuota,
})
return remainingQuota, nil
}
func (h *LoketHandler) GetKlinikRuang(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
// Get data klinik dan ruangan
listKlinikRuangan, err := h.GetRuang(ctx, dbConn)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, master.KlinikRuanganResponse{
Message: "Data klinik dan ruangan tidak ditemukan",
Data: []*master.KlinikRuanganGrouped{},
Meta: map[string]interface{}{
"total": 0,
},
})
return
}
logger.Error("Failed to get klinik ruangan", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve data"})
return
}
// Group by klinik
groupedData := h.groupByKlinikRuangan(listKlinikRuangan)
c.JSON(http.StatusOK, master.KlinikRuanganResponse{
Message: "Data klinik dan ruangan berhasil diambil",
Data: groupedData,
Meta: map[string]interface{}{
"total": len(groupedData),
},
})
}
func (h *LoketHandler) GetRuang(ctx context.Context, dbConn *sqlx.DB) ([]*master.KlinikRuangan, error) {
query := `
SELECT
mhs.id as "idklinik",
mhs.name as "namaklinik",
mshs.id as "idruangan",
mshs.name as "namaruangan"
FROM master.ms_healthcare_service mhs
LEFT JOIN master.ms_sub_healthcare_service mshs
ON mshs.fk_ms_healthcare_service_id = mhs.id
ORDER BY mhs.id, mshs.id
`
var listKlinikRuangan []*master.KlinikRuangan
err := dbConn.SelectContext(ctx, &listKlinikRuangan, query)
if err != nil {
return nil, fmt.Errorf("failed to get klinik ruangan: %w", err)
}
if len(listKlinikRuangan) == 0 {
return nil, sql.ErrNoRows
}
logger.Info("Klinik ruangan retrieved successfully", map[string]interface{}{
"total": len(listKlinikRuangan),
})
return listKlinikRuangan, nil
}
func (h *LoketHandler) groupByKlinikRuangan(data []*master.KlinikRuangan) []*master.KlinikRuanganGrouped {
klinikMap := make(map[string]*master.KlinikRuanganGrouped)
ruanganMap := make(map[string]map[string]bool)
for _, item := range data {
if !item.IDKlinik.Valid || item.IDKlinik.String == "" {
continue
}
idKlinik := item.IDKlinik.String
if _, exists := klinikMap[idKlinik]; !exists {
namaKlinik := ""
if item.NamaKlinik.Valid {
namaKlinik = item.NamaKlinik.String
}
klinikMap[idKlinik] = &master.KlinikRuanganGrouped{
IDKlinik: idKlinik,
NamaKlinik: namaKlinik,
Ruangan: []master.RuanganDetail{},
}
ruanganMap[idKlinik] = make(map[string]bool)
}
if item.IDRuangan.Valid && item.IDRuangan.String != "" {
idRuangan := item.IDRuangan.String
if !ruanganMap[idKlinik][idRuangan] {
namaRuangan := ""
if item.NamaRuangan.Valid {
namaRuangan = item.NamaRuangan.String
}
ruanganDetail := master.RuanganDetail{
IDRuangan: idRuangan,
NamaRuangan: namaRuangan,
}
klinikMap[idKlinik].Ruangan = append(klinikMap[idKlinik].Ruangan, ruanganDetail)
ruanganMap[idKlinik][idRuangan] = true
}
}
}
result := make([]*master.KlinikRuanganGrouped, 0, len(klinikMap))
for _, klinik := range klinikMap {
result = append(result, klinik)
}
return result
}
+949
View File
@@ -0,0 +1,949 @@
package master
import (
"api-service/internal/config"
"api-service/internal/database"
"api-service/internal/models"
modelsMaster "api-service/internal/models/master"
queryUtils "api-service/internal/utils/query"
"api-service/internal/utils/validation"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
"log"
"net/http"
"strings"
"sync"
"time"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
validate.RegisterValidation("satudata_status", validated)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
func validated(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
func NewPoliHandler() *PoliHandler {
// Initialize QueryBuilder with allowed columns list for security.
queryBuilder := queryUtils.NewQueryBuilder(queryUtils.DBTypePostgreSQL).
SetAllowedColumns([]string{
"mrc.id",
"mrc.name",
"mrc.code",
"mrc.quota",
"mrc.active",
"mrc.fk_ref_healthcare_type_id",
"mrc.fk_ref_service_type_id",
"rht.id",
"rht.name",
"rst.id",
"rst.name",
"mrcvt.id",
"mrcvt.fk_ms_registration_counter_id",
"mrcvt.fk_ref_visit_type_id",
"rvt.id",
"rvt.name",
"mrcpt.id",
"mrcpt.fk_ms_registration_counter_id",
"mrcpt.fk_ref_payment_type_id",
"rpt.id",
"rpt.name",
"mrchs.id",
"mrchs.fk_ms_registration_counter_id",
"mrchs.fk_ms_healthcare_service_id",
"mhss2.fk_ms_healthcare_service_id",
"master.ms_healthcare_service_shift",
"fk_ms_registration_counter_id",
"master.ms_healthcare_service_schedule",
"mhss3.fk_ms_healthcare_service_id",
"mhs.id",
"mhs.name",
"mhss2",
"mhss2.quota",
"mhss2.shift",
"namaloket",
"kodeloket",
"kuotaloket",
"loketaktif",
"jenisloket",
"tipeloket",
"tipevisit",
"pembayaran",
"namaklinik",
"idpayment",
"id",
"name",
"code",
"quota",
"active",
"shift",
"kuota",
})
return &PoliHandler{
db: db,
queryBuilder: queryBuilder,
validator: validation.NewDynamicValidator(queryBuilder),
}
}
type PoliHandler struct {
db database.Service
queryBuilder *queryUtils.QueryBuilder
validator *validation.DynamicValidator
}
func (h *PoliHandler) GetAnjungan(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
req := c.Param("jenis")
var tipeKlinik string
switch strings.ToLower(req) {
case "reguler", "REGULER":
tipeKlinik = "KLINIK REGULER"
case "vip":
tipeKlinik = "KLINIK VIP"
case "eksekutif", "EKSEKUTIF":
tipeKlinik = "KLINIK EKSEKUTIF"
default:
}
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
currentDate := time.Now().Format("2006-01-02")
query := queryUtils.DynamicQuery{
From: "master.ms_kiosk",
Aliases: "mk",
Fields: []queryUtils.SelectField{
{Expression: "mk.name", Alias: "tipeanjungan"},
{Expression: "mk.active", Alias: "active"},
{Expression: "mhs.id", Alias: "idklinik"},
{Expression: "mhs.name", Alias: "namaklinik"},
{Expression: "mhs.code", Alias: "code"},
{Expression: "mhss.ds_sd_spesialis", Alias: "spesialis"},
{Expression: "mhss.fk_sd_spesialis_id", Alias: "idspesialis"},
{Expression: "mks.day_of_week", Alias: "hari"},
{Expression: "mhss3.available_start_time", Alias: "mulai"},
{Expression: "mhss3.available_end_time", Alias: "selesai"},
{Expression: "mks.available_start_time", Alias: "mulaianjungan"},
{Expression: "mks.available_end_time", Alias: "selesaianjungan"},
{Expression: "mhss2.shift_number", Alias: "shift"},
{Expression: "mhss2.quota", Alias: "kuota"},
{Expression: "mrc.id", Alias: "idloket"},
{Expression: "mrc.name", Alias: "namaloket"},
{Expression: "mrc.code", Alias: "kodeloket"},
{Expression: "mrc.quota", Alias: "quota_loket"},
{Expression: "(mrc.quota - COALESCE(tmrcsq.seat_quota, 0))", Alias: "available_quota"},
{Expression: "rpt.id", Alias: "idpayment"},
{Expression: "rpt.name", Alias: "paymenttype"},
},
Joins: []queryUtils.Join{
{
Type: "LEFT",
Table: "master.ms_kiosk_healthcare_service",
Alias: "mkhs",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mk.id", Operator: queryUtils.OpEqual, Value: "mkhs.fk_ms_kiosk_id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_healthcare_service",
Alias: "mhs",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mkhs.fk_ms_healthcare_service_id", Operator: queryUtils.OpEqual, Value: "mhs.id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_healthcare_service_spesialis",
Alias: "mhss",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mhs.id", Operator: queryUtils.OpEqual, Value: "mhss.fk_ms_healthcare_service_id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_kiosk_schedule",
Alias: "mks",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mks.fk_ms_kiosk_id", Operator: queryUtils.OpEqual, Value: "mk.id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_healthcare_service_shift",
Alias: "mhss2",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mhss2.fk_ms_healthcare_service_id", Operator: queryUtils.OpEqual, Value: "mkhs.id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_registration_counter_healthcare_service",
Alias: "mrchs",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrchs.fk_ms_healthcare_service_id", Operator: queryUtils.OpEqual, Value: "mhs.id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_registration_counter",
Alias: "mrc",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "fk_ms_registration_counter_id", Operator: queryUtils.OpEqual, Value: "mrc.id"},
},
},
},
{
Type: "LEFT",
Table: "temporary.tm_registration_counter_seat_quota",
Alias: "tmrcsq",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "tmrcsq.fk_ms_registration_counter_id", Operator: queryUtils.OpEqual, Value: "mrc.id"},
{Column: "tmrcsq.date", Operator: queryUtils.OpEqual, Value: currentDate},
},
},
},
{
Type: "LEFT",
Table: "master.ms_healthcare_service_schedule",
Alias: "mhss3",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mhss3.fk_ms_healthcare_service_id", Operator: queryUtils.OpEqual, Value: "mhs.id"},
},
},
},
{
Type: "INNER",
Table: "master.ms_kiosk_payment_type",
Alias: "mkpt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mkpt.fk_ms_kiosk_id", Operator: queryUtils.OpEqual, Value: "mk.id"},
},
},
},
{
Type: "INNER",
Table: "master.ms_healthcare_service_payment_type",
Alias: "mhspt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mhspt.fk_ms_healthcare_service_id", Operator: queryUtils.OpEqual, Value: "mhs.id"},
{Column: "mkpt.fk_ref_payment_type_id", Operator: queryUtils.OpEqual, Value: "mhspt.fk_ref_payment_type_id"},
},
},
},
{
Type: "INNER",
Table: "reference.ref_payment_type",
Alias: "rpt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mkpt.fk_ref_payment_type_id", Operator: queryUtils.OpEqual, Value: "rpt.id"},
},
},
},
},
Filters: []queryUtils.FilterGroup{
{
Filters: []queryUtils.DynamicFilter{
{
Column: "mk.name",
Operator: queryUtils.OpEqual,
Value: tipeKlinik,
},
},
},
},
}
log.Println(query)
ListPoli, total, err := h.GetdataSpesialis(ctx, dbConn, query)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, modelsMaster.PoliklinikResponse{
Message: "Data spesialis tidak ditemukan",
Data: []*modelsMaster.PoliklinikGrouped{},
Meta: map[string]interface{}{
"total": 0,
"count": 0,
},
})
return
}
logger.Error("Failed to get spesialis data", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve data"})
return
}
groupedData := h.groupByKlinik(ListPoli)
response := modelsMaster.PoliklinikResponse{
Message: "Data Poli berhasil diambil",
Data: groupedData,
Meta: map[string]interface{}{
"total": total,
"count": len(ListPoli),
},
}
c.JSON(http.StatusOK, response)
}
func (h *PoliHandler) GetdataSpesialis(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) ([]*modelsMaster.ListPoli, int, error) {
var List []*modelsMaster.ListPoli
queryCtx, queryCancel := context.WithTimeout(ctx, 30*time.Second)
defer queryCancel()
logger.Info("Executing spesialis query")
err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &List)
if err != nil {
logger.Error("Failed to execute spesialis query", map[string]interface{}{
"error": err.Error(),
"query": query,
})
return nil, 0, fmt.Errorf("failed to execute spesialis query: %w", err)
}
total := len(List)
logger.Info("Query execution completed", map[string]interface{}{
"totalRecords": total,
"returnedRecords": len(List),
})
// Check if no data found
if total == 0 {
return nil, 0, sql.ErrNoRows
}
return List, total, nil
}
//func (h *PoliHandler) groupByKlinik(data []*modelsMaster.ListPoli) []*modelsMaster.PoliklinikGrouped {
// klinikMap := make(map[string]*modelsMaster.PoliklinikGrouped)
// spesialisMap := make(map[string]map[int]bool)
// jadwalMap := make(map[string]map[string]bool)
// loketMap := make(map[string]map[string]bool)
// paymentMap := make(map[string]map[string]bool)
//
// for _, item := range data {
// // Skip jika IDKlinik tidak valid atau kosong
// if !item.IDKlinik.Valid || item.IDKlinik.String == "" {
// continue
// }
//
// idKlinik := item.IDKlinik.String
//
// // Jika klinik belum ada di map, buat entry baru
// if _, exists := klinikMap[idKlinik]; !exists {
// namaKlinik := ""
// if item.NamaKlinik.Valid {
// namaKlinik = item.NamaKlinik.String
// }
//
// klinikMap[idKlinik] = &modelsMaster.PoliklinikGrouped{
// TipeAnjungan: item.TipeAnjungan,
// Active: item.Active,
// IDKlinik: idKlinik,
// NamaKlinik: namaKlinik,
// Shift: item.Shift,
// Kuota: item.Kuota,
// Code: item.Code,
// Jadwal: []modelsMaster.ScheduleDetail{},
// Spesialis: []modelsMaster.SpesialisDetail{},
// Loket: []modelsMaster.LoketDetail{},
// Payment: []modelsMaster.PaymentDetail{},
// }
//
// // Inisialisasi map untuk tracking
// spesialisMap[idKlinik] = make(map[int]bool)
// jadwalMap[idKlinik] = make(map[string]bool)
// loketMap[idKlinik] = make(map[string]bool)
// paymentMap[idKlinik] = make(map[string]bool)
// }
//
// // Tambahkan jadwal jika belum ada
// if item.Hari != "" && item.JamMulai != "" && item.JamSelesai != "" {
// jamOperasional := fmt.Sprintf("%s - %s", item.JamMulai, item.JamSelesai)
// jadwalKey := fmt.Sprintf("%s_%s", item.Hari, jamOperasional)
//
// // Cek apakah jadwal dengan hari dan jam operasional ini sudah ada
// if !jadwalMap[idKlinik][jadwalKey] {
// schedule := modelsMaster.ScheduleDetail{
// Hari: item.Hari,
// JamOperasional: jamOperasional,
// }
//
// klinikMap[idKlinik].Jadwal = append(klinikMap[idKlinik].Jadwal, schedule)
// jadwalMap[idKlinik][jadwalKey] = true
// }
// }
//
// // Tambahkan spesialis jika belum ada
// if item.IDSpesialis.Valid && item.IDSpesialis.Int64 != 0 && item.Spesialis.Valid && item.Spesialis.String != "" {
// idSpesialis := int(item.IDSpesialis.Int64)
//
// if !spesialisMap[idKlinik][idSpesialis] {
// spesialisDetail := modelsMaster.SpesialisDetail{
// IDSpesialis: idSpesialis,
// Spesialis: item.Spesialis.String,
// }
//
// klinikMap[idKlinik].Spesialis = append(klinikMap[idKlinik].Spesialis, spesialisDetail)
// spesialisMap[idKlinik][idSpesialis] = true
// }
// }
//
// if item.IDloket.Valid && item.IDloket.String != "" {
// idLoket := item.IDloket.String
//
// if !loketMap[idKlinik][idLoket] {
// namaLoket := ""
// if item.Namaloket.Valid {
// namaLoket = item.Namaloket.String
// }
//
// kodeLoket := ""
// if item.KodeLoket.Valid {
// kodeLoket = item.KodeLoket.String
// }
//
// loketDetail := modelsMaster.LoketDetail{
// IDLoket: idLoket,
// NamaLoket: namaLoket,
// KodeLoket: kodeLoket,
// }
//
// klinikMap[idKlinik].Loket = append(klinikMap[idKlinik].Loket, loketDetail)
// loketMap[idKlinik][idLoket] = true
// }
// }
// if item.PaymentType.Valid && item.PaymentType.String != "" {
// idPayment := int(item.IdPayment.Int64)
// paymentType := item.PaymentType.String
//
// if !paymentMap[idKlinik][paymentType] {
// paymentDetail := modelsMaster.PaymentDetail{
// IDPayment: idPayment,
// PaymentType: paymentType,
// }
//
// klinikMap[idKlinik].Payment = append(klinikMap[idKlinik].Payment, paymentDetail)
// paymentMap[idKlinik][paymentType] = true
// }
// }
// }
//
// result := make([]*modelsMaster.PoliklinikGrouped, 0, len(klinikMap))
// for _, klinik := range klinikMap {
// result = append(result, klinik)
// }
//
// return result
//}
func (h *PoliHandler) groupByKlinik(data []*modelsMaster.ListPoli) []*modelsMaster.PoliklinikGrouped {
klinikMap := make(map[string]*modelsMaster.PoliklinikGrouped)
spesialisMap := make(map[string]map[int]bool)
jadwalMap := make(map[string]map[string]bool)
loketMap := make(map[string][]modelsMaster.LoketDetail) // ✅ Array untuk menyimpan semua loket
paymentMap := make(map[string]map[string]bool)
for _, item := range data {
// Skip jika IDKlinik tidak valid atau kosong
if !item.IDKlinik.Valid || item.IDKlinik.String == "" {
continue
}
idKlinik := item.IDKlinik.String
// Jika klinik belum ada di map, buat entry baru
if _, exists := klinikMap[idKlinik]; !exists {
namaKlinik := ""
if item.NamaKlinik.Valid {
namaKlinik = item.NamaKlinik.String
}
klinikMap[idKlinik] = &modelsMaster.PoliklinikGrouped{
TipeAnjungan: item.TipeAnjungan,
Active: item.Active,
IDKlinik: idKlinik,
NamaKlinik: namaKlinik,
Shift: item.Shift,
Kuota: item.Kuota,
Code: item.Code,
Jadwal: []modelsMaster.ScheduleDetail{},
Spesialis: []modelsMaster.SpesialisDetail{},
Loket: []modelsMaster.LoketDetail{},
Payment: []modelsMaster.PaymentDetail{},
}
// Inisialisasi map untuk tracking
spesialisMap[idKlinik] = make(map[int]bool)
jadwalMap[idKlinik] = make(map[string]bool)
loketMap[idKlinik] = []modelsMaster.LoketDetail{}
paymentMap[idKlinik] = make(map[string]bool)
}
// Tambahkan jadwal jika belum ada
if item.Hari != "" && item.JamMulai != "" && item.JamSelesai != "" {
jamOperasional := fmt.Sprintf("%s - %s", item.JamMulai, item.JamSelesai)
jadwalKey := fmt.Sprintf("%s_%s", item.Hari, jamOperasional)
if !jadwalMap[idKlinik][jadwalKey] {
schedule := modelsMaster.ScheduleDetail{
Hari: item.Hari,
JamOperasional: jamOperasional,
}
klinikMap[idKlinik].Jadwal = append(klinikMap[idKlinik].Jadwal, schedule)
jadwalMap[idKlinik][jadwalKey] = true
}
}
// Tambahkan spesialis jika belum ada
if item.IDSpesialis.Valid && item.IDSpesialis.Int64 != 0 && item.Spesialis.Valid && item.Spesialis.String != "" {
idSpesialis := int(item.IDSpesialis.Int64)
if !spesialisMap[idKlinik][idSpesialis] {
spesialisDetail := modelsMaster.SpesialisDetail{
IDSpesialis: idSpesialis,
Spesialis: item.Spesialis.String,
}
klinikMap[idKlinik].Spesialis = append(klinikMap[idKlinik].Spesialis, spesialisDetail)
spesialisMap[idKlinik][idSpesialis] = true
}
}
// ✅ LOGIC BARU: Kumpulkan semua loket dulu
if item.IDloket.Valid && item.IDloket.String != "" {
idLoket := item.IDloket.String
// Cek apakah loket ini sudah ada di array
loketExists := false
for _, loket := range loketMap[idKlinik] {
if loket.IDLoket == idLoket {
loketExists = true
break
}
}
if !loketExists {
namaLoket := ""
if item.Namaloket.Valid {
namaLoket = item.Namaloket.String
}
kodeLoket := ""
if item.KodeLoket.Valid {
kodeLoket = item.KodeLoket.String
}
availableQuota := 0
if item.AvailableQuota.Valid {
availableQuota = int(item.AvailableQuota.Int64)
} else if item.QuotaLoket.Valid {
// Kalau available_quota NULL, berarti belum ada yang pakai (awal hari)
availableQuota = int(item.QuotaLoket.Int64)
}
loketDetail := modelsMaster.LoketDetail{
IDLoket: idLoket,
NamaLoket: namaLoket,
KodeLoket: kodeLoket,
AvailableQuota: availableQuota,
}
loketMap[idKlinik] = append(loketMap[idKlinik], loketDetail)
}
}
// Payment (tetap sama)
if item.PaymentType.Valid && item.PaymentType.String != "" {
idPayment := int(item.IdPayment.Int64)
paymentType := item.PaymentType.String
if !paymentMap[idKlinik][paymentType] {
paymentDetail := modelsMaster.PaymentDetail{
IDPayment: idPayment,
PaymentType: paymentType,
}
klinikMap[idKlinik].Payment = append(klinikMap[idKlinik].Payment, paymentDetail)
paymentMap[idKlinik][paymentType] = true
}
}
}
// ✅ LOGIC PEMILIHAN LOKET
for idKlinik, loketList := range loketMap {
if len(loketList) == 0 {
continue
}
// Cek apakah ada loket yang sudah terpakai (ada record quota hari ini)
adaYangTerpakai := false
for _, loket := range loketList {
// Kalau available_quota < quota_loket, berarti sudah ada yang pakai
for _, item := range data {
if item.IDKlinik.Valid && item.IDKlinik.String == idKlinik &&
item.IDloket.Valid && item.IDloket.String == loket.IDLoket {
if item.AvailableQuota.Valid && item.QuotaLoket.Valid {
if item.AvailableQuota.Int64 < item.QuotaLoket.Int64 {
adaYangTerpakai = true
break
}
}
}
}
if adaYangTerpakai {
break
}
}
if adaYangTerpakai {
// ✅ Sudah ada yang pakai tiket: Pilih 1 loket dengan quota terbanyak
var selectedLoket *modelsMaster.LoketDetail
maxQuota := -1
for i := range loketList {
if loketList[i].AvailableQuota > maxQuota {
maxQuota = loketList[i].AvailableQuota
selectedLoket = &loketList[i]
}
}
if selectedLoket != nil {
klinikMap[idKlinik].Loket = []modelsMaster.LoketDetail{*selectedLoket}
}
} else {
// ✅ Awal hari (belum ada yang pakai): Tampilkan semua loket
klinikMap[idKlinik].Loket = loketList
}
}
result := make([]*modelsMaster.PoliklinikGrouped, 0, len(klinikMap))
for _, klinik := range klinikMap {
result = append(result, klinik)
}
return result
}
func (h *PoliHandler) GetLoket(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
query := queryUtils.DynamicQuery{
From: "master.ms_registration_counter",
Aliases: "mrc",
Fields: []queryUtils.SelectField{
{Expression: "mrc.id", Alias: "idloket"},
{Expression: "mrc.name", Alias: "namaloket"},
{Expression: "mrc.code", Alias: "kodeloket"},
{Expression: "mrc.quota", Alias: "kuotaloket"},
{Expression: "mrc.active", Alias: "loketaktif"},
{Expression: "rht.name", Alias: "jenisloket"},
{Expression: "rst.name", Alias: "tipeloket"},
{Expression: "rvt.name", Alias: "tipevisit"},
{Expression: "rpt.name", Alias: "pembayaran"},
{Expression: "mhs.name", Alias: "namaklinik"},
{Expression: "mhs.id", Alias: "idklinik"},
},
Joins: []queryUtils.Join{
{
Type: "LEFT",
Table: "reference.ref_healthcare_type",
Alias: "rht",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrc.fk_ref_service_type_id", Operator: queryUtils.OpEqual, Value: "rht.id"},
},
},
},
{
Type: "LEFT",
Table: "reference.ref_service_type",
Alias: "rst",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrc.fk_ref_service_type_id", Operator: queryUtils.OpEqual, Value: "rst.id"},
},
},
},
{
Type: "INNER",
Table: "master.ms_registration_counter_visit_type",
Alias: "mrcvt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrcvt.fk_ms_registration_counter_id", Operator: queryUtils.OpEqual, Value: "mrc.id"},
},
},
},
{
Type: "LEFT",
Table: "reference.ref_visit_type",
Alias: "rvt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrcvt.fk_ref_visit_type_id", Operator: queryUtils.OpEqual, Value: "rvt.id"},
},
},
},
{
Type: "INNER",
Table: "master.ms_registration_counter_payment_type",
Alias: "mrcpt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrcpt.fk_ms_registration_counter_id", Operator: queryUtils.OpEqual, Value: "mrc.id"},
},
},
},
{
Type: "LEFT",
Table: "reference.ref_payment_type",
Alias: "rpt",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrcpt.fk_ref_payment_type_id", Operator: queryUtils.OpEqual, Value: "rpt.id"},
},
},
},
{
Type: "INNER",
Table: "master.ms_registration_counter_healthcare_service",
Alias: "mrchs",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrchs.fk_ms_registration_counter_id", Operator: queryUtils.OpEqual, Value: "mrc.id"},
},
},
},
{
Type: "LEFT",
Table: "master.ms_healthcare_service",
Alias: "mhs",
OnConditions: queryUtils.FilterGroup{
Filters: []queryUtils.DynamicFilter{
{Column: "mrchs.fk_ms_healthcare_service_id", Operator: queryUtils.OpEqual, Value: "mhs.id"},
},
},
},
},
}
log.Println(query)
ListLoket, total, err := h.GetdataLoket(ctx, dbConn, query)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, modelsMaster.LoketResponse{
Message: "Data loket tidak ditemukan",
Data: []*modelsMaster.LoketGrouped{},
Meta: map[string]interface{}{
"total": 0,
"count": 0,
},
})
return
}
logger.Error("Failed to get loket data", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve data"})
return
}
// GROUPING DATA DISINI
groupedData := h.groupByLoket(ListLoket)
response := modelsMaster.LoketResponse{
Message: "Data Loket berhasil diambil",
Data: groupedData,
Meta: map[string]interface{}{
"total_loket": len(groupedData),
"total_records": total,
},
}
c.JSON(http.StatusOK, response)
}
func (h *PoliHandler) GetdataLoket(ctx context.Context, dbConn *sqlx.DB, query queryUtils.DynamicQuery) ([]*modelsMaster.ListLoket, int, error) {
var List []*modelsMaster.ListLoket
queryCtx, queryCancel := context.WithTimeout(ctx, 30*time.Second)
defer queryCancel()
logger.Info("Executing loket query")
err := h.queryBuilder.ExecuteQuery(queryCtx, dbConn, query, &List)
if err != nil {
logger.Error("Failed to execute loket query", map[string]interface{}{
"error": err.Error(),
"query": query,
})
return nil, 0, fmt.Errorf("failed to execute loket query: %w", err)
}
total := len(List)
logger.Info("Query execution completed", map[string]interface{}{
"totalRecords": total,
"returnedRecords": len(List),
})
if total == 0 {
return nil, 0, sql.ErrNoRows
}
return List, total, nil
}
// FUNGSI UNTUK GROUPING - Group by Loket
func (h *PoliHandler) groupByLoket(data []*modelsMaster.ListLoket) []*modelsMaster.LoketGrouped {
loketMap := make(map[string]*modelsMaster.LoketGrouped)
visitMap := make(map[string]map[string]bool)
klinikMap := make(map[string]map[string]bool) // Ubah jadi tracking by IDKlinik
pembayaranMap := make(map[string]map[string]bool)
for _, item := range data {
if item.KodeLoket == "" {
continue
}
// Jika loket belum ada di map, buat entry baru
if _, exists := loketMap[item.KodeLoket]; !exists {
loketMap[item.KodeLoket] = &modelsMaster.LoketGrouped{
IdLoket: item.IdLoket,
TipeAnjungan: item.NamaLoket,
KodeLoket: item.KodeLoket,
KuotaLoket: item.KuotaLoket,
LoketAktif: item.LoketAktif,
JenisLoket: item.JenisLoket,
TipeLoket: item.TipeLoket,
TipeVisit: []modelsMaster.ListVisit{},
NamaKlinik: []modelsMaster.ListKlinik{},
NamaPembayaran: []modelsMaster.ListPembayaran{},
}
// Inisialisasi map untuk tracking
visitMap[item.KodeLoket] = make(map[string]bool)
klinikMap[item.KodeLoket] = make(map[string]bool)
pembayaranMap[item.KodeLoket] = make(map[string]bool)
}
if item.TipeVisit != "" {
if !visitMap[item.KodeLoket][item.TipeVisit] {
visit := modelsMaster.ListVisit{
TipeVisit: item.TipeVisit,
}
loketMap[item.KodeLoket].TipeVisit = append(loketMap[item.KodeLoket].TipeVisit, visit)
visitMap[item.KodeLoket][item.TipeVisit] = true
}
}
if item.IDKlinik != "" && item.NamaKlinik != "" {
if !klinikMap[item.KodeLoket][item.IDKlinik] {
klinik := modelsMaster.ListKlinik{
IDKlinik: item.IDKlinik,
NamaKlinik: item.NamaKlinik,
}
loketMap[item.KodeLoket].NamaKlinik = append(loketMap[item.KodeLoket].NamaKlinik, klinik)
klinikMap[item.KodeLoket][item.IDKlinik] = true
}
}
if item.Pembayaran != "" {
if !pembayaranMap[item.KodeLoket][item.Pembayaran] {
pembayaran := modelsMaster.ListPembayaran{
NamaPembayaran: item.Pembayaran,
}
loketMap[item.KodeLoket].NamaPembayaran = append(loketMap[item.KodeLoket].NamaPembayaran, pembayaran)
pembayaranMap[item.KodeLoket][item.Pembayaran] = true
}
}
}
// Convert map ke slice
result := make([]*modelsMaster.LoketGrouped, 0, len(loketMap))
for _, loket := range loketMap {
result = append(result, loket)
}
return result
}
+272
View File
@@ -2147,3 +2147,275 @@ func (h *PasienHandler) calculateMeta(limit, offset, total int) models.MetaRespo
CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0,
}
}
func (h *PasienHandler) GetPasienByNOMR(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Nomr := c.Param("nomr")
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
Pasien, err := h.Getpasien(ctx, dbConn, Nomr)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{
"error": "Pasien tidak ditemukan",
"nomr": Nomr,
})
return
}
logger.Error("Failed to get pasien", map[string]interface{}{
"error": err.Error(),
"nomr": Nomr,
})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve pasien data"})
return
}
logger.Info("Pasien data retrieved successfully", map[string]interface{}{
"nomr": Nomr,
"nama": Pasien.NamaLengkap,
})
c.JSON(http.StatusOK, gin.H{
"message": "Data pasien berhasil diambil",
"data": Pasien,
})
}
func (h *PasienHandler) Getpasien(ctx context.Context, dbConn *sqlx.DB, nomr string) (*pasienModels.PasienData, error) {
query := `
SELECT
mp.nomr,
mp.nama,
mp.notelp,
mp.jeniskelamin,
mp.tgllahir,
mp.alamat,
COALESCE(mp2.idprovinsi, 0) as idprovinsi,
COALESCE(mp2.namaprovinsi, '') as namaprovinsi,
COALESCE(mk2.idkota, 0) as idkota,
COALESCE(mk2.namakota, '') as namakota,
COALESCE(mk.idkecamatan, 0) as idkecamatan,
COALESCE(mk.namakecamatan, '') as namakecamatan,
COALESCE(mk3.idkelurahan, 0) as idkelurahan,
COALESCE(mk3.namakelurahan, '') as namakelurahan
FROM m_pasien mp
LEFT JOIN m_provinsi mp2 ON mp.kdprovinsi = mp2.idprovinsi
LEFT JOIN m_kecamatan mk ON mp.kdkecamatan = mk.idkecamatan
LEFT JOIN m_kota mk2 ON mk.idkota = mk2.idkota
LEFT JOIN m_kelurahan mk3 ON mp.kelurahan = mk3.idkelurahan
WHERE mp.nomr = $1
`
var pasien pasienModels.PasienData
err := dbConn.QueryRowContext(ctx, query, nomr).Scan(
&pasien.Nomr,
&pasien.NamaLengkap,
&pasien.NoTelp,
&pasien.JenisKelamin,
&pasien.TTL,
&pasien.Alamat,
&pasien.IdProvinsi,
&pasien.NamaProvinsi,
&pasien.IdKota,
&pasien.NamaKota,
&pasien.IdKecamatan,
&pasien.NamaKecamatan,
&pasien.IdKelurahan,
&pasien.NamaKelurahan,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, fmt.Errorf("failed to query pasien: %w", err)
}
logger.Info("Pasien query executed successfully", map[string]interface{}{
"nomr": nomr,
"nama": pasien.NamaLengkap,
})
return &pasien, nil
}
func (h *PasienHandler) SearchPasienByNama(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Get query parameter
nama := c.Query("nama")
// Validasi minimal 2 karakter untuk search
if len(nama) < 2 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Minimal 2 karakter untuk pencarian",
"message": "Silahkan ketik minimal 2 huruf",
})
return
}
dbConn, err := h.db.GetSQLXDB("postgres_simrs")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
// Search pasien by nama
pasienList, err := h.GetPasienByNamaPattern(ctx, dbConn, nama)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, gin.H{
"message": "Tidak ada pasien ditemukan",
"data": []pasienModels.PasienData{},
"meta": map[string]interface{}{
"total": 0,
"search_query": nama,
},
})
return
}
logger.Error("Failed to search pasien", map[string]interface{}{
"error": err.Error(),
"nama": nama,
})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to search pasien"})
return
}
logger.Info("Pasien search completed", map[string]interface{}{
"search_query": nama,
"total_found": len(pasienList),
})
c.JSON(http.StatusOK, gin.H{
"message": "Data pasien berhasil ditemukan",
"data": pasienList,
"meta": map[string]interface{}{
"total": len(pasienList),
"search_query": nama,
},
})
}
func (h *PasienHandler) GetPasienByNamaPattern(ctx context.Context, dbConn *sqlx.DB, namaPattern string) ([]*pasienModels.PasienData, error) {
searchPattern := "%" + namaPattern + "%"
query := `
SELECT
mp.nomr,
mp.nama,
mp.notelp,
mp.jeniskelamin,
mp.tgllahir,
mp.alamat,
mp2.idprovinsi,
mp2.namaprovinsi,
mk2.idkota,
mk2.namakota,
mk.idkecamatan,
mk.namakecamatan,
mk3.idkelurahan,
mk3.namakelurahan
FROM m_pasien mp
LEFT JOIN m_provinsi mp2 ON mp.kdprovinsi = mp2.idprovinsi
LEFT JOIN m_kecamatan mk ON mp.kdkecamatan = mk.idkecamatan
LEFT JOIN m_kota mk2 ON mk.idkota = mk2.idkota
LEFT JOIN m_kelurahan mk3 ON mp.kelurahan = mk3.idkelurahan
WHERE mp.nama ILIKE $1
ORDER BY mp.nama ASC
LIMIT 1
`
rows, err := dbConn.QueryContext(ctx, query, searchPattern)
if err != nil {
return nil, fmt.Errorf("failed to execute search query: %w", err)
}
defer rows.Close()
var pasienList []*pasienModels.PasienData
for rows.Next() {
var pasien pasienModels.PasienData
err := rows.Scan(
&pasien.Nomr,
&pasien.NamaLengkap,
&pasien.NoTelp,
&pasien.JenisKelamin,
&pasien.TTL,
&pasien.Alamat,
&pasien.IdProvinsi,
&pasien.NamaProvinsi,
&pasien.IdKota,
&pasien.NamaKota,
&pasien.IdKecamatan,
&pasien.NamaKecamatan,
&pasien.IdKelurahan,
&pasien.NamaKelurahan,
)
if err != nil {
return nil, fmt.Errorf("failed to scan row: %w", err)
}
// Clean strings
pasien.Nomr = cleanString(pasien.Nomr)
pasien.NamaLengkap = cleanString(pasien.NamaLengkap)
pasien.NoTelp = cleanString(pasien.NoTelp)
pasien.JenisKelamin = cleanString(pasien.JenisKelamin)
pasien.Alamat = cleanString(pasien.Alamat)
pasien.NamaProvinsi = cleanString(pasien.NamaProvinsi)
pasien.NamaKota = cleanString(pasien.NamaKota)
pasien.NamaKecamatan = cleanString(pasien.NamaKecamatan)
pasien.NamaKelurahan = cleanString(pasien.NamaKelurahan)
pasienList = append(pasienList, &pasien)
}
if err = rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
if len(pasienList) == 0 {
return nil, sql.ErrNoRows
}
logger.Info("Search query executed successfully", map[string]interface{}{
"pattern": namaPattern,
"found": len(pasienList),
})
return pasienList, nil
}
func cleanString(s string) string {
// Trim leading dan trailing whitespace
s = strings.TrimSpace(s)
// Replace newline, carriage return, dan tab di tengah string dengan single space
s = strings.ReplaceAll(s, "\n", " ")
s = strings.ReplaceAll(s, "\r", " ")
s = strings.ReplaceAll(s, "\t", " ")
// Replace multiple spaces dengan single space
s = strings.Join(strings.Fields(s), " ")
return s
}
+122
View File
@@ -0,0 +1,122 @@
package reference
import (
"api-service/internal/config"
"api-service/internal/database"
"api-service/internal/models"
modelsReference "api-service/internal/models/reference"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/jmoiron/sqlx"
"net/http"
"sync"
"time"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("retribusi_status", validateRetribusiStatus)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
func validateRetribusiStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
type referenceHandler struct {
db database.Service
}
func NewreferenceHandler() *referenceHandler {
return &referenceHandler{
db: db,
}
}
func (h *referenceHandler) Getvisitpasien(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second)
defer cancel()
dbAntrean, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Failed to connect postgres_antrean", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
Listvisit, err := h.Getrefvisitpasien(ctx, dbAntrean)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusOK, modelsReference.VisitStatusResponse{
Message: "Data visit status tidak ditemukan",
Data: []*modelsReference.VisitStatus{},
Meta: map[string]interface{}{
"total": 0,
},
})
return
}
logger.Error("Failed to get visit status", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve data"})
return
}
c.JSON(http.StatusOK, modelsReference.VisitStatusResponse{
Message: "Data visit status berhasil diambil",
Data: Listvisit,
Meta: map[string]interface{}{
"total": len(Listvisit),
},
})
}
func (h *referenceHandler) Getrefvisitpasien(ctx context.Context, dbConn *sqlx.DB) ([]*modelsReference.VisitStatus, error) {
query := `
SELECT
rvs.id as idvisit,
rvs.name as namastatus,
rvs.description as deskripsistatus,
rhs.id as idklinikstatus,
rhs.name as namastatusklinik
FROM reference.ref_visit_status rvs
LEFT JOIN reference.ref_healthcare_status rhs
ON rvs.fk_ref_healthcare_status_id = rhs.id
ORDER BY rvs.id ASC
`
var visitStatusList []*modelsReference.VisitStatus
err := dbConn.SelectContext(ctx, &visitStatusList, query)
if err != nil {
return nil, fmt.Errorf("failed to get visit status: %w", err)
}
if len(visitStatusList) == 0 {
return nil, sql.ErrNoRows
}
logger.Info("Visit status retrieved successfully", map[string]interface{}{
"total": len(visitStatusList),
})
return visitStatusList, nil
}
-1
View File
@@ -34,7 +34,6 @@ var (
validate *validator.Validate
)
// Initialize the database connection and validator once
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
+143
View File
@@ -0,0 +1,143 @@
package transaction
import (
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"github.com/jmoiron/sqlx"
)
func (h *TiketHandler) CheckAndLockQuota(ctx context.Context, dbConn *sqlx.DB, loketID int, date string) (quotaSeat int, usedQuota int, err error) {
query := `
SELECT mrc.quota,
COALESCE(tmrcsq.seat_quota, 0) as used_quota
FROM master.ms_registration_counter mrc
LEFT JOIN temporary.tm_registration_counter_seat_quota tmrcsq
ON tmrcsq.fk_ms_registration_counter_id = mrc.id
AND tmrcsq.date = $1
WHERE mrc.id = $2
FOR UPDATE OF mrc
`
err = dbConn.QueryRowContext(ctx, query, date, loketID).Scan(&quotaSeat, &usedQuota)
return quotaSeat, usedQuota, err
}
func (h *TiketHandler) IncrementUsedQuota(ctx context.Context, dbConn *sqlx.DB, loketID int, date string, newUUID string) (newUsedQuota int, err error) {
checkQuery := `
SELECT seat_quota
FROM temporary.tm_registration_counter_seat_quota
WHERE fk_ms_registration_counter_id = $1
AND date = $2
`
var currentQuota int
err = dbConn.QueryRowContext(ctx, checkQuery, loketID, date).Scan(&currentQuota)
if err == sql.ErrNoRows {
insertQuery := `
INSERT INTO temporary.tm_registration_counter_seat_quota
(id, fk_ms_registration_counter_id, date, seat_quota)
VALUES ($1, $2, $3, 1)
RETURNING seat_quota
`
err = dbConn.QueryRowContext(ctx, insertQuery, newUUID, loketID, date).Scan(&newUsedQuota)
if err != nil {
return 0, fmt.Errorf("failed to insert quota: %w", err)
}
return newUsedQuota, nil
} else if err != nil {
// Error lain
return 0, fmt.Errorf("failed to check existing quota: %w", err)
}
updateQuery := `
UPDATE temporary.tm_registration_counter_seat_quota
SET seat_quota = seat_quota + 1
WHERE fk_ms_registration_counter_id = $1
AND date = $2
RETURNING seat_quota
`
err = dbConn.QueryRowContext(ctx, updateQuery, loketID, date).Scan(&newUsedQuota)
if err != nil {
return 0, fmt.Errorf("failed to update quota: %w", err)
}
return newUsedQuota, nil
}
func (h *TiketHandler) MinQuota(ctx context.Context, dbConn *sqlx.DB, loketID int, date string) (newUsedQuota int, err error) {
// Step 1: Cek quota saat ini
checkQuery := `
SELECT seat_quota
FROM temporary.tm_registration_counter_seat_quota
WHERE fk_ms_registration_counter_id = $1
AND date = $2
`
var currentQuota int
err = dbConn.QueryRowContext(ctx, checkQuery, loketID, date).Scan(&currentQuota)
if err == sql.ErrNoRows {
// Tidak ada record, berarti quota belum pernah dipakai
logger.Warn("No quota record found, nothing to decrement", map[string]interface{}{
"loket_id": loketID,
"date": date,
})
return 0, fmt.Errorf("no quota record found for this loket and date")
} else if err != nil {
return 0, fmt.Errorf("failed to check existing quota: %w", err)
}
// Step 2: Validasi quota tidak boleh negatif
if currentQuota <= 0 {
logger.Warn("Quota already at zero, cannot decrement", map[string]interface{}{
"loket_id": loketID,
"current_quota": currentQuota,
})
return 0, fmt.Errorf("quota already at zero")
}
// Step 3: Kurangi quota
updateQuery := `
UPDATE temporary.tm_registration_counter_seat_quota
SET seat_quota = seat_quota - 1
WHERE fk_ms_registration_counter_id = $1
AND date = $2
AND seat_quota > 0
RETURNING seat_quota
`
err = dbConn.QueryRowContext(ctx, updateQuery, loketID, date).Scan(&newUsedQuota)
if err != nil {
return 0, fmt.Errorf("failed to decrement quota: %w", err)
}
logger.Info("Quota decremented successfully", map[string]interface{}{
"loket_id": loketID,
"new_used_quota": newUsedQuota,
})
return newUsedQuota, nil
}
func (h *TiketHandler) GetLoketIDByTicketID(ctx context.Context, dbConn *sqlx.DB, ticketID int64) (loketID int, err error) {
query := `
SELECT fk_ms_registration_counter_id
FROM "transaction".tr_patient_visit_healthcare_service
WHERE id = $1
`
err = dbConn.QueryRowContext(ctx, query, ticketID).Scan(&loketID)
if err != nil {
if err == sql.ErrNoRows {
return 0, fmt.Errorf("ticket not found")
}
return 0, fmt.Errorf("failed to get loket ID: %w", err)
}
return loketID, nil
}
+807
View File
@@ -0,0 +1,807 @@
package transaction
import (
"api-service/internal/config"
"api-service/internal/database"
"api-service/internal/models"
"api-service/internal/models/transaction"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"log"
"net/http"
"strconv"
"sync"
"time"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
func validated(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("retribusi_status", validated)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
type TiketHandler struct {
db database.Service
}
func NewtiketHandler() *TiketHandler {
return &TiketHandler{
db: db,
}
}
func (h *TiketHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{Error: errorMessage, Code: statusCode, Message: err.Error(), Timestamp: time.Now()})
}
func (h *TiketHandler) Createtiket(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var req transaction.CreateTicketRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
log.Println(req)
kodeID, err := strconv.Atoi(req.IDLoket)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Kode harus berupa angka"})
return
}
IDKlinik, err := strconv.Atoi(req.IdKlinik)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Kode harus berupa angka"})
return
}
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
newUUID := uuid.New().String()
currentDate := time.Now().Format("2006-01-02")
codeResult, err := h.GetKode(ctx, dbConn, kodeID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
Shift, err := h.GetShift(ctx, dbConn, IDKlinik)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
quotaSeat, usedQuota, err := h.CheckAndLockQuota(ctx, dbConn, kodeID, currentDate)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Loket tidak ditemukan"})
return
}
logger.Error("Failed to check quota", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check quota"})
return
}
availableQuota := quotaSeat - usedQuota
if availableQuota <= 0 {
logger.Warn("Quota loket sudah penuh", map[string]interface{}{
"id_loket": kodeID,
"quota_seat": quotaSeat,
"used_quota": usedQuota,
"available_quota": availableQuota,
})
c.JSON(http.StatusConflict, gin.H{
"error": "Quota loket sudah penuh",
"details": fmt.Sprintf("Quota tersedia: %d", availableQuota),
})
return
}
newUsedQuota, err := h.IncrementUsedQuota(ctx, dbConn, kodeID, currentDate, newUUID)
if err != nil {
logger.Error("Failed to update quota", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update quota"})
return
}
logger.Info("Quota updated successfully", map[string]interface{}{
"id_loket": req.IDLoket,
"new_used_quota": newUsedQuota,
"remaining_quota": quotaSeat - newUsedQuota,
})
barcodeNumber, err := h.GenerateVisitCode(ctx, dbConn.DB)
if err != nil {
logger.Error("Failed to generate barcode", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate barcode: " + err.Error()})
return
}
barcodeData, err := h.insertBarcode(ctx, dbConn.DB, barcodeNumber)
if err != nil {
logger.Error("Failed to insert barcode", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert barcode: " + err.Error()})
return
}
ticketNumber, err := h.generateTicketNumber(ctx, dbConn.DB, codeResult, currentDate)
if err != nil {
logger.Error("Failed to generate ticket", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate ticket: " + err.Error()})
return
}
idtiket, datetimeStart, err := h.insertTicket(ctx, dbConn.DB, barcodeData.IdBarcode, ticketNumber, codeResult, req.IdKlinik, req.IDLoket, req.IDpembayaran)
if err != nil {
logger.Error("Failed to insert ticket", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert ticket: " + err.Error()})
return
}
errVisit := h.insertcalling(ctx, dbConn.DB, req.Idklinikstatus, req.Statuspasien, idtiket)
if errVisit != nil {
logger.Error("Failed to insert calling ", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + err.Error()})
return
}
errVisitservice := h.insertvisit(ctx, dbConn.DB, req.Statuspasien, idtiket)
if errVisitservice != nil {
logger.Error("Failed to insert calling ", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + err.Error()})
return
}
errVisit2 := h.insertcalling(ctx, dbConn.DB, req.Idklinikstatus, req.Statuspasien2, idtiket)
if errVisit2 != nil {
logger.Error("Failed to insert calling ", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + err.Error()})
return
}
errVisitservice2 := h.insertvisit(ctx, dbConn.DB, req.Statuspasien2, idtiket)
if errVisitservice2 != nil {
logger.Error("Failed to insert calling ", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + err.Error()})
return
}
logger.Info("Barcode and ticket generated successfully", map[string]interface{}{
"barcode": barcodeNumber,
"ticket": req.IDLoket + ticketNumber,
"visit_id": barcodeData.IdBarcode,
})
resbarcodeData := &transaction.BarcodeData{
IdBarcode: barcodeData.IdBarcode,
Ticket: codeResult + ticketNumber,
Barcode: barcodeNumber,
NamaKlinik: req.NamaKlinik,
CreatedAt: barcodeData.CreatedAt,
Active: true,
Shift: "shift" + Shift,
Dokter: req.NamaDokter,
DatetimeStart: datetimeStart,
RemainingQuota: strconv.Itoa(quotaSeat - newUsedQuota),
}
c.JSON(http.StatusCreated, transaction.BarcodeResponse{
Message: "Barcode berhasil di-generate",
Data: resbarcodeData,
})
}
func (h *TiketHandler) GetKode(ctx context.Context, dbConn *sqlx.DB, kodeID int) (string, error) {
var code string
query := `SELECT mrc.code FROM master.ms_registration_counter mrc WHERE mrc.id = $1`
err := dbConn.QueryRowContext(ctx, query, kodeID).Scan(&code)
if err != nil {
if err == sql.ErrNoRows {
return "", fmt.Errorf("kode dengan id %d tidak ditemukan", kodeID)
}
return "", fmt.Errorf("failed to get kode: %w", err)
}
return code, nil
}
func (h *TiketHandler) GetShift(ctx context.Context, dbConn *sqlx.DB, IDKlinik int) (string, error) {
var shift string
query := ` select mhss.shift_number from master.ms_healthcare_service mhs
inner join master.ms_healthcare_service_shift mhss on mhss.fk_ms_healthcare_service_id = mhs.id
where mhs.id = $1`
err := dbConn.QueryRowContext(ctx, query, IDKlinik).Scan(&shift)
if err != nil {
if err == sql.ErrNoRows {
return "", fmt.Errorf("kode dengan id %d tidak ditemukan", IDKlinik)
}
return "", fmt.Errorf("failed to get kode: %w", err)
}
return shift, nil
}
func (h *TiketHandler) GenerateVisitCode(ctx context.Context, dbConn *sql.DB) (string, error) {
today := time.Now().Format("060102") // Format: YYMMDD
query := `
SELECT tpv.visit_code::TEXT
FROM transaction.tr_patient_visit tpv
WHERE tpv.visit_code::TEXT LIKE $1 || '%'
AND tpv.registration_datetime >= CURRENT_DATE
AND tpv.registration_datetime < CURRENT_DATE + INTERVAL '1 day'
ORDER BY tpv.visit_code DESC
LIMIT 1
`
var lastVisitCode string
err := dbConn.QueryRowContext(ctx, query, today).Scan(&lastVisitCode)
var newNumber int
if err == sql.ErrNoRows {
newNumber = 1
} else if err != nil {
return "", fmt.Errorf("failed to get last visit code: %w", err)
} else {
if len(lastVisitCode) >= 11 {
lastNumberStr := lastVisitCode[6:] // Ambil 5 digit terakhir
lastNumber, _ := strconv.Atoi(lastNumberStr)
newNumber = lastNumber + 1
} else {
newNumber = 1
}
}
// Format: YYMMDD + 5 digit dengan leading zero
newVisitCode := fmt.Sprintf("%s%05d", today, newNumber)
return newVisitCode, nil
}
func (h *TiketHandler) insertBarcode(ctx context.Context, dbConn *sql.DB, barcode string) (*transaction.BarcodeData, error) {
barcodeInt, err := strconv.ParseInt(barcode, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to convert barcode to integer: %w", err)
}
query := `
INSERT INTO transaction.tr_patient_visit
(visit_code, registration_datetime,active)
VALUES
($1, NOW(),true)
RETURNING id,visit_code, registration_datetime, active
`
var barcodeData transaction.BarcodeData
var visitCodeInt int64
err = dbConn.QueryRowContext(ctx, query, barcodeInt).Scan(
&barcodeData.IdBarcode,
&visitCodeInt,
&barcodeData.CreatedAt,
&barcodeData.Active,
)
if err != nil {
return nil, fmt.Errorf("failed to insert barcode: %w", err)
}
barcodeData.Barcode = strconv.FormatInt(visitCodeInt, 10)
return &barcodeData, nil
}
// func (h *TiketHandler) generateTicketNumber(ctx context.Context, dbConn *sql.DB, healthcareServiceCode string, currentDate string) (string, error) {
// query := `
// SELECT ticket
// FROM transaction.tr_patient_visit_healthcare_service tpvhs
// WHERE tpvhs.healtcare_service_code = $1
// AND DATE(tpvhs.datetime_start) = $2
// ORDER BY tpvhs.datetime_start DESC, tpvhs.id DESC
// LIMIT 1
// `
//
// var lastTicket string
// err := dbConn.QueryRowContext(ctx, query, healthcareServiceCode, currentDate).Scan(&lastTicket)
//
// var nextNumber int
//
// if err == sql.ErrNoRows {
// nextNumber = 1
// logger.Info("No ticket found for today, starting from 001", map[string]interface{}{
// "code": healthcareServiceCode,
// })
// } else if err != nil {
// return "", fmt.Errorf("failed to query last ticket: %w", err)
// } else {
// lastNumber, err := strconv.Atoi(lastTicket)
// if err != nil {
// nextNumber = 1
// logger.Warn("Failed to parse ticket number, resetting to 001", map[string]interface{}{
// "last_ticket": lastTicket,
// "error": err.Error(),
// })
// } else {
// nextNumber = lastNumber + 1
// }
//
// logger.Info("Found last ticket", map[string]interface{}{
// "last_ticket": lastTicket,
// "next_number": nextNumber,
// })
// }
//
// if nextNumber > 999 {
// return "", fmt.Errorf("maximum ticket limit reached for today (999)")
// }
//
// ticketNumber := fmt.Sprintf("%03d", nextNumber)
//
// return ticketNumber, nil
// }
func (h *TiketHandler) generateTicketNumber(ctx context.Context, dbConn *sql.DB, healthcareServiceCode string, currentDate string) (string, error) {
query := `
SELECT COALESCE(MAX(CAST(ticket AS INTEGER)), 0) as max_ticket
FROM transaction.tr_patient_visit_healthcare_service tpvhs
WHERE tpvhs.healtcare_service_code = $1
AND DATE(tpvhs.datetime_start) = $2
`
var maxTicket int
err := dbConn.QueryRowContext(ctx, query, healthcareServiceCode, currentDate).Scan(&maxTicket)
if err != nil {
return "", fmt.Errorf("failed to query max ticket: %w", err)
}
nextNumber := maxTicket + 1
logger.Info("Generating next ticket number", map[string]interface{}{
"code": healthcareServiceCode,
"date": currentDate,
"max_ticket": maxTicket,
"next_number": nextNumber,
})
if nextNumber > 999 {
return "", fmt.Errorf("maximum ticket limit reached for today (999)")
}
ticketNumber := fmt.Sprintf("%03d", nextNumber)
return ticketNumber, nil
}
func (h *TiketHandler) inserttiket(ctx context.Context, dbcoon *sql.DB, idticket int64, idklinik string) error {
query := `
INSERT INTO "transaction".tr_patient_visit_healthcare_service_healthcare_service
(fk_tr_patient_visit_healthcare_service_id, fk_ms_healthcare_service_id)
VALUES
($1, $2)
`
_, err := dbcoon.ExecContext(ctx, query, idticket, idklinik)
if err != nil {
return fmt.Errorf("failed to insert ticket relation: %w", err)
}
return nil
}
func (h *TiketHandler) insertcalling(ctx context.Context, dbcoon *sql.DB, IdHealthcarestatus string, IdVisitstatus string, idtiket int64) error {
query := `
INSERT INTO "transaction".tr_patient_visit_healthcare_service_calling
(fk_ref_healthcare_status_id, fk_ref_visit_status_id,fk_ref_healthcare_type_id,fk_tr_patient_visit_healthcare_service_id,active,datetime)
VALUES
($1,$2,$3,$4,TRUE,NOW())
`
_, err := dbcoon.ExecContext(ctx, query, IdHealthcarestatus, IdVisitstatus, IdHealthcarestatus, idtiket)
if err != nil {
return fmt.Errorf("failed to insert ticket relation: %w", err)
}
return nil
}
func (h *TiketHandler) insertvisit(ctx context.Context, dbcoon *sql.DB, IdVisitstatus string, idtiket int64) error {
query := `
INSERT INTO "transaction".tr_patient_visit_healthcare_service_visit_status
(fk_ref_visit_status_id,fk_tr_patient_visit_healthcare_service_id,datetime)
VALUES
($1,$2,NOW())
`
_, err := dbcoon.ExecContext(ctx, query, IdVisitstatus, idtiket)
if err != nil {
return fmt.Errorf("failed to insert ticket relation: %w", err)
}
return nil
}
func (h *TiketHandler) insertTicket(ctx context.Context, dbcoon *sql.DB, visitID string, ticket string, healthcareServiceCode string, idKlinik string, idloket string, idpembayaran string) (int64, time.Time, error) {
query := `
INSERT INTO transaction.tr_patient_visit_healthcare_service
(ticket, healtcare_service_code, fk_tr_patient_visit_id,fk_ms_healthcare_service_id,fk_ms_registration_counter_id,fk_ref_payment_type_id,datetime_start,active)
VALUES
($1,$2,$3,$4,$5,$6,NOW(),true)
RETURNING id, datetime_start
`
var id int64
var datetimeStart time.Time
err := dbcoon.QueryRowContext(ctx, query, ticket, healthcareServiceCode, visitID, idKlinik, idloket, idpembayaran).Scan(&id, &datetimeStart)
if err != nil {
return 0, time.Time{}, fmt.Errorf("failed to insert ticket: %w", err)
}
return id, datetimeStart, nil
}
func (h *TiketHandler) Updatecekin(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var req transaction.Checkin
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
log.Println(req)
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
ticketID, err := h.GetTicketIDByVisitCode(ctx, dbConn, req.Visitcode)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{
"error": "Ticket tidak ditemukan",
"visit_code": req.Visitcode,
})
return
}
logger.Error("Failed to get ticket", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve ticket"})
return
}
log.Println(ticketID)
errVisit := h.insertcalling2(ctx, dbConn.DB, req.Idklinikstatus, req.Statuspasien, ticketID)
if errVisit != nil {
logger.Error("Failed to insert calling", map[string]interface{}{"error": errVisit.Error()}) // PERBAIKAN: errVisit bukan err
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + errVisit.Error()}) // PERBAIKAN
return
}
errVisitservice := h.insertvisit2(ctx, dbConn.DB, req.Statuspasien, ticketID)
if errVisitservice != nil {
logger.Error("Failed to insert visit service", map[string]interface{}{"error": errVisitservice.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert visit service: " + errVisitservice.Error()}) // PERBAIKAN
return
}
errVisit2 := h.insertcalling2(ctx, dbConn.DB, req.Idklinikstatus2, req.Statuspasien2, ticketID)
if errVisit2 != nil {
logger.Error("Failed to insert calling 2", map[string]interface{}{"error": errVisit2.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling 2: " + errVisit2.Error()}) // PERBAIKAN
return
}
errVisitservice2 := h.insertvisit2(ctx, dbConn.DB, req.Statuspasien2, ticketID)
if errVisitservice2 != nil {
logger.Error("Failed to insert visit service 2", map[string]interface{}{"error": errVisitservice2.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert visit service 2: " + errVisitservice2.Error()}) // PERBAIKAN
return
}
err = h.UpdateCheckin(ctx, dbConn, &req)
if err != nil {
logger.Error("Failed to update check-in", map[string]interface{}{
"error": err.Error(),
"visitCode": req.Visitcode,
})
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to update check-in",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Check-in berhasil",
"data": req.Visitcode,
})
}
func (h *TiketHandler) UpdateCheckin(ctx context.Context, dbConn *sqlx.DB, req *transaction.Checkin) error {
query := `
UPDATE "transaction".tr_patient_visit_healthcare_service tpvhs
SET check_in = true, check_in_datetime = NOW()
WHERE fk_tr_patient_visit_id = (
SELECT id
FROM "transaction".tr_patient_visit tpv
WHERE tpv.visit_code = $1
)
`
result, err := dbConn.ExecContext(ctx, query, req.Visitcode)
if err != nil {
return fmt.Errorf("failed to execute update query: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("visit code tidak ditemukan: %s", req.Visitcode)
}
logger.Info("Check-in updated successfully", map[string]interface{}{
"visitCode": req.Visitcode,
"rowsAffected": rowsAffected,
})
return nil
}
func (h *TiketHandler) GetTicketIDByVisitCode(ctx context.Context, dbConn *sqlx.DB, visitCode string) (*transaction.TicketID, error) {
query := `
SELECT tpvhs.id
FROM "transaction".tr_patient_visit tpv
INNER JOIN "transaction".tr_patient_visit_healthcare_service tpvhs
ON tpvhs.fk_tr_patient_visit_id = tpv.id
WHERE tpv.visit_code = $1
and tpvhs.active = true
`
var ticketID transaction.TicketID
err := dbConn.QueryRowContext(ctx, query, visitCode).Scan(&ticketID.ID)
if err != nil {
if err == sql.ErrNoRows {
return nil, sql.ErrNoRows
}
return nil, fmt.Errorf("failed to get ticket ID: %w", err)
}
logger.Info("Ticket ID retrieved successfully", map[string]interface{}{
"visitCode": visitCode,
"ticketID": ticketID.ID,
})
return &ticketID, nil
}
func (h *TiketHandler) insertcalling2(ctx context.Context, dbcoon *sql.DB, IdHealthcarestatus string, IdVisitstatus string, idtiket *transaction.TicketID) error {
query := `
INSERT INTO "transaction".tr_patient_visit_healthcare_service_calling
(fk_ref_healthcare_status_id, fk_ref_visit_status_id, fk_ref_healthcare_type_id, fk_tr_patient_visit_healthcare_service_id, active, datetime)
VALUES
($1, $2, $3, $4, TRUE, NOW())
`
_, err := dbcoon.ExecContext(ctx, query, IdHealthcarestatus, IdVisitstatus, IdHealthcarestatus, idtiket.ID)
if err != nil {
return fmt.Errorf("failed to insert calling: %w", err)
}
return nil
}
func (h *TiketHandler) insertvisit2(ctx context.Context, dbcoon *sql.DB, IdVisitstatus string, idtiket *transaction.TicketID) error {
query := `
INSERT INTO "transaction".tr_patient_visit_healthcare_service_visit_status
(fk_ref_visit_status_id, fk_tr_patient_visit_healthcare_service_id, datetime)
VALUES
($1, $2, NOW())
`
_, err := dbcoon.ExecContext(ctx, query, IdVisitstatus, idtiket.ID)
if err != nil {
return fmt.Errorf("failed to insert visit status: %w", err)
}
return nil
}
func (h *TiketHandler) CreateStatus(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var req transaction.UpdateTiket
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
log.Println(req)
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
ticketID, err := h.GetTicketIDByVisitCode(ctx, dbConn, req.Visitcode)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{
"error": "Ticket tidak ditemukan",
"visit_code": req.Visitcode,
})
return
}
logger.Error("Failed to get ticket", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve ticket"})
return
}
errVisit := h.insertcalling2(ctx, dbConn.DB, req.Idklinikstatus, req.Statuspasien, ticketID)
if errVisit != nil {
logger.Error("Failed to insert calling", map[string]interface{}{"error": errVisit.Error()}) // PERBAIKAN: errVisit bukan err
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + errVisit.Error()}) // PERBAIKAN
return
}
errVisitservice := h.insertvisit2(ctx, dbConn.DB, req.Statuspasien, ticketID)
if errVisitservice != nil {
logger.Error("Failed to insert visit service", map[string]interface{}{"error": errVisitservice.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert visit service: " + errVisitservice.Error()}) // PERBAIKAN
return
}
errVisit2 := h.insertcalling2(ctx, dbConn.DB, req.Idklinikstatus2, req.Statuspasien2, ticketID)
if errVisit2 != nil {
logger.Error("Failed to insert calling 2", map[string]interface{}{"error": errVisit2.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling 2: " + errVisit2.Error()}) // PERBAIKAN
return
}
errVisitservice2 := h.insertvisit2(ctx, dbConn.DB, req.Statuspasien2, ticketID)
if errVisitservice2 != nil {
logger.Error("Failed to insert visit service 2", map[string]interface{}{"error": errVisitservice2.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert visit service 2: " + errVisitservice2.Error()}) // PERBAIKAN
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Update berhasil",
"data": req.Visitcode,
})
}
func (h *TiketHandler) SelesaiStatus(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var req transaction.TiketSelesai
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
log.Println(req)
loketID, err := strconv.Atoi(req.IDLoket)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID Loket harus berupa angka"})
return
}
dbConn, err := h.db.GetSQLXDB("postgres_antrean")
if err != nil {
logger.Error("Database connection failed", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database connection failed"})
return
}
ticketID, err := h.GetTicketIDByVisitCode(ctx, dbConn, req.Visitcode)
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{
"error": "Ticket tidak ditemukan",
"visit_code": req.Visitcode,
})
return
}
logger.Error("Failed to get ticket", map[string]interface{}{"error": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve ticket"})
return
}
errVisit := h.insertcalling2(ctx, dbConn.DB, req.Idklinikstatus, req.Statuspasien, ticketID)
if errVisit != nil {
logger.Error("Failed to insert calling", map[string]interface{}{"error": errVisit.Error()}) // PERBAIKAN: errVisit bukan err
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert calling: " + errVisit.Error()}) // PERBAIKAN
return
}
errVisitservice := h.insertvisit2(ctx, dbConn.DB, req.Statuspasien, ticketID)
if errVisitservice != nil {
logger.Error("Failed to insert visit service", map[string]interface{}{"error": errVisitservice.Error()}) // PERBAIKAN
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert visit service: " + errVisitservice.Error()}) // PERBAIKAN
return
}
currentDate := time.Now().Format("2006-01-02")
newUsedQuota, err := h.MinQuota(ctx, dbConn, loketID, currentDate)
if err != nil {
logger.Error("Failed to return quota", map[string]interface{}{
"error": err.Error(),
"loket_id": loketID,
"ticket_id": ticketID,
"visit_code": req.Visitcode,
})
} else {
logger.Info("Quota returned successfully", map[string]interface{}{
"loket_id": loketID,
"new_used_quota": newUsedQuota,
"ticket_id": ticketID,
"visit_code": req.Visitcode,
})
}
c.JSON(http.StatusOK, gin.H{
"message": "Update status selesai berhasil",
"data": req.Visitcode,
"Quota": newUsedQuota,
})
}