first commit
This commit is contained in:
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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("aSeat, &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(¤tQuota)
|
||||
|
||||
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(¤tQuota)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user