initial monorepo commit (fe + be)
This commit is contained in:
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func KonekDB() (*gorm.DB, error) {
|
||||
dsn := os.Getenv("DB_DSN")
|
||||
|
||||
if dsn == "" {
|
||||
user := envOrDefault("DB_USER", "root")
|
||||
pass := envOrDefault("DB_PASSWORD", "")
|
||||
host := envOrDefault("DB_HOST", "localhost")
|
||||
port := envOrDefault("DB_PORT", "3306")
|
||||
name := envOrDefault("DB_NAME", "care_it_data")
|
||||
|
||||
fmt.Println("DB_USER:", os.Getenv("DB_USER"))
|
||||
fmt.Println("DB_PASSWORD:", os.Getenv("DB_PASSWORD"))
|
||||
fmt.Println("DB_HOST:", os.Getenv("DB_HOST"))
|
||||
fmt.Println("DB_PORT:", os.Getenv("DB_PORT"))
|
||||
fmt.Println("DB_NAME:", os.Getenv("DB_NAME"))
|
||||
fmt.Println("HOST:", os.Getenv("HOST"))
|
||||
fmt.Println("PORT:", os.Getenv("PORT"))
|
||||
|
||||
dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, pass, host, port, name)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal membuka koneksi database: %w", err)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func envOrDefault(key, fallback string) string {
|
||||
val := os.Getenv(key)
|
||||
if val == "" {
|
||||
return fallback
|
||||
}
|
||||
return val
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func KonekPG() (*gorm.DB, error) {
|
||||
dsn := os.Getenv("DB_DSN")
|
||||
|
||||
if dsn == "" {
|
||||
user := envOrDefaultPG("DB_USER", "postgres")
|
||||
pass := envOrDefaultPG("DB_PASSWORD", "gakbikinkembung25")
|
||||
host := envOrDefaultPG("DB_HOST", "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
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
module backendcareit
|
||||
|
||||
go 1.23.6
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
@@ -0,0 +1,883 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/middleware"
|
||||
"backendcareit/models"
|
||||
"backendcareit/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *gin.Engine) {
|
||||
// Routes get dokter
|
||||
r.GET("/dokter", listDokterHandler)
|
||||
// Routes get ruangan
|
||||
r.GET("/ruangan", listRuanganHandler)
|
||||
// Routes get icd9 icd10
|
||||
r.GET("/icd10", listICD10Handler)
|
||||
r.GET("/icd9", listICD9Handler)
|
||||
// Health check
|
||||
r.GET("/", healthHandler)
|
||||
// Routes tarif
|
||||
r.GET("/tarifBPJSRawatInap", listTarifBPJSRawatInapHandler)
|
||||
r.GET("/tarifBPJS/:kode", detailTarifBPJSRawatInapHandler)
|
||||
r.GET("/tarifBPJSRawatJalan", listTarifBPJSRawatJalanHandler)
|
||||
r.GET("/tarifBPJSRawatJalan/:kode", detailTarifBPJSRawatJalanHandler)
|
||||
r.GET("/tarifRS", listTarifRSHandler)
|
||||
r.GET("/tarifRS/:kode", detailTarifRSHandler)
|
||||
r.GET("/tarifRSByKategori/:kategori", listTarifRSByKategoriHandler)
|
||||
// Routes pasien & billing
|
||||
r.GET("/pasien/search", SearchPasienHandler)
|
||||
r.GET("/pasien/:id", GetPasien)
|
||||
r.POST("/billing", CreateBillingHandler)
|
||||
r.GET("/billing/aktif", GetBillingAktifByNamaHandler)
|
||||
r.PUT("/billing/:id", UpdateBillingHandler)
|
||||
|
||||
//close billing
|
||||
r.POST("/billing/close", CloseBillingHandler)
|
||||
|
||||
//get all billing aktif
|
||||
r.GET("/billing/aktif/all", GetAllBillingaktifhandler)
|
||||
|
||||
//admin edit inacbg
|
||||
r.PUT("/admin/inacbg", EditINACBGAdminHandler)
|
||||
// Admin: get all billing
|
||||
r.GET("/admin/billing", GetAllBillingHandler)
|
||||
// Admin: get riwayat billing (sudah ditutup)
|
||||
r.GET("/admin/riwayat-billing", GetRiwayatBillingHandler)
|
||||
// Admin: get riwayat billing with all patient data
|
||||
r.GET("/admin/riwayat-pasien-all", GetRiwayatPasienAllHandler)
|
||||
// Admin: get billing by ID
|
||||
r.GET("/admin/billing/:id", GetBillingByIDHandler)
|
||||
// Admin: post INACBG
|
||||
r.POST("/admin/inacbg", PostINACBGAdminHandler)
|
||||
// Admin: get ruangan dengan pasien
|
||||
r.GET("/admin/ruangan-dengan-pasien", GetRuanganWithPasienHandler)
|
||||
// Login dokter
|
||||
r.POST("/login", LoginDokterHandler(database.DB))
|
||||
// login admin
|
||||
r.POST("/admin/login", LoginAdminHandler(database.DB))
|
||||
}
|
||||
|
||||
// Coba tes koneksi dulu ya
|
||||
|
||||
func healthHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"message": "Server berjalan",
|
||||
})
|
||||
}
|
||||
|
||||
//Handler buat /admin/billing
|
||||
|
||||
func GetAllBillingHandler(c *gin.Context) {
|
||||
|
||||
data, err := services.GetAllBilling(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// Handler buat /admin/billing/:id
|
||||
func GetBillingByIDHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
data, err := services.GetBillingByID(database.DB, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// edit inacbg admin
|
||||
func EditINACBGAdminHandler(c *gin.Context) {
|
||||
var input models.Edit_INACBG_Request
|
||||
|
||||
// Ensure JSON
|
||||
if c.GetHeader("Content-Type") != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.Edit_INACBG_Admin(database.DB, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengedit INACBG",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "INACBG berhasil diupdate",
|
||||
})
|
||||
}
|
||||
|
||||
// Post INACBG from admin
|
||||
func PostINACBGAdminHandler(c *gin.Context) {
|
||||
var input models.Post_INACBG_Admin
|
||||
|
||||
// Ensure JSON
|
||||
if c.GetHeader("Content-Type") != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.Post_INACBG_Admin(database.DB, input); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal memproses INACBG",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "INACBG berhasil disimpan",
|
||||
})
|
||||
}
|
||||
|
||||
// List tarif BPJS Rawat Inap
|
||||
func listTarifBPJSRawatInapHandler(c *gin.Context) {
|
||||
data, err := services.GetTarifBPJSRawatInap()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func detailTarifBPJSRawatInapHandler(c *gin.Context) {
|
||||
kode := c.Param("kode")
|
||||
data, err := services.GetTarifBPJSRawatInapByKode(kode)
|
||||
if err != nil {
|
||||
if services.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"status": "not_found",
|
||||
"message": "Kode tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// List tarif BPJS Rawat Jalan
|
||||
func listTarifBPJSRawatJalanHandler(c *gin.Context) {
|
||||
data, err := services.GetTarifBPJSRawatJalan()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func detailTarifBPJSRawatJalanHandler(c *gin.Context) {
|
||||
kode := c.Param("kode")
|
||||
data, err := services.GetTarifBPJSRawatJalanByKode(kode)
|
||||
if err != nil {
|
||||
if services.IsNotFound(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"status": "not_found",
|
||||
"message": "Kode tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// List tarif RS
|
||||
func listTarifRSHandler(c *gin.Context) {
|
||||
data, err := services.GetTarifRS()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func detailTarifRSHandler(c *gin.Context) {
|
||||
kode := c.Param("kode")
|
||||
data, err := services.GetTarifRSByKode(kode)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func listTarifRSByKategoriHandler(c *gin.Context) {
|
||||
kategori := c.Param("kategori")
|
||||
data, err := services.GetTarifRSByKategori(kategori)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// ICD9
|
||||
func listICD9Handler(c *gin.Context) {
|
||||
data, err := services.GetICD9()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// ICD10
|
||||
func listICD10Handler(c *gin.Context) {
|
||||
data, err := services.GetICD10()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// ruangan
|
||||
func listRuanganHandler(c *gin.Context) {
|
||||
data, err := services.GetRuangan()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// GetRuanganWithPasienHandler - Ambil ruangan yang punya pasien
|
||||
func GetRuanganWithPasienHandler(c *gin.Context) {
|
||||
data, err := services.GetRuanganWithPasien(database.DB)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
// dokter
|
||||
func listDokterHandler(c *gin.Context) {
|
||||
data, err := services.GetDokter()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data",
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
//Liat pasien sudah atau belum
|
||||
|
||||
func GetPasien(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
|
||||
// Konversi string ke int
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"message": "ID pasien harus berupa angka",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pasien, err := services.GetPasienByID(id)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{
|
||||
"message": "Pasien tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"message": "Data pasien ditemukan",
|
||||
"data": pasien,
|
||||
})
|
||||
}
|
||||
|
||||
//add pasien baru
|
||||
|
||||
// CreateBillingHandler handler untuk membuat billing baru dari data frontend
|
||||
func CreateBillingHandler(c *gin.Context) {
|
||||
// Pastikan JSON
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Content-Type harus application/json",
|
||||
"error": "Content-Type yang diterima: " + contentType,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Gunakan map untuk menerima JSON fleksibel (bisa string atau array untuk nama_dokter)
|
||||
var rawData map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&rawData); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Konversi nama_dokter dari string ke array jika perlu
|
||||
if namaDokterRaw, ok := rawData["nama_dokter"]; ok {
|
||||
switch v := namaDokterRaw.(type) {
|
||||
case string:
|
||||
// Jika string, konversi ke array dengan 1 elemen
|
||||
if v != "" {
|
||||
rawData["nama_dokter"] = []string{v}
|
||||
} else {
|
||||
rawData["nama_dokter"] = []string{}
|
||||
}
|
||||
case []interface{}:
|
||||
// Jika sudah array, konversi ke []string
|
||||
namaDokterArray := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
if str, ok := item.(string); ok && str != "" {
|
||||
namaDokterArray = append(namaDokterArray, str)
|
||||
}
|
||||
}
|
||||
rawData["nama_dokter"] = namaDokterArray
|
||||
case []string:
|
||||
// Sudah dalam format yang benar
|
||||
rawData["nama_dokter"] = v
|
||||
default:
|
||||
rawData["nama_dokter"] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// Konversi map ke BillingRequest
|
||||
var input models.BillingRequest
|
||||
// Marshal dan unmarshal untuk konversi yang aman
|
||||
jsonData, err := json.Marshal(rawData)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal memproses data",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonData, &input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Data tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Panggil service → return 5 data
|
||||
billing, pasien, tindakanList, icd9List, icd10List, err :=
|
||||
services.DataFromFE(input)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal membuat billing",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Response lengkap ke FE
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Billing berhasil dibuat",
|
||||
"data": gin.H{
|
||||
"pasien": pasien,
|
||||
"billing": billing,
|
||||
"tindakan_rs": tindakanList,
|
||||
"icd9": icd9List,
|
||||
"icd10": icd10List,
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// GetBillingAktifByNamaHandler - Ambil billing aktif berdasarkan nama
|
||||
// Endpoint: GET /billing/aktif?nama_pasien=...
|
||||
// Mengembalikan billing aktif + semua tindakan & ICD & dokter & INACBG & DPJP
|
||||
func GetBillingAktifByNamaHandler(c *gin.Context) {
|
||||
nama := c.Query("nama_pasien")
|
||||
if strings.TrimSpace(nama) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "nama_pasien wajib diisi",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, err := services.GetBillingDetailAktifByNama(nama)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"status": "not_found",
|
||||
"message": "Billing aktif untuk pasien tersebut tidak ditemukan",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data billing",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Billing aktif ditemukan",
|
||||
"data": gin.H{
|
||||
"billing": billing,
|
||||
"tindakan_rs": tindakan,
|
||||
"icd9": icd9,
|
||||
"icd10": icd10,
|
||||
"dokter": dokter,
|
||||
"inacbg_ri": inacbgRI,
|
||||
"inacbg_rj": inacbgRJ,
|
||||
"id_dpjp": dpjp,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//search pasien by nama handler
|
||||
|
||||
func SearchPasienHandler(c *gin.Context) {
|
||||
nama := c.Query("nama")
|
||||
|
||||
pasien, err := services.SearchPasienByNama(nama)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengambil data pasien",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"data": pasien,
|
||||
})
|
||||
}
|
||||
|
||||
// Login dokter
|
||||
func LoginDokterHandler(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"status": "error",
|
||||
"message": "Payload login tidak valid",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||||
|
||||
var dokter models.Dokter
|
||||
if err := db.Where("LOWER(\"Email_UB\") = ? OR LOWER(\"Email_Pribadi\") = ?", email, email).
|
||||
First(&dokter).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Email atau password salah",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal memproses login",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Password check — skip if password column is empty
|
||||
if dokter.Password != "" && dokter.Password != req.Password {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Email atau password salah",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := middleware.GenerateToken(dokter, email)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal membuat token",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"token": token,
|
||||
"dokter": gin.H{
|
||||
"id": dokter.ID_Dokter,
|
||||
"nama": dokter.Nama_Dokter,
|
||||
"ksm": dokter.KSM,
|
||||
"email": email,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SendEmailTestHandler handler untuk test email
|
||||
func SendEmailTestHandler(c *gin.Context) {
|
||||
if err := services.SendEmailTest(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"status": "error",
|
||||
"message": "Gagal mengirim email test",
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "success",
|
||||
"message": "Email test berhasil dikirim ke [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,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/handlers"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
|
||||
db, err := database.KonekPG()
|
||||
if err != nil {
|
||||
log.Fatal("Gagal koneksi database:", err)
|
||||
}
|
||||
database.DB = db
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
config := cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
}
|
||||
r.Use(cors.New(config))
|
||||
|
||||
handlers.RegisterRoutes(r)
|
||||
|
||||
host := os.Getenv("HOST")
|
||||
if host == "" {
|
||||
host = "0.0.0.0"
|
||||
}
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8081"
|
||||
}
|
||||
|
||||
listenAddr := fmt.Sprintf("%s:%s", host, port)
|
||||
fmt.Printf("Server berjalan di http://%s\n", listenAddr)
|
||||
fmt.Println("Akses dari jaringan lain menggunakan IP lokal komputer + port", port)
|
||||
if err := r.Run(listenAddr); err != nil {
|
||||
log.Fatal("Gagal menjalankan server:", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
|
||||
"backendcareit/models"
|
||||
)
|
||||
|
||||
// Payload untuk login dokter - berisi credensial yang diperlukan
|
||||
|
||||
var jwtSecret = []byte(getJWTSecret())
|
||||
|
||||
func getJWTSecret() string {
|
||||
if secret := os.Getenv("JWT_SECRET"); secret != "" {
|
||||
return secret
|
||||
}
|
||||
return "SECRET_KAMU"
|
||||
}
|
||||
|
||||
// LoginDokterHandler - Ini handler POST /login yang ngecek kredensial dokter
|
||||
// Kalau cocok, langsung kasih JWT ke dia
|
||||
|
||||
// GenerateToken - Bikin JWT buat dokter, berlaku 24 jam terus expired hehe
|
||||
func GenerateToken(dokter models.Dokter, email string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"id": dokter.ID_Dokter,
|
||||
"nama": dokter.Nama_Dokter,
|
||||
"ksm": dokter.KSM,
|
||||
"email": email,
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// GenerateTokenAdmin - Serupa dengan dokter, tapi ini buat admin dengan role khusus
|
||||
func GenerateTokenAdmin(admin models.Admin_Ruangan, namaAdmin string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"id": admin.ID_Admin,
|
||||
"nama_admin": admin.Nama_Admin,
|
||||
"id_ruangan": admin.ID_Ruangan,
|
||||
"role": "admin",
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// AuthMiddleware - Middleware pengecekan token JWT
|
||||
// Ngecek header Authorization, parse tokennya, terus simpan data di context jika valid
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Authorization header wajib menggunakan Bearer token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer"))
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("metode tanda tangan tidak dikenal")
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Token tidak valid atau kadaluarsa",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"status": "error",
|
||||
"message": "Token tidak valid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("dokter_id", claims["id"])
|
||||
c.Set("dokter_nama", claims["nama"])
|
||||
c.Set("dokter_ksm", claims["ksm"])
|
||||
c.Set("dokter_email", claims["email"])
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Tarif Models - Data struktur buat tarif dari berbagai sumber
|
||||
|
||||
type TarifBPJSRawatInap struct {
|
||||
KodeINA string `gorm:"column:ID_INACBG_RI"`
|
||||
Deskripsi string `gorm:"column:Tindakan_RI"`
|
||||
Kelas1 float64 `gorm:"column:Tarif_Kelas_1"`
|
||||
Kelas2 float64 `gorm:"column:Tarif_Kelas_2"`
|
||||
Kelas3 float64 `gorm:"column:Tarif_Kelas_3"`
|
||||
}
|
||||
|
||||
type TarifBPJSRawatJalan struct {
|
||||
KodeINA string `gorm:"column:ID_INACBG_RJ"`
|
||||
Deskripsi string `gorm:"column:Tindakan_RJ"`
|
||||
TarifINACBG float64 `gorm:"column:Tarif_RJ" json:"tarif_inacbg"`
|
||||
}
|
||||
|
||||
type TarifRS struct {
|
||||
KodeRS string `gorm:"column:ID_Tarif_RS"`
|
||||
Deskripsi string `gorm:"column:Tindakan_RS"`
|
||||
Harga int `gorm:"column:Tarif_RS"`
|
||||
Kategori string `gorm:"column:Kategori_RS"`
|
||||
}
|
||||
|
||||
func (TarifBPJSRawatJalan) TableName() string {
|
||||
return "ina_cbg_rawatjalan"
|
||||
}
|
||||
|
||||
func (TarifBPJSRawatInap) TableName() string {
|
||||
return "ina_cbg_rawatinap"
|
||||
}
|
||||
|
||||
func (TarifRS) TableName() string {
|
||||
return "tarif_rs"
|
||||
}
|
||||
|
||||
// billing_inacbg_RI
|
||||
type Billing_INACBG_RI struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing"`
|
||||
Kode_INACBG string `gorm:"column:ID_INACBG_RI"`
|
||||
}
|
||||
|
||||
func (Billing_INACBG_RI) TableName() string {
|
||||
return "billing_inacbg_ri"
|
||||
}
|
||||
|
||||
// billing_inacbg_RJ
|
||||
type Billing_INACBG_RJ struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing"`
|
||||
Kode_INACBG string `gorm:"column:ID_INACBG_RJ"`
|
||||
}
|
||||
|
||||
func (Billing_INACBG_RJ) TableName() string {
|
||||
return "billing_inacbg_rj"
|
||||
}
|
||||
|
||||
type Billing_DPJP struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey"`
|
||||
ID_DPJP int `gorm:"column:ID_DPJP;primaryKey"`
|
||||
}
|
||||
|
||||
func (Billing_DPJP) TableName() string {
|
||||
return "billing_dpjp"
|
||||
}
|
||||
|
||||
// ICD9
|
||||
|
||||
type ICD9 struct {
|
||||
Kode_ICD9 string `gorm:"column:ID_ICD9"`
|
||||
Prosedur string `gorm:"column:Prosedur"`
|
||||
Versi string `gorm:"column:Versi_ICD9"`
|
||||
}
|
||||
|
||||
func (ICD9) TableName() string {
|
||||
return "icd9"
|
||||
}
|
||||
|
||||
// ICD10
|
||||
type ICD10 struct {
|
||||
Kode_ICD10 string `gorm:"column:ID_ICD10"`
|
||||
Diagnosa string `gorm:"column:Diagnosa"`
|
||||
Versi string `gorm:"column:Versi_ICD10"`
|
||||
}
|
||||
|
||||
func (ICD10) TableName() string {
|
||||
return "icd10"
|
||||
}
|
||||
|
||||
// ruangan
|
||||
type Ruangan struct {
|
||||
ID_Ruangan string `gorm:"column:ID_Ruangan"`
|
||||
Jenis_Ruangan string `gorm:"column:Jenis_Ruangan"`
|
||||
Nama_Ruangan string `gorm:"column:Nama_Ruangan"`
|
||||
Keterangan string `gorm:"column:keterangan"`
|
||||
Kategori_ruangan string `gorm:"column:kategori_ruangan"`
|
||||
}
|
||||
|
||||
func (Ruangan) TableName() string {
|
||||
return "ruangan"
|
||||
}
|
||||
|
||||
// dokter
|
||||
type Dokter struct {
|
||||
ID_Dokter int `gorm:"column:ID_Dokter;primaryKey"`
|
||||
Nama_Dokter string `gorm:"column:Nama_Dokter"`
|
||||
Password string `gorm:"column:Password"`
|
||||
Status string `gorm:"column:Status"`
|
||||
KSM string `gorm:"column:KSM"`
|
||||
Email_UB string `gorm:"column:Email_UB"`
|
||||
Email_Pribadi string `gorm:"column:Email_Pribadi"`
|
||||
}
|
||||
|
||||
func (Dokter) TableName() string {
|
||||
return "dokter"
|
||||
}
|
||||
|
||||
// PASIEN
|
||||
type Pasien struct {
|
||||
ID_Pasien int `gorm:"column:ID_Pasien;primaryKey;autoIncrement"`
|
||||
Nama_Pasien string `gorm:"column:Nama_Pasien"`
|
||||
Jenis_Kelamin string `gorm:"column:Jenis_Kelamin"`
|
||||
Usia int `gorm:"column:Usia"`
|
||||
Ruangan string `gorm:"column:Ruangan"`
|
||||
Kelas string `gorm:"column:Kelas"`
|
||||
}
|
||||
|
||||
type Kelas string
|
||||
|
||||
const (
|
||||
Kelas_1 Kelas = "1"
|
||||
Kelas_2 Kelas = "2"
|
||||
Kelas_3 Kelas = "3"
|
||||
)
|
||||
|
||||
type Jenis_kelamin string
|
||||
|
||||
const (
|
||||
Jenis_Kelamin_Laki_laki Jenis_kelamin = "Laki-laki"
|
||||
Jenis_Kelamin_Perempuan Jenis_kelamin = "Perempuan"
|
||||
)
|
||||
|
||||
func (Pasien) TableName() string {
|
||||
return "pasien"
|
||||
}
|
||||
|
||||
// login dokter
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
//edit inacbg admin
|
||||
|
||||
type Edit_INACBG_Request struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Tipe_inacbg string `json:"tipe_inacbg"`
|
||||
Kode_inacbg []string `json:"kode_inacbg"`
|
||||
Kode_Delete []string `json:"kode_delete"`
|
||||
Total_Klaim float64 `json:"total_klaim"`
|
||||
Billing_Sign string `json:"billing_sign"`
|
||||
}
|
||||
|
||||
//close billing
|
||||
|
||||
type Close_billing struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Tanggal_Keluar string `json:"tanggal_keluar"`
|
||||
}
|
||||
|
||||
//riwayat billing pasien
|
||||
|
||||
type Riwayat_Pasien_all struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
ID_Pasien int `json:"id_pasien"`
|
||||
Nama_Pasien string `json:"nama_pasien"`
|
||||
Jenis_Kelamin string `json:"jenis_kelamin"`
|
||||
Usia int `json:"usia"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Nama_Ruangan string `json:"nama_ruangan"` // ← Added: Ruangan name for display
|
||||
Kelas string `json:"kelas"`
|
||||
ID_DPJP int `json:"id_dpjp"` // ← Added: Doctor in charge
|
||||
Nama_DPJP string `json:"nama_dpjp"`
|
||||
Tanggal_Keluar string `json:"tanggal_keluar"`
|
||||
Tanggal_Masuk string `json:"tanggal_masuk"` // Tanggal_Masuk *time.Time `json:"tanggal_masuk"`
|
||||
Tanggal_Tindakan *time.Time `json:"tanggal_tindakan"`
|
||||
Tindakan_RS []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
Kode_INACBG string `json:"kode_inacbg"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"` // ← Added: Hospital tariff
|
||||
Total_Klaim float64 `json:"total_klaim"` // ← Added: BPJS claim
|
||||
}
|
||||
|
||||
//billing pasien
|
||||
|
||||
type BillingPasien struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;autoIncrement" json:"id_billing"`
|
||||
ID_Pasien int `gorm:"column:ID_Pasien" json:"id_pasien"`
|
||||
Cara_Bayar string `gorm:"column:Cara_Bayar" json:"cara_bayar"`
|
||||
Tanggal_masuk *time.Time `gorm:"column:Tanggal_Masuk" json:"tanggal_masuk"`
|
||||
Tanggal_keluar *time.Time `gorm:"column:Tanggal_Keluar" json:"tanggal_keluar"`
|
||||
Total_Tarif_RS float64 `gorm:"column:Total_Tarif_RS" json:"total_tarif_rs"`
|
||||
Total_Klaim float64 `gorm:"column:Total_Klaim" json:"total_klaim"`
|
||||
Billing_sign string `gorm:"column:Billing_Sign" json:"billing_sign"`
|
||||
}
|
||||
|
||||
type Cara_bayar string
|
||||
|
||||
const (
|
||||
Cara_Bayar_BPJS Cara_bayar = "BPJS"
|
||||
Cara_Bayar_UMUM Cara_bayar = "UMUM"
|
||||
)
|
||||
|
||||
func (BillingPasien) TableName() string {
|
||||
return "billing_pasien"
|
||||
}
|
||||
|
||||
// BillingRequest untuk menerima data dari frontend
|
||||
type BillingRequest struct {
|
||||
Nama_Dokter []string `json:"nama_dokter" binding:"required"` // Array untuk multiple doctors
|
||||
Nama_Pasien string `json:"nama_pasien" binding:"required"`
|
||||
Jenis_Kelamin string `json:"jenis_kelamin" binding:"required"`
|
||||
Usia int `json:"usia" binding:"required"`
|
||||
ID_DPJP int `json:"id_dpjp"`
|
||||
Ruangan string `json:"ruangan" binding:"required"`
|
||||
Kelas string `json:"kelas" binding:"required"`
|
||||
Tindakan_RS []string `json:"tindakan_rs" binding:"required"`
|
||||
Tanggal_Keluar string `json:"tanggal_keluar"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10" binding:"required"`
|
||||
Cara_Bayar string `json:"cara_bayar" binding:"required"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"`
|
||||
Total_Klaim_BPJS float64 `json:"total_klaim_bpjs"` // ← Added: Baseline BPJS claim from FE
|
||||
}
|
||||
|
||||
// admin ruangan //Admin_Ruangan
|
||||
|
||||
type Admin_Ruangan struct {
|
||||
ID_Admin int `gorm:"column:ID_Admin"`
|
||||
Nama_Admin string `gorm:"column:Nama_Admin"`
|
||||
Password string `gorm:"column:Password"`
|
||||
ID_Ruangan string `gorm:"column:ID_Ruangan"`
|
||||
}
|
||||
|
||||
func (Admin_Ruangan) TableName() string {
|
||||
return "admin_ruangan"
|
||||
}
|
||||
|
||||
// billing_Tidakan
|
||||
|
||||
type Billing_Tindakan struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
|
||||
ID_Tarif_RS string `gorm:"column:ID_Tarif_RS;primaryKey;not null"`
|
||||
Tanggal_Tindakan *time.Time `gorm:"column:tanggal_tindakan"`
|
||||
}
|
||||
|
||||
func (Billing_Tindakan) TableName() string {
|
||||
return "billing_tindakan"
|
||||
}
|
||||
|
||||
// billing_ICD9 dan ICD10
|
||||
|
||||
type Billing_ICD9 struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
|
||||
ID_ICD9 string `gorm:"column:ID_ICD9;primaryKey;not null"`
|
||||
}
|
||||
|
||||
type Billing_ICD10 struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing;primaryKey;not null"`
|
||||
ID_ICD10 string `gorm:"column:ID_ICD10;primaryKey;not null"`
|
||||
}
|
||||
|
||||
func (Billing_ICD9) TableName() string {
|
||||
return "billing_icd9"
|
||||
}
|
||||
func (Billing_ICD10) TableName() string {
|
||||
return "billing_icd10"
|
||||
}
|
||||
|
||||
// billing_Dokter - relasi many-to-many antara billing dan dokter dengan tracking tanggal
|
||||
type Billing_Dokter struct {
|
||||
ID_Billing int `gorm:"column:ID_Billing"`
|
||||
ID_Dokter int `gorm:"column:ID_Dokter"`
|
||||
Tanggal *time.Time `gorm:"column:tanggal"` // Tanggal kapan dokter menangani pasien
|
||||
}
|
||||
|
||||
func (Billing_Dokter) TableName() string {
|
||||
return "billing_dokter"
|
||||
}
|
||||
|
||||
// riwayat pasien
|
||||
type Riwayat_Pasien struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Nama_pasien string `json:"nama_pasien"`
|
||||
ID_Pasien int `json:"id_pasien"`
|
||||
Kelas string `json:"Kelas"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"`
|
||||
Total_Klaim float64 `json:"total_klaim"`
|
||||
Tindakan_RS []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
INACBG_RI []string `json:"inacbg_ri"`
|
||||
INACBG_RJ []string `json:"inacbg_rj"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
Nama_Dokter []string `json:"nama_dokter"`
|
||||
}
|
||||
|
||||
// Request untuk tampilan data Admin ( pengisian inacbg)
|
||||
type Request_Admin_Inacbg struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Nama_pasien string `json:"nama_pasien"`
|
||||
ID_Pasien int `json:"id_pasien"`
|
||||
Kelas string `json:"Kelas"`
|
||||
Ruangan string `json:"ruangan"`
|
||||
Total_Tarif_RS float64 `json:"total_tarif_rs"`
|
||||
Total_Klaim float64 `json:"total_klaim"`
|
||||
ID_DPJP int `json:"id_dpjp"`
|
||||
Tindakan_RS []string `json:"tindakan_rs"`
|
||||
ICD9 []string `json:"icd9"`
|
||||
ICD10 []string `json:"icd10"`
|
||||
INACBG_RI []string `json:"inacbg_ri"`
|
||||
INACBG_RJ []string `json:"inacbg_rj"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
Nama_Dokter []string `json:"nama_dokter"`
|
||||
}
|
||||
|
||||
// post ke data base
|
||||
type Post_INACBG_Admin struct {
|
||||
ID_Billing int `json:"id_billing"`
|
||||
Tipe_inacbg string `json:"tipe_inacbg"`
|
||||
Kode_INACBG []string `json:"kode_inacbg"`
|
||||
Total_klaim float64 `json:"total_klaim"`
|
||||
Billing_sign string `json:"billing_sign"`
|
||||
Tanggal_keluar string `json:"tanggal_keluar"` // Diisi oleh admin billing
|
||||
}
|
||||
|
||||
// login dokter
|
||||
type loginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (loginRequest) TableName() string {
|
||||
return "dokter"
|
||||
}
|
||||
|
||||
// getpasienwithallicd9andicd10,andtindakanrs
|
||||
@@ -0,0 +1,87 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"backendcareit/database"
|
||||
)
|
||||
|
||||
func CheckAdmin() {
|
||||
// Nyambungin ke database
|
||||
db, err := database.KonekDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal koneksi database: %v", err)
|
||||
}
|
||||
|
||||
// Set koneksi database
|
||||
database.DB = db
|
||||
|
||||
// Cek data admin
|
||||
type AdminRuangan struct {
|
||||
ID_Admin int `gorm:"column:ID_Admin"`
|
||||
Nama_Admin string `gorm:"column:Nama_Admin"`
|
||||
Password string `gorm:"column:Password"`
|
||||
ID_Ruangan *int `gorm:"column:ID_Ruangan"`
|
||||
}
|
||||
var admins []AdminRuangan
|
||||
|
||||
// Ambil semua admin
|
||||
result := db.Table("admin_ruangan").Find(&admins)
|
||||
if result.Error != nil {
|
||||
log.Fatalf("Gagal query admin: %v", result.Error)
|
||||
}
|
||||
|
||||
fmt.Printf("Total admin ditemukan: %d\n\n", len(admins))
|
||||
|
||||
if len(admins) == 0 {
|
||||
fmt.Println("⚠️ Tidak ada data admin di database!")
|
||||
fmt.Println("\nJalankan script insert_admin.go untuk menambahkan data admin:")
|
||||
fmt.Println(" go run scripts/insert_admin.go")
|
||||
return
|
||||
}
|
||||
|
||||
// Tampilin semua admin
|
||||
for i, admin := range admins {
|
||||
fmt.Printf("Admin #%d:\n", i+1)
|
||||
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
|
||||
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
|
||||
fmt.Printf(" Password: '%s' (length: %d)\n", admin.Password, len(admin.Password))
|
||||
if admin.ID_Ruangan != nil {
|
||||
fmt.Printf(" ID_Ruangan: %d\n", *admin.ID_Ruangan)
|
||||
} else {
|
||||
fmt.Printf(" ID_Ruangan: NULL\n")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// Test query buat username 'admin'
|
||||
var admin AdminRuangan
|
||||
err = db.Table("admin_ruangan").
|
||||
Where("Nama_Admin = ?", "admin").
|
||||
First(&admin).Error
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("❌ Query dengan Nama_Admin = 'admin' GAGAL")
|
||||
fmt.Printf(" Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Query dengan Nama_Admin = 'admin' BERHASIL")
|
||||
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
|
||||
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
|
||||
fmt.Printf(" Password: '%s'\n", admin.Password)
|
||||
}
|
||||
|
||||
// Test query case-insensitive
|
||||
err = db.Table("admin_ruangan").
|
||||
Where("LOWER(Nama_Admin) = LOWER(?)", "admin").
|
||||
First(&admin).Error
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("❌ Query case-insensitive GAGAL")
|
||||
fmt.Printf(" Error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("✅ Query case-insensitive BERHASIL")
|
||||
fmt.Printf(" ID_Admin: %d\n", admin.ID_Admin)
|
||||
fmt.Printf(" Nama_Admin: '%s'\n", admin.Nama_Admin)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package scripts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"backendcareit/database"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Nyambungin ke database
|
||||
db, err := database.KonekDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Gagal koneksi database: %v", err)
|
||||
}
|
||||
|
||||
// Set koneksi database
|
||||
database.DB = db
|
||||
|
||||
// Cek admin udah ada atau belum
|
||||
var count int64
|
||||
db.Table("admin_ruangan").Where("Nama_Admin = ?", "admin").Count(&count)
|
||||
|
||||
if count > 0 {
|
||||
fmt.Println("Admin dengan username 'admin' sudah ada di database.")
|
||||
fmt.Println("Ngapus admin yang lama...")
|
||||
db.Table("admin_ruangan").Where("Nama_Admin = ?", "admin").Delete(nil)
|
||||
}
|
||||
|
||||
// Masukin admin yang baru
|
||||
result := db.Exec(`
|
||||
INSERT INTO admin_ruangan (Nama_Admin, Password, ID_Ruangan)
|
||||
VALUES (?, ?, ?)
|
||||
`, "admin", "admin123", nil)
|
||||
|
||||
if result.Error != nil {
|
||||
log.Fatalf("Gagal insert admin: %v", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected > 0 {
|
||||
fmt.Println("✓ Data admin berhasil ditambahkan!")
|
||||
fmt.Println(" Username: admin")
|
||||
fmt.Println(" Password: admin123")
|
||||
} else {
|
||||
fmt.Println("Tidak ada data yang ditambahkan.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/models"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Edit_INACBG_Admin(db *gorm.DB, input models.Edit_INACBG_Request) error {
|
||||
log.Printf("[Edit INACBG] Received ID_Billing=%d, Tipe=%s, Kode_count=%d, Delete_count=%d, Total_Klaim=%.2f, Billing_Sign=%s\n",
|
||||
input.ID_Billing, input.Tipe_inacbg, len(input.Kode_inacbg), len(input.Kode_Delete), input.Total_Klaim, input.Billing_Sign)
|
||||
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. Hapus kode yang udah dipilih untuk dihapus
|
||||
if len(input.Kode_Delete) > 0 {
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
if err := tx.Where("\"ID_Billing\" = ? AND \"ID_INACBG_RI\" IN ?", input.ID_Billing, input.Kode_Delete).Delete(&models.Billing_INACBG_RI{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal delete INACBG RI: %w", err)
|
||||
}
|
||||
case "RJ":
|
||||
if err := tx.Where("\"ID_Billing\" = ? AND \"ID_INACBG_RJ\" IN ?", input.ID_Billing, input.Kode_Delete).Delete(&models.Billing_INACBG_RJ{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal delete INACBG RJ: %w", err)
|
||||
}
|
||||
default:
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("invalid tipe_inacbg: %s", input.Tipe_inacbg)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tambahin kode INACBG yang baru
|
||||
if len(input.Kode_inacbg) > 0 {
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
for _, kode := range input.Kode_inacbg {
|
||||
inacbgRI := models.Billing_INACBG_RI{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
}
|
||||
if err := tx.Create(&inacbgRI).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RI kode %s: %w", kode, err)
|
||||
}
|
||||
}
|
||||
case "RJ":
|
||||
for _, kode := range input.Kode_inacbg {
|
||||
inacbgRJ := models.Billing_INACBG_RJ{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
}
|
||||
if err := tx.Create(&inacbgRJ).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RJ kode %s: %w", kode, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update data di tabel billing_pasien
|
||||
updateData := map[string]interface{}{
|
||||
"Total_Klaim": input.Total_Klaim,
|
||||
"Billing_Sign": input.Billing_Sign,
|
||||
}
|
||||
|
||||
if err := tx.Model(&models.BillingPasien{}).Where("\"ID_Billing\" = ?", input.ID_Billing).Updates(updateData).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal update billing_pasien: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. Ngirim email kalo billing_sign gak kosong
|
||||
go func(id int) {
|
||||
if err := SendEmailBillingSignToDokter(id); err != nil {
|
||||
log.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", id, err)
|
||||
}
|
||||
}(input.ID_Billing)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// EditPasienComplete - Update data identitas pasien dalam billing (nama, umur, ruangan, dll) terus dengan lookup kode
|
||||
func EditPasienComplete(billingId int, namaPasien string, usia int, jeniKelamin string, ruangan string, kelas string, tindakan []string, icd9 []string, icd10 []string, billingSign *string, totalTarifRS *float64) error {
|
||||
log.Printf("[EditPasien] START - billingId:%d, nama:%s, tindakan_count:%d, icd9_count:%d, icd10_count:%d\n", billingId, namaPasien, len(tindakan), len(icd9), len(icd10))
|
||||
|
||||
// Get billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.Where("\"ID_Billing\" = ?", billingId).First(&billing).Error; err != nil {
|
||||
log.Printf("[EditPasien] ERROR - billing not found: %v\n", err)
|
||||
return errors.New("billing tidak ditemukan")
|
||||
}
|
||||
log.Printf("[EditPasien] ✓ Billing found - ID_Pasien: %d\n", billing.ID_Pasien)
|
||||
|
||||
// Start transaction
|
||||
tx := database.DB.Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Update pasien data
|
||||
if err := tx.Model(&models.Pasien{}).
|
||||
Where("\"ID_Pasien\" = ?", billing.ID_Pasien).
|
||||
Updates(map[string]interface{}{
|
||||
"Nama_Pasien": namaPasien,
|
||||
"Usia": usia,
|
||||
"Jenis_Kelamin": jeniKelamin,
|
||||
"Ruangan": ruangan,
|
||||
"Kelas": kelas,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update data pasien: " + err.Error())
|
||||
}
|
||||
|
||||
// Delete existing tindakan
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_Tindakan{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete tindakan: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new tindakan dengan lookup berdasarkan nama tindakan
|
||||
now := time.Now()
|
||||
for _, tindakanNama := range tindakan {
|
||||
if tindakanNama != "" {
|
||||
log.Printf("[EditPasien] Looking up tindakan: '%s'\n", tindakanNama)
|
||||
// Lookup tarif by deskripsi (nama tindakan) - use quoted column name for PostgreSQL
|
||||
var tarif models.TarifRS
|
||||
if err := tx.Where("\"Tindakan_RS\" = ?", tindakanNama).First(&tarif).Error; err != nil {
|
||||
log.Printf("[EditPasien] ERROR - tindakan lookup failed: %v\n", err)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tx.Rollback()
|
||||
log.Printf("[EditPasien] ERROR - tindakan '%s' not found in tarif_rs\n", tindakanNama)
|
||||
return fmt.Errorf("tindakan '%s' tidak ditemukan", tindakanNama)
|
||||
}
|
||||
tx.Rollback()
|
||||
return errors.New("gagal lookup tindakan: " + err.Error())
|
||||
}
|
||||
log.Printf("[EditPasien] ✓ Tindakan found - ID: %s, Harga: %d\n", tarif.KodeRS, tarif.Harga)
|
||||
|
||||
newTindakan := models.Billing_Tindakan{
|
||||
ID_Billing: billingId,
|
||||
ID_Tarif_RS: tarif.KodeRS,
|
||||
Tanggal_Tindakan: &now,
|
||||
}
|
||||
if err := tx.Create(&newTindakan).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert tindakan: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD9
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD9{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD9: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD9 dengan lookup berdasarkan nama prosedur
|
||||
for _, icd9Nama := range icd9 {
|
||||
if icd9Nama != "" {
|
||||
// Lookup ICD9 by prosedur name
|
||||
var icd9Data models.ICD9
|
||||
if err := tx.Where("\"Prosedur\" = ?", icd9Nama).First(&icd9Data).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("ICD9 '%s' tidak ditemukan", icd9Nama)
|
||||
}
|
||||
tx.Rollback()
|
||||
return errors.New("gagal lookup ICD9: " + err.Error())
|
||||
}
|
||||
|
||||
newICD9 := models.Billing_ICD9{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD9: icd9Data.Kode_ICD9,
|
||||
}
|
||||
if err := tx.Create(&newICD9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD9: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD10
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD10{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD10: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD10 dengan lookup berdasarkan nama diagnosa
|
||||
for _, icd10Nama := range icd10 {
|
||||
if icd10Nama != "" {
|
||||
// Lookup ICD10 by diagnosa name
|
||||
var icd10Data models.ICD10
|
||||
if err := tx.Where("\"Diagnosa\" = ?", icd10Nama).First(&icd10Data).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("ICD10 '%s' tidak ditemukan", icd10Nama)
|
||||
}
|
||||
tx.Rollback()
|
||||
return errors.New("gagal lookup ICD10: " + err.Error())
|
||||
}
|
||||
|
||||
newICD10 := models.Billing_ICD10{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD10: icd10Data.Kode_ICD10,
|
||||
}
|
||||
if err := tx.Create(&newICD10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD10: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update billing_sign jika dikirimkan dari FE
|
||||
if billingSign != nil {
|
||||
if err := tx.Model(&models.BillingPasien{}).
|
||||
Where("\"ID_Billing\" = ?", billingId).
|
||||
Update("Billing_Sign", *billingSign).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update billing_sign: " + err.Error())
|
||||
}
|
||||
}
|
||||
// Update total_tarif_rs jika dikirimkan dari FE
|
||||
if totalTarifRS != nil {
|
||||
if err := tx.Model(&models.BillingPasien{}).
|
||||
Where("\"ID_Billing\" = ?", billingId).
|
||||
Update("Total_Tarif_RS", *totalTarifRS).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update total_tarif_rs: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.New("gagal commit transaction: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Post_INACBG_Admin(db *gorm.DB, input models.Post_INACBG_Admin) error {
|
||||
// Debug log
|
||||
log.Printf("[INACBG] Input received: ID_Billing=%d, Tipe=%s, Kode_count=%d, Total_klaim=%.2f, BillingSign=%s\n",
|
||||
input.ID_Billing, input.Tipe_inacbg, len(input.Kode_INACBG), input.Total_klaim, input.Billing_sign)
|
||||
|
||||
tx := db.Begin()
|
||||
if tx.Error != nil {
|
||||
log.Printf("[INACBG] Error starting transaction: %v\n", tx.Error)
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// Ensure rollback on panic / unexpected error
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[INACBG] Panic recovered: %v\n", r)
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// Validate input
|
||||
if input.Tipe_inacbg != "RI" && input.Tipe_inacbg != "RJ" {
|
||||
tx.Rollback()
|
||||
err := errors.New("invalid tipe_inacbg: must be 'RI' or 'RJ'")
|
||||
log.Printf("[INACBG] Validation error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
if len(input.Kode_INACBG) == 0 {
|
||||
tx.Rollback()
|
||||
err := errors.New("Kode_INACBG tidak boleh kosong")
|
||||
log.Printf("[INACBG] Validation error: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Ngambil billing dulu buat dapetin total klaim yang lama
|
||||
var existingBilling models.BillingPasien
|
||||
if err := tx.First(&existingBilling, input.ID_Billing).Error; err != nil {
|
||||
tx.Rollback()
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = fmt.Errorf("billing dengan ID_Billing=%d tidak ditemukan", input.ID_Billing)
|
||||
log.Printf("[INACBG] %v\n", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("[INACBG] Error fetching billing: %v\n", err)
|
||||
return fmt.Errorf("gagal mengambil billing: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] Found billing: ID=%d, Current_Total_Klaim=%.2f\n", existingBilling.ID_Billing, existingBilling.Total_Klaim)
|
||||
|
||||
// Hitung total klaim yang baru = yang lama + tambahan
|
||||
newTotalKlaim := input.Total_klaim
|
||||
log.Printf("[INACBG] New total klaim: %.2f + %.2f = %.2f\n", existingBilling.Total_Klaim, input.Total_klaim, newTotalKlaim)
|
||||
|
||||
// Parse Tanggal_Keluar jika diisi oleh admin
|
||||
var keluarPtr *time.Time
|
||||
if input.Tanggal_keluar != "" && input.Tanggal_keluar != "null" {
|
||||
s := input.Tanggal_keluar
|
||||
var parsed time.Time
|
||||
var err error
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
parsed, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
t := parsed
|
||||
keluarPtr = &t
|
||||
log.Printf("[INACBG] Parsed tanggal_keluar: %v\n", t)
|
||||
break
|
||||
}
|
||||
}
|
||||
if keluarPtr == nil {
|
||||
tx.Rollback()
|
||||
err := fmt.Errorf("invalid tanggal_keluar format: %s", input.Tanggal_keluar)
|
||||
log.Printf("[INACBG] %v\n", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update total klaim kumulatif sama tanggal keluar (kalo ada yang ngirim)
|
||||
updateData := map[string]interface{}{
|
||||
"\"Total_Klaim\"": newTotalKlaim,
|
||||
}
|
||||
if keluarPtr != nil {
|
||||
updateData["\"Tanggal_Keluar\""] = keluarPtr
|
||||
}
|
||||
|
||||
// Kalo frontend kirim billing_sign, langsung simpen ke kolom Billing_Sign
|
||||
if input.Billing_sign != "" {
|
||||
updateData["\"Billing_Sign\""] = input.Billing_sign
|
||||
log.Printf("[INACBG] Will update Billing_Sign to: %s\n", input.Billing_sign)
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] Update data: %v\n", updateData)
|
||||
|
||||
res := tx.Model(&models.BillingPasien{}).
|
||||
Where("\"ID_Billing\" = ?", input.ID_Billing).
|
||||
Updates(updateData)
|
||||
|
||||
if res.Error != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("[INACBG] Error updating billing: %v\n", res.Error)
|
||||
return fmt.Errorf("gagal update billing: %w", res.Error)
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] Updated %d rows in billing_pasien\n", res.RowsAffected)
|
||||
|
||||
// DELETE semua kode INACBG yang lama buat billing ini (biar gak duplikat pas INSERT)
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
if err := tx.Where("\"ID_Billing\" = ?", input.ID_Billing).Delete(&models.Billing_INACBG_RI{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("[INACBG] Error deleting old INACBG RI: %v\n", err)
|
||||
return fmt.Errorf("gagal delete INACBG RI lama: %w", err)
|
||||
}
|
||||
log.Printf("[INACBG] Deleted old INACBG RI records for ID_Billing=%d\n", input.ID_Billing)
|
||||
|
||||
case "RJ":
|
||||
if err := tx.Where("\"ID_Billing\" = ?", input.ID_Billing).Delete(&models.Billing_INACBG_RJ{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("[INACBG] Error deleting old INACBG RJ: %v\n", err)
|
||||
return fmt.Errorf("gagal delete INACBG RJ lama: %w", err)
|
||||
}
|
||||
log.Printf("[INACBG] Deleted old INACBG RJ records for ID_Billing=%d\n", input.ID_Billing)
|
||||
}
|
||||
|
||||
// Bulk insert kode INACBG berdasarkan tipenya (udah dihapus yang lama)
|
||||
switch input.Tipe_inacbg {
|
||||
case "RI":
|
||||
records := make([]models.Billing_INACBG_RI, 0, len(input.Kode_INACBG))
|
||||
for _, kode := range input.Kode_INACBG {
|
||||
records = append(records, models.Billing_INACBG_RI{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&records).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RI: %w", err)
|
||||
}
|
||||
|
||||
case "RJ":
|
||||
records := make([]models.Billing_INACBG_RJ, 0, len(input.Kode_INACBG))
|
||||
for _, kode := range input.Kode_INACBG {
|
||||
records = append(records, models.Billing_INACBG_RJ{
|
||||
ID_Billing: input.ID_Billing,
|
||||
Kode_INACBG: kode,
|
||||
})
|
||||
}
|
||||
if err := tx.Create(&records).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("gagal insert INACBG RJ: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Printf("[INACBG] Error committing transaction: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[INACBG] ✅ Successfully saved INACBG for ID_Billing=%d, billing_sign=%s\n", input.ID_Billing, input.Billing_sign)
|
||||
|
||||
// Ngirim email ke dokter kalo billing_sign gak kosong
|
||||
if input.Billing_sign != "" && strings.TrimSpace(input.Billing_sign) != "" {
|
||||
// Ngirim email asynchronous (kalo gagal, jangan perpengaruh proses utama)
|
||||
// Log error tapi jangan return, biar proses utama tetep berhasil
|
||||
if err := SendEmailBillingSignToDokter(input.ID_Billing); err != nil {
|
||||
// Log error tapi tidak return error agar proses utama tetap berhasil
|
||||
// Di production, bisa pake logger yang lebih proper
|
||||
fmt.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", input.ID_Billing, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetAllBilling(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ngambil semua billing yang belum ditutup (Tanggal_Keluar masih kosong)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulin dulu semua ID_Billing sama ID_Pasien
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Loaded %d pasien from database\n", len(pasienList))
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
log.Printf("[DEBUG] Pasien %d: Nama=%s, Ruangan=%s\n", p.ID_Pasien, p.Nama_Pasien, p.Ruangan)
|
||||
}
|
||||
|
||||
// Ambil tindakan yang berkaitan sama billing-billing ini
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ngambil semua ICD9 yang ada
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil semua ICD10 yang ada
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RI
|
||||
inacbgRIMap := make(map[int][]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RJ
|
||||
inacbgRJMap := make(map[int][]string)
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil dokter dari tabel billing_dokter, diurutkan berdasarkan tanggal
|
||||
dokterMap := make(map[int][]string)
|
||||
var dokterRows []struct {
|
||||
ID_Billing int
|
||||
Nama string
|
||||
Tanggal time.Time
|
||||
}
|
||||
if err := db.Table("\"billing_dokter\"").
|
||||
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\", \"tanggal\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Order("\"tanggal\" ASC").
|
||||
Scan(&dokterRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dokterRows {
|
||||
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
|
||||
}
|
||||
|
||||
// Ambil nama ruangan buat di-mapping dari ID jadi Nama
|
||||
ruanganNameMap := make(map[string]string)
|
||||
var ruanganRows []struct {
|
||||
ID_Ruangan string
|
||||
Nama_Ruangan string
|
||||
}
|
||||
if err := db.Table("\"ruangan\"").
|
||||
Select("\"ID_Ruangan\", \"Nama_Ruangan\"").
|
||||
Scan(&ruanganRows).Error; err != nil {
|
||||
log.Printf("[WARNING] Gagal ngambil ruangan: %v\\n", err)
|
||||
// Lanjutin aja, ID jadi fallback
|
||||
} else {
|
||||
for _, row := range ruanganRows {
|
||||
ruanganNameMap[row.ID_Ruangan] = row.Nama_Ruangan
|
||||
}
|
||||
log.Printf("[DEBUG] Loaded %d ruangan mappings\n", len(ruanganNameMap))
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang bagus
|
||||
var result []models.Request_Admin_Inacbg
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
// ruangan bisa jadi udah nama, bukan ID, langsung pake aja
|
||||
ruanganDisplay := pasien.Ruangan
|
||||
|
||||
// Tapi kalo mirip ID dan ada mapping, pake nama yang sudah dimapping
|
||||
if mappedName, exists := ruanganNameMap[pasien.Ruangan]; exists && mappedName != "" {
|
||||
ruanganDisplay = mappedName
|
||||
}
|
||||
|
||||
item := models.Request_Admin_Inacbg{
|
||||
ID_Billing: b.ID_Billing,
|
||||
Nama_pasien: pasien.Nama_Pasien,
|
||||
ID_Pasien: b.ID_Pasien,
|
||||
Kelas: pasien.Kelas,
|
||||
Ruangan: ruanganDisplay, // ← Use name directly if available, or mapped name
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
INACBG_RI: inacbgRIMap[b.ID_Billing],
|
||||
INACBG_RJ: inacbgRJMap[b.ID_Billing],
|
||||
Billing_sign: b.Billing_sign,
|
||||
Nama_Dokter: dokterMap[b.ID_Billing],
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetBillingByID - Get specific billing data by ID
|
||||
func GetBillingByID(db *gorm.DB, id string) (map[string]interface{}, error) {
|
||||
var billing models.BillingPasien
|
||||
|
||||
if err := db.Where("\"ID_Billing\" = ?", id).First(&billing).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("billing dengan ID=%s tidak ditemukan", id)
|
||||
}
|
||||
return nil, fmt.Errorf("gagal mengambil billing: %w", err)
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"id_billing": billing.ID_Billing,
|
||||
"id_pasien": billing.ID_Pasien,
|
||||
"cara_bayar": billing.Cara_Bayar,
|
||||
"tanggal_masuk": billing.Tanggal_masuk,
|
||||
"tanggal_keluar": billing.Tanggal_keluar,
|
||||
"total_tarif_rs": billing.Total_Tarif_RS,
|
||||
"total_klaim": billing.Total_Klaim,
|
||||
"billing_sign": billing.Billing_sign,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SendEmail - Ngirim email pake SMTP bro
|
||||
func SendEmail(to, subject, body string) error {
|
||||
// Ambil konfigurasi dari env variable dulu, lebih aman
|
||||
from := os.Getenv("EMAIL_FROM")
|
||||
password := os.Getenv("EMAIL_PASSWORD")
|
||||
smtpHost := os.Getenv("SMTP_HOST")
|
||||
smtpPort := os.Getenv("SMTP_PORT")
|
||||
|
||||
// Kalau env variable gak ada, pake default value (biar kompatibel sama versi lama)
|
||||
if from == "" {
|
||||
from = "[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
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetAllBillingaktif(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ambil semua billing yang masih aktif (belum ditutup, Tanggal_Keluar masih kosong)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulin dulu semua ID_Billing dan ID_Pasien buat di-query
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
}
|
||||
|
||||
// Ambil tindakan yang berkaitan sama billing-billing ini
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD9
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD10
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil INACBG RI
|
||||
inacbgRIMap := make(map[int][]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil INACBG RJ
|
||||
inacbgRJMap := make(map[int][]string)
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil dokter dari billing_dokter dengan urutan tanggal
|
||||
dokterMap := make(map[int][]string)
|
||||
var dokterRows []struct {
|
||||
ID_Billing int
|
||||
Nama string
|
||||
}
|
||||
if err := db.Table("\"billing_dokter\"").
|
||||
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Order("tanggal ASC").
|
||||
Scan(&dokterRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dokterRows {
|
||||
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang keren
|
||||
var result []models.Request_Admin_Inacbg
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
item := models.Request_Admin_Inacbg{
|
||||
ID_Billing: b.ID_Billing,
|
||||
Nama_pasien: pasien.Nama_Pasien,
|
||||
ID_Pasien: b.ID_Pasien,
|
||||
Kelas: pasien.Kelas,
|
||||
Ruangan: pasien.Ruangan,
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
INACBG_RI: inacbgRIMap[b.ID_Billing],
|
||||
INACBG_RJ: inacbgRJMap[b.ID_Billing],
|
||||
Billing_sign: b.Billing_sign,
|
||||
Nama_Dokter: dokterMap[b.ID_Billing],
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Ambil ID_tarif_RS dari nama Tindakan_RS
|
||||
func GetTarifRSByTindakan(tindakans []string) ([]models.TarifRS, error) {
|
||||
var tarifList []models.TarifRS
|
||||
|
||||
if err := database.DB.
|
||||
Where("\"Tindakan_RS\" IN ?", tindakans).
|
||||
Find(&tarifList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tarifList, nil
|
||||
}
|
||||
|
||||
// GetPasienByID - Cari pasien berdasarkan ID nya
|
||||
func GetPasienByID(id int) (*models.Pasien, error) {
|
||||
var pasien models.Pasien
|
||||
|
||||
if err := database.DB.Where("\"ID_Pasien\" = ?", id).First(&pasien).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pasien, nil
|
||||
}
|
||||
|
||||
// GetPasienByNama - Cari pasien berdasarkan nama mereka
|
||||
func GetPasienByNama(nama string) (*models.Pasien, error) {
|
||||
var pasien models.Pasien
|
||||
|
||||
if err := database.DB.Where("\"Nama_Pasien\" = ?", nama).First(&pasien).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pasien, nil
|
||||
}
|
||||
|
||||
// SearchPasienByNama - Pencarian pasien pake nama (bisa partial)
|
||||
func SearchPasienByNama(nama string) ([]models.Pasien, error) {
|
||||
var pasien []models.Pasien
|
||||
|
||||
err := database.DB.
|
||||
Where("\"Nama_Pasien\" LIKE ?", "%"+nama+"%").
|
||||
Find(&pasien).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pasien, nil
|
||||
}
|
||||
|
||||
// GetBillingDetailAktifByNama - Ambil data billing lengkap (billing, tindakan, ICD, dokter, INACBG, DPJP) buat satu pasien dari nama
|
||||
// Return: billing, tindakan, icd9, icd10, dokter, inacbgRI, inacbgRJ, dpjp, error
|
||||
func GetBillingDetailAktifByNama(namaPasien string) (*models.BillingPasien, []string, []string, []string, []string, []string, []string, int, error) {
|
||||
// Cari pasien dulu
|
||||
var pasien models.Pasien
|
||||
if err := database.DB.Where("\"Nama_Pasien\" = ?", namaPasien).First(&pasien).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
// Cari billing aktif terakhir pasien ini (yang belum ditutup, Tanggal_Keluar IS NULL)
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.
|
||||
Where("\"ID_Pasien\" = ? AND \"Tanggal_Keluar\" IS NULL", pasien.ID_Pasien).
|
||||
Order("\"ID_Billing\" DESC").
|
||||
First(&billing).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
|
||||
// Ambil semua tindakan (join billing_tindakan -> tarif_rs)
|
||||
var tindakanJoin []struct {
|
||||
Nama string `gorm:"column:Tindakan_RS"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_tindakan\" bt").
|
||||
Select("tr.\"Tindakan_RS\"").
|
||||
Joins("JOIN \"tarif_rs\" tr ON bt.\"ID_Tarif_RS\" = tr.\"ID_Tarif_RS\"").
|
||||
Where("bt.\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&tindakanJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
tindakanNames := make([]string, 0, len(tindakanJoin))
|
||||
for _, t := range tindakanJoin {
|
||||
tindakanNames = append(tindakanNames, t.Nama)
|
||||
}
|
||||
|
||||
// Ambil semua ICD9
|
||||
var icd9Join []struct {
|
||||
Prosedur string `gorm:"column:Prosedur"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_icd9\" bi").
|
||||
Select("i.\"Prosedur\"").
|
||||
Joins("JOIN \"icd9\" i ON bi.\"ID_ICD9\" = i.\"ID_ICD9\"").
|
||||
Where("bi.\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&icd9Join).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
icd9Names := make([]string, 0, len(icd9Join))
|
||||
for _, i := range icd9Join {
|
||||
icd9Names = append(icd9Names, i.Prosedur)
|
||||
}
|
||||
|
||||
// Ambil semua ICD10
|
||||
var icd10Join []struct {
|
||||
Diagnosa string `gorm:"column:Diagnosa"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_icd10\" bi").
|
||||
Select("i.\"Diagnosa\"").
|
||||
Joins("JOIN \"icd10\" i ON bi.\"ID_ICD10\" = i.\"ID_ICD10\"").
|
||||
Where("bi.\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&icd10Join).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
icd10Names := make([]string, 0, len(icd10Join))
|
||||
for _, i := range icd10Join {
|
||||
icd10Names = append(icd10Names, i.Diagnosa)
|
||||
}
|
||||
|
||||
// Ambil semua dokter dari billing_dokter dengan tanggal
|
||||
var dokterJoin []struct {
|
||||
Nama string `gorm:"column:Nama_Dokter"`
|
||||
Tanggal *time.Time `gorm:"column:Tanggal"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_dokter\"").
|
||||
Select("\"Nama_Dokter\", \"tanggal\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"billing_dokter\".\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Order("tanggal ASC").
|
||||
Scan(&dokterJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
dokterNames := make([]string, 0, len(dokterJoin))
|
||||
for _, d := range dokterJoin {
|
||||
dokterNames = append(dokterNames, d.Nama)
|
||||
}
|
||||
|
||||
// Ambil semua INACBG RI
|
||||
var inacbgRIJoin []struct {
|
||||
Kode string `gorm:"column:ID_INACBG_RI"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_inacbg_ri\"").
|
||||
Select("\"ID_INACBG_RI\"").
|
||||
Where("\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&inacbgRIJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
inacbgRINames := make([]string, 0, len(inacbgRIJoin))
|
||||
for _, row := range inacbgRIJoin {
|
||||
inacbgRINames = append(inacbgRINames, row.Kode)
|
||||
}
|
||||
|
||||
// Ambil semua INACBG RJ
|
||||
var inacbgRJJoin []struct {
|
||||
Kode string `gorm:"column:ID_INACBG_RJ"`
|
||||
}
|
||||
if err := database.DB.
|
||||
Table("\"billing_inacbg_rj\"").
|
||||
Select("\"ID_INACBG_RJ\"").
|
||||
Where("\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
Scan(&inacbgRJJoin).Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
inacbgRJNames := make([]string, 0, len(inacbgRJJoin))
|
||||
for _, row := range inacbgRJJoin {
|
||||
inacbgRJNames = append(inacbgRJNames, row.Kode)
|
||||
}
|
||||
|
||||
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
|
||||
var dpjpRow struct {
|
||||
ID_DPJP int `gorm:"column:ID_DPJP"`
|
||||
}
|
||||
var idDPJP int
|
||||
if err := database.DB.
|
||||
Table("\"billing_dpjp\"").
|
||||
Select("\"ID_DPJP\"").
|
||||
Where("\"ID_Billing\" = ?", billing.ID_Billing).
|
||||
First(&dpjpRow).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil, nil, nil, nil, nil, nil, 0, err
|
||||
}
|
||||
// Jika tidak ada DPJP, idDPJP = 0 (normal, boleh tidak ada)
|
||||
idDPJP = 0
|
||||
} else {
|
||||
idDPJP = dpjpRow.ID_DPJP
|
||||
}
|
||||
|
||||
return &billing, tindakanNames, icd9Names, icd10Names, dokterNames, inacbgRINames, inacbgRJNames, idDPJP, nil
|
||||
}
|
||||
|
||||
// GetDokterByNama - Cari dokter berdasarkan nama mereka
|
||||
func GetDokterByNama(nama string) (*models.Dokter, error) {
|
||||
var dokter models.Dokter
|
||||
|
||||
if err := database.DB.Where("\"Nama_Dokter\" = ?", nama).First(&dokter).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dokter, nil
|
||||
}
|
||||
|
||||
func DataFromFE(input models.BillingRequest) (
|
||||
*models.BillingPasien,
|
||||
*models.Pasien,
|
||||
[]models.Billing_Tindakan,
|
||||
[]models.Billing_ICD9,
|
||||
[]models.Billing_ICD10,
|
||||
error,
|
||||
) {
|
||||
|
||||
tx := database.DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, nil, nil, nil, nil, tx.Error
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// ===========================
|
||||
// 1. CARI ATAU BUAT PASIEN
|
||||
// ===========================
|
||||
var pasien models.Pasien
|
||||
result := tx.Where("\"Nama_Pasien\" = ?", input.Nama_Pasien).First(&pasien)
|
||||
|
||||
// Jika pasien sudah ada, update data jika ada perubahan (usia, ruangan, kelas, jenis_kelamin)
|
||||
if result.Error == nil {
|
||||
updated := false
|
||||
if pasien.Usia != input.Usia {
|
||||
pasien.Usia = input.Usia
|
||||
updated = true
|
||||
}
|
||||
if pasien.Ruangan != input.Ruangan {
|
||||
pasien.Ruangan = input.Ruangan
|
||||
updated = true
|
||||
}
|
||||
if pasien.Kelas != input.Kelas {
|
||||
pasien.Kelas = input.Kelas
|
||||
updated = true
|
||||
}
|
||||
if pasien.Jenis_Kelamin != input.Jenis_Kelamin {
|
||||
pasien.Jenis_Kelamin = input.Jenis_Kelamin
|
||||
updated = true
|
||||
}
|
||||
if updated {
|
||||
if err := tx.Save(&pasien).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal update data pasien: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
|
||||
pasien = models.Pasien{
|
||||
Nama_Pasien: input.Nama_Pasien,
|
||||
Jenis_Kelamin: input.Jenis_Kelamin,
|
||||
Usia: input.Usia,
|
||||
Ruangan: input.Ruangan,
|
||||
Kelas: input.Kelas,
|
||||
}
|
||||
|
||||
if err := tx.Create(&pasien).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal membuat pasien baru: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal mencari pasien: %s", result.Error.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if pasien.ID_Pasien == 0 {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("ID_Pasien tidak valid")
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 2. CARI SEMUA DOKTER
|
||||
// ===========================
|
||||
var dokterList []models.Dokter
|
||||
for _, namaDokter := range input.Nama_Dokter {
|
||||
var dokter models.Dokter
|
||||
if err := tx.Where("\"Nama_Dokter\" = ?", namaDokter).First(&dokter).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("dokter '%s' tidak ditemukan", namaDokter)
|
||||
}
|
||||
dokterList = append(dokterList, dokter)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Parse Tanggal_Keluar (frontend sends string). Accept multiple formats.
|
||||
var keluarPtr *time.Time
|
||||
if input.Tanggal_Keluar != "" && input.Tanggal_Keluar != "null" {
|
||||
s := input.Tanggal_Keluar
|
||||
// Try several common layouts
|
||||
var parsed time.Time
|
||||
var err error
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
parsed, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
t := parsed
|
||||
keluarPtr = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
if keluarPtr == nil {
|
||||
// If parsing failed, return error
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil, fmt.Errorf("invalid tanggal_keluar format: %s", input.Tanggal_Keluar)
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 3. CARI / BUAT BILLING
|
||||
// ===========================
|
||||
// Catatan:
|
||||
// - Kita anggap "billing aktif" = billing yang belum ditutup (Tanggal_Keluar IS NULL) untuk pasien ini.
|
||||
// - Jika ada billing aktif, update; jika tidak, buat billing baru.
|
||||
var billing models.BillingPasien
|
||||
billingResult := tx.
|
||||
Where("\"ID_Pasien\" = ? AND \"Tanggal_Keluar\" IS NULL", pasien.ID_Pasien).
|
||||
Order("\"ID_Billing\" DESC").
|
||||
First(&billing)
|
||||
|
||||
if billingResult.Error != nil {
|
||||
if errors.Is(billingResult.Error, gorm.ErrRecordNotFound) {
|
||||
// Belum ada billing aktif → buat billing baru
|
||||
billing = models.BillingPasien{
|
||||
ID_Pasien: pasien.ID_Pasien,
|
||||
Cara_Bayar: input.Cara_Bayar,
|
||||
Tanggal_masuk: &now,
|
||||
Tanggal_keluar: keluarPtr,
|
||||
Total_Tarif_RS: input.Total_Tarif_RS,
|
||||
Total_Klaim: input.Total_Klaim_BPJS, // ← Changed: Use input value instead of hardcoded 0
|
||||
}
|
||||
|
||||
// jika frontend mengirim billing_sign, gunakan itu, kalau tidak gunakan default ""
|
||||
if input.Billing_sign != "" {
|
||||
billing.Billing_sign = input.Billing_sign
|
||||
} else {
|
||||
billing.Billing_sign = ""
|
||||
}
|
||||
|
||||
if err := tx.Create(&billing).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal membuat billing: %s", err.Error())
|
||||
}
|
||||
} else {
|
||||
// Error lain saat cari billing
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal mencari billing pasien: %s", billingResult.Error.Error())
|
||||
}
|
||||
} else {
|
||||
// Sudah ada billing aktif → update data billing lama, tambahkan tindakan / ICD baru
|
||||
billing.Cara_Bayar = input.Cara_Bayar
|
||||
if keluarPtr != nil {
|
||||
billing.Tanggal_keluar = keluarPtr
|
||||
}
|
||||
// Tambahkan total tarif dari request baru
|
||||
billing.Total_Tarif_RS += input.Total_Tarif_RS
|
||||
// Update Total_Tarif_BPJS if:
|
||||
// 1. Not yet set (== 0), OR
|
||||
// 2. Input value is higher (more accurate baseline from FE)
|
||||
// This ensures we always have the correct baseline, not accumulated value from INACBG
|
||||
if input.Total_Klaim_BPJS > 0 && (billing.Total_Klaim == 0 || input.Total_Klaim_BPJS > billing.Total_Klaim) {
|
||||
billing.Total_Klaim = input.Total_Klaim_BPJS
|
||||
log.Printf("[Billing] Updated Total_Tarif_BPJS to %.2f\n", input.Total_Klaim_BPJS)
|
||||
}
|
||||
|
||||
// Log input billing_sign untuk debug
|
||||
log.Printf("[Billing Update] Received input.Billing_sign: '%s' (empty=%v)\n", input.Billing_sign, input.Billing_sign == "")
|
||||
|
||||
// Jika frontend mengirim Billing_sign, gunakan; jika tidak, hitung di backend
|
||||
if input.Billing_sign != "" {
|
||||
billing.Billing_sign = input.Billing_sign
|
||||
log.Printf("[Billing Update] Updated Billing_sign to: %s\n", input.Billing_sign)
|
||||
}
|
||||
|
||||
if err := tx.Save(&billing).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal update billing pasien: %s", err.Error())
|
||||
}
|
||||
|
||||
// Jika frontend mengirim Billing_sign pada update, kirim notifikasi email ke dokter secara async
|
||||
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 4. SIMPAN DOKTER KE BILLING_DOKTER DENGAN TANGGAL
|
||||
// ===========================
|
||||
// Tidak menghapus dokter lama, hanya menambahkan dokter baru dengan tanggal hari ini
|
||||
// Ini memungkinkan tracking dokter yang berbeda setiap hari
|
||||
tanggalHariIni := time.Now()
|
||||
|
||||
// Insert semua dokter baru ke billing_dokter dengan tanggal hari ini
|
||||
// Cek dulu apakah dokter dengan tanggal yang sama sudah ada (untuk menghindari duplikasi)
|
||||
var billingDokterList []models.Billing_Dokter
|
||||
for _, dokter := range dokterList {
|
||||
// Cek apakah dokter ini sudah ada di billing dengan tanggal yang sama
|
||||
var existing models.Billing_Dokter
|
||||
result := tx.Where("\"ID_Billing\" = ? AND \"ID_Dokter\" = ? AND DATE(tanggal) = DATE(?)",
|
||||
billing.ID_Billing, dokter.ID_Dokter, tanggalHariIni).First(&existing)
|
||||
|
||||
// Jika belum ada, tambahkan
|
||||
if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
billingDokter := models.Billing_Dokter{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_Dokter: dokter.ID_Dokter,
|
||||
Tanggal: &tanggalHariIni,
|
||||
}
|
||||
billingDokterList = append(billingDokterList, billingDokter)
|
||||
}
|
||||
// Jika sudah ada, skip (tidak perlu insert lagi)
|
||||
}
|
||||
|
||||
if len(billingDokterList) > 0 {
|
||||
if err := tx.Create(&billingDokterList).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing dokter: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// 4.5 SIMPAN DPJP KE BILLING_DPJP
|
||||
// ===========================
|
||||
// Insert DPJP (Doctor In Charge) ke tabel billing_dpjp jika ID_DPJP disediakan
|
||||
if input.ID_DPJP > 0 {
|
||||
billingDPJP := models.Billing_DPJP{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_DPJP: input.ID_DPJP,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billingDPJP).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing DPJP: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Printf("[Billing DPJP] Inserted billing %d with DPJP %d\n", billing.ID_Billing, input.ID_DPJP)
|
||||
}
|
||||
|
||||
var billingTindakanList []models.Billing_Tindakan
|
||||
var billingICD9List []models.Billing_ICD9
|
||||
var billingICD10List []models.Billing_ICD10
|
||||
|
||||
for _, tindakan := range input.Tindakan_RS {
|
||||
var tarif models.TarifRS
|
||||
|
||||
if err := tx.Where("\"Tindakan_RS\" = ?", tindakan).First(&tarif).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("tindakan '%s' tidak ditemukan", tindakan)
|
||||
}
|
||||
|
||||
billTindakan := models.Billing_Tindakan{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_Tarif_RS: tarif.KodeRS,
|
||||
Tanggal_Tindakan: &tanggalHariIni,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billTindakan).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing tindakan: %s", err.Error())
|
||||
}
|
||||
|
||||
billingTindakanList = append(billingTindakanList, billTindakan)
|
||||
}
|
||||
|
||||
for _, icd := range input.ICD9 {
|
||||
var icd9 models.ICD9
|
||||
|
||||
if err := tx.Where("\"Prosedur\" = ?", icd).First(&icd9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("ICD9 '%s' tidak ditemukan", icd)
|
||||
}
|
||||
|
||||
billICD9 := models.Billing_ICD9{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_ICD9: icd9.Kode_ICD9,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billICD9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing ICD9: %s", err.Error())
|
||||
}
|
||||
|
||||
billingICD9List = append(billingICD9List, billICD9)
|
||||
}
|
||||
|
||||
for _, icd := range input.ICD10 {
|
||||
var icd10 models.ICD10
|
||||
|
||||
if err := tx.Where("\"Diagnosa\" = ?", icd).First(&icd10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("ICD10 '%s' tidak ditemukan", icd)
|
||||
}
|
||||
|
||||
billICD10 := models.Billing_ICD10{
|
||||
ID_Billing: billing.ID_Billing,
|
||||
ID_ICD10: icd10.Kode_ICD10,
|
||||
}
|
||||
|
||||
if err := tx.Create(&billICD10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, nil, nil, nil, nil,
|
||||
fmt.Errorf("gagal insert billing ICD10: %s", err.Error())
|
||||
}
|
||||
|
||||
billingICD10List = append(billingICD10List, billICD10)
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return nil, nil, nil, nil, nil, err
|
||||
}
|
||||
if input.Billing_sign != "" && strings.TrimSpace(input.Billing_sign) != "" {
|
||||
go func(id int) {
|
||||
if err := SendEmailBillingSignToDokter(id); err != nil {
|
||||
fmt.Printf("Warning: Gagal mengirim email ke dokter untuk billing ID %d: %v\n", id, err)
|
||||
}
|
||||
}(billing.ID_Billing)
|
||||
}
|
||||
|
||||
return &billing, &pasien, billingTindakanList, billingICD9List, billingICD10List, nil
|
||||
}
|
||||
|
||||
// GetLastBillingByNama - Ambil billing terakhir pasien (buat dapetin baseline total_klaim pas billing baru dibuat)
|
||||
func GetLastBillingByNama(namaPasien string) (*models.BillingPasien, error) {
|
||||
// Cari pasien dulu
|
||||
var pasien models.Pasien
|
||||
if err := database.DB.Where("\"Nama_Pasien\" = ?", namaPasien).First(&pasien).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Cari billing terakhir pasien ini (paling baru berdasarkan ID_Billing)
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.
|
||||
Where("\"ID_Pasien\" = ?", pasien.ID_Pasien).
|
||||
Order("\"ID_Billing\" DESC").
|
||||
First(&billing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &billing, nil
|
||||
}
|
||||
|
||||
// UpdateBillingIdentitas - update data identitas pasien dalam billing
|
||||
func UpdateBillingIdentitas(billingId int, namaPasien string, usia int, jeniKelamin string, ruangan string, kelas string, tindakan []string, icd9 []string, icd10 []string) error {
|
||||
// Get billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.Where("\"ID_Billing\" = ?", billingId).First(&billing).Error; err != nil {
|
||||
return errors.New("billing tidak ditemukan")
|
||||
}
|
||||
|
||||
// Start transaction
|
||||
tx := database.DB.Begin()
|
||||
|
||||
// Update pasien data
|
||||
pasien := models.Pasien{}
|
||||
if err := tx.Model(&pasien).
|
||||
Where("\"ID_Pasien\" = ?", billing.ID_Pasien).
|
||||
Updates(map[string]interface{}{
|
||||
"\"Nama_Pasien\"": namaPasien,
|
||||
"\"Usia\"": usia,
|
||||
"\"Jenis_Kelamin\"": jeniKelamin,
|
||||
"\"Ruangan\"": ruangan,
|
||||
"\"Kelas\"": kelas,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal update data pasien: " + err.Error())
|
||||
}
|
||||
|
||||
// Delete existing tindakan
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_Tindakan{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete tindakan: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new tindakan
|
||||
for _, t := range tindakan {
|
||||
if t != "" {
|
||||
newTindakan := models.Billing_Tindakan{
|
||||
ID_Billing: billingId,
|
||||
ID_Tarif_RS: t,
|
||||
}
|
||||
if err := tx.Create(&newTindakan).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert tindakan: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD9
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD9{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD9: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD9
|
||||
for _, i := range icd9 {
|
||||
if i != "" {
|
||||
newICD9 := models.Billing_ICD9{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD9: i,
|
||||
}
|
||||
if err := tx.Create(&newICD9).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD9: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete existing ICD10
|
||||
if err := tx.Where("\"ID_Billing\" = ?", billingId).Delete(&models.Billing_ICD10{}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal delete ICD10: " + err.Error())
|
||||
}
|
||||
|
||||
// Insert new ICD10
|
||||
for _, i := range icd10 {
|
||||
if i != "" {
|
||||
newICD10 := models.Billing_ICD10{
|
||||
ID_Billing: billingId,
|
||||
ID_ICD10: i,
|
||||
}
|
||||
if err := tx.Create(&newICD10).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return errors.New("gagal insert ICD10: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return errors.New("gagal commit transaction: " + err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
)
|
||||
|
||||
// CloseBilling - Nutup billing dengan set Tanggal_Keluar (selesai dah pasiennya)
|
||||
func CloseBilling(closeReq models.Close_billing) error {
|
||||
// Cari billing berdasarkan ID_Billing
|
||||
var billing models.BillingPasien
|
||||
if err := database.DB.Where("\"ID_Billing\" = ?", closeReq.ID_Billing).First(&billing).Error; err != nil {
|
||||
return fmt.Errorf("billing dengan ID %d tidak ditemukan: %w", closeReq.ID_Billing, err)
|
||||
}
|
||||
|
||||
// Parse Tanggal_Keluar dari string ke time.Time
|
||||
// Menggunakan multiple layouts seperti di billing_pasien.go
|
||||
var keluarTime *time.Time
|
||||
if closeReq.Tanggal_Keluar != "" {
|
||||
s := closeReq.Tanggal_Keluar
|
||||
var parsed time.Time
|
||||
var err error
|
||||
layouts := []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"}
|
||||
for _, layout := range layouts {
|
||||
parsed, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
t := parsed
|
||||
keluarTime = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
if keluarTime == nil {
|
||||
return fmt.Errorf("format tanggal_keluar tidak valid: %s", closeReq.Tanggal_Keluar)
|
||||
}
|
||||
} else {
|
||||
return errors.New("tanggal_keluar tidak boleh kosong")
|
||||
}
|
||||
|
||||
// Update Tanggal_keluar pada billing
|
||||
billing.Tanggal_keluar = keluarTime
|
||||
|
||||
// Simpan perubahan
|
||||
if err := database.DB.Save(&billing).Error; err != nil {
|
||||
return fmt.Errorf("gagal update billing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func GetRiwayatPasienAll(db *gorm.DB) ([]models.Riwayat_Pasien_all, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ngambil semua billing yang udah ditutup (Tanggal_Keluar udah ada)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NOT NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulkan semua ID_Billing dan ID_Pasien
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ?", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
}
|
||||
|
||||
// Ambil tindakan hanya untuk billing terkait
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ambil tanggal tindakan dari tabel billing_tindakan
|
||||
tindakanDateMap := make(map[int]*time.Time)
|
||||
var tindakanDateRows []struct {
|
||||
ID_Billing int
|
||||
Tanggal_Tindakan *time.Time
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"tanggal_tindakan\"").
|
||||
Scan(&tindakanDateRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanDateRows {
|
||||
if t.Tanggal_Tindakan != nil {
|
||||
tindakanDateMap[t.ID_Billing] = t.Tanggal_Tindakan
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil ICD9
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD10
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil INACBG - yang RI dikasih prioritas duluan
|
||||
inacbgMap := make(map[int]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgMap[row.ID_Billing] = row.Kode
|
||||
}
|
||||
|
||||
// Kalo gada RI, ambil dari RJ aja
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
if _, exists := inacbgMap[row.ID_Billing]; !exists {
|
||||
inacbgMap[row.ID_Billing] = row.Kode
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
|
||||
dpjpMap := make(map[int]int)
|
||||
var dpjpRows []struct {
|
||||
ID_Billing int
|
||||
ID_DPJP int
|
||||
}
|
||||
if err := db.Table("\"billing_dpjp\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_DPJP\"").
|
||||
Scan(&dpjpRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dpjpRows {
|
||||
dpjpMap[row.ID_Billing] = row.ID_DPJP
|
||||
}
|
||||
|
||||
// nama dokter susai dpjp ya gais
|
||||
dpjpNameMap := make(map[int]string)
|
||||
var dpjpNameRows []struct {
|
||||
ID_Dokter int
|
||||
Nama_Dokter string
|
||||
}
|
||||
if err := db.Table("\"dokter\"").
|
||||
Select("\"ID_Dokter\", \"Nama_Dokter\"").
|
||||
Scan(&dpjpNameRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dpjpNameRows {
|
||||
dpjpNameMap[row.ID_Dokter] = row.Nama_Dokter
|
||||
}
|
||||
|
||||
// Ambil nama ruangan buat di-mapping dari ID jadi Nama
|
||||
ruanganNameMap := make(map[string]string)
|
||||
var ruanganRows []struct {
|
||||
ID_Ruangan string
|
||||
Nama_Ruangan string
|
||||
}
|
||||
if err := db.Table("\"ruangan\"").
|
||||
Select("\"ID_Ruangan\", \"Nama_Ruangan\"").
|
||||
Scan(&ruanganRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range ruanganRows {
|
||||
ruanganNameMap[row.ID_Ruangan] = row.Nama_Ruangan
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang bagus
|
||||
var result []models.Riwayat_Pasien_all
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
item := models.Riwayat_Pasien_all{
|
||||
ID_Billing: b.ID_Billing,
|
||||
ID_Pasien: pasien.ID_Pasien,
|
||||
Nama_Pasien: pasien.Nama_Pasien,
|
||||
Jenis_Kelamin: pasien.Jenis_Kelamin,
|
||||
Usia: pasien.Usia,
|
||||
Ruangan: pasien.Ruangan,
|
||||
Nama_Ruangan: ruanganNameMap[pasien.Ruangan],
|
||||
Kelas: pasien.Kelas,
|
||||
ID_DPJP: dpjpMap[b.ID_Billing],
|
||||
Nama_DPJP: dpjpNameMap[dpjpMap[b.ID_Billing]],
|
||||
Tanggal_Keluar: b.Tanggal_keluar.Format("2006-01-02"),
|
||||
Tanggal_Masuk: b.Tanggal_masuk.Format("2006-01-02"), //b.Tanggal_masuk,
|
||||
Tanggal_Tindakan: tindakanDateMap[b.ID_Billing],
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
Kode_INACBG: inacbgMap[b.ID_Billing],
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetAllRiwayatpasien(db *gorm.DB) ([]models.Request_Admin_Inacbg, error) {
|
||||
var billings []models.BillingPasien
|
||||
|
||||
// Ngambil semua billing yang udah ditutup (Tanggal_Keluar ada isinya)
|
||||
if err := db.Where("\"Tanggal_Keluar\" IS NOT NULL").Find(&billings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Kumpulkan semua ID_Billing dan ID_Pasien
|
||||
var billingIDs []int
|
||||
var pasienIDs []int
|
||||
|
||||
for _, b := range billings {
|
||||
billingIDs = append(billingIDs, b.ID_Billing)
|
||||
pasienIDs = append(pasienIDs, b.ID_Pasien)
|
||||
}
|
||||
|
||||
// Ambil pasien yang ada di billing aja
|
||||
pasienMap := make(map[int]models.Pasien)
|
||||
var pasienList []models.Pasien
|
||||
|
||||
if err := db.Where("\"ID_Pasien\" IN ? ", pasienIDs).Find(&pasienList).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range pasienList {
|
||||
pasienMap[p.ID_Pasien] = p
|
||||
}
|
||||
|
||||
// Ambil tindakan hanya untuk billing terkait
|
||||
tindakanMap := make(map[int][]string)
|
||||
var tindakanRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_tindakan\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_Tarif_RS\" as \"Kode\"").
|
||||
Scan(&tindakanRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, t := range tindakanRows {
|
||||
tindakanMap[t.ID_Billing] = append(tindakanMap[t.ID_Billing], t.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD9
|
||||
icd9Map := make(map[int][]string)
|
||||
var icd9Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd9\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD9\" as \"Kode\"").
|
||||
Scan(&icd9Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd9Rows {
|
||||
icd9Map[row.ID_Billing] = append(icd9Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil ICD10
|
||||
icd10Map := make(map[int][]string)
|
||||
var icd10Rows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
|
||||
if err := db.Table("\"billing_icd10\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_ICD10\" as \"Kode\"").
|
||||
Scan(&icd10Rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range icd10Rows {
|
||||
icd10Map[row.ID_Billing] = append(icd10Map[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RI
|
||||
inacbgRIMap := make(map[int][]string)
|
||||
var inacbgRIRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_ri\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RI\" as \"Kode\"").
|
||||
Scan(&inacbgRIRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRIRows {
|
||||
inacbgRIMap[row.ID_Billing] = append(inacbgRIMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ngambil INACBG RJ
|
||||
inacbgRJMap := make(map[int][]string)
|
||||
var inacbgRJRows []struct {
|
||||
ID_Billing int
|
||||
Kode string
|
||||
}
|
||||
if err := db.Table("\"billing_inacbg_rj\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_INACBG_RJ\" as \"Kode\"").
|
||||
Scan(&inacbgRJRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range inacbgRJRows {
|
||||
inacbgRJMap[row.ID_Billing] = append(inacbgRJMap[row.ID_Billing], row.Kode)
|
||||
}
|
||||
|
||||
// Ambil dokter dari tabel billing_dokter, diurutkan berdasarkan tanggal
|
||||
dokterMap := make(map[int][]string)
|
||||
var dokterRows []struct {
|
||||
ID_Billing int
|
||||
Nama string
|
||||
}
|
||||
if err := db.Table("\"billing_dokter\"").
|
||||
Select("\"ID_Billing\", \"Nama_Dokter\" as \"Nama\"").
|
||||
Joins("JOIN \"dokter\" ON \"billing_dokter\".\"ID_Dokter\" = \"dokter\".\"ID_Dokter\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Order("tanggal ASC").
|
||||
Scan(&dokterRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dokterRows {
|
||||
dokterMap[row.ID_Billing] = append(dokterMap[row.ID_Billing], row.Nama)
|
||||
}
|
||||
|
||||
// Ambil DPJP (Doctor In Charge) dari billing_dpjp
|
||||
dpjpMap := make(map[int]int)
|
||||
var dpjpRows []struct {
|
||||
ID_Billing int
|
||||
ID_DPJP int
|
||||
}
|
||||
if err := db.Table("\"billing_dpjp\"").
|
||||
Where("\"ID_Billing\" IN ?", billingIDs).
|
||||
Select("\"ID_Billing\", \"ID_DPJP\"").
|
||||
Scan(&dpjpRows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, row := range dpjpRows {
|
||||
dpjpMap[row.ID_Billing] = row.ID_DPJP
|
||||
}
|
||||
|
||||
// Rapihin semua data jadi response yang bagus
|
||||
var result []models.Request_Admin_Inacbg
|
||||
|
||||
for _, b := range billings {
|
||||
pasien := pasienMap[b.ID_Pasien]
|
||||
|
||||
item := models.Request_Admin_Inacbg{
|
||||
ID_Billing: b.ID_Billing,
|
||||
Nama_pasien: pasien.Nama_Pasien,
|
||||
ID_Pasien: pasien.ID_Pasien,
|
||||
Kelas: pasien.Kelas,
|
||||
Ruangan: pasien.Ruangan,
|
||||
Total_Tarif_RS: b.Total_Tarif_RS,
|
||||
Total_Klaim: b.Total_Klaim,
|
||||
ID_DPJP: dpjpMap[b.ID_Billing],
|
||||
Tindakan_RS: tindakanMap[b.ID_Billing],
|
||||
ICD9: icd9Map[b.ID_Billing],
|
||||
ICD10: icd10Map[b.ID_Billing],
|
||||
INACBG_RI: inacbgRIMap[b.ID_Billing],
|
||||
INACBG_RJ: inacbgRJMap[b.ID_Billing],
|
||||
Billing_sign: b.Billing_sign,
|
||||
Nama_Dokter: dokterMap[b.ID_Billing],
|
||||
}
|
||||
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"backendcareit/database"
|
||||
"backendcareit/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Ambil tarif BPJS untuk rawat inap yaa
|
||||
func GetTarifBPJSRawatInap() ([]models.TarifBPJSRawatInap, error) {
|
||||
var data []models.TarifBPJSRawatInap
|
||||
if err := database.DB.Model(&models.TarifBPJSRawatInap{}).Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetTarifBPJSRawatInapByKode(kode string) (*models.TarifBPJSRawatInap, error) {
|
||||
var data models.TarifBPJSRawatInap
|
||||
if err := database.DB.Model(&models.TarifBPJSRawatInap{}).Where("ID_INACBG_RI = ?", kode).First(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// Ngambil tarif untuk pasien rawat jalan
|
||||
func GetTarifBPJSRawatJalan() ([]models.TarifBPJSRawatJalan, error) {
|
||||
var data []models.TarifBPJSRawatJalan
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetTarifBPJSRawatJalanByKode(kode string) (*models.TarifBPJSRawatJalan, error) {
|
||||
var data models.TarifBPJSRawatJalan
|
||||
if err := database.DB.Where("ID_INACBG_RJ = ?", kode).First(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// Ambil tarif rumah sakit aja bro
|
||||
func GetTarifRS() ([]models.TarifRS, error) {
|
||||
var data []models.TarifRS
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func GetTarifRSByKode(kode string) (*models.TarifRS, error) {
|
||||
var data models.TarifRS
|
||||
if err := database.DB.Where("ID_Tarif_RS = ?", kode).First(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func GetTarifRSByKategori(kategori string) ([]models.TarifRS, error) {
|
||||
var data []models.TarifRS
|
||||
if err := database.DB.Where("Kategori_RS = ?", kategori).Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return err == gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// Ambil data ICD9 - kode diagnosa versi lama
|
||||
func GetICD9() ([]models.ICD9, error) {
|
||||
var data []models.ICD9
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Ambil data ICD10 - kode diagnosa versi baru
|
||||
func GetICD10() ([]models.ICD10, error) {
|
||||
var data []models.ICD10
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Ambil daftar semua ruangan di RS
|
||||
func GetRuangan() ([]models.Ruangan, error) {
|
||||
var data []models.Ruangan
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetRuanganWithPasien - Get ruangan yang memiliki minimal 1 pasien
|
||||
func GetRuanganWithPasien(db *gorm.DB) ([]models.Ruangan, error) {
|
||||
var data []models.Ruangan
|
||||
// JOIN dengan pasien table dan filter yang punya pasien
|
||||
if err := db.
|
||||
Distinct("ruangan.*").
|
||||
Table("ruangan").
|
||||
Joins("INNER JOIN pasien ON ruangan.\"Nama_Ruangan\" = pasien.\"Ruangan\"").
|
||||
Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Ambil list semua dokter yang ada
|
||||
func GetDokter() ([]models.Dokter, error) {
|
||||
var data []models.Dokter
|
||||
if err := database.DB.Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,789 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Backend Careit - Test Interface</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
opacity: 0.9;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: #f5f5f5;
|
||||
padding: 15px 20px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.endpoint-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.endpoint-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.endpoint-item label {
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.endpoint-item input {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-list {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.btn-list:hover {
|
||||
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.response-area {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.response-header h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-loading {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.view-toggle button {
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.view-toggle button.active {
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9), 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#response,
|
||||
#tableResponse {
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#tableResponse {
|
||||
display: none;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: white;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #eee;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #f1f5ff;
|
||||
}
|
||||
|
||||
.health-check {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: #e8f5e9;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.health-status {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.health-ok {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.health-error {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏥 Backend Careit</h1>
|
||||
<p>Test Interface untuk API Backend</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<!-- Health Check -->
|
||||
<div class="health-check">
|
||||
<h2>Health Check</h2>
|
||||
<button onclick="checkHealth()" style="margin-top: 10px;">Cek Status Server</button>
|
||||
<div id="healthStatus" class="health-status"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif BPJS Rawat Inap -->
|
||||
<div class="section">
|
||||
<div class="section-header">📋 Tarif BPJS Rawat Inap</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifBPJSRawatInap()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode INA CBG:</label>
|
||||
<input type="text" id="kodeRawatInap" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifBPJSRawatInapByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif BPJS Rawat Jalan -->
|
||||
<div class="section">
|
||||
<div class="section-header">🚶 Tarif BPJS Rawat Jalan</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifBPJSRawatJalan()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode INA CBG:</label>
|
||||
<input type="text" id="kodeRawatJalan" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifBPJSRawatJalanByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tarif RS -->
|
||||
<div class="section">
|
||||
<div class="section-header">🏨 Tarif RS</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getTarifRS()">List Semua Tarif</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kode RS:</label>
|
||||
<input type="text" id="kodeRS" placeholder="Masukkan kode...">
|
||||
<button onclick="getTarifRSByKode()">Cari by Kode</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Kategori:</label>
|
||||
<input type="text" id="kategoriRS" placeholder="Masukkan kategori...">
|
||||
<button onclick="getTarifRSByKategori()">Cari by Kategori</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD9 -->
|
||||
<div class="section">
|
||||
<div class="section-header">🧬 Kode Tindakan ICD9</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getICD9()">List Semua Kode ICD9</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterICD9" placeholder="Ketik untuk filter di tabel..." oninput="filterICD9ClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/icd9</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON (misal: kode, deskripsi, dll).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ICD10 -->
|
||||
<div class="section">
|
||||
<div class="section-header">🩺 Kode Diagnosis ICD10</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getICD10()">List Semua Kode ICD10</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterICD10" placeholder="Ketik untuk filter di tabel..." oninput="filterICD10ClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/icd10</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dokter -->
|
||||
<div class="section">
|
||||
<div class="section-header">👨⚕️ Data Dokter</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getDokter()">List Semua Dokter</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterDokter" placeholder="Ketik untuk filter di tabel..." oninput="filterDokterClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/dokter</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ruangan -->
|
||||
<div class="section">
|
||||
<div class="section-header">🏥 Data Ruangan</div>
|
||||
<div class="section-content">
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-item">
|
||||
<button class="btn-list" onclick="getRuangan()">List Semua Ruangan</button>
|
||||
</div>
|
||||
<div class="endpoint-item">
|
||||
<label>Filter (client side, berdasarkan teks):</label>
|
||||
<input type="text" id="filterRuangan" placeholder="Ketik untuk filter di tabel..." oninput="filterRuanganClientSide()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 10px;">
|
||||
Data diambil dari endpoint <code>/ruangan</code>. Tabel di bawah akan otomatis menyesuaikan kolom dari field JSON.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Response Area -->
|
||||
<div class="response-area">
|
||||
<div class="response-header">
|
||||
<h3>Response:</h3>
|
||||
<span id="statusBadge" class="status-badge" style="display: none;"></span>
|
||||
</div>
|
||||
<div class="view-toggle">
|
||||
<button id="btnJsonView" class="active" onclick="setViewMode('json')">JSON</button>
|
||||
<button id="btnTableView" onclick="setViewMode('table')">Table (Auto)</button>
|
||||
</div>
|
||||
<div id="response"></div>
|
||||
<div id="tableResponse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = 'http://localhost:8081';
|
||||
|
||||
function showResponse(data, status = 'ok') {
|
||||
const responseDiv = document.getElementById('response');
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const statusBadge = document.getElementById('statusBadge');
|
||||
|
||||
// Tampilkan JSON mentah
|
||||
responseDiv.textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
// Coba render tabel
|
||||
renderTable(data);
|
||||
|
||||
statusBadge.style.display = 'inline-block';
|
||||
statusBadge.className = 'status-badge status-' + status;
|
||||
statusBadge.textContent = status === 'ok' ? '✓ Success' : status === 'loading' ? '⏳ Loading...' : '✗ Error';
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
const responseDiv = document.getElementById('response');
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const btnJson = document.getElementById('btnJsonView');
|
||||
const btnTable = document.getElementById('btnTableView');
|
||||
|
||||
if (mode === 'json') {
|
||||
responseDiv.style.display = 'block';
|
||||
tableDiv.style.display = 'none';
|
||||
btnJson.classList.add('active');
|
||||
btnTable.classList.remove('active');
|
||||
} else {
|
||||
responseDiv.style.display = 'none';
|
||||
tableDiv.style.display = 'block';
|
||||
btnJson.classList.remove('active');
|
||||
btnTable.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
function formatRupiah(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
// Convert to number
|
||||
const num = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(num)) {
|
||||
return value;
|
||||
}
|
||||
// Format dengan pemisah ribuan
|
||||
return 'Rp ' + num.toLocaleString('id-ID', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function isTarifColumn(key) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return lowerKey.includes('tarif') ||
|
||||
lowerKey.includes('harga') ||
|
||||
lowerKey.includes('kelas') ||
|
||||
lowerKey.includes('tarif_inacbg');
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
|
||||
// Reset isi
|
||||
tableDiv.innerHTML = '';
|
||||
|
||||
// Hanya render tabel untuk array of objects
|
||||
if (!Array.isArray(data) || data.length === 0 || typeof data[0] !== 'object') {
|
||||
setViewMode('json');
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = Array.from(
|
||||
data.reduce((set, item) => {
|
||||
Object.keys(item || {}).forEach(k => set.add(k));
|
||||
return set;
|
||||
}, new Set())
|
||||
);
|
||||
|
||||
if (keys.length === 0) {
|
||||
setViewMode('json');
|
||||
return;
|
||||
}
|
||||
|
||||
const table = document.createElement('table');
|
||||
const thead = document.createElement('thead');
|
||||
const tbody = document.createElement('tbody');
|
||||
|
||||
const headerRow = document.createElement('tr');
|
||||
keys.forEach(key => {
|
||||
const th = document.createElement('th');
|
||||
th.textContent = key;
|
||||
headerRow.appendChild(th);
|
||||
});
|
||||
thead.appendChild(headerRow);
|
||||
|
||||
data.forEach(row => {
|
||||
const tr = document.createElement('tr');
|
||||
keys.forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
const value = row && key in row ? row[key] : '';
|
||||
|
||||
// Format tarif dengan rupiah
|
||||
if (isTarifColumn(key) && (typeof value === 'number' || (typeof value === 'string' && !isNaN(value) && value !== ''))) {
|
||||
td.textContent = formatRupiah(value);
|
||||
td.style.textAlign = 'right';
|
||||
} else {
|
||||
td.textContent = typeof value === 'object' ? JSON.stringify(value) : value;
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
table.appendChild(thead);
|
||||
table.appendChild(tbody);
|
||||
tableDiv.appendChild(table);
|
||||
|
||||
// Auto switch ke view tabel
|
||||
setViewMode('table');
|
||||
}
|
||||
|
||||
async function checkHealth() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/`);
|
||||
const data = await response.json();
|
||||
|
||||
const healthStatus = document.getElementById('healthStatus');
|
||||
if (response.ok) {
|
||||
healthStatus.textContent = '✓ Server Online';
|
||||
healthStatus.className = 'health-status health-ok';
|
||||
} else {
|
||||
healthStatus.textContent = '✗ Server Error';
|
||||
healthStatus.className = 'health-status health-error';
|
||||
}
|
||||
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
const healthStatus = document.getElementById('healthStatus');
|
||||
healthStatus.textContent = '✗ Connection Error';
|
||||
healthStatus.className = 'health-status health-error';
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatInap() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatInap`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatInapByKode() {
|
||||
const kode = document.getElementById('kodeRawatInap').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJS/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatJalan() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifBPJSRawatJalanByKode() {
|
||||
const kode = document.getElementById('kodeRawatJalan').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifBPJSRawatJalan/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRS() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRS`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRSByKode() {
|
||||
const kode = document.getElementById('kodeRS').value;
|
||||
if (!kode) {
|
||||
alert('Silakan masukkan kode!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRS/${encodeURIComponent(kode)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getTarifRSByKategori() {
|
||||
const kategori = document.getElementById('kategoriRS').value;
|
||||
if (!kategori) {
|
||||
alert('Silakan masukkan kategori!');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/tarifRSByKategori/${encodeURIComponent(kategori)}`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function getICD9() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/icd9`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterICD9ClientSide() {
|
||||
const input = document.getElementById('filterICD9');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getICD10() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/icd10`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterICD10ClientSide() {
|
||||
const input = document.getElementById('filterICD10');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getDokter() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/dokter`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterDokterClientSide() {
|
||||
const input = document.getElementById('filterDokter');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
async function getRuangan() {
|
||||
try {
|
||||
showResponse('Loading...', 'loading');
|
||||
const response = await fetch(`${API_BASE}/ruangan`);
|
||||
const data = await response.json();
|
||||
showResponse(data, response.ok ? 'ok' : 'error');
|
||||
} catch (error) {
|
||||
showResponse({ error: error.message }, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function filterRuanganClientSide() {
|
||||
const input = document.getElementById('filterRuangan');
|
||||
const filter = input.value.toLowerCase();
|
||||
const tableDiv = document.getElementById('tableResponse');
|
||||
const table = tableDiv.querySelector('table');
|
||||
if (!table) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
const text = row.textContent.toLowerCase();
|
||||
row.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto check health on load
|
||||
window.onload = function() {
|
||||
checkHealth();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user