initial monorepo commit (fe + be)

This commit is contained in:
AnggerRevo5
2026-01-27 15:48:29 +07:00
commit 9df710d81e
189 changed files with 137113 additions and 0 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# DB_USER=careit_user
DB_USER=postgres
DB_PASSWORD= gakbikinkembung25
# DB_HOST=31.97.109.192
DB_HOST= localhost
DB_PORT=5432
DB_NAME=careit_db
HOST=0.0.0.0
PORT=8081
# PORT=8082
EMAIL_FROM=[email protected]
EMAIL_PASSWORD=gkhz bjax uamw xydf
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
+34
View File
@@ -0,0 +1,34 @@
# 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
+71
View File
@@ -0,0 +1,71 @@
{
"nama_dokter": "dr. Fadilah Muttaqin, Spp.A,MBiomed",
"nama_pasien": "Budi Hartono",
"jenis_kelamin": "Laki-laki",
"usia": 40,
"ruangan": "ICU",
"kelas": "1",
"tindakan_rs": [
"ASUHAN KEFARMASIAN SELAMA PERAWATAN - RAWAT INAP",
"BERCAK DARAH KERING"
],
"icd9": [
"Therapeutic ultrasound",
"Therapeutic ultrasound of vessels of head and neck"
],
"icd10": [
"Cholera",
"Cholera due to vibrio cholerae 01, biovar eltor"
],
"cara_bayar": "UMUM",
"total_tarif_rs": 250000
}
FE harus kirim gini
data untuk admin dari be:
{
"data": [
{
"nama_pasien": "mahdi Jamaludin",
"id_pasien": 1,
"Kelas": "2",
"ruangan": "R. Nusa Dua",
"total_tarif_rs": 150000,
"tindakan_rs": [
"DAR.001",
"DAR.002"
],
"icd9": [
"00.0",
"00"
],
"icd10": [
"A00",
"A00.0"
]
}
],
"status": "success"
}
if strings.TrimSpace(dokter.Password) == "" || dokter.Password != req.Password {
c.JSON(http.StatusUnauthorized, gin.H{
"status": "error",
"message": "Email atau password salah",
})
return
}
{
"dokter": {
"email": "hajengwulandari.fk@ub.ac.id",
"id": 2,
"ksm": "Anak",
"nama": "dr. Hajeng Wulandari, Sp.A, Mbiomed"
},
"status": "success",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhamVuZ3d1bGFuZGFyaS5ma0B1Yi5hYy5pZCIsImV4cCI6MTc2NDc1NzIxMCwiaWF0IjoxNzY0NjcwODEwLCJpZCI6Miwia3NtIjoiQW5hayIsIm5hbWEiOiJkci4gSGFqZW5nIFd1bGFuZGFyaSwgU3AuQSwgTWJpb21lZCJ9.X1PyxjbC1Ht3DFbvi4svqXY4hsNIS_nmYMROkRaK-Ko"
}
jadi data yang dihitung cuma yang rawat inap nanti yang isi tanggal keluar berarti admin billing dan nanti total tarif dan total klaim nanti di tampilin juga ketika datanya di tampilin sama kaya tindakan dan tarif rs nanti di admin billing juga bisa liat data tindakan lama dan icd lama dan tindakan baru dan icd bari dan inacbg lama dan inacbg baru plus total tarif yang lama di tambah yang barui dan total klaim lama nanti setelah dimasukan ditambah lagi sama total klaim baru baru dihitung billing sign baru paham gak
+10
View File
@@ -0,0 +1,10 @@
{
"cells": [],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
+49
View File
@@ -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", "localhost")
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
}
+55
View File
@@ -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
)
+118
View File
@@ -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=
+883
View File
@@ -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 [email protected] dan [email protected]",
})
}
// 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,
})
}
+54
View File
@@ -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)
}
}
+104
View File
@@ -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()
}
}
+352
View File
@@ -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
+87
View File
@@ -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)
}
}
+47
View File
@@ -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.")
}
}
+96
View File
@@ -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
}
+173
View File
@@ -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
}
+284
View File
@@ -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 = "[email protected]"
}
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 = "[email protected]"
}
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{"[email protected]", "[email protected]", "[email protected]"}
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
}
+169
View File
@@ -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
}
+673
View File
@@ -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
}
+133
View File
@@ -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
}
+154
View File
@@ -0,0 +1,154 @@
# Panduan Testing Billing API
## 1. Pastikan Server Berjalan
Jalankan server Go terlebih dahulu:
```bash
go run main.go
```
Server akan berjalan di: `http://localhost:8081`
## 2. Testing di Postman
### Setup Request
1. **Method:** `POST`
2. **URL:** `http://localhost:8081/billing`
3. **Headers:**
- Key: `Content-Type`
- Value: `application/json`
### Body (Raw JSON)
Buka tab **Body** → pilih **raw** → pilih **JSON** dari dropdown, lalu paste JSON berikut:
```json
{
"nama_dokter": "dr. Hajeng Wulandari, Sp.A, Mbiomed",
"nama_pasien": "Budi Santoso",
"jenis_kelamin": "Laki-laki",
"usia": 45,
"ruangan": "R001",
"kelas": "1",
"tindakan_rs": "T001",
"icd9": "ICD9-001",
"icd10": "ICD10-001",
"cara_bayar": "BPJS",
"total_tarif_rs": 500000
}
```
**Catatan:**
- Pastikan `nama_dokter` sesuai dengan data yang ada di database
- Untuk melihat daftar dokter, gunakan: `GET http://localhost:8081/dokter`
## 3. Response yang Diharapkan
### Success (200 OK)
```json
{
"status": "success",
"message": "Billing berhasil dibuat",
"data": {
"billing": {
"ID_Billing": 1,
"ID_Pasien": 1,
"Cara_Bayar": "BPJS",
"Tanggal_masuk": "2024-01-15T10:30:00Z",
"Tanggal_keluar": null,
"ID_Dokter": 2,
"Total_Tarif_RS": 500000,
"Total_Tarif_BPJS": 0,
"Billing_sign": "created"
},
"pasien": {
"ID_Pasien": 1,
"Nama_Pasien": "Budi Santoso",
"Jenis_Kelamin": "Laki-laki",
"Usia": 45,
"Ruangan": "R001",
"Kelas": "1"
}
}
}
```
### Error - Dokter Tidak Ditemukan (500)
```json
{
"status": "error",
"message": "Gagal membuat billing",
"error": "dokter dengan nama Dr. Ahmad Wijaya tidak ditemukan"
}
```
### Error - Validasi Gagal (400)
```json
{
"status": "error",
"message": "Data tidak valid",
"error": "Key: 'BillingRequest.Nama_Dokter' Error:Field validation for 'Nama_Dokter' failed on the 'required' tag"
}
```
## 4. Testing Skenario
### Skenario 1: Pasien Baru
- Kirim request dengan `nama_pasien` yang belum ada di database
- Sistem akan membuat pasien baru dengan ID auto-increment
- Billing akan dibuat dengan ID_Pasien dari pasien baru
### Skenario 2: Pasien Sudah Ada
- Kirim request dengan `nama_pasien` yang sudah ada di database
- Sistem akan menggunakan data pasien yang sudah ada
- Billing akan dibuat dengan ID_Pasien dari pasien yang sudah ada
### Skenario 3: Dokter Tidak Ditemukan
- Kirim request dengan `nama_dokter` yang tidak ada di database
- Sistem akan mengembalikan error
## 5. Endpoint Lain untuk Testing
### Get Daftar Dokter
```
GET http://localhost:8081/dokter
```
### Get Pasien by ID
```
GET http://localhost:8081/pasien/1
```
### Health Check
```
GET http://localhost:8081/
```
## 6. Checklist Sebelum Testing
- [ ] Server Go sudah berjalan di port 8081
- [ ] Database sudah terkoneksi
- [ ] Header `Content-Type: application/json` sudah diset
- [ ] Body menggunakan raw JSON (bukan form-data)
- [ ] Nama dokter sesuai dengan data di database
- [ ] Semua field required sudah terisi
## 7. Troubleshooting
### Error: "Content-Type harus application/json"
**Solusi:** Pastikan di tab Headers ada:
- Key: `Content-Type`
- Value: `application/json`
### Error: "dokter dengan nama ... tidak ditemukan"
**Solusi:**
1. Cek daftar dokter dengan `GET /dokter`
2. Gunakan nama dokter yang sesuai dengan data di database
### Error: "Data tidak valid"
**Solusi:**
1. Pastikan semua field required terisi
2. Pastikan format JSON benar (kurung kurawal lengkap)
3. Pastikan field names menggunakan lowercase dengan underscore (snake_case)
+97
View File
@@ -0,0 +1,97 @@
# Setup Postman untuk Testing Billing API
## Endpoint
**Method:** `POST`
**URL:** `http://localhost:8080/billing`
*(Sesuaikan dengan port server Anda)*
## Headers (PENTING!)
Pastikan header berikut sudah diset:
```
Content-Type: application/json
```
## Body (Raw JSON)
Pilih tab **Body** → pilih **raw** → pilih **JSON** dari dropdown
Kemudian paste JSON berikut:
```json
{
"nama_dokter": "Dr. Ahmad Wijaya",
"nama_pasien": "Budi Santoso",
"jenis_kelamin": "Laki-laki",
"usia": 45,
"ruangan": "R001",
"kelas": "1",
"tindakan_rs": "T001",
"icd9": "ICD9-001",
"icd10": "ICD10-001",
"cara_bayar": "BPJS",
"total_tarif_rs": 500000
}
```
## Field yang Required (Wajib Diisi)
- `nama_dokter` (string)
- `nama_pasien` (string)
- `jenis_kelamin` (string) - "Laki-laki" atau "Perempuan"
- `usia` (integer)
- `ruangan` (string)
- `kelas` (string) - "1", "2", atau "3"
- `tindakan_rs` (string)
- `icd9` (string)
- `icd10` (string)
- `cara_bayar` (string) - "BPJS" atau "UMUM"
- `total_tarif_rs` (integer) - optional
## Response Success (200 OK)
```json
{
"status": "success",
"message": "Billing berhasil dibuat",
"data": {
"billing": {
"ID_Billing": "BILL-...",
"ID_Pasien": "PAS-...",
"Cara_Bayar": "BPJS",
"Tanggal_masuk": "2024-01-15T10:30:00Z",
"Tanggal_keluar": null,
"ID_Dokter": "DOK-001",
"Total_Tarif_RS": 500000,
"Total_Tarif_BPJS": 0,
"Billing_sign": "created"
},
"pasien": {
"ID_Pasien": "PAS-...",
"Nama_Pasien": "Budi Santoso",
"Jenis_Kelamin": "Laki-laki",
"Usia": 45,
"Ruangan": "R001",
"Kelas": "1"
}
}
}
```
## Troubleshooting
### Error: "Content-Type harus application/json"
**Solusi:** Pastikan di tab Headers, ada header:
- Key: `Content-Type`
- Value: `application/json`
### Error: "Data tidak valid" dengan semua field required
**Kemungkinan penyebab:**
1. Body tidak dikirim sebagai JSON (mungkin masih form-data atau x-www-form-urlencoded)
2. Format JSON salah (kurung kurawal tidak lengkap, koma salah, dll)
3. Field names tidak sesuai (harus lowercase dengan underscore)
**Solusi:**
1. Pastikan di tab Body, pilih **raw** dan dropdown menunjukkan **JSON**
2. Copy-paste ulang JSON dari contoh di atas
3. Pastikan semua field required terisi
### Error: "dokter dengan nama ... tidak ditemukan"
**Solusi:** Pastikan nama dokter yang dikirim sudah ada di database. Cek dengan GET `/dokter` terlebih dahulu.
+115
View File
@@ -0,0 +1,115 @@
# Cara Menambahkan Data Dummy Admin
File ini berisi instruksi untuk menambahkan data dummy admin ke database.
## Data Admin
- **Username**: `admin`
- **Password**: `admin123`
## Cara 1: Menggunakan File SQL (Recommended)
1. Buka MySQL client atau phpMyAdmin
2. Pilih database `care_it_data`
3. Jalankan file SQL:
```sql
-- Hapus data admin jika sudah ada
DELETE FROM `admin_ruangan` WHERE `Nama_Admin` = 'admin';
-- Insert data admin baru
INSERT INTO `admin_ruangan` (`Nama_Admin`, `Password`, `ID_Ruangan`)
VALUES ('admin', 'admin123', NULL);
```
Atau jalankan file SQL langsung:
```bash
mysql -u root -p care_it_data < sql/insert_admin_dummy.sql
```
## Cara 2: Menggunakan Script Go
1. Pastikan Anda berada di direktori `Backend_CareIt`
2. Jalankan script:
```bash
go run scripts/insert_admin.go
```
Script akan otomatis:
- Menghapus admin lama jika sudah ada
- Menambahkan admin baru dengan username `admin` dan password `admin123`
## Cara 3: Menggunakan MySQL Command Line
```bash
mysql -u root -p care_it_data
```
Kemudian jalankan:
```sql
DELETE FROM `admin_ruangan` WHERE `Nama_Admin` = 'admin';
INSERT INTO `admin_ruangan` (`Nama_Admin`, `Password`, `ID_Ruangan`)
VALUES ('admin', 'admin123', NULL);
```
## Verifikasi
### Cara 1: Menggunakan Script Go (Recommended)
```bash
cd Backend_CareIt
go run scripts/check_admin.go
```
Script ini akan menampilkan:
- Semua data admin di database
- Test query untuk memastikan data bisa diakses
- Informasi detail tentang setiap admin
### Cara 2: Menggunakan MySQL Query
```sql
SELECT * FROM admin_ruangan WHERE Nama_Admin = 'admin';
```
Anda seharusnya melihat data admin dengan:
- `ID_Admin`: (auto increment)
- `Nama_Admin`: admin
- `Password`: admin123
- `ID_Ruangan`: NULL
## Login
Setelah data ditambahkan, Anda bisa login dengan:
- **User Type**: Admin (pilih radio button "Admin")
- **Username**: `admin`
- **Password**: `admin123`
## Troubleshooting
### Masalah: "Payload login tidak valid"
1. Pastikan semua field terisi (username dan password tidak kosong)
2. Pastikan backend sudah di-compile ulang setelah perubahan
3. Restart backend server
### Masalah: "Username atau password salah"
1. Verifikasi data admin ada di database:
```bash
go run scripts/check_admin.go
```
2. Pastikan username dan password sesuai (case-sensitive untuk password)
3. Pastikan tidak ada whitespace di username/password
4. Cek log backend untuk error detail
### Masalah: Admin masuk ke dashboard dokter
1. Pastikan memilih radio button "Admin" sebelum login
2. Clear browser cache dan localStorage
3. Pastikan `userRole` di localStorage adalah "admin"
### Masalah: Data admin tidak ditemukan
1. Jalankan script insert admin lagi:
```bash
go run scripts/insert_admin.go
```
2. Verifikasi dengan check script:
```bash
go run scripts/check_admin.go
```
3. Pastikan koneksi database benar di `database/db.go`
+419
View File
@@ -0,0 +1,419 @@
# CareIT Database Views Documentation
## 📋 Overview
Dokumentasi lengkap untuk semua views yang telah ditambahkan ke database CareIT untuk optimasi query dan mempercepat pengaksesan data.
---
## 📊 Daftar Views
### 1. **v_billing_pasien_info**
**Tujuan:** Menampilkan informasi billing pasien dengan data lengkap dan status real-time
**Kolom:**
- `ID_Billing` - ID Billing (PK)
- `ID_Pasien` - ID Pasien (FK)
- `Nama_Pasien` - Nama lengkap pasien
- `Jenis_Kelamin` - Jenis kelamin (Laki-laki/Perempuan)
- `Usia` - Usia pasien
- `Ruangan` - Nama ruangan
- `Kelas` - Kelas perawatan (1, 2, 3)
- `Cara_Bayar` - Metode pembayaran (BPJS/Umum)
- `Tanggal_Masuk` - Tanggal masuk rumah sakit
- `Tanggal_Keluar` - Tanggal keluar (NULL jika masih aktif)
- `Hari_Inap` - Jumlah hari perawatan (calculated)
- `Total_Tarif_RS` - Total tarif RS
- `Total_Klaim` - Total klaim BPJS
- `Billing_Sign` - Status billing (Hijau/Kuning/Merah)
- `Status_Pasien` - Status pasien (Aktif/Selesai)
**Use Case:**
- Dashboard billing pasien
- List view semua pasien
- Filter berdasarkan status atau cara bayar
- Monitoring status pasien aktif
**Query Example:**
```sql
SELECT * FROM v_billing_pasien_info
WHERE Status_Pasien = 'Aktif'
ORDER BY Tanggal_Masuk DESC;
```
---
### 2. **v_billing_detail**
**Tujuan:** Menampilkan detail billing dengan informasi dokter dan tindakan
**Kolom:**
- `ID_Billing` - ID Billing
- `ID_Pasien` - ID Pasien
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Tanggal_Masuk` - Tanggal masuk
- `Tanggal_Keluar` - Tanggal keluar
- `Dokter` - Daftar dokter yang menangani
- `KSM` - Kelompok Staf Medis yang terlibat
- `Jumlah_Dokter` - Jumlah dokter yang terlibat
- `Jumlah_Tindakan` - Jumlah tindakan yang dilakukan
- `Total_Tarif_RS` - Total tarif RS
- `Total_Klaim` - Total klaim
- `Billing_Sign` - Status billing
**Use Case:**
- Reporting detail billing
- Tracking dokter per pasien
- Analisis jumlah tindakan per billing
- Verifikasi komposisi tim medis
**Query Example:**
```sql
SELECT * FROM v_billing_detail
WHERE Cara_Bayar = 'BPJS'
AND Jumlah_Dokter >= 2;
```
---
### 3. **v_billing_diagnosis_procedure**
**Tujuan:** Menampilkan diagnosa dan prosedur medis per billing
**Kolom:**
- `ID_Billing` - ID Billing
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Kode_Diagnosa` - Daftar kode ICD10 diagnosa
- `Diagnosa` - Daftar diagnosa lengkap
- `Jumlah_Diagnosa` - Jumlah diagnosa
- `Kode_Prosedur` - Daftar kode ICD9 prosedur
- `Prosedur` - Daftar prosedur lengkap
- `Jumlah_Prosedur` - Jumlah prosedur
- `Tanggal_Masuk` - Tanggal masuk
- `Tanggal_Keluar` - Tanggal keluar
**Use Case:**
- Medical record extraction
- Clinical audit trail
- Diagnosis tracking
- Procedure validation
- Export untuk verifikasi medis
**Query Example:**
```sql
SELECT * FROM v_billing_diagnosis_procedure
WHERE Diagnosa LIKE '%A00%';
```
---
### 4. **v_billing_inacbg_code**
**Tujuan:** Menampilkan INACBG code (RI dan RJ) untuk BPJS claim processing
**Kolom:**
- `ID_Billing` - ID Billing
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Tipe_Perawatan` - Tipe perawatan (RI=Rawat Inap, RJ=Rawat Jalan)
- `Kode_INACBG_RI` - Daftar kode INACBG RI
- `Kode_INACBG_RJ` - Daftar kode INACBG RJ
- `Jumlah_INACBG_RI` - Jumlah kode RI
- `Jumlah_INACBG_RJ` - Jumlah kode RJ
- `Total_Klaim` - Total klaim
- `Billing_Sign` - Status billing
**Use Case:**
- BPJS claim submission
- INACBG code verification
- Claim tracking
- DRG mapping validation
- Financial reconciliation
**Query Example:**
```sql
SELECT * FROM v_billing_inacbg_code
WHERE Cara_Bayar = 'BPJS'
AND Billing_Sign IN ('Kuning', 'Merah');
```
---
### 5. **v_ruangan_pasien_aktif**
**Tujuan:** Dashboard ruangan dengan occupancy rate dan distribusi pasien per kelas
**Kolom:**
- `ID_Ruangan` - ID Ruangan (PK)
- `Nama_Ruangan` - Nama ruangan
- `Jenis_Ruangan` - Jenis ruangan
- `Kategori_ruangan` - Kategori ruangan
- `Jumlah_Pasien_Aktif` - Total pasien aktif di ruangan
- `Pasien_Kelas_1` - Jumlah pasien kelas 1
- `Pasien_Kelas_2` - Jumlah pasien kelas 2
- `Pasien_Kelas_3` - Jumlah pasien kelas 3
- `Nama_Pasien` - Daftar nama pasien aktif
**Use Case:**
- Real-time occupancy dashboard
- Room management
- Patient distribution analysis
- Capacity planning
- Class-based tracking
**Query Example:**
```sql
SELECT * FROM v_ruangan_pasien_aktif
WHERE Jumlah_Pasien_Aktif > 0
ORDER BY Jumlah_Pasien_Aktif DESC;
```
---
### 6. **v_dokter_billing_stat**
**Tujuan:** Statistik kinerja dokter dengan tracking billing dan klaim
**Kolom:**
- `ID_Dokter` - ID Dokter (PK)
- `Nama_Dokter` - Nama dokter
- `Status` - Status (DPJP/PPDS)
- `KSM` - Kelompok Staf Medis
- `Jumlah_Billing` - Jumlah billing yang ditangani
- `Jumlah_Pasien` - Jumlah pasien unik
- `Total_Klaim` - Total klaim dari semua billing
- `Tanggal_Pasien_Terakhir` - Tanggal pasien terakhir ditangani
- `Tipe_Pasien` - Tipe pasien yang ditangani (BPJS/Umum)
**Use Case:**
- Dokter performance dashboard
- Workload analysis
- Billing tracking per dokter
- KSM comparative analysis
- Productivity metrics
**Query Example:**
```sql
SELECT * FROM v_dokter_billing_stat
WHERE Jumlah_Billing > 5
ORDER BY Total_Klaim DESC;
```
---
### 7. **v_pasien_billing_history**
**Tujuan:** Riwayat lengkap pasien dengan semua billing dan klaim
**Kolom:**
- `ID_Pasien` - ID Pasien (PK)
- `Nama_Pasien` - Nama lengkap pasien
- `Jenis_Kelamin` - Jenis kelamin
- `Usia` - Usia pasien
- `Ruangan` - Ruangan tempat dirawat
- `Jumlah_Billing` - Total billing sepanjang waktu
- `Jumlah_Billing_Aktif` - Billing yang masih aktif
- `Jumlah_Billing_Selesai` - Billing yang sudah selesai
- `Total_Klaim_Keseluruhan` - Total klaim keseluruhan
- `Tanggal_Masuk_Terakhir` - Tanggal masuk terakhir
- `Tanggal_Keluar_Terakhir` - Tanggal keluar terakhir
- `Riwayat_Cara_Bayar` - Riwayat cara pembayaran
**Use Case:**
- Patient medical history
- Complete patient profile
- Historical billing analysis
- Treatment continuity tracking
- Patient lifetime value analysis
**Query Example:**
```sql
SELECT * FROM v_pasien_billing_history
WHERE Jumlah_Billing > 1
ORDER BY Total_Klaim_Keseluruhan DESC;
```
---
### 8. **v_billing_summary_harian**
**Tujuan:** Summary harian billing untuk operational dashboard
**Kolom:**
- `Tanggal` - Tanggal (DATE)
- `Jumlah_Billing_Masuk` - Jumlah billing masuk hari tersebut
- `Billing_Keluar_Hari_Sama` - Billing keluar di hari yang sama
- `Billing_Aktif` - Billing yang masih aktif (tidak keluar)
- `Status_Hijau` - Jumlah billing dengan status Hijau
- `Status_Kuning` - Jumlah billing dengan status Kuning
- `Status_Merah` - Jumlah billing dengan status Merah
- `Total_Tarif_RS_Harian` - Total tarif RS harian
- `Total_Klaim_Harian` - Total klaim harian
- `Tipe_Pasien_Masuk` - Tipe pasien yang masuk (BPJS/Umum)
**Use Case:**
- Daily operational report
- Real-time monitoring dashboard
- Hospital KPI tracking
- Revenue analysis
- Status distribution monitoring
**Query Example:**
```sql
SELECT * FROM v_billing_summary_harian
WHERE Tanggal BETWEEN DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND CURDATE()
ORDER BY Tanggal DESC;
```
---
### 9. **v_billing_tarif_analysis**
**Tujuan:** Analisis detail tarif dan klaim untuk financial validation
**Kolom:**
- `ID_Billing` - ID Billing
- `Nama_Pasien` - Nama pasien
- `Cara_Bayar` - Metode pembayaran
- `Total_Tarif_RS` - Total tarif RS
- `Total_Klaim` - Total klaim
- `Selisih_Tarif_Klaim` - Selisih tarif dan klaim
- `Persentase_Klaim` - Persentase klaim terhadap tarif (%)
- `Billing_Sign` - Status billing
- `Jumlah_Tindakan` - Jumlah tindakan
- `Jumlah_Kode_INACBG` - Jumlah kode INACBG
**Use Case:**
- Financial audit
- Tarif vs claim analysis
- Billing accuracy validation
- Revenue reconciliation
- Claim percentage tracking
**Query Example:**
```sql
SELECT * FROM v_billing_tarif_analysis
WHERE Persentase_Klaim < 50
AND Billing_Sign = 'Merah';
```
---
### 10. **v_ksm_performance**
**Tujuan:** Performance metrics per KSM (Kelompok Staf Medis)
**Kolom:**
- `KSM` - Nama KSM (PK)
- `Jumlah_Dokter` - Jumlah dokter di KSM
- `Jumlah_Billing_Ditangani` - Total billing yang ditangani
- `Avg_Billing_Per_Dokter` - Rata-rata billing per dokter
- `Total_Klaim_KSM` - Total klaim keseluruhan KSM
- `Billing_Sign_Hijau` - Jumlah billing status Hijau
- `Billing_Sign_Kuning` - Jumlah billing status Kuning
- `Billing_Sign_Merah` - Jumlah billing status Merah
**Use Case:**
- Departmental performance analysis
- Comparative KSM metrics
- Resource allocation planning
- Quality metrics tracking
- Financial performance by department
**Query Example:**
```sql
SELECT * FROM v_ksm_performance
ORDER BY Total_Klaim_KSM DESC;
```
---
## 🔍 Index Recommendation
Untuk optimasi lebih lanjut, recommend untuk membuat index pada kolom yang sering digunakan:
```sql
-- Index untuk v_billing_pasien_info
CREATE INDEX idx_billing_pasien_status ON billing_pasien(Tanggal_Keluar, Billing_Sign);
CREATE INDEX idx_billing_pasien_cara_bayar ON billing_pasien(Cara_Bayar);
CREATE INDEX idx_pasien_ruangan ON pasien(Ruangan);
-- Index untuk v_billing_detail
CREATE INDEX idx_billing_dokter_id ON billing_dokter(ID_Billing, ID_Dokter);
CREATE INDEX idx_dokter_ksm ON dokter(KSM);
-- Index untuk v_billing_diagnosis_procedure
CREATE INDEX idx_billing_icd10 ON billing_icd10(ID_Billing);
CREATE INDEX idx_billing_icd9 ON billing_icd9(ID_Billing);
-- Index untuk v_billing_inacbg_code
CREATE INDEX idx_billing_inacbg_ri ON billing_inacbg_ri(ID_Billing);
CREATE INDEX idx_billing_inacbg_rj ON billing_inacbg_rj(ID_Billing);
-- Index untuk tanggal
CREATE INDEX idx_billing_tanggal_masuk ON billing_pasien(Tanggal_Masuk);
CREATE INDEX idx_billing_tanggal_keluar ON billing_pasien(Tanggal_Keluar);
```
---
## 📝 Notes & Best Practices
### ✅ Kelebihan menggunakan Views:
1. **Performa Lebih Cepat** - Query sudah pre-compiled
2. **Konsistensi Data** - Logika aggregation terpusat
3. **Kemudahan Maintenance** - Perubahan logic hanya di satu tempat
4. **Security** - Bisa membatasi akses ke kolom tertentu
5. **Abstraksi** - Frontend tidak perlu tahu struktur table kompleks
### ⚠️ Perhatian:
1. Views adalah **read-only** (SELECT only) di versi MariaDB ini
2. Performa depends pada database size - gunakan index yang tepat
3. Aggregate functions (COUNT, SUM, etc) bisa lambat untuk dataset besar
4. Refresh view dengan menjalankan EXPLAIN untuk cek query plan
### 🚀 Tips Optimasi:
1. Gunakan WHERE clause untuk filter sebanyak mungkin
2. Limit hasil jika tidak perlu semua data
3. Cache hasil di aplikasi jika data tidak berubah sering
4. Monitor query performance dengan EXPLAIN
5. Update index statistics secara berkala
---
## 📊 Contoh Penggunaan di Backend
### Go Example (menggunakan GORM):
```go
// Model untuk View
type BillingPasienInfo struct {
IDBilling int `gorm:"column:ID_Billing"`
IDPasien int `gorm:"column:ID_Pasien"`
NamaPasien string `gorm:"column:Nama_Pasien"`
CaraBayar string `gorm:"column:Cara_Bayar"`
HariInap int `gorm:"column:Hari_Inap"`
StatusPasien string `gorm:"column:Status_Pasien"`
}
func (BillingPasienInfo) TableName() string {
return "v_billing_pasien_info"
}
// Usage dalam handler
func GetBillingAktif(db *gorm.DB, c *gin.Context) {
var billings []BillingPasienInfo
db.Where("Status_Pasien = ?", "Aktif").
Order("Tanggal_Masuk DESC").
Find(&billings)
c.JSON(200, billings)
}
```
---
## 🔄 Update & Maintenance
Views akan **secara otomatis** updated ketika data di table yang di-reference berubah. Tidak perlu maintenance manual.
---
**Dokumentasi dibuat: 23 Desember 2025**
**Database: CareIT v2**
**Version: 1.0**
File diff suppressed because it is too large Load diff
File diff suppressed because it is too large Load diff
File diff suppressed because it is too large Load diff
@@ -0,0 +1,16 @@
-- Insert data dummy untuk admin ruangan
-- Username: admin
-- Password: admin123
-- Hapus data admin jika sudah ada (untuk testing)
DELETE FROM `admin_ruangan` WHERE `Nama_Admin` = 'admin';
-- Insert data admin baru
INSERT INTO `admin_ruangan` (`ID_Admin`, `Nama_Admin`, `Password`, `ID_Ruangan`)
VALUES (1, 'admin', 'admin123', NULL);
-- Jika ID_Admin sudah ada, gunakan ID yang lebih tinggi
-- Atau biarkan AUTO_INCREMENT yang mengatur
-- INSERT INTO `admin_ruangan` (`Nama_Admin`, `Password`, `ID_Ruangan`)
-- VALUES ('admin', 'admin123', NULL);
@@ -0,0 +1,14 @@
{
"nama_dokter": "Dr. Ahmad Wijaya",
"nama_pasien": "Budi Santoso",
"jenis_kelamin": "Laki-laki",
"usia": 45,
"ruangan": "R001",
"kelas": "1",
"tindakan_rs": "T001",
"icd9": "ICD9-001",
"icd10": "ICD10-001",
"cara_bayar": "BPJS",
"total_tarif_rs": 500000
}
@@ -0,0 +1,14 @@
{
"nama_dokter": "Dr. Ahmad Wijaya",
"nama_pasien": "Budi Santoso",
"jenis_kelamin": "Laki-laki",
"usia": 45,
"ruangan": "R001",
"kelas": "1",
"tindakan_rs": "T001",
"icd9": "ICD9-001",
"icd10": "ICD10-001",
"cara_bayar": "BPJS",
"total_tarif_rs": 500000
}
@@ -0,0 +1,26 @@
{
"status": "success",
"message": "Billing berhasil dibuat",
"data": {
"billing": {
"ID_Billing": "BILL-12345678-1234-1234-1234-123456789abc",
"ID_Pasien": "PAS-87654321-4321-4321-4321-cba987654321",
"Cara_Bayar": "BPJS",
"Tanggal_masuk": "2024-01-15T10:30:00Z",
"Tanggal_keluar": null,
"ID_Dokter": "DOK-001",
"Total_Tarif_RS": 500000,
"Total_Tarif_BPJS": 0,
"Billing_sign": "created"
},
"pasien": {
"ID_Pasien": "PAS-87654321-4321-4321-4321-cba987654321",
"Nama_Pasien": "Budi Santoso",
"Jenis_Kelamin": "Laki-laki",
"Usia": 45,
"Ruangan": "R001",
"Kelas": "1"
}
}
}
@@ -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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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/[email protected]/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;
}
}
+789
View File
@@ -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>