Doctor
This commit is contained in:
686
internal/handlers/doctor/doctor.go
Normal file
686
internal/handlers/doctor/doctor.go
Normal file
@@ -0,0 +1,686 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
models "api-service/internal/models"
|
||||
"api-service/internal/models/doctor"
|
||||
"api-service/internal/utils/validation"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
doctordb database.Service
|
||||
doctoronce sync.Once
|
||||
doctorvalidate *validator.Validate
|
||||
)
|
||||
|
||||
// Initialize the database connection and validator
|
||||
func init() {
|
||||
doctoronce.Do(func() {
|
||||
doctordb = database.New(config.LoadConfig())
|
||||
doctorvalidate = validator.New()
|
||||
doctorvalidate.RegisterValidation("doctor_status", validateDoctorStatus)
|
||||
if doctordb == nil {
|
||||
log.Fatal("Failed to initialize database connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Custom validation for doctor status
|
||||
func validateDoctorStatus(fl validator.FieldLevel) bool {
|
||||
return models.IsValidStatus(fl.Field().String())
|
||||
}
|
||||
|
||||
// DoctorHandler handles doctor services
|
||||
type DoctorHandler struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewDoctorHandler creates a new DoctorHandler
|
||||
func NewDoctorHandler() *DoctorHandler {
|
||||
return &DoctorHandler{
|
||||
db: doctordb,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDoctor godoc
|
||||
// @Summary Get doctor with pagination and optional aggregation
|
||||
// @Description Returns a paginated list of doctors with optional summary statistics
|
||||
// @Tags Doctor
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit (max 100)" default(10)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Param include_summary query bool false "Include aggregation summary" default(false)
|
||||
// @Param status query string false "Filter by status"
|
||||
// @Param search query string false "Search in multiple fields"
|
||||
// @Success 200 {object} doctor.DoctorGetResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/doctors [get]
|
||||
func (h *DoctorHandler) GetDoctor(c *gin.Context) {
|
||||
// Parse pagination parameters
|
||||
limit, offset, err := h.parsePaginationParams(c)
|
||||
if err != nil {
|
||||
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse filter parameters
|
||||
filter := h.parseFilterParams(c)
|
||||
includeAggregation := c.Query("include_summary") == "true"
|
||||
|
||||
// Get database connection
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Execute concurrent operations
|
||||
var (
|
||||
items []doctor.Doctor
|
||||
total int
|
||||
aggregateData *models.AggregateData
|
||||
wg sync.WaitGroup
|
||||
errChan = make(chan error, 3)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// Fetch total count
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
|
||||
mu.Lock()
|
||||
errChan <- fmt.Errorf("failed to get total count: %w", err)
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Fetch main data
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := h.fetchDoctors(ctx, dbConn, filter, limit, offset)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to fetch data: %w", err)
|
||||
} else {
|
||||
items = result
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Fetch aggregation data if requested
|
||||
if includeAggregation {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := h.getAggregateData(ctx, dbConn, filter)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
|
||||
} else {
|
||||
aggregateData = result
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
meta := h.calculateMeta(limit, offset, total)
|
||||
response := doctor.DoctorGetResponse{
|
||||
Message: "Data doctor berhasil diambil",
|
||||
Data: items,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
if includeAggregation && aggregateData != nil {
|
||||
response.Summary = aggregateData
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetDoctorByID godoc
|
||||
// @Summary Get Doctor by ID
|
||||
// @Description Returns a single doctor by ID
|
||||
// @Tags Doctor
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Doctor ID (UUID)"
|
||||
// @Success 200 {object} doctor.DoctorGetByIDResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||
// @Failure 404 {object} models.ErrorResponse "Doctor not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/doctor/{id} [get]
|
||||
func (h *DoctorHandler) GetDoctorByPoliID(c *gin.Context) {
|
||||
id := c.Param("poli")
|
||||
|
||||
// Validate UUID format
|
||||
if _, err := uuid.Parse(id); err != nil {
|
||||
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetDB("db_")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
item, err := h.getDoctorByPoliID(ctx, dbConn, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
h.respondError(c, "Doctor not found", err, http.StatusNotFound)
|
||||
} else {
|
||||
h.logAndRespondError(c, "Failed to get doctor", err, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response := doctor.DoctorGetByIDResponse{
|
||||
Message: "doctor details retrieved successfully",
|
||||
Data: item,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetDoctorStats godoc
|
||||
// @Summary Get doctor statistics
|
||||
// @Description Returns comprehensive statistics about doctor data
|
||||
// @Tags Doctor
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status query string false "Filter statistics by status"
|
||||
// @Success 200 {object} models.AggregateData "Statistics data"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/doctors/stats [get]
|
||||
func (h *DoctorHandler) GetDoctorStats(c *gin.Context) {
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
filter := h.parseFilterParams(c)
|
||||
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Statistik doctor berhasil diambil",
|
||||
"data": aggregateData,
|
||||
})
|
||||
}
|
||||
|
||||
// Database operations
|
||||
func (h *DoctorHandler) getDoctorByPoliID(ctx context.Context, dbConn *sql.DB, id string) (*doctor.Doctor, error) {
|
||||
query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM master.ms_doctor WHERE id = $1 AND status != 'deleted'"
|
||||
row := dbConn.QueryRowContext(ctx, query, id)
|
||||
|
||||
var item doctor.Doctor
|
||||
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (h *DoctorHandler) createDoctor(ctx context.Context, dbConn *sql.DB, req *doctor.DoctorCreateRequest) (*doctor.Doctor, error) {
|
||||
id := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
query := "INSERT INTO master.ms_doctor (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
|
||||
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
|
||||
|
||||
var item doctor.Doctor
|
||||
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create doctor: %w", err)
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// func (h *DoctorHandler) updateDoctor(ctx context.Context, dbConn *sql.DB, req *doctor.DoctorUpdateRequest) (*doctor.Doctor, error) {
|
||||
// now := time.Now()
|
||||
|
||||
// query := "UPDATE master.ms_doctor SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
|
||||
// row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
|
||||
|
||||
// var item doctor.Doctor
|
||||
// err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("failed to update doctor: %w", err)
|
||||
// }
|
||||
|
||||
// return &item, nil
|
||||
// }
|
||||
|
||||
// func (h *DoctorHandler) deleteDoctor(ctx context.Context, dbConn *sql.DB, id string) error {
|
||||
// now := time.Now()
|
||||
// query := "UPDATE master.ms_doctor SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'"
|
||||
|
||||
// result, err := dbConn.ExecContext(ctx, query, id, now)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to delete doctor: %w", err)
|
||||
// }
|
||||
|
||||
// rowsAffected, err := result.RowsAffected()
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
// }
|
||||
|
||||
// if rowsAffected == 0 {
|
||||
// return sql.ErrNoRows
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (h *DoctorHandler) fetchDoctors(ctx context.Context, dbConn *sql.DB, filter doctor.DoctorFilter, limit, offset int) ([]doctor.Doctor, error) {
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
query := fmt.Sprintf(`SELECT md.id, md.name
|
||||
FROM master.ms_doctor md
|
||||
left join master.ms_doctor_healthcare_service mdhs on mdhs.fk_ms_doctor_id = md.id
|
||||
left join master.ms_healthcare_service mhs on mdhs.fk_ms_healthcare_service_id = mhs.id
|
||||
WHERE %s LIMIT $%d OFFSET $%d`, whereClause, len(args)+1, len(args)+2)
|
||||
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := dbConn.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch doctors query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]doctor.Doctor, 0, limit)
|
||||
for rows.Next() {
|
||||
item, err := h.scanDoctor(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan Doctor failed: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully fetched %d doctors with filters applied", len(items))
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Optimized scanning function
|
||||
func (h *DoctorHandler) scanDoctor(rows *sql.Rows) (doctor.Doctor, error) {
|
||||
var item doctor.Doctor
|
||||
|
||||
// Scan into individual fields to handle nullable types properly
|
||||
err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.Name,
|
||||
)
|
||||
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (h *DoctorHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter doctor.DoctorFilter, total *int) error {
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM master.ms_doctor WHERE %s", whereClause)
|
||||
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
|
||||
return fmt.Errorf("total count query failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get comprehensive aggregate data dengan filter support
|
||||
func (h *DoctorHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter doctor.DoctorFilter) (*models.AggregateData, error) {
|
||||
aggregate := &models.AggregateData{
|
||||
ByStatus: make(map[string]int),
|
||||
}
|
||||
|
||||
// Build where clause untuk filter
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
|
||||
// Use concurrent execution untuk performance
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
// 1. Count by status
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM master.ms_doctor WHERE %s GROUP BY status ORDER BY status", whereClause)
|
||||
|
||||
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("status query failed: %w", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
mu.Lock()
|
||||
for rows.Next() {
|
||||
var status string
|
||||
var count int
|
||||
if err := rows.Scan(&status, &count); err != nil {
|
||||
mu.Unlock()
|
||||
errChan <- fmt.Errorf("status scan failed: %w", err)
|
||||
return
|
||||
}
|
||||
aggregate.ByStatus[status] = count
|
||||
switch status {
|
||||
case "active":
|
||||
aggregate.TotalActive = count
|
||||
case "draft":
|
||||
aggregate.TotalDraft = count
|
||||
case "inactive":
|
||||
aggregate.TotalInactive = count
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
errChan <- fmt.Errorf("status iteration error: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. Get last updated time dan today statistics
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Last updated
|
||||
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM master.ms_doctor WHERE %s AND date_updated IS NOT NULL", whereClause)
|
||||
var lastUpdated sql.NullTime
|
||||
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
|
||||
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Today statistics
|
||||
today := time.Now().Format("2006-01-02")
|
||||
todayStatsQuery := fmt.Sprintf(`
|
||||
SELECT
|
||||
SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today,
|
||||
SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today
|
||||
FROM master.ms_doctor
|
||||
WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause)
|
||||
|
||||
todayArgs := append(args, today)
|
||||
var createdToday, updatedToday int
|
||||
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
|
||||
errChan <- fmt.Errorf("today stats query failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
if lastUpdated.Valid {
|
||||
aggregate.LastUpdated = &lastUpdated.Time
|
||||
}
|
||||
aggregate.CreatedToday = createdToday
|
||||
aggregate.UpdatedToday = updatedToday
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Wait for all goroutines
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
// Enhanced error handling
|
||||
func (h *DoctorHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
log.Printf("[ERROR] %s: %v", message, err)
|
||||
h.respondError(c, message, err, statusCode)
|
||||
}
|
||||
|
||||
func (h *DoctorHandler) 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(),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse pagination parameters dengan validation yang lebih ketat
|
||||
func (h *DoctorHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
|
||||
limit := 10 // Default limit
|
||||
offset := 0 // Default offset
|
||||
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
parsedLimit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
|
||||
}
|
||||
if parsedLimit <= 0 {
|
||||
return 0, 0, fmt.Errorf("limit must be greater than 0")
|
||||
}
|
||||
if parsedLimit > 100 {
|
||||
return 0, 0, fmt.Errorf("limit cannot exceed 100")
|
||||
}
|
||||
limit = parsedLimit
|
||||
}
|
||||
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
parsedOffset, err := strconv.Atoi(offsetStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
|
||||
}
|
||||
if parsedOffset < 0 {
|
||||
return 0, 0, fmt.Errorf("offset cannot be negative")
|
||||
}
|
||||
offset = parsedOffset
|
||||
}
|
||||
|
||||
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
|
||||
return limit, offset, nil
|
||||
}
|
||||
|
||||
func (h *DoctorHandler) parseFilterParams(c *gin.Context) doctor.DoctorFilter {
|
||||
filter := doctor.DoctorFilter{}
|
||||
|
||||
if status := c.Query("active"); status != "" {
|
||||
if models.IsValidStatus(status) {
|
||||
filter.Status = &status
|
||||
}
|
||||
}
|
||||
|
||||
if search := c.Query("search"); search != "" {
|
||||
filter.Search = &search
|
||||
}
|
||||
|
||||
if poli := c.Query("mhs.id"); poli != "" {
|
||||
filter.Poli = &poli
|
||||
}
|
||||
|
||||
// Parse date filters
|
||||
// if dateFromStr := c.Query("date_from"); dateFromStr != "" {
|
||||
// if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
|
||||
// filter.DateFrom = &dateFrom
|
||||
// }
|
||||
// }
|
||||
|
||||
// if dateToStr := c.Query("date_to"); dateToStr != "" {
|
||||
// if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
|
||||
// filter.DateTo = &dateTo
|
||||
// }
|
||||
// }
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// Build WHERE clause dengan filter parameters
|
||||
func (h *DoctorHandler) buildWhereClause(filter doctor.DoctorFilter) (string, []interface{}) {
|
||||
conditions := []string{"1=1"}
|
||||
args := []interface{}{}
|
||||
paramCount := 1
|
||||
|
||||
if filter.Status != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("active = $%d", paramCount))
|
||||
args = append(args, *filter.Status)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
if filter.Search != nil {
|
||||
searchCondition := fmt.Sprintf("mhs.id = $%d", paramCount)
|
||||
conditions = append(conditions, searchCondition)
|
||||
searchTerm := "%" + *filter.Search + "%"
|
||||
args = append(args, searchTerm)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
// if filter.DateFrom != nil {
|
||||
// conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
|
||||
// args = append(args, *filter.DateFrom)
|
||||
// paramCount++
|
||||
// }
|
||||
|
||||
// if filter.DateTo != nil {
|
||||
// conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
|
||||
// args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
|
||||
// paramCount++
|
||||
// }
|
||||
|
||||
return strings.Join(conditions, " AND "), args
|
||||
}
|
||||
|
||||
func (h *DoctorHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
||||
totalPages := 0
|
||||
currentPage := 1
|
||||
if limit > 0 {
|
||||
totalPages = (total + limit - 1) / limit // Ceiling division
|
||||
currentPage = (offset / limit) + 1
|
||||
}
|
||||
|
||||
return models.MetaResponse{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
CurrentPage: currentPage,
|
||||
HasNext: offset+limit < total,
|
||||
HasPrev: offset > 0,
|
||||
}
|
||||
}
|
||||
|
||||
// validateDoctorSubmission performs validation for duplicate entries and daily submission limits
|
||||
// func (h *DoctorHandler) validateDoctorSubmission(ctx context.Context, dbConn *sql.DB, req *doctor.DoctorCreateRequest) error {
|
||||
// // Import the validation utility
|
||||
// validator := validation.NewDuplicateValidator(dbConn)
|
||||
|
||||
// // Use default configuration
|
||||
// config := validation.ValidationConfig{
|
||||
// TableName: "master.ms_doctor",
|
||||
// IDColumn: "id",
|
||||
// StatusColumn: "status",
|
||||
// DateColumn: "date_created",
|
||||
// ActiveStatuses: []string{"active", "draft"},
|
||||
// }
|
||||
|
||||
// // Validate duplicate entries with active status for today
|
||||
// err := validator.ValidateDuplicate(ctx, config, "dummy_id")
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("validation failed: %w", err)
|
||||
// }
|
||||
|
||||
// // Validate once per day submission
|
||||
// err = validator.ValidateOncePerDay(ctx, "master.ms_doctor", "id", "date_created", "daily_limit")
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("daily submission limit exceeded: %w", err)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Example usage of the validation utility with custom configuration
|
||||
// func (h *DoctorHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *doctor.DoctorCreateRequest) error {
|
||||
// // Create validator instance
|
||||
// validator := validation.NewDuplicateValidator(dbConn)
|
||||
|
||||
// // Use custom configuration
|
||||
// config := validation.ValidationConfig{
|
||||
// TableName: "master.ms_doctor",
|
||||
// IDColumn: "id",
|
||||
// StatusColumn: "status",
|
||||
// DateColumn: "date_created",
|
||||
// ActiveStatuses: []string{"active", "draft"},
|
||||
// AdditionalFields: map[string]interface{}{
|
||||
// "name": req.Name,
|
||||
// },
|
||||
// }
|
||||
|
||||
// // Validate with custom fields
|
||||
// fields := map[string]interface{}{
|
||||
// "name": *req.Name,
|
||||
// }
|
||||
|
||||
// err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("custom validation failed: %w", err)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// GetLastSubmissionTime example
|
||||
func (h *DoctorHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
|
||||
validator := validation.NewDuplicateValidator(dbConn)
|
||||
return validator.GetLastSubmissionTime(ctx, "master.ms_doctor", "id", "date_created", identifier)
|
||||
}
|
||||
61
internal/models/doctor/doctor.go
Normal file
61
internal/models/doctor/doctor.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Doctor represents the data structure for the doctor table
|
||||
// with proper null handling and optimized JSON marshaling
|
||||
type Doctor struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Name sql.NullString `json:"name,omitempty" db:"name"`
|
||||
}
|
||||
|
||||
// Custom JSON marshaling untuk Doctor agar NULL values tidak muncul di response
|
||||
func (r Doctor) MarshalJSON() ([]byte, error) {
|
||||
type Alias Doctor
|
||||
aux := &struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
if r.Name.Valid {
|
||||
aux.Name = &r.Name.String
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper methods untuk mendapatkan nilai yang aman
|
||||
func (r *Doctor) GetName() string {
|
||||
if r.Name.Valid {
|
||||
return r.Name.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response struct untuk GET by ID
|
||||
type DoctorGetByIDResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Doctor `json:"data"`
|
||||
}
|
||||
|
||||
// Enhanced GET response dengan pagination dan aggregation
|
||||
type DoctorGetResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []Doctor `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
Summary *models.AggregateData `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// Filter struct untuk query parameters
|
||||
type DoctorFilter struct {
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
Poli *string `json:"mhs.id,omitempty" form:"mhs.id"`
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package patient
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
// Search represents the data structure for the search table
|
||||
// with proper null handling and optimized JSON marshaling
|
||||
type Search struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
|
||||
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
|
||||
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
|
||||
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
|
||||
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
|
||||
Name sql.NullString `json:"name,omitempty" db:"name"`
|
||||
}
|
||||
|
||||
// Custom JSON marshaling untuk Search agar NULL values tidak muncul di response
|
||||
func (r Search) MarshalJSON() ([]byte, error) {
|
||||
type Alias Search
|
||||
aux := &struct {
|
||||
Sort *int `json:"sort,omitempty"`
|
||||
UserCreated *string `json:"user_created,omitempty"`
|
||||
DateCreated *time.Time `json:"date_created,omitempty"`
|
||||
UserUpdated *string `json:"user_updated,omitempty"`
|
||||
DateUpdated *time.Time `json:"date_updated,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
|
||||
if r.Sort.Valid {
|
||||
sort := int(r.Sort.Int32)
|
||||
aux.Sort = &sort
|
||||
}
|
||||
if r.UserCreated.Valid {
|
||||
aux.UserCreated = &r.UserCreated.String
|
||||
}
|
||||
if r.DateCreated.Valid {
|
||||
aux.DateCreated = &r.DateCreated.Time
|
||||
}
|
||||
if r.UserUpdated.Valid {
|
||||
aux.UserUpdated = &r.UserUpdated.String
|
||||
}
|
||||
if r.DateUpdated.Valid {
|
||||
aux.DateUpdated = &r.DateUpdated.Time
|
||||
}
|
||||
if r.Name.Valid {
|
||||
aux.Name = &r.Name.String
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper methods untuk mendapatkan nilai yang aman
|
||||
func (r *Search) GetName() string {
|
||||
if r.Name.Valid {
|
||||
return r.Name.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
// Filter struct untuk query parameters
|
||||
type SearchFilter struct {
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
authHandlers "api-service/internal/handlers/auth"
|
||||
doctorDoctorHandlers "api-service/internal/handlers/doctor"
|
||||
healthcareHealthcareHandlers "api-service/internal/handlers/healthcare"
|
||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||
kioskListkioskHandlers "api-service/internal/handlers/kiosk"
|
||||
@@ -831,5 +832,14 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
healthcareHealthcareGroup.GET("/stats", healthcareHealthcareHandler.GetHealthcareStats)
|
||||
}
|
||||
|
||||
// Doctor endpoints
|
||||
doctorDoctorHandler := doctorDoctorHandlers.NewDoctorHandler()
|
||||
doctorDoctorGroup := v1.Group("/doctor")
|
||||
{
|
||||
doctorDoctorGroup.GET("", doctorDoctorHandler.GetDoctor)
|
||||
// doctorDoctorGroup.GET("/:id", doctorDoctorHandler.GetDoctorByPoliID)
|
||||
// doctorDoctorGroup.GET("/stats", doctorDoctorHandler.GetDoctorStats)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user