From 7b6987ebbdaf567a44e5fdd0ebfd215fece64de7 Mon Sep 17 00:00:00 2001 From: "achmad.nauval.0510" Date: Mon, 1 Dec 2025 15:40:11 +0700 Subject: [PATCH] pembuatan service crud visit register pasien --- internal/handlers/visit/patientvisith.go | 649 +++++++++++++++++++++++ internal/models/visit/visit.go | 203 +++++++ internal/routes/v1/routes.go | 10 + 3 files changed, 862 insertions(+) create mode 100644 internal/handlers/visit/patientvisith.go create mode 100644 internal/models/visit/visit.go diff --git a/internal/handlers/visit/patientvisith.go b/internal/handlers/visit/patientvisith.go new file mode 100644 index 00000000..ff0b53c3 --- /dev/null +++ b/internal/handlers/visit/patientvisith.go @@ -0,0 +1,649 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models" + pasienVisit "api-service/internal/models/visit" + "context" + "database/sql" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" +) + +var ( + patientVisitDB database.Service + patientVisitOnce sync.Once + patientVisitValidate *validator.Validate +) + +// Initialize the database connection and validator +func init() { + patientVisitOnce.Do(func() { + patientVisitDB = database.New(config.LoadConfig()) + patientVisitValidate = validator.New() + if patientVisitDB == nil { + log.Fatal("Failed to initialize database connection for patient visit") + } + }) +} + +// PatientVisitHandler handles patient visit services +type PatientVisitHandler struct { + db database.Service +} + +// NewPatientVisitHandler creates a new PatientVisitHandler +func NewPatientVisitHandler() *PatientVisitHandler { + return &PatientVisitHandler{ + db: patientVisitDB, + } +} + +// GetPatientVisits godoc +// @Summary Get patient visits with pagination +// @Description Returns a paginated list of patient visits +// @Tags PatientVisit +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param active query bool false "Filter by active status" +// @Param check_in query bool false "Filter by check-in status" +// @Param fk_ms_doctor_id query int false "Filter by doctor ID" +// @Success 200 {object} models.TrPatientVisitGetListResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient-visit [get] +func (h *PatientVisitHandler) GetPatientVisits(c *gin.Context) { + limit, offset, err := h.parsePaginationParams(c) + if err != nil { + h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) + return + } + + filter := h.parseFilterParams(c) + dbConn, err := h.db.GetDB("default") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + var ( + items []pasienVisit.TrPatientVisit + total int + wg sync.WaitGroup + mu sync.Mutex + ) + + wg.Add(1) + go func() { + defer wg.Done() + if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { + mu.Lock() + log.Printf("failed to get total count: %v", err) + mu.Unlock() + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.fetchPatientVisits(ctx, dbConn, filter, limit, offset) + mu.Lock() + if err != nil { + log.Printf("failed to fetch data: %v", err) + } else { + items = result + } + mu.Unlock() + }() + + wg.Wait() + + meta := h.calculateMeta(limit, offset, total) + response := pasienVisit.TrPatientVisitGetListResponse{ + Message: "Data patient visit berhasil diambil", + Data: items, + Meta: meta, + } + + c.JSON(http.StatusOK, response) +} + +// CreatePatientVisit godoc +// @Summary Create a new patient visit +// @Description Creates a new patient visit record +// @Tags PatientVisit +// @Accept json +// @Produce json +// @Param request body models.TrPatientVisitCreateRequest true "Patient Visit creation request" +// @Success 201 {object} models.TrPatientVisitCreateResponse "Patient Visit created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient-visit [post] +func (h *PatientVisitHandler) CreatePatientVisit(c *gin.Context) { + var req pasienVisit.TrPatientVisitCreateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + if err := patientVisitValidate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("default") + 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.createPatientVisit(ctx, dbConn, &req) + if err != nil { + h.logAndRespondError(c, "Failed to create patient visit", err, http.StatusInternalServerError) + return + } + + response := pasienVisit.TrPatientVisitCreateResponse{ + Message: "Patient visit berhasil dibuat", + Data: item, + } + + c.JSON(http.StatusCreated, response) +} + +// UpdatePatientVisit godoc +// @Summary Update an existing patient visit (full update) +// @Description Updates an existing patient visit record with all fields +// @Tags PatientVisit +// @Accept json +// @Produce json +// @Param id path int true "Patient Visit ID" +// @Param request body models.TrPatientVisitUpdateRequest true "Patient Visit update request" +// @Success 200 {object} models.TrPatientVisitUpdateResponse "Patient Visit updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Patient Visit not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient-visit/{id} [put] +func (h *PatientVisitHandler) UpdatePatientVisit(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseInt(idStr, 10, 32) + if err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + var req pasienVisit.TrPatientVisitUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + req.ID = int32(id) + if err := patientVisitValidate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("default") + 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.updatePatientVisit(ctx, dbConn, &req) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Patient Visit not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update patient visit", err, http.StatusInternalServerError) + } + return + } + + response := pasienVisit.TrPatientVisitUpdateResponse{ + Message: "Patient visit berhasil diperbarui", + Data: item, + } + + c.JSON(http.StatusOK, response) +} + +// PatchPatientVisit godoc +// @Summary Partially update an existing patient visit +// @Description Partially updates an existing patient visit record. Only send the fields you want to change. +// @Tags PatientVisit +// @Accept json +// @Produce json +// @Param id path int true "Patient Visit ID" +// @Param request body models.TrPatientVisitPatchRequest true "Patient Visit patch request" +// @Success 200 {object} models.TrPatientVisitPatchResponse "Patient Visit patched successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Patient Visit not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /patient-visit/{id} [patch] +func (h *PatientVisitHandler) PatchPatientVisit(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.ParseInt(idStr, 10, 32) + if err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("default") + 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() + + err = h.cancelPatientVisit(ctx, dbConn, int32(id)) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Patient Visit not found", err, http.StatusNotFound) + } else if err.Error() == "already canceled" { + h.respondError(c, "Patient visit is already canceled", err, http.StatusBadRequest) + } else { + h.logAndRespondError(c, "Failed to cancel patient visit", err, http.StatusInternalServerError) + } + return + } + + response := pasienVisit.TrPatientVisitDeleteResponse{ + Message: "Patient visit berhasil dibatalkan", + ID: idStr, + } + + c.JSON(http.StatusOK, response) +} + + +func (h *PatientVisitHandler) cancelPatientVisit(ctx context.Context, dbConn *sql.DB, id int32) error { + + var currentActiveStatus sql.NullBool + checkQuery := `SELECT active FROM "transaction".tr_patient_visit WHERE id = $1` + err := dbConn.QueryRowContext(ctx, checkQuery, id).Scan(¤tActiveStatus) + + if err != nil { + if err == sql.ErrNoRows { + return sql.ErrNoRows + } + + return fmt.Errorf("failed to check patient visit status: %w", err) + } + + if currentActiveStatus.Valid && !currentActiveStatus.Bool { + return fmt.Errorf("already canceled") + } + + query := `UPDATE "transaction".tr_patient_visit SET active = false WHERE id = $1` + result, err := dbConn.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to cancel patient visit: %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 *PatientVisitHandler) getPatientVisitByID(ctx context.Context, dbConn *sql.DB, id int32) (*pasienVisit.TrPatientVisit, error) { + query := `SELECT id, barcode, registration_date, service_date, check_in_date, check_in, active, fk_ms_doctor_id + FROM "transaction".tr_patient_visit WHERE id = $1` + row := dbConn.QueryRowContext(ctx, query, id) + + var item pasienVisit.TrPatientVisit + err := row.Scan( + &item.ID, + &item.Barcode, + &item.RegistrationDate, + &item.ServiceDate, + &item.CheckInDate, + &item.CheckIn, + &item.Active, + &item.FKMsDoctorID, + ) + if err != nil { + return nil, err + } + + return &item, nil +} + +func (h *PatientVisitHandler) createPatientVisit(ctx context.Context, dbConn *sql.DB, req *pasienVisit.TrPatientVisitCreateRequest) (*pasienVisit.TrPatientVisit, error) { + query := `INSERT INTO "transaction".tr_patient_visit + (barcode, registration_date, service_date, check_in_date, check_in, active, fk_ms_doctor_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, barcode, registration_date, service_date, check_in_date, check_in, active, fk_ms_doctor_id` + + row := dbConn.QueryRowContext( + ctx, + query, + req.Barcode, + req.RegistrationDate, + req.ServiceDate, + req.CheckInDate, + req.CheckIn, + req.Active, + req.FKMsDoctorID, + ) + + var item pasienVisit.TrPatientVisit + err := row.Scan( + &item.ID, + &item.Barcode, + &item.RegistrationDate, + &item.ServiceDate, + &item.CheckInDate, + &item.CheckIn, + &item.Active, + &item.FKMsDoctorID, + ) + if err != nil { + return nil, fmt.Errorf("failed to create patient visit: %w", err) + } + + return &item, nil +} + +func (h *PatientVisitHandler) updatePatientVisit(ctx context.Context, dbConn *sql.DB, req *pasienVisit.TrPatientVisitUpdateRequest) (*pasienVisit.TrPatientVisit, error) { + query := `UPDATE "transaction".tr_patient_visit + SET barcode = $2, registration_date = $3, service_date = $4, check_in_date = $5, check_in = $6, active = $7, fk_ms_doctor_id = $8 + WHERE id = $1 + RETURNING id, barcode, registration_date, service_date, check_in_date, check_in, active, fk_ms_doctor_id` + + row := dbConn.QueryRowContext( + ctx, + query, + req.ID, + req.Barcode, + req.RegistrationDate, + req.ServiceDate, + req.CheckInDate, + req.CheckIn, + req.Active, + req.FKMsDoctorID, + ) + + var item pasienVisit.TrPatientVisit + err := row.Scan( + &item.ID, + &item.Barcode, + &item.RegistrationDate, + &item.ServiceDate, + &item.CheckInDate, + &item.CheckIn, + &item.Active, + &item.FKMsDoctorID, + ) + if err != nil { + return nil, fmt.Errorf("failed to update patient visit: %w", err) + } + + return &item, nil +} + +func (h *PatientVisitHandler) patchPatientVisit(ctx context.Context, dbConn *sql.DB, id int32, req *pasienVisit.TrPatientVisitPatchRequest) error { + setParts := []string{} + args := []interface{}{} + argIndex := 1 + + + if req.Active != nil { + setParts = append(setParts, "active = false") + args = append(args, *req.Active) + argIndex++ + } + + if len(setParts) == 0 { + return fmt.Errorf("no fields to update") + } + + query := fmt.Sprintf(`UPDATE "transaction".tr_patient_visit SET %s WHERE id = $%d`, strings.Join(setParts, ", "), argIndex) + args = append(args, id) + + result, err := dbConn.ExecContext(ctx, query, args...) + if err != nil { + return fmt.Errorf("failed to patch patient visit: %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 *PatientVisitHandler) deletePatientVisit(ctx context.Context, dbConn *sql.DB, id int32) error { + query := `UPDATE "transaction".tr_patient_visit SET active = false WHERE id = $1` + result, err := dbConn.ExecContext(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to delete patient visit: %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 *PatientVisitHandler) fetchPatientVisits(ctx context.Context, dbConn *sql.DB, filter pasienVisit.TrPatientVisitFilter, limit, offset int) ([]pasienVisit.TrPatientVisit, error) { + whereClause, args := h.buildWhereClause(filter) + query := fmt.Sprintf(`SELECT id, barcode, registration_date, service_date, check_in_date, check_in, active, fk_ms_doctor_id + FROM "transaction".tr_patient_visit + WHERE %s ORDER BY id 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 patient visits query failed: %w", err) + } + defer rows.Close() + + items := make([]pasienVisit.TrPatientVisit, 0, limit) + for rows.Next() { + var item pasienVisit.TrPatientVisit + err := rows.Scan( + &item.ID, + &item.Barcode, + &item.RegistrationDate, + &item.ServiceDate, + &item.CheckInDate, + &item.CheckIn, + &item.Active, + &item.FKMsDoctorID, + ) + if err != nil { + return nil, fmt.Errorf("scan patient visit failed: %w", err) + } + items = append(items, item) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + return items, nil +} + +func (h *PatientVisitHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter pasienVisit.TrPatientVisitFilter, total *int) error { + whereClause, args := h.buildWhereClause(filter) + countQuery := fmt.Sprintf("SELECT COUNT(*) FROM \"transaction\".tr_patient_visit 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 +} + +// --- Helper Methods --- +func (h *PatientVisitHandler) 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 *PatientVisitHandler) 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 *PatientVisitHandler) parsePaginationParams(c *gin.Context) (int, int, error) { + limit := 10 + offset := 0 + + 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 || parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit must be between 1 and 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 + } + + return limit, offset, nil +} + +func (h *PatientVisitHandler) parseFilterParams(c *gin.Context) pasienVisit.TrPatientVisitFilter { + filter := pasienVisit.TrPatientVisitFilter{} + + if activeStr := c.Query("active"); activeStr != "" { + if active, err := strconv.ParseBool(activeStr); err == nil { + filter.Active = &active + } + } + if checkInStr := c.Query("registration_date"); checkInStr != "" { + if checkIn, err := strconv.ParseBool(checkInStr); err == nil { + filter.CheckIn = &checkIn + } + } + if doctorStr := c.Query("fk_ms_doctor_id"); doctorStr != "" { + if doctorID, err := strconv.ParseInt(doctorStr, 10, 32); err == nil { + doctorID32 := int32(doctorID) + filter.FKMsDoctorID = &doctorID32 + } + } + + // // Date range filter + // if dateFromStr := c.Query("registration_date_from"); dateFromStr != "" { + // if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { + // filter.RegistrationDateFrom = &dateFrom + // } + // } + // if dateToStr := c.Query("registration_date_to"); dateToStr != "" { + // if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { + // filter.RegistrationDateTo = &dateTo + // } + // } + + if dateStr := c.Query("date"); dateStr != "" { + if date, err := time.Parse("2006-01-02", dateStr); err == nil { + startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location()) + endOfDay := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, date.Location()) + + filter.RegistrationDateFrom = &startOfDay + filter.RegistrationDateTo = &endOfDay + return filter + } + } + + return filter +} + +func (h *PatientVisitHandler) buildWhereClause(filter pasienVisit.TrPatientVisitFilter) (string, []interface{}) { + conditions := []string{"1=1"} + args := []interface{}{} + paramCount := 1 + + if filter.RegistrationDateFrom != nil { + conditions = append(conditions, fmt.Sprintf("registration_date >= $%d", paramCount)) + args = append(args, *filter.RegistrationDateFrom) + paramCount++ + } + if filter.RegistrationDateTo != nil { + conditions = append(conditions, fmt.Sprintf("registration_date <= $%d", paramCount)) + args = append(args, *filter.RegistrationDateTo) + paramCount++ + } + + + return strings.Join(conditions, " AND "), args +} + +func (h *PatientVisitHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages := 0 + currentPage := 1 + if limit > 0 { + totalPages = (total + limit - 1) / limit + currentPage = (offset / limit) + 1 + } + + return models.MetaResponse{ + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, + CurrentPage: currentPage, + HasNext: offset+limit < total, + HasPrev: offset > 0, + } +} \ No newline at end of file diff --git a/internal/models/visit/visit.go b/internal/models/visit/visit.go new file mode 100644 index 00000000..5db87f37 --- /dev/null +++ b/internal/models/visit/visit.go @@ -0,0 +1,203 @@ +package models + +import ( + "api-service/internal/models" + "database/sql" + "encoding/json" + "time" +) + +// ================================================================================= +// 1. MODEL DATABASE (Core Struct) +// ================================================================================= + +// TrPatientVisit mewakili tabel "transaction".tr_patient_visit +type TrPatientVisit struct { + ID int32 `json:"id" db:"id"` + Barcode sql.NullInt16 `json:"barcode,omitempty" db:"barcode"` + RegistrationDate sql.NullTime `json:"registration_date,omitempty" db:"registration_date"` + ServiceDate sql.NullTime `json:"service_date,omitempty" db:"service_date"` + CheckInDate sql.NullTime `json:"check_in_date,omitempty" db:"check_in_date"` + CheckIn sql.NullBool `json:"check_in,omitempty" db:"check_in"` + Active sql.NullBool `json:"active,omitempty" db:"active"` + FKMsDoctorID sql.NullInt32 `json:"fk_ms_doctor_id,omitempty" db:"fk_ms_doctor_id"` +} + +// MarshalJSON adalah custom JSON marshaller untuk menghasilkan output yang bersih. +func (t TrPatientVisit) MarshalJSON() ([]byte, error) { + type Alias TrPatientVisit + aux := &struct { + Barcode *int16 `json:"barcode,omitempty"` + RegistrationDate *time.Time `json:"registration_date,omitempty"` + ServiceDate *time.Time `json:"service_date,omitempty"` + CheckInDate *time.Time `json:"check_in_date,omitempty"` + CheckIn *bool `json:"check_in,omitempty"` + Active *bool `json:"active,omitempty"` + FKMsDoctorID *int32 `json:"fk_ms_doctor_id,omitempty"` + *Alias + }{ + Alias: (*Alias)(&t), + } + + if t.Barcode.Valid { + aux.Barcode = &t.Barcode.Int16 + } + if t.RegistrationDate.Valid { + aux.RegistrationDate = &t.RegistrationDate.Time + } + if t.ServiceDate.Valid { + aux.ServiceDate = &t.ServiceDate.Time + } + if t.CheckInDate.Valid { + aux.CheckInDate = &t.CheckInDate.Time + } + if t.CheckIn.Valid { + aux.CheckIn = &t.CheckIn.Bool + } + if t.Active.Valid { + aux.Active = &t.Active.Bool + } + if t.FKMsDoctorID.Valid { + aux.FKMsDoctorID = &t.FKMsDoctorID.Int32 + } + + return json.Marshal(aux) +} + +// --- Helper Methods untuk Akses Nilai yang Aman --- +func (t *TrPatientVisit) GetBarcode() int16 { + if t.Barcode.Valid { + return t.Barcode.Int16 + } + return 0 +} + +func (t *TrPatientVisit) GetRegistrationDate() time.Time { + if t.RegistrationDate.Valid { + return t.RegistrationDate.Time + } + return time.Time{} +} + +func (t *TrPatientVisit) GetServiceDate() time.Time { + if t.ServiceDate.Valid { + return t.ServiceDate.Time + } + return time.Time{} +} + +func (t *TrPatientVisit) GetCheckInDate() time.Time { + if t.CheckInDate.Valid { + return t.CheckInDate.Time + } + return time.Time{} +} + +func (t *TrPatientVisit) GetCheckIn() bool { + if t.CheckIn.Valid { + return t.CheckIn.Bool + } + return false +} + +func (t *TrPatientVisit) GetActive() bool { + if t.Active.Valid { + return t.Active.Bool + } + return false +} + +func (t *TrPatientVisit) GetFKMsDoctorID() int32 { + if t.FKMsDoctorID.Valid { + return t.FKMsDoctorID.Int32 + } + return 0 +} + + +// ================================================================================= +// 2. MODEL UNTUK REQUEST & RESPONSE API +// ================================================================================= + +// --- Response Models --- +type TrPatientVisitGetResponse struct { + Message string `json:"message"` + Data *TrPatientVisit `json:"data"` +} + +type TrPatientVisitGetListResponse struct { + Message string `json:"message"` + Data []TrPatientVisit `json:"data"` + Meta models.MetaResponse `json:"meta"` +} + +type TrPatientVisitCreateResponse struct { + Message string `json:"message"` + Data *TrPatientVisit `json:"data"` +} + +type TrPatientVisitUpdateResponse struct { + Message string `json:"message"` + Data *TrPatientVisit `json:"data"` +} + +type TrPatientVisitPatchResponse struct { + Message string `json:"message"` + Data *TrPatientVisit `json:"data"` +} + +type TrPatientVisitDeleteResponse struct { + Message string `json:"message"` + ID string `json:"id"` +} + +// --- Request Models --- + +// TrPatientVisitCreateRequest adalah model untuk request POST (create) +// Field yang nullable di DB menggunakan pointer agar opsional di request body. +type TrPatientVisitCreateRequest struct { + Barcode *int16 `json:"barcode,omitempty"` + RegistrationDate *time.Time `json:"registration_date"` + ServiceDate *time.Time `json:"service_date,omitempty"` + CheckInDate *time.Time `json:"check_in_date,omitempty"` + CheckIn *bool `json:"check_in,omitempty"` + Active *bool `json:"active,omitempty"` + FKMsDoctorID *int32 `json:"fk_ms_doctor_id,omitempty"` +} + +// TrPatientVisitUpdateRequest adalah model untuk request PUT (update penuh) +// PUT biasanya mengharuskan semua field yang akan diupdate dikirim. +// ID diambil dari path parameter. +type TrPatientVisitUpdateRequest struct { + ID int32 `json:"id"` // ID dari path + Barcode *int16 `json:"barcode,omitempty"` + RegistrationDate *time.Time `json:"registration_date"` + ServiceDate *time.Time `json:"service_date,omitempty"` + CheckInDate *time.Time `json:"check_in_date,omitempty"` + CheckIn *bool `json:"check_in,omitempty"` + Active *bool `json:"active,omitempty"` + FKMsDoctorID *int32 `json:"fk_ms_doctor_id,omitempty"` +} + +// TrPatientVisitPatchRequest adalah model untuk request PATCH (update parsial) +// PATCH hanya mengirim field yang ingin diubah. +// Penggunaan pointer memungkinkan kita membedakan antara "tidak dikirim" dan "dikirim dengan nilai nol". +type TrPatientVisitPatchRequest struct { + Barcode *int16 `json:"barcode,omitempty"` + RegistrationDate *time.Time `json:"registration_date,omitempty"` + ServiceDate *time.Time `json:"service_date,omitempty"` + CheckInDate *time.Time `json:"check_in_date,omitempty"` + CheckIn *bool `json:"check_in,omitempty"` + Active *bool `json:"active,omitempty"` + FKMsDoctorID *int32 `json:"fk_ms_doctor_id,omitempty"` +} + +type TrPatientVisitFilter struct { + Active *bool `json:"active,omitempty" form:"active"` + CheckIn *bool `json:"check_in,omitempty" form:"check_in"` + FKMsDoctorID *int32 `json:"fk_ms_doctor_id,omitempty" form:"fk_ms_doctor_id"` + RegistrationDateFrom *time.Time `json:"registration_date_from,omitempty" form:"registration_date_from"` + RegistrationDateTo *time.Time `json:"registration_date_to,omitempty" form:"registration_date_to"` + Date *time.Time `json:"date,omitempty" form:"date"` + Search *string `json:"search,omitempty" form:"search"` +} \ No newline at end of file diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 274ad39c..6f2e62d0 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -10,6 +10,7 @@ import ( pesertaHandlers "api-service/internal/handlers/peserta" registerCounterkHandlers "api-service/internal/handlers/registrasi" retribusiHandlers "api-service/internal/handlers/retribusi" + patientVisitHandlers "api-service/internal/handlers/visit" "api-service/internal/handlers/websocket" websocketHandlers "api-service/internal/handlers/websocket" "api-service/internal/middleware" @@ -820,5 +821,14 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { regitsterCounterGroup.GET("/stats", registerCounterHandler.GetRegistrationCounterStats) } + patientVisitHandler := patientVisitHandlers.NewPatientVisitHandler() + patientVisitGroup := v1.Group("/visit") + { + patientVisitGroup.GET("", patientVisitHandler.GetPatientVisits) + patientVisitGroup.POST("", patientVisitHandler.CreatePatientVisit) + patientVisitGroup.PATCH("/:id/cancel",patientVisitHandler.PatchPatientVisit ) + patientVisitGroup.PUT("/:id", patientVisitHandler.UpdatePatientVisit) + } + return router }