first commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
.git
|
||||
.gitignore
|
||||
*.log
|
||||
.env
|
||||
.env.local
|
||||
README.md
|
||||
DEBUGGING_GUIDE.md
|
||||
INACBG_DEBUGGING_GUIDE.md
|
||||
INACBG_STATUS.md
|
||||
MIGRASI_MYSQL_KE_POSTGRESQL_GUIDE.md
|
||||
FIX_RIWAYAT_BILLING_FIELDS.md
|
||||
ERROR_ANALYSIS_TANGGAL_KELUAR.md
|
||||
BILLING_DPJP_INTEGRATION.md
|
||||
main.c
|
||||
BELAJAR SEK/
|
||||
testingFE/
|
||||
sql/
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,35 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
# Capacitor
|
||||
android/
|
||||
ios/
|
||||
.capacitor/
|
||||
|
||||
# Build outputs
|
||||
www/
|
||||
*.apk
|
||||
*.aab
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Go
|
||||
*.exe
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env
|
||||
@@ -0,0 +1,15 @@
|
||||
# Build stage
|
||||
FROM golang:1.23-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache ca-certificates
|
||||
COPY --from=builder /app/main .
|
||||
EXPOSE 8081
|
||||
CMD ["./main"]
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func KonekDB() (*gorm.DB, error) {
|
||||
dsn := os.Getenv("DB_DSN")
|
||||
|
||||
if dsn == "" {
|
||||
user := envOrDefault("DB_USER", "root")
|
||||
pass := envOrDefault("DB_PASSWORD", "")
|
||||
host := envOrDefault("DB_HOST", "localhost")
|
||||
port := envOrDefault("DB_PORT", "3306")
|
||||
name := envOrDefault("DB_NAME", "care_it_data")
|
||||
|
||||
fmt.Println("DB_USER:", os.Getenv("DB_USER"))
|
||||
fmt.Println("DB_PASSWORD:", os.Getenv("DB_PASSWORD"))
|
||||
fmt.Println("DB_HOST:", os.Getenv("DB_HOST"))
|
||||
fmt.Println("DB_PORT:", os.Getenv("DB_PORT"))
|
||||
fmt.Println("DB_NAME:", os.Getenv("DB_NAME"))
|
||||
fmt.Println("HOST:", os.Getenv("HOST"))
|
||||
fmt.Println("PORT:", os.Getenv("PORT"))
|
||||
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, name)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal membuka koneksi database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func envOrDefault(key, fallback string) string {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func KonekPG() (*gorm.DB, error) {
|
||||
dsn := os.Getenv("DB_DSN")
|
||||
|
||||
if dsn == "" {
|
||||
user := envOrDefaultPG("DB_USER", "postgres")
|
||||
pass := envOrDefaultPG("DB_PASSWORD", "gakbikinkembung25")
|
||||
host := envOrDefaultPG("DB_HOST", "postgres")
|
||||
port := envOrDefaultPG("DB_PORT", "5432")
|
||||
name := envOrDefaultPG("DB_NAME", "careit_db")
|
||||
|
||||
fmt.Println("DB_USER:", os.Getenv("DB_USER"))
|
||||
fmt.Println("DB_PASSWORD:", os.Getenv("DB_PASSWORD"))
|
||||
fmt.Println("DB_HOST:", os.Getenv("DB_HOST"))
|
||||
fmt.Println("DB_PORT:", os.Getenv("DB_PORT"))
|
||||
fmt.Println("DB_NAME:", os.Getenv("DB_NAME"))
|
||||
fmt.Println("HOST:", os.Getenv("HOST"))
|
||||
fmt.Println("PORT:", os.Getenv("PORT"))
|
||||
|
||||
dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", host, port, user, pass, name)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal membuka koneksi database: %w", err)
|
||||
} else {
|
||||
fmt.Println("Koneksi ke database PostgreSQL berhasil!")
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func envOrDefaultPG(key, fallback string) string {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
module backendcareit
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
@@ -0,0 +1,883 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/middleware"
|
||||
"backendcareit/models"
|
||||
"backendcareit/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *gin.Engine) {
|
||||
// Routes get dokter
|
||||
r.GET("/dokter", listDokterHandler)
|
||||
// Routes get ruangan
|
||||
r.GET("/ruangan", listRuanganHandler)
|
||||
// Routes get icd9 icd10
|
||||
r.GET("/icd10", listICD10Handler)
|
||||
r.GET("/icd9", listICD9Handler)
|
||||
// Health check
|
||||
r.GET("/", healthHandler)
|
||||
// Routes tarif
|
||||
r.GET("/tarifBPJSRawatInap", listTarifBPJSRawatInapHandler)
|
||||
r.GET("/tarifBPJS/:kode", detailTarifBPJSRawatInapHandler)
|
||||
r.GET("/tarifBPJSRawatJalan", listTarifBPJSRawatJalanHandler)
|
||||
r.GET("/tarifBPJSRawatJalan/:kode", detailTarifBPJSRawatJalanHandler)
|
||||
r.GET("/tarifRS", listTarifRSHandler)
|
||||
r.GET("/tarifRS/:kode", detailTarifRSHandler)
|
||||
r.GET("/tarifRSByKategori/:kategori", listTarifRSByKategoriHandler)
|
||||
// Routes pasien & billing
|
||||
r.GET("/pasien/search", SearchPasienHandler)
|
||||
r.GET("/pasien/:id", GetPasien)
|
||||
r.POST("/billing", CreateBillingHandler)
|
||||
r.GET("/billing/aktif", GetBillingAktifByNamaHandler)
|
||||
r.PUT("/billing/:id", UpdateBillingHandler)
|
||||
|
||||
//close billing
|
||||
r.POST("/billing/close", CloseBillingHandler)
|
||||
|
||||
//get all billing aktif
|
||||
r.GET("/billing/aktif/all", GetAllBillingaktifhandler)
|
||||
|
||||
//admin edit inacbg
|
||||
r.PUT("/admin/inacbg", EditINACBGAdminHandler)
|
||||
// Admin: get all billing
|
||||
r.GET("/admin/billing", GetAllBillingHandler)
|
||||
// Admin: get riwayat billing (sudah ditutup)
|
||||
r.GET("/admin/riwayat-billing", GetRiwayatBillingHandler)
|
||||
// Admin: get riwayat billing with all patient data
|
||||
r.GET("/admin/riwayat-pasien-all", GetRiwayatPasienAllHandler)
|
||||
// Admin: get billing by ID
|
||||
r.GET("/admin/billing/:id", GetBillingByIDHandler)
|
||||
// Admin: post INACBG
|
||||
r.POST("/admin/inacbg", PostINACBGAdminHandler)
|
||||
// Admin: get ruangan dengan pasien
|
||||
r.GET("/admin/ruangan-dengan-pasien", GetRuanganWithPasienHandler)
|
||||
// Login dokter
|
||||
r.POST("/login", LoginDokterHandler(database.DB))
|
||||
// login admin
|
||||
r.POST("/admin/login", LoginAdminHandler(database.DB))
|
||||
}
|
||||
|
||||
// Coba tes koneksi dulu ya
|
||||
|
||||
func healthHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"message": "Server berjalan",
|
||||
})
|
||||
}
|
||||
|
||||
//Handler buat /admin/billing
|
||||
|
||||
func GetAllBillingHandler(c *gin.Context) {
|
||||
|
||||
data, err := services.GetAllBilling(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// Handler buat /admin/billing/:id
|
||||
func GetBillingByIDHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
data, err := services.GetBillingByID(database.DB, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// edit inacbg admin
|
||||
func EditINACBGAdminHandler(c *gin.Context) {
|
||||
var input models.Edit_INACBG_Request
|
||||
|
||||
// Ensure JSON
|
||||
if c.GetHeader("Content-Type") != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.Edit_INACBG_Admin(database.DB, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengedit INACBG",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "INACBG berhasil diupdate",
|
||||
})
|
||||
}
|
||||
|
||||
// Post INACBG from admin
|
||||
func PostINACBGAdminHandler(c *gin.Context) {
|
||||
var input models.Post_INACBG_Admin
|
||||
|
||||
// Ensure JSON
|
||||
if c.GetHeader("Content-Type") != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.Post_INACBG_Admin(database.DB, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal memproses INACBG",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "INACBG berhasil disimpan",
|
||||
})
|
||||
}
|
||||
|
||||
// List tarif BPJS Rawat Inap
|
||||
func listTarifBPJSRawatInapHandler(c *gin.Context) {
|
||||
data, err := services.GetTarifBPJSRawatInap()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func detailTarifBPJSRawatInapHandler(c *gin.Context) {
|
||||
kode := c.Param("kode")
|
||||
data, err := services.GetTarifBPJSRawatInapByKode(kode)
|
||||
if err != nil {
|
||||
if services.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"status": "not_found",
|
||||
"message": "Kode tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// List tarif BPJS Rawat Jalan
|
||||
func listTarifBPJSRawatJalanHandler(c *gin.Context) {
|
||||
data, err := services.GetTarifBPJSRawatJalan()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func detailTarifBPJSRawatJalanHandler(c *gin.Context) {
|
||||
kode := c.Param("kode")
|
||||
data, err := services.GetTarifBPJSRawatJalanByKode(kode)
|
||||
if err != nil {
|
||||
if services.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"status": "not_found",
|
||||
"message": "Kode tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// List tarif RS
|
||||
func listTarifRSHandler(c *gin.Context) {
|
||||
data, err := services.GetTarifRS()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func detailTarifRSHandler(c *gin.Context) {
|
||||
kode := c.Param("kode")
|
||||
data, err := services.GetTarifRSByKode(kode)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func listTarifRSByKategoriHandler(c *gin.Context) {
|
||||
kategori := c.Param("kategori")
|
||||
data, err := services.GetTarifRSByKategori(kategori)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// ICD9
|
||||
func listICD9Handler(c *gin.Context) {
|
||||
data, err := services.GetICD9()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// ICD10
|
||||
func listICD10Handler(c *gin.Context) {
|
||||
data, err := services.GetICD10()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// ruangan
|
||||
func listRuanganHandler(c *gin.Context) {
|
||||
data, err := services.GetRuangan()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// GetRuanganWithPasienHandler - Ambil ruangan yang punya pasien
|
||||
func GetRuanganWithPasienHandler(c *gin.Context) {
|
||||
data, err := services.GetRuanganWithPasien(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// dokter
|
||||
func listDokterHandler(c *gin.Context) {
|
||||
data, err := services.GetDokter()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
//Liat pasien sudah atau belum
|
||||
|
||||
func GetPasien(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
// Konversi string ke int
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"message": "ID pasien harus berupa angka",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pasien, err := services.GetPasienByID(id)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{
|
||||
"message": "Pasien tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"message": "Data pasien ditemukan",
|
||||
"data": pasien,
|
||||
})
|
||||
}
|
||||
|
||||
//add pasien baru
|
||||
|
||||
// CreateBillingHandler handler untuk membuat billing baru dari data frontend
|
||||
func CreateBillingHandler(c *gin.Context) {
|
||||
// Pastikan JSON
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
"error": "Content-Type yang diterima: " + contentType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Gunakan map untuk menerima JSON fleksibel (bisa string atau array untuk nama_dokter)
|
||||
var rawData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&rawData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Konversi nama_dokter dari string ke array jika perlu
|
||||
if namaDokterRaw, ok := rawData["nama_dokter"]; ok {
|
||||
switch v := namaDokterRaw.(type) {
|
||||
case string:
|
||||
// Jika string, konversi ke array dengan 1 elemen
|
||||
if v != "" {
|
||||
rawData["nama_dokter"] = []string{v}
|
||||
} else {
|
||||
rawData["nama_dokter"] = []string{}
|
||||
}
|
||||
case []interface{}:
|
||||
// Jika sudah array, konversi ke []string
|
||||
namaDokterArray := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok && str != "" {
|
||||
namaDokterArray = append(namaDokterArray, str)
|
||||
}
|
||||
}
|
||||
rawData["nama_dokter"] = namaDokterArray
|
||||
case []string:
|
||||
// Sudah dalam format yang benar
|
||||
rawData["nama_dokter"] = v
|
||||
default:
|
||||
rawData["nama_dokter"] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// Konversi map ke BillingRequest
|
||||
var input models.BillingRequest
|
||||
// Marshal dan unmarshal untuk konversi yang aman
|
||||
jsonData, err := json.Marshal(rawData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal memproses data",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonData, &input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Panggil service → return 5 data
|
||||
billing, pasien, tindakanList, icd9List, icd10List, err :=
|
||||
services.DataFromFE(input)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal membuat billing",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Response lengkap ke FE
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Billing berhasil dibuat",
|
||||
"data": gin.H{
|
||||
"pasien": pasien,
|
||||
"billing": billing,
|
||||
"tindakan_rs": tindakanList,
|
||||
"icd9": icd9List,
|
||||
"icd10": icd10List,
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetBillingAktifByNamaHandler - Ambil billing aktif berdasarkan nama
|
||||
// Endpoint: GET /billing/aktif?nama_pasien=...
|
||||
// Mengembalikan billing aktif + semua tindakan & ICD & dokter & INACBG & DPJP
|
||||
func GetBillingAktifByNamaHandler(c *gin.Context) {
|
||||
nama := c.Query("nama_pasien")
|
||||
if strings.TrimSpace(nama) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "nama_pasien wajib diisi",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, err := services.GetBillingDetailAktifByNama(nama)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"status": "not_found",
|
||||
"message": "Billing aktif untuk pasien tersebut tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data billing",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Billing aktif ditemukan",
|
||||
"data": gin.H{
|
||||
"billing": billing,
|
||||
"tindakan_rs": tindakan,
|
||||
"icd9": icd9,
|
||||
"icd10": icd10,
|
||||
"dokter": dokter,
|
||||
"inacbg_ri": inacbgRI,
|
||||
"inacbg_rj": inacbgRJ,
|
||||
"id_dpjp": dpjp,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//search pasien by nama handler
|
||||
|
||||
func SearchPasienHandler(c *gin.Context) {
|
||||
nama := c.Query("nama")
|
||||
|
||||
pasien, err := services.SearchPasienByNama(nama)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data pasien",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": pasien,
|
||||
})
|
||||
}
|
||||
|
||||
// Login dokter
|
||||
func LoginDokterHandler(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Payload login tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||||
|
||||
var dokter models.Dokter
|
||||
if err := db.Where("LOWER(\"Email_UB\") = ? OR LOWER(\"Email_Pribadi\") = ?", email, email).
|
||||
First(&dokter).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Email atau password salah",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal memproses login",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Password check — skip if password column is empty
|
||||
if dokter.Password != "" && dokter.Password != req.Password {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Email atau password salah",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateToken(dokter, email)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal membuat token",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"token": token,
|
||||
"dokter": gin.H{
|
||||
"id": dokter.ID_Dokter,
|
||||
"nama": dokter.Nama_Dokter,
|
||||
"ksm": dokter.KSM,
|
||||
"email": email,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SendEmailTestHandler handler untuk test email
|
||||
func SendEmailTestHandler(c *gin.Context) {
|
||||
if err := services.SendEmailTest(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengirim email test",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Email test berhasil dikirim ke stylohype685@gmail.com dan pasaribumonica2@gmail.com",
|
||||
})
|
||||
}
|
||||
|
||||
// SendEmailCustomHandler - kirim email tes ke daftar penerima yang diberikan
|
||||
func SendEmailCustomHandler(c *gin.Context) {
|
||||
var req struct {
|
||||
To []string `json:"to" binding:"required,min=1"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "Request harus JSON dengan field 'to' sebagai array email"})
|
||||
return
|
||||
}
|
||||
|
||||
subject := req.Subject
|
||||
if strings.TrimSpace(subject) == "" {
|
||||
subject = "Test Email - Sistem Billing Care IT"
|
||||
}
|
||||
|
||||
body := req.Body
|
||||
if strings.TrimSpace(body) == "" {
|
||||
body = "<p>Ini adalah email test dari sistem billing Care IT.</p>"
|
||||
}
|
||||
|
||||
if err := services.SendEmailToMultiple(req.To, subject, body); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Gagal mengirim email", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success", "message": "Email test berhasil dikirim"})
|
||||
}
|
||||
|
||||
func LoginAdminHandler(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Nama_Admin string `json:"Nama_Admin" binding:"required"`
|
||||
Password string `json:"Password" binding:"required"`
|
||||
}
|
||||
|
||||
// Bind & validate
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Nama_Admin dan Password harus diisi",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Trim dan normalize input
|
||||
namaAdmin := strings.TrimSpace(req.Nama_Admin)
|
||||
if namaAdmin == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Nama_Admin tidak boleh kosong",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Query admin_ruangan dengan case-insensitive
|
||||
var admin models.Admin_Ruangan //Admin_Ruangan
|
||||
if err := db.Where("LOWER(\"Nama_Admin\") = ?", strings.ToLower(namaAdmin)).First(&admin).Error; err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Admin tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check password
|
||||
if admin.Password != req.Password {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Password salah",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate token & return
|
||||
token, err := middleware.GenerateTokenAdmin(admin, req.Nama_Admin)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Gagal membuat token",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"token": token,
|
||||
"admin": gin.H{
|
||||
"id": admin.ID_Admin,
|
||||
"nama_admin": admin.Nama_Admin,
|
||||
"id_ruangan": admin.ID_Ruangan,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateBillingHandler - update identitas pasien dalam billing
|
||||
func UpdateBillingHandler(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
billingId, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "ID billing tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var updateReq struct {
|
||||
Nama_Pasien string `json:"nama_pasien"`
|
||||
Usia int `json:"usia"`
|
||||
Jenis_Kelamin string `json:"jenis_kelamin"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Kelas string `json:"kelas"`
|
||||
Tindakan_Rs []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
Billing_sign *string `json:"billing_sign"` // Optional: jika dikirimkan akan diupdate
|
||||
Total_Tarif_RS *float64 `json:"total_tarif_rs"` // Optional: jika dikirimkan akan diupdate
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&updateReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Panggil service untuk update dengan lookup kode
|
||||
if err := services.EditPasienComplete(billingId, updateReq.Nama_Pasien, updateReq.Usia, updateReq.Jenis_Kelamin, updateReq.Ruangan, updateReq.Kelas, updateReq.Tindakan_Rs, updateReq.ICD9, updateReq.ICD10, updateReq.Billing_sign, updateReq.Total_Tarif_RS); err != nil {
|
||||
log.Printf("[EDIT_HANDLER] ERROR - Service returned error: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal update data billing",
|
||||
"error": err.Error(),
|
||||
"details": err.Error(), // Add details untuk debugging
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Data billing berhasil diupdate",
|
||||
})
|
||||
}
|
||||
|
||||
// CloseBillingHandler - handler untuk menutup billing
|
||||
func CloseBillingHandler(c *gin.Context) {
|
||||
var closeReq models.Close_billing
|
||||
|
||||
// Pastikan JSON
|
||||
if c.GetHeader("Content-Type") != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&closeReq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.CloseBilling(closeReq); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal menutup billing",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Billing berhasil ditutup",
|
||||
})
|
||||
}
|
||||
|
||||
// GetRiwayatBillingHandler - Handler buat ngambil riwayat billing yang udah ditutup
|
||||
func GetRiwayatBillingHandler(c *gin.Context) {
|
||||
data, err := services.GetAllRiwayatpasien(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
func GetAllBillingaktifhandler(c *gin.Context) {
|
||||
data, err := services.GetAllBillingaktif(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRiwayatPasienAllHandler - Handler buat ngambil riwayat pasien lengkap
|
||||
func GetRiwayatPasienAllHandler(c *gin.Context) {
|
||||
data, err := services.GetRiwayatPasienAll(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/handlers"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
db, err := database.KonekPG()
|
||||
if err != nil {
|
||||
log.Fatal("Gagal koneksi database:", err)
|
||||
}
|
||||
database.DB = db
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
config := cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
}
|
||||
r.Use(cors.New(config))
|
||||
|
||||
handlers.RegisterRoutes(r)
|
||||
|
||||
host := os.Getenv("HOST")
|
||||
if host == "" {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8081"
|
||||
}
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%s", host, port)
|
||||
fmt.Printf("Server berjalan di http://%s\n", listenAddr)
|
||||
fmt.Println("Akses dari jaringan lain menggunakan IP lokal komputer + port", port)
|
||||
if err := r.Run(listenAddr); err != nil {
|
||||
log.Fatal("Gagal menjalankan server:", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"backendcareit/models"
|
||||
)
|
||||
|
||||
// Payload untuk login dokter - berisi credensial yang diperlukan
|
||||
|
||||
var jwtSecret = []byte(getJWTSecret())
|
||||
|
||||
func getJWTSecret() string {
|
||||
if secret := os.Getenv("JWT_SECRET"); secret != "" {
|
||||
return secret
|
||||
}
|
||||
return "SECRET_KAMU"
|
||||
}
|
||||
|
||||
// LoginDokterHandler - Ini handler POST /login yang ngecek kredensial dokter
|
||||
// Kalau cocok, langsung kasih JWT ke dia
|
||||
|
||||
// GenerateToken - Bikin JWT buat dokter, berlaku 24 jam terus expired hehe
|
||||
func GenerateToken(dokter models.Dokter, email string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"id": dokter.ID_Dokter,
|
||||
"nama": dokter.Nama_Dokter,
|
||||
"ksm": dokter.KSM,
|
||||
"email": email,
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// GenerateTokenAdmin - Serupa dengan dokter, tapi ini buat admin dengan role khusus
|
||||
func GenerateTokenAdmin(admin models.Admin_Ruangan, namaAdmin string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"id": admin.ID_Admin,
|
||||
"nama_admin": admin.Nama_Admin,
|
||||
"id_ruangan": admin.ID_Ruangan,
|
||||
"role": "admin",
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// AuthMiddleware - Middleware pengecekan token JWT
|
||||
// Ngecek header Authorization, parse tokennya, terus simpan data di context jika valid
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Authorization header wajib menggunakan Bearer token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer"))
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("metode tanda tangan tidak dikenal")
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Token tidak valid atau kadaluarsa",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Token tidak valid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("dokter_id", claims["id"])
|
||||
c.Set("dokter_nama", claims["nama"])
|
||||
c.Set("dokter_ksm", claims["ksm"])
|
||||
c.Set("dokter_email", claims["email"])
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Tarif Models - Data struktur buat tarif dari berbagai sumber
|
||||
|
||||
type TarifBPJSRawatInap struct {
|
||||
KodeINA string `gorm:"column:ID_INACBG_RI"`
|
||||
Deskripsi string `gorm:"column:Tindakan_RI"`
|
||||
Kelas1 float64 `gorm:"column:Tarif_Kelas_1"`
|
||||
Kelas2 float64 `gorm:"column:Tarif_Kelas_2"`
|
||||
Kelas3 float64 `gorm:"column:Tarif_Kelas_3"`
|
||||
}
|
||||
|
||||
type TarifBPJSRawatJalan struct {
|
||||
KodeINA string `gorm:"column:ID_INACBG_RJ"`
|
||||
Deskripsi string `gorm:"column:Tindakan_RJ"`
|
||||
TarifINACBG float64 `gorm:"column:Tarif_RJ" json:"tarif_inacbg"`
|
||||
}
|
||||
|
||||
type TarifRS struct {
|
||||
KodeRS string `gorm:"column:ID_Tarif_RS"`
|
||||
Deskripsi string `gorm:"column:Tindakan_RS"`
|
||||
Harga int `gorm:"column:Tarif_RS"`
|
||||
Kategori string `gorm:"column:Kategori_RS"`
|
||||
}
|
||||
|
||||
func (TarifBPJSRawatJalan) TableName() string {
|
||||
return "ina_cbg_rawatjalan"
|
||||
}
|
||||
|
||||
func (TarifBPJSRawatInap) TableName() string {
|
||||
return "ina_cbg_rawatinap"
|
||||
}
|
||||
|
||||
func (TarifRS) TableName() string {
|
||||
return "tarif_rs"
|
||||
}
|
||||
|
||||
// billing_inacbg_RI
|
||||
type Billing_INACBG_RI struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing"`
|
||||
Kode_INACBG string `gorm:"column:ID_INACBG_RI"`
|
||||
}
|
||||
|
||||
func (Billing_INACBG_RI) TableName() string {
|
||||
return "billing_inacbg_ri"
|
||||
}
|
||||
|
||||
// billing_inacbg_RJ
|
||||
type Billing_INACBG_RJ struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing"`
|
||||
Kode_INACBG string `gorm:"column:ID_INACBG_RJ"`
|
||||
}
|
||||
|
||||
func (Billing_INACBG_RJ) TableName() string {
|
||||
return "billing_inacbg_rj"
|
||||
}
|
||||
|
||||
type Billing_DPJP struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey"`
|
||||
ID_DPJP int `gorm:"column:ID_DPJP;primaryKey"`
|
||||
}
|
||||
|
||||
func (Billing_DPJP) TableName() string {
|
||||
return "billing_dpjp"
|
||||
}
|
||||
|
||||
// ICD9
|
||||
|
||||
type ICD9 struct {
|
||||
Kode_ICD9 string `gorm:"column:ID_ICD9"`
|
||||
Prosedur string `gorm:"column:Prosedur"`
|
||||
Versi string `gorm:"column:Versi_ICD9"`
|
||||
}
|
||||
|
||||
func (ICD9) TableName() string {
|
||||
return "icd9"
|
||||
}
|
||||
|
||||
// ICD10
|
||||
type ICD10 struct {
|
||||
Kode_ICD10 string `gorm:"column:ID_ICD10"`
|
||||
Diagnosa string `gorm:"column:Diagnosa"`
|
||||
Versi string `gorm:"column:Versi_ICD10"`
|
||||
}
|
||||
|
||||
func (ICD10) TableName() string {
|
||||
return "icd10"
|
||||
}
|
||||
|
||||
// ruangan
|
||||
type Ruangan struct {
|
||||
ID_Ruangan string `gorm:"column:ID_Ruangan"`
|
||||
Jenis_Ruangan string `gorm:"column:Jenis_Ruangan"`
|
||||
Nama_Ruangan string `gorm:"column:Nama_Ruangan"`
|
||||
Keterangan string `gorm:"column:keterangan"`
|
||||
Kategori_ruangan string `gorm:"column:kategori_ruangan"`
|
||||
}
|
||||
|
||||
func (Ruangan) TableName() string {
|
||||
return "ruangan"
|
||||
}
|
||||
|
||||
// dokter
|
||||
type Dokter struct {
|
||||
ID_Dokter int `gorm:"column:ID_Dokter;primaryKey"`
|
||||
Nama_Dokter string `gorm:"column:Nama_Dokter"`
|
||||
Password string `gorm:"column:Password"`
|
||||
Status string `gorm:"column:Status"`
|
||||
KSM string `gorm:"column:KSM"`
|
||||
Email_UB string `gorm:"column:Email_UB"`
|
||||
Email_Pribadi string `gorm:"column:Email_Pribadi"`
|
||||
}
|
||||
|
||||
func (Dokter) TableName() string {
|
||||
return "dokter"
|
||||
}
|
||||
|
||||
// PASIEN
|
||||
type Pasien struct {
|
||||
ID_Pasien int `gorm:"column:ID_Pasien;primaryKey;autoIncrement"`
|
||||
Nama_Pasien string `gorm:"column:Nama_Pasien"`
|
||||
Jenis_Kelamin string `gorm:"column:Jenis_Kelamin"`
|
||||
Usia int `gorm:"column:Usia"`
|
||||
Ruangan string `gorm:"column:Ruangan"`
|
||||
Kelas string `gorm:"column:Kelas"`
|
||||
}
|
||||
|
||||
type Kelas string
|
||||
|
||||
const (
|
||||
Kelas_1 Kelas = "1"
|
||||
Kelas_2 Kelas = "2"
|
||||
Kelas_3 Kelas = "3"
|
||||
)
|
||||
|
||||
type Jenis_kelamin string
|
||||
|
||||
const (
|
||||
Jenis_Kelamin_Laki_laki Jenis_kelamin = "Laki-laki"
|
||||
Jenis_Kelamin_Perempuan Jenis_kelamin = "Perempuan"
|
||||
)
|
||||
|
||||
func (Pasien) TableName() string {
|
||||
return "pasien"
|
||||
}
|
||||
|
||||
// login dokter
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
//edit inacbg admin
|
||||
|
||||
type Edit_INACBG_Request struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Tipe_inacbg string `json:"tipe_inacbg"`
|
||||
Kode_inacbg []string `json:"kode_inacbg"`
|
||||
Kode_Delete []string `json:"kode_delete"`
|
||||
Total_Klaim float64 `json:"total_klaim"`
|
||||
Billing_Sign string `json:"billing_sign"`
|
||||
}
|
||||
|
||||
//close billing
|
||||
|
||||
type Close_billing struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Tanggal_Keluar string `json:"tanggal_keluar"`
|
||||
}
|
||||
|
||||
//riwayat billing pasien
|
||||
|
||||
type Riwayat_Pasien_all struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
ID_Pasien int `json:"id_pasien"`
|
||||
Nama_Pasien string `json:"nama_pasien"`
|
||||
Jenis_Kelamin string `json:"jenis_kelamin"`
|
||||
Usia int `json:"usia"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Nama_Ruangan string `json:"nama_ruangan"` // ← Added: Ruangan name for display
|
||||
Kelas string `json:"kelas"`
|
||||
ID_DPJP int `json:"id_dpjp"` // ← Added: Doctor in charge
|
||||
Nama_DPJP string `json:"nama_dpjp"`
|
||||
Tanggal_Keluar string `json:"tanggal_keluar"`
|
||||
Tanggal_Masuk string `json:"tanggal_masuk"` // Tanggal_Masuk *time.Time `json:"tanggal_masuk"`
|
||||
Tanggal_Tindakan *time.Time `json:"tanggal_tindakan"`
|
||||
Tindakan_RS []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
Kode_INACBG string `json:"kode_inacbg"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"` // ← Added: Hospital tariff
|
||||
Total_Klaim float64 `json:"total_klaim"` // ← Added: BPJS claim
|
||||
}
|
||||
|
||||
//billing pasien
|
||||
|
||||
type BillingPasien struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;autoIncrement" json:"id_billing"`
|
||||
ID_Pasien int `gorm:"column:ID_Pasien" json:"id_pasien"`
|
||||
Cara_Bayar string `gorm:"column:Cara_Bayar" json:"cara_bayar"`
|
||||
Tanggal_masuk *time.Time `gorm:"column:Tanggal_Masuk" json:"tanggal_masuk"`
|
||||
Tanggal_keluar *time.Time `gorm:"column:Tanggal_Keluar" json:"tanggal_keluar"`
|
||||
Total_Tarif_RS float64 `gorm:"column:Total_Tarif_RS" json:"total_tarif_rs"`
|
||||
Total_Klaim float64 `gorm:"column:Total_Klaim" json:"total_klaim"`
|
||||
Billing_sign string `gorm:"column:Billing_Sign" json:"billing_sign"`
|
||||
}
|
||||
|
||||
type Cara_bayar string
|
||||
|
||||
const (
|
||||
Cara_Bayar_BPJS Cara_bayar = "BPJS"
|
||||
Cara_Bayar_UMUM Cara_bayar = "UMUM"
|
||||
)
|
||||
|
||||
func (BillingPasien) TableName() string {
|
||||
return "billing_pasien"
|
||||
}
|
||||
|
||||
// BillingRequest untuk menerima data dari frontend
|
||||
type BillingRequest struct {
|
||||
Nama_Dokter []string `json:"nama_dokter" binding:"required"` // Array untuk multiple doctors
|
||||
Nama_Pasien string `json:"nama_pasien" binding:"required"`
|
||||
Jenis_Kelamin string `json:"jenis_kelamin" binding:"required"`
|
||||
Usia int `json:"usia" binding:"required"`
|
||||
ID_DPJP int `json:"id_dpjp"`
|
||||
Ruangan string `json:"ruangan" binding:"required"`
|
||||
Kelas string `json:"kelas" binding:"required"`
|
||||
Tindakan_RS []string `json:"tindakan_rs" binding:"required"`
|
||||
Tanggal_Keluar string `json:"tanggal_keluar"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10" binding:"required"`
|
||||
Cara_Bayar string `json:"cara_bayar" binding:"required"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"`
|
||||
Total_Klaim_BPJS float64 `json:"total_klaim_bpjs"` // ← Added: Baseline BPJS claim from FE
|
||||
}
|
||||
|
||||
// admin ruangan //Admin_Ruangan
|
||||
|
||||
type Admin_Ruangan struct {
|
||||
ID_Admin int `gorm:"column:ID_Admin"`
|
||||
Nama_Admin string `gorm:"column:Nama_Admin"`
|
||||
Password string `gorm:"column:Password"`
|
||||
ID_Ruangan string `gorm:"column:ID_Ruangan"`
|
||||
}
|
||||
|
||||
func (Admin_Ruangan) TableName() string {
|
||||
return "admin_ruangan"
|
||||
}
|
||||
|
||||
// billing_Tidakan
|
||||
|
||||
type Billing_Tindakan struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
|
||||
ID_Tarif_RS string `gorm:"column:ID_Tarif_RS;primaryKey;not null"`
|
||||
Tanggal_Tindakan *time.Time `gorm:"column:tanggal_tindakan"`
|
||||
}
|
||||
|
||||
func (Billing_Tindakan) TableName() string {
|
||||
return "billing_tindakan"
|
||||
}
|
||||
|
||||
// billing_ICD9 dan ICD10
|
||||
|
||||
type Billing_ICD9 struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
|
||||
ID_ICD9 string `gorm:"column:ID_ICD9;primaryKey;not null"`
|
||||
}
|
||||
|
||||
type Billing_ICD10 struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
|
||||
ID_ICD10 string `gorm:"column:ID_ICD10;primaryKey;not null"`
|
||||
}
|
||||
|
||||
func (Billing_ICD9) TableName() string {
|
||||
return "billing_icd9"
|
||||
}
|
||||
func (Billing_ICD10) TableName() string {
|
||||
return "billing_icd10"
|
||||
}
|
||||
|
||||
// billing_Dokter - relasi many-to-many antara billing dan dokter dengan tracking tanggal
|
||||
type Billing_Dokter struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing"`
|
||||
ID_Dokter int `gorm:"column:ID_Dokter"`
|
||||
Tanggal *time.Time `gorm:"column:tanggal"` // Tanggal kapan dokter menangani pasien
|
||||
}
|
||||
|
||||
func (Billing_Dokter) TableName() string {
|
||||
return "billing_dokter"
|
||||
}
|
||||
|
||||
// riwayat pasien
|
||||
type Riwayat_Pasien struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Nama_pasien string `json:"nama_pasien"`
|
||||
ID_Pasien int `json:"id_pasien"`
|
||||
Kelas string `json:"Kelas"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"`
|
||||
Total_Klaim float64 `json:"total_klaim"`
|
||||
Tindakan_RS []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
INACBG_RI []string `json:"inacbg_ri"`
|
||||
INACBG_RJ []string `json:"inacbg_rj"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
Nama_Dokter []string `json:"nama_dokter"`
|
||||
}
|
||||
|
||||
// Request untuk tampilan data Admin ( pengisian inacbg)
|
||||
type Request_Admin_Inacbg struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Nama_pasien string `json:"nama_pasien"`
|
||||
ID_Pasien int `json:"id_pasien"`
|
||||
Kelas string `json:"Kelas"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"`
|
||||
Total_Klaim float64 `json:"total_klaim"`
|
||||
ID_DPJP int `json:"id_dpjp"`
|
||||
Tindakan_RS []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
INACBG_RI []string `json:"inacbg_ri"`
|
||||
INACBG_RJ []string `json:"inacbg_rj"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
Nama_Dokter []string `json:"nama_dokter"`
|
||||
}
|
||||
|
||||
// post ke data base
|
||||
type Post_INACBG_Admin struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Tipe_inacbg string `json:"tipe_inacbg"`
|
||||
Kode_INACBG []string `json:"kode_inacbg"`
|
||||
Total_klaim float64 `json:"total_klaim"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
Tanggal_keluar string `json:"tanggal_keluar"` // Diisi oleh admin billing
|
||||
}
|
||||
|
||||
// login dokter
|
||||
type loginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (loginRequest) TableName() string {
|
||||
return "dokter"
|
||||
}
|
||||
|
||||
// getpasienwithallicd9andicd10,andtindakanrs
|
||||
@@ -0,0 +1,87 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"backendcareit/database"
|
||||
)
|
||||
|
||||
func CheckAdmin() {
|
||||
// Nyambungin ke database
|
||||
db, err := database.KonekDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal koneksi database: %v", err)
|
||||
}
|
||||
|
||||
// Set koneksi database
|
||||
database.DB = db
|
||||
|
||||
// Cek data admin
|
||||
type AdminRuangan struct {
|
||||
ID_Admin int `gorm:"column:ID_Admin"`
|
||||
Nama_Admin string `gorm:"column:Nama_Admin"`
|
||||
Password string `gorm:"column:Password"`
|
||||
ID_Ruangan *int `gorm:"column:ID_Ruangan"`
|
||||
}
|
||||
var admins []AdminRuangan
|
||||
|
||||
// Ambil semua admin
|
||||
result := db.Table("admin_ruangan").Find(&admins)
|
||||
if result.Error != nil {
|
||||
log.Fatalf("Gagal query admin: %v", result.Error)
|
||||
}
|
||||
|
||||
fmt.Printf("Total admin ditemukan: %d\n\n", len(admins))
|
||||
|
||||
if len(admins) == 0 {
|
||||
fmt.Println("⚠️ Tidak ada data admin di database!")
|
||||
fmt.Println("\nJalankan script insert_admin.go untuk menambahkan data admin:")
|
||||
fmt.Println(" go run scripts/insert_admin.go")
|
||||
return
|
||||
}
|
||||
|
||||
// Tampilin semua admin
|
||||
for i, admin := range admins {
|
||||
fmt.Printf("Admin #%d:\n", i+1)
|
||||
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
|
||||
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
|
||||
fmt.Printf(" Password: '%s' (length: %d)\n", admin.Password, len(admin.Password))
|
||||
if admin.ID_Ruangan != nil {
|
||||
fmt.Printf(" ID_Ruangan: %d\n", *admin.ID_Ruangan)
|
||||
} else {
|
||||
fmt.Printf(" ID_Ruangan: NULL\n")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Test query buat username 'admin'
|
||||
var admin AdminRuangan
|
||||
err = db.Table("admin_ruangan").
|
||||
Where("Nama_Admin = ?", "admin").
|
||||
First(&admin).Error
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("❌ Query dengan Nama_Admin = 'admin' GAGAL")
|
||||
fmt.Printf(" Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Query dengan Nama_Admin = 'admin' BERHASIL")
|
||||
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
|
||||
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
|
||||
fmt.Printf(" Password: '%s'\n", admin.Password)
|
||||
}
|
||||
|
||||
// Test query case-insensitive
|
||||
err = db.Table("admin_ruangan").
|
||||
Where("LOWER(Nama_Admin) = LOWER(?)", "admin").
|
||||
First(&admin).Error
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("❌ Query case-insensitive GAGAL")
|
||||
fmt.Printf(" Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Query case-insensitive BERHASIL")
|
||||
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
|
||||
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"backendcareit/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Nyambungin ke database
|
||||
db, err := database.KonekDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal koneksi database: %v", err)
|
||||
}
|
||||
|
||||
// Set koneksi database
|
||||
database.DB = db
|
||||
|
||||
// Cek admin udah ada atau belum
|
||||
var count int64
|
||||
db.Table("admin_ruangan").Where("Nama_Admin = ?", "admin").Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
fmt.Println("Admin dengan username 'admin' sudah ada di database.")
|
||||
fmt.Println("Ngapus admin yang lama...")
|
||||
db.Table("admin_ruangan").Where("Nama_Admin = ?", "admin").Delete(nil)
|
||||
}
|
||||
|
||||
// Masukin admin yang baru
|
||||
result := db.Exec(`
|
||||
INSERT INTO admin_ruangan (Nama_Admin, Password, ID_Ruangan)
|
||||
VALUES (?, ?, ?)
|
||||
`, "admin", "admin123", nil)
|
||||
|
||||
if result.Error != nil {
|
||||
log.Fatalf("Gagal insert admin: %v", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
fmt.Println("✓ Data admin berhasil ditambahkan!")
|
||||
fmt.Println(" Username: admin")
|
||||
fmt.Println(" Password: admin123")
|
||||
} else {
|
||||
fmt.Println("Tidak ada data yang ditambahkan.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/models"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Edit_INACBG_Admin(db *gorm.DB, input models.Edit_INACBG_Request) error {
|
||||
log.Printf("[Edit INACBG] Received ID_Billing=%d, Tipe=%s, Kode_count=%d, Delete_count=%d, Total_Klaim=%.2f, Billing_Sign=%s\n",
|
||||
input.ID_Billing, input.Tipe_inacbg, len(input.Kode_inacbg), len(input.Kode_Delete), input.Total_Klaim, input.Billing_Sign)
|
||||
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. Hapus kode yang udah dipilih untuk dihapus
|
||||
if len(input.Kode_Delete) > 0 {
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
if err := tx.Where("\"ID_Billing\" = ? AND \"ID_INACBG_RI\" IN ?", input.ID_Billing, input.Kode_Delete).Delete(&models.Billing_INACBG_RI{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal delete INACBG RI: %w", err)
|
||||
}
|
||||
case "RJ":
|
||||
if err := tx.Where("\"ID_Billing\" = ? AND \"ID_INACBG_RJ\" IN ?", input.ID_Billing, input.Kode_Delete).Delete(&models.Billing_INACBG_RJ{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal delete INACBG RJ: %w", err)
|
||||
}
|
||||
default:
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("invalid tipe_inacbg: %s", input.Tipe_inacbg)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tambahin kode INACBG yang baru
|
||||
if len(input.Kode_inacbg) > 0 {
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
for _, kode := range input.Kode_inacbg {
|
||||
inacbgRI := models.Billing_INACBG_RI{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
}
|
||||
if err := tx.Create(&inacbgRI).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RI kode %s: %w", kode, err)
|
||||
}
|
||||
}
|
||||
case "RJ":
|
||||
for _, kode := range input.Kode_inacbg {
|
||||
inacbgRJ := models.Billing_INACBG_RJ{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
}
|
||||
if err := tx.Create(&inacbgRJ).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RJ kode %s: %w", kode, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update data di tabel billing_pasien
|
||||
updateData := map[string]interface{}{
|
||||
"Total_Klaim": input.Total_Klaim,
|
||||
"Billing_Sign": input.Billing_Sign,
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.BillingPasien{}).Where("\"ID_Billing\" = ?", input.ID_Billing).Updates(updateData).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal update billing_pasien: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. Ngirim email kalo billing_sign gak kosong
|
||||
go func(id int) {
|
||||
if err := SendEmailBillingSignToDokter(id); err != nil {
|
||||
log.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", id, err)
|
||||
}
|
||||
}(input.ID_Billing)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// EditPasienComplete - Update data identitas pasien dalam billing (nama, umur, ruangan, dll) terus dengan lookup kode
|
||||
func EditPasienComplete(billingId int, namaPasien string, usia int, jeniKelamin string, ruangan string, kelas string, tindakan []string, icd9 []string, icd10 []string, billingSign *string, totalTarifRS *float64) error {
|
||||
log.Printf("[EditPasien] START - billingId:%d, nama:%s, tindakan_count:%d, icd9_count:%d, icd10_count:%d\n", billingId, namaPasien, len(tindakan), len(icd9), len(icd10))
|
||||
|
||||
// Get billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.Where("\"ID_Billing\" = ?", billingId).First(&billing).Error; err != nil {
|
||||
log.Printf("[EditPasien] ERROR - billing not found: %v\n", err)
|
||||
return errors.New("billing tidak ditemukan")
|
||||
}
|
||||
log.Printf("[EditPasien] ✓ Billing found - ID_Pasien: %d\n", billing.ID_Pasien)
|
||||
|
||||
// Start transaction
|
||||
tx := database.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Update pasien data
|
||||
if err := tx.Model(&models.Pasien{}).
|
||||
Where("\"ID_Pasien\" = ?", billing.ID_Pasien).
|
||||
Updates(map[string]interface{}{
|
||||
"Nama_Pasien": namaPasien,
|
||||
"Usia": usia,
|
||||
"Jenis_Kelamin": jeniKelamin,
|
||||
"Ruangan": ruangan,
|
||||
"Kelas": kelas,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update data pasien: " + err.Error())
|
||||
}
|
||||
|
||||
// Delete existing tindakan
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_Tindakan{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete tindakan: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new tindakan dengan lookup berdasarkan nama tindakan
|
||||
now := time.Now()
|
||||
for _, tindakanNama := range tindakan {
|
||||
if tindakanNama != "" {
|
||||
log.Printf("[EditPasien] Looking up tindakan: '%s'\n", tindakanNama)
|
||||
// Lookup tarif by deskripsi (nama tindakan) - use quoted column name for PostgreSQL
|
||||
var tarif models.TarifRS
|
||||
if err := tx.Where("\"Tindakan_RS\" = ?", tindakanNama).First(&tarif).Error; err != nil {
|
||||
log.Printf("[EditPasien] ERROR - tindakan lookup failed: %v\n", err)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tx.Rollback()
|
||||
log.Printf("[EditPasien] ERROR - tindakan '%s' not found in tarif_rs\n", tindakanNama)
|
||||
return fmt.Errorf("tindakan '%s' tidak ditemukan", tindakanNama)
|
||||
}
|
||||
tx.Rollback()
|
||||
return errors.New("gagal lookup tindakan: " + err.Error())
|
||||
}
|
||||
log.Printf("[EditPasien] ✓ Tindakan found - ID: %s, Harga: %d\n", tarif.KodeRS, tarif.Harga)
|
||||
|
||||
newTindakan := models.Billing_Tindakan{
|
||||
ID_Billing: billingId,
|
||||
ID_Tarif_RS: tarif.KodeRS,
|
||||
Tanggal_Tindakan: &now,
|
||||
}
|
||||
if err := tx.Create(&newTindakan).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert tindakan: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD9
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD9{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD9: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD9 dengan lookup berdasarkan nama prosedur
|
||||
for _, icd9Nama := range icd9 {
|
||||
if icd9Nama != "" {
|
||||
// Lookup ICD9 by prosedur name
|
||||
var icd9Data models.ICD9
|
||||
if err := tx.Where("\"Prosedur\" = ?", icd9Nama).First(&icd9Data).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("ICD9 '%s' tidak ditemukan", icd9Nama)
|
||||
}
|
||||
tx.Rollback()
|
||||
return errors.New("gagal lookup ICD9: " + err.Error())
|
||||
}
|
||||
|
||||
newICD9 := models.Billing_ICD9{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD9: icd9Data.Kode_ICD9,
|
||||
}
|
||||
if err := tx.Create(&newICD9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD9: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD10
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD10{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD10: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD10 dengan lookup berdasarkan nama diagnosa
|
||||
for _, icd10Nama := range icd10 {
|
||||
if icd10Nama != "" {
|
||||
// Lookup ICD10 by diagnosa name
|
||||
var icd10Data models.ICD10
|
||||
if err := tx.Where("\"Diagnosa\" = ?", icd10Nama).First(&icd10Data).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("ICD10 '%s' tidak ditemukan", icd10Nama)
|
||||
}
|
||||
tx.Rollback()
|
||||
return errors.New("gagal lookup ICD10: " + err.Error())
|
||||
}
|
||||
|
||||
newICD10 := models.Billing_ICD10{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD10: icd10Data.Kode_ICD10,
|
||||
}
|
||||
if err := tx.Create(&newICD10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD10: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update billing_sign jika dikirimkan dari FE
|
||||
if billingSign != nil {
|
||||
if err := tx.Model(&models.BillingPasien{}).
|
||||
Where("\"ID_Billing\" = ?", billingId).
|
||||
Update("Billing_Sign", *billingSign).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update billing_sign: " + err.Error())
|
||||
}
|
||||
}
|
||||
// Update total_tarif_rs jika dikirimkan dari FE
|
||||
if totalTarifRS != nil {
|
||||
if err := tx.Model(&models.BillingPasien{}).
|
||||
Where("\"ID_Billing\" = ?", billingId).
|
||||
Update("Total_Tarif_RS", *totalTarifRS).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update total_tarif_rs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.New("gagal commit transaction: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Post_INACBG_Admin(db *gorm.DB, input models.Post_INACBG_Admin) error {
|
||||
// Debug log
|
||||
log.Printf("[INACBG] Input received: ID_Billing=%d, Tipe=%s, Kode_count=%d, Total_klaim=%.2f, BillingSign=%s\n",
|
||||
input.ID_Billing, input.Tipe_inacbg, len(input.Kode_INACBG), input.Total_klaim, input.Billing_sign)
|
||||
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
log.Printf("[INACBG] Error starting transaction: %v\n", tx.Error)
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// Ensure rollback on panic / unexpected error
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[INACBG] Panic recovered: %v\n", r)
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Validate input
|
||||
if input.Tipe_inacbg != "RI" && input.Tipe_inacbg != "RJ" {
|
||||
tx.Rollback()
|
||||
err := errors.New("invalid tipe_inacbg: must be 'RI' or 'RJ'")
|
||||
log.Printf("[INACBG] Validation error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if len(input.Kode_INACBG) == 0 {
|
||||
tx.Rollback()
|
||||
err := errors.New("Kode_INACBG tidak boleh kosong")
|
||||
log.Printf("[INACBG] Validation error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Ngambil billing dulu buat dapetin total klaim yang lama
|
||||
var existingBilling models.BillingPasien
|
||||
if err := tx.First(&existingBilling, input.ID_Billing).Error; err != nil {
|
||||
tx.Rollback()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = fmt.Errorf("billing dengan ID_Billing=%d tidak ditemukan", input.ID_Billing)
|
||||
log.Printf("[INACBG] %v\n", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("[INACBG] Error fetching billing: %v\n", err)
|
||||
return fmt.Errorf("gagal mengambil billing: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] Found billing: ID=%d, Current_Total_Klaim=%.2f\n", existingBilling.ID_Billing, existingBilling.Total_Klaim)
|
||||
|
||||
// Hitung total klaim yang baru = yang lama + tambahan
|
||||
newTotalKlaim := input.Total_klaim
|
||||
log.Printf("[INACBG] New total klaim: %.2f + %.2f = %.2f\n", existingBilling.Total_Klaim, input.Total_klaim, newTotalKlaim)
|
||||
|
||||
// Parse Tanggal_Keluar jika diisi oleh admin
|
||||
var keluarPtr *time.Time
|
||||
if input.Tanggal_keluar != "" && input.Tanggal_keluar != "null" {
|
||||
s := input.Tanggal_keluar
|
||||
var parsed time.Time
|
||||
var err error
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
parsed, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
t := parsed
|
||||
keluarPtr = &t
|
||||
log.Printf("[INACBG] Parsed tanggal_keluar: %v\n", t)
|
||||
break
|
||||
}
|
||||
}
|
||||
if keluarPtr == nil {
|
||||
tx.Rollback()
|
||||
err := fmt.Errorf("invalid tanggal_keluar format: %s", input.Tanggal_keluar)
|
||||
log.Printf("[INACBG] %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update total klaim kumulatif sama tanggal keluar (kalo ada yang ngirim)
|
||||
updateData := map[string]interface{}{
|
||||
"\"Total_Klaim\"": newTotalKlaim,
|
||||
}
|
||||
if keluarPtr != nil {
|
||||
updateData["\"Tanggal_Keluar\""] = keluarPtr
|
||||
}
|
||||
|
||||
// Kalo frontend kirim billing_sign, langsung simpen ke kolom Billing_Sign
|
||||
if input.Billing_sign != "" {
|
||||
updateData["\"Billing_Sign\""] = input.Billing_sign
|
||||
log.Printf("[INACBG] Will update Billing_Sign to: %s\n", input.Billing_sign)
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] Update data: %v\n", updateData)
|
||||
|
||||
res := tx.Model(&models.BillingPasien{}).
|
||||
Where("\"ID_Billing\" = ?", input.ID_Billing).
|
||||
Updates(updateData)
|
||||
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("[INACBG] Error updating billing: %v\n", res.Error)
|
||||
return fmt.Errorf("gagal update billing: %w", res.Error)
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] Updated %d rows in billing_pasien\n", res.RowsAffected)
|
||||
|
||||
// DELETE semua kode INACBG yang lama buat billing ini (biar gak duplikat pas INSERT)
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
if err := tx.Where("\"ID_Billing\" = ?", input.ID_Billing).Delete(&models.Billing_INACBG_RI{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("[INACBG] Error deleting old INACBG RI: %v\n", err)
|
||||
return fmt.Errorf("gagal delete INACBG RI lama: %w", err)
|
||||
}
|
||||
log.Printf("[INACBG] Deleted old INACBG RI records for ID_Billing=%d\n", input.ID_Billing)
|
||||
|
||||
case "RJ":
|
||||
if err := tx.Where("\"ID_Billing\" = ?", input.ID_Billing).Delete(&models.Billing_INACBG_RJ{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("[INACBG] Error deleting old INACBG RJ: %v\n", err)
|
||||
return fmt.Errorf("gagal delete INACBG RJ lama: %w", err)
|
||||
}
|
||||
log.Printf("[INACBG] Deleted old INACBG RJ records for ID_Billing=%d\n", input.ID_Billing)
|
||||
}
|
||||
|
||||
// Bulk insert kode INACBG berdasarkan tipenya (udah dihapus yang lama)
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
records := make([]models.Billing_INACBG_RI, 0, len(input.Kode_INACBG))
|
||||
for _, kode := range input.Kode_INACBG {
|
||||
records = append(records, models.Billing_INACBG_RI{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&records).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RI: %w", err)
|
||||
}
|
||||
|
||||
case "RJ":
|
||||
records := make([]models.Billing_INACBG_RJ, 0, len(input.Kode_INACBG))
|
||||
for _, kode := range input.Kode_INACBG {
|
||||
records = append(records, models.Billing_INACBG_RJ{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&records).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RJ: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Printf("[INACBG] Error committing transaction: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] ✅ Successfully saved INACBG for ID_Billing=%d, billing_sign=%s\n", input.ID_Billing, input.Billing_sign)
|
||||
|
||||
// Ngirim email ke dokter kalo billing_sign gak kosong
|
||||
if input.Billing_sign != "" && strings.TrimSpace(input.Billing_sign) != "" {
|
||||
// Ngirim email asynchronous (kalo gagal, jangan perpengaruh proses utama)
|
||||
// Log error tapi jangan return, biar proses utama tetep berhasil
|
||||
if err := SendEmailBillingSignToDokter(input.ID_Billing); err != nil {
|
||||
// Log error tapi tidak return error agar proses utama tetap berhasil
|
||||
// Di production, bisa pake logger yang lebih proper
|
||||
fmt.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", input.ID_Billing, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAllBilling(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ngambil semua billing yang belum ditutup (Tanggal_Keluar masih kosong)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulin dulu semua ID_Billing sama ID_Pasien
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Loaded %d pasien from database\n", len(pasienList))
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
log.Printf("[DEBUG] Pasien %d: Nama=%s, Ruangan=%s\n", p.ID_Pasien, p.Nama_Pasien, p.Ruangan)
|
||||
}
|
||||
|
||||
// Ambil tindakan yang berkaitan sama billing-billing ini
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ngambil semua ICD9 yang ada
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil semua ICD10 yang ada
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RI
|
||||
inacbgRIMap := make(map[int][]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RJ
|
||||
inacbgRJMap := make(map[int][]string)
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil dokter dari tabel billing_dokter, diurutkan berdasarkan tanggal
|
||||
dokterMap := make(map[int][]string)
|
||||
var dokterRows []struct {
|
||||
ID_Billing int
|
||||
Nama string
|
||||
Tanggal time.Time
|
||||
}
|
||||
if err := db.Table("\"billing_dokter\"").
|
||||
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\", \"tanggal\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Order("\"tanggal\" ASC").
|
||||
Scan(&dokterRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dokterRows {
|
||||
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
|
||||
}
|
||||
|
||||
// Ambil nama ruangan buat di-mapping dari ID jadi Nama
|
||||
ruanganNameMap := make(map[string]string)
|
||||
var ruanganRows []struct {
|
||||
ID_Ruangan string
|
||||
Nama_Ruangan string
|
||||
}
|
||||
if err := db.Table("\"ruangan\"").
|
||||
Select("\"ID_Ruangan\", \"Nama_Ruangan\"").
|
||||
Scan(&ruanganRows).Error; err != nil {
|
||||
log.Printf("[WARNING] Gagal ngambil ruangan: %v\\n", err)
|
||||
// Lanjutin aja, ID jadi fallback
|
||||
} else {
|
||||
for _, row := range ruanganRows {
|
||||
ruanganNameMap[row.ID_Ruangan] = row.Nama_Ruangan
|
||||
}
|
||||
log.Printf("[DEBUG] Loaded %d ruangan mappings\n", len(ruanganNameMap))
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang bagus
|
||||
var result []models.Request_Admin_Inacbg
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
// ruangan bisa jadi udah nama, bukan ID, langsung pake aja
|
||||
ruanganDisplay := pasien.Ruangan
|
||||
|
||||
// Tapi kalo mirip ID dan ada mapping, pake nama yang sudah dimapping
|
||||
if mappedName, exists := ruanganNameMap[pasien.Ruangan]; exists && mappedName != "" {
|
||||
ruanganDisplay = mappedName
|
||||
}
|
||||
|
||||
item := models.Request_Admin_Inacbg{
|
||||
ID_Billing: b.ID_Billing,
|
||||
Nama_pasien: pasien.Nama_Pasien,
|
||||
ID_Pasien: b.ID_Pasien,
|
||||
Kelas: pasien.Kelas,
|
||||
Ruangan: ruanganDisplay, // ← Use name directly if available, or mapped name
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
INACBG_RI: inacbgRIMap[b.ID_Billing],
|
||||
INACBG_RJ: inacbgRJMap[b.ID_Billing],
|
||||
Billing_sign: b.Billing_sign,
|
||||
Nama_Dokter: dokterMap[b.ID_Billing],
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetBillingByID - Get specific billing data by ID
|
||||
func GetBillingByID(db *gorm.DB, id string) (map[string]interface{}, error) {
|
||||
var billing models.BillingPasien
|
||||
|
||||
if err := db.Where("\"ID_Billing\" = ?", id).First(&billing).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("billing dengan ID=%s tidak ditemukan", id)
|
||||
}
|
||||
return nil, fmt.Errorf("gagal mengambil billing: %w", err)
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"id_billing": billing.ID_Billing,
|
||||
"id_pasien": billing.ID_Pasien,
|
||||
"cara_bayar": billing.Cara_Bayar,
|
||||
"tanggal_masuk": billing.Tanggal_masuk,
|
||||
"tanggal_keluar": billing.Tanggal_keluar,
|
||||
"total_tarif_rs": billing.Total_Tarif_RS,
|
||||
"total_klaim": billing.Total_Klaim,
|
||||
"billing_sign": billing.Billing_sign,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SendEmail - Ngirim email pake SMTP bro
|
||||
func SendEmail(to, subject, body string) error {
|
||||
// Ambil konfigurasi dari env variable dulu, lebih aman
|
||||
from := os.Getenv("EMAIL_FROM")
|
||||
password := os.Getenv("EMAIL_PASSWORD")
|
||||
smtpHost := os.Getenv("SMTP_HOST")
|
||||
smtpPort := os.Getenv("SMTP_PORT")
|
||||
|
||||
// Kalau env variable gak ada, pake default value (biar kompatibel sama versi lama)
|
||||
if from == "" {
|
||||
from = "careit565@gmail.com"
|
||||
}
|
||||
if password == "" {
|
||||
password = "gkhz bjax uamw xydf"
|
||||
}
|
||||
if smtpHost == "" {
|
||||
smtpHost = "smtp.gmail.com"
|
||||
}
|
||||
if smtpPort == "" {
|
||||
smtpPort = "587"
|
||||
}
|
||||
|
||||
if from == "" || password == "" || smtpHost == "" || smtpPort == "" {
|
||||
return fmt.Errorf("konfigurasi email tidak lengkap. Pastikan EMAIL_FROM, EMAIL_PASSWORD, SMTP_HOST, dan SMTP_PORT sudah di-set")
|
||||
}
|
||||
|
||||
// Setup authentication
|
||||
auth := smtp.PlainAuth("", from, password, smtpHost)
|
||||
|
||||
// Format email message
|
||||
msg := []byte(fmt.Sprintf("To: %s\r\n", to) +
|
||||
fmt.Sprintf("Subject: %s\r\n", subject) +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"Content-Type: text/html; charset=UTF-8\r\n" +
|
||||
"\r\n" +
|
||||
body + "\r\n")
|
||||
|
||||
// Send email
|
||||
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
|
||||
err := smtp.SendMail(addr, auth, from, []string{to}, msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gagal mengirim email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendEmailToMultiple - Ngirim email ke banyak orang sekaligus
|
||||
func SendEmailToMultiple(to []string, subject, body string) error {
|
||||
from := os.Getenv("EMAIL_FROM")
|
||||
password := os.Getenv("EMAIL_PASSWORD")
|
||||
smtpHost := os.Getenv("SMTP_HOST")
|
||||
smtpPort := os.Getenv("SMTP_PORT")
|
||||
|
||||
if from == "" {
|
||||
from = "asikmahdi@gmail.com"
|
||||
}
|
||||
if password == "" {
|
||||
password = "njom rhxb prrj tuoj"
|
||||
}
|
||||
if smtpHost == "" {
|
||||
smtpHost = "smtp.gmail.com"
|
||||
}
|
||||
if smtpPort == "" {
|
||||
smtpPort = "587"
|
||||
}
|
||||
|
||||
if from == "" || password == "" || smtpHost == "" || smtpPort == "" {
|
||||
return fmt.Errorf("konfigurasi email tidak lengkap")
|
||||
}
|
||||
|
||||
if len(to) == 0 {
|
||||
return fmt.Errorf("daftar penerima email tidak boleh kosong")
|
||||
}
|
||||
|
||||
// Setup authentication
|
||||
auth := smtp.PlainAuth("", from, password, smtpHost)
|
||||
|
||||
// Rapihin header To buat semua penerima
|
||||
toHeader := strings.Join(to, ", ")
|
||||
|
||||
// Format email message
|
||||
msg := []byte(fmt.Sprintf("To: %s\r\n", toHeader) +
|
||||
fmt.Sprintf("Subject: %s\r\n", subject) +
|
||||
"MIME-Version: 1.0\r\n" +
|
||||
"Content-Type: text/html; charset=UTF-8\r\n" +
|
||||
"\r\n" +
|
||||
body + "\r\n")
|
||||
|
||||
// Kirim email ke semua orang sekaligus
|
||||
addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)
|
||||
err := smtp.SendMail(addr, auth, from, to, msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gagal mengirim email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendEmailTest - Cuma buat test kirim email ke teman-teman
|
||||
func SendEmailTest() error {
|
||||
to := []string{"stylohype685@gmail.com", "pasaribumonica2@gmail.com", "yestondehaan607@gmail.com"}
|
||||
subject := "Test Email - Sistem Billing Care IT"
|
||||
body := `
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
|
||||
.content { background-color: #f9f9f9; padding: 20px; margin-top: 20px; }
|
||||
.footer { margin-top: 20px; padding: 10px; text-align: center; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>Test Email - Sistem Billing Care IT</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Halo!</p>
|
||||
<p>Ini adalah email test dari sistem billing Care IT.</p>
|
||||
<p>Jika Anda menerima email ini, berarti sistem email berfungsi dengan baik.</p>
|
||||
<p>Terima kasih!</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Sistem Billing Care IT</p>
|
||||
<p>Email ini dikirim untuk keperluan testing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
if err := SendEmailToMultiple(to, subject, body); err != nil {
|
||||
return fmt.Errorf("gagal mengirim email test: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendEmailBillingSignToDokter mengirim email ke semua dokter yang menangani pasien tentang billing sign
|
||||
func SendEmailBillingSignToDokter(idBilling int) error {
|
||||
// 1. Ambil billing berdasarkan ID_Billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.First(&billing, idBilling).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("billing dengan ID_Billing=%d tidak ditemukan", idBilling)
|
||||
}
|
||||
return fmt.Errorf("gagal mengambil billing: %w", err)
|
||||
}
|
||||
|
||||
// 2. Ambil semua dokter dari billing_dokter
|
||||
var dokterList []models.Dokter
|
||||
if err := database.DB.
|
||||
Table("\"billing_dokter\" bd").
|
||||
Select("d.*").
|
||||
Joins("JOIN \"dokter\" d ON bd.\"ID_Dokter\" = d.\"ID_Dokter\"").
|
||||
Where("bd.\"ID_Billing\" = ?", idBilling).
|
||||
Find(&dokterList).Error; err != nil {
|
||||
return fmt.Errorf("gagal mengambil dokter: %w", err)
|
||||
}
|
||||
|
||||
if len(dokterList) == 0 {
|
||||
return fmt.Errorf("tidak ada dokter yang terkait dengan billing ID_Billing=%d", idBilling)
|
||||
}
|
||||
|
||||
// 3. Ambil data pasien untuk informasi lengkap
|
||||
var pasien models.Pasien
|
||||
if err := database.DB.Where("\"ID_Pasien\" = ?", billing.ID_Pasien).First(&pasien).Error; err != nil {
|
||||
return fmt.Errorf("gagal mengambil data pasien: %w", err)
|
||||
}
|
||||
|
||||
// 4. Format billing sign untuk ditampilkan
|
||||
billingSignDisplay := strings.ToUpper(billing.Billing_sign)
|
||||
if billingSignDisplay == "" {
|
||||
billingSignDisplay = "Belum ditentukan"
|
||||
}
|
||||
|
||||
// Untuk pengiriman ke dokter: kirim personalisasi per dokter (salam pakai nama dokter)
|
||||
// Kumpulkan alamat per dokter dan jalankan pengiriman secara async (goroutine)
|
||||
anyEmail := false
|
||||
subject := fmt.Sprintf("Notifikasi Billing Sign - Pasien: %s", pasien.Nama_Pasien)
|
||||
|
||||
for _, dokter := range dokterList {
|
||||
// kumpulkan alamat untuk dokter ini
|
||||
addrs := make([]string, 0, 2)
|
||||
if e := strings.TrimSpace(dokter.Email_UB); e != "" {
|
||||
addrs = append(addrs, e)
|
||||
}
|
||||
if e := strings.TrimSpace(dokter.Email_Pribadi); e != "" {
|
||||
// hindari duplikat antara UB dan pribadi
|
||||
if len(addrs) == 0 || addrs[0] != e {
|
||||
addrs = append(addrs, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
continue
|
||||
}
|
||||
anyEmail = true
|
||||
|
||||
// buat body yang dipersonalisasi untuk dokter ini
|
||||
doctorName := dokter.Nama_Dokter
|
||||
if doctorName == "" {
|
||||
doctorName = "Dokter"
|
||||
}
|
||||
|
||||
bodyForDokter := fmt.Sprintf(`
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background-color: #4CAF50; color: white; padding: 20px; text-align: center; }
|
||||
.content { background-color: #f9f9f9; padding: 20px; margin-top: 20px; }
|
||||
.info-row { margin: 10px 0; }
|
||||
.label { font-weight: bold; }
|
||||
.billing-sign { font-size: 18px; font-weight: bold; padding: 10px; text-align: center; margin: 20px 0; }
|
||||
.sign-hijau { background-color: #4CAF50; color: white; }
|
||||
.sign-kuning { background-color: #FFC107; color: #333; }
|
||||
.sign-orange { background-color: #FF9800; color: white; }
|
||||
.sign-merah { background-color: #F44336; color: white; }
|
||||
.footer { margin-top: 20px; padding: 10px; text-align: center; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h2>Notifikasi Billing Sign</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Yth. Dr. %s,</p>
|
||||
<p>Berikut adalah informasi billing sign untuk pasien yang Anda tangani:</p>
|
||||
<div class="info-row"><span class="label">Nama Pasien:</span> %s</div>
|
||||
<div class="info-row"><span class="label">ID Billing:</span> %d</div>
|
||||
<div class="info-row"><span class="label">Ruangan:</span> %s</div>
|
||||
<div class="info-row"><span class="label">Kelas:</span> %s</div>
|
||||
<div class="info-row"><span class="label">Cara Bayar:</span> %s</div>
|
||||
<div class="info-row"><span class="label">Total Tarif RS:</span> Rp %.2f</div>
|
||||
<div class="info-row"><span class="label">Total Klaim BPJS:</span> Rp %.2f</div>
|
||||
<div class="billing-sign sign-%s">Billing Sign: %s</div>
|
||||
<p>Terima kasih atas perhatiannya.</p>
|
||||
</div>
|
||||
<div class="footer"><p>Sistem Billing Care IT</p><p>Email ini dikirim secara otomatis, mohon tidak membalas email ini.</p></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, doctorName, pasien.Nama_Pasien, billing.ID_Billing, pasien.Ruangan, pasien.Kelas,
|
||||
billing.Cara_Bayar, billing.Total_Tarif_RS, billing.Total_Klaim,
|
||||
strings.ToLower(billing.Billing_sign), billingSignDisplay)
|
||||
|
||||
// kirim async ke alamat dokter ini
|
||||
go func(addrs []string, subj, body string, id int) {
|
||||
if err := SendEmailToMultiple(addrs, subj, body); err != nil {
|
||||
fmt.Printf("Warning: Gagal mengirim email ke %v untuk billing %d: %v\n", addrs, id, err)
|
||||
} else {
|
||||
fmt.Printf("Info: Email notifikasi terkirim ke %v untuk billing %d\n", addrs, id)
|
||||
}
|
||||
}(addrs, subject, bodyForDokter, billing.ID_Billing)
|
||||
}
|
||||
|
||||
if !anyEmail {
|
||||
return fmt.Errorf("tidak ada dokter dengan email yang terdaftar untuk billing ID_Billing=%d", idBilling)
|
||||
}
|
||||
|
||||
// Return immediately; actual sending berjalan di goroutine
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetAllBillingaktif(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ambil semua billing yang masih aktif (belum ditutup, Tanggal_Keluar masih kosong)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulin dulu semua ID_Billing dan ID_Pasien buat di-query
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
}
|
||||
|
||||
// Ambil tindakan yang berkaitan sama billing-billing ini
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD9
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD10
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil INACBG RI
|
||||
inacbgRIMap := make(map[int][]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil INACBG RJ
|
||||
inacbgRJMap := make(map[int][]string)
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil dokter dari billing_dokter dengan urutan tanggal
|
||||
dokterMap := make(map[int][]string)
|
||||
var dokterRows []struct {
|
||||
ID_Billing int
|
||||
Nama string
|
||||
}
|
||||
if err := db.Table("\"billing_dokter\"").
|
||||
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Order("tanggal ASC").
|
||||
Scan(&dokterRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dokterRows {
|
||||
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang keren
|
||||
var result []models.Request_Admin_Inacbg
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
item := models.Request_Admin_Inacbg{
|
||||
ID_Billing: b.ID_Billing,
|
||||
Nama_pasien: pasien.Nama_Pasien,
|
||||
ID_Pasien: b.ID_Pasien,
|
||||
Kelas: pasien.Kelas,
|
||||
Ruangan: pasien.Ruangan,
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
INACBG_RI: inacbgRIMap[b.ID_Billing],
|
||||
INACBG_RJ: inacbgRJMap[b.ID_Billing],
|
||||
Billing_sign: b.Billing_sign,
|
||||
Nama_Dokter: dokterMap[b.ID_Billing],
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Ambil ID_tarif_RS dari nama Tindakan_RS
|
||||
func GetTarifRSByTindakan(tindakans []string) ([]models.TarifRS, error) {
|
||||
var tarifList []models.TarifRS
|
||||
|
||||
if err := database.DB.
|
||||
Where("\"Tindakan_RS\" IN ?", tindakans).
|
||||
Find(&tarifList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tarifList, nil
|
||||
}
|
||||
|
||||
// GetPasienByID - Cari pasien berdasarkan ID nya
|
||||
func GetPasienByID(id int) (*models.Pasien, error) {
|
||||
var pasien models.Pasien
|
||||
|
||||
if err := database.DB.Where("\"ID_Pasien\" = ?", id).First(&pasien).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pasien, nil
|
||||
}
|
||||
|
||||
// GetPasienByNama - Cari pasien berdasarkan nama mereka
|
||||
func GetPasienByNama(nama string) (*models.Pasien, error) {
|
||||
var pasien models.Pasien
|
||||
|
||||
if err := database.DB.Where("\"Nama_Pasien\" = ?", nama).First(&pasien).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pasien, nil
|
||||
}
|
||||
|
||||
// SearchPasienByNama - Pencarian pasien pake nama (bisa partial)
|
||||
func SearchPasienByNama(nama string) ([]models.Pasien, error) {
|
||||
var pasien []models.Pasien
|
||||
|
||||
err := database.DB.
|
||||
Where("\"Nama_Pasien\" LIKE ?", "%"+nama+"%").
|
||||
Find(&pasien).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pasien, nil
|
||||
}
|
||||
|
||||
// GetBillingDetailAktifByNama - Ambil data billing lengkap (billing, tindakan, ICD, dokter, INACBG, DPJP) buat satu pasien dari nama
|
||||
// Return: billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, error
|
||||
func GetBillingDetailAktifByNama(namaPasien string) (*models.BillingPasien, []string, []string, []string, []string, []string, []string, int, error) {
|
||||
// Cari pasien dulu
|
||||
var pasien models.Pasien
|
||||
if err := database.DB.Where("\"Nama_Pasien\" = ?", namaPasien).First(&pasien).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
// Cari billing aktif terakhir pasien ini (yang belum ditutup, Tanggal_Keluar IS NULL)
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.
|
||||
Where("\"ID_Pasien\" = ? AND \"Tanggal_Keluar\" IS NULL", pasien.ID_Pasien).
|
||||
Order("\"ID_Billing\" DESC").
|
||||
First(&billing).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
// Ambil semua tindakan (join billing_tindakan -> tarif_rs)
|
||||
var tindakanJoin []struct {
|
||||
Nama string `gorm:"column:Tindakan_RS"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_tindakan\" bt").
|
||||
Select("tr.\"Tindakan_RS\"").
|
||||
Joins("JOIN \"tarif_rs\" tr ON bt.\"ID_Tarif_RS\" = tr.\"ID_Tarif_RS\"").
|
||||
Where("bt.\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&tindakanJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
tindakanNames := make([]string, 0, len(tindakanJoin))
|
||||
for _, t := range tindakanJoin {
|
||||
tindakanNames = append(tindakanNames, t.Nama)
|
||||
}
|
||||
|
||||
// Ambil semua ICD9
|
||||
var icd9Join []struct {
|
||||
Prosedur string `gorm:"column:Prosedur"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_icd9\" bi").
|
||||
Select("i.\"Prosedur\"").
|
||||
Joins("JOIN \"icd9\" i ON bi.\"ID_ICD9\" = i.\"ID_ICD9\"").
|
||||
Where("bi.\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&icd9Join).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
icd9Names := make([]string, 0, len(icd9Join))
|
||||
for _, i := range icd9Join {
|
||||
icd9Names = append(icd9Names, i.Prosedur)
|
||||
}
|
||||
|
||||
// Ambil semua ICD10
|
||||
var icd10Join []struct {
|
||||
Diagnosa string `gorm:"column:Diagnosa"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_icd10\" bi").
|
||||
Select("i.\"Diagnosa\"").
|
||||
Joins("JOIN \"icd10\" i ON bi.\"ID_ICD10\" = i.\"ID_ICD10\"").
|
||||
Where("bi.\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&icd10Join).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
icd10Names := make([]string, 0, len(icd10Join))
|
||||
for _, i := range icd10Join {
|
||||
icd10Names = append(icd10Names, i.Diagnosa)
|
||||
}
|
||||
|
||||
// Ambil semua dokter dari billing_dokter dengan tanggal
|
||||
var dokterJoin []struct {
|
||||
Nama string `gorm:"column:Nama_Dokter"`
|
||||
Tanggal *time.Time `gorm:"column:Tanggal"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_dokter\"").
|
||||
Select("\"Nama_Dokter\", \"tanggal\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"billing_dokter\".\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Order("tanggal ASC").
|
||||
Scan(&dokterJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
dokterNames := make([]string, 0, len(dokterJoin))
|
||||
for _, d := range dokterJoin {
|
||||
dokterNames = append(dokterNames, d.Nama)
|
||||
}
|
||||
|
||||
// Ambil semua INACBG RI
|
||||
var inacbgRIJoin []struct {
|
||||
Kode string `gorm:"column:ID_INACBG_RI"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_inacbg_ri\"").
|
||||
Select("\"ID_INACBG_RI\"").
|
||||
Where("\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&inacbgRIJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
inacbgRINames := make([]string, 0, len(inacbgRIJoin))
|
||||
for _, row := range inacbgRIJoin {
|
||||
inacbgRINames = append(inacbgRINames, row.Kode)
|
||||
}
|
||||
|
||||
// Ambil semua INACBG RJ
|
||||
var inacbgRJJoin []struct {
|
||||
Kode string `gorm:"column:ID_INACBG_RJ"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_inacbg_rj\"").
|
||||
Select("\"ID_INACBG_RJ\"").
|
||||
Where("\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&inacbgRJJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
inacbgRJNames := make([]string, 0, len(inacbgRJJoin))
|
||||
for _, row := range inacbgRJJoin {
|
||||
inacbgRJNames = append(inacbgRJNames, row.Kode)
|
||||
}
|
||||
|
||||
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
|
||||
var dpjpRow struct {
|
||||
ID_DPJP int `gorm:"column:ID_DPJP"`
|
||||
}
|
||||
var idDPJP int
|
||||
if err := database.DB.
|
||||
Table("\"billing_dpjp\"").
|
||||
Select("\"ID_DPJP\"").
|
||||
Where("\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
First(&dpjpRow).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
// Jika tidak ada DPJP, idDPJP = 0 (normal, boleh tidak ada)
|
||||
idDPJP = 0
|
||||
} else {
|
||||
idDPJP = dpjpRow.ID_DPJP
|
||||
}
|
||||
|
||||
return &billing, tindakanNames, icd9Names, icd10Names, dokterNames, inacbgRINames, inacbgRJNames, idDPJP, nil
|
||||
}
|
||||
|
||||
// GetDokterByNama - Cari dokter berdasarkan nama mereka
|
||||
func GetDokterByNama(nama string) (*models.Dokter, error) {
|
||||
var dokter models.Dokter
|
||||
|
||||
if err := database.DB.Where("\"Nama_Dokter\" = ?", nama).First(&dokter).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dokter, nil
|
||||
}
|
||||
|
||||
func DataFromFE(input models.BillingRequest) (
|
||||
*models.BillingPasien,
|
||||
*models.Pasien,
|
||||
[]models.Billing_Tindakan,
|
||||
[]models.Billing_ICD9,
|
||||
[]models.Billing_ICD10,
|
||||
error,
|
||||
) {
|
||||
|
||||
tx := database.DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, nil, nil, nil, nil, tx.Error
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// ===========================
|
||||
// 1. CARI ATAU BUAT PASIEN
|
||||
// ===========================
|
||||
var pasien models.Pasien
|
||||
result := tx.Where("\"Nama_Pasien\" = ?", input.Nama_Pasien).First(&pasien)
|
||||
|
||||
// Jika pasien sudah ada, update data jika ada perubahan (usia, ruangan, kelas, jenis_kelamin)
|
||||
if result.Error == nil {
|
||||
updated := false
|
||||
if pasien.Usia != input.Usia {
|
||||
pasien.Usia = input.Usia
|
||||
updated = true
|
||||
}
|
||||
if pasien.Ruangan != input.Ruangan {
|
||||
pasien.Ruangan = input.Ruangan
|
||||
updated = true
|
||||
}
|
||||
if pasien.Kelas != input.Kelas {
|
||||
pasien.Kelas = input.Kelas
|
||||
updated = true
|
||||
}
|
||||
if pasien.Jenis_Kelamin != input.Jenis_Kelamin {
|
||||
pasien.Jenis_Kelamin = input.Jenis_Kelamin
|
||||
updated = true
|
||||
}
|
||||
if updated {
|
||||
if err := tx.Save(&pasien).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal update data pasien: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
|
||||
pasien = models.Pasien{
|
||||
Nama_Pasien: input.Nama_Pasien,
|
||||
Jenis_Kelamin: input.Jenis_Kelamin,
|
||||
Usia: input.Usia,
|
||||
Ruangan: input.Ruangan,
|
||||
Kelas: input.Kelas,
|
||||
}
|
||||
|
||||
if err := tx.Create(&pasien).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal membuat pasien baru: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal mencari pasien: %s", result.Error.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if pasien.ID_Pasien == 0 {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("ID_Pasien tidak valid")
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 2. CARI SEMUA DOKTER
|
||||
// ===========================
|
||||
var dokterList []models.Dokter
|
||||
for _, namaDokter := range input.Nama_Dokter {
|
||||
var dokter models.Dokter
|
||||
if err := tx.Where("\"Nama_Dokter\" = ?", namaDokter).First(&dokter).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("dokter '%s' tidak ditemukan", namaDokter)
|
||||
}
|
||||
dokterList = append(dokterList, dokter)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Parse Tanggal_Keluar (frontend sends string). Accept multiple formats.
|
||||
var keluarPtr *time.Time
|
||||
if input.Tanggal_Keluar != "" && input.Tanggal_Keluar != "null" {
|
||||
s := input.Tanggal_Keluar
|
||||
// Try several common layouts
|
||||
var parsed time.Time
|
||||
var err error
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
parsed, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
t := parsed
|
||||
keluarPtr = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
if keluarPtr == nil {
|
||||
// If parsing failed, return error
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("invalid tanggal_keluar format: %s", input.Tanggal_Keluar)
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 3. CARI / BUAT BILLING
|
||||
// ===========================
|
||||
// Catatan:
|
||||
// - Kita anggap "billing aktif" = billing yang belum ditutup (Tanggal_Keluar IS NULL) untuk pasien ini.
|
||||
// - Jika ada billing aktif, update; jika tidak, buat billing baru.
|
||||
var billing models.BillingPasien
|
||||
billingResult := tx.
|
||||
Where("\"ID_Pasien\" = ? AND \"Tanggal_Keluar\" IS NULL", pasien.ID_Pasien).
|
||||
Order("\"ID_Billing\" DESC").
|
||||
First(&billing)
|
||||
|
||||
if billingResult.Error != nil {
|
||||
if errors.Is(billingResult.Error, gorm.ErrRecordNotFound) {
|
||||
// Belum ada billing aktif → buat billing baru
|
||||
billing = models.BillingPasien{
|
||||
ID_Pasien: pasien.ID_Pasien,
|
||||
Cara_Bayar: input.Cara_Bayar,
|
||||
Tanggal_masuk: &now,
|
||||
Tanggal_keluar: keluarPtr,
|
||||
Total_Tarif_RS: input.Total_Tarif_RS,
|
||||
Total_Klaim: input.Total_Klaim_BPJS, // ← Changed: Use input value instead of hardcoded 0
|
||||
}
|
||||
|
||||
// jika frontend mengirim billing_sign, gunakan itu, kalau tidak gunakan default ""
|
||||
if input.Billing_sign != "" {
|
||||
billing.Billing_sign = input.Billing_sign
|
||||
} else {
|
||||
billing.Billing_sign = ""
|
||||
}
|
||||
|
||||
if err := tx.Create(&billing).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal membuat billing: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
// Error lain saat cari billing
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal mencari billing pasien: %s", billingResult.Error.Error())
|
||||
}
|
||||
} else {
|
||||
// Sudah ada billing aktif → update data billing lama, tambahkan tindakan / ICD baru
|
||||
billing.Cara_Bayar = input.Cara_Bayar
|
||||
if keluarPtr != nil {
|
||||
billing.Tanggal_keluar = keluarPtr
|
||||
}
|
||||
// Tambahkan total tarif dari request baru
|
||||
billing.Total_Tarif_RS += input.Total_Tarif_RS
|
||||
// Update Total_Tarif_BPJS if:
|
||||
// 1. Not yet set (== 0), OR
|
||||
// 2. Input value is higher (more accurate baseline from FE)
|
||||
// This ensures we always have the correct baseline, not accumulated value from INACBG
|
||||
if input.Total_Klaim_BPJS > 0 && (billing.Total_Klaim == 0 || input.Total_Klaim_BPJS > billing.Total_Klaim) {
|
||||
billing.Total_Klaim = input.Total_Klaim_BPJS
|
||||
log.Printf("[Billing] Updated Total_Tarif_BPJS to %.2f\n", input.Total_Klaim_BPJS)
|
||||
}
|
||||
|
||||
// Log input billing_sign untuk debug
|
||||
log.Printf("[Billing Update] Received input.Billing_sign: '%s' (empty=%v)\n", input.Billing_sign, input.Billing_sign == "")
|
||||
|
||||
// Jika frontend mengirim Billing_sign, gunakan; jika tidak, hitung di backend
|
||||
if input.Billing_sign != "" {
|
||||
billing.Billing_sign = input.Billing_sign
|
||||
log.Printf("[Billing Update] Updated Billing_sign to: %s\n", input.Billing_sign)
|
||||
}
|
||||
|
||||
if err := tx.Save(&billing).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal update billing pasien: %s", err.Error())
|
||||
}
|
||||
|
||||
// Jika frontend mengirim Billing_sign pada update, kirim notifikasi email ke dokter secara async
|
||||
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 4. SIMPAN DOKTER KE BILLING_DOKTER DENGAN TANGGAL
|
||||
// ===========================
|
||||
// Tidak menghapus dokter lama, hanya menambahkan dokter baru dengan tanggal hari ini
|
||||
// Ini memungkinkan tracking dokter yang berbeda setiap hari
|
||||
tanggalHariIni := time.Now()
|
||||
|
||||
// Insert semua dokter baru ke billing_dokter dengan tanggal hari ini
|
||||
// Cek dulu apakah dokter dengan tanggal yang sama sudah ada (untuk menghindari duplikasi)
|
||||
var billingDokterList []models.Billing_Dokter
|
||||
for _, dokter := range dokterList {
|
||||
// Cek apakah dokter ini sudah ada di billing dengan tanggal yang sama
|
||||
var existing models.Billing_Dokter
|
||||
result := tx.Where("\"ID_Billing\" = ? AND \"ID_Dokter\" = ? AND DATE(tanggal) = DATE(?)",
|
||||
billing.ID_Billing, dokter.ID_Dokter, tanggalHariIni).First(&existing)
|
||||
|
||||
// Jika belum ada, tambahkan
|
||||
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
billingDokter := models.Billing_Dokter{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_Dokter: dokter.ID_Dokter,
|
||||
Tanggal: &tanggalHariIni,
|
||||
}
|
||||
billingDokterList = append(billingDokterList, billingDokter)
|
||||
}
|
||||
// Jika sudah ada, skip (tidak perlu insert lagi)
|
||||
}
|
||||
|
||||
if len(billingDokterList) > 0 {
|
||||
if err := tx.Create(&billingDokterList).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing dokter: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 4.5 SIMPAN DPJP KE BILLING_DPJP
|
||||
// ===========================
|
||||
// Insert DPJP (Doctor In Charge) ke tabel billing_dpjp jika ID_DPJP disediakan
|
||||
if input.ID_DPJP > 0 {
|
||||
billingDPJP := models.Billing_DPJP{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_DPJP: input.ID_DPJP,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billingDPJP).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing DPJP: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Printf("[Billing DPJP] Inserted billing %d with DPJP %d\n", billing.ID_Billing, input.ID_DPJP)
|
||||
}
|
||||
|
||||
var billingTindakanList []models.Billing_Tindakan
|
||||
var billingICD9List []models.Billing_ICD9
|
||||
var billingICD10List []models.Billing_ICD10
|
||||
|
||||
for _, tindakan := range input.Tindakan_RS {
|
||||
var tarif models.TarifRS
|
||||
|
||||
if err := tx.Where("\"Tindakan_RS\" = ?", tindakan).First(&tarif).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("tindakan '%s' tidak ditemukan", tindakan)
|
||||
}
|
||||
|
||||
billTindakan := models.Billing_Tindakan{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_Tarif_RS: tarif.KodeRS,
|
||||
Tanggal_Tindakan: &tanggalHariIni,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billTindakan).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing tindakan: %s", err.Error())
|
||||
}
|
||||
|
||||
billingTindakanList = append(billingTindakanList, billTindakan)
|
||||
}
|
||||
|
||||
for _, icd := range input.ICD9 {
|
||||
var icd9 models.ICD9
|
||||
|
||||
if err := tx.Where("\"Prosedur\" = ?", icd).First(&icd9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("ICD9 '%s' tidak ditemukan", icd)
|
||||
}
|
||||
|
||||
billICD9 := models.Billing_ICD9{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_ICD9: icd9.Kode_ICD9,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billICD9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing ICD9: %s", err.Error())
|
||||
}
|
||||
|
||||
billingICD9List = append(billingICD9List, billICD9)
|
||||
}
|
||||
|
||||
for _, icd := range input.ICD10 {
|
||||
var icd10 models.ICD10
|
||||
|
||||
if err := tx.Where("\"Diagnosa\" = ?", icd).First(&icd10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("ICD10 '%s' tidak ditemukan", icd)
|
||||
}
|
||||
|
||||
billICD10 := models.Billing_ICD10{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_ICD10: icd10.Kode_ICD10,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billICD10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing ICD10: %s", err.Error())
|
||||
}
|
||||
|
||||
billingICD10List = append(billingICD10List, billICD10)
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if input.Billing_sign != "" && strings.TrimSpace(input.Billing_sign) != "" {
|
||||
go func(id int) {
|
||||
if err := SendEmailBillingSignToDokter(id); err != nil {
|
||||
fmt.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", id, err)
|
||||
}
|
||||
}(billing.ID_Billing)
|
||||
}
|
||||
|
||||
return &billing, &pasien, billingTindakanList, billingICD9List, billingICD10List, nil
|
||||
}
|
||||
|
||||
// GetLastBillingByNama - Ambil billing terakhir pasien (buat dapetin baseline total_klaim pas billing baru dibuat)
|
||||
func GetLastBillingByNama(namaPasien string) (*models.BillingPasien, error) {
|
||||
// Cari pasien dulu
|
||||
var pasien models.Pasien
|
||||
if err := database.DB.Where("\"Nama_Pasien\" = ?", namaPasien).First(&pasien).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cari billing terakhir pasien ini (paling baru berdasarkan ID_Billing)
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.
|
||||
Where("\"ID_Pasien\" = ?", pasien.ID_Pasien).
|
||||
Order("\"ID_Billing\" DESC").
|
||||
First(&billing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &billing, nil
|
||||
}
|
||||
|
||||
// UpdateBillingIdentitas - update data identitas pasien dalam billing
|
||||
func UpdateBillingIdentitas(billingId int, namaPasien string, usia int, jeniKelamin string, ruangan string, kelas string, tindakan []string, icd9 []string, icd10 []string) error {
|
||||
// Get billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.Where("\"ID_Billing\" = ?", billingId).First(&billing).Error; err != nil {
|
||||
return errors.New("billing tidak ditemukan")
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
tx := database.DB.Begin()
|
||||
|
||||
// Update pasien data
|
||||
pasien := models.Pasien{}
|
||||
if err := tx.Model(&pasien).
|
||||
Where("\"ID_Pasien\" = ?", billing.ID_Pasien).
|
||||
Updates(map[string]interface{}{
|
||||
"\"Nama_Pasien\"": namaPasien,
|
||||
"\"Usia\"": usia,
|
||||
"\"Jenis_Kelamin\"": jeniKelamin,
|
||||
"\"Ruangan\"": ruangan,
|
||||
"\"Kelas\"": kelas,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update data pasien: " + err.Error())
|
||||
}
|
||||
|
||||
// Delete existing tindakan
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_Tindakan{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete tindakan: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new tindakan
|
||||
for _, t := range tindakan {
|
||||
if t != "" {
|
||||
newTindakan := models.Billing_Tindakan{
|
||||
ID_Billing: billingId,
|
||||
ID_Tarif_RS: t,
|
||||
}
|
||||
if err := tx.Create(&newTindakan).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert tindakan: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD9
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD9{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD9: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD9
|
||||
for _, i := range icd9 {
|
||||
if i != "" {
|
||||
newICD9 := models.Billing_ICD9{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD9: i,
|
||||
}
|
||||
if err := tx.Create(&newICD9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD9: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD10
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD10{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD10: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD10
|
||||
for _, i := range icd10 {
|
||||
if i != "" {
|
||||
newICD10 := models.Billing_ICD10{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD10: i,
|
||||
}
|
||||
if err := tx.Create(&newICD10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD10: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.New("gagal commit transaction: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
)
|
||||
|
||||
// CloseBilling - Nutup billing dengan set Tanggal_Keluar (selesai dah pasiennya)
|
||||
func CloseBilling(closeReq models.Close_billing) error {
|
||||
// Cari billing berdasarkan ID_Billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.Where("\"ID_Billing\" = ?", closeReq.ID_Billing).First(&billing).Error; err != nil {
|
||||
return fmt.Errorf("billing dengan ID %d tidak ditemukan: %w", closeReq.ID_Billing, err)
|
||||
}
|
||||
|
||||
// Parse Tanggal_Keluar dari string ke time.Time
|
||||
// Menggunakan multiple layouts seperti di billing_pasien.go
|
||||
var keluarTime *time.Time
|
||||
if closeReq.Tanggal_Keluar != "" {
|
||||
s := closeReq.Tanggal_Keluar
|
||||
var parsed time.Time
|
||||
var err error
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
parsed, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
t := parsed
|
||||
keluarTime = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
if keluarTime == nil {
|
||||
return fmt.Errorf("format tanggal_keluar tidak valid: %s", closeReq.Tanggal_Keluar)
|
||||
}
|
||||
} else {
|
||||
return errors.New("tanggal_keluar tidak boleh kosong")
|
||||
}
|
||||
|
||||
// Update Tanggal_keluar pada billing
|
||||
billing.Tanggal_keluar = keluarTime
|
||||
|
||||
// Simpan perubahan
|
||||
if err := database.DB.Save(&billing).Error; err != nil {
|
||||
return fmt.Errorf("gagal update billing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetRiwayatPasienAll(db *gorm.DB) ([]models.Riwayat_Pasien_all, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ngambil semua billing yang udah ditutup (Tanggal_Keluar udah ada)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NOT NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulkan semua ID_Billing dan ID_Pasien
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
}
|
||||
|
||||
// Ambil tindakan hanya untuk billing terkait
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ambil tanggal tindakan dari tabel billing_tindakan
|
||||
tindakanDateMap := make(map[int]*time.Time)
|
||||
var tindakanDateRows []struct {
|
||||
ID_Billing int
|
||||
Tanggal_Tindakan *time.Time
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"tanggal_tindakan\"").
|
||||
Scan(&tindakanDateRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanDateRows {
|
||||
if t.Tanggal_Tindakan != nil {
|
||||
tindakanDateMap[t.ID_Billing] = t.Tanggal_Tindakan
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil ICD9
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD10
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil INACBG - yang RI dikasih prioritas duluan
|
||||
inacbgMap := make(map[int]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgMap[row.ID_Billing] = row.Kode
|
||||
}
|
||||
|
||||
// Kalo gada RI, ambil dari RJ aja
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
if _, exists := inacbgMap[row.ID_Billing]; !exists {
|
||||
inacbgMap[row.ID_Billing] = row.Kode
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
|
||||
dpjpMap := make(map[int]int)
|
||||
var dpjpRows []struct {
|
||||
ID_Billing int
|
||||
ID_DPJP int
|
||||
}
|
||||
if err := db.Table("\"billing_dpjp\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_DPJP\"").
|
||||
Scan(&dpjpRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dpjpRows {
|
||||
dpjpMap[row.ID_Billing] = row.ID_DPJP
|
||||
}
|
||||
|
||||
// nama dokter susai dpjp ya gais
|
||||
dpjpNameMap := make(map[int]string)
|
||||
var dpjpNameRows []struct {
|
||||
ID_Dokter int
|
||||
Nama_Dokter string
|
||||
}
|
||||
if err := db.Table("\"dokter\"").
|
||||
Select("\"ID_Dokter\", \"Nama_Dokter\"").
|
||||
Scan(&dpjpNameRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dpjpNameRows {
|
||||
dpjpNameMap[row.ID_Dokter] = row.Nama_Dokter
|
||||
}
|
||||
|
||||
// Ambil nama ruangan buat di-mapping dari ID jadi Nama
|
||||
ruanganNameMap := make(map[string]string)
|
||||
var ruanganRows []struct {
|
||||
ID_Ruangan string
|
||||
Nama_Ruangan string
|
||||
}
|
||||
if err := db.Table("\"ruangan\"").
|
||||
Select("\"ID_Ruangan\", \"Nama_Ruangan\"").
|
||||
Scan(&ruanganRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range ruanganRows {
|
||||
ruanganNameMap[row.ID_Ruangan] = row.Nama_Ruangan
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang bagus
|
||||
var result []models.Riwayat_Pasien_all
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
item := models.Riwayat_Pasien_all{
|
||||
ID_Billing: b.ID_Billing,
|
||||
ID_Pasien: pasien.ID_Pasien,
|
||||
Nama_Pasien: pasien.Nama_Pasien,
|
||||
Jenis_Kelamin: pasien.Jenis_Kelamin,
|
||||
Usia: pasien.Usia,
|
||||
Ruangan: pasien.Ruangan,
|
||||
Nama_Ruangan: ruanganNameMap[pasien.Ruangan],
|
||||
Kelas: pasien.Kelas,
|
||||
ID_DPJP: dpjpMap[b.ID_Billing],
|
||||
Nama_DPJP: dpjpNameMap[dpjpMap[b.ID_Billing]],
|
||||
Tanggal_Keluar: b.Tanggal_keluar.Format("2006-01-02"),
|
||||
Tanggal_Masuk: b.Tanggal_masuk.Format("2006-01-02"), //b.Tanggal_masuk,
|
||||
Tanggal_Tindakan: tindakanDateMap[b.ID_Billing],
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
Kode_INACBG: inacbgMap[b.ID_Billing],
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetAllRiwayatpasien(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ngambil semua billing yang udah ditutup (Tanggal_Keluar ada isinya)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NOT NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulkan semua ID_Billing dan ID_Pasien
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ? ", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
}
|
||||
|
||||
// Ambil tindakan hanya untuk billing terkait
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD9
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD10
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RI
|
||||
inacbgRIMap := make(map[int][]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RJ
|
||||
inacbgRJMap := make(map[int][]string)
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil dokter dari tabel billing_dokter, diurutkan berdasarkan tanggal
|
||||
dokterMap := make(map[int][]string)
|
||||
var dokterRows []struct {
|
||||
ID_Billing int
|
||||
Nama string
|
||||
}
|
||||
if err := db.Table("\"billing_dokter\"").
|
||||
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Order("tanggal ASC").
|
||||
Scan(&dokterRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dokterRows {
|
||||
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
|
||||
}
|
||||
|
||||
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
|
||||
dpjpMap := make(map[int]int)
|
||||
var dpjpRows []struct {
|
||||
ID_Billing int
|
||||
ID_DPJP int
|
||||
}
|
||||
if err := db.Table("\"billing_dpjp\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_DPJP\"").
|
||||
Scan(&dpjpRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dpjpRows {
|
||||
dpjpMap[row.ID_Billing] = row.ID_DPJP
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang bagus
|
||||
var result []models.Request_Admin_Inacbg
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
item := models.Request_Admin_Inacbg{
|
||||
ID_Billing: b.ID_Billing,
|
||||
Nama_pasien: pasien.Nama_Pasien,
|
||||
ID_Pasien: pasien.ID_Pasien,
|
||||
Kelas: pasien.Kelas,
|
||||
Ruangan: pasien.Ruangan,
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
ID_DPJP: dpjpMap[b.ID_Billing],
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
INACBG_RI: inacbgRIMap[b.ID_Billing],
|
||||
INACBG_RJ: inacbgRJMap[b.ID_Billing],
|
||||
Billing_sign: b.Billing_sign,
|
||||
Nama_Dokter: dokterMap[b.ID_Billing],
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Ambil tarif BPJS untuk rawat inap yaa
|
||||
func GetTarifBPJSRawatInap() ([]models.TarifBPJSRawatInap, error) {
|
||||
var data []models.TarifBPJSRawatInap
|
||||
if err := database.DB.Model(&models.TarifBPJSRawatInap{}).Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetTarifBPJSRawatInapByKode(kode string) (*models.TarifBPJSRawatInap, error) {
|
||||
var data models.TarifBPJSRawatInap
|
||||
if err := database.DB.Model(&models.TarifBPJSRawatInap{}).Where("ID_INACBG_RI = ?", kode).First(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// Ngambil tarif untuk pasien rawat jalan
|
||||
func GetTarifBPJSRawatJalan() ([]models.TarifBPJSRawatJalan, error) {
|
||||
var data []models.TarifBPJSRawatJalan
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetTarifBPJSRawatJalanByKode(kode string) (*models.TarifBPJSRawatJalan, error) {
|
||||
var data models.TarifBPJSRawatJalan
|
||||
if err := database.DB.Where("ID_INACBG_RJ = ?", kode).First(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// Ambil tarif rumah sakit aja bro
|
||||
func GetTarifRS() ([]models.TarifRS, error) {
|
||||
var data []models.TarifRS
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetTarifRSByKode(kode string) (*models.TarifRS, error) {
|
||||
var data models.TarifRS
|
||||
if err := database.DB.Where("ID_Tarif_RS = ?", kode).First(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func GetTarifRSByKategori(kategori string) ([]models.TarifRS, error) {
|
||||
var data []models.TarifRS
|
||||
if err := database.DB.Where("Kategori_RS = ?", kategori).Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// Ambil data ICD9 - kode diagnosa versi lama
|
||||
func GetICD9() ([]models.ICD9, error) {
|
||||
var data []models.ICD9
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Ambil data ICD10 - kode diagnosa versi baru
|
||||
func GetICD10() ([]models.ICD10, error) {
|
||||
var data []models.ICD10
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Ambil daftar semua ruangan di RS
|
||||
func GetRuangan() ([]models.Ruangan, error) {
|
||||
var data []models.Ruangan
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetRuanganWithPasien - Get ruangan yang memiliki minimal 1 pasien
|
||||
func GetRuanganWithPasien(db *gorm.DB) ([]models.Ruangan, error) {
|
||||
var data []models.Ruangan
|
||||
// JOIN dengan pasien table dan filter yang punya pasien
|
||||
if err := db.
|
||||
Distinct("ruangan.*").
|
||||
Table("ruangan").
|
||||
Joins("INNER JOIN pasien ON ruangan.\"Nama_Ruangan\" = pasien.\"Ruangan\"").
|
||||
Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Ambil list semua dokter yang ada
|
||||
func GetDokter() ([]models.Dokter, error) {
|
||||
var data []models.Dokter
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Billing INACBG</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row h-100">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h5>Ruangan</h5>
|
||||
</div>
|
||||
<div class="ruangan-list" id="ruanganList">
|
||||
<!-- Will be populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 main-content">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2>Data Billing Pasien</h2>
|
||||
<div class="text-muted small" id="currentDate"></div>
|
||||
</div>
|
||||
<div class="search-box mt-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Cari billing pasien dian">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Table -->
|
||||
<div class="billing-table-container">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Pasien</th>
|
||||
<th>Nama</th>
|
||||
<th>Total Tarif RS</th>
|
||||
<th>Total Klaim BPJS</th>
|
||||
<th>Billing Sign</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="billingTableBody">
|
||||
<!-- Will be populated by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Data Pasien</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Patient Info Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Nama Lengkap</h6>
|
||||
<input type="text" id="modalNamaPasien" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ID Pasien</h6>
|
||||
<input type="text" id="modalIdPasien" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">Kelas</h6>
|
||||
<input type="text" id="modalKelas" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dokter yang Menangani -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary fw-bold">Dokter yang Menangani</h6>
|
||||
<div id="dokterList" class="border rounded p-2 bg-light small">
|
||||
<span class="text-muted">Memuat data dokter...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tindakan & ICD - Pisah Lama vs Baru -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary fw-bold">Tindakan RS</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Data yang sudah ada:</small>
|
||||
<div id="tindakanLama" class="border rounded p-2 bg-light small"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Data baru (akan ditambahkan):</small>
|
||||
<div id="tindakanBaru" class="border rounded p-2 bg-light small text-muted">Belum ada data baru</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary fw-bold">ICD 9 & ICD 10</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">ICD 9 - Data yang sudah ada:</small>
|
||||
<div id="icd9Lama" class="border rounded p-2 bg-light small mb-2"></div>
|
||||
<small class="text-muted">ICD 10 - Data yang sudah ada:</small>
|
||||
<div id="icd10Lama" class="border rounded p-2 bg-light small"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">ICD 9 - Data baru:</small>
|
||||
<div id="icd9Baru" class="border rounded p-2 bg-light small mb-2 text-muted">Belum ada data baru</div>
|
||||
<small class="text-muted">ICD 10 - Data baru:</small>
|
||||
<div id="icd10Baru" class="border rounded p-2 bg-light small text-muted">Belum ada data baru</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Total Tarif RS (Kumulatif)</h6>
|
||||
<input type="text" id="modalTotalTarif" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Form -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">INA CBG</h6>
|
||||
<form id="inacbgForm">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tipe INACBG</label>
|
||||
<select id="tipeInacbg" class="form-select">
|
||||
<option value="">-- Pilih Tipe --</option>
|
||||
<option value="RI">RI (Rawat Inap)</option>
|
||||
<option value="RJ">RJ (Rawat Jalan)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Masukkan Kode INA CBGS</label>
|
||||
<div class="input-group">
|
||||
<select id="inacbgCode" class="form-select" disabled>
|
||||
<option value="">-- Pilih Tipe INACBG Dulu --</option>
|
||||
</select>
|
||||
<span class="input-group-text" style="cursor: pointer; user-select: none;" title="Ganti ke input manual" onclick="toggleInacbgInput()">
|
||||
↔️
|
||||
</span>
|
||||
</div>
|
||||
<input type="text" id="inacbgCodeManual" class="form-control d-none mt-2" placeholder="Ketik kode INACBG manual">
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<button type="button" id="addCodeBtn" class="btn btn-primary w-100">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Lama -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">INACBG yang sudah ada sebelumnya:</label>
|
||||
<div id="inacbgLamaContainer" class="border rounded p-2 bg-light small">
|
||||
<div id="inacbgRILama" class="mb-1"></div>
|
||||
<div id="inacbgRJLama"></div>
|
||||
<div id="totalKlaimLama" class="mt-2 fw-bold"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Baru -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">INACBG Baru (akan ditambahkan):</label>
|
||||
<div id="codeList" class="border rounded p-2 bg-light">
|
||||
<small class="text-muted">Belum ada kode baru</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Total Klaim Lama</label>
|
||||
<input type="number" id="totalKlaimLamaInput" class="form-control" placeholder="0" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Total Klaim Baru <span class="text-muted small">(Otomatis)</span></label>
|
||||
<input type="number" id="totalKlaim" class="form-control" placeholder="0" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Total Klaim Akhir</label>
|
||||
<input type="number" id="totalKlaimAkhir" class="form-control fw-bold" placeholder="0" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Billing Sign</label>
|
||||
<div id="billingSignContainer" class="mt-1">
|
||||
<span id="billingSignBadge" class="badge bg-secondary">-</span>
|
||||
<span id="billingSignText" class="ms-2 text-muted small"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tanggal Keluar</label>
|
||||
<input type="date" id="tanggalKeluar" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="formAlert" class="alert d-none" role="alert"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="scriptAdmin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Billing INACBG</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row h-100">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h5>Ruangan</h5>
|
||||
</div>
|
||||
<div class="ruangan-list" id="ruanganList">
|
||||
<!-- Will be populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 main-content">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2>Data Billing Pasien</h2>
|
||||
<div class="text-muted small" id="currentDate"></div>
|
||||
</div>
|
||||
<div class="search-box mt-3">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Cari billing pasien dian">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Table -->
|
||||
<div class="billing-table-container">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID Pasien</th>
|
||||
<th>Nama</th>
|
||||
<th>Billing Sign</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="billingTableBody">
|
||||
<!-- Will be populated by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">Data Pasien</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Patient Info Section -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Nama Lengkap</h6>
|
||||
<input type="text" id="modalNamaPasien" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ID Pasien</h6>
|
||||
<input type="text" id="modalIdPasien" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">Kelas</h6>
|
||||
<input type="text" id="modalKelas" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tindakan & Pemeriksaan -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Tindakan dan Pemeriksaan Penunjang</h6>
|
||||
<input type="text" id="modalTindakan" class="form-control" readonly placeholder="(diambil dari billing)">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">Total Tarif RS</h6>
|
||||
<input type="text" id="modalTotalTarif" class="form-control" readonly>
|
||||
</div>
|
||||
|
||||
<!-- ICD Codes -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ICD 9</h6>
|
||||
<input type="text" id="modalICD9" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6 class="text-secondary">ICD 10</h6>
|
||||
<input type="text" id="modalICD10" class="form-control" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INACBG Form -->
|
||||
<div class="mb-4">
|
||||
<h6 class="text-secondary">INA CBG</h6>
|
||||
<form id="inacbgForm">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Tipe INACBG</label>
|
||||
<select id="tipeInacbg" class="form-select">
|
||||
<option value="">-- Pilih Tipe --</option>
|
||||
<option value="RI">RI (Rawat Inap)</option>
|
||||
<option value="RJ">RJ (Rawat Jalan)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Masukkan Kode INA CBGS</label>
|
||||
<select id="inacbgCode" class="form-select" disabled>
|
||||
<option value="">-- Pilih Tipe INACBG Dulu --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<button type="button" id="addCodeBtn" class="btn btn-primary">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="codeList" class="mb-3">
|
||||
<!-- Added codes will appear here -->
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Total Klaim BJPS <span class="text-muted small">(Otomatis)</span></label>
|
||||
<input type="number" id="totalKlaim" class="form-control" placeholder="0" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="formAlert" class="alert d-none" role="alert"></div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-success">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="scriptAdmin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,715 @@
|
||||
// Configuration
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
let billingData = [];
|
||||
let currentEditingBilling = null;
|
||||
let inacbgCodes = [];
|
||||
let tarifCache = {}; // Cache for tarif data
|
||||
let isManualInacbgMode = false; // Track if user is in manual input mode
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateCurrentDate();
|
||||
loadBillingData();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Update current date
|
||||
function updateCurrentDate() {
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const today = new Date().toLocaleDateString('id-ID', options);
|
||||
document.getElementById('currentDate').textContent = today;
|
||||
}
|
||||
|
||||
// Load billing data from API
|
||||
async function loadBillingData() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/billing`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
billingData = data.data || [];
|
||||
console.log('Billing data loaded:', billingData);
|
||||
// Debug: cek apakah total_klaim ada di response
|
||||
if (billingData.length > 0) {
|
||||
console.log('Sample billing item:', billingData[0]);
|
||||
console.log('Total klaim dari sample:', billingData[0].total_klaim);
|
||||
}
|
||||
|
||||
renderBillingTable();
|
||||
renderRuanganSidebar();
|
||||
} catch (err) {
|
||||
console.error('Error loading billing data:', err);
|
||||
document.getElementById('billingTableBody').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-danger">Gagal memuat data: ${err.message}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render billing table
|
||||
function renderBillingTable() {
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (billingData.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Tidak ada data billing</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
billingData.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
|
||||
const totalTarif = billing.total_tarif_rs || 0;
|
||||
const totalKlaim = billing.total_klaim || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>Rp ${Number(totalTarif).toLocaleString('id-ID')}</td>
|
||||
<td>Rp ${Number(totalKlaim).toLocaleString('id-ID')}</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get billing sign badge class and color
|
||||
function getBillingSignColor(billingSign) {
|
||||
const normalizedSign = (billingSign || '').toString().toLowerCase();
|
||||
switch (normalizedSign) {
|
||||
case 'hijau':
|
||||
return '#28a745';
|
||||
case 'kuning':
|
||||
return '#ffc107';
|
||||
case 'orange':
|
||||
return '#fd7e14';
|
||||
case 'merah':
|
||||
case 'created':
|
||||
return '#dc3545';
|
||||
default:
|
||||
return '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
function getBillingSignBadgeClass(billingSign) {
|
||||
const normalizedSign = (billingSign || '').toString().toLowerCase();
|
||||
switch (normalizedSign) {
|
||||
case 'hijau':
|
||||
return 'hijau';
|
||||
case 'kuning':
|
||||
return 'kuning';
|
||||
case 'orange':
|
||||
return 'orange';
|
||||
case 'merah':
|
||||
return 'merah';
|
||||
case 'created':
|
||||
return 'created';
|
||||
default:
|
||||
return 'created';
|
||||
}
|
||||
}
|
||||
|
||||
// Render ruangan sidebar
|
||||
function renderRuanganSidebar() {
|
||||
const uniqueRuangans = [...new Set(billingData.map(b => b.ruangan))];
|
||||
const ruanganList = document.getElementById('ruanganList');
|
||||
ruanganList.innerHTML = '';
|
||||
|
||||
if (uniqueRuangans.length === 0) {
|
||||
ruanganList.innerHTML = '<p class="text-muted">Tidak ada ruangan</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueRuangans.forEach((ruangan, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'ruangan-item';
|
||||
item.textContent = ruangan || `Ruangan ${index + 1}`;
|
||||
item.onclick = () => filterByRuangan(ruangan);
|
||||
ruanganList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter billing by ruangan
|
||||
function filterByRuangan(ruangan) {
|
||||
const filtered = billingData.filter(b => b.ruangan === ruangan);
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Tidak ada data untuk ruangan ini</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
const totalTarif = billing.total_tarif_rs || 0;
|
||||
const totalKlaim = billing.total_klaim || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>Rp ${Number(totalTarif).toLocaleString('id-ID')}</td>
|
||||
<td>Rp ${Number(totalKlaim).toLocaleString('id-ID')}</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
function openEditModal(billingId) {
|
||||
currentEditingBilling = billingData.find(b => b.id_billing === billingId);
|
||||
if (!currentEditingBilling) {
|
||||
alert('Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate modal with billing data
|
||||
document.getElementById('modalNamaPasien').value = currentEditingBilling.nama_pasien || '';
|
||||
document.getElementById('modalIdPasien').value = currentEditingBilling.id_pasien || '';
|
||||
document.getElementById('modalKelas').value = currentEditingBilling.Kelas || '';
|
||||
|
||||
// Tampilkan dokter yang menangani pasien
|
||||
const dokterList = currentEditingBilling.nama_dokter || [];
|
||||
const dokterListEl = document.getElementById('dokterList');
|
||||
if (dokterList.length > 0) {
|
||||
dokterListEl.innerHTML = dokterList.map(dokter =>
|
||||
`<span class="badge bg-info me-2 mb-1">${dokter}</span>`
|
||||
).join('');
|
||||
} else {
|
||||
dokterListEl.innerHTML = '<span class="text-muted">Belum ada data dokter</span>';
|
||||
}
|
||||
|
||||
// Total tarif & total klaim kumulatif
|
||||
// Handle berbagai kemungkinan nama field (case-insensitive)
|
||||
const totalTarif = Number(currentEditingBilling.total_tarif_rs || currentEditingBilling.Total_Tarif_RS || 0);
|
||||
const totalKlaimLama = Number(currentEditingBilling.total_klaim || currentEditingBilling.Total_Klaim || currentEditingBilling.total_klaim_lama || 0);
|
||||
document.getElementById('modalTotalTarif').value = totalTarif.toLocaleString('id-ID');
|
||||
|
||||
// Tindakan RS - semua yang ada sekarang = "lama" (karena tidak ada cara membedakan mana yang baru)
|
||||
const tindakanLama = currentEditingBilling.tindakan_rs || [];
|
||||
document.getElementById('tindakanLama').textContent = tindakanLama.length > 0 ? tindakanLama.join(', ') : 'Tidak ada';
|
||||
document.getElementById('tindakanBaru').textContent = 'Belum ada data baru';
|
||||
|
||||
// ICD9 & ICD10 - semua yang ada sekarang = "lama"
|
||||
const icd9Lama = currentEditingBilling.icd9 || [];
|
||||
const icd10Lama = currentEditingBilling.icd10 || [];
|
||||
document.getElementById('icd9Lama').textContent = icd9Lama.length > 0 ? icd9Lama.join(', ') : 'Tidak ada';
|
||||
document.getElementById('icd10Lama').textContent = icd10Lama.length > 0 ? icd10Lama.join(', ') : 'Tidak ada';
|
||||
document.getElementById('icd9Baru').textContent = 'Belum ada data baru';
|
||||
document.getElementById('icd10Baru').textContent = 'Belum ada data baru';
|
||||
|
||||
// INACBG Lama
|
||||
const existingRI = currentEditingBilling.inacbg_ri || [];
|
||||
const existingRJ = currentEditingBilling.inacbg_rj || [];
|
||||
const inacbgRILamaEl = document.getElementById('inacbgRILama');
|
||||
const inacbgRJLamaEl = document.getElementById('inacbgRJLama');
|
||||
const totalKlaimLamaEl = document.getElementById('totalKlaimLama');
|
||||
|
||||
// Debug: log untuk cek data yang diterima
|
||||
console.log('=== DEBUG TOTAL KLAIM LAMA ===');
|
||||
console.log('Current editing billing:', currentEditingBilling);
|
||||
console.log('total_klaim:', currentEditingBilling.total_klaim);
|
||||
console.log('Total_Klaim:', currentEditingBilling.Total_Klaim);
|
||||
console.log('total_klaim_lama:', currentEditingBilling.total_klaim_lama);
|
||||
console.log('Total klaim lama (processed):', totalKlaimLama);
|
||||
console.log('All keys in billing object:', Object.keys(currentEditingBilling));
|
||||
console.log('================================');
|
||||
|
||||
if (existingRI.length > 0) {
|
||||
inacbgRILamaEl.innerHTML = `<strong>RI:</strong> ${existingRI.join(', ')}`;
|
||||
} else {
|
||||
inacbgRILamaEl.textContent = 'RI: Tidak ada';
|
||||
}
|
||||
|
||||
if (existingRJ.length > 0) {
|
||||
inacbgRJLamaEl.innerHTML = `<strong>RJ:</strong> ${existingRJ.join(', ')}`;
|
||||
} else {
|
||||
inacbgRJLamaEl.textContent = 'RJ: Tidak ada';
|
||||
}
|
||||
|
||||
// Tampilkan total klaim lama (selalu tampilkan, meskipun 0)
|
||||
totalKlaimLamaEl.textContent = `Total Klaim Lama: Rp ${totalKlaimLama.toLocaleString('id-ID')}`;
|
||||
|
||||
// Set total klaim lama di input
|
||||
document.getElementById('totalKlaimLamaInput').value = totalKlaimLama.toFixed(0);
|
||||
|
||||
// Set tanggal keluar jika ada
|
||||
// (akan diisi oleh admin, jadi kosong dulu)
|
||||
document.getElementById('tanggalKeluar').value = '';
|
||||
|
||||
// Reset INACBG form
|
||||
inacbgCodes = [];
|
||||
isManualInacbgMode = false;
|
||||
document.getElementById('inacbgCode').value = '';
|
||||
document.getElementById('inacbgCode').disabled = true;
|
||||
document.getElementById('inacbgCode').classList.remove('d-none');
|
||||
document.getElementById('inacbgCodeManual').value = '';
|
||||
document.getElementById('inacbgCodeManual').classList.add('d-none');
|
||||
document.getElementById('inacbgCode').innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
document.getElementById('tipeInacbg').value = '';
|
||||
document.getElementById('totalKlaim').value = '0';
|
||||
document.getElementById('codeList').innerHTML = '<small class="text-muted">Belum ada kode baru</small>';
|
||||
document.getElementById('totalKlaimAkhir').value = totalKlaimLama.toFixed(0);
|
||||
document.getElementById('formAlert').classList.add('d-none');
|
||||
|
||||
// Update billing sign display awal
|
||||
updateBillingSignDisplay();
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Toggle between dropdown and manual input
|
||||
function toggleInacbgInput() {
|
||||
isManualInacbgMode = !isManualInacbgMode;
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
const codeManual = document.getElementById('inacbgCodeManual');
|
||||
|
||||
if (isManualInacbgMode) {
|
||||
// Switch to manual input
|
||||
codeSelect.classList.add('d-none');
|
||||
codeManual.classList.remove('d-none');
|
||||
codeManual.focus();
|
||||
codeManual.value = '';
|
||||
} else {
|
||||
// Switch back to dropdown
|
||||
codeSelect.classList.remove('d-none');
|
||||
codeManual.classList.add('d-none');
|
||||
codeManual.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Tipe INACBG change
|
||||
document.getElementById('tipeInacbg').addEventListener('change', loadInacbgCodes);
|
||||
|
||||
// Add code button
|
||||
document.getElementById('addCodeBtn').addEventListener('click', addInacbgCode);
|
||||
|
||||
// INACBG form submit
|
||||
document.getElementById('inacbgForm').addEventListener('submit', submitInacbgForm);
|
||||
|
||||
// Search input
|
||||
document.getElementById('searchInput').addEventListener('input', searchBilling);
|
||||
|
||||
// Manual input enter key
|
||||
document.getElementById('inacbgCodeManual').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addInacbgCode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load INACBG codes based on tipe
|
||||
async function loadInacbgCodes() {
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
|
||||
if (!tipe) {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = tipe === 'RI' ? '/tarifBPJSRawatInap' : '/tarifBPJSRawatJalan';
|
||||
|
||||
try {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">Memuat...</option>';
|
||||
|
||||
// Check cache first
|
||||
if (!tarifCache[tipe]) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
tarifCache[tipe] = await res.json();
|
||||
}
|
||||
|
||||
const data = tarifCache[tipe] || [];
|
||||
const items = Array.isArray(data) ? data : [];
|
||||
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Kode --</option>';
|
||||
codeSelect.disabled = false;
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
// Use KodeINA as value and Deskripsi as display text
|
||||
option.value = item.KodeINA || item.kodeINA || item.KodeINA || '';
|
||||
option.textContent = item.Deskripsi || item.deskripsi || item.Deskripsi || '';
|
||||
|
||||
// If value is empty but we have other fields, try alternatives
|
||||
if (!option.value) {
|
||||
option.value = item.KodeINA_RJ || item.kodeINA_RJ || item.KodeINA_RI || item.kodeINA_RI || '';
|
||||
}
|
||||
|
||||
codeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
console.log(`Loaded ${items.length} INACBG codes for type ${tipe}`);
|
||||
} catch (err) {
|
||||
console.error('Error loading INACBG codes:', err);
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = `<option value="">Error: ${err.message}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Get tarif for a code from cache or return 0
|
||||
function getTarifForCode(code, tipe, kelas = null) {
|
||||
let tarif = 0;
|
||||
const tarifData = tarifCache[tipe] || [];
|
||||
const tarifItem = tarifData.find(item => (item.KodeINA || item.kodeINA) === code);
|
||||
|
||||
if (tarifItem) {
|
||||
if (tipe === 'RI') {
|
||||
// Get tarif based on patient class
|
||||
if (!kelas) kelas = currentEditingBilling.Kelas;
|
||||
if (kelas === '1') {
|
||||
tarif = tarifItem.Kelas1 || 0;
|
||||
} else if (kelas === '2') {
|
||||
tarif = tarifItem.Kelas2 || 0;
|
||||
} else if (kelas === '3') {
|
||||
tarif = tarifItem.Kelas3 || 0;
|
||||
}
|
||||
} else if (tipe === 'RJ') {
|
||||
// Get tarif directly from TarifINACBG field
|
||||
tarif = tarifItem.TarifINACBG || tarifItem.tarif_inacbg || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return tarif;
|
||||
}
|
||||
|
||||
// Add INACBG code (from dropdown or manual input)
|
||||
async function addInacbgCode() {
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
|
||||
if (!tipe) {
|
||||
alert('Pilih tipe INACBG terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
let code = '';
|
||||
let codeText = '';
|
||||
|
||||
if (isManualInacbgMode) {
|
||||
// Manual input mode
|
||||
const manualInput = document.getElementById('inacbgCodeManual').value.trim().toUpperCase();
|
||||
if (!manualInput) {
|
||||
alert('Masukkan kode INACBG');
|
||||
return;
|
||||
}
|
||||
code = manualInput;
|
||||
codeText = manualInput; // Manual input, use code as text
|
||||
} else {
|
||||
// Dropdown mode
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
const selectedOption = codeSelect.options[codeSelect.selectedIndex];
|
||||
code = codeSelect.value.trim();
|
||||
codeText = selectedOption.textContent.trim();
|
||||
|
||||
if (!code) {
|
||||
alert('Pilih kode INACBG terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (inacbgCodes.some(c => c.value === code)) {
|
||||
alert('Kode sudah ditambahkan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tarif for this code
|
||||
const tarif = getTarifForCode(code, tipe);
|
||||
|
||||
inacbgCodes.push({ value: code, text: codeText, tarif: tarif });
|
||||
|
||||
// Clear input/select
|
||||
if (isManualInacbgMode) {
|
||||
document.getElementById('inacbgCodeManual').value = '';
|
||||
} else {
|
||||
document.getElementById('inacbgCode').value = '';
|
||||
}
|
||||
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after adding code
|
||||
}
|
||||
|
||||
// Render code list
|
||||
function renderCodeList() {
|
||||
const codeList = document.getElementById('codeList');
|
||||
codeList.innerHTML = '';
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
codeList.innerHTML = '<small class="text-muted">Belum ada kode baru</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
inacbgCodes.forEach((codeObj, index) => {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'code-badge';
|
||||
const tarifDisplay = codeObj.tarif ? `(Rp${codeObj.tarif.toLocaleString('id-ID')})` : '';
|
||||
badge.innerHTML = `
|
||||
${codeObj.text || codeObj.value} ${tarifDisplay}
|
||||
<span class="remove-btn" onclick="removeInacbgCode(${index})">×</span>
|
||||
`;
|
||||
codeList.appendChild(badge);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total klaim dari kode baru SAJA (lama sudah tercatat di total_klaim backend)
|
||||
function calculateTotalKlaim() {
|
||||
const totalBaru = inacbgCodes.reduce((sum, code) => sum + (code.tarif || 0), 0);
|
||||
document.getElementById('totalKlaim').value = totalBaru.toFixed(0);
|
||||
|
||||
// Hitung total klaim akhir = lama + baru
|
||||
const totalKlaimLama = parseFloat(document.getElementById('totalKlaimLamaInput').value) || 0;
|
||||
const totalKlaimAkhir = totalKlaimLama + totalBaru;
|
||||
document.getElementById('totalKlaimAkhir').value = totalKlaimAkhir.toFixed(0);
|
||||
|
||||
// Update billing sign display berdasarkan total tarif RS kumulatif vs total klaim akhir
|
||||
updateBillingSignDisplay();
|
||||
}
|
||||
|
||||
// Remove INACBG code
|
||||
function removeInacbgCode(index) {
|
||||
inacbgCodes.splice(index, 1);
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after removing code
|
||||
}
|
||||
|
||||
// Hitung billing sign berdasarkan rumus:
|
||||
// persentase = (total_tarif_rs / total_klaim_akhir) * 100
|
||||
function calculateBillingSign() {
|
||||
// totalTarifRs sudah kumulatif (lama + baru) dari backend
|
||||
const totalTarifRsStr = document.getElementById('modalTotalTarif').value.replace(/[^\d]/g, '');
|
||||
const totalTarifRs = parseFloat(totalTarifRsStr) || 0;
|
||||
|
||||
// total klaim akhir = lama + baru
|
||||
const totalKlaimAkhir = parseFloat(document.getElementById('totalKlaimAkhir').value) || 0;
|
||||
|
||||
if (totalTarifRs <= 0 || totalKlaimAkhir <= 0) {
|
||||
return { sign: null, percentage: 0 };
|
||||
}
|
||||
|
||||
const percentage = (totalTarifRs / totalKlaimAkhir) * 100;
|
||||
let sign = 'hijau';
|
||||
|
||||
if (percentage <= 25) {
|
||||
sign = 'hijau';
|
||||
} else if (percentage >= 26 && percentage <= 50) {
|
||||
sign = 'kuning';
|
||||
} else if (percentage >= 51 && percentage <= 75) {
|
||||
sign = 'orange';
|
||||
} else if (percentage >= 76) {
|
||||
sign = 'merah';
|
||||
}
|
||||
|
||||
return { sign, percentage };
|
||||
}
|
||||
|
||||
// Update tampilan billing sign di modal
|
||||
function updateBillingSignDisplay() {
|
||||
const container = document.getElementById('billingSignContainer');
|
||||
const badgeEl = document.getElementById('billingSignBadge');
|
||||
const textEl = document.getElementById('billingSignText');
|
||||
|
||||
if (!container || !badgeEl || !textEl) return;
|
||||
|
||||
const { sign, percentage } = calculateBillingSign();
|
||||
|
||||
if (!sign) {
|
||||
badgeEl.className = 'badge bg-secondary';
|
||||
badgeEl.textContent = '-';
|
||||
textEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const color = getBillingSignColor(sign);
|
||||
badgeEl.className = 'badge';
|
||||
badgeEl.style.backgroundColor = color;
|
||||
badgeEl.textContent = sign.toUpperCase();
|
||||
|
||||
const roundedPct = percentage.toFixed(2);
|
||||
textEl.textContent = `Tarif RS ≈ ${roundedPct}% dari BPJS`;
|
||||
}
|
||||
|
||||
// Format billing sign ke Title Case agar sesuai enum di DB
|
||||
function formatBillingSignValue(sign) {
|
||||
if (!sign) return '';
|
||||
const lower = sign.toLowerCase();
|
||||
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
||||
}
|
||||
|
||||
// Submit INACBG form
|
||||
async function submitInacbgForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const tipeInacbg = document.getElementById('tipeInacbg').value.trim();
|
||||
// total klaim BARU (tambahan); lama sudah tersimpan di backend
|
||||
const totalKlaimBaru = parseFloat(document.getElementById('totalKlaim').value) || 0;
|
||||
|
||||
// Validation
|
||||
if (!currentEditingBilling) {
|
||||
showAlert('danger', 'Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
showAlert('danger', 'Tambahkan minimal satu kode INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tipeInacbg) {
|
||||
showAlert('danger', 'Pilih tipe INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalKlaimBaru === 0) {
|
||||
showAlert('danger', 'Total klaim tambahan tidak boleh 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hitung billing sign berdasarkan total tarif RS dan total klaim
|
||||
const { sign: billingSign } = calculateBillingSign();
|
||||
const formattedBillingSign = formatBillingSignValue(billingSign);
|
||||
|
||||
// Ambil tanggal keluar jika diisi
|
||||
const tanggalKeluar = document.getElementById('tanggalKeluar').value.trim();
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
id_billing: currentEditingBilling.id_billing,
|
||||
tipe_inacbg: tipeInacbg,
|
||||
kode_inacbg: inacbgCodes.map(c => c.value), // Extract just the codes
|
||||
total_klaim: totalKlaimBaru, // Total klaim BARU saja (akan ditambahkan ke yang lama di backend)
|
||||
billing_sign: formattedBillingSign, // kirim billing sign sesuai enum DB
|
||||
tanggal_keluar: tanggalKeluar || '' // Tanggal keluar diisi oleh admin
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/inacbg`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.error || result.message || 'Gagal menyimpan INACBG');
|
||||
}
|
||||
|
||||
showAlert('success', 'INACBG berhasil disimpan');
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
loadBillingData();
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
showAlert('danger', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert in modal
|
||||
function showAlert(type, message) {
|
||||
const alert = document.getElementById('formAlert');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
alert.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Search billing
|
||||
function searchBilling(e) {
|
||||
const keyword = e.target.value.toLowerCase().trim();
|
||||
|
||||
if (keyword === '') {
|
||||
renderBillingTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = billingData.filter(b =>
|
||||
(b.nama_pasien && b.nama_pasien.toLowerCase().includes(keyword)) ||
|
||||
(b.id_pasien && b.id_pasien.toString().includes(keyword))
|
||||
);
|
||||
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">Tidak ada hasil pencarian</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
const totalTarif = billing.total_tarif_rs || 0;
|
||||
const totalKlaim = billing.total_klaim || 0;
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>Rp ${Number(totalTarif).toLocaleString('id-ID')}</td>
|
||||
<td>Rp ${Number(totalKlaim).toLocaleString('id-ID')}</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// Configuration
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
let billingData = [];
|
||||
let currentEditingBilling = null;
|
||||
let inacbgCodes = [];
|
||||
let tarifCache = {}; // Cache for tarif data
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateCurrentDate();
|
||||
loadBillingData();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
// Update current date
|
||||
function updateCurrentDate() {
|
||||
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
const today = new Date().toLocaleDateString('id-ID', options);
|
||||
document.getElementById('currentDate').textContent = today;
|
||||
}
|
||||
|
||||
// Load billing data from API
|
||||
async function loadBillingData() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/billing`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const data = await res.json();
|
||||
billingData = data.data || [];
|
||||
console.log('Billing data loaded:', billingData);
|
||||
|
||||
renderBillingTable();
|
||||
renderRuanganSidebar();
|
||||
} catch (err) {
|
||||
console.error('Error loading billing data:', err);
|
||||
document.getElementById('billingTableBody').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-danger">Gagal memuat data: ${err.message}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render billing table
|
||||
function renderBillingTable() {
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (billingData.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Tidak ada data billing</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
billingData.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Get billing sign badge class and color
|
||||
function getBillingSignColor(billingSign) {
|
||||
switch (billingSign) {
|
||||
case 'hijau':
|
||||
return '#28a745';
|
||||
case 'kuning':
|
||||
return '#ffc107';
|
||||
case 'merah':
|
||||
case 'created':
|
||||
return '#dc3545';
|
||||
default:
|
||||
return '#6c757d';
|
||||
}
|
||||
}
|
||||
|
||||
function getBillingSignBadgeClass(billingSign) {
|
||||
switch (billingSign) {
|
||||
case 'hijau':
|
||||
return 'hijau';
|
||||
case 'kuning':
|
||||
return 'kuning';
|
||||
case 'merah':
|
||||
return 'merah';
|
||||
case 'created':
|
||||
return 'created';
|
||||
default:
|
||||
return 'created';
|
||||
}
|
||||
}
|
||||
|
||||
// Render ruangan sidebar
|
||||
function renderRuanganSidebar() {
|
||||
const uniqueRuangans = [...new Set(billingData.map(b => b.ruangan))];
|
||||
const ruanganList = document.getElementById('ruanganList');
|
||||
ruanganList.innerHTML = '';
|
||||
|
||||
if (uniqueRuangans.length === 0) {
|
||||
ruanganList.innerHTML = '<p class="text-muted">Tidak ada ruangan</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueRuangans.forEach((ruangan, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'ruangan-item';
|
||||
item.textContent = ruangan || `Ruangan ${index + 1}`;
|
||||
item.onclick = () => filterByRuangan(ruangan);
|
||||
ruanganList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter billing by ruangan
|
||||
function filterByRuangan(ruangan) {
|
||||
const filtered = billingData.filter(b => b.ruangan === ruangan);
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Tidak ada data untuk ruangan ini</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Open edit modal
|
||||
function openEditModal(billingId) {
|
||||
currentEditingBilling = billingData.find(b => b.id_billing === billingId);
|
||||
if (!currentEditingBilling) {
|
||||
alert('Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate modal with billing data
|
||||
document.getElementById('modalNamaPasien').value = currentEditingBilling.nama_pasien || '';
|
||||
document.getElementById('modalIdPasien').value = currentEditingBilling.id_pasien || '';
|
||||
document.getElementById('modalKelas').value = currentEditingBilling.Kelas || '';
|
||||
document.getElementById('modalTindakan').value = (currentEditingBilling.tindakan_rs || []).join(', ') || '';
|
||||
document.getElementById('modalTotalTarif').value = currentEditingBilling.total_tarif_rs || '';
|
||||
document.getElementById('modalICD9').value = (currentEditingBilling.icd9 || []).join(', ') || '';
|
||||
document.getElementById('modalICD10').value = (currentEditingBilling.icd10 || []).join(', ') || '';
|
||||
|
||||
// Reset INACBG form
|
||||
inacbgCodes = [];
|
||||
document.getElementById('inacbgCode').value = '';
|
||||
document.getElementById('inacbgCode').disabled = true;
|
||||
document.getElementById('inacbgCode').innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
document.getElementById('tipeInacbg').value = '';
|
||||
document.getElementById('totalKlaim').value = '';
|
||||
document.getElementById('codeList').innerHTML = '';
|
||||
document.getElementById('formAlert').classList.add('d-none');
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners() {
|
||||
// Tipe INACBG change
|
||||
document.getElementById('tipeInacbg').addEventListener('change', loadInacbgCodes);
|
||||
|
||||
// Add code button
|
||||
document.getElementById('addCodeBtn').addEventListener('click', addInacbgCode);
|
||||
|
||||
// INACBG form submit
|
||||
document.getElementById('inacbgForm').addEventListener('submit', submitInacbgForm);
|
||||
|
||||
// Search input
|
||||
document.getElementById('searchInput').addEventListener('input', searchBilling);
|
||||
}
|
||||
|
||||
// Load INACBG codes based on tipe
|
||||
async function loadInacbgCodes() {
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
|
||||
if (!tipe) {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Tipe INACBG Dulu --</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = tipe === 'RI' ? '/tarifBPJSRawatInap' : '/tarifBPJSRawatJalan';
|
||||
|
||||
try {
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = '<option value="">Memuat...</option>';
|
||||
|
||||
// Check cache first
|
||||
if (!tarifCache[tipe]) {
|
||||
const res = await fetch(`${API_BASE}${endpoint}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
tarifCache[tipe] = await res.json();
|
||||
}
|
||||
|
||||
const data = tarifCache[tipe] || [];
|
||||
const items = Array.isArray(data) ? data : [];
|
||||
|
||||
codeSelect.innerHTML = '<option value="">-- Pilih Kode --</option>';
|
||||
codeSelect.disabled = false;
|
||||
|
||||
items.forEach(item => {
|
||||
const option = document.createElement('option');
|
||||
// Use KodeINA as value and Deskripsi as display text
|
||||
option.value = item.KodeINA || item.kodeINA || item.KodeINA || '';
|
||||
option.textContent = item.Deskripsi || item.deskripsi || item.Deskripsi || '';
|
||||
|
||||
// If value is empty but we have other fields, try alternatives
|
||||
if (!option.value) {
|
||||
option.value = item.KodeINA_RJ || item.kodeINA_RJ || item.KodeINA_RI || item.kodeINA_RI || '';
|
||||
}
|
||||
|
||||
codeSelect.appendChild(option);
|
||||
});
|
||||
|
||||
console.log(`Loaded ${items.length} INACBG codes for type ${tipe}`);
|
||||
} catch (err) {
|
||||
console.error('Error loading INACBG codes:', err);
|
||||
codeSelect.disabled = true;
|
||||
codeSelect.innerHTML = `<option value="">Error: ${err.message}</option>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add INACBG code
|
||||
async function addInacbgCode() {
|
||||
const codeSelect = document.getElementById('inacbgCode');
|
||||
const selectedOption = codeSelect.options[codeSelect.selectedIndex];
|
||||
const code = codeSelect.value.trim();
|
||||
const codeText = selectedOption.textContent.trim();
|
||||
const tipe = document.getElementById('tipeInacbg').value;
|
||||
|
||||
if (!code) {
|
||||
alert('Pilih kode INACBG terlebih dahulu');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inacbgCodes.some(c => c.value === code)) {
|
||||
alert('Kode sudah ditambahkan');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get tarif for this code
|
||||
let tarif = 0;
|
||||
const tarifData = tarifCache[tipe] || [];
|
||||
const tarifItem = tarifData.find(item => (item.KodeINA || item.kodeINA) === code);
|
||||
|
||||
if (tarifItem) {
|
||||
if (tipe === 'RI') {
|
||||
// Get tarif based on patient class
|
||||
const kelas = currentEditingBilling.Kelas;
|
||||
if (kelas === '1') {
|
||||
tarif = tarifItem.Kelas1 || 0;
|
||||
} else if (kelas === '2') {
|
||||
tarif = tarifItem.Kelas2 || 0;
|
||||
} else if (kelas === '3') {
|
||||
tarif = tarifItem.Kelas3 || 0;
|
||||
}
|
||||
} else if (tipe === 'RJ') {
|
||||
// Get tarif directly from TarifINACBG field
|
||||
tarif = tarifItem.TarifINACBG || tarifItem.tarif_inacbg || 0;
|
||||
}
|
||||
}
|
||||
|
||||
inacbgCodes.push({ value: code, text: codeText, tarif: tarif });
|
||||
codeSelect.value = '';
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after adding code
|
||||
}
|
||||
|
||||
// Render code list
|
||||
function renderCodeList() {
|
||||
const codeList = document.getElementById('codeList');
|
||||
codeList.innerHTML = '';
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
codeList.innerHTML = '<p class="text-muted small">Belum ada kode</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
inacbgCodes.forEach((codeObj, index) => {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'code-badge';
|
||||
const tarifDisplay = codeObj.tarif ? `(Rp${codeObj.tarif.toLocaleString('id-ID')})` : '';
|
||||
badge.innerHTML = `
|
||||
${codeObj.text || codeObj.value} ${tarifDisplay}
|
||||
<span class="remove-btn" onclick="removeInacbgCode(${index})">×</span>
|
||||
`;
|
||||
codeList.appendChild(badge);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total klaim from selected codes
|
||||
function calculateTotalKlaim() {
|
||||
const total = inacbgCodes.reduce((sum, code) => sum + (code.tarif || 0), 0);
|
||||
document.getElementById('totalKlaim').value = total.toFixed(0);
|
||||
}
|
||||
|
||||
// Remove INACBG code
|
||||
function removeInacbgCode(index) {
|
||||
inacbgCodes.splice(index, 1);
|
||||
renderCodeList();
|
||||
calculateTotalKlaim(); // Update total after removing code
|
||||
}
|
||||
|
||||
// Submit INACBG form
|
||||
async function submitInacbgForm(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const tipeInacbg = document.getElementById('tipeInacbg').value.trim();
|
||||
const totalKlaim = parseFloat(document.getElementById('totalKlaim').value) || 0;
|
||||
|
||||
// Validation
|
||||
if (!currentEditingBilling) {
|
||||
showAlert('danger', 'Data billing tidak ditemukan');
|
||||
return;
|
||||
}
|
||||
|
||||
if (inacbgCodes.length === 0) {
|
||||
showAlert('danger', 'Tambahkan minimal satu kode INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tipeInacbg) {
|
||||
showAlert('danger', 'Pilih tipe INACBG');
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalKlaim === 0) {
|
||||
showAlert('danger', 'Total klaim tidak boleh 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
id_billing: currentEditingBilling.id_billing,
|
||||
tipe_inacbg: tipeInacbg,
|
||||
kode_inacbg: inacbgCodes.map(c => c.value), // Extract just the codes
|
||||
total_klaim: totalKlaim,
|
||||
billing_sign: 'created' // or any status you want
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/inacbg`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result.error || result.message || 'Gagal menyimpan INACBG');
|
||||
}
|
||||
|
||||
showAlert('success', 'INACBG berhasil disimpan');
|
||||
setTimeout(() => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editModal')).hide();
|
||||
loadBillingData();
|
||||
}, 1500);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
showAlert('danger', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert in modal
|
||||
function showAlert(type, message) {
|
||||
const alert = document.getElementById('formAlert');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
alert.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// Search billing
|
||||
function searchBilling(e) {
|
||||
const keyword = e.target.value.toLowerCase().trim();
|
||||
|
||||
if (keyword === '') {
|
||||
renderBillingTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = billingData.filter(b =>
|
||||
(b.nama_pasien && b.nama_pasien.toLowerCase().includes(keyword)) ||
|
||||
(b.id_pasien && b.id_pasien.toString().includes(keyword))
|
||||
);
|
||||
|
||||
const tbody = document.getElementById('billingTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (filtered.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted">Tidak ada hasil pencarian</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(billing => {
|
||||
const row = document.createElement('tr');
|
||||
const badgeColor = getBillingSignColor(billing.billing_sign);
|
||||
const badgeClass = getBillingSignBadgeClass(billing.billing_sign);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${billing.id_pasien || '-'}</td>
|
||||
<td>
|
||||
<a href="#" class="text-primary text-decoration-none" onclick="openEditModal(${billing.id_billing}); return false;">
|
||||
${billing.nama_pasien || '-'}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="billing-sign-badge ${badgeClass}" style="background-color: ${badgeColor};" title="${billing.billing_sign}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="openEditModal(${billing.id_billing})">
|
||||
✎ Edit
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/* General Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #dee2e6;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #007bff;
|
||||
}
|
||||
|
||||
.sidebar-header h5 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ruangan-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.ruangan-item {
|
||||
padding: 10px 15px;
|
||||
background-color: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
color: #6c757d;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.ruangan-item:hover {
|
||||
background-color: #e7f3ff;
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.ruangan-item.active {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header .text-muted {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Billing Table */
|
||||
.billing-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
padding: 15px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Billing Sign Badge */
|
||||
.billing-sign-badge {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.billing-sign-badge.created {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.billing-sign-badge.kuning {
|
||||
background-color: #ffc107;
|
||||
}
|
||||
|
||||
.billing-sign-badge.hijau {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.billing-sign-badge.merah {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.modal-header .modal-title {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body h6 {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.modal-body .form-control {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.modal-body .form-control:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.modal-body .form-select {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-body .form-select:focus {
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Code List */
|
||||
.code-badge {
|
||||
display: inline-block;
|
||||
background-color: #e7f3ff;
|
||||
border: 1px solid #007bff;
|
||||
border-radius: 20px;
|
||||
padding: 6px 12px;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9rem;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.code-badge .remove-btn {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
border-color: #218838;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.header h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.table thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
display: block;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.table tbody td {
|
||||
display: block;
|
||||
text-align: right;
|
||||
padding-left: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table tbody td:before {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Form Billing Pasien BPJS</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0 text-white">Form Billing Pasien BPJS</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="bpjsForm">
|
||||
|
||||
<!-- === DOKTER === -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nama Dokter</label>
|
||||
<div class="searchable-select-wrapper" id="wrapper_nama_dokter">
|
||||
<input type="text" class="searchable-select-input" id="nama_dokter" readonly placeholder="Dokter...">
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_nama_dokter">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_nama_dokter" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_nama_dokter"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_nama_dokter" name="nama_dokter"></select>
|
||||
</div>
|
||||
|
||||
<!-- NAMA PASIEN -->
|
||||
<div class="mb-3 position-relative">
|
||||
<label class="form-label">Nama Pasien</label>
|
||||
<input type="text" class="form-control" id="nama_pasien" autocomplete="off">
|
||||
<div id="list_pasien" class="autocomplete-list"></div>
|
||||
<input type="hidden" id="id_pasien" name="id_pasien">
|
||||
</div>
|
||||
|
||||
<!-- Auto Fill atau Manual -->
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Jenis Kelamin</label>
|
||||
<input type="text" class="form-control" id="jenis_kelamin" placeholder="Auto fill atau isi manual">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Usia</label>
|
||||
<input type="number" class="form-control" id="usia" placeholder="Auto fill atau isi manual">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Ruangan</label>
|
||||
<div class="searchable-select-wrapper" id="wrapper_ruangan">
|
||||
<input type="text" class="searchable-select-input" id="ruangan" readonly placeholder="-- Pilih Ruangan --">
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_ruangan">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_ruangan" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_ruangan"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_ruangan" name="ruangan"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Kelas</label>
|
||||
<select class="form-select" id="kelas" name="kelas">
|
||||
<option value="">-- Pilih Kelas --</option>
|
||||
<option value="1">Kelas 1</option>
|
||||
<option value="2">Kelas 2</option>
|
||||
<option value="3">Kelas 3</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Cara Bayar</label>
|
||||
<select class="form-select" id="cara_bayar" name="cara_bayar">
|
||||
<option value="">-- Pilih Cara Bayar --</option>
|
||||
<option value="BPJS">BPJS</option>
|
||||
<option value="UMUM">Umum</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Tanggal Keluar diisi oleh Admin Billing, bukan di form dokter -->
|
||||
</div>
|
||||
|
||||
<!-- TINDAKAN -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tindakan RS</label>
|
||||
<div class="searchable-select-wrapper multi-select" id="wrapper_tarif_rs">
|
||||
<div class="searchable-select-input" id="tarif_rs" placeholder="-- Pilih --">
|
||||
<input type="text" id="input_tarif_rs" placeholder="Tindakan..." autocomplete="off">
|
||||
</div>
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_tarif_rs">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_tarif_rs" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_tarif_rs"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_tarif_rs" name="tarif_rs" multiple></select>
|
||||
</div>
|
||||
|
||||
<!-- ICD -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ICD 9</label>
|
||||
<div class="searchable-select-wrapper multi-select" id="wrapper_icd9">
|
||||
<div class="searchable-select-input" id="icd9" placeholder="-- Pilih --">
|
||||
<input type="text" id="input_icd9" placeholder="-- Pilih --" autocomplete="off">
|
||||
</div>
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_icd9">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_icd9" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_icd9"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_icd9" name="icd9" multiple></select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">ICD 10</label>
|
||||
<div class="searchable-select-wrapper multi-select" id="wrapper_icd10">
|
||||
<div class="searchable-select-input" id="icd10" placeholder="-- Pilih --">
|
||||
<input type="text" id="input_icd10" placeholder="-- Pilih --" autocomplete="off">
|
||||
</div>
|
||||
<span class="searchable-select-arrow">▼</span>
|
||||
<div class="searchable-select-dropdown" id="dropdown_icd10">
|
||||
<div class="searchable-select-search">
|
||||
<input type="text" placeholder="Cari..." id="search_icd10" autocomplete="off">
|
||||
</div>
|
||||
<div class="searchable-select-options" id="options_icd10"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="form-select d-none" id="select_icd10" name="icd10" multiple></select>
|
||||
</div>
|
||||
|
||||
<!-- RIWAYAT BILLING AKTIF (TINDAKAN & ICD SEBELUMNYA) -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Riwayat Tindakan & ICD (Billing Aktif)</label>
|
||||
<div id="billing_history_info" class="small text-muted mb-2">
|
||||
Belum ada data yang dimuat. Pilih pasien untuk melihat riwayat.
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h6 class="fw-semibold">Tindakan RS</h6>
|
||||
<ul id="history_tindakan_rs" class="list-group small"></ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="fw-semibold">ICD 9</h6>
|
||||
<ul id="history_icd9" class="list-group small"></ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="fw-semibold">ICD 10</h6>
|
||||
<ul id="history_icd10" class="list-group small"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOTAL TARIF RS -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Total Tarif RS</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">Rp</span>
|
||||
<input type="text" class="form-control" id="total_tarif_rs" name="total_tarif_rs" readonly placeholder="0">
|
||||
</div>
|
||||
<small class="text-muted">Total akan dihitung otomatis berdasarkan tindakan yang dipilih</small>
|
||||
</div>
|
||||
|
||||
<div id="formAlert" class="alert d-none" role="alert"></div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
💾 Simpan Data
|
||||
</button>
|
||||
<button type="button" id="saveDraftBtn" class="btn btn-outline-secondary btn-lg">💾 Save Draft</button>
|
||||
<button type="button" id="clearDraftBtn" class="btn btn-outline-danger btn-lg">🗑️ Clear Draft</button>
|
||||
<div id="draftStatus" class="align-self-center ms-2 text-muted" style="font-size:0.95rem"></div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- App script: loads dropdowns, autocomplete and form handlers -->
|
||||
<script src="script.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,616 @@
|
||||
/* ============= GLOBAL STYLES ============= */
|
||||
:root {
|
||||
--primary-color: #0d6efd;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #198754;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #0dcaf0;
|
||||
--light-bg: #f8f9fa;
|
||||
--border-color: #dee2e6;
|
||||
--shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--shadow-lg: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--border-radius: 0.5rem;
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ============= CARD STYLES ============= */
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--shadow-lg);
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ============= FORM STYLES ============= */
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-control:disabled {
|
||||
background-color: #e9ecef;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ============= BUTTON STYLES ============= */
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-weight: 600;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px rgba(13, 110, 253, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #0056b3 0%, #004085 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(13, 110, 253, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ============= AUTOCOMPLETE STYLES ============= */
|
||||
.autocomplete-list {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: white;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
box-shadow: var(--shadow-lg);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.autocomplete-list.show {
|
||||
display: block;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover {
|
||||
background: linear-gradient(90deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.autocomplete-item.text-muted {
|
||||
color: var(--secondary-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.autocomplete-item.text-danger {
|
||||
color: var(--danger-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============= SEARCHABLE DROPDOWN STYLES ============= */
|
||||
.searchable-select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.searchable-select-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 2.5rem 0.625rem 0.75rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.searchable-select-input:hover {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.searchable-select-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
|
||||
.searchable-select-input.searching {
|
||||
cursor: text;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.searchable-select-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1050;
|
||||
background: white;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 0.25rem;
|
||||
max-height: 250px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
.searchable-select-dropdown.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.searchable-select-search {
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.searchable-select-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.searchable-select-search input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.searchable-select-options {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.searchable-select-options::-webkit-scrollbar-thumb:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.searchable-select-option {
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: var(--transition);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.searchable-select-option:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.searchable-select-option:hover,
|
||||
.searchable-select-option.highlighted {
|
||||
background: linear-gradient(90deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.searchable-select-option.selected {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.searchable-select-option.selected::after {
|
||||
content: " ✓";
|
||||
float: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.searchable-select-arrow {
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
transition: transform 0.3s ease;
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.open .searchable-select-arrow {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.searchable-select-no-results {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ============= MULTI-SELECT STYLES ============= */
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input {
|
||||
min-height: 38px;
|
||||
max-height: 100px;
|
||||
padding: 0.25rem 2.5rem 0.25rem 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar-thumb {
|
||||
background: var(--primary-color);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input::-webkit-scrollbar-thumb:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input:empty::before {
|
||||
content: attr(placeholder);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.selected-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #0056b3 100%);
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
gap: 0.25rem;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 3px rgba(13, 110, 253, 0.3);
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.selected-chip .chip-remove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 0 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
transition: var(--transition);
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selected-chip .chip-remove:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input input {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 0.375rem;
|
||||
background: transparent;
|
||||
cursor: text;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.searchable-select-wrapper.multi-select .searchable-select-input input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ============= LOADING STYLES ============= */
|
||||
.loading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 0.15em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.select-loading {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid var(--primary-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: translateY(-50%) rotate(0deg); }
|
||||
100% { transform: translateY(-50%) rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ============= RESPONSIVE STYLES ============= */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select,
|
||||
.searchable-select-input {
|
||||
font-size: 0.95rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.row > [class*="col-"] {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
body {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.searchable-select-dropdown {
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.autocomplete-list {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= UTILITY CLASSES ============= */
|
||||
.position-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* ============= ANIMATIONS ============= */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
/* ============= TOTAL TARIF RS STYLES ============= */
|
||||
#total_tarif_rs {
|
||||
font-weight: 700;
|
||||
color: #198754;
|
||||
font-size: 1.25rem;
|
||||
text-align: right;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: linear-gradient(135deg, var(--success-color) 0%, #146c43 100%);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.input-group .form-control:focus {
|
||||
border-color: var(--success-color);
|
||||
box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
|
||||
}
|
||||
|
||||
/* ============= FOCUS VISIBLE ============= */
|
||||
*:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ============= PRINT STYLES ============= */
|
||||
@media print {
|
||||
body {
|
||||
background: white;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.btn,
|
||||
.searchable-select-arrow,
|
||||
.autocomplete-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,789 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backend Careit - Test Interface</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: #f5f5f5;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.endpoint-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.endpoint-item label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.endpoint-item input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-list {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-list:hover {
|
||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.response-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.response-header h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-loading {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.view-toggle button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.view-toggle button.active {
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#response,
|
||||
#tableResponse {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#tableResponse {
|
||||
display: none;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: white;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #eee;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #f1f5ff;
|
||||
}
|
||||
|
||||
.health-check {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.health-ok {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.health-error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏥 Backend Careit</h1>
|
||||
<p>Test Interface untuk API Backend</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Health Check -->
|
||||
<div class="health-check">
|
||||
<h2>Health Check</h2>
|
||||
<button onclick="checkHealth()" style="margin-top: 10px;">Cek Status Server</button>
|
||||
<div id="healthStatus" class="health-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif BPJS Rawat Inap -->
|
||||
<div class="section">
|
||||
<div class="section-header">📋 Tarif BPJS Rawat Inap</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifBPJSRawatInap()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode INA CBG:</label>
|
||||
<input type="text" id="kodeRawatInap" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifBPJSRawatInapByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif BPJS Rawat Jalan -->
|
||||
<div class="section">
|
||||
<div class="section-header">🚶 Tarif BPJS Rawat Jalan</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifBPJSRawatJalan()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode INA CBG:</label>
|
||||
<input type="text" id="kodeRawatJalan" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifBPJSRawatJalanByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif RS -->
|
||||
<div class="section">
|
||||
<div class="section-header">🏨 Tarif RS</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifRS()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode RS:</label>
|
||||
<input type="text" id="kodeRS" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifRSByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kategori:</label>
|
||||
<input type="text" id="kategoriRS" placeholder="Masukkan kategori...">
|
||||
<button onclick="getTarifRSByKategori()">Cari by Kategori</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD9 -->
|
||||
<div class="section">
|
||||
<div class="section-header">🧬 Kode Tindakan ICD9</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getICD9()">List Semua Kode ICD9</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterICD9" placeholder="Ketik untuk filter di tabel..." oninput="filterICD9ClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/icd9</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON (misal: kode, deskripsi, dll).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD10 -->
|
||||
<div class="section">
|
||||
<div class="section-header">🩺 Kode Diagnosis ICD10</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getICD10()">List Semua Kode ICD10</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterICD10" placeholder="Ketik untuk filter di tabel..." oninput="filterICD10ClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/icd10</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dokter -->
|
||||
<div class="section">
|
||||
<div class="section-header">👨⚕️ Data Dokter</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getDokter()">List Semua Dokter</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterDokter" placeholder="Ketik untuk filter di tabel..." oninput="filterDokterClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/dokter</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ruangan -->
|
||||
<div class="section">
|
||||
<div class="section-header">🏥 Data Ruangan</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getRuangan()">List Semua Ruangan</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterRuangan" placeholder="Ketik untuk filter di tabel..." oninput="filterRuanganClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/ruangan</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Area -->
|
||||
<div class="response-area">
|
||||
<div class="response-header">
|
||||
<h3>Response:</h3>
|
||||
<span id="statusBadge" class="status-badge" style="display: none;"></span>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button id="btnJsonView" class="active" onclick="setViewMode('json')">JSON</button>
|
||||
<button id="btnTableView" onclick="setViewMode('table')">Table (Auto)</button>
|
||||
</div>
|
||||
<div id="response"></div>
|
||||
<div id="tableResponse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
|
||||
function showResponse(data, status = 'ok') {
|
||||
const responseDiv = document.getElementById('response');
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
|
||||
// Tampilkan JSON mentah
|
||||
responseDiv.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
// Coba render tabel
|
||||
renderTable(data);
|
||||
|
||||
statusBadge.style.display = 'inline-block';
|
||||
statusBadge.className = 'status-badge status-' + status;
|
||||
statusBadge.textContent = status === 'ok' ? '✓ Success' : status === 'loading' ? '⏳ Loading...' : '✗ Error';
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
const responseDiv = document.getElementById('response');
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const btnJson = document.getElementById('btnJsonView');
|
||||
const btnTable = document.getElementById('btnTableView');
|
||||
|
||||
if (mode === 'json') {
|
||||
responseDiv.style.display = 'block';
|
||||
tableDiv.style.display = 'none';
|
||||
btnJson.classList.add('active');
|
||||
btnTable.classList.remove('active');
|
||||
} else {
|
||||
responseDiv.style.display = 'none';
|
||||
tableDiv.style.display = 'block';
|
||||
btnJson.classList.remove('active');
|
||||
btnTable.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function formatRupiah(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
// Convert to number
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(num)) {
|
||||
return value;
|
||||
}
|
||||
// Format dengan pemisah ribuan
|
||||
return 'Rp ' + num.toLocaleString('id-ID', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function isTarifColumn(key) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return lowerKey.includes('tarif') ||
|
||||
lowerKey.includes('harga') ||
|
||||
lowerKey.includes('kelas') ||
|
||||
lowerKey.includes('tarif_inacbg');
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
|
||||
// Reset isi
|
||||
tableDiv.innerHTML = '';
|
||||
|
||||
// Hanya render tabel untuk array of objects
|
||||
if (!Array.isArray(data) || data.length === 0 || typeof data[0] !== 'object') {
|
||||
setViewMode('json');
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
data.reduce((set, item) => {
|
||||
Object.keys(item || {}).forEach(k => set.add(k));
|
||||
return set;
|
||||
}, new Set())
|
||||
);
|
||||
|
||||
if (keys.length === 0) {
|
||||
setViewMode('json');
|
||||
return;
|
||||
}
|
||||
|
||||
const table = document.createElement('table');
|
||||
const thead = document.createElement('thead');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
const headerRow = document.createElement('tr');
|
||||
keys.forEach(key => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = key;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
thead.appendChild(headerRow);
|
||||
|
||||
data.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
keys.forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
const value = row && key in row ? row[key] : '';
|
||||
|
||||
// Format tarif dengan rupiah
|
||||
if (isTarifColumn(key) && (typeof value === 'number' || (typeof value === 'string' && !isNaN(value) && value !== ''))) {
|
||||
td.textContent = formatRupiah(value);
|
||||
td.style.textAlign = 'right';
|
||||
} else {
|
||||
td.textContent = typeof value === 'object' ? JSON.stringify(value) : value;
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(thead);
|
||||
table.appendChild(tbody);
|
||||
tableDiv.appendChild(table);
|
||||
|
||||
// Auto switch ke view tabel
|
||||
setViewMode('table');
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/`);
|
||||
const data = await response.json();
|
||||
|
||||
const healthStatus = document.getElementById('healthStatus');
|
||||
if (response.ok) {
|
||||
healthStatus.textContent = '✓ Server Online';
|
||||
healthStatus.className = 'health-status health-ok';
|
||||
} else {
|
||||
healthStatus.textContent = '✗ Server Error';
|
||||
healthStatus.className = 'health-status health-error';
|
||||
}
|
||||
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
const healthStatus = document.getElementById('healthStatus');
|
||||
healthStatus.textContent = '✗ Connection Error';
|
||||
healthStatus.className = 'health-status health-error';
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatInap() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatInap`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatInapByKode() {
|
||||
const kode = document.getElementById('kodeRawatInap').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJS/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatJalan() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatJalanByKode() {
|
||||
const kode = document.getElementById('kodeRawatJalan').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRS() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRS`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRSByKode() {
|
||||
const kode = document.getElementById('kodeRS').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRS/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRSByKategori() {
|
||||
const kategori = document.getElementById('kategoriRS').value;
|
||||
if (!kategori) {
|
||||
alert('Silakan masukkan kategori!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRSByKategori/${encodeURIComponent(kategori)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getICD9() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/icd9`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterICD9ClientSide() {
|
||||
const input = document.getElementById('filterICD9');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getICD10() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/icd10`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterICD10ClientSide() {
|
||||
const input = document.getElementById('filterICD10');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getDokter() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/dokter`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterDokterClientSide() {
|
||||
const input = document.getElementById('filterDokter');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getRuangan() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/ruangan`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterRuanganClientSide() {
|
||||
const input = document.getElementById('filterRuangan');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto check health on load
|
||||
window.onload = function() {
|
||||
checkHealth();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user